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:

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!


  1. 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 ↩︎

  2. 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 ↩︎