-rwxr-xr--」走到「写一条 udev 规则,让每个串口插上就有固定的名字和正确的权限」。学完之后,Permission denied 不再靠 sudo 硬扛,udev 也不再是黑盒。你兴冲冲地给 STM32 写好固件,插上 USB 转串口模块,打开烧录工具,然后收获了一行红字:
# PlatformIO / pyserial could not open port /dev/ttyUSB0: [Errno 13] Permission denied: '/dev/ttyUSB0' # 或者 minicom / screen minicom: cannot open /dev/ttyUSB0: Permission denied # 或者 STM32CubeProgrammer Error: Port /dev/ttyUSB0 is not accessible
这时候你去搜,十篇文章里有九篇告诉你:
sudo chmod 777 /dev/ttyUSB0
敲完确实能用。但拔掉再插,又不行了。更糟的是,有些人一怒之下用 sudo 启动整个 IDE,结果工程目录里多出一堆 root 所有的文件,下次普通用户反而改不动自己的代码。
chmod 777 是临时且错误的做法。正确的解法只有两个,而且都很简单:把自己加进设备所属的组(一步到位),或者写一条 udev 规则(一劳永逸 + 还能固定设备名)。下面从最基础的权限讲起。Windows 里串口叫 COM3、COM4;Linux 里它就是一个放在 /dev 目录下的文件。想看它的权限,用 ls -l:
$ ls -l /dev/ttyUSB0
crw-rw---- 1 root dialout 188, 0 9月 3 04:12 /dev/ttyUSB0
这行输出信息量极大,我们把它拆开看:
crw-rw---- 逐位拆解:类型位 + 属主 / 属组 / 其他 各三位注意 r w x 对文件和目录的含义完全不同,这是新手第二大困惑:
| 权限位 | 对普通文件 | 对目录 |
|---|---|---|
| r (read) | 能读取文件内容,比如 cat | 能列出目录里有哪些文件(ls) |
| w (write) | 能修改文件内容 | 能在目录里新建/删除/重命名文件 |
| x (execute) | 能当程序执行 | 能进入该目录(cd),以及访问其中的文件 |
x 就进不去——即使有 r 能列出名字,也读不到内容。这是"我能 ls 但打不开文件"的经典原因。你一定见过 chmod 755、chmod 644 这种写法。它其实是三位数,每一位是一组 rwx 的和:
只需记三个数:7 = 全部权限,6 = 读写,0 = 没权限。日常开发里 90% 的场景就够用了。
| 问题 | 说明 |
|---|---|
| ① 它只是临时的 | 设备节点是每次插入时由 udev 动态创建的。你改的是这一次创建的节点,拔掉后节点消失,下次插入时 udev 按默认规则重建,权限回到 crw-rw----。 |
| ② 它把门开给了所有人 | 777 = 任何用户都能读写。在多人共用 / 服务器环境里,别人可以直接 cat /dev/ttyUSB0 偷听你的串口数据。 |
| ③ 它掩盖了真正的需求 | 你要的不是"让所有人都能用",而是"让我能用"。前者是破坏安全模型,后者只需要一个正确的组成员身份。 |
回到那行输出:crw-rw---- 1 root dialout …。设备属于 dialout 组,而该组的权限是 rw-。所以只要你在 dialout 组里,就能读写它。
$ ls -l /dev/ttyUSB0 crw-rw---- 1 root dialout 188, 0 ... /dev/ttyUSB0 # 第 4 列 root 是属主,第 5 列 dialout 就是属组
dialout;Arch / Manjaro 是 uucp;某些发行版还有 serial。以你机器上 ls -l 显示的为准,不要照抄。$ groups
wenji adm cdrom sudo dip plugdev lpadmin
# 没有 dialout,这就是打不开的原因
sudo usermod -aG dialout $USER
# ↑↑ 一定要有 -a(append),否则会把你从其他组踢出去!
usermod 后,你现在这个终端里的 groups 不会变。必须注销重新登录(或者重启)。newgrp dialout——但它只对这一个 shell 有效,新开的终端还是不行。$ groups # 重新登录后执行,应该能看到 dialout wenji adm dialout cdrom sudo dip plugdev lpadmin $ echo "hello" > /dev/ttyUSB0 # 能写进去就说明权限对了 $ cat /dev/ttyUSB0 # Ctrl+C 退出
到这一步,绝大多数人的 Permission denied 就已经解决了。但如果你还想要下面这些,就需要 udev:
ttyUSB0/1/2 的对应关系都变,得一个个试/dev/tty_stm32、/dev/tty_esp32 这种有意义的名字dialout,想自己指定属组和权限/dev 目录下那几百个节点并不是写死在磁盘上的,而是每次开机、每次插入时由 udev 动态生成的。理解了这个流程图,你就理解了为什么权限会"重置"。
/dev/ttyUSB0 不是文件,是 udev 按照规则"现场变出来"的一个入口。你想改权限,就要改"变出它来的那份规则",而不是去改变出来的结果。$ lsusb Bus 001 Device 005: ID 1a86:7523 QinHeng Electronics CH340 serial converter Bus 001 Device 003: ID 0483:3748 STMicroelectronics ST-LINK/V2 Bus 001 Device 007: ID 10c4:ea60 Silicon Labs CP210x UART Bridge Bus 001 Device 009: ID 0403:6001 Future Technology Devices FT232 Serial
1a86:7523 冒号前是厂商 ID(idVendor),后面是产品 ID(idProduct)。这就是 udev 认设备的身份证。
1a86:752310c4:ea600403:60010483:37480483:374b0483:374e / 374f| 符号 | 含义 | 例子 |
|---|---|---|
== | 匹配:判断条件是否成立(如果…) | ATTRS{idVendor}=="1a86" |
= | 赋值:设置属性(那么…) | GROUP="dialout" |
+= | 追加:不覆盖已有值(推荐用于 SYMLINK) | SYMLINK+="tty_stm32" |
:= | 强制赋值:后面的规则不允许再改 | GROUP:="dialout" |
常用的键就这几个:
| 键 | 作用 | 常用值 |
|---|---|---|
KERNEL | 匹配内核给的设备名 | "ttyUSB*"、"ttyACM*" |
SUBSYSTEM | 匹配子系统 | "tty"、"usb" |
ATTRS{idVendor} | 匹配厂商 ID | "1a86" |
ATTRS{idProduct} | 匹配产品 ID | "7523" |
ATTRS{serial} | 匹配设备序列号(区分两个同型号设备的关键) | "0001" |
MODE | 设置权限(八进制) | "0666" |
GROUP | 设置属组 | "dialout" |
SYMLINK | 创建固定名字的软链接 | "tty_stm32" |
sudo nano /etc/udev/rules.d/99-usb-serial.rules
# ===== 99-usb-serial.rules ===== # 文件名前面的数字 = 执行顺序,99 保证它在系统默认规则之后运行(从而覆盖默认值) # 必须以 .rules 结尾,否则 udev 完全不理会这个文件 # ① CH340/CH341 —— 最便宜的 USB 转串口 SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", \ MODE="0664", GROUP="dialout", SYMLINK+="tty_ch340" # ② CP210x —— NodeMCU/ESP32 开发板常见 SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", \ MODE="0664", GROUP="dialout", SYMLINK+="tty_esp32" # ③ FT232 —— 工业模块常见,给一个固定名字 SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", \ MODE="0664", GROUP="dialout", SYMLINK+="tty_ftdi" # ④ ST-Link —— 调试器走 usb 子系统,不是 tty,所以 SUBSYSTEM 不同 SUBSYSTEM=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="3748", \ MODE="0664", GROUP="plugdev", TAG+="uaccess" # ⑤ 两个同型号 CH340(用序列号区分,先查:udevadm info -a -n /dev/ttyUSB0 | grep serial) SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{serial}=="0001", SYMLINK+="tty_motor" SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{serial}=="0002", SYMLINK+="tty_sensor"
\ 是 shell 续行符示意,写进文件时请ATTRS{}(带 S)匹配的是父设备链上的属性,ATTR{}(无 S)只匹配当前设备。写 VID/PID 一律用 ATTRS{}。# 让 udev 重新读取规则文件(改完规则必须做) sudo udevadm control --reload-rules # 对所有已连接的设备重新跑一遍规则(不用拔插) sudo udevadm trigger # 只看效果: ls -l /dev/tty_ch340 /dev/tty_esp32 # lrwxrwxrwx 1 root root 7 ... /dev/tty_ch340 -> ttyUSB0 # 现在写代码里直接写 /dev/tty_ch340,永远不用担心编号变化
# ① 内核到底认没认这个设备?(不认就是硬件/虚拟机问题,不是 udev 的锅) dmesg | tail -20 # 期望看到:usb 1-1: ch341-uart converter now attached to ttyUSB0 # ② 打印这台设备的全部可匹配属性(写规则时照着抄) udevadm info -a -n /dev/ttyUSB0 | less # 会输出好几段 "looking at parent device",VID/PID 通常在其中一段 # ③ 干跑一次:看看 udev 认为自己会怎么处理这台设备 udevadm test $(udevadm info -q path -n /dev/ttyUSB0) 2>&1 | tail -30 # 输出里找 "Handling device node" / "setting permissions" / "creating link" # ④ 实时监控:插拔时看事件是否到达 udev udevadm monitor --udev # 插上设备,应该刷出 UDEV [xxx] add /devices/.../ttyUSB0
| 现象 | 根因 | 解法 |
|---|---|---|
| 加了 dialout 组,还是 denied | 组身份在登录时确定,没注销重登 | 注销重登录,或 newgrp dialout 临时生效 |
| 虚拟机里根本看不到 /dev/ttyUSB0 | USB 设备没从宿主机转发给虚拟机 | VMware:虚拟机设置 → USB 控制器 → 连接;VirtualBox:设备 → USB → 勾选 |
| WSL 里 udev 规则完全不生效 | WSL 不运行 systemd/udevd,设备节点由 Windows 侧管理 | WSL2 改用 usbipd-win 绑定,或直接在 Windows 下用 COM 口 |
| 设备出现几秒后又消失 | Ubuntu 22.04+ 的盲文服务 brltty 会抢占 CH340 | sudo apt remove brltty,然后拔插 |
| 写了规则文件但毫无反应 | 文件名不以 .rules 结尾,或没执行 reload | 确认文件名 + sudo udevadm control --reload-rules && sudo udevadm trigger |
| 软链接建出来了,但权限还是不对 | 软链接指向目标节点,权限由目标节点决定 | 在规则里同时设 MODE="0664" 和 GROUP="dialout" |
| 两个同型号模块分不清 | VID/PID 完全相同,规则无法区分 | 加 ATTRS{serial}=="...",或用 KERNELS 匹配插在哪个 USB 口 |
| 用 sudo 跑 IDE 后,工程文件变 root 所有 | 不要靠 sudo 绕权限问题 | 修好权限后执行 sudo chown -R $USER:$USER 工程目录 恢复 |
#!/bin/bash # 串口打不开?从上往下跑一遍,90% 的问题会在第 3 步之前现形 echo "① 内核认到设备了吗" dmesg | tail -15 | grep -iE "tty|usb|ch34|cp210|ftdi" echo "② 设备节点在吗、权限长什么样" ls -l /dev/ttyUSB* /dev/ttyACM* 2>/dev/null || echo " → 节点不存在,回到第 1 步" echo "③ 我在该在的组里吗" echo " 设备属组: $(stat -c %G /dev/ttyUSB0 2>/dev/null)" echo " 我的组 : $(id -nG)" echo "④ udev 规则命中了吗" udevadm info -a -n /dev/ttyUSB0 2>/dev/null | grep -E "idVendor|idProduct|serial" | head -6 echo "⑤ 端口被别的进程占着吗(不是权限问题,是占用问题)" lsof /dev/ttyUSB0 2>/dev/null || echo " 未被占用" # 常见凶手:ModemManager、brltty、没关掉的 minicom、上次调试残留的 openocd # 解决:sudo systemctl stop ModemManager ; sudo apt remove brltty
Permission denied 和 Device or resource busy,这是两个完全不同的问题——前者是权限(看组),后者是被占用(lsof 找出凶手并关掉)。别用同一种方法去治。crw-rw---- 1 root dialout 中,一个既不是 root、也不在 dialout 组的用户,有什么权限?c 是类型位,之后每三位一组分别是属主(rw-)、属组(rw-)、其他(---)。该用户落在"其他",是 ---,所以读写都不行,这正是 Permission denied 的来源。chmod 777 /dev/ttyUSB0 只能管一次?/dev 下的设备节点是 udev 在设备插入时动态创建的。你修改的是这一次创建的节点;拔掉后节点消失,下次插入时 udev 按默认规则重新创建,权限回到默认值。sudo usermod -aG dialout $USER 后,为什么当前终端还是打不开串口?newgrp dialout。-a(append)也很关键,漏了会把你从其他组里踢出去。ATTRS{}(带 S)。ATTR{} 只匹配当前设备节点自身的属性,而 VID/PID 挂在父设备(USB 接口/设备)链上;ATTRS{} 会沿父链向上搜索,才能匹配到。ATTRS{serial}=="..."(芯片序列号),其次用 KERNELS=="1-1.2" 匹配物理 USB 口位置。序列号可通过 udevadm info -a -n /dev/ttyUSB0 | grep serial 查到。brltty 的 udev 规则会抢占 CH340 类设备。执行 sudo apt remove brltty 后重新插拔即可。同类问题也可能是 ModemManager 在拨号探测。