关闭USB CDC-ACM串行接口时清除缓冲区的问题排查
我正在为NXP i.MX 8M mini SoC嵌入式设备开发软件,该设备运行Linux 5.15.32内核,通过USB复合gadget利用SoC内置USB接口提供串行I/O功能。
该gadget同时提供RNDIS网络接口和ACM串行接口,两者功能正常,但存在以下问题:当周期性向ACM设备/dev/ttyGS0写入数据的应用终止时,关闭该设备文件描述符有时会耗时16秒。
现象细节
- 延迟仅在ACM接口另一端无应用读取数据时出现;若主机端有读取操作(如执行
cat /dev/ttyACM0 > /dev/null),close()调用会立即完成。 - 确定延迟是由USB驱动中排队的数据导致,
close()调用在等待数据被远端接收,16秒后超时。
已尝试的解决方法
- 在
close()前调用tcflush(fd, TCIOFLUSH),无效果; - 将波特率设为
B0(调制解调器挂起事件),期望清除数据,同样无效; - 显式启用/禁用流控制(
c_cflag |= CRTSCTS),无变化。
相关配置代码
USB gadget配置脚本(bash)
modprobe libcomposite cd /sys/kernel/config/usb_gadget mkdir g cd g echo 0x1d6b > idVendor # Linux Foundation echo 0x0104 > idProduct # Multifunction composite gadget echo 0x0100 > bcdDevice # v1.0.0 echo 0x0200 > bcdUSB # USB 2.0 # Miscellaneous / Wire Adapter Multifunction programming interface echo 0xEF > bDeviceClass echo 0x02 > bDeviceSubClass echo 0x01 > bDeviceProtocol mkdir -p strings/0x409 echo "my_serial_no" > strings/0x409/serialnumber echo "my_manufacturer" > strings/0x409/manufacturer mkdir -p functions/acm.usb0 mkdir -p configs/c.1 echo 250 > configs/c.1/MaxPower ln -s functions/acm.usb0 configs/c.1 (creation and configuration of RNDIS function omitted) ln -s configs/c.1 os_desc
文件描述符打开与配置代码(C++)
fd = open("/dev/ttyGS0", O_RDWR | O_NONBLOCK); struct termios settings; tcgetattr(fd, &settings); cfsetospeed(&settings, B1000000); // 1Mbps settings.c_cflag &= ~(CSTOPB | // 1 stop bit CSIZE | // Clear previous bit-size bits PARENB); // Don't use parity checking settings.c_cflag |= (CS8 | // 8 data bits CLOCAL); // Ignore modem control lines settings.c_iflag &= ~(IGNBRK | // Don't ignore break BRKINT | // Don't generate SIGINT on BRK PARMRK | // Don't mark framing/parity errors ISTRIP | // Don't strip the 8th bit INLCR | // Don't translate NL to CR IGNCR | // Don't ignore CR ICRNL | // Don't ignore NL IXON); // Don't use XON/XOFF flow control settings.c_oflag &= ~OPOST; // Don't use implementation-defined output processing settings.c_lflag &= ~(ECHO | // Don't echo input characters ECHONL | // Don't echo NL ICANON | // Don't use canonical mode ISIG | // Don't generate signals IEXTEN); // Don't use implementation-defined input processing settings.c_cc[VTIME] = 0; // Disable timeout for non canonical read settings.c_cc[VMIN] = 0; // No minimum chars for non canonical read tcsetattr(fd, TCSANOW, &settings); tcflush(fd, TCOFLUSH);
1. 是否可通过修改ACM设备配置或termios配置解决该问题?
常规的termios配置(包括流控制、波特率、flush操作)无法解决这个问题,因为延迟的根源是USB gadget驱动层的发送队列未被清空,而非tty层的缓冲区。你尝试的tcflush仅操作tty层的用户态/内核态缓冲区,无法触达USB驱动中已提交到硬件队列或等待主机ACK的数据。
ACM设备的gadget配置(如class/subclass/protocol)也不影响关闭时的超时逻辑,这些配置仅定义设备的USB枚举属性,和驱动的关闭行为无关。
2. 若不可行,是否有办法告知Linux关闭文件描述符时无需等待?
可以在close()之前调用ioctl(fd, TIOCSFLUSH, (void*)1),这个操作会强制tty驱动清空所有未发送的缓冲区,包括USB驱动层的队列。Linux 5.15内核已支持该操作,能替代无效的tcflush,让close()无需等待超时。
另外,打开设备时设置O_NDELAY标志仅影响读写操作的阻塞行为,对close()的等待逻辑无直接作用。
3. 若上述均不可行,是否有办法让ACM驱动刷新/丢弃缓冲区以避免close()等待?
除了TIOCSFLUSH ioctl,还可以通过sysfs节点重置ACM功能:
- 进入ACM功能的sysfs目录(如
/sys/kernel/config/usb_gadget/g/functions/acm.usb0) - 执行
echo 1 > reset,这会重置ACM接口的发送队列,丢弃所有未发送的数据。
不过该方法需要应用拥有root权限,且可能影响其他使用该ACM接口的进程,需谨慎使用。
如果允许修改内核,可在drivers/usb/gadget/function/f_acm.c的acm_tty_release函数中,强制调用usb_ep_disable或直接清空发送队列,替代默认的超时等待逻辑,之后重新编译内核即可。
4. 若以上都无法实现,设备能否检测到ACM接口另一端是否有应用连接,从而让我的应用在无接收端时停止发送数据?
可以通过检测tty设备的CD(Carrier Detect)状态判断主机端是否有应用打开ACM接口:当主机端打开/dev/ttyACM0时,USB gadget驱动会设置tty的CD标志;主机端关闭设备时,CD标志会被清除。
应用中可通过以下代码检测:
int status; ioctl(fd, TIOCMGET, &status); if (!(status & TIOCM_CD)) { // 主机端无应用连接,停止发送数据 }
也可以监听tty设备的POLLPRI事件,当CD状态变化时,应用会收到通知,从而动态调整发送逻辑。
内容的提问来源于stack exchange,提问作者David C.

