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.
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
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.
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
.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.
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.
rts instructions), and that I need to store anything I need in 3 bytes: the
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
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.
a:$01 is important, because with direct page addressing,
$000001 at this point in the code, where I want to test that I can write to the memory address
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
post_done label points to the start of the old ROM code, which is currently just a “hello world” program.
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
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 tales up around 1.1KiB.