6502 + FPGA
Well, I’ve gone and made another 6502 project.
Instead of doing it on a breadboard or making a custom circuit board, I decided this time I would utilize an FPGA for everything except the actual CPU. Having everything in the FPGA means that things like IO and the boot ROM can be changed all in the computer.
To hook up the processor to the FPGA, I put together a little adapter board which consists of a 40 pin socket and a 40 pin female pin header. This then plugs directly into the expansion header of the DE10-lite, although the headers are the same on my DE0 so it should work there too.
The basic setup is as usual, 32k of ram in the bottom half, and 32k of rom in the upper half. There is one quirk though, and that is that the 6502 is designed for non-synchronous memory, while the memory inside the fpga is synchrnous. The way around this was to just half the speed of the cpu clock compared to the memory clock. The 6502 asserts addresses/data on the falling edge, then reads data on the next falling edge. By doubling the memory clock, the falling edge of the cpu clock is a rising edge of the memory clock, so the memory will get two rising edges before the cpu will move on.
The biggest advantage of this project for me is the IO though, the worst part of building the previous systems was address decoding and trying to add new devices. With the FPGA though, address decoding is only a single line of code.
The board includes 6 7-segment displays, which were the first things that I added. The verilog code is pretty simple:
This is the address decoding logic, as you can see it is super simple.
The code here is also pretty simple. _data
are the internal registers, of
which there are 4 of. The first 3 represent the 3 pairs of digits, and the
fourth one is a mask. If we try and write to the registers, it writes to the
correct one.
We then create the hex drivers using the hex values, and then mask them. Note the the mas is using bitwise or, and the mask bit is inverted since the hex digits are active low, so we want to set all of the bits to high to turn it off.
The software to drive them is written in assembly, since it will be faster than C.
The first function takes in a byte to display and the index to display at:
_hex_set_8:
phx
cmp #$3 ; If idx >= 3 then fail
bcc @1
plx
lda #$1
rts
@1: tax ; Move idx into x
jsr popa ; put val into a
sta SEVEN_SEG,x ; write to val
lda #$0
plx
rts
As I discussed in my previous post about cc65 calling conventions, the rightmost argument is
present in the primary register, which would be A
for an 8 bit value, and the
rest of the arguments are on the parameter stack. The first thing is we check
to see if the index is greater than or equal to 3, which would be an invalid
index. If it is valid, then we write the value to SEVEN_SEG,x which would be
the address of the display plus the index.
The next one is even simpler:
_hex_set_16:
sta SEVEN_SEG
stx SEVEN_SEG+1
lda #$0
rts
16 bit values are passed with the low byte in A
and the high byte in X
, so
we just write the values in A and X to the display.
_hex_set_24:
sta SEVEN_SEG
stx SEVEN_SEG+1
lda sreg
sta SEVEN_SEG+2
lda #$0
rts
This one is pretty similar, but shows off how 32 bit arguments are passed. The
low 16 bits are present in A/X
as before, but the high 16 bits are present in
a zero-page location called sreg
. Since there are only 6 digits, we don’t
care about the high 8 bits which is why we don’t read sreg+1
as you might
otherwise.
_hex_enable:
sta SEVEN_SEG+3
rts
And finally the simplest of them all, the mask function just writes the value
in A
, no nonsense.
That is all that I have done so far, but I hope to eventually add USB through the MAX3421E that we got for 385. I have a bunch of code for it, but it needs to be rewritten in the style that cc65 expects (that is, not C99).
I am having a lot of fun going back to my roots though.
View on my GitLab