[NOTE: this is mostly a collection of notes and not quite a specification nor an implementation thereof]
"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.
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.
- This is used as the default value for
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
throughttyS3
, even thoughttyS2
andttyS3
are phantoms - the first six ports of the multiport card filling
ttyS4
throughttyS9
- then for the last two, we need to "find unused" , which means we come back around to
ttyS2
andttyS3
.
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?
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 setnr_uarts
to exactly the number of serial ports they have. - Get rid of the
SERIAL_PORT_DFNS
preloading? now:- an external PCI port no longer starts at
ttyS4
and you've shifted all the indices. - if your BIOS specifies ports out-of-order you've just changed the order of your onboard ports too.
- an external PCI port no longer starts at
So if the ttyS*
namespace is doomed to its past sins, what other options do we have?
There are two existing models that can be followed:
- Network devices have a "persistent naming scheme"-- see systemd.net-naming-scheme.
- systemd udev (and forks such as eudev) already have a persistent naming scheme for serial ports under
/dev/serial
... but only for USB devices.
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.
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.
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."
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
- this doc says this is preferred over
_DDN
- this raspberry pi one might be an example
- kernel already appears to export this as
description
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.
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.
/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
isio
ormmio
; the address comes fromport
oriomem_base
respectively
/dev/serial/by-id/onboard-port$env{ID_INDEX}
/dev/serial/by-label/$env{ID_LABEL}
- if a string label is present
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.
/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
- DSN also doesn't seem to be cleanly exported to userspace (
/dev/serial/by-addr/$env{ID_IO_TYPE}-$env{ID_ADDR}
/dev/serial/by-label/$env{ID_LABEL}(-port$env{.ID_PORT}
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...
- 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?
- A
serial_id
helper that implements all of the glue for this (similar tonet_id
).