Skip to content

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

RtcReadTimeA = hours, X = minutes, Y = seconds
RtcReadDateA = day, X = month, Y = year — and the century lands in RTC_BUF_CENT
RtcWriteTimeThe same three, going in
RtcWriteDateThe 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

asm
; 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.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 ", $00
RUN
THE TIME IS 09:30:00 ON 26/12/2026
AND THE CLOCK CARD REMEMBERS 30

OK

PrintTwo 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

RtcReadNVRAMAddress in X, byte back in A
RtcWriteNVRAMAddress 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.

asm
  ldx #HIGH_SCORE_LOW
  lda ScoreLow
  jsr RtcWriteNVRAM
  ldx #HIGH_SCORE_HIGH
  lda ScoreHigh
  jsr RtcWriteNVRAM

A 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. The usual answer is a two-byte signature of your own — write something recognizable alongside your data, and treat everything as unset until you read it back.

The same 256 bytes are what BASIC's NVRAM reaches, so a program in each language can leave notes for the other.

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.

asm
  lda RTC_SEC                   ; packed decimal, straight from the chip
  and #$0F                      ; the units digit

Next: interrupts.

Written for BIOS v1.5. Released under the MIT License.