视频加载失败

STM32 串口拔插后 Permission denied:启动能连,重连就没权限

1845 字
9 分钟
STM32 串口拔插后 Permission denied:启动能连,重连就没权限

设备先插着,再进软件,串口能连上。程序已经跑起来之后,把 STM32 拔出来再插回去,重连却失败了。

日志还是那句 Permission denied。第一次看见的时候,我很容易以为是重连逻辑写坏了,或者板子拔插后枚举乱了。后来才发现:软件其实已经再次扫到设备了,卡在打开 /dev/ttyACM0。板子没坏,重连也找到了口,Linux 不肯让这个普通用户进程打开新出现的那个设备节点。

这篇就当一次认真的小总结。


先看现象#

典型日志长这样:

设备: /dev/ttyACM0, 描述: STM32 Virtual ComPort, VID: 1155, PID: 22336
尝试连接: /dev/ttyACM0
✗ 无法打开串口 /dev/ttyACM0: [Errno 13] Permission denied: '/dev/ttyACM0'

两行信息已经足够定位:

  • VID: 1155 就是十六进制 0x0483,STMicroelectronics
  • PID: 22336 就是十六进制 0x5740,STM32 虚拟串口的常见产品 ID
  • [Errno 13] 就是 EACCES:当前进程对这个设备文件没有读写权限

软件已经再次扫到设备了。卡点只有一个:打开 /dev/ttyACM0 这一步。不是找不到板子,也不是固件坏了。

对应关系也很干净:

操作结果
设备已插入 → 启动软件能连接
软件运行中拔出再插没权限,不能重连

这是 Linux 串口设备权限问题。启动时能连,只说明当时那个节点被放过一次权;拔插之后那是另一个文件。


为什么进去能连接#

启动脚本里往往已经有类似这样的一句:

Terminal window
chmod 666 /dev/ttyACM* /dev/ttyUSB*

然后才启动你的串口程序。流程是:

  1. STM32 已经插着,/dev/ttyACM0 已经存在
  2. 启动脚本把当时那个文件改成 666(谁都能开)
  3. 软件打开的就是这个被改过的节点 → 连接成功

这次成功只说明「启动前那一次 chmod 生效了」。它不说明用户在 dialout 组,也不说明以后拔插都有权限。


为什么软件里拔插就没权限#

Linux 对 USB 串口的默认权限通常是:

crw-rw---- 1 root dialout /dev/ttyACM0

也就是 0660:只有 root 和 dialout 能打开。现场跑软件的用户通常不在 dialout 组。

USB 一拔,原来的 /dev/ttyACM0 这个文件就消失,启动时改过的 666 一起没了。
再插上,内核会新建一个设备节点。名字可能还叫 ttyACM0,但是另一个文件,权限回到默认 0660。

此时:

  • 启动脚本早就结束了,不会再执行 chmod 666
  • 软件里的重连只是普通用户再 open() 一次,没有 sudo,也不会改权限
  • 用户又不在 dialout 组
  • 就算你刚刚把用户加进了 dialout,已经在跑的这个进程也不会自动获得新组

所以重连报 Permission denied。

启动前已插入
→ 启动脚本 chmod 666(只改这一次、只改当时那个节点)
→ 程序打开成功
软件运行中拔出
→ 旧 /dev/ttyACM0 消失,666 一起没了
再插进去
→ 新节点默认 0660
→ 没人再 chmod
→ 重连没权限

软件侧会按已绑定的板子去重新打开串口,但打不开是操作系统权限,不是重连逻辑没找到设备。


为什么不能指望「名字还是 ttyACM0」#

设备名可以还叫 /dev/ttyACM0。拔插后那是内核新创建的 inode。

chmod 改的是文件,不是「这个 USB 口永远 666」。没有 udev 规则时,每次插上都会重新变成 0660。

可以把它想成:你给门口那把锁换过一次钥匙,门拆掉重建之后,锁又回到出厂设置。


正确做法:启动时放一次权,拔插后让 udev 自动放权#

应用代码不用改。扫描和重连逻辑已经再次找到设备了,缺的是新节点上的权限。

要同时做两件事,职责不一样:

  • udev 0666:设备每次出现时自动放权,软件里拔插后重连才打得开
  • 启动直前再 chmod 666 一次:保证这一次进软件时,已经插着的口一定能打开

不要用 MODE="0660"。用户不在 dialout 时,拔插后还是没权限;已经在跑的进程也拿不到新加的组。

1. 写入 udev 规则#

再插上来也要谁都能读写,不能依赖当前进程是否已经加入 dialout。

# ModemManager 看的是 USB 设备,不是 tty 节点
ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="0483", ENV{ID_MM_DEVICE_IGNORE}="1"
# STM32 虚拟串口(VID 0483,也就是日志里的 1155)
SUBSYSTEM=="tty", ATTRS{idVendor}=="0483", MODE="0666", GROUP="dialout"
# 这类口都放权;ttyUSB 是转接芯片常见的名字
KERNEL=="ttyACM*", MODE="0666"
KERNEL=="ttyUSB*", MODE="0666"

作用拆开看:

  • MODE="0666":新节点一出现就是谁都能读写
  • idVendor=="0483":对准 STM32
  • KERNEL=="ttyACM*" / ttyUSB*:这类口都放权
  • ID_MM_DEVICE_IGNORE=1:避免 ModemManager 把 STM32 CDC 口当成调制解调器抢走。这句要写在 USB 设备上,写在 SUBSYSTEM=="tty" 那一行常常不生效

0666 的代价很明确:这台机器上任何用户都能读写这个串口。开发板、工控机、自己盯着的现场机器,通常可以接受。

写入可以用 tee。普通 sudo 会把 heredoc 传给 tee;只有 sudo -S(从 stdin 读密码)才会和 heredoc 抢输入。

Terminal window
sudo tee /etc/udev/rules.d/99-stm32-serial.rules >/dev/null <<'EOF'
ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}=="0483", ENV{ID_MM_DEVICE_IGNORE}="1"
SUBSYSTEM=="tty", ATTRS{idVendor}=="0483", MODE="0666", GROUP="dialout"
KERNEL=="ttyACM*", MODE="0666"
KERNEL=="ttyUSB*", MODE="0666"
EOF
sudo usermod -aG dialout "$(whoami)"
sudo udevadm control --reload-rules
sudo udevadm trigger --subsystem-match=tty
sudo udevadm settle
sudo sh -c 'for dev in /dev/ttyUSB* /dev/ttyACM*; do [ -e "$dev" ] && chmod 666 "$dev"; done'

usermod -aG dialout 仍然值得做:下次重新登录后,即使规则被收回到 0660,组也能兜底。它帮不了现在已经在跑的这个进程去打开拔插后的新节点。

规则改完之后,更稳的做法是拔插一次 USB。ModemManager 如果已经占着口,还需要 sudo systemctl restart ModemManager。

2. 启动应用直前再 chmod 666 一次#

保证这一次进软件时,已经插着的口一定能打开:

Terminal window
sudo sh -c 'for dev in /dev/ttyUSB* /dev/ttyACM*; do [ -e "$dev" ] && chmod 666 "$dev"; done'
./your_serial_app

两件事不要混:

  • 启动时已插入 → 仍靠这次 chmod(和以前一样能连)
  • 软件里再拔插 → 靠上面的 udev 0666,新节点自动有权限,重连才能成功

怎么确认已经修好#

不要只看启动那一次能不能连。把设备拔出再插上,然后执行:

Terminal window
ls -l /dev/ttyACM0

期望:

crw-rw-rw- 1 root dialout ... /dev/ttyACM0

即 0666(rw-rw-rw-)。若仍是 crw-rw----(0660),软件里重连仍会没权限。

规则要写到实际跑应用的那台机器上。只改开发机无效。


如果修好后变成 busy#

权限过了,日志却说端口被占用,那就是另一件事了。先查谁占着口:

Terminal window
sudo lsof /dev/ttyACM0

现场常见的抢口选手是 ModemManager。udev 里那句 ID_MM_DEVICE_IGNORE 必须打在 USB 设备上;改完规则后重启 ModemManager 或拔插一次,已经占用的口才会放开。


收一收#

这题的解题顺序其实很短:

  1. 先确认是启动能连、运行中拔插后才 EACCES
  2. chmod 改的是当时那个文件,不是这个 USB 口永远 666
  3. 拔插后名字可以还叫 ttyACM0,那是内核新建的 inode,权限回到 0660
  4. 已经在跑的进程没有 sudo,也不会因为 usermod 就突然进组
  5. 启动那一次靠 chmod;软件里再拔插,靠 udev 每次出现都设成 0666

板子常常是无辜的。真正挡路的,是那一次 chmod 走不进重连这条路。


—— Yumi

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

STM32 串口拔插后 Permission denied:启动能连,重连就没权限
https://www.yumio.cc/posts/stm32-serial-permission-denied/
作者
Yumi
发布于
2026-08-28
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Yumi
躬身入局,心为主理,行有尺度,自持本心
公告
欢迎来到 Yumi 的小窝喵~随便坐坐,记得常来玩呀!
分类
标签
最新动态
站点统计
文章
2
分类
2
标签
7
总字数
2,010
运行时长
0 天
最后活动
0 天前