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:
| Input | ROM path | Input contract |
|---|---|---|
| Signed 8-bit | $8C68 | Value in A, −128 to 127. |
| Signed 16-bit | JGIVAYF / $AF03 | High byte in A, low byte in Y. |
| Unsigned 16-bit | JFLOATC / $AF0F | High/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.
References: byte conversion in the ROM listing · Mapping the Commodore 128, printed pages 231–232.
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:
lda fragment.fieldLength
cmp #3
bcc !+
jmp big
ROM dispatch in fac.asm · The ROM-call macros · AIS field widths
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:
| Location | Contents |
|---|---|
$63 | Biased exponent; zero marks the number zero. |
$64–$67 | Four mantissa bytes, most significant first, with the leading 1 present for a normalized nonzero value. |
$68 | Separate 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.
ROM unpacking restores the leading 1 and separates the sign · FAC1 memory map, printed pages 41–44
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.
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
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.
Preparation, sign handling and leading-bit search
2. Normalize the four-byte mantissa
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
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.
transselects 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
SECandCLCinstructions 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.