Building a 65C816 test board

In the last few months, I’ve been learning about the 65C816 processor, and trying to build a working computer which uses it. My latest breadboard-based prototype was not reliable, and I decided to convert it to a PCB to hopefully eliminate the problem, or to at least identify it.

Quick goals

I was aiming to make a debug-friendly 4-layer PCB, the size of two standard breadboards. This will be my first time designing a 4-layer board, and also my first time using KiCad 6 to create a PCB.

I didn’t have a working prototype, so I built in test points for connecting an oscilloscope or logic analyser to troubleshoot. In case of errors, I made it possible to leave some components unpopulated, and instead drive signals externally.

I also left some extra footprints which I might use for future improvements, or can fall back to if some of my ideas don’t work out.

The clock

Some components require the inverse of the CPU clock, and there was previously a small delay from inverting the signal. I don’t think this is a problem on its own, but I introduced a D-type flip-flop to create a proper two-phase clock.

This does halve the CPU clock speed, so I added jumpers to allow an alternative clock source to be selected for the CPU, where previously the UART and CPU clocks needed to be the same. Note that there is an error in the wiring here, which I only discovered later.

The oscillator I’m using on the breadboard prototype is a DIP-8 package. A larger variety of oscillators are available as a 5x7mm QFN package, where the four corner pins have the same function as the DIP-8 version. I added an alternative footprint, which fits without taking up any extra board space. I expect that I’ll only use this if I attempt to run the board at 3.3V later.

Some 6-pin QFN oscillators provide an inverted clock output on one of the pins, and it would be an idea to use one of those in a future design to reduce the component count.

New reset controller

Several components on the board require a reset input. These are mostly active-low inputs, but one component has an active-high reset. I discovered the tiny MIC2775 supervisory IC, which provides both.

I haven’t used this part before, so in case of problems, I also made it possible to remove this part and use a DS1813, which generates an active-low reset on my prototype.

These parts work by sensing voltage, and drop-in alternatives exist for either part if I move to 3.3V.

ROM

I am currently using a parallel EEPROM to store code for my prototype, and will add this to the PCB for this test board. As with the the clock and reset controller, I’m working with 5V components today, but considering what I would need to change to run the whole board at 3.3V.

The few EEPROMs which are available at 3.3V use a different package, and are quite slow (200ns or worse). Parallel flash chips seem to be the most promising alternative, since they have a similar interface, although I haven’t confirmed that I can program them yet. I added a footprint for the SST39LF010, which also has a 5V variant with the same pin-out.

Other footprints

I also added footprints so that I could connect the SD card module which I used on my 6502 computer, a piezo buzzer, and some electrolytic capacitors if needed.

Design lessons learned

Although my first attempt at making a PCB was a success, I learned a few things which I’m using here.

For that previous project, I found that machined-pin sockets are not very tolerant of used chips with bent legs, so I’ve used cheaper stamped pin sockets for this test board. I’ve also left spacing at the ends of each socketed IC, so that they can be pulled out more easily.

I made an effort to keep unrelated traces away from points which I need to solder, because I had some problems with this on my last board.

I also avoided creating one large expansion header, and instead exposed signals in places which are easy to route. For example, the I/O device select signals are exposed on a pin header right beside the chip which generates them.

PCB layout

The standard process is to make a schematic, then place components, then route traces.

For this test board, I instead chose the physical dimensions of the board first, and added components incrementally (particularly test points, expansion headers and alternative footprints) until I was making good use of the available space. KiCad has an item in the “Tools” menu to “Update PCB from Schematic”, which I used extensively.

Just to see how it would look, I also added the letters “65C816” to the back copper layer, which is visible in the top-right here.

I put a lot of time into labeling different parts of the board to help with debug/assembly, and used the 3D view to check that it wasn’t too crowded.

Before sending the files to a manufacturer, I printed it at 1:1 scale for a reality check.

Among other things, this confirmed that the ROM sockets had enough clearance from other components.

It also confirmed that either DIP-8 or QFN-packaged oscillators would fit.

Assembly

I ordered the boards from a manufacturer which I’ve used before. They are large-ish, lead-free 4-layer boards, but I’m not optimising for cost. I’ve left a lot of options in the board, so I’m hoping to make use of several of these PCBs in different configurations, depending on which direction this project goes.

I assembled the board incrementally, starting with the power LED. Most of the passive components on this board are 0603 (imperial) size surface-mount parts, and I’ve used footprints with long pads for hand soldering.

The first problem I found was with the reset button – I had switched to using a footprint with the correct pin spacing, but had assigned the pins incorrectly, so I needed to cut/bend some legs and install it rotated 90 degrees.

I next found a problem with the clock, where I had wired up the flip-flop incorrectly in the schematic – the /Q output should go to D. I could recover from this by cutting a trace and running a short wire. Of course the board is mirrored when flipped upside-down, so I cut the wrong trace first and needed to repair that too.

I added enough components to get the CPU to run NOP instructions from ROM, then built up to a running some test programs which I’ve blogged about here. The final mistake I discovered was a mix-up with some of the lines used for selecting I/O devices, which means that devices are not mapped to their intended addresses. I can work around this in software.

This is already an improvement over the breadboard prototype, because I can quickly swap the ROM chip without accidentally disconnecting anything.

The board looked fairly complete at this point, and the only major component missing was the UART chip. This was the part which did not work reliably in my prototype, so I was prepared to do some debugging here. Note that I’ve got orange test points all over the board to connect important signals to an oscilloscope, with a few black test points for GND connections. All of the 74-series chips are in the 74AC logic family, and I sourced 74HC versions as well in case I needed to switch any to see the difference.

However, I was able to run more or less the same test program I used before, and it now works reliably. This is captured through a Cypress FX2-based logic analyser using Sigrok.

It’s great that this works, but I don’t know for sure why this was so unreliable on my previous prototype. Several possible causes were eliminated through this process, since I used a new UART chip and freshly programmed address decode PLD, and eliminated a possible timing issue. On a PCB, I’m also able to make better electrical connections, and add a ground plane, which is an immediate advantage over breadboards.

The design as it stands

Now that I am back to having a working prototype, I’ll take this chance to post some updated schematics.

Just a note of caution: this is snapshot of a work-in-progress learning project. I’m absolutely aware that there are errors in here, and that the layout is quite messy. Still, I hope that this is useful to anybody else who is attempting to use this relatively obscure CPU.

The design no longer fits on one page, so I’ve split it into 3 sheets.

CPU

The CPU sheet contains the circuitry for de-multiplexing the data bus and bank address byte. All power, reset, and clock components are in here as well, along with pin headers for the address & data bus, and all those test points.

There are a lot of “just in case” components as well, such as pull-up resistors on the data bus, which I have not fitted.

Memory

The memory section is quite straightforward. I’ve got a PLD generating chip-selects for ROM, RAM0, or RAM1, with some extra components to add some flexibility.

I/O

In case I/O is selected, there are three possible choices: the 65C22 VIA, or one of the two UART interfaces. Most of the other components on this sheet are optional footprints or external ports. Note that the clock going into the UART is mis-labelled on this sheet.

Next steps

I’m going to spend some time using this board as a development platform for low-level software which targets the 65C816 CPU. I’ll most likely also use the emulator which I put together a few weeks ago to speed up development, since I’ve been able to confirm that the code I’m writing works on real hardware.

The basic functionality is now a lot more stable than what I had before, so this test board will allow me to prototype some different hardware options once I’ve got some simple text-based programs up and running.

The hardware design, software and emulator for this computer can be found in the GitHub repository for this project. I’m updating the repository as I make progress with this project, and the version used for this blog post is here.

65C816 computer – second prototype

Back in February, I blogged about my 65816 computer prototype on breadboards. I recently spent some time re-building this in an attempt to add some improvements.

The new prototype didn’t work particularly well, so I’ve left out quite a lot of detail from this update.

Power delivery

I soldered stranded wire to pin-headers to deliver a reliable ground and +5V power connection to each breadboard. This corrected some problems from my previous prototype, which used a long chain of unreliable connections.

Adding more RAM

I added a second 512 KiB RAM chip, and extended the address decoding scheme to provide a chip-select signal for it. The computer now uses the planned 20-bit address bus, though the wiring around these chips became very dense.

Adding a UART chip

I previously tested an NXP SC16C752 UART, and added it to this computer to provide text I/O. This is a small surface-mount chip, which I am connecting via a break-out board.

My previous prototype only supported one I/O device, so I also extended the address decoding scheme with a 74AC138 decoder.

Problems

I could get some simple test programs running, but text I/O did not work reliably at all. With my limited debugging tools (just a logic analyser), I was only able to conclude that data being written by the CPU was different to what was being received by the UART.

This seemed like it could be a timing or signal integrity issue, but I couldn’t be certain from looking at digital output with a low sample-rate. This image shows some control signals from when I was troubleshooting this.

Next steps

After hitting a dead-end, I made a list of possible causes, and decided to re-build this prototype on a debug-friendly PCB, and to source an oscilloscope for troubleshooting.

More on that in my next post!

Building an emulator for my 65C816 computer

I’ve been working on adding text-based input and output to my 65C816 computer prototype, but it’s not yet working reliably.

To unblock software development, I decided to put together an emulator, so that I can test my code while the hardware is out of action.

Choosing a base project

There were three main options I considered for running 65C816 programs on a modern computer,

  1. Write a new emulator from scratch
  2. Extract the useful parts of an open source Super Nintendo or Apple IIgs emulator and adapt them
  3. Find a 65C816 CPU emulator, and extend it.

I found a good candidate for the third option in Lib65816 by Francesco Rigoni, which is a C++ library for emulating a 65C816 CPU. The code is GPLv3 licensed, and I could see where I would need to make changes to match the design of my computer.

Process

After reading the code, I could see some places where I would need to modify the library, so I copied it in to a new folder, together with its logging dependency and sample program, and refactored it into one project.

I started by extending RAM up to 16 banks (1 megabyte), and mapping in a ROM device which serves bytes from a file. I also updated the handling of interrupt vectors, so that the CPU would retrieve them from the system bus.

Adding 65C22 support

My first test program was an example of preemptive multi-tasking from my previous blog post. At this point it ran, but did not switch between tasks, since that requires extra hardware support.

I implemented a minimal support for the 65C22 VIA used in my computer design, just enough to log changes to the outputs, and to fire interrupts from a timer.

The Lib65816 library does not support NMI interrupts, which are used in this test program. I added edge detection, and connected the interrupt output of the 65C22 to the CPU NMI input in code.

This screen capture shows the test program during switching between two tasks, with a different VIA port being accessed before and after the switch.

This gave me some confidence that the CPU emulation was good enough to develop on, so I moved on to the next test program.

Adding serial support

The second test program is also from an earlier blog post, and tests printing and reading characters from an NXP SC16C752 UART.

I added a very minimal emulation of the UART chip I’m using, suppressed all of the logging, and converted output to use the curses library. This last step enabled character-by-character I/O, instead of the default line-buffered I/O.

This screen capture shows the program running under emulation. It simply prints “Hello world”, then echoes back the next 3 characters that the user types.

Wrap-up

I didn’t plan to write an emulator for my custom computer, but I am currently debugging some tricky hardware problems, and this detour will allow me to continue to make progress in other areas while I try to find a solution.

I plan to keep the emulator up to date with hardware changes, since it will make it possible to demonstrate the system without any hardware access. It’s also easier to debug issues now that I can choose two different execution environments.

The hardware design, software and emulator for this computer are online at mike42/65816-computer. I would like to again acknowledge Francesco Rigoni’s work on Lib65816. I’m very grateful that he chose to release this under the GPL, since it allows me to re-use the code for my project.

Let’s implement preemptive multitasking

I am currently working on building a computer based on the 65C816 processor. This processor has a 16-bit stack pointer, which should make it far more capable than the earlier 65C02, at least in theory.

I wanted to check my understanding of how the stack works on this processor, so I tried to build the simplest possible implementation of preemptive multitasking in 65C816 assembly.

What I’m working with

I am working with a breadboard computer. It has no operating system, and no ability to load programs or start/stop processes.

To get code running, I am assembling it on a modern computer, then writing the resulting binary to an EEPROM chip.

To see the result of the code, I am using a logic analyser to watch some output pins on the 65C22 VIA. The 65C22 chip also has a timer, so I connected its interrupt output to the NMI input of the CPU for this test.

Test programs

I wrote two simple programs which infinitely loop. These programs use ca65 assembler syntax.

The 65C22 has two 8-bit ports, so I made each test program write a recognisable pattern to its own output port, so that it’s easy to see which one is running at any given time, and check that it is not disrupted by the task switching.

The first program writes to Port A of the 65C22, and alternates the value between 0000 0000, and 0000 0011.

.segment "CODE"
; Task 1
task_1_main:
  .a8                             ; use 8-bit accumulator and index registers
  .i8
  sep #%00110000
@repeat_1:
  lda #%00000000                  ; alternate between two values
  sta PORTA
  lda #%00000011
  sta PORTA
  jmp @repeat_1                   ; repeat forever

The second program writes to Port B of the 65C22. This uses a ror instruction to produce a pattern across all digits of the output port.

.segment "CODE"
; Task 2
task_2_main:
  .a8                             ; use 8-bit accumulator and index registers
  .i8
  sep #%00110000
  clc
  lda #%01010101                  ; grab a start value
@repeat_2:
  ror                             ; rotate right
  sta PORTB
  jmp @repeat_2                   ; repeat forever

Context switching

I’m implementing round-robin scheduling between two processes.

My basic plan was to use a regular interrupt routine to save context, switch the stack pointer to the other process, restore context for the next process, then return from the interrupt.

.segment "CODE"
nmi:
  .a16
  .i16
  rep #%00110000
  ; Save task context to stack
  pha                             ; Push A, X, Y
  phx
  phy
  phb                             ; Push data bank, direct register
  phd

  ; swap stack pointer so that we will restore the other task
  tsc                             ; transfer current stack pointer to memory
  sta temp
  lda next_task_sp                ; load next stack pointer from memory
  tcs
  lda temp                        ; previous task is up next
  sta next_task_sp

  ; Clear interrut
  .a8
  .i8
  sep #%00110000                  ; save X only (assumes it is 8 bits..)
  ldx T1C_L                       ; Clear the interrupt, side-effect of reading

  .a16
  .i16
  rep #%00110000
  ; Restore process context from stack, reverse order
  pld                             ; Pull direct register, data bank
  plb
  ply                             ; Pull Y, X, A
  plx
  pla
  rti

Setup process

The above switch works while the two processes are up and running, that takes some setting up. Firstly, the two variables need to be referenced somewhere.

.segment "BSS"
next_task_sp: .res 2              ; Stack pointer of whichever task is not currently running
temp: .res 2

The boot-up routine then sets everything up. It’s broken up a bit here. First is the CPU initialisation, since the 65C816 will boot to emulation mode.

.segment "CODE"
reset:
  clc                             ; switch to native mode
  xce

The second step is to start a task in the background. This involves pushing appropriate values to the stack, so that when we context switch, “Task 2” will start executing from the top.

  ; Save context as if we are at task_2_main, so we can switch to it later.
  .a16                            ; use 16-bit accumulator and index registers
  .i16
  rep #%00110000
  lda #$3000                      ; set up stack, direct page at $3000
  tcs
  tcd
  ; emulate what is pushed to the stack before NMI is called: program bank, program counter, processor status register
  phk                             ; program bank register, same as current code, will be 0 here.
  pea task_2_main                 ; 16 bit program counter for the start of this task
  php                             ; processor status register
  ; match what we push to the stack in the nmi routine
  lda #0
  tax
  tay
  pha                             ; Push A, X, Y
  phx
  phy
  phb                             ; Push data bank, direct register
  phd
  tsc                             ; save stack pointer to next_task_sp
  sta next_task_sp

  lda #$2000                      ; set up stack, direct page at $2000 for task_1_main
  tcs
  tcd

The nex step is to set up a timer, so that the interrupt routine will fire regularly. This causes interrupts to occur ~28 times per second.

  ; Set up the interrupt timer
  .a8                             ; use 8-bit accumulator and index registers
  .i8
  sep #%00110000
  lda #%11111111                  ; set all VIA pins to output
  sta DDRA
  sta DDRB
  ; set up timer 1
  lda #%01000000                  ; set ACR. first two bits = 01 is continuous interrupts for T1
  sta ACR
  lda #%11000000                  ; enable VIA interrupt for T1
  sta IER
  ; set up a timer at ~65535 clock pulses.
  lda #$ff                        ; set T1 low-order counter
  sta T1C_L
  lda #$ff                        ; set T1 high-order counter
  sta T1C_H

Finally, “Task 1” can be started in the foreground.

  ; start running task 1
  jmp task_1_main

Linker configuration and boilerplate

This is the first time I’m blogging about code written in 65C816 native mode, so for the sake of completeness, I’ll also include the updated linker configuration. The most important change (compared with this blog post) to that 32 bytes are now set aside for interrupt vectors.

MEMORY {
    ZP:     start = $00,    size = $0100, type = rw, file = "";
    RAM:    start = $0100,  size = $7e00, type = rw, file = "";
    PRG:    start = $e000,  size = $2000, type = ro, file = %O, fill = yes, fillval = $00;
}

SEGMENTS {
    ZEROPAGE: load = ZP,  type = zp;
    BSS:      load = RAM, type = bss;
    CODE:     load = PRG, type = ro,  start = $e000;
    VECTORS:  load = PRG, type = ro,  start = $ffe0;
}

Note that this linker configuration is not quite complete, but it is good enough to get code running. The zero page is not relevant for the code I’ll be writing, so I might remove that at some point.

I also have definitions for the 65C22 I/O registers, which correspond to the location that it is mapped into RAM on my prototype computer.

PORTB = $c000
PORTA = $c001
DDRB = $c002
DDRA = $c003
T1C_L = $c004
T1C_H = $c005
ACR = $c00b
IFR = $c00b
IER = $c00e

The final piece of the puzzle then, are definitions for all those interrupt vectors.

.segment "CODE"
irq:
  rti

unused_interrupt:                 ; Probably make this into a crash.
  rti

.segment "VECTORS"
; native mode interrupt vectors
.word unused_interrupt            ; Reserved
.word unused_interrupt            ; Reserved
.word unused_interrupt            ; COP
.word unused_interrupt            ; BRK
.word unused_interrupt            ; Abort
.word nmi                         ; NMI
.word unused_interrupt            ; Reserved
.word irq                         ; IRQ

; emulation mode interrupt vectors
.word unused_interrupt            ; Reserved
.word unused_interrupt            ; Reserved
.word unused_interrupt            ; COP
.word unused_interrupt            ; Reserved
.word unused_interrupt            ; Abort
.word unused_interrupt            ; NMI
.word reset                       ; Reset
.word unused_interrupt            ; IRQ/BRK

The process for building this assembly code into a usable ROM are:

ca65 --cpu 65816 main.s
ld65 -o rom.bin -C system.cfg main.o

This outputs an 8KiB file. I need to pad this with zeroes up to 32KiB, which is the size of the ROM chip I am burning it to.

truncate -s 32768 rom.bin

Lastly, I write this to the ROM.

minipro -p AT28C256 -w rom.bin

Result

I connected a logic analyser to two wires from each output port, the NMI interrupt line, and the reset signal, and opened up sigrok.

This clearly shows the CPU switching between the two processes each time an interrupt is triggered.

Zooming in on the task cut-over, I can also see that the patterns on each port are different, as expected.

Of course this did not work the first time, and I spent quite a bit of time de-constructing and re-constructing the code to isolate bugs.

As result, I shipped some improvements to my 6502 assembly plugin for JetBrains IDE’s, which I have blogged about previously. The plugin will now provide suggestions for mnemonics. There is a project setting for 6502 vs 65C02 vs 65C816 mode, and it will only suggest mnemonics which are available for the CPU.

It also provides a weak warning for binary or hex numbers which are not 8, 16 or 24 bits, which would have saved me some time.

Lastly, it has optional checking for undefined/unused values. This helps catch problems in the editor, and allows me to identify unused code and variables.

Wrap-up

I already knew how multitasking works on a high-level, but it was quite interesting to implement it myself. I have been using this as a test program for my 65C816 computer, since it exercises RAM, ROM, I/O, and uses interrupts.

I don’t think I will be able to use this exact method for more complex programs, because it will break down a bit with multiple interrupt sources. After writing this code, I also found that it’s not common for modern systems to save register values to the stack when context-switching, and that a process control block is used instead.

The next step for my 65C816 computer will be the addition of an NXP UART, which I tested in an earlier blog post.

Interfacing an NXP UART to an 8-bit computer

I recently tested the SC16C752 UART from NXP with my 6502 computer, and I was able to use it to receive and send characters. This blog post shows the simple test program and circuit that I used.

Some background

I built a 65C02-based computer last year, which uses a serial connection for text I/O. I used the WDC 65C51N to provide a UART interface, and I found it to be quite limited.

I am now building a 16-bit computer, and I want to avoid the 65C51N, for two main reasons:

  • It runs at 5v, and I’m planning to convert the design to 3.3v later on. The other 65-series chips I’m using support a wide voltage range.
  • It can only store one byte at a time. I am planning to run at a higher baud rate, and the system will have multiple interrupt sources. It would take very careful programming to avoid losing bytes with a 65C51N in this setup.

I spent some time investigating alternative UART IC’s, and found some discussion on 6502.org about replacing the 6551 with an NXP industrial (SC26 or SC28 series) UART. At the time of writing, many of the hobbyist-friendly PLCC versions of these chips are listed as end of life, though I’ve since found pin and software-compatible versions of this made by MaxLinear (eg. the XR88C92).

For my uses though, I chose the NXP SC16C752. It has two channels, 64-byte receive and transmit buffers, and supports a wide voltage range. I don’t mind that it’s a different series to these standard chips, because I plan to write software from scratch. The only downside is the packaging, which is LQFP48 with 0.5mm pin pitch. I needed to solder this tiny chip to an adapter board in order to use it.

The test circuit

I wired this up to my 6502 computer project as shown in the diagram below. IO3 is a spare address decoding output, which maps the chip to address $8c00, while the rest of the signals connect to the 6502 CPU directly.

I used a 74AC00 and 74AC04 to generate the active-high reset and separate read/write signals (as suggested on the same thread linked before). I plan to use a PLD to generate a qualified write signals on my 65C816 computer build.

Note that there are separate chip-selects for each UART, and I’ve only connected one here. I have also not connected the interrupt output for this test, and have some unused inputs which are left floating.

Developing a test program

I started writing some code, and connected a logic analyser to the UART output. The early tests did not work well, and I now know that this is because I had not enabled the FIFOs, causing characters to be lost.

This screen capture shows an attempt to send the entire alphabet to the UART, but only the letters “A”, “B” and “M” appearing at the output.

I came up with this test program, which does 8-N-1 communication at 115,200 bits per second.

;
; Test program for the SC16C752B dual UART from NXP
;
.import sys_exit

; Assuming /CSA is connected to IO3
UART_BASE = $8c00

; UART registers
; Not included: FIFO ready register, Xon1 Xon2 words.
UART_RHR = UART_BASE        ; Receiver Holding Register (RHR) - R
UART_THR = UART_BASE        ; Transmit Holding Register (THR) - W
UART_IER = UART_BASE + 1    ; Interrupt Enable Register (IER) - R/W
UART_IIR = UART_BASE + 2    ; Interrupt Identification Register (IIR) - R
UART_FCR = UART_BASE + 2    ; FIFO Control Register (FCR) - W
UART_LCR = UART_BASE + 3    ; Line Control Register (LCR) - R/W
UART_MCR = UART_BASE + 4    ; Modem Control Register (MCR) - R/W
UART_LSR = UART_BASE + 5    ; Line Status Register (LSR) - R
UART_MSR = UART_BASE + 6    ; Modem Status Register (MSR) - R
UART_SPR = UART_BASE + 7    ; Scratchpad Register (SPR) - R/W
; Different meaning when LCR is logic 1
UART_DLL = UART_BASE        ; Divisor latch LSB - R/W
UART_DLM = UART_BASE + 1    ; Divisor latch MSB - R/W
; Different meaning when LCR is %1011 1111 ($bh).
UART_EFR = UART_BASE + 2    ; Enhanced Feature Register (EFR) - R/W
UART_TCR = UART_BASE + 6    ; Transmission Control Register (TCR) - R/W
UART_TLR = UART_BASE + 7    ; Trigger Level Register (TLR) - R/W

.segment "CODE"
main:
  lda #$80                  ; Enable divisor latches
  sta UART_LCR
  lda #1                    ; Set divisior to 1 - on a 1.8432 MHZ XTAL1, this gets 115200bps.
  sta UART_DLL
  lda #0
  sta UART_DLM
  lda #%00010111            ; Sets up 8-n-1
  sta UART_LCR
  lda #%00001111            ; Enable FIFO, set DMA mode 1
  sta UART_FCR
  jsr test_print
  jsr test_recv
  lda #0
  jmp sys_exit

test_print:                 ; Send test string to UART
  ldx #0
@test_char:
  lda test_string, X
  beq @test_done
  sta UART_THR
  inx
  jmp @test_char
@test_done:
  rts

test_string: .asciiz "Hello, world!"

test_recv:                  ; Receive three characters and echo them back
  jsr uart_recv_char
  sta UART_THR
  jsr uart_recv_char
  sta UART_THR
  jsr uart_recv_char
  sta UART_THR
  rts

uart_recv_char:             ; Receive next char to A register
  lda UART_LSR
  and #%00000001
  cmp #%00000001
  bne uart_recv_char
  lda UART_RHR
  rts

The program sends the text “Hello, world!”, then waits for input. The next three characters it receives are sent back to the user, and then the program terminates. The only thing in this test program which is specific to my 6502 computer is sys_exit, which a routine to transfer control back to the operating system.

It was very rewarding to see the fast, error-free text output on the logic analyser for the first time. This is a great improvement from the 65C51N.

I was also surprised at how short the delay is to echo back a character. It will be interesting to see how much the latency increases once there is some more complex software handling the input.

Wrap up and next steps

I plan to use the NXP SC16C752 for my 65C816 computer project, which will be controlled via text-based input and output.

I’m testing it on its own first, so that I can refer to a known-good setup if it doesn’t work. This is enough to get started, though there are some features which I wasn’t able to test yet, since I can’t use custom interrupt service routines on this setup.

65C816 computer – initial prototype

I am currently working on building a 65C816-based computer from scratch. I have some ideas for how I want to do this, but my first task is to put together a working prototype. I’ll be extending this in different ways to work towards the goals outlined in my last blog post.

This is about the simplest computer I can come up with which uses the 65C816 processor, but there is still quite a lot to go though.

Parts

All of the integrated circuits on this prototype are through-hole packages, suitable for use on a breadboard.

  • WDC W65C816S CPU
  • WDC W65C22S VIA I/O chip.
  • Alliance AS6C4008-55PCN 512k x 8 55ns SRAM
  • Atmel AT28C256 32k x 8 EEPROM
  • Atmel ATF22LV10 PLD for address decoding
  • 74AC04, 74AC245, 74AC573 for handling the multiplexed data bus and bank address bits
  • 1.8432 MHz oscillator
  • DS1813-5+ supervisory circuit

I also used the following equipment to program the chips, and view the result of the test program:

  • TL-866II+ EEPROM programmer
  • 8-channel Cypress FX2-based logic analyser

I’m working on Linux, and also use the following software tools:

  • ca65 – for assembling code for the CPU
  • minipro – for programming the EEPROM and PLD.
  • galette – for assembling the logic definitions for the PLD.
  • sigrok – for working with the logic analyser

Bank address latching

The 65C816 has a 24-bit address bus, and the top 8 bits are multiplexed onto the data bus. This is not a big problem, but it does mean that I need some extra glue logic compared to a plain 6502 system. A diagram in the 65C816 data sheet shows a suggestion for how to do this, and I’m starting with that implementation.

I used a 74AC04 to invert the clock, 74AC245 bus transceiver for the data bus, and a 74AC573 latch for the bank address (upper 8 bits of the address bus). This first page also shows the DS1813-5+ supervisory circuit on the reset line, which resets all of the components on power-up.

I’m aware that I should be generating a two-phase clock (there is some discussion about that here), though I can’t see any problems arising from the the delay on the inverted clock on this particular circuit.

Memory map and address decoding

The memory map for this computer is very simple. All of this will change, but I did need to start with something to get some code running.

Only the lower 19 bits of the address space are properly decoded, and mapped to one of three chips.

Address Maps to
010000 – 07FFFF RAM – 448 KiB
00E000 – 00FFFF ROM – 8 KiB
00C000 – 00DFFF I/O – 8 KiB
000000 – 00BFFF RAM – 48 KiB

I used a programmable logic device (PLD) to implement this. It’s easy enough to do this with discrete gates, but a PLD will make it much easier to keep chip count and propagation delays under control as I add more features. I’ve written about programming PLD’s in previous blog posts (here, here and here), and I plan to use them to implement several features.

The particular part I am programming here is an ATF22LV10. These are still produced the time of writing, and operate at either 3.3 volts or 5 volts, which is useful for this project. They can be also programmed with the common TL-866II+ chip programmer, which I also use for programming EEPROMs.

The definitions for decoding this are below. I called this file address_decode.pld.

GAL22V10
AddrDecode

Clock  RW     NC     NC     A19    A18    A17    A16    A15    A14    A13    GND
NC     /RAMCS /ROMCS /IOCS  /RD    /WR    NC     NC     NC     NC     NC     VCC

; ROM is the top 8 KiB of bank 0.
; prefix is 0000 111xxxxx xxxxxxxx
ROMCS = /A19*/A18*/A17*/A16*A15*A14*A13

; IO addresses are mapped below ROM in bank 0.
; prefix is 0000 110xxxxx xxxxxxxx
IOCS = /A19*/A18*/A17*/A16*A15*A14*/A13

; RAM is selected for any address does *not* match 0000 11xxxxxx xxxxxxxx
RAMCS = A19 + A18 + A17 + A16 + /A15 + /A14

; Qualify writes with clock
RD = RW
WR = Clock*/RW

DESCRIPTION

Address decode logic for 65C816 computer.

ROM is top 8K of bank 0, I/O is 8k below that, everything else is RAM.

I assembled this with galette:

galette address_decode.pld

This generates a .jed file, which can be used to program the chip. The ATF22LV10 is not on the compatibility list for my programmer, but it works just fine if I identify it as an ATF22V10CQZ.

minipro -p ATF22V10CQZ -w address_decode.jed

Firmware and testing

I came up with a simple test program which would require working ROM, RAM, and I/O. The CPU boots to 6502 emulation mode, and this test program does not access memory outside bank 0.

I created a linker configuration file which mostly reflects the system’s memory map, and called it system.cfg.

MEMORY {
    ZP:     start = $00,    size = $0100, type = rw, file = "";
    RAM:    start = $0100,  size = $7e00, type = rw, file = "";
    PRG:    start = $e000,  size = $2000, type = ro, file = %O, fill = yes, fillval = $00;
}

SEGMENTS {
    ZEROPAGE: load = ZP,  type = zp;
    BSS:      load = RAM, type = bss;
    CODE:     load = PRG, type = ro,  start = $e000;
    VECTORS:  load = PRG, type = ro,  start = $fffa;
}

I then wrote this test program, main.s. The program runs from ROM, and writes a hard-coded value to one of the output ports. It then stores a 0 in RAM, then increments it and stores that to an output port in an infinite loop, which will show a counting pattern if RAM can be read and written.

PORTB = $c000
DDRB = $c002

SOME_ADDRESS_IN_RAM = $beef

.segment "CODE"

; Write 42 to VIA port B
reset:
  lda #%11111111 ; Set all pins to output
  sta DDRB

  lda #42
  sta PORTB

; Start at 0, and increment a value in RAM
  lda #00
  sta SOME_ADDRESS_IN_RAM

loop:
  inc SOME_ADDRESS_IN_RAM
  lda SOME_ADDRESS_IN_RAM
  sta PORTB         ; Output the value
  jmp loop

nmi:
  rti

irq:
  rti

.segment "VECTORS"
.word nmi
.word reset
.word irq

I assembled this with ca65, then linked it with ld65. As far as the assembler knows, it is generating code for a 6502 CPU, not a 65C816. I am not using any advanced features, so this should be fine for this test.

ca65 main.s
ld65 -o rom.bin -C system.cfg main.o

This outputs an 8 KiB binary file, which is the amount of ROM space mapped in the computer’s memory. The chip is actually 32 KiB though, so I filled the rest with zeroes.

truncate -s 32768 rom.bin

I then wrote the program to the EEPROM using minipro.

minipro -p AT28C256 -w rom.bin

I ran this program, with a logic analyser connected to port B of the 65C22 VIA. The capture below shows the initial hard-coded number, followed by the counting pattern, which indicates that everything is working as expected.

Wrap-up

I’m starting simple here, so that I have a working base system to modify. Everything seems to be working fine on breadboards, so I’ll proceed with that for now. I may still need to re-build everything with shorter wires, since this is getting quite difficult to work with.

It is also worth noting that most of the components here should run at 3.3 volts, which is where this project is eventually headed. Only the clock, reset IC and EEPROM would need to be substituted at the moment.

This blog post is only a snapshot of the project. You can check the current status on GitHub here.

A first look at the 65C816 processor

The 65C816 is an interesting processor from the 1980’s. I recently wired one up on a breadboard, and I’ll be blogging here about my attempts to build something useful with it.

How I got here

I spent a couple of months last year designing and building a computer based on the 65C02, an old 8 bit processor. I wanted to create a hardware platform for running 6502 assembly programs. This was successful, and I learned a lot about how computers work while building and programming it.

This has got me interested in the 65C816, which is a later extension of the same architecture. The only well-known systems which used it were the Super Nintendo and Apple IIGS. From a programmer’s point of view, this chip seems far more capable than its pre-decessor, though developer tools are a bit scarce.

New project goals

My plan is to build a modern computer which uses the 65C816 CPU, so that I can really learn how it works.

Over a few revisions, I am aiming to build up to a system with a serial connection for simple text I/O, a modest clock speed, and 1 megabyte of static RAM.

I’ve researched some existing designs, and settled on two simple constraints to give this project its own character.

  1. Use only in-production parts.
  2. Don’t use an FPGA or microcontroller to bootstrap the system.

This will ensure that I am learning what it takes to build a computer around this processor, and not just offloading the tricky parts to more powerful device with better tooling. I also hope this increases the accessibility of the design, since anybody could build it without needing to obtain obscure retro parts.

I am not avoiding programmable logic entirely though. Where classic designs often used custom chips, I will be using ATF22V10 PLD’s. I will also leave the door open adding video output via a microcontroller-based terminal emulator in the future.

Lastly, I’m selecting parts with a view to converting everything from 5 volts to 3.3 volts part-way through the project, which will open up many possibilities.

Other than the Apple IIGS and Super Nintendo, there are three hobbyist designs which I’m using for inspiration:

A quick note about open source

It’s never been a stated goal of this blog, but wherever possible, I use open source tools, and produce open source software. I expect that to be difficult for this project. The 65C816 not widely used, and most hobbyist code for the CPU is licensed as source-available freeware, with restrictions on commercial use.

I would like to be able to release my code under an OSI-approved open source license, and have the option of incorporating copyleft code later on. Unfortunately for me, it seems I will need to write a lot of low-level code from scratch to get this kind of licensing certainty. I will be working primarily in assembly language, though C code would be very useful, so I will certainly be spending some time investigating compiler options along the way.

A smoke test

This is the first iteration of the design, which will live for now on a series of solderless breadboards.

This is simply a 65C816 processor, being fed a hard-coded NOP (no-operation) command. When it runs, the lights show the address bus counting upwards, as the CPU program counter increases.

The RDY, ABORTB, and BE inputs are connected to +5v through resistors, while RESB, NMIB, IRQB are connected to +5v directly. The NOP opcode is fed to the CPU over the data bus, by making the pattern 11101010 (0xEA) with 1k resistors. The schematic for this is below.

The clock is an LM555 timer, fed through a 74AC04 inverter, because the rise and fall times on a 555 are not fast enough. The clock speed is 6.9Hz (that is not a typo), since R1 is 10 kΩ, and R2 is 100 kΩ, and the capacitor is 1 µF.

Next steps

This is going to be a long project, which I will develop incrementally. The next step will be adding ROM, RAM, glue logic and a 65C22 I/O chip.

I am also testing alternatives to the 65C51N UART chip, which I found quite limited when I used it for my last project.

Rendering my 6502 computer project in Blender

For the past couple of months, I’ve been working on building a 6502-based computer from scratch.

In a previous blog post, I wrote about designing a 3D-printed enclosure for this project in Blender.

During this build, I needed to wait for some parts, so I had some time to see what I could do with the 3D models of the case and circuit board. I ended up animating the assembly process, and learned a couple of tricks along the way.

Splitting components and PCB

I already had a 3D model of the circuit board and components. This was initially exported from KiCad, in STEP format. I then used FreeCAD to convert it from STEP to STL, which I imported into Blender for designing the case. I also repeated this export with some different footprints, since I needed sockets and microchips.

It is a challenge to make this look good, because the circuit board and all components come in as one object, and have lost all of the detail from the KiCad 3D view.

I spent a lot of time splitting objects, then selecting faces so that I could assign different materials. I’m using a free BlenderKit account for materials, and using procedural materials where possible.

I also modeled the DC barrel jack and slide switches, which were the first of many components which were missing from the original KiCad export. Still, something was missing.

Adding missing details

I thought that the PCB in the previous image was too bare. The real PCB uses plated through-holes, and almost every hole in a real PCB has a shiny solder pad around it.

To fix this, I exported the front solder-mask layer from KiCad as an SVG, which looks something like this:

I imported this SVG into Blender, scaled it to fit the PCB, extruded it, then went through a series of boolean modifiers. This resulted in a new object which contained every part of the PCB which was not covered in solder mask. I assigned this a shiny metal material.

After this was applied, I subtracted this from the original PCB model, giving the parts of the PCB which are covered in solder mask.

Showing both of these at the same time, I have a PCB with solder pads and plated through-holes.

Importing the silkscreen

This still was not right. On the real PCB, everything is labelled.

I exported the front silkscreen layer from KiCad as an SVG. This layer looks something like this:

I then used InkScape to change this to white text on a transparent background, before finally exporting it as a PNG. I mixed this into the procedural texture which I was using for the PCB, using the transparency as the mix factor. As a final touch, I also exported the front copper layer, and used this as the displacement input for the final material. The nodes are shown in the screen capture below.

This allows me to render a PCB which looks a lot like the real thing.

UV unwrapping everything

In the earlier render, all of the chips had clear surfaces, while real chips are covered with text and logos.

I created small transparent/brown PNG images for each chip, using Inkscape, then UV unwrapped the chips. I mixed the logo into the texture, much the same as I did for the PCB.

I also UV unwrapped each resistor, in order to paint on the correct colour codes. This was perhaps the most egregious waste of time in this whole thing.

Adding final parts

There were plenty more parts which I couldn’t export from KiCad, and needed to model from scratch. I used the bolt factory add-on to create screws, nuts, and hex standoffs (both a screw and a nut, combined).

I also added power and reset buttons (which are partly threaded), plus the speaker. I decided not to add wires, since it would be beyond my skill at the moment.

When everything is displayed at once, a lot of this detail is hidden behind other objects.

Animating

With that last render, I decided to see if I could make an animation of all of the parts coming together. I’ve never animated anything in Blender, so I had a lot to learn.

I added a tracking constraint to the camera, which is panning around an invisible object on the table, and animated everything using key-framed movement. Parts are raised out of view with slightly randomised rotation/location before falling into place.

Rendering

I exported the animation from Blender as 360 separate 1920 x 1080 images, to be played back at 30 frames per second. I would normally use ffmpeg to convert an image sequence into a video, but I wanted to try adding an audio track.

I imported the rendered animation into Kdenlive, which can process image sequences (the button is “Add clip” / “Import as sequence”), fetched a track from the Free Music Archive (Scanglobe – Current. CC BY-NC), and rendered the project.

Wrap-up

I wanted to get a textured, 3D model of this entire project so that I could easily illustrate documentation and project pages. I definitely achieved this, though working with the exported circuit board and components in Blender was tedious.

The version of Blender used here is 2.93, and the files were exported from KiCad 5.1.10.

Assembling my 6502 computer

I recently finished assembling my 6502 computer, so that the whole project is housed in a 3D-printed case.

This is something that should be easy. I already blogged about making the case, and making the circuit board. All that is left is to put the board in the case, right? Well thanks to some mistakes on my part, things are not quite so simple.

More parts

I needed quite a few parts to finish this project, including a power button, reset button, hex standoffs, short screws, and rubber feet. Since this is the first time I’m hitting this stage of a project, I also needed to buy a Du Pont connector kit, stranded wire, heat-shrink tube, and glue.

I have been powering the project from a USB/serial adapter, so I also bought a new one to install permanently. A full parts list is available in the GitHub repository for this project.

I also needed to widen the hole for the reset button due to a mix-up. The specific part I used requires a 7mm hole, while I had designed the case for a button with a 6mm hole.

Making connections

The first step was to modify the board so that it would be compatible with the new UART, which involved de-soldering the female pin headers which I installed previously, and replacing them with male pin headers. I used only a soldering iron, solder wick, and flux for de-soldering, and it was not a fun process. The board survived, so I’ll call it a success.

Next, I soldered wires to the underside of the board for the power button, reset button, and speaker to connect. Each of these is a pair of wires, soldered at one end, and a 2-pin female Du Pont connector on the other.

I attached corresponding connectors on the buttons and speaker. The power button also has an in-built LED, and there are plenty of places to power this from the board’s expansion headers. I added a 2-pin female Du Pont connector, plus a resistor, neatly hidden by heat-shrink.

I checked everything with a multimeter before powering it on. The UART pins were connected correctly to the 65C51N chip and ground. No continuity between the board’s power and the external power unless the power button is pressed, no continuity between the reset line and ground unless the reset button is pressed, and so on.

Troubleshooting

When I powered on the computer, nothing came up on the terminal, which means that I broke something. I tested some voltages, and found the computer was stuck in reset.

The reset signal on the 6502 is active-low, and I’ve added a supervisory IC. Any time the reset signal is low, or the power drops below a minimum voltage, this IC holds it low for 150ms before returning it to +5v. I was confident that the reset buttons and power voltage were fine, so I suspected that one one of the other chips could be causing the problem.

Everything is socketed, so I removed the 65C51N and tested again. This chip is closest to the de-soldering work I was doing earlier, so I thought I may have damaged it.

There was no change, so I started removing other chips, and found that after removing the CPU, the computer was no longer getting stuck in reset.

Following that lead, I found that one of the data bus lines, D4, was connected to the reset line. This would cause activity on the data bus to reset the computer, which would cause this problem.

A quick look at the board in KiCad showed that at one of the points I had attached the reset button, D4 is 0.5mm away from the reset line.

I de-soldered that wire and found a tiny break in the solder mask on D4. This was easy enough to fix, I simply attached the wire for the external reset button elsewhere.

I also bent the CPU pins when re-installing, but after fixing that, I was back in business.

SD card changes

I have been writing some test programs which use a microSD card via one of the 65C22 I/O ports, and wanted to fit this in the case as well.

I swapped the Adafruit ADA254 microSD break-out for the SparkFun DEV-13743. It is slightly smaller, includes pull-up resistors, and still provides level-shifting.

I made a small adapter by attaching a female pin header to a Du Pont housing with superglue. I couldn’t get the “card detect” feature to work, so I wired it up differently to my previous blog post, and updated the code to match. Details of the new wiring is on GitHub.

I also added some masking tape when I installed this to prevent any short circuits. The metal on the outside of the microSD socket is connected to ground, and it could touch some IC pins.

Quick lessons

This is only a learning project for me, so I am trying to take some lessons away from each part of it.

The high-level lessons would be to plan for external connections before building the PCB, and to have all parts on hand before designing the case. That would have saved a lot of re-work, though adapting a design without starting from scratch is a valuable skill in itself.

If I were starting from scratch, I would add pin headers to the board for connecting the power and reset buttons instead of including buttons on the board, and remove the (now unused) DC barrel jack as a power input. I would also make the board larger, and the case taller, so that everything is less cramped. It takes a lot of force to remove chips from machined pin sockets, and the board is too compact to get a screwdriver under the chips.

SD card support is also a bit of a hack, and could be built-in to the board with some effort.

Final results

I now have a fully self-contained 8 bit computer, and it looks more like a finished project than an in-progress experiment. It boots to BASIC, or with the flip of a switch, launches a ROM which allows me to load machine code assembled on my PC.

With the lid removed, I can access the 40-pin expansion header, and one spare I/O port.

The hardware side of this project is now done, and I have a platform for testing any 6502 assembly code on real hardware.

Adding an SD card reader to my 6502 computer

I recently attempted to add SD card support to my home-built 6502 computer.

This was successful, but more challenging than I expected. This blog post is just a few things that I learned along the way.

Background

SD cards are a popular way to add storage to electronics projects. In my case, I will be using the SD card in SPI mode. My home-built computer has an expansion port with some GPIO pins, which should be sufficient for this.

My basic plan was to:

  • write some test data to an SD card from a modern computer
  • interface the SD card to my home-built 6502 computer
  • write a 6502 assembly program to print out the test data

Since this is a learning project, I started from scratch, and avoided reading any existing implementations of SPI or SD cards before jumping in.

Test data

I started this project by generating a small test file, repeating each character in the alphabet 512 times.

echo abcdefghijklmnopqrstuvwxyz | fold -w1 | while read line; do yes $line | head -n512 ; done | tr -d "\n" > disk.img

It takes a one-line shell command to write this image to an SD card on all good operating systems, or a few clicks in the disk imaging utility.

dd if=disk.img of=/dev/my_sd_card

My laptop has an SD card slot, but I also have Windows on it at the moment, which turned this into a far larger problem than I could have imagined.

Driver support for this particular SD card reader was dropped in Windows 10, there are no built-in tools for imaging disks on Windows, and I had to try multiple tools before I could find one which would write a raw disk image (one which does not contain a valid filesystem).

In retrospect, this was the first of many lessons about not having the right tools for this project.

Physical interface

I connected an SD card module to my 6502 computer, via a breadboard. I used a module which could level-shift, because my computer runs at 5V, where SD cards usually run at 3.3V. This particular module is an Adafruit ADA254.

I also added pull-up resistors to all of the data lines, after noticing that there were several sitations where they would float. The wiring I used is:

The full schematic for my 6502 computer is on this blog post, while the schematic for the SD card module is online here.

Software: Part 1

With some test data and a hardware interface, I got to the third step, which was to read data from the SD card. I was completely unprepared for how tricky it is to do this from scratch, without the right experience or debugging tools.

As a quick re-cap, my development environment is still very limited: I am writing 6502 assembly on a modern computer, and I have the ability to send the assembled program to my home-built 6502 computer over a serial connection.

I started by using lecture notes from the University at Buffalo’s EE-379 course, available online here. This describes SPI, SD card command structure, and the sequence to initialise a card.

The two protocols I needed to implement are SPI, which transfers the bits, plus the SD card commands which run over that. I had to write quite a lot of code with no feedback, because it takes several commands before SD cards are ready to work with in SPI mode, and the exact details vary depending on the generation of card (SDHC vs SDXC, etc).

After days of debugging, I was unable to get a response from the SD card. With few other options, I started filling my code with the assembly-language equivalent of print statements, and found that every byte from the SD card was the default FF. This means ‘no data’ in this context.

Print statements also slowed the program down to a crawl, which allowed me to use a pizeo buzzer to listen to each line. This clicks each time a signal changes between 0 and 1, so I was able to check that each line had the expected activity. I found that the SD card was sending responses, but that the computer was not receiving them, because they were not connected to eachother on the breadboard.

Software: Part 2

After connecting the card correctly, I could see responses for the first time. The format here is byte sent / byte received, with line breaks between commands.

#
SD card detected
40/7f 00/ff 00/ff 00/ff 00/ff 95/ff ff/ff ff/01 ff/ff
48/7f 00/ff 00/ff 01/ff aa/ff 0f/ff ff/ff ff/09 ff/ff ff/ff ff/ff ff/ff
7a/7f 00/ff 00/ff 00/ff 00/ff 75/ff ff/ff ff/01 ff/00 ff/ff ff/80 ff/00 ff/ff ff/ff
#

The correct initialisation sequence depends on the type of SD card, and I spent a lot of time going through each possibility, looking for one one which worked. When this failed, I found this summary online, which includes more information about decoding the responses: Secure Digital (SD) Card Spec and Info.

As it turned out, I was not sending the correct CRC for CMD8, and a CRC error is indicated by the 09 (binary 0000 1001) response to the second command above. Each bit in the response has a different meaning, and I had mistaken this for indicating an unsupported command, which is expected for some card types.

It’s a good thing that I had this problem, because searching for the CRC algorithm turned up this collection of AVR C tutorials, which includes a 4-part series on implementing SD card support.

I noticed that all of the command examples were wrapped with code like this:

    // assert chip select
    SPI_transfer(0xFF);
    CS_ENABLE();
    SPI_transfer(0xFF);
    // ... code to send a command...
    // deassert chip select
    SPI_transfer(0xFF);
    CS_DISABLE();
    SPI_transfer(0xFF);

Later on, I had a problem where responses were sometimes mis-aligned by one bit. When I also tried sending empty bytes between commands, while the SD card is not selected, these problems disappeared entirely.

Software: Part 3

With the initialisation code complete, I could finally request data from the SD card.

Data on an SD card is arranged in 512-byte blocks by default. I wrote a routine to read one block of data from the SD card into RAM, plus a routine to print sections of RAM.

The result was a program which prints the first kilobyte of the SD card, which is the letter a 512 times, followed by the letter b 512 times.

This screen capture shows the output of a program matching a hexdump of the test file, which is exactly what I was aiming for.

The full code is too long to include in a blog post, but can be found in the repository for this project, mike42/6502-computer on GitHub.

Some lessons

While I was able to read data from an SD card, this took a long time, and there were several points where I nearly hit a dead-end.

I couldn’t see what was happening at a hardware level, and I couldn’t compare my setup with one that worked, because I had never done anything similar before. Spot-the-difference is a powerful debugging tool, and by jumping straight into 6502 assembly, while avoiding existing 6502 implementations, I definitely made things difficult for myself.

With that in mind, there are several ways I could have made this easier:

  • Better equipment. A logic analyser, an SD card writer, and spare SD cards would have been useful for testing.
  • Building somewhere less-constrained first. I could have implemented this first on a Raspberry Pi, then ported the result to 6502 assembly. It would then be possible to verify my hardware setup by running existing code, while still getting the learning experience from writing my implementation from scratch. Note that this would have required a different SD card break-out board.
  • Building something simpler first. For example, Ben Eater released a a YouTube video around the time I was working on this, where he reads from an SPI temperature sensor on a 6502 computer. It would be much easier to get started if I had used any other SPI device on a 6502 first.

The next step in this project is to load programs from an SD card, which would require me to implement a filesystem. This is significantly more complex than what I’ve done here, and I know not to approach it the same way.

More equipment

I picked up a logic analyser, USB SD card reader, alternative SD card module, and a spare SD card.

The logic analyser is compatible with sigrok, which is open source. This is the first time I’ve used this software, and it can decode SD card commands running over SPI, among other things.

The USB SD card reader will allow me to write disk images directly from my development environment, instead of copying them over to a laptop. I do most of my development from a virtual machine, and USB peripherals are easy to pass though.

The alternative SD card module is from SparkFun. Compared with the module I was writing in this blog post, it has a smaller footprint, and includes the pull-up resistors. I’m hoping I can use this to fit an SD card into the 3D-printed enclosure for this project.