Atari Lynx programming tutorial series:
The previous parts we have looked mainly at graphics and memory. The Lynx has two other interesting features that set it apart from other handheld consoles. The sound and the ability to connect up to 16 Lynxes and allow them to communicate to one another. This part we will look into the details of ComLynx. ComLynx was the official name for the connection capability of one or more Lynx consoles. It will take more than one part to cover everything, so we will get started with the basics of serial communication and the hardware inside the Lynx. This helps understand how everything works and gives valuable insights before we move up an abstraction level by using the CC65 serial driver.
Primer in UART and serial communication
Before we dive into ComLynx we need a good understanding of UART and the serial form of communication that comes with it. UART is short for Universal Asynchronous Receiver/Transmitter and is a piece of hardware (an integrated circuit) that can do serial communication over a small number of lines. Usually the communication physical interface between two such hardware components consists of a cable that has a couple of wires, each with a dedicated purpose. Typically these are a ground (GND) and a receive (RX) and transmit (TX) line at a minimum. Other lines (CTS, RTS, DSR, DTR) can help create a more robust communication allowing for handshakes and transmission control. The Lynx does not have these, so let’s steer clear of those.
The RX and TX line can have a high (e.g. +5V, although the exact voltage may vary) voltage and low voltage (0V and again low might be a different voltage, potentially negative). Using the two voltage levels the lines can send bits across the line by alternating high and low to indicate 1 and 0 respectively. It is similar to morse code where the short and long beeps are also two distinct signals that allow you to build characters. Where it differs is that in this serial communication it is customary to have a particular transmission protocol.
The most common protocol used for communication defines a way to send/receive data and check that the data has arrived completely and without error. The terminology includes Mark (for the high signal, or 1) and Space (for the low signal, or 0). The idea is that each piece of data is surrounded by a start bit and stop bits. The start bit signals the start of the data to follow and serves as a sync point for the receiver to begin reading the data that follows using its internal clock. When the data has been sent, one or two stop bits follow to signal the end. The stop bits might be preceded with a parity bit that helps determine errors.
The parity bit is something special. It can be used by the hardware to detect errors in some cases. The parity bit helps to determine and check the parity of the data. It works like this. The bits in the data that have a high value (1) are counted. The parity bit is chosen to be 0 or 1 depending on the kind of parity check. With ‘even’ parity the parity bit is determined to result in an even number of 1 bits. Similarly, ‘odd’ parity is used when the number must be odd, not even. For example, say the data transmitted was 10011001. That’s four ones, so that is an even number. With even parity the parity bit should be zero, otherwise the total number of ones including the parity bit will be odd. For odd parity it would be necessary to chose the parity bit as one resulting in 5 ones which is odd. Otherwise the total number would remain an even number. Whenever the parity bit is incorrect for the chosen convention (odd or even) it means an error condition has been detected. Since the check is based on a single bit with two values of which one is correct, it means that you will only find errors in 50% of the cases. Not perfect, but still better than nothing.
Three other options for the parity bit exist: None, Mark and Space. None means that no parity bit is included. The stop bit(s) will immediately follow the data bits in that case. Mark parity always has a high parity bit, and Space has a low parity bit.
Each bit that is transmitted is defined by the high- or low-ness of the line for a period of time. That time, the bit time, is determined by the clock speed of the UART. You have probably come across the unit of Baud, or “Bits per second”. It was pretty common to refer to your modem speed in Baud. E.g. my first modem was a Dynalink 14k4 baud modem, capable of transmitting 14400 symbols (or tones) per second. Although Baud and bits per second are not necessarily the same, it does hold true for the UART in the Lynx. For digital devices the symbols are bits, hence the reason that Baud and bits per second are equivalent.
UART in the Lynx
The UART of the Lynx is a circuit that lives inside of Mikey. The UART supports various baud rates, and has several settings for the parity bit. The baud rate is governed by the countdown value and frequency settings you provide to timer 4. The timer governs the pace in which the bit are transferred. All Lynx consoles should have the same baud rate to understand each others bit rate.
The data that is transmitted over the wire has the following 11-bits format:
The start bit and stop bit are always present and have a values of 0 and 1 respectively. This is a common choice as it will make sure that there is always a transition between the stop and start bit (1 to o). The data is sent with the least significant bit first (LSB).
The parity bit is set depending on a chosen setting. The Lynx supports the variations of Odd, Even, Mark and Space. The omission of None means that the 9th bit is always sent. Not all of the parity settings are actually useable in a real-life scenario. For odd and even parity the bit is set appropriate. However, the hardware has a flaw that results in the parity calculation to include the parity bit itself. The Lynx can communicate with another Lynx just fine, because both have this bug. On the other hand, the Lynx will have a hard time communicating with non-Lynx devices (such as a PC with a serial port) that will check the parity bit the normal way.
The Lynx’s UART has a TX, RX and GND line for send, receive and ground. The peculiar thing about the Lynx wiring of the cable is that the RX and TX are connected together inside the ComLynx cable.
The design choice to connect RX and TX together has a lot of consequences:
- The hardware now has the simplest setup.
- Whatever a Lynx transmits is also received by himself.
- The Lynx can detect when there is something connected to the ComLynx port, because it will be “short-circuited”.
- No hardware or software handshakes are possible.
- When one Lynx talks, all others must listen to avoid transmission errors.
Inside the UART there is transmitter and receiver hardware. For transmitting the UART provides a holding register and a shift register. The holding register can hold the next byte that must be sent. The shift register pushes the actual 8 data bits over the wire, wrapping it with the start, parity and stop bits.
To send something you first put a byte in the holding register. The hardware transfers it to the shift register when it is empty and ready to accept the next byte. Then the shift register starts sending it out. In the meantime you can put the next byte in the holding register, because that has become empty after the byte was transferred to the shift register.
The UART send and receive mechanism uses timer 4 as its clock to generate the bit rate and it can fire interrupts (IRQ) when actually sending or transmitting. Indirectly the interrupts are a way of notifying you of a newly received byte or an empty holding register. In the interrupt handler you can check flags (TXRDY and RXRDY as we’ll see in a moment) to see whether a byte was sent or received. If so, you should put a new byte in the holding to keep the outbound dataflow going and you should read received bytes quick enough before the next one arrives. Should you be slow to put in a new byte to transmit, you’re wasting time sending all data. If you are too late to pick up a byte from the receive holding register, you will get an overflow because the receiver cannot place the new byte and lose some data.
It’s your choice whether you want to use interrupts for sending and receiving or not. Using interrupts you can continue doing other stuff, so this is a reasonable option when you are performing other tasks (gameplay is an example) and want to send and receive data in the meantime during interrupt handling. On the other hand, should you have a dedicated part of your program/game that does send or receive only, you can use the send and receive flags to check whether you should use SERDAT.
A programmer’s look at UART
Now that we know what the hardware looks like and how it behaves, it is time to look at the programming side of things again. The Lynx has various hardware registers inside Mikey that have a memory address so you can change settings for the UART hardware and help send and receive data.
First of all, the UART uses timer 4 to serve as the baud rate generator. So, by setting the time of each timer tick and the countdown period you can indirectly specify the baud rate. The baud rate is calculated by determining the time it takes to send 8 bits when timer 4 has a particular speed. The baud rate calculation is as follows:
where rtimer4 and ftimer4 are the reload (countdown number) value and frequency of timer 4. The countdown value is set to a minimum of 1, but the timer will trigger when it underflows (from 0 to a virtual -1). It will need a minimum of two timer periods to trigger. The frequency inverted gives the number of clock ticks per seconds. The end result is the baud rate as it is frequently used (bytes per second) for devices, but not the official definition of bits per second.
|1 MHz||1000000/((1+1)·8) = 62500 Bps|
|1 MHz||500000/((1+1)·8) = 31250 Bps|
|1 MHz||1000000/((12+1)·8) = 9615 ≈ 9600 Bps|
|500 kHz||500000/((207+1)·8) = 300,5 Bps|
15625/((255+1)·8) = 7,63
The most important two memory locations are SERCTL (0xFD8C) and SERDAT (0xFD8D). The first, SERCTL, refers to the serial control register and allows you to change settings. SERDAT is where you will read or write the serial data.
SERCTL turns out to be a weird register. The behavior is totally different when writing to or reading from it. In other words, when you write a specific value, and then read it, you will probably get a different value from the one you wrote.
Transmitter interrupt enable
The interrupt bit for timer 4 will correspond to the transmitter ready bit (i.e. you can put a new character in SERDAT)
Transmitter buffer empty
The buffer to hold data is ready to accept another byte. You can write it to SERDAT.
Receiver interrupt enable
With this enabled the interrupt bit for timer 4 will correspond to the receiver ready bit (i.e. a character was received and can be read from SERDAT).
Receive character ready
A character was received and can be read from SERDAT.
No idea what they meant to keep compatible in the future.
Transmitter totally done
The transmitter has both an empty buffer and shift register. All offered data has been sent completely.
Parity bit enabled
The parity checking is enabled and the parity bit will be calculated according to the odd or even setting (see PAREVEN).
Received parity error
The data that was received had a parity error, so the parity bit did not match according to the parity setting.
Resets all errors
Writing a value with this bit set will clear all three errors (parity, framing and overrun) should they be set.
Received overrun error
The data in the receive holding register was not read quickly enough. The new data could not be delivered.
1: Open Collector
0: TTL driver
Choose between these two modes of transmission. A bug in the hardware causes the state of the output to be high after power up. The advice is to set the bit to Open Collector (1) to fix the problem.
Received framing error
There was an error in the frame. That probably means that from the suspected start bit the stop bit was not received at the expected moment. It usually means that more than one Lynx is sending at the same time.
Sends a break
For as long as this bit remains 1.
It should be set at least for a 24 bit period according to the current baud rate. The specification mentions that a break is a start bit followed by 8 zero data bits, a parity bit and the absence of a stop bit at the expected time.
A break was received because for at least 24 bits of transmit time, there was a high signal.
1: Even parity (or Mark)
0: Odd parity (or Space)
This parity is used for both sending and receiving. When PAREN is 1 these parity values are used. If PAREN is 0 then the value in parentheses is used (Mark or Space) as the value of the 9th bit.
This bit reflects the parity bit of a received frame. It is set to the parity calculation when PAREN is 1. Or it is whatever PAREVEN is at sender when PAREN is 0.
A deeper look at the way the data is being transmitted will explain how the byte travels through the UART transmitter. It is important to realize that the TXRDY bit refers to the holding register, while the TXEMPTY bit represents both empty state of the holding and the shift register.
Here’s how the various states of the TXRDY and TXEMPTY bits change throughout the lifecycle of sending. The scenario shows how two bytes are sent in sequence from the start when no transmission has been done yet.
The reverse is true for receiving data, except that the data is eventually put into the receive holding register, which can be read from SERDAT.
The UART will always check the parity bit on received data whether PAREN is set to enabled or not. It does look at the PAREVEN bit, so for Even and Odd a calculation is done, but for Mark and Space it only compares the value of the bit to the setting. Any parity errors are always reported through the PARERR bit in a read from SERCTL. You can inspect the value of the parity bit through PARBIT in the SERCTL register by reading from it.
OK, no coding just yet, but a lot of background on the UART in the Atari Lynx and how it works. This will help us get started with the ComLynx features. Next time we will look at the way CC65 provides serial communication and also how to dig a little deeper and get started with the interrupts and low level timers and registers.
Till next time.