Skip to content

A Streaming CRC-8 on C64

A saved score or a small asset can pick up an accidental changed byte.

A saved score or a small asset can pick up an accidental changed byte. A streaming checksum gives the loader something to compare before using the data, without needing to hold a second copy of the whole message.

C64 shows the working diagnostic row F4 39 00 00 00 00 00 00.
A native completed caller shows its state as hexadecimal bytes. The field order below explains the row; the full packet retains every call.

Include, initialise and call

Reserve CRC, DATA as 2 contiguous private bytes in that order. Include the helper once. Call streaming_crc8_init at the intended reset boundary: it clears every named byte, including input fields. Set the starting values afterwards, then call streaming_crc8_step for each intended service.

Initialise, feed the counted payload bytes one at a time and store the final CRC separately. A reader recomputes over the same span and compares with the stored value. Do not accidentally include the stored checksum in one side’s payload but exclude it on the other.

CRC is the retained eight-bit remainder and DATA is the next input byte. Initialise once per message, then call step for each byte in order. This is a non-reflected, left-shifting CRC with initial remainder zero and no final XOR; the qualified polynomials are 07 and 31, with the x⁸ term implicit. DATA survives and CRC is replaced. Producer and consumer must agree on every parameter and the exact covered bytes.

Reusable initialisation and service

6502.inc
; SPDX-License-Identifier: MIT
; A Streaming CRC-8. Contiguous private bytes: CRC, DATA.
; init clears all fields; set inputs afterwards. No I/O, waits or IRQ ownership.
; A/X/flags change; Y and decimal mode survive. D=0 for binary arithmetic.
streaming_crc8_init:
    lda #0
    sta CRC
    sta DATA
    rts
streaming_crc8_step:
    lda CRC
    eor DATA
    sta CRC
    ldx #8
crc_bit:
    asl CRC
    bcc crc_next
    lda CRC
    eor #CRC_POLY
    sta CRC
crc_next:
    dex
    bne crc_bit
    rts
component_end:

State, registers and ownership

Two bytes, one persistent remainder and one input. The demonstration feeds the ASCII bytes of 123456789. It finishes at hexadecimal F4 for polynomial 07 and A2 for polynomial 31. These are this implementation’s directly checked message results, not a claim that every CRC-8 convention has the same answer.

A/X/flags may change; Y and decimal mode survive. Clear decimal mode before binary arithmetic; the caller uses CLD. A direct call needs two return-address stack bytes.

The implementation binds fixed symbols and has one mutation owner. Independent users need separate state blocks and scoped/renamed labels, including routine labels; sharing the same bindings means sharing the state. Do not let an interrupt and the main loop mutate the same block without an explicit ownership policy.

The standalone PRG owns screen/font/colour RAM, disables interrupt sources and does not restore BASIC. Its private scratch uses $70–$76 and the state binding starts at $80; its diagnostic output starts at $C000.

A caller you can adapt

The buildable program supplies bindings and include order. Its separate client retains state between calls. checked_step calls the public streaming_crc8_step entry and checks register sentinels; emit_state records diagnostics. Your game can call the public entry directly, remove the diagnostics and consume the documented outputs.

Bindings, buildable caller and include order

demo.asm
CRC=$80
DATA=$81
STATE=$80
input_ptr=$70
cases_left=$72
ptr=$74
saved_y=$76
OUTPUT=$c000
preview=OUTPUT+1400
ready=OUTPUT+1408
CUSTOM=0
BROKEN=0
COYOTE_STEPS=4
BUFFER_STEPS=4
TEXT_CAP=8
PROPERTY_COUNT=16
STREAM_BYTES=13
CRC_POLY=7
!source "caller.inc"
!source "exercise.inc"
component_start:
!source "component.inc"
!source "data.inc"
!source "cases.inc"
hex_font:
    !byte 0,0,0,0,0,0,0,0,0,56,40,40,40,56,0,0,0,16,48,16,16,56,0,0,0,56,8,56,32,56,0,0,0,56,8,56,8,56,0,0,0,40,40,56,8,8,0,0,0,56,32,56,8,56,0,0,0,56,32,56,40,56,0,0,0,56,8,8,8,8,0,0,0,56,40,56,40,56,0,0,0,56,40,56,8,56,0,0,0,16,40,56,40,40,0,0,0,48,40,48,40,48,0,0,0,56,32,32,32,56,0,0,0,48,40,40,40,48,0,0,0,56,32,48,32,56,0,0,0,56,32,48,32,32,0,0
code_end:

A separate client retaining state between calls

exercise.inc, excerpt
preview_rules:
!if CUSTOM {
    lda #49
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #50
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #51
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #52
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #53
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #54
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #55
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #56
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #57
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
} else {
    lda #49
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #50
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #51
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #52
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #53
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #54
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #55
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #56
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
    lda #57
    sta DATA
!if BROKEN {
    lda #0
    sta CRC
    jsr checked_step
} else {
    jsr checked_step
}
    jsr emit_state
}
    rts

The display fields are CRC, DATA; remaining display fields are zero. Each is one hexadecimal byte. The normal row is F4 39 00 00 00 00 00 00. This is a completed state observation, not a recording of an actual character jumping, a camera scrolling or an asset being displayed.

Make it your own

The custom build uses polynomial 31 instead of 07. Both are complete specified parameter sets here; changing the polynomial on only one side makes otherwise identical payloads disagree.

Maintained custom configuration or caller

custom.asm
CRC=$80
DATA=$81
STATE=$80
input_ptr=$70
cases_left=$72
ptr=$74
saved_y=$76
OUTPUT=$c000
preview=OUTPUT+1400
ready=OUTPUT+1408
CUSTOM=1
BROKEN=0
COYOTE_STEPS=2
BUFFER_STEPS=2
TEXT_CAP=4
PROPERTY_COUNT=16
STREAM_BYTES=13
CRC_POLY=49
!source "caller.inc"
!source "exercise.inc"
component_start:
!source "component.inc"
!source "data.inc"
!source "cases.inc"
hex_font:
    !byte 0,0,0,0,0,0,0,0,0,56,40,40,40,56,0,0,0,16,48,16,16,56,0,0,0,56,8,56,32,56,0,0,0,56,8,56,8,56,0,0,0,40,40,56,8,8,0,0,0,56,32,56,8,56,0,0,0,56,32,56,40,56,0,0,0,56,8,8,8,8,0,0,0,56,40,56,40,56,0,0,0,56,40,56,8,56,0,0,0,16,40,56,40,40,0,0,0,48,40,48,40,48,0,0,0,56,32,32,32,56,0,0,0,48,40,40,40,48,0,0,0,56,32,48,32,56,0,0,0,56,32,48,32,32,0,0
code_end:

The custom row is A2 39 00 00 00 00 00 00. These records qualify the named builds and inputs; another layout, format or input domain needs explicit bounds and fresh execution evidence.

Common mistake and repair

The wrong client clears CRC before every byte. Only the last byte contributes to the final result, which becomes AF for the normal polynomial instead of F4. Reset once at the message boundary, then retain the remainder between calls.

Select the deliberately wrong client

wrong.asm
CRC=$80
DATA=$81
STATE=$80
input_ptr=$70
cases_left=$72
ptr=$74
saved_y=$76
OUTPUT=$c000
preview=OUTPUT+1400
ready=OUTPUT+1408
CUSTOM=0
BROKEN=1
COYOTE_STEPS=4
BUFFER_STEPS=4
TEXT_CAP=8
PROPERTY_COUNT=16
STREAM_BYTES=13
CRC_POLY=7
!source "caller.inc"
!source "exercise.inc"
component_start:
!source "component.inc"
!source "data.inc"
!source "cases.inc"
hex_font:
    !byte 0,0,0,0,0,0,0,0,0,56,40,40,40,56,0,0,0,16,48,16,16,56,0,0,0,56,8,56,32,56,0,0,0,56,8,56,8,56,0,0,0,40,40,56,8,8,0,0,0,56,32,56,8,56,0,0,0,56,32,56,40,56,0,0,0,56,8,8,8,8,0,0,0,56,40,56,40,56,0,0,0,56,40,56,8,56,0,0,0,16,40,56,40,40,0,0,0,48,40,48,40,48,0,0,0,56,32,32,32,56,0,0,0,48,40,40,40,48,0,0,0,56,32,48,32,56,0,0,0,56,32,48,32,32,0,0
code_end:
C64 shows the deliberate fault row AF 39 00 00 00 00 00 00.
The wrong client completes with AF 39 00 00 00 00 00 00. Its code and the explanation above identify the cause; the capture observes the result.

The BROKEN branch is visible in the client. repaired.asm restores the normal selection; its executable matches demo exactly. Spectrum tape filenames may differ while loaded CODE remains identical. The fault is a labelled experiment, not a suggested alternative.

Cost, limits and verification

The measured layout has 29 code bytes, 0 immutable data bytes and 2 private state bytes. Alignment, stack usage, diagnostic tables and display machinery are additional. The extents come from the loaded program’s symbols, not from counting source lines.

Exactly eight bit stages per input byte, with a conditional polynomial XOR in each stage. No lookup table is required. A 256-byte table can trade ROM space for fewer per-byte operations, but that is a separate implementation and is not measured here.

An eight-bit CRC has only 256 results and cannot detect every possible change. It checks accidental corruption; it is not authentication or protection against someone deliberately editing a save. Wider CRCs, table-based updates and other parameter conventions need separate contracts and vectors. Saving atomically and validating the file length remain separate concerns.

Four native runs use PAL C64 with stock BASIC/KERNAL PRG startup and RUN; Asm198x 0.0.72 and Emu198x 0.44.2. Each checks 64 boundary/varied/error cases, dirty initialisation and every state of a separate sequential client against an independent integer model. This is not exhaustive coverage of every possible byte combination. Source/tool/firmware hashes, immutable loaded code/data, native startup/completion, preserved-register sentinels, helper/data sizes, screenshot bytes/pixels and exact repair are checked. Deliberately altered observations must fail. Firmware is hashed, never distributed.

Related technique · Maths for Games · The Craft.

Source and native evidence.