01 / WHERE DOES THE STRUCTURE EXIST?
The assembler prepares. The 8502 reads.
AIS4CBM uses Kick Assembler, by Mads Nielsen, to assemble its machine-language engine. Alongside assembly instructions, its scripting language provides structures, lists and hashtables. These help us construct the program on the development computer.
A structure definition does not automatically allocate a matching record in the Commodore’s memory. We choose what to emit with directives such as .byte and .word. The running C128 receives the generated code and data.
- 01 / ON THE PCDescribe the field
A named structure holds its input width, output size and type.
- 02 / DURING ASSEMBLYEmit a small table
Kick Assembler writes three descriptor bytes and resolves references to them.
- 03 / ON THE C128Read and decode
The 8502 reads those bytes and calls a shared field handler.
This distinction explains the benefit: descriptive source code can produce a compact runtime representation. The field names and hashtable machinery do their work during assembly.
Kick Assembler references: the scripting language · directives that define values or emit bytes.
02 / START WITH ONE FIELD
Three properties describe the work.
The project defines a small structure for a field’s shape:
.struct aField {bits,bytes,dataType}
| Property | Meaning | Speed example |
|---|---|---|
bits | Bits to consume from the incoming payload. | 10 |
bytes | Bytes used by the decoded field. | 2 |
dataType | The project’s numeric or text type code. | uInt |
aField(10,2,uInt) creates a structure value. Its .bits property is 10, .bytes is 2, and .dataType is the project constant uInt. AIS4CBM defines uInt=1, Int=2 and String=3. These names describe our decoded fields; they are not Kick Assembler’s own value types.
A hashtable gives each description a name. This abbreviated excerpt keeps the project’s identifiers, including the spelling allFiledsHT:
.define allFiledsHT {
.var allFiledsHT = Hashtable()
}
.eval allFiledsHT.put("mmsi",aField(30,4,uInt))
.eval allFiledsHT.put("sog",aField(10,2,uInt))
.eval allFiledsHT.put("name",aField(120,20,String))
put(key,value) stores a description; get(key) retrieves it. For example, allFiledsHT.get("sog").bits gives 10. .var defines a scripting variable and .eval evaluates a scripting expression. Neither instruction is executed by the 8502.
Actual field definitions · User-defined structures · Kick Assembler hashtables.
03 / TURN DESCRIPTIONS INTO DATA
This loop runs during assembly.
keys() gives us a list of the catalogue’s names. The following loop looks up each description and emits three bytes into AISFields:
AISFields:
.for (var i=0; i<allFiledsHT.keys().size(); i++) {
.var fieldKey = allFiledsHT.keys().get(i)
field:
.byte allFiledsHT.get(fieldKey).bits
.byte allFiledsHT.get(fieldKey).bytes
.byte allFiledsHT.get(fieldKey).dataType
}
For speed, those bytes are 0A 02 01: ten input bits, two output bytes and unsigned integer. They describe the field; they do not contain a vessel’s actual speed. The current descriptor size is three bytes.
Each iteration has its own field label. Kick Assembler lets the next loop refer to the label in iteration i as AISFields[i].field. We use that address to build a second hashtable:
.for (var i=0; i<allFiledsHT.keys().size(); i++) {
.eval aisFieldAddressByName.put(
allFiledsHT.keys().get(i), AISFields[i].field)
}
The first hashtable maps name → description. The second maps name → address of emitted descriptor. Both are assembly-time helpers. A later .word aisFieldAddressByName.get("sog") places the resolved two-byte address into the program.
AISFields, its size guard and aisFieldAddressByName · Accessing labels from for/while loops.
04 / PUT THE FIELDS IN ORDER
A catalogue defines shape. A layout defines order.
Different AIS messages reuse many of the same fields. The common header refers to type, repeat indicator and MMSI:
hmesg: .word aisFieldAddressByName.get("type")
.word aisFieldAddressByName.get("ri")
.word aisFieldAddressByName.get("mmsi")
.byte 0
The decoder follows the references in this sequence. It consumes 6 + 2 + 30 = 38 incoming bits, producing 1 + 1 + 4 = 6 decoded bytes. It then uses the decoded message type to select the payload layout. The final zero marks the end of this project’s list.
Kick Assembler List() values also describe the order used for offset and size calculations. For example, the position-report list is:
.var posmsgFieldList = List().add("navst","rot","sog","p_acc","long","lat","cog","hdg","time_stamp").lock()
add() appends items in order, get(i) retrieves an item and size() gives the count. lock() prevents later changes to this list. Helpers sum the descriptions’ output-byte widths to calculate a field’s decoded offset. The current source declares the corresponding runtime .word entries separately, so both representations must stay in agreement.
At runtime, processMessage reads a descriptor address, loads its bit count, byte count and type, then calls numericFieldHandler or stringFieldHandler. When the field finishes, it advances the output pointer by the descriptor’s byte count. Signed and unsigned numbers share the extraction loop; later conversion interprets their type.
processMessage and the two field handlers · List values · Variables and locking mutable values.
05 / FOLLOW A FIELD YOURSELF
One description. Three bytes. A decoded value.
Choose a field and change its example value. Notice that the descriptor stays the same when the value changes. Then read the bits one at a time, or decode the entire field.
Speed: aField(10,2,uInt) emits descriptor 0A 02 01. The ten input bits 0010111011 represent 187. Decoding produces 00 BB, which the Class A/B viewer displays as 18.7 knots. Changing that speed does not change the descriptor.
Enable JavaScript to change fields and step through the interactive example.
- bits
- bytes
- dataType
Three descriptor bytes · hexadecimal
On the C128 · consume the field’s bits
Green bits have been read. Gold marks the next bit.
Decoded output bytes · hexadecimalThis browser illustration models field extraction from an already isolated bit sequence. It does not run Kick Assembler or emulate the 8502, NMEA checksums, six-bit transport armour or fragmented-message handling. Bit groups are for readability; a real field can begin partway through a transport character. Example values are illustrative. Numeric range checks here concern storage width, not every protocol validity rule.
06 / CHANGE THE DESCRIPTION
Adding a field need not add an extraction routine.
For a new field that uses an existing encoding, define its shape and place its descriptor reference at the correct point in the message. The shared handlers already know how to consume a stated number of bits and produce the requested output bytes.
- Describe the field. Give it a name, input width, output size and one of the supported types.
- Place it in the actual message layout. Match the sender’s field order and widths; include any intervening spare bits. Update the matching ordered list used by offset and size helpers.
- Check the consumers. Keep decoded buffers and stored payloads large enough. Review field selectors, formatting, scaling and availability rules. Rebuild dependent tables and callers together.
- Verify a known message. Check both the new field and the fields after it: an incorrect width moves the bit boundary for everything that follows.
The catalogue’s iteration order is not the protocol’s field order. Runtime message layouts make that order explicit. Generated descriptor indices also feed BASIC selectors, so adding entries requires checking the matching frontend constants.
The reusable part is the extraction mechanism. A new encoding can need a new handler, and a conditional message layout can need additional logic. AIS4CBM’s type-24 static report already has a separate conditional payload path. Describing fields makes extension easier while leaving these responsibilities visible.
07 / THE IDEA TRAVELS
From vessels to aircraft.
Suppose the next application tracks aircraft. We would describe an identifier, altitude, groundspeed, position and heading, then arrange those fields into the required message layouts. Shared routines could still perform the repetitive extraction work.
There is already a small example in this project: AIS message type 9 describes search-and-rescue aircraft. Its layout uses altitude alongside familiar fields such as speed, longitude, latitude and course. The interactive altitude example above uses that real descriptor:
.eval allFiledsHT.put("altitude",aField(12,2,uInt))An aircraft feed using another protocol, such as ADS-B, would also require the appropriate framing, encodings and interpretation. The transferable idea is to describe field layouts and reuse the decoding machinery wherever those encodings allow it.
Built with Kick Assembler
Thanks to Mads Nielsen for Kick Assembler. Its scripting and data structures make these field descriptions and generated tables possible.
- Kick Assembler website and official manual.
- decoderHeaders.asm — field catalogue, descriptors, message layouts and offset helpers.
- decoder.asm — the shared decoder and field handlers.
- Ask ML, get a BASIC value: USR — how the frontend reads a decoded field.
- From an AIS integer to FAC — converting a decoded integer to a BASIC number.
Source excerpts retain the project’s identifiers and instructions, with surrounding code omitted and whitespace simplified. They are explanations of the existing build, not a standalone decoder. Source links follow main.