Old 6502 Device
March 27, 2023
Categories: retro computing hardware reverse engineering
Tags: commodore 6502 gpib
YouTube Rabbit Hole
I’ve got a decent amount of YouTube subscriptions that are dedicated to retro computing, and one of my favorites is Adrian’s Digital Basement. He does a pretty good job of getting into the details without you needing to be an expert to understand, and sometimes comes across some very interesting bits of hardware. Now the very clip-bait-y title aside, his video I almost threw away this historical chip grabbed my attention not just for the fact he found a very early 6502 (sorry spoilers) but the tiny ROM had to be interesting for what the device did.
From what Adrian saw, the device seemed to be some sort of parallel to serial converter. As luck would have it, a commenter found a magazine article describing the hardware. Jumping back to 1979 the September issue of Kilobaud Microcomputing and turn to page 100 and we get an article called Make PET Hard Copy Easy. So this device isn’t just any parallel to serial device, it’s an IEEE-488 HPIB/GPIB device!
So now I had to take a peek at the code, because GPIB can be complicated but somehow this device only uses a couple logic gates, a CPU, ROM, and UART. Taking the 256 bytes from the ROM dump, we get a file that looks like this kilobaud-gpib-rs232.bin and I can run that through the disassembler from the cc65 project aptly named da65. If you want to follow along, the complete annotated disassembly is here kilobaud-gpib-rs232.asm.
Which End Is Up?
Probably a good place to start isn’t the code, but instead the schematic. This will be handy so we can get an idea of what the memory addresses are going to be for the couple of chips we have. Something that stands out is that not all of the address lines are connected, the upper 5 bits are just ignored1. Then the next 3 upper bits get combined with the clock to decide what we are selecting. Our memory map should look something like this:
- $x700 - $x700 : ROM
- $x600 - $x6FF : UART SWE
- $x500 - $x5FF : GPIB EOI, ATN, DAV + Hardware Switch
- $x300 - $x3FF : GPIB Data Bus
- $x200 - $x2FF : GPIB NDAC & NRFD
- $x000 - $x0FF : UART TDS
Ok great, especially since the ROM uses just the start address for doing anything other than reading from the ROM. So next checking out the UART and GPIB standards will help understand the rest. (The hardware switch is for 72 or 80 column mode)
For the UART, this uses the AY-5-1013 which seems weird today with independent transmit and receive sides, but for this we only use the transmit side. At $x600 we have SWE, which is read only and gives us the status information. Then $x000 is TDS, which is write only for the byte we want to transmit.
On the GPIB side, we have a bunch of status signals broken into 2 groups. NDAC and NRFD are used by the device to talk with the host computer. While EOI, ATN and DAV are used by the host computer to talk with the device. Looking at a single byte with the most significant bit on the left, the bits map as follows:
7 0
DAV, Hardware Switch, ATN, EOI |D S A E x x x x|
NDAC & NRFD |R D x x x x x x|
Pass Go
With this in mind, we can start to tease apart what is going on with the code. My reference along the way for the GPIB interface was Michael Steil’s Commodore Peripheral Bus series.
Conveniently all the standard 6502 vectors point to the start of the ROM, where a little house keeping is done. First we set the GPIB flags of Not Ready for Data (NRFD) and Not Data Accepted (NDAC). Note setting the flag here means we write a 0 to the flip flop, that will come out inverted as a 1 on the bus. With clearing doing the reverse. Then we wait for the UART to be ready, and as this is the version of the ROM that is expecting a printer, we send out a Carriage Return and some Line Feeds to get the printer primed.
Begin:
ldx #$09
stx $0200 ; Set NRFD & NDAC (logic inverted)
lda #$0D ; Carriage return
WaitUARTInit:
ldy $0600 ; Read UART status word
bpl WaitUARTInit
sta $00 ; Load A into UART data buffer
dex
cpx #$08
bne LFF15 ; Sends 1 CR then 8 LF to printer
lda #$0A ; Line feed
LFF15: cpx #$00
bne WaitUARTInit
txs ; Set stack pointer to 0?
Now we get to the main heart of things; where we signal we are ready on to other GPIB devices and check if any data is valid to process or jump back to being ready.
IEEEReady:
lda #$80
sta $0200 ; Clear NRFD (logic inverted)
WaitVaidData:
lda $0500 ; Read IEEE Status
bpl WaitVaidData
ldy $0300 ; Read IEEE Data bus
ldx #$40
stx $0200 ; Clear NDAC (logic inverted)
WaitDataReadAck:
ldx $0500 ; Read IEEE Status
bmi WaitDataReadAck
ldx #$20
stx $0200 ; Set NRFD & NDAC (logic inverted)
and #$20 ; IEEE ATN flag set?
beq IEEEReady
cpy #$24 ; Value of "$" on bus?
bne IEEEReady
Since we now have done the first part of the GPIB handshake, and we know there is going to be data coming in to print there is some prep that is going on for this. The confusing thing is we are messing with the stack pointer, but there isn’t any RAM in the stack area
LFF3E: tsx ; Put stack pointer in X...?
txa
and #$7F ; Clear flag for...?
tax
txs ; Save back on stack pointer
Then just like before, we preform another GPIB handshake to get the next byte of data from the bus
lda #$80
sta $0200 ; Clear NRFD (logic inverted)
WaitVaidData2:
LFF49: lda $0500 ; Read IEEE Status
bpl WaitVaidData2
ldy $0300 ; Read IEEE Data bus
ldx #$40
stx $0200 ; Clear NDAC (logic inverted)
WaitDataReadAck2:
LFF56: ldx $0500
bmi WaitDataReadAck2
ldx #$20
stx $0200 ; Set NRFD & NDAC (logic inverted)
Here we hand things off to processing the data and sending things off to the printer
Get This To The Presses
So once again, we need to do housekeeping as part of the GPIB handshakes, but to make things a little more interesting this is written to avoid a bug that was in early 6502 processors: The ROR bug2. So we want to move the EOI bit into the carry, but we have to go the long way to do it
lsr a
lsr a
lsr a
lsr a
lsr a ; Put EOI in Carry flag
tsx ; Reading stack pointer again...?
txa
bcc NoEOI
clc
adc #$80 ; Set EOI flag on stack (One more byte after this one coming)
tax
txs ; Save to stack pointer.. now with flag
With this out of the way, and more confusing stack pointer stuff going on that is sort of getting clearer, we can now do some fun stuff. Time to get the data out to the printer, making sure it is a character that can be printed or doing some line handling
NoEOI:
LFF6E: tya ; Move data to A and check for things
cpy #$0A ; Is line feed?
beq JustLF
cpy #$0D ; Is carriage return?
beq LFFB0 ; Jumps into middle of HardLF
lda #$20
cpy #$21
bcc WaitUARTReadyToSend
cpy #$60
bcc AlreadyAscii
cpy #$C1 ; Check one bound of PETSCII Lowercase
bcc WaitUARTReadyToSend
cpy #$E0 ; Check other bound of PETSCII Lowercase
bcs WaitUARTReadyToSend
tya
and #$7F ; Convert to ASCII lowercase
ora #$20
tay
AlreadyAscii:
LFF8F: tya
WaitUARTReadyToSend:
LFF90: ldx $0600 ; Read UART status word
bpl WaitUARTReadyToSend
sta $00
tsx
pla ; Increment stack pointer/strobe UART
txa
and #$7F ; Clear EOI flag on stack
tax
lda $0500 ; Read IEEE Status
and #$40 ; Check if hardware switch open (80/72 column mode)
bne NotEightyColumn
cpx #$4F ; On 79th column?
bcc NoHardLF
bcs HardLF
NotEightyColumn:
LFFAA: cpx #$47 ; On 71st column?
bcc NoHardLF
The line feeding is where we finally get to see the weird stack pointer stuff come fully into the limelight. First we have to wait on the UART to send out the last character and then a line feed, but then we always clear out the lower 7 bits of the X register and set the stack pointer. So all along, the weird stack stuff has been keeping track of the number of characters we have printed, which makes sense since we have to do that somewhere but I never thought to use the stack pointer for that. In this case this register is able to be a very handy counter, even including an increment instruction!
HardLF:
LFFAE: lda #$0A ; Line feed
LFFB0: ldy #$0A
WaitUART:
LFFB2: ldx $0600 ; Read UART status word
bpl WaitUART
sta $00 ; Load A into UART data buffer
lda #$00
dey
cpy #$09
bne LFFC2
lda #$0D ; Carriage return
LFFC2: cpy #$00
bne WaitUART
ldx #$40
LFFC8: dey ; Busy wait 255 * 16 * 4
bne LFFC8
dex
bne LFFC8
tsx ; Restore current characters printed
txa
and #$80 ; Reset character count (keeping EOI flag if set)
tax
txs ; Save current characters printed (0 + EOI flag if set)
NoHardLF:
LFFD4: tsx ; Restore current characters printed
bpl LFFDA
jmp IEEEReady
LFFDA: jmp MoreDataToPrint ; One more byte to read from controller
JustLF:
LFFDD: ldx $0600 ; Read UART status word
bpl JustLF
sta $00 ; Load A into UART data buffer
bmi NoHardLF
Where We Leave Things
With 20 bytes of free space in the ROM, there probably isn’t much that could be squeezed in there. Might be possible to create a little set of loops to check for a command to LISTEN so the printer is only activated when we want. Then instead of printing out all the traffic on the GPIB bus we could hook up a disk drive or two. But then again the simple answer is just to turn off the printer.
It would also be interesting to see the other versions of the ROM that are hinted at for writing to a baudot style teletype. With many of those having a shift between figures and letters, it probably would just always revert to one of the modes whenever another is sent.
The use of the stack pointer as a counter continues to just feel so wrong, but that’s mostly because I’m coming at software development from a much more operating system friendly way. Not that I’ll probably ever have a situation where I have a 6502 without any RAM, but thinking about the stack pointer as a counter for sure may. So thanks again Adrian, not just for saving a beautiful 6502, but for leading me down a rabbit hole of early home computing history!
-
Since only 11 address lines are used, it might have been possible to produce a commercial version with some variant like the 6507 (used in the Atari 2600). More a thought experiment, magazine readers were probably only able to get full 6502s anyway ↩︎
-
Early 6502s from 1976 didn’t have a working ROR instruction. Adrian does some testing with the 6502 in his device to see if it had the bug. Michael Steil goes into some deep dives to see why exactly it fails Measuring the ROR Bug in Early MOS 6502 Though it is also entirely possible this isn’t a bug, and was the design intent of the original 6502 processor. Tube Time did some investigation on early die shots and noticed there aren’t any transistors for implementing the ROR instruction The 6502 Rotate Right Myth ↩︎