
简介本资源是一份面向嵌入式Linux驱动开发者与内核学习者的16550 UART串口驱动实践代码包聚焦于Linux环境下16550增强型串口硬件的驱动实现、调试与集成。资源包含11个核心文件涵盖3个C源码serpi.c、serpi_test.c、serpi_write.c实现驱动主体与测试逻辑2个头文件serpi.h、serial_reg.h定义寄存器映射与接口结构3个Shell脚本loads.sh、unloads.sh、disableserial.sh支持模块动态加载/卸载及串口禁用另含Makefile构建配置、README.md说明文档及编译生成的disser.ko内核模块整体压缩包仅13KB轻量但功能完整。已有1266人学习下载适合正在学习字符设备驱动开发、需深入理解中断处理、FIFO机制、波特率配置及内核模块交互流程的中高级开发者。通过该包可直接复现驱动编译加载、串口通信验证与底层寄存器操作是掌握Linux串口子系统原理与实战调试的高价值参考样本。1. 为什么 Linux 下串口收不到数据16550 UART 芯片不是“即插即用”驱动没对齐硬件寄存器映射就等于没接线你手头有一块工业主板或老式工控机串口设备比如 Modbus RTU 传感器、PLC 或 GPS 模块死活不回数据dmesg | grep tty显示ttyS0已注册stty -F /dev/ttyS0 9600也能设波特率但cat /dev/ttyS0就是空——连个换行符都不吐。这不是线没接好也不是设备坏了而是你正在和一个被现代内核“半放弃”的古老芯片打交道UART 16550。它不是 USB 转串口那种即插即用的黑盒而是靠内存映射 I/O 和精确的寄存器时序活着的“机械表芯”。16550_serpi-master这个仓库名里的serpi是serial port interface的缩写不是某个品牌而是指代一套面向 16550 兼容芯片的轻量级 Linux 字符设备驱动框架它不依赖8250通用驱动栈绕过serial_core的抽象层直接操作I/O port或MMIO地址只为解决一个痛点在嵌入式定制内核、实时系统或老旧 x86 平台下让 16550 真正稳定收发不丢字节、不溢出、不卡死。如果你正在做电力终端、铁路信号采集、数控机床通信或国产化替代项目且串口通信必须扛住 115200 波特率持续满载、抗电磁干扰、支持 FIFO 深度自定义那这个驱动不是“可选”而是“刚需”。它不解决 USB 串口CH340/CP2102、也不碰蓝牙串口只专注一件事把0x3F8COM1 默认端口上的 16550A 芯片变成 Linux 内核里一块听话的、可预测的、低延迟的字符设备。2. 从裸寄存器到字符设备16550_serpi 驱动的三层架构与最小启动路径16550_serpi-master不是一个“编译完 insmod 就能用”的傻瓜包。它是一套可裁剪的驱动开发模板核心价值在于其分层清晰、寄存器操作透明、中断逻辑可控。理解这三层才能避开后续所有玄学问题。2.1 硬件抽象层HAL为什么必须重写uart16550_read_reg()而不是抄8250的16550 的寄存器布局是标准的但访问方式千差万别传统 x86 PC用inb/outb指令访问 I/O 端口如0x3F8需request_region()申请资源ARM 嵌入式平台如 i.MX6、RK339916550 常作为 IP 核集成在 SoC 内走MMIO内存映射 I/O用readl/writelPCIe 扩展卡如 MOXA C104BAR 地址动态分配需pci_iomap()ioremap()。16550_serpi的 HAL 层强制你实现struct uart16550_ops// drivers/serial/16550_serpi/uart16550.h struct uart16550_ops { u8 (*read_reg)(struct uart16550_port *port, int reg); void (*write_reg)(struct uart16550_port *port, int reg, u8 val); void (*enable_irq)(struct uart16550_port *port); void (*disable_irq)(struct uart16550_port *port); int (*request_resources)(struct uart16550_port *port); void (*release_resources)(struct uart16550_port *port); };提示不要试图复用8250的serial_in/serial_out宏。8250为兼容性做了大量条件编译CONFIG_SERIAL_8250_RUNTIME_UARTS而16550_serpi要求你明确知道你的平台是I/O port还是MMIO是否需要barrier()内存屏障读写是否需加锁这些决策点都在 HAL 层暴露给你。2.2 设备模型层platform_devicevspci_device—— 怎么让内核找到你的串口16550_serpi支持两种注册方式选错一种dmesg就不会打印驱动加载日志platform_device最常用适用于板载串口如 Intel QM67 芯片组的 COM1。你需要在 DTSDevice Tree或板级初始化代码中声明// arch/arm/boot/dts/imx6q-sabresd.dts uart1 { compatible ns16550a; reg 0x02020000 0x1000; // MMIO 地址 interrupts GIC_SPI 60 IRQ_TYPE_LEVEL_HIGH; clock-names ipg, per; clocks clks IMX6Q_CLK_UART1, clks IMX6Q_CLK_UART1; status okay; };驱动侧匹配of_match_tablestatic const struct of_device_id uart16550_of_match[] { { .compatible ns16550a, }, { .compatible pnpPNP0501, }, // legacy PnP ID { } };pci_device适用于 PCIe 串口卡。16550_serpi提供uart16550_pci_probe()但需手动填充pci_device_id表static const struct pci_device_id uart16550_pci_ids[] { { PCI_DEVICE(0x131f, 0x0001) }, // MOXA C104 vendor/device ID { 0 } }; MODULE_DEVICE_TABLE(pci, uart16550_pci_ids);参数说明compatible字符串必须与驱动of_match_table完全一致大小写敏感reg地址必须是物理地址DTS 中或 BAR 地址PCIe 中不能是虚拟地址interrupts必须对应 GIC/SOC 中断控制器的实际编号查芯片手册确认。2.3 字符设备层cdev_add()后/dev/ttyS0是怎么被open()触发的16550_serpi的字符设备注册极简不走tty_register_driver()大框架而是直接绑定file_operationsstatic const struct file_operations uart16550_fops { .owner THIS_MODULE, .open uart16550_open, .read uart16550_read, .write uart16550_write, .ioctl uart16550_ioctl, .poll uart16550_poll, .llseek no_llseek, };关键在uart16550_open()它不做任何缓冲区分配只检查port-membase是否有效、port-irq是否已 request并调用uart16550_hw_init()初始化寄存器static int uart16550_open(struct inode *inode, struct file *filp) { struct uart16550_port *port container_of(inode-i_cdev, struct uart16550_port, cdev); if (!port-membase !port-iobase) return -ENODEV; // 关键清空 FIFO设置 Line Control Register (LCR) port-ops-write_reg(port, UART_LCR, UART_LCR_WLEN8 | UART_LCR_STOP2); // 8N2 port-ops-write_reg(port, UART_FCR, UART_FCR_ENABLE_FIFO | UART_FCR_CLEAR_XMIT | UART_FCR_CLEAR_RCVR); // 启用接收中断 port-ops-write_reg(port, UART_IER, UART_IER_RDI); port-ops-enable_irq(port); filp-private_data port; return 0; }逻辑说明open()时才真正初始化硬件避免模块加载时抢占资源UART_FCR设置CLEAR_XMIT/CLEAR_RCVR是为了清除上电残留数据UART_IER_RDI只开接收中断RDI不启用发送中断THRE因为write()是阻塞式轮询发送——这是16550_serpi的设计哲学收发解耦接收靠中断发送靠轮询杜绝因发送卡顿导致接收中断丢失。3. 编译、加载与设备节点生成三步走通make modules_install后的/dev/ttyS016550_serpi-master的 Makefile 是典型的内核模块构建风格但有三个易错点必须手动干预。3.1 Kbuild 配置KDIR必须指向你正在运行的内核源码树# Makefile KDIR ? /lib/modules/$(shell uname -r)/build obj-m 16550_serpi.o 16550_serpi-objs : uart16550_core.o uart16550_platform.o uart16550_pci.o all: make -C $(KDIR) M$(PWD) modules clean: make -C $(KDIR) M$(PWD) clean注意/lib/modules/$(uname -r)/build是符号链接指向/usr/src/linux-headers-$(uname -r)。如果你用的是定制内核如 Yocto 构建的linux-yocto这个路径可能不存在。正确做法是# 查看当前内核版本 uname -r # 输出5.10.123-custom # 确保该目录存在且包含 Makefile ls /usr/src/linux-headers-5.10.123-custom/Makefile # 若不存在需安装对应 headers 包或软链到你的内核源码根目录 sudo ln -sf /path/to/your/kernel/source /lib/modules/5.10.123-custom/build3.2 模块签名仅限 Secure Boot 启用环境modprobe报Required key not available怎么办当dmesg出现[ 123.456789] 16550_serpi: module verification failed: signature and/or required key missing - tainting kernel说明模块未签名而内核启用了CONFIG_MODULE_SIG_FORCEy。解决方案只有两个临时禁用签名验证测试用echo 0 | sudo tee /sys/module/module/parameters/enforce sudo modprobe 16550_serpi永久签名生产环境必需使用scripts/sign-file工具需先生成私钥signing_key.priv和 X.509 证书signing_key.x509# 生成密钥对仅一次 openssl req -new -x509 -newkey rsa:2048 -keyout signing_key.priv -out signing_key.x509 -days 36500 -subj /CNMy 16550 Driver Key/ -nodes # 签名模块 scripts/sign-file sha256 ./signing_key.priv ./signing_key.x509 ./16550_serpi.ko参数说明sha256是哈希算法必须与内核CONFIG_MODULE_SIG_HASHsha256一致signing_key.x509需导入 UEFI Secure Boot 密钥数据库DB否则即使签名成功modprobe仍会拒绝加载。3.3 设备节点自动创建udev规则没生效手动mknod是最后手段正常情况下16550_serpi加载后应自动创建/dev/ttyS0。若ls /dev/ttyS*为空先检查# 查看模块是否加载 lsmod | grep 16550_serpi # 应输出模块名、大小、使用计数 # 查看内核日志 dmesg | tail -20 | grep -i tty\|16550 # 输出示例 # [ 456.789012] 16550_serpi: Registered ttyS0 at 0x000003f8 (irq 4) is a 16550A若dmesg有注册日志但无设备节点说明udev规则缺失。16550_serpi不自带 udev 规则需手动添加# /etc/udev/rules.d/99-16550-serpi.rules KERNELttyS[0-9]*, SUBSYSTEMtty, DRIVER16550_serpi, SYMLINKserial/%k # 重载规则 sudo udevadm control --reload-rules sudo udevadm trigger --subsystem-matchtty避坑SUBSYSTEMtty和DRIVER16550_serpi必须同时匹配缺一不可SYMLINKserial/%k创建软链/dev/serial/ttyS0比直接NAME更安全避免与8250驱动冲突。4. 避坑16550_serpi 驱动的 4 个血泪经验——为什么cat /dev/ttyS0还是空16550_serpi的稳定性远超8250但前提是避开以下硬伤。这些不是 bug而是对硬件时序和内核机制的误读。4.1 现象dmesg显示ttyS0注册成功但echo test /dev/ttyS0无任何输出示波器测 TX 引脚无波形原因16550_serpi的write()是纯轮询发送它不检查THRETransmit Holding Register Empty标志位就直接写UART_TX寄存器导致数据被覆盖。标准 16550A 的THRE位在 FIFO 满时为 0但某些兼容芯片如 TI TL16C550C的THRE行为异常。解决在uart16550_write()中强制加入THRE等待循环// drivers/serial/16550_serpi/uart16550_core.c static ssize_t uart16550_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos) { struct uart16550_port *port filp-private_data; int i; u8 ch; for (i 0; i count; i) { if (get_user(ch, buf i)) return -EFAULT; // 关键等待 THRE 置位再写数据 while (!(port-ops-read_reg(port, UART_LSR) UART_LSR_THRE)) cpu_relax(); // 避免 busy-wait 占满 CPU可用 udelay(1) 替代 port-ops-write_reg(port, UART_TX, ch); } return count; }4.2 现象高波特率115200下接收数据大量丢失dmesg频繁打印overrun原因16550_serpi默认 FIFO 触发级别为11 字节触发中断在 115200 波特率下CPU 中断响应延迟 1 字节接收时间约 86.8μs导致 FIFO 溢出。解决修改uart16550_hw_init()中的UART_FCR设置将触发级别升至14最大深度// 在 uart16550_hw_init() 中 port-ops-write_reg(port, UART_FCR, UART_FCR_ENABLE_FIFO | UART_FCR_CLEAR_XMIT | UART_FCR_CLEAR_RCVR | UART_FCR_TRIGGER_14); // 替换原 UART_FCR_TRIGGER_1参数说明UART_FCR_TRIGGER_14对应FCR[7:6]11表示 FIFO 达 14 字节时触发中断大幅降低中断频率给 CPU 充足处理时间。4.3 现象stty -F /dev/ttyS0显示cs8 -parenb -parodd -cmspar -hupcl -cstopb正常但read()返回-EAGAIN原因16550_serpi的read()实现为非阻塞轮询它只读取 FIFO 中当前存在的字节不等待新数据。而用户空间程序如minicom默认以O_NONBLOCK打开设备导致read()立即返回-EAGAIN。解决在用户空间显式设置阻塞模式或修改驱动read()为阻塞式// 用户空间修复推荐 exec 3 /dev/ttyS0 stty -F /dev/ttyS0 115200 cs8 -cstopb -parenb # 用 dd 替代 cat避免 shell 缓冲干扰 dd if/dev/ttyS0 bs1 count10 2/dev/null或驱动侧加wait_event_interruptible()但会增加复杂度不推荐。4.4 现象多串口同时加载时ttyS1无法注册dmesg报Unable to handle kernel NULL pointer dereference原因16550_serpi的platform_driver使用全局struct uart16550_port ports[MAX_PORTS]数组但MAX_PORTS默认为1。当第二个platform_device注册时probe()函数尝试写入ports[1]而该内存未初始化。解决在uart16550_platform.c开头修改宏定义// drivers/serial/16550_serpi/uart16550_platform.c #define MAX_PORTS 4 // 支持最多 4 个串口 static struct uart16550_port ports[MAX_PORTS]; static int port_count;并在uart16550_platform_probe()中增加边界检查if (port_count MAX_PORTS) { dev_err(pdev-dev, Exceeded maximum port count %d\n, MAX_PORTS); return -ENOMEM; } port ports[port_count];5. 验证与调优用scope和strace抓住 16550 的真实心跳驱动能加载、设备节点能创建只是万里长征第一步。真正的验收标准是在 115200 波特率、连续 10 分钟满载100% duty cycle下零丢帧、零 overrun、中断延迟 50μs。以下是我在电力终端项目中验证16550_serpi的四步法。5.1 第一步用逻辑分析仪抓RX/TX引脚确认硬件层无毛刺这是最硬核的验证。不要相信dmesg或cat直接看波形接线RX→ LA 通道 0TX→ LA 通道 1GND 共地设置 LA采样率 ≥ 10 MHz115200 波特率需至少 10× 采样触发条件为RX下降沿起始位发送固定帧printf \x01\x02\x03\x04\x05 /dev/ttyS0观察波形✅ 正确每个字节 10 位1 起始 8 数据 1 停止位宽 ≈ 8.68μs无粘连、无抖动❌ 错误位宽跳变时钟不稳、起始位丢失LSR未清、停止位缺失LCR设置错误。技巧用LA的协议解析功能直接 decode UART对比发送内容与接收波形 HEX 值误差为 0 才算通过。5.2 第二步用strace监控read()系统调用看内核到用户空间的数据流strace能暴露驱动read()的真实行为# 启动一个持续读进程 strace -e traceread,write -p $(pgrep -f cat /dev/ttyS0) 21 | grep read输出示例read(3, \x01\x02\x03\x04\x05, 4096) 5 read(3, \x06\x07\x08\x09\x0a, 4096) 5✅ 正常每次read()返回字节数稳定无-EAGAIN或0❌ 异常出现read(3, , 4096) 0EOF说明驱动提前关闭了设备或频繁-EAGAIN驱动未正确处理阻塞。5.3 第三步用perf测中断延迟定位IRQ响应瓶颈16550_serpi的性能天花板在中断延迟。用perf抓取irq/4-ttyS0的调度延迟# 开启 perf 监控需 root sudo perf record -e irq:irq_handler_entry,irq:irq_handler_exit -g -a sleep 10 sudo perf script | grep irq/4 | head -20关键指标irq_handler_entry到irq_handler_exit的时间差应 50μs。若 100μs检查是否有其他高优先级中断抢占如网卡eth0CONFIG_PREEMPT_RT是否启用实时内核可降至 10μsirqbalance是否将ttyS0中断绑定到 busy CPU core用cat /proc/interrupts | grep ttyS0查看。5.4 第四步压力测试脚本 —— 模拟真实 Modbus 场景写一个 Python 脚本模拟工业现场最严苛的通信# stress_test.py import serial, time, random ser serial.Serial(/dev/ttyS0, 115200, timeout0.1) # 发送 Modbus RTU 请求帧功能码 0x03读保持寄存器 req b\x01\x03\x00\x00\x00\x0a\xc5\xcb # slave1, start0, len10 start time.time() for i in range(1000): ser.write(req) try: rsp ser.read(256) # 读响应 if len(rsp) 5: # 至少 5 字节slavefuncbytecntdatacrc print(fFrame {i}: short response {len(rsp)} bytes) except Exception as e: print(fFrame {i}: exception {e}) end time.time() print(f1000 frames in {end-start:.2f}s, avg {1000/(end-start):.1f} fps)✅ 通过标准1000 帧全部收到完整响应无short response报告❌ 失败信号short response出现 3 次或exception频发说明驱动read()有竞态。我在线上项目踩过的最大坑是以为16550_serpi的read()是原子的结果在多线程访问同一/dev/ttyS0时port-rx_fifo指针被并发修改导致read()返回乱码。后来加了spin_lock_irqsave(port-lock, flags)才彻底解决。这个教训让我养成了一个习惯任何直接操作硬件 FIFO 的驱动只要涉及多线程或中断上下文共享锁必须加在寄存器读写之前而不是函数入口。希望帮到你。本文还有配套的精品资源点击获取