Linux login shell on a serial port

Monday, 28 November 2023

I wanted to be able to log in to my Linux desktop PC via a serial cable.

SSH is the obvious way to connect to a Linux PC from elsewhere, but this is only possible when a network connection can be established between the Linux PC and the other machine. In some cases, that's not possible. An RS-232 serial cable is an old-school fallback.

Linux can be set up to allow a user to connect via the serial port. It is exactly like the text-mode login process which is used if the X display is not running, or if you switch from the X display to a text console by press Control+Alt+F1. The user enters the username and password, and if they are correct, shell access is granted. It is very simple. You can connect to a Linux PC from very old hardware like this - the relevant standards date from 1960.

What happened to the COM port?

Many PCs do not have physical serial ports any more. At one time the DB-9 connector was a common sight on the back of a PC, and it was often a serial port, though the same connector was also used for CGA and MDA monitors, and more obscure uses are possible, such as CAN. Around 1990, the "COM" port was typically used for a mouse or a modem. But later, particularly after the introduction of the ATX standard, the mouse connection moved to a dedicated PS/2 port (and later to USB). The modem moved to a PCI expansion card, before being replaced entirely by Ethernet. There was no need for a "COM" port.

The serial port functionality often still existed as pins on the motherboard, and they could be brought to a physical port on the back of the case if required. Some motherboards still have this, but it is no longer common. Nowadays, if you need a serial port, it is usually easier to use to a USB adapter, as this also avoids any need to find the appropriate wiring hardware. Multiple USB serial adapters can be connected at the same time, which is useful in an environment where many serial ports are needed, e.g. when debugging or testing embedded systems.

Disadvantages of USB serial adapters

USB serial adapters have a disadvantage: bootloaders can't use them because special drivers are required. You cannot interact with GRUB or see the early boot messages from the Linux kernel. This is in contrast to serial ports built into the PC, which have a very standardised and simplistic interface (dating from the 8250 UART). These are always usable. But a USB serial adapter only becomes usable once the kernel has booted and the USB stack is running.

This rules out the "easy" way to configure a serial port for login. systemd includes a feature named systemd-getty-generator which will detect if Linux has been booted with output to a serial console (e.g. with the option console=ttyS0). Then it will create the appropriate configuration files so that the login prompt also appears on that serial port. This is very clever, but it will only work for a built-in serial port. For USB serial adapters, a different solution is needed.

A second problem with USB serial adapters is that the device name is not predictable if you have more than one. You may find that the order changes if you reboot. This would not happen with built-in serial ports: ttyS0 is always COM1, I/O port address 0x3f8. For USB serial ports, addresses and names are assigned dynamically, and are therefore unpredictable. Luckily, there is also a solution for this.

Using udev rules to assign specific device names to serial ports

The assignment of USB devices to names can be controlled using udev rules. You can create a rule stating that a specific USB device should be given a particular name. USB devices are recognised by attributes such as the vendor ID and the product ID.

Here is a rule to recognise a Silicon Labs CP2102 USB serial adapter and assign it a special name, console_serial:

    ACTION=="add", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", SYMLINK+="console_serial"

This rule should be placed in the udev rules directory. It might appear as the only line in a file named (for example) /etc/udev/rules.d/10-console-serial.rules.

The rule ensures that this particular USB device will be accessible as /dev/console_serial if it is connected.

If there were multiple CP2102 devices, then other attributes could be used to distinguish them based on the physical port (e.g. KERNELS). The correct values for these attributes can be found using a command such as udevadm info, as explained on this page. This is useful if you ever connect additional USB serial adapters to the PC, which is more likely than it may seem!

Arduino devices usually have a built-in USB serial adapter - so do AdaFruit boards, and I have seen FPGA boards and GPS receiver modules that also have them, along with embedded systems development kits, which sometimes have many serial adapters on board. Because this kind of device is often added or removed, the device names can be unpredictable. Therefore, it is a good idea to use an additional attribute such as KERNELS in order to dedicate a serial device to a specific purpose, such as a login console, just in case other USB devices happen to include serial adapters.

Some USB serial adapters also have unique serial numbers, or a way to set a unique ID using a tool. udev rules can recognise these serial numbers and use them to make the correct assignment, which is helpful in an environment where many USB serial adapters may be in use. This has the further advantage that moving the device to a different USB port will preserve the correct name. I understand that both Silicon Labs and FTDI devices have this feature, though a special programming tool from the manufacturer is needed, and I have not tried this myself.

Enabling a serial login console with systemd

Having set up a udev rule, the next step is to configure systemd to allow logins via the serial port. Assuming that the udev rule assigns the name "console_serial", the following two commands set up a basic service:

    sudo systemctl enable serial-getty@console_serial.service
    sudo systemctl start serial-getty@console_serial.service

The serial console settings are 9600 bits per second, 8 bits, no parity, 1 stop bit. It is a good idea to test that this is working. When connecting via the serial port, press Enter to make the login prompt appear. If it does not, enter journalctl -r on Linux to view the systemd log and check for any problems running the service.

Increasing the baud rate

9600 bits per second is pretty slow. The Linux console is usable but any command that produces a lot of output will be difficult to use.

Unfortunately systemd does not make it easy to change the baud rate. You need to replace the service file in order to customise it, and add a new command to set the rate. Here is how:

First, stop the existing service and disable it:

    sudo systemctl stop serial-getty@console_serial.service
    sudo systemctl disable serial-getty@console_serial.service

Next, create a modifiable copy of the service file:

    sudo cp /usr/lib/systemd/system/serial-getty@.service /etc/systemd/system/serial-getty@console_serial.service

Edit /etc/systemd/system/serial-getty@console_serial.service to replace the ExecStart line with the following two lines:

    ExecStart=-/sbin/agetty -o '-p -- \\u' -s - $TERM
    ExecStartPre=/usr/bin/stty -F /dev/%I speed 115200

The use of -s on the ExecStart line prevents agetty resetting the serial port's baud rate. The ExecStartPre line sets the baud rate to 115200.

Next, test these changes:

    sudo systemctl daemon-reload
    sudo systemctl enable serial-getty@console_serial.service
    sudo systemctl start serial-getty@console_serial.service

If there are any problems, journalctl -r may be helpful for debugging. The stty command can also be helpful to see the current configuration of the serial device, e.g. stty -F /dev/console_serial. In particular this shows the current baud rate. If it is 9600, something is wrong! You need to restart the service and use daemon-reload after making changes.

Higher speeds?

If you can get the "login" prompt at 115200 bits per second, you might like to try using a higher baud rate. USB serial adapters often support "high" baud rates such as 460800 or 921600, but be aware that higher rates also have a higher possibility of error. The RS-232 standard is not intended for high speeds and the data can easily become corrupted.

When using 921600 baud, I found that the up arrow key sometimes seemed to be typing a capital 'A'. I traced this to corruption of the escape sequence representing the up arrow, which is "\x1b[A". The solution was to lower the speed. The RS-232 standard does allow for some minimal error detection with parity bits, but there is no error correction, so I didn't bother to experiment with enabling that feature.

Colour

systemd assumes that the connected terminal is vt220, which does not support colour. You can edit the ExecStart command to replace $TERM with something more appropriate for whatever you are using to connect: for example, if using PuTTY, you might use putty-256color.

Summary

My final configuration is as follows:

In /etc/udev/rules.d/10-console-serial.rules, a configuration for a Silicon Labs CP2102 USB serial adapter connected to USB port 1-9. The device appears as /dev/console_serial:

    ACTION=="add", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", SYMLINK+="console_serial", KERNELS=="1-9"

In /etc/systemd/system/serial-getty@console_serial.service, a configuration for 115200 baud and a putty-256color terminal:

    ExecStart=-/sbin/agetty -o '-p -- \\u' -s - putty-256color
    ExecStartPre=/usr/bin/stty -F /dev/%I speed 115200