Skip to content

Instantly share code, notes, and snippets.

@bstreiff
Last active January 8, 2025 03:24
Show Gist options
  • Save bstreiff/2992c8246a31dfa1f176781f5dc60119 to your computer and use it in GitHub Desktop.
Save bstreiff/2992c8246a31dfa1f176781f5dc60119 to your computer and use it in GitHub Desktop.
Predictable Serial Port Names

[NOTE: this is mostly a collection of notes and not quite a specification nor an implementation thereof]

Predictable Serial Port Names

"How do I identify what I should open() for my RS-232 or RS-485 serial port?" is a surprisingly difficult question to answer on Linux.

You might think that "/dev/ttyS0, /dev/ttyS1, etc are my serial ports in order, right?" and the answer to that is Maybe.

First off, your serial ports might not even be /dev/ttyS*. USB serial ports can be /dev/ttyACM* or /dev/ttyUSB*. Depending on your hardware you might have one of many other different prefixes!

bstreiff@starthinker:~/git/linux$ grep -r dev_name drivers/tty | grep \"tty
drivers/tty/serial/apbuart.c:	.dev_name = "ttyS",
drivers/tty/serial/sunsu.c:	.dev_name		= "ttyS",
drivers/tty/serial/sunzilog.c:	.dev_name	=	"ttyS",
drivers/tty/serial/sunhv.c:	.dev_name		= "ttyHV",
drivers/tty/serial/sunplus-uart.c:	.dev_name	= "ttySUP",
drivers/tty/serial/21285.c:	.dev_name		= "ttyFB",
drivers/tty/serial/icom.c:	.dev_name = "ttyA",
drivers/tty/serial/mxs-auart.c:	.dev_name	= "ttyAPP",
drivers/tty/serial/timbuart.c:	.dev_name = "ttyTU",
drivers/tty/serial/altera_uart.c:	.dev_name	= "ttyAL",
drivers/tty/serial/qcom_geni_serial.c:	.dev_name = "ttyMSM",
drivers/tty/serial/qcom_geni_serial.c:	.dev_name = "ttyHS",
drivers/tty/serial/amba-pl011.c:	.dev_name		= "ttyAMA",
drivers/tty/serial/altera_jtaguart.c:	.dev_name	= "ttyJ",
drivers/tty/serial/liteuart.c:	.dev_name = "ttyLXU",
drivers/tty/serial/bcm63xx_uart.c:	.dev_name	= "ttyS",
drivers/tty/serial/sccnxp.c:	s->uart.dev_name	= "ttySC";
drivers/tty/serial/digicolor-usart.c:	.dev_name	= "ttyS",
drivers/tty/serial/sa1100.c:	.dev_name		= "ttySA",
drivers/tty/serial/mcf.c:	.dev_name	= "ttyS",
drivers/tty/serial/tegra-tcu.c:	tcu->driver.dev_name = "ttyTCU";
drivers/tty/serial/men_z135_uart.c:	.dev_name = "ttyHSU",
drivers/tty/serial/mpc52xx_uart.c:	.dev_name	= "ttyPSC",
drivers/tty/serial/jsm/jsm_driver.c:	.dev_name	= "ttyn",
drivers/tty/serial/serial-tegra.c:	.dev_name	= "ttyTHS",
drivers/tty/serial/ucc_uart.c:	.dev_name       = "ttyQE",
drivers/tty/serial/amba-pl010.c:	.dev_name		= "ttyAM",
drivers/tty/serial/zs.c:	.dev_name		= "ttyS",
drivers/tty/serial/mvebu-uart.c:	.dev_name		= "ttyMV",
drivers/tty/serial/pxa.c:	.dev_name	= "ttyS",
drivers/tty/serial/max3100.c:	.dev_name       = "ttyMAX",
drivers/tty/serial/ar933x_uart.c:	.dev_name	= "ttyATH",
drivers/tty/serial/dz.c:	.dev_name		= "ttyS",
drivers/tty/serial/sunsab.c:	.dev_name		= "ttyS",
drivers/tty/serial/sh-sci.c:	.dev_name	= "ttySC",
drivers/tty/serial/mux.c:	.dev_name = "ttyB",
drivers/tty/serial/ip22zilog.c:	.dev_name	= "ttyS",
drivers/tty/serial/sc16is7xx.c:	.dev_name	= "ttySC",
drivers/tty/serial/max310x.c:	.dev_name	= "ttyMAX",
drivers/tty/serial/rp2.c:	.dev_name			= "ttyRP",
drivers/tty/serial/lantiq.c:	.dev_name =	"ttyLTQ",
drivers/tty/serial/msm_serial.c:	.dev_name = "ttyMSM",
drivers/tty/serial/cpm_uart/cpm_uart_core.c:	.dev_name	= "ttyCPM",
drivers/tty/serial/8250/8250_core.c:	.dev_name		= "ttyS",
drivers/tty/serial/vt8500_serial.c:	.dev_name	= "ttyWMT",

"Okay, okay, but what about if I just have me some 8250-style ports? Surely those work reasonably, right?"

Well.

8250_core is just weird

The 8250_core driver has a couple knobs:

  • CONFIG_SERIAL_8250_NR_UARTS
    • This is the build-time maximum of the number of 8250 serial ports that the driver will support (it's used as the max size of an array). It defaults to 4, so if you have a 16-serial-port PCI card you're gonna have to recompile your kernel. (Debian sets it to 32, at least.)
  • CONFIG_SERIAL_8250_RUNTIME_UARTS
    • This is used as the default value for 8250.nr_uarts, and while it describes its setting as the number of serial ports "registered at boot time", it is effectively a runtime limit on then number of 8250 serial ports.

The way that the 8250 driver works, contrary to any other device driver on Linux that I can find, is that it creates ttyS0 through ttyS(nr_uarts-1) nodes up-front. As UARTs are registered in the system, they are either merged into an existing ttyS* entry or registered into an unused one by serial8250_find_match_or_unused.

Oh, and you get a bunch of them registered up-front for you! The Linux kernel defines a hardcoded table, SERIAL_PORT_DFNS that defines a predefined set of UARTs. On x86, these are at I/O ports 0x3F8, 0x2F8, 0x3E8, and 0x2E8 These ports are added without their presence being probed-for or enumerated in any table (e.g. ACPI DSDT). So if you have exactly four serial ports in your system at those addresses, then sure! Everything's great.

Not all x86 platforms are like that, either. A typical PC, you might have fewer serial ports than that-- but Linux will dutifully create you a /dev/ttyS3, even though no such device actually exists in your system. But it has a device node, things like /sys/class/tty/ttyS3/device exist and point at some weird platform:serial8250 phantom device, /sys/class/tty/ttyS3/port dutifully points to 0x2E8.

Which means that if you dutifully say "okay, I have two on-board serial ports, and an 8-port PCI serial card. I should set 8250.nr_uarts=10 and we're good, right?", like this stackoverflow user asked 9 years ago, you will end up with:

  • the legacy mechanisms filling ttyS0 through ttyS3, even though ttyS2 and ttyS3 are phantoms
  • the first six ports of the multiport card filling ttyS4 through ttyS9
  • then for the last two, we need to "find unused" , which means we come back around to ttyS2 and ttyS3.

You get all your devices, but in a completely unintuitive order.

If the user had set 8250.nr_uarts=12 or more, they would end up with their multiport card being ttyS4 through ttyS11. But why would they do this, when they have 10 serial ports?

Fixing the 8250_core driver to not do this

haha.

no.

If you dare to try to fix this, to make 8250 devices behave more reasonably relative to other device classes, you will break people's configurations. See this and this and this and this for a small sampling.

There simply isn't a way to make the ttyS* namespace work in an intuitive manner that doesn't break anyone:

  • Get rid of the absurely low nr_uarts limits? Then you break the "wraparound" behavior for anyone that set nr_uarts to exactly the number of serial ports they have.
  • Get rid of the SERIAL_PORT_DFNS preloading? now:

So if the ttyS* namespace is doomed to its past sins, what other options do we have?

Naming serial ports

There are two existing models that can be followed:

Since udev has already established a convention for /dev/serial, it makes the most sense to extend that, but we can use the network-naming to inform some choices.

USB-connected serial ports

Presently, USB serial devices already get symlinks under /dev/serial/by-path/ and /dev/serial/by-id/. The -port suffix is only present for usb-serial ports with a port_number.

  • /dev/serial/by-path/$env{ID_PATH}(-port$env{.ID_PORT}
  • /dev/serial/by-id/$env{ID_BUS}-$env{ID_SERIAL}-if$env{ID_USB_INTERFACE_NUM}(-port$env{.ID_PORT}

No change is proposed to these names.

Legacy-enumerated serial ports

Let's not think about those. Here in 2023, it seems completely reasonable to expect that the legacy probing of UARTs at hardcoded addresses is no longer necessary-- on the handful of systems that I have checked, PNP0501 devices are present in ACPI, so these can be covered by the ACPI-enumeration cases below.

I'd also like to see a config knob for disabling the use of the SERIAL_PORT_DFNS table entirely. It would have to default to enabled for the sake of backwards-compatibility ("we don't break userspace") but this would allow system integrators to assent to "Yes my BIOS tables can be trusted."

Firmware-enumerated (ACPI PNP/OF) serial ports

ACPI

We can number or label onboard serial port devices via ACPI using the _UID; note that:

  • _UID can be either a number or a string. In all of the DSDTs I have observed it's a number.
  • When _UID is a number, it is freeform-- the "first" serial port might be 0, it might be 1.
  • Note that there's also no guarantee that ports appear in sequential order, see this post where someone has a BIOS where COM2 is specified before COM1

We can also label onboard serial port devices via ACPI using _STR

We can also label onboard serial port devices via ACPI using the _DDN.

  • This use seems to be atypical? At least on one machine I have this is set to "COM1"?
  • also seeing it in EDK2 though, see this dsdt.asl
  • note that this is intended for "DOS Device Names" and not a general-purpose label
  • there was a patch to export ddn to userspace that claimed to have been queued up, but afaict it never made it into Linus's tree
    • perhaps it would be good to revisit this patch

If there is no string name specified, it might be reasonable to treat the name as an implied COMn where n is the value of a numeric _UID. This seems to match up with BIOSes that I've seen.

OF device tree

We can also number and label onboard serial port devices via device tree (OF).

Indexes are derived from the alias index. Labels will be derived from an optional label, if present.

symlink patterns to create

  • /dev/serial/by-path/$env{ID_PATH}(-port$env{.ID_PORT})
  • /dev/serial/by-addr/$env{ID_IO_TYPE}-$env{ID_ADDR}
    • ID_IO_TYPE is io or mmio; the address comes from port or iomem_base respectively
  • /dev/serial/by-id/onboard-port$env{ID_INDEX}
  • /dev/serial/by-label/$env{ID_LABEL}
    • if a string label is present

PCI(e)-connected serial ports

We can number and label onboard serial port devices via ACPI the same way as is done for network interfaces-- via ACPI _DSM or via SMBIOS type 41. As they are described by firmware, such devices can be considered to be "onboard". This is already handled in the kernel via pci-sysfs.c, so should require no new code?

Note that the indices chosen must not conflict with _HID-numbered devices. This might have to be left up to the system integrator?

TODO: We also need some sort of mechanism for determining whether or not this is a multiport serial card? PCI class ID helps, but is not sufficient for some types of PCI serial devices that don't use that.

symlinks to create

  • /dev/serial/by-path/$env{ID_PATH}(-port$env{.ID_PORT})
  • /dev/serial/by-id/$env{ID_BUS}-$env{ID_VENDOR_ID}_$env{ID_MODEL_ID}-$env{ID_SERIAL}(-port$env{.ID_PORT}
    • could also be names instead of VID/PID? but this would require hwdb, unlike USB (which has string descriptors)
    • serial number? this is a PCIe capability, not present on PCI
      • DSN also doesn't seem to be cleanly exported to userspace (pci_get_dsn called by a few drivers, but not generically presented in sysfs anywhere; would have to be read out of PCI /config
  • /dev/serial/by-addr/$env{ID_IO_TYPE}-$env{ID_ADDR}
  • /dev/serial/by-label/$env{ID_LABEL}(-port$env{.ID_PORT}

Others

TODO: systemd's udev-builtin-net_id.c covers a whole bunch of other cases (Linux on System Z, Broadcom bus, virtio, etc) that probably need to get looked at too...

Work potentially needed

Kernel

  • Exporting a /serial for PCI(e) devices with DSNs defined seems generally useful instead of forcing udev to parse /config
    • /serial matches the name used for USB devices
    • Potentially other PCI devices could export an attribute with the same name with sufficient driver support. For example, the PXI-8432/2 has a serial number that can be read programmatically, but is not present in its PCI config space.
  • _DDN should be exported to userspace like _HID/_UID/_STR are.
    • By possibly dusting off that old patch?

systemd/udev

  • A serial_id helper that implements all of the glue for this (similar to net_id).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment