Computer Labs 2013/2014 - 1st Semester
Lab 7: The PC's Serial Port


1. Objectives

The objectives of this lab is that you learn 1) the operation of the PC's Serial Port(UART) and 2) how to use its low level interface.

In contrast with the previous labs, this one will span over two classes.

2. What to Do

Write in C several functions that use the UART low level interface. The key functionality to implement is:

  1. Read and display the configuration of the UART
  2. Configure the communication parameters of the UART
  3. Both send and receive characters in polled mode.
  4. Either send or receive characters in interrupt mode. Which one you have to implement, will be specified in the second class.
  5. Use the UART FIFOs. You'll have to use FIFOs for only one combination of reception/transmission and interrupt/polling. Which combination you have to implement will be specified in class.

Like in recent labs you are not given the prototypes of the functions to implement: the specification of these functions is part of your job. In specifying these functions, you should try to develop a library of functions for the UART that you will be able to reuse in your project. Therefore, in grading your submission, we will take into account its flexibility/generality. Furthermore, this handout is an incomplete specification of what you'll have to implement. It will be completed with information we will provide you at the beginning of your second lab class. This information may vary from section to section (i.e. "de turma para turma").

To make the task of grading your assignment feasible, you are required to implement the following test functions:

  1. int ser_test_conf(unsigned short base_addr)
  2. int ser_test_set(unsigned short base_addr, unsigned long bits, unsigned long stop, long parity, unsigned long rate)
  3. int ser_test_poll(unsigned short base_addr, unsigned char tx, unsigned long bits, unsigned long stop, long parity, unsigned long rate, int stringc, char *strings[])
  4. int ser_test_int(/* details to be provided */)
  5. int ser_test_fifo(/* details to be provided */)

These functions are declared in header file test7.h, and file test7.c contains their implementation stubs. You may find it convenient to add your test code to that file; this way you will avoid mistakes in their definition (any mistakes will be our own responsibility). Section 5 describes what these functions should do.

IMP. In addition to these files, and possibly other files, you should submit the file lab7.c, which should contain only the main() function. This function will be graded based on the flexibility it affords in testing your code. For grading your submission, we will use our own main() function.

2.1 Class Preparation

The details concerning ser_test_int() and ser_test_fifo() will be revealed only at the beginning of the second lab class. Thus, you should plan to implement the remaining functions in the first lab.

So that you can accomplish the objectives of the this lab, you should do some homework for each of the classes

For the first class, you should read, and understand, this handout and the class notes. If you wish, you can skip the parts on interrupts and FIFOs. Furthermore, you should:

  1. Create folder lab7 at the top level of your SVN repository;
  2. Write the main() function in lab7.c, that will allow you to test your code in a flexible way;
  3. Write ser_test_conf that reads and displays the UART configuration.

Obviously the files with your code should be added and transferred to the SVN repository, before the beginning of the first class.

2.2 Second class Preparation

For the second class, you should read carefully the material on interrupts and FIFO, not only in the class notes and this handout, but also in the data sheet of the 16550D UART.

It may also be advantageous, if you try to implement some basic interrupt functionality.

3. The PC's Serial Port (UART)

One of the standard I/O devices of the PC used to be the serial port, which allowed the PC to communicate with external "devices", such as modems, mice, terminals or even other PCs using asynchronous serial communication according to the RS-232 specification. The RS-232 specification is rather limited and nowadays most PCs instead support other more efficient serial communication protocols, such as USB or eSATA. Nevertheless, RS-232 communication is still common in the embedded systems world because of its simplicity.

The serial communications controller used in the original PC was the 8250, and was known as the Universal Asynchronous Receiver Transmitter (UART). This was a simple controller that did not support buffering (via FIFOs). With the PC-AT came the 16450 "model" and later the 16550. Although the 16550 had a 16-character FIFOs for receiving and transmitting, it had a bug that made the FIFOS useless. This was later fixed in the 16550A. Other versions of the UART with larger buffers were later released, e.g. the 16750 had 64-byte FIFOs. More recently, the UART functionality was integrated in the "chipset" (the "southbridge").

3.1 Asynchronous Serial Communication

Serial communication is a data communications technique in which the bits of the data are sent one after the other via a single "communications line". This contrasts with parallel communication in which several bits of the data are sent simultaneously via several "communication lines" such as in the data bus of a computer.

There are mainly two kinds of serial communication: synchronous and asynchronous. In the former, the data is sent along with a clock signal to allow the receiver to synchronize with the bit stream and properly "decode" the bits received. This either requires special circuitry to recover the clock signal from the bit stream or a separate line for the clock signal. In asynchronous communication, no clock signal is sent with the data. Instead, the data stream is chopped in "characters" of 5 to 8 bits, and synchronization is performed at each character. This is done by using a clock with a frequency much higher than the bitrate, i.e. the number of bits per second transmitted, and by sending a start bit before the data bits in a character and one or more stop bits after the data bits in a character. Because the stop bits are a high level voltage and the start bit is a low level voltage, this guarantees that there is a voltage transition at the beginning of each character that allows the communications circuits to synchronize with the bit stream. The synchronization needs to be good enough only for the duration of one character, as it is performed at the beginning of every character.

Another communication parameter in asynchronous communication is the parity. Communication signals may be affected by (electromagnetic) noise leading to communications errors, i.e. the decoding of a signal as a 1 when it was supposed to be a 0, or vice-versa. An approach to detect (some of) these errors is to add a parity bit, whose value depends on the number of "1" data-bits and on the parity being used. If the parity is even, then the number of "1" data-bits in a character and in the parity bit must be even, if the parity is odd, then the number of "1" data-bits in a characters and in the parity bit must be odd. Note, that whatever the number of "1" data-bits in a character, it is always possible to ensure the parity "agreed" by setting the parity bit to a proper value.

So that a transmitter and a receiver can communicate successfully they must use the same communication parameters, i.e. bitrate, number of bits per character, number of stop bits and parity.

Finally, serial communication, both synchronous and asynchronous, may be classified in one of three types: simplex, (full-)duplex and half-duplex. In simplex communication the bit stream flows only in one direction. That is, one of the ends of the channel is always the transmitter, whereas the other end of the channel is always the receiver. In (full-)duplex communication there may be two bit streams flowing simultaneously in opposite directions. Finally, in half-duplex communication there is at most one bit stream at any time, but it can be in either direction. These terms were originally used to describe the capabilities of the (hardware) communication system, but they can also be applied to the way a program uses that communication system.

Although the PC's serial port supports full duplex communication, in this lab you will use only simplex communication.

3.2 The UART Registers

The UART provides 10 addressable registers which are used for: 1) transfer data; 2) check the status of data transfer; 3) control data transfer. We will describe shortly only those that are relevant for this lab:

Receiver Buffer (RBR)
Register to read the characters received from the serial line
Transmitter Holding (THR)
Register to write the characters to send to the serial line
Line Status (LSR)
Register that contains several status bits regarding the serial communication
Line Control (LCR)
Register to configure the different asynchronous serial communication parameters
Divisor Latch Least(DLL) significant byte
LSB of clock divisor for setting the bit-rate
Divisor Latch Most(DLM) significant byte
MSB of clock divisor for setting the bit-rate
Interrupt Enable (IER)
Register to control the interrupts generated by the UART
Interrupt Identification Register (IIR)
Register that contains several status bits relevant to handle interrupts. It includes also some bits pertaining to the status of FIFO operation
FIFO Control (FCR)
Register to control the operation of the FIFO buffers, if available

The Divisor Latch (DL) register is a 16-bit register that contains the divisor for setting the bit-rate. I.e., in the PC, the bit-rate is obtained by dividing 115 200 by the contents of the DL register.

There are two issues pertaining to the initialization of the DL register. First, the 16-bit DL register is exported by the UART as two 8-bit registers, the DLL and the DLM, which contain the LSB and the MSB of the DL register, respectively. Second, the DLL and the DLM have the same bus address as other registers. Thus, to access DLL and the DLM a programmer must first set the DL Access Bit (DLAB) of the LCR, then actually access the DLL/DLM register, and finally reset the DLAB so that the other registers can be accessed.

Table 1 lists the registers that you'll need to use in this lab.

Addr. DLAB Read Write
0 0 Receiver Buffer Transmitter Holding
1 0 Interrupt Enable
2 X Interrupt Identification FIFO Control
3 X Line Control
5 X Line Status -
0 1 Divisor Latch Least Significant Byte
1 1 Divisor Latch Most Significant Byte
Table 1: Addressing information of the relevant UART registers.

These registers are in the IA32 I/O address space and are accessible starting at the base address of the corresponding serial port. Table 2 shows the base address (and range) and the IRQ lines used by the two first serial ports of a PC. (A PC may have more than one serial port, which are usually named COM1, COM2, COM3 and so on.)

Serial PortBase Address(-range)IRQ
COM10x3F8(-0x3FF)4
COM20x2F8(-0x2FF)3
Table 2: Base address and IRQ of the two first serial ports of a PC.

3.3 Configuration of the Serial Communication Parameters

In serial communication, both ends of the serial line must agree on the communication parameters. In the case of asynchronous communication these include the bit rate, the number of bits per character, the number of stop bits and the parity. The configuration of these parameters is done by writing the registers LCR (see Table 3), DLL and DLM.

Line Control Register (LCR)
Bit Function
0,1 Word Length Select
0 0 5
0 1 6
1 0 7
1 1 8
2 No. of stop bits
0 1
1 2
5,4,3 Parity
X X 0 None
0 0 1 Odd
0 1 1 Even
1 0 1 1
1 1 1 0
6 Set Break Enable
7 DLAB
1 Selects DL
0 Selects Data
Table 3: LCR bits

As mentioned above the bit-rate is configured by loading the 16-bit DL register with the value given by 115200/b, where b is the bit-rate. Also, as already mentioned this value has to be split in its MSB and its LSB, and these bytes written to the DLM and DLL registers, respectively. Again, to access these registers you must first set the DLAB of the LCR to 1. After configuring the bit-rate you should reset the DLAB (to zero).

3.3 Sending and Receiving Characters in Polled Mode

When the UART is operating in polled mode the device driver must continually check the LSR to detect whether a relevant event such as the reception of character has occurred and then performing the necessary actions. (In the example given, this would be reading the character from the UART.) Table 4 shows the bits of the LSR (the bits relevant for this lab are typed in bold).

Line Status Register (LSR)
Bit Meaning
0 Receiver Ready
1 Overrun Error
2 Parity Error
3 Framing Error
4 Break Interrupt
5 Transmitter Holding Register Empty
6 Transmitter Empty
Table 4: Line Status Register (LSR)

When a character written to the THR has been moved to the transmitter shift register, the UART sets the "Transmitter Holding Register Empty" bit, and the device driver can write another character to the THR. When this happens, the "Transmitter Holding Register Empty" bit is automatically cleared.

When the UART puts a character received in the Receiver Buffer Register it sets the "Receiver Ready" bit. This bit is automatically cleared by reading the character from the Receiver Buffer Register.

The remaining bits report possible error conditions:

Overrun Error (UART_LSR_OE)
When set signals that at least one character in the Buffer Receiver Register was overwritten by another character, i.e. lost.
Parity Error (UART_LSR_PE)
When set, this bit indicates that the received character does not have the expected parity.
Framing Error (UART_LSR_FE)
When set, this bit indicates that the received character did not have a valid stop bit.

All three bits are cleared automatically when the UART_LSR register is read.

3.4 Sending and Receiving Characters with Interrupts

Polling operation may require too much CPU resources or else risk losing characters and/or lower the effective bit-rate. By setting appropriately the IER (Table 5) it is possible to program the UART to generate interrupts whenever a character has been received or a new character can be sent, thus improving resource utilization.

Interrupt Enable Register (IER)
Bit Function
0 Enable Received Data Interrupt
1 Enable Transmitter Empty Interrupt
2 Enable Receiver Line Status Interrupt
3 Enable Modem Status Interrupt
5,4 Not relevant for Lab 7
7,6 Reserved
Table 5: Interrupt Enable Register (IER)

The bits relevant for this lab are in bold. The use of bits 0 and 1 should be obvious: if set, the UART will generate an interrupt whenever a character is received and whenever it is ready to accept a new character for transmission, respectively. Bit 2 can be used to program the UART to generate an interrupt in the case of a communication error (parity, overrun or framing).

If the UART is programmed to generate interrupts on more than one event, the interrupt handler has to find out which event generated the interrupt, so that it can take the required actions. The IIR (Table 6) has several status bits that can be used to determine the event that generated an interrupt.

Interrupt Identification Register (IIR)
Bit Meaning
0 Interrupt Status
0 Pending
1 Not Pending
3,2,1 Interrupt Origin
0 0 0 Modem Status
0 0 1 Transmitter Empty
1 0 0 Character Timeout Indication
0 1 0 Received Data Available
0 1 1 Line Status
4 Reserved
5 64-byte FIFO (only some UARTs)
7,6 FIFO Status
0 0 No FIFO
1 0 Unusable
1 1 Enabled
Table 6: Interrupt Indication Register (IIR)

If bit 0 is set to 1, then the UART did not generate the interrupt (bit 0 is active "low"), and no further processing is required. This may happen if the IRQ line is shared by the UART and other devices. If bit 0 is set to 0, then the UART generated the interrupt, and the interrupt handler should read bits 1, 2 and 3 to find out the cause of the interrupt. These 3 bits are not used as a bit mask, but as an identifier: the UART uses them to report only one interrupt at a time. If several interrupts are pending, the UART prioritizes them and reports them one after the other according to the ensuing order.

As already mentioned, the IIR includes some bits related to the status of the FIFOs. These are described below.

3.5 FIFOs

The absence of buffering in the early UARTs had a major effect on the performance. Basically, the CPU is interrupted on every character received/transmitted. For high bitrates this leads to a high interrupt-rate and degrades the system's performance. Furthermore, if the CPU is not quick enough to respond to interrupts generated upon reception of a packet, received characters may be overwritten and therefore lost.

Later UART versions addressed these issues by adding two First-In-First-Out (FIFO) buffers with capacity of several characters, one for received characters and the other for characters to be transmitted. By loading the transmit FIFO with several characters, the driver will be interrupted only when all the characters are transmitted rather than after the transmission of each character. Likewise, a receiver may be interrupted only after the reception of several characters, thus reducing the interrupt rate. Furthermore, by setting a trigger-level, i.e. the number of characters in the reception FIFO that will generate an interrupt, to a value smaller than the size of the FIFO, the IH will have several bit-times before a character is written over, i.e. before a receiver overrun occurs.

Operation of the UART FIFO's, when present, is controlled via the FCR (Table 7). The bits relevant for this lab are shown in bold.

FIFO Control Register (FCR)
Bit Function
0 Enable FIFO
1 Clear Receive FIFO
2 Clear Transmit FIFO
3 DMA Mode Select (on some UARTs only)
4 Reserved
5 Enable 64-byte FIFO (on some UARTs only)
7,6 Rx FIFO Interrupt Trigger Level
0 0 1
0 1 4
1 0 8
1 1 14
Table 7: FIFO Control Register (FCR)

To activate FIFO operation, you must first set bit 0. If the FIFO has more than 16 bytes, then to use the additional bytes, bit 5 must also be set. Because the FIFO capacity varies from early models to more recent models, the IIR should be read to check that FIFOs are indeed activated.

To reduce the rate at which receive interrupts are generated bits 7 and 6 should be set to the appropriate values. The values shown apply to UARTs with 16-byte FIFOs. For example, if these bits are 1 and 0, respectively, then the UART will interrupt only when the FIFO has 8 bytes. Note that the higher the level, lower will be the interrupt rate. However, the time the interrupt handler will have to process the interrupt before a receive overrun occurs will be smaller.

The interrupt handler should read as many characters from the reception FIFO as possible. In order to decouple the UART code from application specific code, the IH should also use a SW FIFO buffer, or queue. The job of the IH would then be to move the characters from the UART's reception FIFO to the queue. On the other hand the application specific code would read the characters from the queue and process them.

For more details on the UART registers, you can read data sheet of a 16550D UART. Note that this UART has only 16 byte FIFOs, hence some of the register descriptions above do not match exactly.

4. Minix 3 Notes

Minix 3 has a default device driver for the PC's serial port. To ensure that this device driver does not interfere with your code, you can disable all interrupts from the UART, as long as your code does not use interrupts. If it does, you have to invoke sys_irqsetpolicy() not only with IRQ_REENABLE policy, but also with the IRQ_EXCLUSIVE policy set, as done in previous labs.

5. Test Code

So that we can grade your work, you are required to implement the following test functions. We will develop the code that will call them, so make sure that your implementation matches their prototypes.

In grading your submission, we will take into account not only whether your code does what it should, but also its quality, including its modularity.

5.1 ser_test_conf(unsigned short base_addr)

The purpose of this function is to test whether your code is able to read the control/status registers of the UART, and to interpret their content by displaying it in a format that is convenient for humans to read. More specifically, it should display the communication parameters (bit-rate, number of bits per character, etc) and which interrupts are enabled in the serial port whose base address is specified in the argument.

You can test this function by comparing its output with that of the stty command:

stty -a < /dev/tty00

assuming that you are displaying the configuration of the first serial port, and that you have not changed its configuration with your own code.

5.2 int ser_test_set(unsigned short base_addr, unsigned long bits, unsigned long stop, long parity, unsigned long rate)

The purpose of this function is to test whether your code is able to set the communication parameters of the serial port whose base address is specified in its first argument. The parity argument should take one of the following 3 values -- -1, 0, 1 -- depending on whether parity should be none, even or odd, respectively. All other arguments are the numeric values of the corresponding communication parameters:

bits
is the number of bits per character
stop
is the number of stop bits
rate
is the bit-rate

You can test this function by using ser_test_conf() that you are supposed to develop first. The stty command cannot be used because the values it reports are the default configuration of the standard Minix 3 serial port driver and not on the current settings of the UART. Most likely this is because stty does not read the UART status registers, but rather returns the configuration parameters kept by the standard device driver. Because you change these parameters directly in the UART, rather than via the standard device driver, it is not aware of the new configuration.

5.3 int ser_test_poll(unsigned short base_addr, unsigned char tx, unsigned long bits, unsigned long stop, long parity, unsigned long rate, int stringc, char *strings[])

The purpose of this function is to test whether your code is able to use the UART in polled mode for simplex communication. The arguments whose name is identical to some argument of ser_test_set() have the meaning of the corresponding argument. The tx argument specifies the role your program should play, whether that of the receiver, if tx is 0, or that of the transmitter, otherwise. In the latter case, stringc specifies the number of elements of the last argument, the array of strings strings to transmit.

(Added Nov. 18) Essentially, your code should configure the specified serial port with the communication parameters provided. Furthermore, if configured as a transmitter, it should send each of the strings in array strings one after the other, using polling. Between strings it should add a space character, ' '. Finally, after sending the last string it should send a dot, '.' and terminate.

(Added Nov. 18) Otherwise, if configured as a receiver, it should receive characters from the UART using polling and display them on the screen, until it receives a dot, which it should also display before terminating. (Therefore, to prevent the receiver from exiting prematurely, none of the strings should include a dot.)

5.4 int ser_test_int(/* to be provided in class */)

The purpose of this function is to test whether your code is able to use the UART with interrupts for simplex communication.

Details regarding this function will be provided at the beginning of your second class. Please note that not all sections will have to use communication in interrupt mode for both transmission and reception. You may even be asked to implement both transmission and reception with polling, as described previously.

5.5 int ser_test_fifo(/* to be provided in class */)

The purpose of this function is to test whether your code is able to use the UART with FIFOs for simplex communication.

Details regarding this function will be provided at the beginning of your second class. Please note that not some sections may be asked to use FIFOs for transmission and in polled mode, e.g., whereas others may be asked to use FIFOs for reception in interrupt mode, e.g..

6. Compiling and Running your Program

Follow the procedures described for the previous labs.

7. Configuring your Program

Your program invokes functions that are privileged. Thus, before you can run it, you need to add a file named lab7 to the /etc/system.conf.d/ directory to grant your program the necessary permissions.

Unless you use privileged functions that are not really necessary, the following entry is enough:

service lab7 { system DEVIO IRQCTL ; ipc SYSTEM rs vm pm vfs ; io 40:4 # Timer 60 # KBC 64 # KBC 70:2 # RTC 2f8:8 # COM2 3f8:8 # COM1 ; irq 0 # TIMER 0 IRQ 1 # KBD IRQ 3 # COM2 IRQ 4 # COM1 IRQ 8 # RTC 12 # AUX/MOUSE IRQ ; uid 0 ; };

8. Testing your Code

8.1 Testing Setup

To test serial communication you need to run both a transmitter and a receiver. In this lab, I recommend that you develop and debug your code using two VMware Players: one executing the driver in receiver mode and the other running the driver in transmitter mode. This is possible because the VMware player allows to connect the (virtual) serial port of a VM to the (virtual) serial port of another VM.

In the lab PCs, the two VM will be already properly set up. If you want to test your code on your own PC, you will have to create a new VM, for example by copying the one you already have. You can find the instructions to do that in the "online" help of the VM Player. Then, you need to add a serial port to both VMs. This can be done as follows (this is based on the steps I took in Linux; in Windows, this may be slightly different):

  1. Choose the Virtual Machine, on the initial window
  2. Choose the option "Edit Virtual Machine Settings"
  3. On the "Hardware" tab, click on the "+Add" button
  4. Select "Serial Port", and click on the "Next" button
  5. Select "Output to socket", and click on the "Next" button
  6. On the new window:
    1. Enter the file name /tmp/inter_vm (actually, this may be any name, but it is recommended that you use some file in the /tmp directory)
    2. Then select "Server" to "A Virtual Machine" (note that this step will be different for the other VM)
    3. Make sure that the "Connect at power on" option is selected.
  7. Finally click on the "Finish" button

The sequence of steps is exactly the same for both machines, but step 6.2 must be different: one of the VM must be configured as a "server", by selecting "Server" to "A Virtual Machine", whereas the other must be configured as a "client", by selecting "Client" to "A Virtual Machine".

These steps will add the first serial port to both VMs, i.e. COM1, and your code will be able to access its UART as described in the previous sections.

However, there is a small issue with this emulation: the two sides need not specify the same communication parameters for the communication to take place, except for the number of bits per character. This is because the two VMs are on the same computer and the communication between the two virtual serial ports is done via sockets, which do not use serial communication, and therefore do not need the communication parameters.

8.2 Final Remarks

Testing applications that consist of more than one process is always hard. In this case, you have to develop and test both the receiver and the transmitter functionality. However, to test the receiver you need a transmitter, preferably debugged. Likewise, to test the transmitter you need a receiver, preferably debugged.

The real problem is to have one side of the communication debugged. After that, you can test and debug the other side of the communication, independently of whether it uses polling, interrupts or any of these with FIFOs. The implementations on the two ends do not need to be equal to inter-operate. In principle, all you need is to use the same communication parameters on both sides.

In many operating systems, there are small utility programs that are able to use the serial port for basic tasks and they can be used to test each of the communication sides, one at a time. Unfortunately, I do not know of any Minix 3 utility with that capability. Therefore, to make your task easier I decided to provide one for this lab. By executing it without any arguments you'll get a "usage message", as follows:

lab7: Usage: one of the following: service run /home/lcom/2012/lab7/lab7 -args "conf 1|2" service run /home/lcom/2012/lab7/lab7 -args "set 1|2 <bits> <stop> even|odd|none <rate>" service run /home/lcom/2012/lab7/lab7 -args "com 1|2 tx|rx <bits> <stop> even|odd|none <rate> <string>*"

Where the arguments have the following meaning:

If you wish, you can play with this program. Make sure that the VMs are properly set, as described above, and that both serial ports are enabled (check the icons on the lower right corner of the VMware Player window). For testing communication, please start the receiver on one VM, and only afterwards start the transmitter on the other.

IMP lab7 sends a dot ('.') as a message terminator. Therefore, your strings should not include that character, otherwise the receiver will ignore the characters that follow the first dot.

9. Submission

As you complete the different milestones of this lab, you should commit your work to the SVN repository in Redmine.

I recommend that you also commit your code even if you have not completed the new functionality you are working on. I.e., I suggest that you use the SVN repository also as a backup. Although this is not the proper way of using an SVN repository, it may save your day. (As the platform we have been using appears not to be robust enough for some of you.)

IMP. You should add to the SVN repository all files needed so that compilation of your code can be done by checking out the lab7 directory, and executing:

make

under that directory in the working copy.

References

Acknowledgments

This lab is based on a lab by João Cardoso and Miguel P. Monteiro for DJGPP running on Windows98.