Faster Transmissions

So here’s where we’re at: The Project:65 computer can read data from the serial port at a pretty good clip now that it’s using the MAX3100 UART’s receive interrupts. However, it only writes a character out when it receives a timer interrupt that runs at 100 Hz. That’s slower than a 1200 baud modem.

Actually, the transmitting data situation improved quite a bit when I switched to using Max3100_SendRecv, the combined read-write subroutine I talked about a couple posts ago. Because the P:65 could now send a byte out every time it read one in, my test program could put out some pretty good full-duplex communication:

Of course that only worked if the P:65 was sending and receiving at the same time. Otherwise it was still stuck at 100 characters per second. This needed some improvement.

I didn’t want to send more than a single data byte in a given interrupt. The MAX3100 has a one-character transmit buffer. As soon as it starts sending a character out the RS-232 port, that buffer is freed, and you can send it the next byte to be transmitted. But then you have to wait for it to start sending that byte before you can send it the next. Any routine that tried to loop and send multiple bytes would have to do a lot of waiting for the buffer to be available.

The better solution, I felt, was to use the MAX3100’s transmit interrupt, “T”. According to the datasheet, T is asserted whenever the transmit buffer is ready to be sent data, and it stays asserted until you read data from the 3100.

I didn’t quite understand how to use the T interrupt the first time I read that. I thought that if I wasn’t sending data out the serial port then I’d get a constant stream of transmit interrupts. That didn’t sound good, so for my first attempt I wrote some code to turn the T interrupt on when I wanted to send data, and turn it off when there was no more data ready to be sent.

This first attempt did not work. At all. I wish I’d remembered to take a screenshot of the garbage characters that filled my terminal window when I tried it.

My mistake was that I’d assumed I was doing this the right way, and as a result I didn’t read the details carefully enough. According to the datasheet, whenever you send a new configuration command to the MAX3100, it zeroes out a bunch of its internal state – including the receive FIFO and any stored data. Reconfiguring the 3100 on the fly was just not going to work.

On the upside, this failed attempt did give me a better sense of how the transmit interrupt behaves. It turns out that T isn’t asserted when the transmit buffer is ready to be sent data. Instead, T is asserted when the transmit buffer becomes ready to be sent data.

This was good news – it meant that I didn’t have to worry about an endless stream of interrupts, or trying to reconfigure the MAX3100’s interrupt mask. Instead, the transmit interrupt gets asserted once whenever the 3100 starts transmitting a byte.

Since I was already calling my combined Max3100_SendRecv subroutine any time the P:65 received an interrupt from the MAX3100, my code was really doing almost all of the work it needed to do. There were two edge conditions that I needed to worry about:

If the MAX3100 is writing out a string of characters, there is one extra transmit interrupt received when it starts transmitting the last character. That’s not a huge problem – Max3100_SendRecv is safe to call when there’s no data to be read or written – but it is slightly wasteful. I briefly wondered if it would be possible to just do nothing during this interrupt if the P:65’s write buffer was empty, but the only way to clear the interrupt is to read from the MAX3100.

As a consequence of this, when we start writing data again after a pause, we don’t see a transmit interrupt – it’s already happened and been cleared. To get around this, my “top-half” PutChar method calls Max3100_SendRecv after adding a character to the write buffer. Either the MAX3100 will accept the character, or it won’t and the transmit interrupt will happen as soon as it is ready. Because I don’t want this operation to be trampled on by the interrupt service routine, PutChar has to disable interrupts during this call.

With those changes in place, sending and receiving on the serial port was working at full speed. After a while I did notice a weird glitch, though. Very occasionally, when the P:65 was trying to write out a string when it wasn’t receiving anything, it would “stall” until it received a character, and then start writing again.

It took quite a while to capture what was happening with the logic analyzer, but the result was worth it. It’s a perfect little illustration of a race condition:

What’s happening on screen is that the program running on the P:65 is calling PutChar for the last two characters of a long string, and each time PutChar calls Max3100_SendRecv, which we see on lines 4 through 7. The status byte the 3100 sends tells us that its transmit buffer is still full, so instead of an actual data character we’re sending the dummy value 0xDD.

But now look at line 2 of the logic analyzer. The 3100 finishes transmitting a character and starts transmitting the next one, which triggers the transmit interrupt. But when the interrupt happens, the P:65 is in the middle of running its Max3100_SendRecv and has interrupts disabled.

Here’s the problem: According to the MAX3100 documentation, the transmit interrupt is cleared whenever data is read from it. And that’s what we see happening: just at the end of the SPI communication, the IRQ line goes high again. When the P:65 re-enables interrupts a few cycles later, the interrupt is gone – and we never even noticed it.

The more I think about it, the more I suspect this is uncommon but also unavoidable. Even if I was handling PutChar in a different way, the same situation could happen while servicing a read interrupt, because receiving and transmitting RS-232 data can line up in arbitrary ways.

I don’t think it’s an insurmountable problem though. The fix I’m looking at is pretty simple: During the timer interrupt, we can check the size of the write buffer. If it’s nonempty, we can run a Max3100_SendRecv call. It will introduce a small latency hitch, but it should be enough to get the transmission started up again after a race like this.