← Small tutorials

05 / KICK ASSEMBLER DATA STRUCTURES

Describe the fields.
Reuse the decoder.

Kick Assembler’s data structures became easier to understand when I used them to describe AIS fields. An MMSI, a speed and a vessel name have different shapes. Give those shapes a description, and shared code can do the repetitive decoding work.

Commodore 128 · Kick Assembler + 8502 assembly · Try the interactive field example ↓

New to AIS? Read what AIS is and why it makes a good Commodore project.

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.

  1. 01 / ON THE PCDescribe the field

    A named structure holds its input width, output size and type.

  2. 02 / DURING ASSEMBLYEmit a small table

    Kick Assembler writes three descriptor bytes and resolves references to them.

  3. 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.

02 / START WITH ONE FIELD

Three properties describe the work.

The project defines a small structure for a field’s shape:

decoderHeaders.asm · the field description
.struct aField {bits,bytes,dataType}
Input width and output size have different jobs
PropertyMeaningSpeed example
bitsBits to consume from the incoming payload.10
bytesBytes used by the decoded field.2
dataTypeThe 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:

decoderHeaders.asm · catalogue declaration and selected entries
.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.

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:

decoderHeaders.asm · current descriptor-table generation
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:

decoderHeaders.asm · associate each field name with its emitted address
.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.

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:

decoderHeaders.asm · the shared header layout
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:

decoderHeaders.asm · the ordered Class A position fields
.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.

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.

A complete example, with or without JavaScript

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.

This 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.

  1. Describe the field. Give it a name, input width, output size and one of the supported types.
  2. 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.
  3. 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.
  4. 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:

decoderHeaders.asm · an existing aircraft field
.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.

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.