Small tutorials

02 / INTEGERS + FLOATING POINT

Four bytes in.
A BASIC number out.

Getting number conversion right was one of AIS4CBM’s early headaches. The decoder had the right integer bytes. BASIC needed a floating-point value. Here is how fac.asm connects the two.

Commodore 128 · 8502 assembly · A walkthrough of the 30-bit MMSI path

01 / WHERE THE ROM HELP STOPS

Two bytes are easy. AIS needs more.

The C128 already has integer-to-floating-point helpers in its BASIC ROM. Byte-sized values and 16-bit integers have ready-made conversion paths:

C128 ROM conversion paths
InputROM pathInput contract
Signed 8-bit$8C68Value in A, −128 to 127.
Signed 16-bitJGIVAYF / $AF03High byte in A, low byte in Y.
Unsigned 16-bitJFLOATC / $AF0FHigh/low bytes at $64/$65, X = $90, carry set.

An unsigned byte can be zero-extended to two bytes; that is the route used in fac.asm. These standard helpers do not accept an arbitrary four-byte integer. BASIC can represent larger numbers, and ROM has lower-level math machinery, but there is no equivalent four-byte input interface in this conversion jump table.

AIS4CBM stores a 30-bit MMSI, 28-bit longitude and 27-bit latitude in four bytes each. The conversion entry tests the storage length and sends wider fields to its own routine:

fac.asm · choose the conversion path
lda fragment.fieldLength
cmp #3
bcc !+
jmp big

02 / KNOW THE DESTINATION

FAC is a working accumulator.

FAC means floating-point accumulator. It is BASIC’s workspace for a number. The conversion constructs these parts of FAC1 in real zero page:

The FAC1 fields used here
LocationContents
$63Biased exponent; zero marks the number zero.
$64–$67Four mantissa bytes, most significant first, with the leading 1 present for a normalized nonzero value.
$68Separate sign; bit 7 indicates a negative number.

For a positive integer, find its highest set bit at position p, counting from bit 0. Shift that bit into the top of the four-byte mantissa, then store the exponent as 129 + p. The mantissa now represents a value from 1 up to, but not including, 2; the exponent supplies its scale.

03 / FOLLOW ONE NUMBER

123456789, byte by byte.

Use 123456789 as an illustrative MMSI-sized value. Its four input bytes are 07 5B CD 15, most significant first. The field descriptor says 30 bits, four bytes, unsigned. Its highest set bit is at position 26.

123456789 starts as bytes 07 5B CD 15. Its highest bit is 26, so Y is 27. Five left shifts produce EB 79 A2 A0. The exponent is 129 plus 26, or 9B hexadecimal, and the sign is zero.
The bits keep their value; normalization changes their position and records the scale. All byte values shown are hexadecimal. Open full size ↗

The search leaves Y = 27, a one-based bit position. The shift count is 32 − 27 = 5. The exponent becomes 129 + 26 = 155 ($9B). FAC1 holds exponent 9B, mantissa EB 79 A2 A0 and sign 00.

If BASIC packs this exact value into a variable, the five bytes are 9B 6B 79 A2 A0: the positive sign replaces the top mantissa bit. This integer fits exactly; normalization discards none of its significant bits.

04 / THE CODE THAT DOES IT

Find the bit. Shift. Set the scale.

These are excerpts from the current src/Include/fac.asm, with spacing simplified and comments omitted. They share labels, metadata and ROM-call macros with the surrounding program; they are not a standalone assembler listing.

Before this search, big clears the exponent and sign, copies the input into the mantissa area and positions the bit mask at the field’s declared width. Signed fields also test their sign bit and call Fac1Complement() when negative. The unsigned MMSI path skips that step.

1. Find the highest set bit

fac.asm · scan the magnitude
facByte:
!:      bit $64
        bne calculateExponent
        lsr
        bne !+
        lda #128
        inc facByte+1
!:      dey
        bne !--
        jmp reset

A holds the bit mask. LSR moves it across a byte; when it runs out, the code starts again at bit 7 of the next byte. inc facByte+1 patches the address operand of BIT. Y tracks the remaining bit position. If no bit is set, the exponent stays zero.

2. Normalize the four-byte mantissa

fac.asm · the complete shiftLeft routine
shiftLeft:
{
        sty counter+1
        lda #32
        sec
counter:
        sbc #0
        tay
!:      clc
        ldx #3
!:      rol workingBuffer+1,x
        dex
        bmi !+
        jmp !-
!:      dey
        bne !---
        rts
}

counter+1 becomes the original Y, so the subtraction calculates 32 − Y. Each pass starts at the least significant byte and rotates towards the most significant byte. Carry passes the outgoing bit from one byte into the next. CLC before each four-byte pass inserts a zero at the low end; SEC makes the subtraction independent of the previous carry.

3. Store the biased exponent

fac.asm · preserve Y across normalization, then set the exponent
calculateExponent:
        tya
        pha
        jsr shiftLeft
        pla
        tay
        dey
        sty exponentPower+1
        lda #129
        clc
exponentPower:
        adc #0
        sta workingBuffer
        jmp reset

The original Y survives the shift routine on the stack. DEY changes its one-based position to the exponent’s zero-based power. The patched ADC then adds 129, with carry explicitly cleared. workingBuffer is defined as $63, the FAC1 exponent.

05 / THE DETAILS THAT MATTER

Make the input contract explicit.

  • Byte width and bit width are different. A 30-bit MMSI occupies four bytes. Its descriptor tells the converter where to begin the search.
  • Preserve byte order. These input bytes are most significant first. A typical 6502 address is stored in the opposite order.
  • Use real zero page. The C128 can relocate it. trans selects page 0 and restores the caller’s page afterwards; code consuming FAC1 must use the correct page too.
  • Carry is part of the calculation. The explicit SEC and CLC instructions keep prior CPU state out of the subtraction, shifts and exponent sum.

Read the code

Links point to main in the public GitLab repository. Source line numbers may move.

  • fac.asm — input metadata, ROM dispatch and the wider conversion path.
  • getDataByField — copy the field descriptor and input bytes, then jump to trans.
  • c128macros.asm — ROM bridges and the complement helper.
  • decoderHeaders.asm — MMSI and coordinate field definitions.