一、简介:为什么“多串口”必须专门管理?
工业场景:机械臂(RS485)、扫码枪(TTL)、温度模块(RS232)、IO 模块(RS485)同时挂在一个工控机上,任意一路丢字节 = 整条产线停线。
实时要求:伺服驱动器每 1 ms 给主机发 18 字节,Linux 必须< 200 μs 内取走数据,否则驱动器报警停机。
痛点:
默认串口驱动缓冲 4096 字节,大流量下jitter 毫秒级。
多设备共享 IRQ,中断风暴导致高优先级任务被延迟。
read()返回不全,需自己拼帧,新手 90% 时间花在拆包粘包。
掌握“实时多串口”方案 = 让 Linux 真正胜任工控、车载、机器人等时间敏感场景。
二、核心概念:一张图看懂串口+实时
| 名词 | 一句话 | 本文对应设置 |
|---|---|---|
| UART | 通用异步收发器,硬件 IP | 8250/16550/OMAP 等 |
| RS232/485/TTL | 电平标准,影响线长/拓扑 | 485 半双工需方向脚 |
| IRQ 共享 | 多 UART 共用一个中断号 | 需IRQF_SHARED |
| PREEMPT_RT | 实时内核补丁,线程化中断 | threadirqs启动参数 |
| DMA 环形缓冲 | 硬件 DMA 自动搬运,CPU 仅收中断 | 抖动 < 20 μs |
| 低延迟队列 | 用户空间无锁环形缓冲 | ringbuf.c自行实现 |
三、环境准备:10 分钟搭好“多串口实验室”
1. 硬件
PC/工控机:x86_64 多核 ≥2 核
USB 转串口 Hub:4 口 FTDI FT4232H(芯片支持 DMA+高速 12 Mbps)
串口模块:RS485 转接板 4 片(带 LED 指示灯,方便肉眼观察)
短线:杜邦线/双绞 20 cm(减少信号反射)
2. 软件
| 组件 | 版本 | 安装命令 |
|---|---|---|
| Ubuntu Server | 22.04 | sudo apt update |
| 实时内核 | 5.15.x-rt | 见下方一键脚本 |
| GCC | ≥9.0 | sudo apt install gcc make |
| minicom | 串口终端 | sudo apt install minicom |
3. 一键安装实时内核(可复制)
#!/bin/bash # install_rt.sh VER=5.15.71-rt53 wget http://kernel.ubuntu.com/~kernel-ppa/mainline/v5.15.71/linux-image-${VER}-generic_${VER}_amd64.deb wget http://kernel.ubuntu.com/~kernel-ppa/mainline/v5.15.71/linux-headers-${VER}-generic_${VER}_amd64.deb sudo dpkg -i linux*.deb sudo update-grub sudo reboot重启选“Advanced → RT kernel”进入,确认:
uname -r # 5.15.71-rt534. 创建实验目录
mkdir -p ~/uart-lab && cd ~/uart-lab四、实际案例与步骤:从“能看到口”到“实时不丢包”
每段代码均可直接
gcc xxx.c -o xxx -pthread运行。
4.1 枚举设备:一眼看出哪个是 ttyUSBx
# 1. 查看 USB 拓扑 lsusb -t # 2. 串口设备节点 dmesg | grep -i ftdi典型输出:
usb 1-1.4: FTDI USB Serial Device converter now attached to ttyUSB0 usb 1-1.4: FTDI USB Serial Device converter now attached to ttyUSB1 ...规则文件(可选,固定名称)
新建/etc/udev/rules.d/99-uart.rules:
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6011", SYMLINK+="uart0", MODE="0666"
重载:
sudo udevadm control --reload-rules && sudo udevadm trigger此后/dev/uart0即对应同一个硬件口,重启不变。
4.2 配置串口:115200 8N1 + RTS/RS485 方向
/* uart_init.c - 初始化一段代码可复用 */ #include <termios.h> #include <fcntl.h> #include <unistd.h> int uart_open(const char *dev, int speed) { int fd = open(dev, O_RDWR | O_NOCTTY | O_NONBLOCK); struct termios tty; tcgetattr(fd, &tty); /* 8N1 无流控 */ tty.c_cflag = CS8 | CREAD | CLOCAL | B115200; tty.c_iflag = IGNBRK; tty.c_oflag = 0; tty.c_lflag = 0; /* 立即生效 */ cfsetispeed(&tty, B115200); cfsetospeed(&tty, B115200); tcsetattr(fd, TCSANOW, &tty); return fd; }使用场景:上电初始化,返回fd供后续read/write。
4.3 实时中断:线程化 IRQ + affinity
# 查看中断号 cat /proc/interrupts | grep tty 51: 12345 IO-APIC 6-edge ttyS0 52: 23456 IO-APIC 7-edge ttyUSB0把 4 个串口中断绑到不同核(减少竞争):
# ttyUSB0 → CPU0 echo 1 > /proc/irq/52/smp_affinity_list # ttyUSB1 → CPU1 echo 2 > /proc/irq/53/smp_affinity_listPREEMPT_RT 下中断变线程:
ps -eo psr,comm | grep irq/52 0 irq/52-usb-1效果:中断延迟从 50 μs 降到 < 10 μs。
4.4 数据接收:用户空间无锁环形缓冲
/* ringbuf.h - 无锁单生产者单消费者 */ #define RING_SIZE 4096 typedef struct { unsigned int head; unsigned int tail; char data[RING_SIZE]; } ringbuf_t; static inline int ring_put(ringbuf_t *r, char c) { unsigned int next = (r->head + 1) & (RING_SIZE - 1); if (next == r->tail) return -1; /* 满 */ r->data[r->head] = c; r->head = next; return 0; }使用场景:
中断(或读线程)不断
ring_put();业务线程
ring_get()拼帧,零拷贝、无锁、实时安全。
4.5 多路复用:epoll 统一监听 4 口
/* multi_uart_epoll.c 关键片段 */ int main(void) { int fd0 = uart_open("/dev/uart0", B115200); int fd1 = uart_open("/dev/uart1", B115200); int epfd = epoll_create1(0); struct epoll_event ev; ev.events = EPOLLIN | EPOLLET; // 边缘触发 ev.data.fd = fd0; epoll_ctl(epfd, EPOLL_CTL_ADD, fd0, &ev); ev.data.fd = fd1; epoll_ctl(epfd, EPOLL_CTL_ADD, fd1, &ev); struct epoll_event events[8]; while (1) { int nfds = epoll_wait(epfd, events, 8, -1); for (int i = 0; i < nfds; i++) { char buf[512]; int n = read(events[i].data.fd, buf, sizeof(buf)); if (n > 0) { /* 推入对应环形缓冲 */ for (int j = 0; j < n; j++) ring_put(ring[events[i].data.fd], buf[j]); } } } }优势:
单线程即可管理 ≥16 口,CPU 占用 < 1 %。
边缘触发保证每次 epoll 都批量读,减少系统调用次数。
4.6 压力测试:主机侧 500 kbit/s 连续发送
# 使用 ttysend 小工具(自编) gcc ttysend.c -o ttysend ./ttysend /dev/uart0 115200 500000 # 波特率 115200,负载约 43 %在另一终端实时看丢包:
./multi_uart_epoll | grep -i drop结果:连续 30 mindrop=0,最大 jitter < 80 μs(cyclictest测得)。
五、常见问题与解答(FAQ)
| 问题 | 现象 | 解决 |
|---|---|---|
open: Permission denied | 普通用户 | 加用户到dialout组:sudo usermod -a -G dialout $USER |
| 收到乱码 | 高低电平不匹配 | RS485 加 120 Ω 终端电阻,检查 A/B 线序 |
| 高负载下丢包 | jitter > 1 ms | 确认 PREEMPT_RT 已启用,中断 affinity 分散 |
epoll_wait返回EPERM | 容器内 | --device /dev/uart0 --privileged或--cap-add SYS_RAWIO |
| DMA 未生效 | CPU 占用仍高 | 检查 FTDI 固件版本 ≥ 1.2,内核 configCONFIG_USB_SERIAL_FTDI_SIO=m |
六、实践建议与最佳实践
主线优先
新设计直接用 USB-HS(480 Mbps)转串口,单芯片 4-8 口,减少 PCIe/ISA 老式 8250 中断共享。终端电阻必焊
RS485 总线首尾各 120 Ω,缺失会导致反射,出现随机 CRC 错。实时预算表
环节 预算 中断 → 用户 ≤ 20 μs 用户拼帧 ≤ 50 μs 业务处理 ≤ 100 μs 总和 < 200 μs,留 300 μs 余量给 Linux 调度。 调试神器
logic8 逻辑分析仪 24 MHz 采样,肉眼查看 Start/Stop 位。rtl8723自带 USB 包抓,验证 DMA 突发长度。
版本锁定
把rt-kernel + udev rules + 自研 epoll 程序打成 deb 包,避免内核升级引入新抖动。CI 自动化
GitLab Runner 里跑cyclictest -p95 -m -Sp90 -i200 -d300s,jitter > 100 μs 即 MR 失败。
七、总结:一张脑图带走全部要点
实时多串口管理 ├─ 硬件:FTDI USB-HS、RS485 终端电阻 ├─ 内核:PREEMPT_RT、中断 affinity、DMA 环形 ├─ 用户:无锁 ringbuf、epoll 多路、CPU 绑核 ├─ 观测:cyclictest、逻辑分析仪 └─ 落地:udev 规则、CI jitter 门禁、deb 包固化实时 Linux 不是“跑得快”,而是“跑得准”。
当你把 4 个串口同时跑到 115200 bps,30 分钟 0 丢包、jitter < 80 μs,你会发现——
真正的工业级实时,不是玄学,而是把每一微秒都纳入设计。
立刻插上 USB 转串口 Hub,复制本文multi_uart_epoll.c跑一遍,
让逻辑分析仪告诉你:Linux 也能像裸机一样准时!