The clock, and memory that lasts
The clock card carries a DS1511Y: a real-time clock with its own battery, and 256 bytes of memory that survive being switched off. The battery is why your ACE knows the date after two weeks in a closet, and the 256 bytes are the best place to keep a high score.
Reading the time
RtcReadTime | A = hours, X = minutes, Y = seconds |
RtcReadDate | A = day, X = month, Y = year — and the century lands in RTC_BUF_CENT |
RtcWriteTime | The same three, going in |
RtcWriteDate | The same, plus RTC_BUF_CENT set first |
Every one of those numbers is ordinary binary. The chip itself stores packed decimal, and the Kernal converts in both directions so you never have to think about it. Hours are 0 to 23; the year is 0 to 99 with the century kept separately, which is how you get 20 and 26 rather than an argument about what year 26 means.
Setting it, reading it, and leaving a note
; Setting the clock and reading it back, then leaving a note in the 256 bytes
; of memory the clock card keeps alive on its battery.
;
; The clock hands over plain binary numbers — hours, minutes, day, month — so
; nothing here has to unpack anything. Printing two digits with a leading zero
; is the only real work.
.setcpu "65C02"
.include "6502-VDP.inc"
.segment "CODE"
BasicStartup:
.byte $0A, $08, $0A, $00, $A5, $32, $30, $36, $30, $00, $00, $00
NOTE_SLOT = 0 ; which of the 256 battery-backed bytes to use
Start:
lda #20 ; the century, kept apart from the year
sta RTC_BUF_CENT
lda #26 ; day
ldx #12 ; month
ldy #26 ; year within the century
jsr RtcWriteDate
lda #9 ; hours
ldx #30 ; minutes
ldy #0 ; seconds
jsr RtcWriteTime
lda #<Now
ldy #>Now
jsr PrintStr
jsr RtcReadTime ; A = hours, X = minutes, Y = seconds
phy ; PrintTwo needs X and Y for itself
phx
jsr PrintTwo ; hours
lda #':'
jsr Chrout
pla
jsr PrintTwo ; minutes
lda #':'
jsr Chrout
pla
jsr PrintTwo ; seconds
lda #<OnThe
ldy #>OnThe
jsr PrintStr
jsr RtcReadDate ; A = day, X = month, Y = year
phy
phx
jsr PrintTwo ; day
lda #'/'
jsr Chrout
pla
jsr PrintTwo ; month
lda #'/'
jsr Chrout
lda RTC_BUF_CENT ; the century the read left behind
jsr PrintTwo
pla
jsr PrintTwo ; year
jsr PrintCRLF
; Those 256 bytes survive a power cut. Write one, read it straight back, and
; it will still be there next week.
lda #30
ldx #NOTE_SLOT
jsr RtcWriteNVRAM
lda #<Remembered
ldy #>Remembered
jsr PrintStr
ldx #NOTE_SLOT
jsr RtcReadNVRAM
jsr PrintTwo
jsr PrintCRLF
rts
; Print A as two decimal digits, leading zero included. Clobbers X.
PrintTwo:
ldx #'0'
@tens:
cmp #10
bcc @units
sbc #10
inx
bra @tens
@units:
ora #'0'
pha ; the units digit, out of the way
txa
jsr Chrout ; tens
pla
jmp Chrout ; units
Now: .byte "THE TIME IS ", $00
OnThe: .byte " ON ", $00
Remembered: .byte "AND THE CLOCK CARD REMEMBERS ", $00RUN
THE TIME IS 09:30:00 ON 26/12/2026
AND THE CLOCK CARD REMEMBERS 30
OKPrintTwo at the bottom is the routine you will keep: PrintDecU16 prints 9 as 9, and a clock wants 09. Repeated subtraction is the cheapest way to split a number under 100 into two digits, and the pha around the first Chrout is there because a routine that prints is allowed to use your registers.
Seconds move while you are reading
The three fields come back from one read, so they are consistent with each other. But if you read the time, do some work, and read the date, midnight can happen in between. Read the date first when it matters.
The 256 bytes
RtcReadNVRAM | Address in X, byte back in A |
RtcWriteNVRAM | Address in X, byte in A |
Addresses 0 to 255, no structure at all — the card gives you the bytes and what they mean is up to you. A high score is two bytes. A settings block is a handful. A "have they seen the tutorial" flag is one bit.
ldx #HIGH_SCORE_LOW
lda ScoreLow
jsr RtcWriteNVRAM
ldx #HIGH_SCORE_HIGH
lda ScoreHigh
jsr RtcWriteNVRAMA fresh card holds garbage, not zero
Battery-backed memory that has never been written contains whatever it powered up with. Do not trust byte 0 to be 0. If you use the raw bytes, keep a signature of your own alongside your data and treat everything as unset until you read it back. The save slots below do that job for you, with a checksum.
The same 256 bytes are what BASIC's NVRAM reaches, so a program in each language can leave notes for the other.
Save slots
Two programs that both use byte 0 will ruin each other's high score. So the Kernal also divides the 256 bytes into 16 save slots of 16 bytes each, and every program that uses them shares the card safely.
| Byte | Holds |
|---|---|
| 0 | The owner ID: one byte your program picks. $00 means the slot is free |
| 1 | A checksum the Kernal works out |
| 2–15 | 14 bytes that are yours |
Slot n starts at byte n × 16. The checksum covers the owner ID and the 14 bytes: start at $A6, then for each byte rotate left one bit and exclusive-OR the byte in. A slot is free, valid (the checksum agrees) or damaged (it does not). Each slot is checked on its own, so one damaged slot never costs you the others.
| Entry | In | Out |
|---|---|---|
NvStat | X = slot (0-15) | A = NV_EMPTY / NV_VALID / NV_BAD, Y = owner ID, C clear C set on no RTC or bad slot (A, Y undefined) |
NvRead | X = slot (0-15), A/Y = destination lo/hi | A = status, Y = owner ID; C clear and the buffer written only if A = NV_VALID C set on no RTC or bad slot (A, Y undefined), or a slot that is not valid (A = its status, Y = its owner ID) — the buffer is untouched |
NvWrite | X = slot (0-15), A/Y = source lo/hi, NV_ID = owner ID ($01-$FF) | C clear; C set on no RTC, bad slot, or NV_ID = 0 (nothing written) |
NvErase | X = slot (0-15) | C clear; C set on no RTC or bad slot |
NvFind | A = owner ID | X = slot, C clear; C set if no slot matched (X undefined) |
NvFormat | nothing | C clear; C set on no RTC |
; A game's save, kept in one of the clock card's sixteen save slots: find the
; slot, write a record, read it back, then damage one byte and watch the
; Kernal refuse to load it.
;
; The Kernal does the checksum. All the game decides is its owner ID and what
; its 14 bytes mean: here a level, a two-byte score and an eleven-letter name.
.setcpu "65C02"
.include "6502-VDP.inc"
.segment "CODE"
BasicStartup:
.byte $0A, $08, $0A, $00, $A5, $32, $30, $36, $30, $00, $00, $00
GAME_ID = $5A ; any value but $00, which means "free"
Start:
; On a ROM older than v1.6 these six entries are reserved slots: a bare RTS
; that leaves carry as it found it. Ask the ROM before trusting the answers.
jsr KernalVersion ; A = major, X = minor
cmp #1
bcc @old
bne @new
cpx #6
bcs @new
@old:
lda #<TooOld
ldy #>TooOld
jmp PrintStr
@new:
lda #GAME_ID
jsr NvFind ; X = our slot, carry set if we have none
bcc @ours
lda #$00
jsr NvFind ; X = the first free slot
bcc @free
lda #<NoRoom
ldy #>NoRoom
jmp PrintStr
@ours:
stx Slot
lda #<Found
ldy #>Found
bra @report
@free:
stx Slot
lda #<NoSave
ldy #>NoSave
@report:
jsr PrintStr
jsr PrintSlot
jsr PrintCRLF
; Save. The owner ID goes in NV_ID; the 14 bytes go by address.
lda #GAME_ID
sta NV_ID
ldx Slot
lda #<Record
ldy #>Record
jsr NvWrite
bcs Failed
; Load it into a different buffer, so what is printed really came back.
ldx Slot
jsr NvStat ; A = NV_EMPTY, NV_VALID or NV_BAD
cmp #NV_VALID
bne Failed
lda #<Loaded
ldy #>Loaded
jsr NvRead
bcs Failed
jsr PrintRecord
; Now damage it: flip every bit of the level byte, going around the Kernal.
lda Slot ; slot n starts at NVRAM n * 16
asl
asl
asl
asl
ora #2 ; + 2 skips the owner ID and the checksum
sta Where
tax
jsr RtcReadNVRAM
eor #$FF
ldx Where
jsr RtcWriteNVRAM
ldx Slot
lda #<Loaded
ldy #>Loaded
jsr NvRead ; carry set: A = its status, Y = its owner
bcc Failed
cmp #NV_BAD
bne Failed
phy
lda #<Damaged
ldy #>Damaged
jsr PrintStr
jsr PrintSlot
lda #<Owner
ldy #>Owner
jsr PrintStr
pla
ldx #0
jsr PrintDecU16
jmp PrintCRLF
Failed:
lda #<Trouble
ldy #>Trouble
jmp PrintStr
; "LEVEL 3, SCORE 1250, ADA" from the 14 bytes at Loaded.
PrintRecord:
lda #<Level
ldy #>Level
jsr PrintStr
lda Loaded
ldx #0
jsr PrintDecU16
lda #<Score
ldy #>Score
jsr PrintStr
lda Loaded + 1
ldx Loaded + 2
jsr PrintDecU16
lda #<Comma
ldy #>Comma
jsr PrintStr
lda #<(Loaded + 3) ; the name is zero-padded, so it prints as is
ldy #>(Loaded + 3)
jsr PrintStr
jmp PrintCRLF
PrintSlot:
lda Slot
ldx #0
jmp PrintDecU16
Slot: .byte 0
Where: .byte 0
; The record as the game keeps it: exactly 14 bytes, the name zero-padded to
; eleven.
Record:
.byte 3 ; level
.word 1250 ; score
.byte "ADA", 0, 0, 0, 0, 0, 0, 0, 0
; Where it comes back to. The byte past the 14 ends an eleven-letter name.
Loaded: .res 15, 0
TooOld: .byte "THIS ROM HAS NO SAVE SLOTS", $0D, $0A, $00
NoRoom: .byte "EVERY SLOT IS TAKEN", $0D, $0A, $00
NoSave: .byte "NO SAVE YET - USING FREE SLOT ", $00
Found: .byte "FOUND OUR SAVE IN SLOT ", $00
Level: .byte "LOADED LEVEL ", $00
Score: .byte ", SCORE ", $00
Comma: .byte ", ", $00
Damaged: .byte "SLOT ", $00
Owner: .byte " IS DAMAGED - OWNER ", $00
Trouble: .byte "THE CLOCK CARD DID NOT ANSWER", $0D, $0A, $00RUN
NO SAVE YET - USING FREE SLOT 0
LOADED LEVEL 3, SCORE 1250, ADA
SLOT 0 IS DAMAGED - OWNER 90
OKThe rules are the same for all six:
- Carry set means nothing happened. No clock card, a slot number of 16 or more,
NvWritewithNV_IDat 0 (useNvErasefor that),NvFindwith no match, orNvReadon a slot that is not valid. - A failed
NvReadstill answers. A holds the slot's status and Y its owner ID, so a game can tell "no save yet" from "your save is damaged". Your buffer is left alone. NvFindmatches damaged slots too, lowest slot first. A game that finds its ID and then gets carry fromNvReadknows its save was damaged, rather than starting over as if it never had one.NV_IDis an input toNvWriteand nothing else. Nothing writes it back;NvStatandNvReadgive you the owner in Y.- X survives
NvStat,NvRead,NvWriteandNvErase, so a loop over the slots needs no reload.NvFindandNvFormatchange it. NvReadandNvWriteuseSTR_PTR($02–$03), just asPrintStrdoes.- Decimal mode and the interrupt flag come back as you left them. A score kept in decimal mode saves safely. Interrupts are held off for the moment a copy takes, because the copy streams bytes through the chip and nothing else may touch it in between. For the same reason an NMI handler must never touch the clock card's memory.
Check the version first
On a ROM older than v1.6 these six addresses are reserved slots: a bare RTS that leaves carry however you had it, which can look like success. Ask KernalVersion for 1.6 or later before trusting an answer, as the listing does. See which ROM am I on?
No clock card, no slots
The slots live on the clock card, so a machine without one, like a KIM built on its own from COB cards, gets carry set from every one of the six. NvFormat empties all 16 at once.
BASIC can read and write the same slots, so a save manager written in BASIC can list your game's saves. See save slots from BASIC.
The registers underneath
$8800 upwards, one per field, all in packed decimal — $59 means 59, not 89. Seconds, minutes, hours, day of week, date, month, year, century, then four alarm registers and a watchdog.
The alarm is worth knowing about: set it and the card can pull the interrupt line at a chosen time. Nothing in the Kernal uses it, so the whole thing is free for you — see Interrupts for how to catch it.
lda RTC_SEC ; packed decimal, straight from the chip
and #$0F ; the units digitNext: interrupts.

