Sistemas Operativos 3º Ano MIEEC
Project - Lab 3

Change log:

2017-04-15
Added Section 6.6

2016-04-30
Added Section 3.0 on the configuration of VBox serial port.


1. Introduction

In the previous lab class, you have implemented the skeleton of a char device driver. In this class, you'll transform that code into a simple device driver for the PC's serial port using polling.

As in the previous labs, you will develop the device driver in an incremental way. Starting with a simple version containing only the initialization and cleanup functions of a Linux kernel module, and then adding multiple features. The final version will include a blocking read() system call and a write() system call that is able to send several characters, in addition to open() and close() system calls.

2. Setup (for the lab PCs)

2020 Update

This year, you will have to use your own PC. So, if you followed the instructions of the previous lab, you can skip this section.

If you are using one of the lab's PCs, you need to setup the VBox VM executing the following steps (assuming you have completed the first project lab):

Remove the VM configuration
If you have imported the VirtualBox appliance in the last lab class, you have now to delete the corresponding configuration as described in Section 2.3 of the first DD handout (DD1).

$ cd ~/VirtualBox\ VMs/ $ rm -rf CICA2
Import the VirtualBox appliance
As described in Section 2.1 of the first DD handout (DD1).
Configure the shared folders
As described in Section 2.2 of the first DD handout (DD1).

You should not need to repeat these steps until you reboot the host OS.

3. The PC's Serial Port

This section provides some background information about the PC's serial port that is fundamental for the development of this lab. Please read it carefully.

The serial port used to be one of the standard IO devices of a PC. However, because of its low performance it has been replaced by other buses, such as the USB, and most new PCs do not have a serial port builtin anymore. In any case, the serial port remains an important IO device to interface with real world devices in embedded applications and, if the PC does not have a builtin serial port, you can always buy a serial card. The VBox can be configured with up to 4 virtual serial ports.

In this handout, we will assume that you have already learned in another course what serial communication is, and therefore will not explain it here. If you'd like to refresh your knowledge you can check some slides I prepared for another course. It includes also some information on the Universal Asynchronous Receiver/Transmitter (UART), the controller of the PC's serial port.

Originally, the PC used the 8250 UART, but as time went by new and more performant UART's compatible with the 8250 were developed. As of 2012, the Virtual Box emulates the 16550A UART, which differs from the original 8250 UART, by supporting FIFO buffers among other features. Apparently, the most recent version of this UART is the 16550D UART from Texas Instruments. (It may be a good idea to make your own copy of this data-sheet, for you never know when it will be wiped out of the Web.)

3.0 Setting the VM's Serial Port

This step may not be necessary. It used to be performed in the first lab, but since this year we generated the VM image in a different way, it may as well be that the serial port is already properly configured. (It has been a while since the image was created.) If not, you'll have to do this step after every boot of your host machine, just like the steps in Section 2, above. In any case, just follow the steps described in the following paragraph after starting the VBox. If Serial Port 1 is already properly configured, i.e. you need not perform the actions described for its configuration, then you won't need to repeat this setting. If on the contrary you need to perform some action, you'll have to reconfigure the serial port every time you reboot the host.

Configuration of the VM's serial port can be done from the Settings window, which pops up when you select the Settings orange icon. On the list at the left of this window you should select the Serial Ports, which is towards the end of the list. On the right hand of the window you'll see now several tabs, one for each of the virtual serial ports supported by the VirtualBox. At this time you need only configure one of the ports, select Port 1. First, check the "Enable Serial Port" box. Then, in the pop-down "Port Mode" list select "Host Pipe". Next, uncheck the "Connect to existing pipe/socket" box, this way VBox will create the pipe when it starts. Finally, you need to specify a file system path on the host OS where this pipe will be created. I suggest that you specify a pipe in the /tmp/ subdirectory, for example /tmp/vbox_ser_port1.

The set of actions you have performed in the previous paragraph connect the serial port 1 of this VM to the "server end" of a pipe established using Unix domain sockets on the host machine. Basically, this means that you can communicate with serial port 1 of this VM using the Unix domain socket /tmp/vbox_ser_port1 in client mode on the host OS. How you can take advantage of this setting in the project will be explained later.

For more details regarding the configuration of serial ports and other virtual hardware components you can read the Chapter 3 of the User Manual.

3.1 UART Registers

The UART provides 10 addressable registers (each of which 8-bit wide) that are used for: 1) transfer data; 2) check the status of data transfer; 3) control data transfer. We will provide a brief description of 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

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 generated from a clock signal with 115 200 Hz by dividing its frequency 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 the 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. These registers are in the IA32 I/O address space and are accessible starting at the base address of the corresponding serial port. For example, if you wish to use COM1, the first serial port, you should add the address in the first column of Table 1 to COM1's base address, which is usually 0x3f8.

Addr. DLAB Read Write
0 0 Receiver Buffer (UART_RX) Transmitter Holding (UART_TX)
1 0 Interrupt Enable (UART_IER)
3 X Line Control (UART_LCR)
5 X Line Status (UART_LSR) -
0 1 Divisor Latch Least Significant Byte (UART_DLL)
1 1 Divisor Latch Most Significant Byte (UART_DLM)
Table 1: Address, symbolic name and acronym used in serial_reg.h of the relevant UART registers.

Although the goal of this project is to develop a device driver supporting polled operation of the UART, you will have to disable all the serial port interrupts by writing a 0 to the Interrupt Enable Register.

3.2 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 2), 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 2: 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. This value has to be split in its MSB and its LSB, and these bytes written to the DLM and DLL registers, respectively. 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 perform the necessary actions. (In the example given, this means reading the character from the UART.) Table 3 shows the bits of the LSR (the bits relevant for this lab are typed in bold).

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

When a character written to the "Transmitter Holding Register (THR)", (UART_TX, in serial_reg.h), has been moved to the UART's transmitter shift register, the UART sets the "Transmitter Holding Register Empty (THRE)" bit (UART_LSR_THRE, in serial_reg.h), and the device driver can write another character to the THR. When this happens, the THRE bit is automatically cleared.

When the UART puts a character received in the Receiver Buffer Register, it sets the "Data Ready" bit (UART_LSR_DR, in serial_reg.h). 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 C Bitwise Operations

To control the operation of the UART, the serial device driver will have to test or change the state of some bits of the UART registers. This is best done using C's bitwise operators, which make it easy to set/reset/test the value of one bit of an integer value. Table 4 summarizes the bitwise operators available in the C language.

C bitwise Operators
OperatorOperationExampleDescription
&anda & bLogical and of bit 0 of a with bit 0 of b, and so on for all bits of a and b
|ora | bLogical or of bit 0 of a with bit 0 of b, and so on for all bits of a and b
^xora ^ bExclusive or of bit 0 of a with bit 0 of b, and so on for all bits of a and b
~negation~aNegation of each of the bits of a
<<left shifta<<pShift the value of a, p bits to the left
>>rigth shifta>>pShift the value of a, p bits to the rigtht. (In some C implementations, depending on whether or not a is a signed integer, the shift is either arithmetic or logic, respectively).
Table 4: C Bitwise Operators

The following are some example C idioms for setting, resetting and testing bits using some of these operators. These examples use the symbolic constants defined in serial_reg.h

Setting bits
E.g. to configure the UART to use 7 bits per character, 1 stop bit and parity odd, you might build the following control byte:
unsigned char lcr = 0; lcr = UART_LCR_WLEN7 | UART_LCR_PARITY;
In some cases, you may wish to set only a bit (or a few of them) in a register without modifying the remaining bits. E.g., if you want to change the DLAB in the LCR without modifying the other bits, you might use the following code:
lcr |= UART_LCR_DLAB;
where we assume that the variable lcr contains the current value of the LCR.
Resetting bits
E.g. after setting the DL registers you need to reset the DLAB bit of the Line Control Register. Assuming that the value of this register is in the variable lcr you can use the following instruction:
lcr &= ~UART_LCR_DLAB;
Testing bits
E.g. to check whether the Transmit Holding Register is empty, you can use the following if clause, where we assume that the variable lsr contains the value of the Line Status Register:
if( lsr & UART_LSR_THRE )

4. The usocat utility

Debugging communication software is challenging. To communicate you need to have a sending and a receiving routine. Unless one of these routines is known to operate reliably, this task can be a nightmare.

In the first lab, we have asked you to configure the serial port of the VM so that it is connected to a software pipe on the host machine, which in the case of Linux is implemented as a Unix domain socket. With this setting, an application on the guest using a serial port can communicate with an application on the host using a Unix domain socket. Therefore, to debug your code on the guest, you can use on the host one of several Linux tools that are able to connect to such a socket.

Because we did not find any such tool installed on the lab PCs, we have developed a very simple utility usocat, whose source code you can find in usocat.c. It is inspired on the socat tool, but its functionality is rather minimal and supports communication via only Unix domain sockets.

Note The Unix domain socket belongs to the host OS. Thus, all the steps described in this section that are related to such a socket or to the usocat utility, should be performed on the host OS.

After compiling usocat.c the usual way, you can invoke it as follows:

./usocat - /tmp/vbox_ser_port1

to forward whatever you type on the stdin (of the host OS), i.e. the keyboard, to the serial port on the VM, via the Unix domain socket /tmp/vbox_ser_port1, assuming you have followed the instructions in the first lab.

If you wish to send data from the VM via its virtual serial port to the host OS, you can use usocat as follows. On the host OS you can type:

./usocat /tmp/vbox_ser_port1 -

which sends to the standard output whatever it receives from the Unix domain socket /tmp/vbox_ser_port1. On the guest OS you can execute the command:

cat > /dev/ttyS0

which sends to the virtual serial port whatever you type on the keyboard. The link between the virtual serial port and the Unix domain socket on the host OS is provided by the VirtualBox.

IMP. By default usocat does not buffer the data input from the standard input, i.e. it writes the characters to the socket as soon as you press a key in the keyboard. This behavior makes it easier to test your code. However, usocat can also buffer the keys until you press the Enter key, if you invoke it as follows:

./usocat -b - /tmp/vbox_ser_port1

The -b option is only valid if the second argument is -. If not, usocat will terminate with an usage message.

5. Disabling the standard serial device driver

Before you start coding your device driver, you need to disable the standard serial device driver.

In principle, each I/O device should be under control of a single device driver. Apparently the way this is ensured is to require the device drivers to reserve the I/O port range used by the corresponding I/O device, using the function declared in <linux/ioport.h>:

struct resource *request_region(unsigned long first, unsigned long n,const char *name);

where first is the first I/O port in the range, n is the number of ports to reserve and name is the name of the device driver. If the I/O port range requested is already being used, request_region returns NULL and the device driver is supposed not to try to access the I/O port range, just like a process should not enter a critical section if an attempt to lock a mutex without waiting fails. In principle, this means that module initialization should fail.

When a device driver is unloaded it should free the I/O port range using the function:

void release_region(unsigned long start, unsigned long n);

thus allowing other device drivers to gain control of the device.

You can see the I/O port ranges currently reserved by the kernel via the /proc filesystem:

cat /proc/ioports

Executing this command in the guest, will show you that the I/O port range of the first serial port (0x3f8-0x3ff) is already assigned to the serial device driver, which is the standard serial device driver.

Unfortunately, the standard serial device driver is not a kernel module, but built into the kernel, and as far as I know there is no clean way to disable it. Therefore I've developed the disser kernel module that uses an ugly hack to disable it, and the disableserial.sh shell script that not only disables the serial port with the help of the disser kernel module, but also removes the file system special files (/dev/ttyS*) used by the standard serial device driver. Thus, after downloading the kernel module and the shell script to /host/lab3, you can disable the standard serial device driver by invoking the shell script as follows:

vbox:/host/lab3$ sudo ./disableserial.sh

After that, access to the serial port via the standard serial device driver will not be possible. The only way to regain access to the serial port via the standard serial device driver is to reboot the guest.

6. The serp char device driver

In this section we'll outline the development of the serp (for serial, polled mode) char device driver that supports simple communication via the PC's serial port using polled I/O.

6.1 Version 1 Driver: A Module whose Initialization Function Sends a Char

With the standard serial device driver out of the way, you can now start the development of your own serial device driver.

The first version of the serp device driver is very simple. In its initialization function it should:

  1. initialize the UART with the following communication parameters: 8-bit chars, 2 stop bits, parity even, and 1200 bps. Because, we will not use interrupts, make sure that the UART is configured not to generate interrupts
  2. send one character via the UART

Although you need to reserve the serial port I/O port range by invoking request_region(), you need not dynamically allocate the major device number nor create the /dev special files, because in this version your driver is not supposed to be accessed by applications.

6.1.1 I/O port access

As already mentioned, in a PC, the UART registers are in the I/O address space, more specifically they use the I/O port range 0x3f8-0x3ff. Thus, to access these registers the processor must issue IN and OUT assembly instructions.

For reasons of portability, Linux offers a set of functions inb()/outb(), inw()/outw), inl()/outl() which allow to read/write data from/to I/O ports. These functions differ on the size of the registers being accessed: inb()/outb() are for 8-bit registers, inw()/outw() for 16-bit registers, and inl()/outl() for 32-bit registers.

Because the UART registers are 8-bit wide, or exported as such, you need use only the functions:

unsigned inb(unsigned port); void outb(unsigned char byte, unsigned port);

Note that these functions are declared in <asm/io.h> because their implementation is architecture specific. In particular, the size of port argument may vary; in the case of the IA32 architecture is an unsigned int. The same applies to the return type of inb, which in the case of the IA32 architecture is a char.

IMP Note that in outb() the port is the second operand whereas the first operand is the data to output.

To write the code for UART access you may wish to use the header file serial_reg.h, which is a leaner version of the standard <linux/serial_reg.h> header file that contains only the definitions for the registers used in this lab.

6.1.2 Waiting for the Transmitter to be ready

Your device driver should probe the THRE (Transmit Hold Register Empty) bit of the LSR (Line Status Register) to ensure that it can write the character without overwriting another character. If the THR is not empty, the driver should wait. Rather than executing a tight test loop, the driver might release the CPU invoking the following function that is declared in <linux/sched.h>:

void schedule();

Given that even at 1200 bps it takes less than 10 ms to send a character, when the "driver" (actually, the process executing the initialization function) is rescheduled to run again, it is likely the transmitter will be ready for the next char.

6.1.3 Test of the driver

To test this version of the driver, you should use the usocat program. First, start usocat to receive data from the Unix domain socket /tmp/vbox_ser_port1 and to display it on the standard output. After that, you should insert the serp module into the kernel, by executing the command insmod serp.ko. Don't forget to remove your module once you are done.

6.2 Version 2 Driver: Add Open, Close and Write System Calls (Write for Single Character Send)

This new version shall support open(), close() and write() for a single character.

This version should not raise major issues. Essentially, all you need to do is to adapt the echo driver from Lab 2 to use the serial port. In particular, you'll have to create special files so that applications are able to use your driver.

To test this new version you'll need to develop a small application that opens the special files of your driver and invokes the relevant system calls. For example, to send several characters, your applications may invoke write() with a single character. Of course, you'll need to use the usocat program on the host.

6.3 Version 3 Driver: Add Read System Call without Blocking for Single Character Receive

In this new version you'll add a read() file operation to support a read system call. However, this file operation will be non-blocking: i.e. it will try to read a character from the UART receiver buffer register, if available, otherwise it will return immediately. Thus, the read() file operation should return one of the 3 following values:

-EAGAIN
there was no character on the UART's receive buffer. The application may try later;
1
there was a character on the UART's receive buffer, which was copied to the input buffer. No error was detected.
-EIO
there was some error on reception, i.e. one or more characters were lost because the program is not sufficiently fast.

To test this new version of the driver you will need to use usocat again. However, this time rather than writing to stdout what it receives in the Unix domain socket, it should write to the latter what it reads in its stdin, which should be what you type.

Furthermore, you'll need to write a user space program to be executed on the guest. This program should open the special device file, first. After that it should call the read() system call in a tight loop. It should break out of the loop only upon reception of a character. Finally, it should close the special device file.

IMP. For better results you should not use usocat's -b option, because with that option the standard input is buffered. Therefore, with that option on, whatever you type on the keyboard will not be written to the socket until you press the ENTER key. At that time, however, the chars will be sent quickly one after the other, and there is the possibility of a receiver overrun. Without buffering, whatever you press will be written immediately to the socket, and your test program on the guest should be able to receive it without errors, if you type sufficiently slow.

6.4 Version 4 Driver: Add a Blocking Read Call

In this new version you will change the read() system call to return only after receiving some character or on an error condition.

Therefore the implementation of the read() file operation should continually query the UART, until it receives a character or an error event occurs. To prevent a very tight busy loop that could affect other processes, if they existed, you can use an approach similar to the one described in Section 6.1.2, i.e to invoke the schedule() function. Some alternatives include:

signed long schedule_timeout(signed int timeout) (declared in <linux/sched.h>)
This function puts the process to sleep for a time interval whose duration is specified in a time unit known as jiffy, which is the period of a periodic interrupt that is used by the kernel to keep track of time. This period is configurable, and the number of jiffies in a second is the value of the symbolic constant HZ, which is defined in <linux/jiffies.h>, which is automatically included by <linux/sched.h>. Note that to specify a time interval in jiffies, you really do not need to know the value of HZ. In any case, for your information I configured the kernel that is installed in the virtual disk image so that HZ=100 to reduce the load of VM on the host, as suggested by the manual of the VBox. The call to sched_timeout() must be preceded by a call to set_current_state() as follows:
set_current_state(TASK_INTERRUPTIBLE); schedule_timeout(delay);
unsigned long msleep_interruptible(unsigned int millisecs) (declared in <linux/delay.h>)
This function puts the process to sleep for a time interval specified in milliseconds in its argument. Essentially, it is equivalent to schedule_timeout(), but slightly more convenient to use.

A more detailed discussion of this issue can be found in Section "Delaying Execution" of Ch. 7 of the LDD3 book, on pg. 190 and following.

You can test this new version of the driver using the program you have used to test version 3.

6.5. Version 5 Driver: Add Multiple Characer Send Call

In this new version you will change the write() file operation to allow sending more than one character with a single system call invocation. Indeed, the previous version of the write() file operation requires one system call invocation per character, which is not very efficient.

This new version should be quiet straightforward, as the implementation of the write() operation already uses the same mechanisms as those of the blocking read() operation. I.e. it polls the UART, and if it is not ready yet to send a new character, it yields the processor. As mentioned above, an alternative would be to sleep for a while.

To test this new version of the driver you can write a new test program based on the one you used in Version 2 of the driver, in Section 6.2, that rather than write a single character should write a string with several characters.

6.6. Version 6 Driver: Add Multiple Characer Read Call

In this new version you will change the read() file operation to allow receiving more than one character with a single system call invocation. Indeed, the previous version of the read() file operation requires one system call invocation per character, which is not very efficient if on the other end is an application like usocat may send a burst of characters. For these applications it might be interesting to allow returning more than one character per read() syscall.

The main issue in this new version is to avoid getting stuck inside the kernel after the burst. Thus, what you can do is use a timeout: after the reception of the first character, if no character is received for some time, the read system call should return to the user whith all the characters it has received up to that instant.

To test this new version of the driver you can write a new test program based on the one you used in Version 3 of the driver, in Section 6.3, that, rather than read one character and exit, should read several characters and print all the characters read before exiting.

Acknowledgments

José Manuel Cruz provided very useful feedback regarding the first version of this handout. Furthermore, he has been steadily improving the usocat utility. More specifically, he was responsible for the last three releases. The first of these, fixed a bug in the first version of the usocat utility that would leave the socket in connected state, if a user terminated it with, for example, Ctrl-C. In the second, he provided the code to reset the terminal to "cannonical mode", at exit of usocat when it is invoked without the -b option. Finally, in the third, he made some improvements that make usocat's output more convenient for testint/debugging the DD's code.