Reprinted from Commodore World Issue #5
The BASIC ROM in the C-64 contains a fairly complete library of floating point routines. There are, however, some pitfalls when trying to access these routines directly from machine language. In this article we will look at some of the more common floating point routines, and things to watch for when calling these routines from a machine language program.
Before we get started, a quick overview is in order. There are two areas in zero page memory defined as floating point registers. These are known as floating point accumulator 1 and floating point accumulator 2. Throughout this article, these will be referred to as register 1 and register 2. Both of these registers are comprised of a one byte exponent followed by a four byte mantissa followed by a one byte sign. Most of the floating point math routines use these two registers, and return the result of the operation in register 1. Almost all of the conversion routines use register 1. There are only a couple that deal with register 2.
Change Is Inevitable
The first thing we'll look at is how to put floating point values into
the registers. Let's start with integer conversion. There are a number
of ways to convert an integer to floating point. The first method we'll
examine is the conversion of an eight bit integer. This is very easy, we
just load the A register with the number to be converted, and then call
the conversion routine. The floating point value is placed into register
1.
ByteToFloat = $BC3C lda #$07 jsr ByteToFloat rtsThere is a similar routine available to convert a sixteen bit integer to floating point. This routine uses the A and Y registers to pass the sixteen bit number. As before, the floating point value is placed into register 1.
WordToFloat = $B391 Value = $1234 ldy #? lda #? jsr WordToFloat rtsHere's where things can get tricky. Both of the previous routines treat the integers as signed numbers. If we pass an integer that we are treating as an unsigned number, and it just so happens that the high bit is set, the conversion routine will happily place a negative floating point value into register 1. This can produce unexpected results, not to mention the urge to throw the computer through the window because the program doesn't work right. The solution is to convert the integer as an unsigned number. This requires a little more work, but can save us the trouble of tracking down the source of the problem, not to mention a costly window replacement. In this example, we'll convert the unsigned value $9FFF to floating point. This method requires the unsigned sixteen bit value to first be written into the first two bytes of the mantissa for register 1. Note that the high byte is stored before the low byte. This differs from the standard 6502 little endian byte ordering. Next, the X register must be loaded with the exponent for the sixteen bit number. Setting the exponent to #$90 places the binary point to the right of the least significant bit of this integer, making it a whole number. And finally, the carry must be set so that our integer will be treated as an unsigned number.
UWordToFloat = $BC49 Mantissa1 = $62 Value = $9FFF lda #? sta Mantissa1 lda #? sta Mantissa1+1 ldx #$90 sec jsr UWordToFloat rtsTelling It Like It Is
ByteToFloat = $BC3C FloatPrint = $BDD7 CHROUT = $FFD2 lda #$64 jsr ByteToFloat jsr FloatPrint lda #$0D jsr CHROUT rtsWe probably won't always want to print the result to the screen, so there's another routine available which places the string in memory at $0100.
ByteToFloat = $BC3C FloatToString = $BDDD lda #$64 jsr ByteToFloat jsr FloatToString rtsUp until now we have been dealing with whole numbers only. Of course, this will not always be the case. As you may have guessed, there is a way to convert an ASCII string to floating point. This null terminated string can be a fractional number, and can even be in scientific notation. The StringToFloat routine uses the CHRGET routine to parse the ASCII string. We must do some preliminary work, which involves setting the CHRGOT pointer to the first character in our string, and then entering at CHRGOT to retrieve the first character. As usual, the floating point value is placed into register 1.
CHRGOT = $79 StringToFloat = $BCF3 lda #? sta CHRGOT+1 lda #? sta CHRGOT+2 jsr CHRGOT jsr StringToFloat rts String .byte "37.321",0Save It For A Rainy Day
Float1ToMem = $BBD4 ldx #? ldy #? jsr Float1ToMem rts Save1 .byte 0,0,0,0,0,0,0,0There is no routine available to transfer register 2 to memory. If we want to save register 2, we must first copy it's contents to register 1 and then transfer register 1 to memory.
Float2ToFloat1 = $BBFC Float1ToMem = $BBD4 jsr Float2ToFloat1 ldx #? ldy #? jsr Float1ToMem rts Save2 .byte 0,0,0,0,0,0,0,0When it comes time to restore the floating point registers, we can transfer these saved values back from memory. This time we have a separate routine for register 2.
MemToFloat1 = $BBA2 MemToFloat2 = $BA8C lda #? ldy #? jsr MemToFloat1 lda #? ldy #? jsr MemToFloat2 rtsThe Way We Were
RoundFloat = $BE2F FloatToInt = $BC9B jsr RoundFloat jsr FloatToInt rtsCool And Calculating
Float1Exp = $61 ByteToFloat = $BC3C Float1ToFloat2 = $BC0C FloatAdd = $B86A lda #$03 jsr ByteToFloat jsr Float1ToFloat2 lda #$02 jsr ByteToFloat lda Float1Exp jsr FloatAdd rtsFloating point subtraction is handled pretty much the same way as floating point addition. The only difference between the two is the order in which the operation occurs. Register 1 is always subtracted from register 2. In this example, we can see how to code the statement 3-2.
Float1Exp = $61 ByteToFloat = $BC3C Float1ToFloat2 = $BC0C FloatSub = $B853 lda #$03 jsr ByteToFloat jsr Float1ToFloat2 lda #$02 jsr ByteToFloat lda Float1Exp jsr FloatSub rtsWell that was fun, let's do some more. How about a simple electronics problem. Let's calculate the DC current for a 5 volt drop across a 4.7K resistor. The formula is I=E/R where E is the DC voltage, R is the resistance, and I is the current.
Float1Exp = $61 ByteToFloat = $BC3C WordToFloat = $B391 Float1ToFloat2 = $BC0C FloatDiv = $BB12 E = 5 R = 4700 lda #E jsr ByteToFloat jsr Float1ToFloat2 ldy #? lda #? jsr WordToFloat lda Float1Exp jsr FloatDivAt this point, register 1 contains the calculated current flow through the resistor, which turns out to be 1.06382979E-03 amperes. We can turn this into something a bit more readable by converting to milliamperes. This is accomplished by multiplying by 1000, after which register 1 contains the calculated current flow in the form 1.06382979 milliamperes.
FloatMult = $BA2B Multiplier = 1000 jsr Float1ToFloat2 ldy #? lda #? jsr WordToFloat lda Float1Exp jsr FloatMult rtsThat covers the four basic arithmetic operations. In this final example, we will take a look at how to perform exponentiation. As with subtraction and division, we need to make sure we use the floating point registers in the proper order. The following code will perform the operation 3^2.
Float1Exp = $61 ByteToFloat = $BC3C Float1ToFloat2 = $BC0C FloatExpon = $BF7B lda #$03 jsr ByteToFloat jsr Float1ToFloat2 lda #$02 jsr ByteToFloat lda Float1Exp jsr FloatExpon rtsThe End Of The Rainbow