I recently implemented simple Power-on self test (POST) routine for my 65C816 test board, so that it can stop and indicate a hardware failure before attempting to run any normal code.
This was an interesting adventure for two main reasons:
- This code needs to work with no RAM present in the computer.
- I wanted to try re-purposing the emulation status (E) output on the 65C816 CPU to blink an LED.
Background
Even modern computers will stop and provide a blinking light or series of beeps if you don’t install RAM or a video card. This is implemented in the BIOS or UEFI.
I got the idea to use the E
or MX
CPU outputs for this purpose from this thread on the 6502.org forums. This method would allow me to blink a light with just a CPU, clock input, and ROM working.
My main goal is to perform a quick test that each device is present, so that start-up fails in a predictable way if I’ve connected something incorrectly. This is much simpler than the POST routine from a real BIOS, because I’m not doing device detection, and I’m not testing every byte of memory.
Boot-up process
On my test board, I’ve connected an LED directly to the emulation status (E
) output on the 65C816 CPU. The CPU starts in emulation mode (E
is high). However I have noticed that on power-up, the value of E
appears to be random until /RES
goes high. If I were wiring this up again, I would also prevent the LED from lighting up while the CPU is in reset:
The first thing the CPU does is read an address from ROM, called the reset vector, which tells it where start executing code.
In my case, the first two instructions set the CPU to native mode, which are clc
(clear carry) and xce
(exchange carry with emulation).
.segment "CODE"
reset:
.a8
.i8
clc ; switch to native mode
xce
jmp post_check_loram
By default accumulator and index registers are 8-bit, the .a8
and .i8
directives simply tell the assembler ca65 that this is the case.
Next, the code will jmp
to the start of the POST process.
Checking low RAM
The first part of the POST procedure checks if the lower part of RAM is available, by writing values to two address and checking that the same values can be read back.
Note that a:$00
causes the assembler to interpret $00
as an absolute address. This will otherwise be interpreted as direct-page address, which is not what’s intended here.
post_check_loram:
ldx #%01010101 ; Power-on self test (POST) - do we have low RAM?
ldy #%10101010
stx a:$00 ; store known values at two addresses
sty a:$01
ldx a:$00 ; read back the values - unlikely to be correct if RAM not present
ldy a:$01
cpx #%01010101
bne post_fail_loram
cpy #%10101010
bne post_fail_loram
jmp post_check_hiram
If this fails, then the boot process stops, and the emulation LED blinks in a distinctive pattern (two blinks) forever.
post_fail_loram: ; blink emulation mode output with two pulses
pause 8
blink
blink
jmp post_fail_loram ; repeat indefinitely
Macros: pause and blink
It’s a mini-challenge to write code to blink an LED in a distinctive pattern without assuming that RAM works. This means no stack operations (eg. jsr
and rts
instructions), and that I need to store anything I need in 3 bytes: the A
, X
, Y
registers. A triple-nested loop is the best I can come up with.
I wrote a pause
macro, which runs a time-wasting loop for the requested duration – approximately a multiple of 100ms at this clock speed. Every time this macro is used, the len
value is substituted in, and this code is included in the source file. This example also uses unnamed labels, which is a ca65
feature for writing messy code.
.macro pause len ; time-wasting loop as macro
lda #len
:
ldx #64
:
ldy #255
:
dey
cpy #0
bne :-
dex
cpx #0
bne :--
dec
cmp #0
bne :---
.endmacro
The second macro I wrote is blink
, which briefly lights up the LED attached to the E
output by toggling emulation mode. I’m using the pause
macro from both native mode and emulation mode in this snippet, so I can only treat A
, X
and Y
as 8-bit registers.
.macro blink
sec ; switch to emulation mode
xce
pause 1
clc ; switch to native mode
xce
pause 2
sec ; switch to emulation mode
.endmacro
Checking high RAM
There is also a second RAM chip, and this process is repeated with some differences. For one, I can now use the stack, which is how I set the data bank byte in this snippet.
Here a:$01
is important, because with direct page addressing, $01
means $000001
at this point in the code, where I want to test that I can write to the memory address $080001
.
post_check_hiram:
ldx #%10101010 ; Power-on self test (POST) - do we have high RAM?
ldy #%01010101
lda #$08 ; data bank to high ram
pha
plb
stx a:$00 ; store known values at two addresses
sty a:$01
ldx a:$00 ; read back the values - unlikely to be correct if RAM not present
ldy a:$01
cpx #%10101010
bne post_fail_hiram
cpy #%01010101
bne post_fail_hiram
lda #$00 ; reset data bank to boot-up value
pha
plb
jmp post_check_via
The failure here is similar, but the LED will blink 3 times instead of 2.
post_fail_hiram: ; blink emulation mode output with three pulses. we could use RAM here?
pause 8
blink
blink
blink
jmp post_fail_hiram ; repeat indefinitely
To make sure that I was writing to different chips, I installed the RAM chips one at a time, first observing the expected failures, and then observing that the code continued past this point with the chip installed.
I also checked with an oscilloscope that both RAM chips are now being accessed during start-up. Now that I’ve got some confidence that the computer now requires both chips to start, I can skip a few debugging steps if I’ve got code that isn’t working later.
Checking the Versatile Interface Adapter (VIA)
The third chip I wanted to add to the POST process is the 65C22 VIA. I kept this check simple, because one read to check for a start-up default is sufficient to test for device presence.
VIA_IER = $c00e
post_check_via: ; Power-on self test (POST) - do we have a 65C22 Versatile Interface Adapter (VIA)?
lda a:VIA_IER
cmp #%10000000 ; start-up state, interrupts enabled overall (IFR7) but all interrupt sources (IFR0-6) disabled.
bne post_fail_via
jmp post_ok
This stops and blinks 4 times if it fails. I recorded the GIF at the top of this blog post by removing the component which generates a chip-select for the VIA, which causes this code to trigger on boot.
post_fail_via:
pause 8
blink
blink
blink
blink
jmp post_fail_via
Beep for good measure
At the end of the POST process, I put in some code to generate a short beep.
This uses the fact that the 65C22 can toggle the PB7
output each time a certain number of clock-cycles pass. I’ve connected a piezo buzzer to that output, which I’m using as a PC speaker. The 65C22 is serving the role of a programmable interrupt timer from the PC world.
VIA_DDRB = $c002
VIA_T1C_L = $c004
VIA_T1C_H = $c005
VIA_ACR = $c00b
BEEP_FREQ_DIVIDER = 461 ; 1KHz, formula is CPU clock / (desired frequency * 2), or 921600 / (1000 * 2) ~= 461
post_ok: ; all good, emit celebratory beep, approx 1KHz for 1/10th second
; Start beep
lda #%10000000 ; VIA PIN PB7 only
sta VIA_DDRB
lda #%11000000 ; set ACR. first two bits = 11 is continuous square wave output on PB7
sta VIA_ACR
lda #<BEEP_FREQ_DIVIDER ; set T1 low-order counter
sta VIA_T1C_L
lda #>BEEP_FREQ_DIVIDER ; set T1 high-order counter
sta VIA_T1C_H
; wait approx 0.1 seconds
pause 1
; Stop beep
lda #%11000000 ; set ACR. returns to a one-shot mode
sta VIA_ACR
stz VIA_T1C_L ; zero the counters
stz VIA_T1C_H
; POST is now done
jmp post_done
The post_done
label points to the start of the old ROM code, which is currently just a “hello world” program.
Next steps
I’m now able to lock in some of my assumptions about should be available in software, so that I can write more complex programs without second-guessing the hardware.
Once the boot ROM is interacting with more hardware, I may add additional checks. I will probably need to split this into different sections, and make use of jsr
/rts
once RAM has been tested, because the macros are currently generating a huge amount of machine code. I have 8KiB of ROM in the memory map for this computer, and the code on this page takes up around 1.1KiB.