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:

KeyRoleOn ttyUSB
KERNEL=="ttyUSB*" or SUBSYSTEM=="tty"Select the character deviceMatching only SUBSYSTEM=="usb" can symlink the wrong node for apps
ATTRS{idVendor} / ATTRS{idProduct}Parent USB IDsCopy hex strings from udevadm info -a
ATTRS{serial}Disambiguate identical donglesPrefer it whenever more than one unit exists
SYMLINK+="name"Add /dev/nameLeaves /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:

  1. ATTR ≠ ATTRS. ATTR is the event device; ATTRS walks parents. Putting VID on the tty’s ATTR usually fails to match.
  2. Same parent for all ATTRS on one line — do not mix attributes from different ancestors (udev(7)).
  3. Case and whitespace — copy strings exactly from udevadm info -a.
  4. /dev/serial/by-id/… already exists — pointing apps there can fix enumeration without a custom name. Use a custom SYMLINK when you need a short stable alias.
  5. 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:

ItemPreferWhy
GROUP="dialout"Matches common Debian/Ubuntu and systemd tty defaultsusermod -aG dialout + re-login
MODE="0660"Owner/group RW onlyAvoid world-writable 0666
OWNER=Only for a dedicated service accountLabs usually need GROUP membership
SYMLINK+="my-uart"Paths in apps / unit Device= / ExecStartDrop 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:

  1. udevadm verify exits 0 — catches syntax, GROUP name resolution, and style (--no-style ignores style only).
  2. /dev/my-uart points at the expected ttyUSBn.
  3. ls -l shows the intended group and mode.
  4. After re-login, a normal user can open the port with serial tools.
  5. 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