Zolatron 64 6502 homebrew – serial port part 2

It didn’t work. Not straight away, anyhow.

Once I’d wired up the 6551 ACIA chip, my UART of choice to give the Zolatron 64 a serial port, I threw together some code to test it. It was pretty simple – it took my already known-good code that prints a message to the LCD and added to that a routine to send a fixed message out over the serial port.

The code is up on GitHub – it’s the z64-02.asm version. Let’s look at a few sections.

The ACIA lives at $B000 on my address map. So one of the things we do is define the addresses of its various registers, some of the settings we’ll want to apply and some bit masks.

; ACIA addresses
ACIA_DATA_REG = $B000 ; transmit/receive data register
ACIA_STAT_REG = $B001 ; status register
ACIA_CMD_REG = $B002  ; command register
ACIA_CTRL_REG = $B003 ; control register
; Following are values for the control register, setting eight data bits, 
; no parity, 1 stop bit and use of the internal baud rate generator
ACIA_8N1_2400 = %10011010
ACIA_8N1_9600 = %10011110
ACIA_8N1_19K2 = %10011111
; Value for the command register: No parity, echo normal, RTS low with no IRQ,
; IRQ enabled on receive, data terminal ready
ACIA_CMD_CFG = %00001011
; Mask values to be ANDed with status reg to check state of ACIA 
ACIA_IRQ_SET = %10000000
ACIA_TX_RDY_BIT = %00010000
ACIA_RX_RDY_BIT = %00001000

We set up the ACIA:

; SETUP ACIA
  lda #0
  sta ACIA_STAT_REG   ; reset ACIA
  sta ACIA_INFO_REG   ; also zero-out info register
  lda #ACIA_8N1_9600  ; set control register config
  sta ACIA_CTRL_REG
  lda #ACIA_CMD_CFG   ; set command register config
  sta ACIA_CMD_REG

Towards the end of the code, I have a serial message defined. This ends with a line feed (ASCII 10) and then a null byte terminator:

.serial_msg
  equs "Zolatron 64 serial message"
  equb 10
  equb 0

And here’s the routine that prints it to the serial port, which I implemented as a sub-routine because I intend to generalise it later. I’m using the X register as an offset counter, to determine which character in the message we want to send next. We pull that character into the Accumulator and then write that character to the data register. Because we’re in write mode and are addressing that register, this has the effect of telling the ACIA to send the character. It’s really that simple. We keep incrementing X until the character it pulls in is ASCII 0, at which point we’re done.

.serial_msg_send
; SERIAL MESSAGE LOOP
  ldx #0                  ; set message offset to 0
.send_char
  lda serial_msg,x        ; load next char
  beq serial_send_end     ; if char is 0, we've finished
  jsr acia_wait_send_clr
  sta ACIA_DATA_REG
  inx
  jmp send_char
.serial_send_end
  rts

Not so fast

You’ll note how this routine calls another subroutine, acia_wait_send_clr. This is meant to check that the previous character has been sent and the ACIA is clear to receive another incoming character. The subroutine waits until this is the case. And here’s how I first implemented it:

.acia_wait_send_clr
  pha                   ; these five lines are how it should be done
  lda ACIA_STAT_REG        
  and #ACIA_TX_RDY_BIT
  beq acia_wait_send_clr
  pla

The idea is that we load the ACIA’s status register into A and AND it against a bit mask to check the state of the TX ready bit. If this produces zero, then we’re not ready, so loop around again. There are two other important instructions – the initial PHA pushes the contents of the Accumulator on to the stack, to preserve its contents. At the end of the routine, we pull those contents back from the stack and into A. That way, we don’t muck up anything important.

Study that bit of code. It contains a fatal error.

The main section of the code has an eternal loop that calls the routine to send the message, then delays for a brief moment, then does it all again, forever.

Huh? What now?

Like I said. It didn’t work. On the plus side, the message was appearing on the serial port, so something was working. I have the Zolatron hooked up to my electronics bench computer (a LattePanda Alpha) via an Adafruit FTDI Friend. With CuteCom running on the Alpha I could see the Zolatron valiantly attemtping to communicate. But all I ever got was the first two characters of the message and then nothing. No looping.

Damn.

I suspected that one of the bugs present in various versions of the 6551, as discussed last time, might be the culprit. So I edited the .acia_wait_send_clr subroutine, replacing the check for the ready bit with just a very short loop. Basically, I put a value into the X register and then decremented it until it was zero.

That worked. Hmmm. It must be to do with those bugs, right?

I tried playing with different starting values for the X register, with the aim of reducing the delay to as short a time as possible. I’d started with $FF, which worked, so next I tried the opposite extreme and started with $01. That didn’t work at all. So then I tried a mid-way point, $F0, and that worked. I was about to start working my way down when I spotted something in the .acia_wait_send_clr subroutine code – actually in the original version, which I’d simply commented out.

The fatal error I mentioned earlier jumped out at me. Every time the ready bit wasn’t ready and I had to go around the loop again, I was including the instruction to push the Accumulator contents to the stack. But this is something that should have happened only once. I was probably overflowing the stack. Doh!

I got rid of the delay loop and changed the code to read like this:

.acia_wait_send_clr
  pha                      ; preserve A state on stack
.acia_wait_send_loop
  lda ACIA_STAT_REG        ; get the status register state
  and #ACIA_TX_RDY_BIT     ; compare with ready bit
  beq acia_wait_send_loop  ; if not set, loop
  pla                      ; recover state of A
  rts

Now there’s only one push to the stack. The loop returns to a point just after this push.

CuteCom on the Alpha. The garbage at the end is the result of turning off the Zolatron.

The reason my delay loop version had worked is that I’d commented out the PHA and PLA. I didn’t need them because the loop didn’t use the Accumulator. This revised code worked!

Testing, testing

Okay. Now we have a working serial port, it’s time to test all those 6551 chips I bought. To recap, I ended up with three kinds.

  • R6551 chips, which have the CTS bug.
  • R65C51 Rockwell chips, which are supposed to be okay.
  • R65C51 Western Digital chips – at least, that’s what the seller said they were, but when they turned up they were branded ‘GT Eµ’.

So many chips. They can’t all be fake, right?

The last one of those was what I had in the Zolatron when everything started working. So I was confident about that. The weird thing is, if it had been a genuine WD chip, it shouldn’t have worked. That’s the version with the stuck-high ready to send bit in the Transmit Data Register. I’d already established that a ready-to-send routine that basically does nothing except wait for a very short time doesn’t work. As the bit is stuck high with WD chips, my current, working routine should always send without waiting, which I know doesn’t work. So I’m pretty confident that these are not the WD version of the CMOS chip. Maybe the GT chips are based on the Rockwell design?

The three original R6551 chips all worked. However, occasionally the computer would need a few resets before it jumped into action. I’m thinking this is to do with the CTS bug.

Of the 65C51 chips with the Rockwell logo, two worked. The Rockwell incarnation is supposed to be solid. Against all expectations, the chip with the very crisp lettering – I mean, it looked brand new – was one of the ones that worked. I’d assumed this had to be a re-lasered phony. It has a 1997 date code. But hey ho.

That left seven non-working chips. More investigation is needed to determine if they are simply dead or fakes, but that can wait for another day. I have five working CMOS chips and three original R6551s to play with.

Next step, writing code to deal with incoming traffic.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.