Zolatron 64 6502 homebrew – serial port part 3

Listening is so much harder than talking, don’t you think?

That’s how it turned out with the Zolatron’s serial port, anyhow. Getting the computer to send messages was pretty easy. Receiving them? Not so much.

The first stages of developing the software were typical. I wrote code that I knew should work. It didn’t. I cursed the binary gods for the fickleness of their logic until, at last, I saw the stupid mistake I’d made. Rinse and repeat.

It still didn’t work.

Chucking the oscilloscope on the IRQ line showed me something that should have been obvious. The line was starting high after a reset, which is correct, and then going low after receiving the first character. Also correct. But then it was just staying low permanently.

So very wrong.

I needed a pull-up on the IRQ line. It turned out that I already knew this. My schematic for the Zolatron clearly shows a 3.3KΩ resistor pulling the IRQ line high. But I’d forgotten to implement this on the breadboard. Doh!

No joy

Adding that resistor certainly helped, but I was still having no joy in receiving short strings of text.

The code isn’t that complex. When the 6551 ACIA receives a character, it triggers an interrupt. My interrupt service routine (ISR) puts the character into a receive buffer and also checks whether the character is a null byte (ASCII 0) or a carriage return (ASCII 13). If so, a flag is set at a memory location I’m using for registering the status of the serial port.

In the main loop of the program, I poll this register to see if that flag has been set. If so, the program prints whatever is in the receive buffer to the LCD display, and then clears the buffer.

As often happens, there were small, incremental improvements that suggested I was heading in the right direction. I used the LCD screen as part of the debugging – getting it to print certain characters to confirm that I was entering specific parts of the code, including the ISR. (And yes, I was.)

Eventually I got to the stage where, when sending a string, the first character would appear on the LCD okay, but all subsequent characters were corrupted. Totally munged.

That got me thinking. I added a test to the ISR to see if the overrun flag was being set. This gets set when data is arriving faster than the processor is processing it. And this turned out to be the case.

That’s hardly surprising, perhaps, with a 6502 machine running at 1MHz using code that can hardly claim to be optimised.

I cranked down the baud rate from 9600 to 1200. Didn’t help. (I’ve since turned it back up again.)

Within the ISR, I added a test near the end to see if the receive register of the ACIA is full. This register is only one character and so fills up quickly. I figured that I could continue pulling data from this register within the ISR – ie, the ISR could handle multiple characters instead of just one. If the processor didn’t have to handle a separate interrupt for every character, it might be able to deal better with the rush. Here’s the current ISR routine:

; ---------INTERRUPT SERVICE ROUTINE (ISR)--------------------------------------
.ISR_handler
  pha ; preserve CPU state on the stack
  txa
  pha
  ; Check which device caused the interrupt.
  bit ACIA_STAT_REG   ; if it was the ACIA that set IRQ low, the N flag is now set
  bmi acia_isr        ; branch if N flag set to 1
  ; do other checks here, branching as appropriate
  jmp exit_isr

.acia_isr
  ldx UART_RX_IDX          ; load the value of the buffer index
.acia_rx_get_char
  lda ACIA_DATA_REG        ; load the byte in the data register into A
  sta UART_RX_BUF,X        ; and store it in the buffer, at the offset
  beq acia_rx_set_null     ; if byte is the 0 terminator, go set the null flag
  cmp #13                  ; or is it a carriage return?
  bne acia_isr_end         ; if not 0 or CR, go to next step
  lda #0                   ; if it's a carriage return, replace the CR code
  sta UART_RX_BUF,X        ; we previously stored with a null
.acia_rx_set_null
  lda UART_STATUS_REG      ; load our status register
  ora #UART_FL_RX_NUL_RCVD ; set the null byte received flag
  sta UART_STATUS_REG      ; re-save the status
.acia_isr_end
  inx ; increment the index
  lda ACIA_STAT_REG        ; Load ACIA status reg - resets interrupt bit
  and #ACIA_RDRF_BIT       ; Is the Receive Data Register Full bit set?
  bne acia_rx_get_char     ; If so, we have more data

  jmp exit_isr
; there will be other stuff here one day, which is why we're jumping above

.exit_isr
  pla ; resume original register state
  tax
  pla
  rti

Getting worser all the time

That didn’t help. In fact, things were getting worse. Sometimes I didn’t see anything on the LCD. And the situation seemed to deteriorate with each new iteration of the code – to the point where, following a reset, the computer sometimes didn’t send the init message via serial and often didn’t even print the welcome message to the LCD. These were things that were sorted long ago. Why would they stop working too?

I stared at the code. It stared back at me. “This should work,” I thought.

Then I rolled back to an earlier iteration of the code – a known-good state where the computer simply printed the welcome message on the LCD and continually sent out a simple message via serial.

It didn’t work.

That was the Aha! moment. Maybe the problem wasn’t software. Maybe it wasn’t meatware. It could be the hardware.

CuteCom communicating with the Zolatron with a 10ms delay.

I used the oscilloscope to probe around, checking that the clock signal was good, that the IRQ line was firing as expected, that there was stuff happening on the data and address lines. All that looked fine. And then I realised that my known-good program was suddenly working again. I’d clearly jiggled a wire somewhere during all that probing.

While doing all this, I also read an interesting thread on Reddit. This described the same situation I was seeing, with the first character being okay and others munged. One person suggested adding a delay between each character sent of, say, 10ms. This is easy enough to do in CuteCom (which I’m using on the Ubuntu-based lab computer).

I switched back to the latest version of the code, added that 10ms delay and… it worked!

The working version of the code is z64-03.asm over on my GitHub.

Next steps

Next up, I may remove that bit of code that keeps looping inside the ISR. It was a kludge to start with and I’m hoping it’s not needed.

I’m finding out, first hand, some of the limitations of working with breadboards. My mate Doug described the sight of my project as being like ‘a cat’s litter tray into which someone’s dumped some old electronics’. He’s not wrong. And so it’s time for me to summon my courage and try to lay out some PCBs.

Back in the software, I also need to add a check to see if the receive buffer is becoming full. At the moment, it’s vulnerable to overflows. Implementing a circular buffer is probably a good idea, if I can get my head around how to do that.

And then, when that’s done, I guess I need to do more with the incoming text than just display it.

So … how do you write an operating system?

 

1 thought on “Zolatron 64 6502 homebrew – serial port part 3

  1. Alexandre Dumont

    I remember the 6551 giving me headaches, and passing by the similar steps as you did as well. You can send at higher speed, but yes you need to have some delay between chars. Not so problematic when sending chars one by one, for example when typing in a terminal like minicom, as you type. It gets problematic if you send a string.
    I think we’d need to implement hw handshake/control flow (using the pins for that purpose, but I have still to figure out how to do that. With hw control flow, I think the sender will only send when the receiver is ready to accept a new char (only 1 byte buffer in the 6551).

    Reply

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.