Serial Ports

Jump to navigation Jump to search

Serial ports are a legacy communications port common on IBM-PC compatible computers. Use of serial ports for connecting peripherals has largely been deprecated in favor of USB and other modern peripheral interfaces, however it is still commonly used in certain industries for interfacing with industrial hardware such as CNC machines or commercial devices such as POS terminals. Historically it was common for many dial-up modems to be connected via a computer's serial port, and the design of the underlying UART hardware itself reflects this.

Serial ports are typically controlled by UART hardware. This is the hardware chip responsible for encoding and decoding the data sent over the serial interface. Modern serial ports typically implement the RS-232 standard, and can use a variety of different connector interfaces. The DE-9 interface is the one most commonly used connector for serial ports in modern systems.

Serial ports are of particular interest to operating-system developers since they are much easier to implement drivers for than USB, and are still commonly found in many x86 systems. It is common for operating-system developers to use a system's serial ports for debugging purposes, since they do not require sophisticated hardware setups and are useful for transmitting information in the early stages of an operating-system's initialization. Many emulators such as QEMU and Bochs allow the redirection of serial output to either stdio or a file on the host computer.

Wires, Pins, Connectors and the like

The Wikipedia page on Serial ports has a lot of information, and it is summarised here. The serial interface is very simple. There are actually two kinds of serial port: 25-pin and 9-pin. 25-pin ports are not any better, they just have more pins (most unused) and are bigger. 9-pin is smaller and is used more often though in the past the 25-pin ones were used more often. The 9-pin ones are called DE-9 (or more commonly, DB-9 even though DE-9 is its technical name) and the 25-pin ones are called DB-25. They plug in to your computer using a female plug (unless your computer is odd and has a female port, in which case your cable will need a male plug). This Wikipedia page has more information on the plug used.

Both have the same basic types of pins. A DB-25 has most of the pins as ground pins or simply unconnected, whereas a DE-9 has only one ground pin. There is a transmitting pin (for sending information away) and a receiving pin (for getting information). Most serial ports run in a duplex mode--that is, they can send and receive simultaneously. There are a few other pins, used for hardware handshaking. In the past, there was no duplex mode, so if a computer wanted to send something it had to tell the other device or computer that it was about to transmit, using one of the hardware handshaking pins. The other device would then use another handshaking pin to tell it to send whatever it wanted to send. Today there is duplex mode, but the handshaking pins are still used.

If you want to connect two computers, you need two things in your cable:

  1. The cable needs to have two female plugs so it can plug into both computers.
  2. The cable needs to have its transmit-receive wires and it's handshaking wires switched. This can be done in the cable itself, or as an extension called a Null Modem

For serial devices, you don't need to setup the cable this way. The receiving end of the device has the wires switched and it has a female port, which means you can plug a male plug into it.

Why Use a Serial Port?

During the early stages of kernel development, you might wonder why you would bother writing a serial driver. There are several reasons why you might:

GDB debugging
You can use the serial port to connect to a host computer, and use the GDB debugger to debug your operating system. This involves writing a stub for GDB within your OS.
Headless console
You can operate the computer without a monitor, keyboard or mouse and instead use the serial port as a console using a protocol such as TTY or VT100.
External logging
When the system itself is in danger of potentially crashing at times, it's nice to get debugging outputs safe to another computer before the test system triple-faults.
Networking and File transfers
Serial ports are useful for transferring information between systems when other more traditional methods are unavailable.

Programming the Serial Communications Port

If you want to use the serial port for communications, you first have to initialize it. You tell it how fast your connection speed between the other computer or device will be (this is called the baud rate)--you must have the same speed as the other device or computer is setup to use, or you will have problems. It is probably safer to use the slower speeds unless you need the faster speeds for some reason, for example if you are playing a multi-player game over a serial connection. You also need to setup the parity type and the number of bits in a character. Once again, your computer must be setup with the same values for these things as the other computer or device has, or communication will not work.

Once you have setup these things, you still need to setup the interrupt handlers. You can poll the port to see if any new things have arrived, or if it's time to send another character, but this slows things down and will not work very well in most real-time applications or multi-threaded environments. In the case of a game, this is not a good idea at all.

You use IRQ #4 for COM ports 1 or 3, and IRQ #3 for COM ports 2 or 4 (you can tell which port sent the interrupt when you receive the interrupt). The IRQ handlers check if you are receiving something, and if so they receive the character and handle it somehow, such as placing it into a buffer. They also check if the other side is ready to receive something from you, and if you have something to send, it is sent.

Port Addresses

The addresses for COM ports can vary depending on how they are connected to the machine and how the BIOS is configured. Some BIOS configuration utilities allow you to see and set what these are, so if you in doubt for a test machine, this might be a good place to look to get you started.

For the most part, the first two COM ports will be at the addresses specified, the addresses for further COM ports are less reliable.

COM Port IO Port
COM1 0x3F8
COM2 0x2F8
COM3 0x3E8
COM4 0x2E8
COM5 0x5F8
COM6 0x4F8
COM7 0x5E8
COM8 0x4E8

You might be able to find the IO port addresses of the COM ports in the BIOS Data Area; however be warned that this won't work on modern/UEFI systems, can tell you about serial ports that only exist in the chipset (and lack any kind of connector that anything can be plugged into), won't tell you about any additional serial ports (e.g. on expansion cards, etc) that firmware doesn't/can't know about, and will make your OS susceptible to "BIOS quirks/bugs". Because the serial ports have relatively standard IO ports it's far more effective to use manual probing techniques instead; specifically, see if the scratch pad register can store a value, then try the loopback test (that you should use to determine if the serial port is faulty anyway).

Once you have the base address of your COM port, you add an offset value to get to one of the data registers. One of the registers hold what is termed the DLAB or Divisor Latch Access Bit. When this bit is set, offsets 0 and 1 are mapped to the low and high bytes of the Divisor register for setting the baud rate of the port. When this bit is clear, offsets 0 and 1 are mapped to their normal registers. The DLAB bit only affects port offsets 0 and 1, the other offsets ignore this setting.

IO Port Offset Setting of DLAB I/O Access Register mapped to this port
+0 0 Read Receive buffer.
+0 0 Write Transmit buffer.
+1 0 Read/Write Interrupt Enable Register.
+0 1 Read/Write With DLAB set to 1, this is the least significant byte of the divisor value for setting the baud rate.
+1 1 Read/Write With DLAB set to 1, this is the most significant byte of the divisor value.
+2 - Read Interrupt Identification
+2 - Write FIFO control registers
+3 - Read/Write Line Control Register. The most significant bit of this register is the DLAB.
+4 - Read/Write Modem Control Register.
+5 - Read Line Status Register.
+6 - Read Modem Status Register.
+7 - Read/Write Scratch Register.

Line Protocol

The serial data transmitted across the wire can have a number of different parameters set. As a rule, the sending device and the receiving device require the same protocol parameter values written to each serial controller in order for communication to be successful.

These days you could consider 8N1 (8 bits, no parity, one stop bit) pretty much the default.

Baud Rate

The serial controller (UART) has an internal clock which runs at 115200 ticks per second and a clock divisor which is used to control the baud rate. This is exactly the same type of system used by the Programmable Interrupt Timer (PIT).

In order to set the speed of the port, calculate the divisor required for the given baud rate and program that in to the divisor register. For example, a divisor of 1 will give 115200 baud, a divisor of 2 will give 57600 baud, 3 will give 38400 baud, etc.

Do not be tempted to use a divisor of 0 to try to get an infinite baud rate, it won't work. Most serial controllers will generate a unspecified and unpredictable baud rate (and anyway infinite baud would mean infinite transmission errors as they are proportional.)

To set the divisor to the controller:

  1. Set the most significant bit of the Line Control Register. This is the DLAB bit, and allows access to the divisor registers.
  2. Send the least significant byte of the divisor value to [PORT + 0].
  3. Send the most significant byte of the divisor value to [PORT + 1].
  4. Clear the most significant bit of the Line Control Register.

Line Control Register

The Line Control register sets the general connection parameters.

Bit 7 Bit 6 Bits 5-3 Bit 2 Bits 1-0
Divisor Latch Access Bit Break Enable Bit Parity Bits Stop Bits Data Bits

Data Bits

The number of bits in a character is variable. Having fewer bits is, of course, faster, but they store less information. If you are only sending ASCII text, you probably only need 7 bits.

Set this value by writing to the two least significant bits of the Line Control Register [PORT + 3].

Bit 1 Bit 0 Character Length (bits)
0 0 5
0 1 6
1 0 7
1 1 8

Stop Bits

The serial controller can be configured to send a number of bits after each character of data. These reliable bits can be used to by the controller to verify that the sending and receiving devices are in phase.

If the character length is specifically 5 bits, the stop bits can only be set to 1 or 1.5. For other character lengths, the stop bits can only be set to 1 or 2.

To set the number of stop bits, set bit 2 of the Line Control Register [PORT + 3].

Bit 2 Stop bits
0 1
1 1.5 / 2 (depending on character length)

Parity Bits

The controller can be made to add or expect a parity bit at the end of each character of data transmitted. With this parity bit, if a single bit of data is inverted by interference, a parity error can be raised. The parity type can be NONE, EVEN, ODD, MARK or SPACE.

If parity is set to NONE, no parity bit will be added and none will be expected. If one is sent by the transmitter and not expected by the receiver, it will likely cause an error.

If the parity is MARK or SPACE, the parity bit will be expected to be always set to 1 or 0 respectively.

If the parity is set to EVEN or ODD, the controller calculates the accuracy of the parity by adding together the values of all the data bits and the parity bit. If the port is set to have EVEN parity, the result must be even. If it is set to have ODD parity, the result must be odd.

To set the port parity, set bits 3, 4 and 5 of the Line Control Register [PORT + 3].

Bit 5 Bit 4 Bit 3 Parity
- - 0 NONE
0 0 1 ODD
0 1 1 EVEN
1 0 1 MARK
1 1 1 SPACE

Interrupt enable register

To communicate with a serial port in interrupt mode, the interrupt-enable-register (see table above) must be set correctly. To determine which interrupts should be enabled, a value with the following bits (0 = disabled, 1 = enabled) must be written to the interrupt-enable-register:

Bit 7-4 Bit 3 Bit 2 Bit 1 Bit 0
Reserved Modem Status Receiver Line Status Transmitter Holding Register Empty Received Data Available

First In First Out Control Register

The First In / First Out Control Register (FCR) is for controlling the FIFO buffers. Access this register by writing to port offset +2.

Bits 7-6 Bits 5-4 Bit 3 Bit 2 Bit 1 Bit 0
Interrupt Trigger Level Reserved DMA Mode Select Clear Transmit FIFO Clear Receive FIFO Enable FIFO's

Clear Transmit FIFO and Clear Receive FIFO

Bit 2 being set clears the Transmit FIFO buffer while Bit 1 being set clears the Receive FIFO buffer. Both bits will set themselves back to 0 after they are done being cleared.

Interrupt Trigger Level

The Interrupt Trigger Level is used to configure how much data must be received in the FIFO Receive buffer before triggering a Received Data Available Interrupt.

Bit 7 Bit 6 Trigger Level
0 0 1 Byte
0 1 4 Bytes
1 0 8 Bytes
1 1 14 Bytes

Interrupt Identification Register

The Interrupt Identification Register (IIR) is for identifying pending interrupts. Access this register by reading from port offset +2.

Bits 7-6 Bits 5-4 Bit 3 Bit 2-1 Bit 0
FIFO Buffer State Reserved Timeout Interrupt Pending (UART 16550) or Reserved Interrupt State Interrupt Pending

Interrupt State

After Interrupt Pending is set, the Interrupt State shows the interrupt that has occurred. They have varying levels of priority, with high-value interrupts handled first, and low-value interrupts being handled last.

Bit 2 Bit 1 Interrupt Priority
0 0 Modem Status 4 (Lowest)
0 1 Transmitter Holding Register Empty 3
1 0 Received Data Available 2
1 1 Receiver Line Status 1 (Highest)

FIFO Buffer State

Bit 7 Bit 6 State
0 0 No FIFO
0 1 FIFO Enabled but Unusable
1 0 FIFO Enabled

Modem Control Register

The Modem Control Register is one half of the hardware handshaking registers. While most serial devices no longer use hardware handshaking, The lines are still included in all 16550 compatible UARTS. These can be used as general purpose output ports, or to actually perform handshaking. By writing to the Modem Control Register, it will set those lines active.

Bit Name Meaning
0 Data Terminal Ready (DTR) Controls the Data Terminal Ready Pin
1 Request to Send (RTS) Controls the Request to Send Pin
2 Out 1 Controls a hardware pin (OUT1) which is unused in PC implementations
3 Out 2 Controls a hardware pin (OUT2) which is used to enable the IRQ in PC implementations
4 Loop Provides a local loopback feature for diagnostic testing of the UART
5 0 Unused
6 0 Unused
7 0 Unused

Most PC serial ports use OUT2 to control a circuit that disconnects (tristates) the IRQ line. This makes it possible for multiple serial ports to share a single IRQ line, as long as only one port is enabled at a time. Loopback mode is a diagnostic feature. When bit 4 is set to logic 1, the following occur the transmitter Serial Output (SOUT) is set to the Marking (logic 1) state; the receiver Serial Input (SIN) is disconnected; the output of the Transmitter Shift Register is ‘‘looped back’’ into the Receiver Shift Register input; the four MODEM Control inputs (DSR, CTS, RI, and DCD) are disconnected; and the four MODEM Control outputs (DTR, RTS, OUT 1, and OUT 2) are internally connected to the four MODEM Control inputs, and the MODEM Control output pins are forced to their inactive state (high). In the loopback mode, data that is transmitted is immediately received. This feature allows the processor to verify the transmit-and received- data paths of the UART. In the loopback mode, the receiver and transmitter interrupts are fully operational. Their sources are external to the part. The MODEM Control Interrupts are also operational, but the interrupts’ sources are now the lower four bits of the MODEM Control Register instead of the four MODEM Control inputs. The interrupts are still controlled by the Interrupt Enable Register.

Line Status Register

The line status register is useful to check for errors and enable polling.

Bit Name Meaning
0 Data ready (DR) Set if there is data that can be read
1 Overrun error (OE) Set if there has been data lost
2 Parity error (PE) Set if there was an error in the transmission as detected by parity
3 Framing error (FE) Set if a stop bit was missing
4 Break indicator (BI) Set if there is a break in data input
5 Transmitter holding register empty (THRE) Set if the transmission buffer is empty (i.e. data can be sent)
6 Transmitter empty (TEMT) Set if the transmitter is not doing anything
7 Impending Error Set if there is an error with a word in the input buffer

Modem Status Register

This register provides the current state of the control lines from a peripheral device. In addition to this current-state information, four bits of the MODEM Status Register provide change information. These bits are set to a logic 1 whenever a control input from the MODEM changes state. They are reset to logic 0 whenever the CPU reads the MODEM Status Register

Bit Name Meaning
0 Delta Clear to Send (DCTS) Indicates that CTS input has changed state since the last time it was read
1 Delta Data Set Ready (DDSR) Indicates that DSR input has changed state since the last time it was read
2 Trailing Edge of Ring Indicator (TERI) Indicates that RI input to the chip has changed from a low to a high state
3 Delta Data Carrier Detect (DDCD) Indicates that DCD input has changed state since the last time it ware read
4 Clear to Send (CTS) Inverted CTS Signal
5 Data Set Ready (DSR) Inverted DSR Signal
6 Ring Indicator (RI) Inverted RI Signal
7 Data Carrier Detect (DCD) Inverted DCD Signal

If Bit 4 of the MCR (LOOP bit) is set, the upper 4 bits will mirror the 4 status output lines set in the Modem Control Register.


Main article: Terminals

Once you can send and receive bytes with confidence, you probably want to connect the serial port to a terminal (or more likely a terminal emulator these days). Those send specific byte sequences when a key is pressed, and can interpret codes to move the cursor on the screen and change color for example.

Example Code


#define PORT 0x3f8          // COM1

static int init_serial() {
   outb(PORT + 1, 0x00);    // Disable all interrupts
   outb(PORT + 3, 0x80);    // Enable DLAB (set baud rate divisor)
   outb(PORT + 0, 0x03);    // Set divisor to 3 (lo byte) 38400 baud
   outb(PORT + 1, 0x00);    //                  (hi byte)
   outb(PORT + 3, 0x03);    // 8 bits, no parity, one stop bit
   outb(PORT + 2, 0xC7);    // Enable FIFO, clear them, with 14-byte threshold
   outb(PORT + 4, 0x0B);    // IRQs enabled, RTS/DSR set
   outb(PORT + 4, 0x1E);    // Set in loopback mode, test the serial chip
   outb(PORT + 0, 0xAE);    // Test serial chip (send byte 0xAE and check if serial returns same byte)

   // Check if serial is faulty (i.e: not same byte as sent)
   if(inb(PORT + 0) != 0xAE) {
      return 1;

   // If serial is not faulty set it in normal operation mode
   // (not-loopback with IRQs enabled and OUT#1 and OUT#2 bits enabled)
   outb(PORT + 4, 0x0F);
   return 0;

Notice that the initialization code above writes to [PORT + 1] twice with different values. This is once to write to the Divisor register along with [PORT + 0] and once to write to the Interrupt register as detailed in the previous section. The second write to the Line Control register [PORT + 3] clears the DLAB again as well as setting various other bits.

Receiving data

int serial_received() {
   return inb(PORT + 5) & 1;

char read_serial() {
   while (serial_received() == 0);

   return inb(PORT);

Sending data

int is_transmit_empty() {
   return inb(PORT + 5) & 0x20;

void write_serial(char a) {
   while (is_transmit_empty() == 0);



Baud Rate
The speed at which the serial line switches between it's two states. This is not equivalent to bps, due to the fact there are start and stop bits. On an 8/N/1 line, 10 baud = 1 byte. Modems are more complex than plain serial lines due to having multiple waveforms, but for the purposes of OSDev this is irrelevant.
The fastest baud rate a serial port can reliably run at is generally 115200 baud.
Baud Rate Divisor
The value the is used by the UART to divide its internal clock by in order to get the actual intended baud rate.
Stop Bits
The NULL bit(s) sent between each character to synchronize the transmitter and the receiver.
For Universal Asynchronous Receiver/Transceiver: the chip that picks a byte a send it bit per bit on the serial line and vice versa.

Related Links