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!
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.
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.
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?