USB Serial udev Symlink: How to Pin /dev/ttyUSB
When you plug in a USB-UART (FTDI, CP210x, CH340, …), the kernel creates nodes like /dev/ttyUSB0 and /dev/ttyUSB1 in enumeration order. Scripts, systemd units, and flashers hard-coded to /dev/ttyUSB0 break as soon as a second dongle, a hub, or a reboot changes that order.
The fix axis is not renaming the kernel node—it is adding a stable alias with SYMLINK+=. The udev manual treats extra symlinks as the supported way to give devices predictable names. This post covers ttyUSB symlink pinning only. Rule-directory primers, HID, and broad dialout/plugdev policy belong in the general udev-rules and input-permissions posts.
Grounded in udev(7) and udevadm(8). No invented board diaries. Examples use placeholder VID/PID/serial values.
What about ATTR keys?
One-line answer: Use ATTR{…} for the event device’s own sysfs attributes; use ATTRS{…} to walk parents. USB VID/PID/serial usually live on the USB parent, not the tty node—so match with ATTRS, and every ATTRS token on one rule line must match the same parent device.
Keys that matter for serial tty:
| Key | Role | On ttyUSB |
|---|---|---|
KERNEL=="ttyUSB*" or SUBSYSTEM=="tty" | Select the character device | Matching only SUBSYSTEM=="usb" can symlink the wrong node for apps |
ATTRS{idVendor} / ATTRS{idProduct} | Parent USB IDs | Copy hex strings from udevadm info -a |
ATTRS{serial} | Disambiguate identical dongles | Prefer it whenever more than one unit exists |
SYMLINK+="name" | Add /dev/name | Leaves /dev/ttyUSBn intact |
Inspect attributes:
# Measure on the live node (e.g. ttyUSB0), then copy into the rule
udevadm info -a -n /dev/ttyUSB0 | grep -E 'ATTRS\{(idVendor|idProduct|serial|manufacturer|product)\}'
Rule sketch (replace IDs with your measured values):
# /etc/udev/rules.d/99-ttyusb-symlink.rules
SUBSYSTEM=="tty", KERNEL=="ttyUSB*", \
ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", \
ATTRS{serial}=="SERIALPLACEHOLDER", \
SYMLINK+="my-uart"
Watch-outs:
ATTR≠ATTRS.ATTRis the event device;ATTRSwalks parents. Putting VID on the tty’sATTRusually fails to match.- Same parent for all
ATTRSon one line — do not mix attributes from different ancestors (udev(7)). - Case and whitespace — copy strings exactly from
udevadm info -a. /dev/serial/by-id/…already exists — pointing apps there can fix enumeration without a custom name. Use a customSYMLINKwhen you need a short stable alias.- Vs the general udev post: here the focus is which ATTRS pin a ttyUSB SYMLINK, not where every rules directory lives.
What about GROUP and MODE?
One-line answer: GROUP= and MODE= override the compiled-in defaults for the device node. Pin the path with SYMLINK; pin access with something like GROUP="dialout" and MODE="0660" (and add the user to dialout).
Full serial-oriented example:
# /etc/udev/rules.d/99-ttyusb-symlink.rules
SUBSYSTEM=="tty", KERNEL=="ttyUSB*", \
ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", \
ATTRS{serial}=="SERIALPLACEHOLDER", \
GROUP="dialout", MODE="0660", \
SYMLINK+="my-uart"
Practical table:
| Item | Prefer | Why |
|---|---|---|
GROUP="dialout" | Matches common Debian/Ubuntu and systemd tty defaults | usermod -aG dialout + re-login |
MODE="0660" | Owner/group RW only | Avoid world-writable 0666 |
OWNER= | Only for a dedicated service account | Labs usually need GROUP membership |
SYMLINK+="my-uart" | Paths in apps / unit Device= / ExecStart | Drop hard-coded /dev/ttyUSB0 |
Membership:
sudo usermod -aG dialout "$USER"
# take effect after logout/login (or a new login session)
groups # confirm dialout
A SYMLINK alone with root:root 0600 fixes the name but still yields Permission denied for normal users. GROUP alone with a /dev/ttyUSB0 hard-code leaves enumeration broken. This post pins both name (SYMLINK) and access (GROUP/MODE). HID, plugdev, and uaccess belong in the input-permissions article.
How do you verify with udevadm?
One-line answer: After saving the file, run udevadm verify for syntax/semantics/style, then control --reload-rules → trigger → info / test / ls -l to confirm SYMLINK and GROUP actually landed.
# 1) Validate the rules file (recent systemd udev)
sudo udevadm verify /etc/udev/rules.d/99-ttyusb-symlink.rules
# 2) Reload rules (does not instantly rewrite already-present devices)
sudo udevadm control --reload-rules
# 3) Re-emit events for tty (or one node)
sudo udevadm trigger --subsystem-match=tty
# sudo udevadm trigger -n /dev/ttyUSB0
# 4) Inspect results
ls -l /dev/my-uart /dev/ttyUSB0
udevadm info -n /dev/ttyUSB0 | grep -E 'DEVNAME|DEVLINKS|ID_VENDOR|ID_MODEL|ID_SERIAL'
# 5) Dry-run (adjust syspath)
sudo udevadm test /sys/class/tty/ttyUSB0 2>&1 | tail -n 50
Checklist:
udevadm verifyexits 0 — catches syntax,GROUPname resolution, and style (--no-styleignores style only)./dev/my-uartpoints at the expectedttyUSBn.ls -lshows the intended group and mode.- After re-login, a normal user can open the port with serial tools.
- Unplug/replug still recreates the same SYMLINK name when ATTRS match.
Common failures: mixing == (match) and = (assign), missing .rules suffix, VID/PID typos, combining ATTRS from different parents, putting SYMLINK only on SUBSYSTEM=="usb", or reloading without trigger/replug.
FAQ
Can I rename /dev/ttyUSB0 itself?
Prefer an extra symlink via SYMLINK+=. Point apps and units at /dev/my-uart (or similar).
Does the same pattern work for ttyACM*?
Yes for CDC-ACM. Use KERNEL=="ttyACM*" (or SUBSYSTEM=="tty") with parent ATTRS{idVendor/idProduct/serial} and SYMLINK+=. The title says ttyUSB; the matching axis is the same.
Is /dev/serial/by-id/… enough?
Often yes for a stable path. Prefer a custom SYMLINK (+ GROUP/MODE) when you want a short shared alias and permissions in one rule.
How is this different from the general udev-rules post?
That post surveys directories, ATTR/KERNEL matching, and verification broadly. This one only pins shaky /dev/ttyUSB enumeration with SYMLINK.
Sources
- udev(7) — ATTR / ATTRS / SYMLINK / GROUP / MODE
- udevadm(8) — verify, info, test, control, trigger
- systemd 50-udev-default.rules.in — default tty* dialout practice