简介这份资源围绕16550 UART在Linux下的串口驱动实现展开面向嵌入式开发、设备驱动编写与系统级调试的学习者帮助理解增强型UART的硬件接口、中断处理与流控制机制。压缩包共11个文件约13KB以C源码与头文件为核心辅以Shell脚本、Makefile、说明文档及编译好的ko模块覆盖驱动主体、测试程序、寄存器定义与模块加载卸载流程便于对照阅读与动手验证。资源中已有1267人学习说明其在串口驱动入门与进阶中具备一定参考价值。通过分析源码结构读者可以掌握初始化、波特率配置、读写操作、CTS/RTS与XON/XOFF流控制等关键环节并借助脚本与内核日志追踪驱动运行状态为嵌入式串口通信开发与内核模块调试积累可复用的思路。1. 16550 驱动到底在驱动什么从一块串口芯片到 tty 子系统的完整链路很多人第一次接触16550是在设备树里看到compatible ns16550a或者在内核日志里看到ttyS0 at MMIO 0x... (irq ...) is a 16550A。但真正动手写一个 16550 驱动或者排查 Linux 串口收数据丢失时才会发现这块芯片背后连着的是中断、FIFO、波特率分频、tty 线路规程和 DMA 一整套东西。serpi-master这类工程通常就是围绕 16550 做串口收发、超时接收和驱动适配的实践集合。它解决的核心问题是在 Linux 下让 UART16550 稳定收发尤其是处理「接收数据丢失」和「超时接收」这两个高频痛点。适合做嵌入式 Linux、工控网关、串口服务器、协议转换器的工程师也适合想从字符设备驱动框架切入内核的开发者。2. 16550 寄存器模型与 Linux 串口子系统的对接方式2.1 16550 的寄存器组和 FIFO 机制16550 相比 8250 最大的改进是带了 16 字节收发 FIFO并且有独立的 FIFO 控制寄存器FCR和中断标识寄存器IIR。它的寄存器是按偏移复用的同一个偏移读和写含义不同这是新手最容易翻车的地方。偏移读DLL 未使能写DLL 未使能读DLL 使能写DLL 使能0RBR 接收缓冲THR 发送保持DLL 分频低DLL 分频低1IER 中断使能IER 中断使能DLM 分频高DLM 分频高2IIR 中断标识FCR FIFO 控制IIRFCR3LCR 线路控制LCRLCRLCR4MCR 调制解调控制MCRMCRMCR5LSR 线路状态—LSR—6MSR 调制解调状态—MSR—关键点LCR 的 bit7 是 DLAB只有置 1 后才能访问 DLL/DLM 设置波特率。设置完必须清掉 DLAB否则后续读写 RBR/THR 会跑到分频寄存器上去表现为「串口完全没数据」。FIFO 控制寄存器 FCR 的典型配置/* 使能 FIFO清空收发 FIFO触发级别设为 8 字节 */ #define UART_FCR_ENABLE_FIFO 0x01 #define UART_FCR_CLEAR_RCVR 0x02 #define UART_FCR_CLEAR_XMIT 0x04 #define UART_FCR_TRIGGER_8 0x80 writel(UART_FCR_ENABLE_FIFO | UART_FCR_CLEAR_RCVR | UART_FCR_CLEAR_XMIT | UART_FCR_TRIGGER_8, port-membase UART_FCR);触发级别决定「FIFO 里攒够多少字节才产生中断」。设太高小包会一直等设太低中断风暴。8 或 14 是常见折中。2.2 Linux 串口驱动分层从 tty 到 8250_coreLinux 的串口驱动不是一个大整体而是分层的tty 层提供/dev/ttyS0设备节点处理线路规程line discipline。serial_coreuart_register_driver、uart_add_one_port管理uart_port和uart_ops。8250_core16550 兼容芯片的通用驱动处理中断、FIFO、波特率。具体平台驱动比如8250_dw、8250_of负责从设备树拿寄存器基址、时钟、中断号。serpi-master这类工程如果自己实现驱动通常是在8250_core之外写一个轻量字符设备或者直接基于serial_core注册uart_ops。两种路线取舍路线优点缺点适用场景基于 8250_core复用成熟中断/FIFO 处理定制困难依赖平台匹配标准 16550 兼容芯片自写字符设备完全可控便于加超时逻辑要自己处理 tty 缓冲、阻塞协议特殊、需要精确超时基于 serial_core兼顾 tty 框架和定制需要理解 uart_ops 全套回调想要 /dev/ttySx 接口我一般会如果只是标准收发直接用 8250_of 设备树如果要实现「超时接收」这种非标准语义就在字符设备层自己做避免和 tty 线路规程打架。2.3 设备树里 16550 节点的最小写法uart0: serial10000000 { compatible ns16550a; reg 0x10000000 0x100; interrupts 10; clock-frequency 1843200; reg-shift 2; reg-io-width 4; fifo-size 16; status okay; };参数说明reg-shift 2寄存器偏移按 4 字节对齐很多 SoC 的 UART 是 32 位访问偏移要左移 2 位。设错会读到错误寄存器。reg-io-width 4访问宽度 4 字节。如果芯片是 1 字节访问要改成 1。clock-frequency输入时钟波特率分频基于它算。写错会导致波特率偏差表现为乱码。fifo-size 16告诉驱动 FIFO 深度16550 是 16。提示reg-shift和reg-io-width是 16550 设备树里最容易配错的两个属性配错后往往不是完全没数据而是数据错位或偶发丢包排查时优先核对。3. 从零跑通 16550 收发中断、FIFO 与超时接收的实现3.1 中断处理IIR 判类型 LSR 判状态16550 的中断是共享的进中断后必须先读 IIR 判断类型再处理。IIR 的 bit0 为 0 表示有未处理中断bit3:1 是类型IIR bit3:1中断类型处理动作011接收数据可用读 RBR 直到 LSR bit0 为 0010发送保持空写 THR 或关中断110接收线路状态读 LSR 清中断000调制解调状态读 MSRstatic irqreturn_t uart16550_isr(int irq, void *dev_id) { struct uart16550_port *p dev_id; u8 iir, lsr; while (1) { iir readb(p-membase UART_IIR); if (iir UART_IIR_NO_INT) break; switch (iir UART_IIR_ID) { case UART_IIR_RDI: /* 接收数据可用 */ while ((lsr readb(p-membase UART_LSR)) UART_LSR_DR) ring_buf_put(p-rx, readb(p-membase UART_RBR)); break; case UART_IIR_THRI: /* 发送保持空 */ if (ring_buf_empty(p-tx)) writeb(readb(p-membase UART_IER) ~UART_IER_THRI, p-membase UART_IER); else writeb(ring_buf_get(p-tx), p-membase UART_THR); break; case UART_IIR_RLSI: /* 线路状态错误 */ lsr readb(p-membase UART_LSR); if (lsr UART_LSR_OE) p-stats.overrun; break; default: break; } } return IRQ_HANDLED; }逻辑说明循环读 IIR 直到 NO_INT 位为 1保证一次中断把所有挂起事件处理完。接收分支里用 LSR 的 DR 位判断 FIFO 是否还有数据避免漏读。发送分支在发送队列空时关掉 THRI 中断防止空转。参数说明UART_IIR_ID是掩码0x0e取 bit3:1。UART_LSR_DR是 bit0UART_LSR_OE是 bit1。这些宏在8250_reg.h或自己定义。3.2 超时接收为什么需要、怎么实现标准 16550 中断是「FIFO 达到触发级别」或「接收超时」触发。芯片自带的接收超时是字符间超时通常 4 个字符时间。但实际协议里经常需要「一帧数据在 N 毫秒内没新字节就认为结束」这就是应用层超时接收。常见做法有两种方案一内核定时器 环形缓冲static void uart16550_rx_timeout(struct timer_list *t) { struct uart16550_port *p from_timer(p, t, rx_timer); unsigned long flags; spin_lock_irqsave(p-lock, flags); if (p-rx.count 0) { /* 把当前缓冲作为一帧提交给上层 */ wake_up_interruptible(p-rx_wait); p-rx.frame_ready 1; } spin_unlock_irqrestore(p-lock, flags); } /* 每收到一个字节就重置定时器 */ static void uart16550_rx_char(struct uart16550_port *p, u8 ch) { ring_buf_put(p-rx, ch); mod_timer(p-rx_timer, jiffies msecs_to_jiffies(p-rx_timeout_ms)); }逻辑说明每来一个字节就mod_timer重置如果超过rx_timeout_ms没有新字节定时器到期标记一帧完成并唤醒读进程。rx_timeout_ms通常设 10~50ms取决于协议帧间隔。参数说明msecs_to_jiffies把毫秒转成 jiffies。定时器精度受HZ影响HZ100时最小 10ms 粒度要更细得用高精度定时器hrtimer。方案二应用层 select/poll VTIME如果不想改内核可以在用户态用termios的VTIME做字符间超时struct termios tio; tcgetattr(fd, tio); tio.c_cc[VTIME] 5; /* 0.5 秒字符间超时 */ tio.c_cc[VMIN] 0; /* 非阻塞读配合 VTIME */ tcsetattr(fd, TCSANOW, tio);VMIN0, VTIME5表示 read 在收到第一个字节后等待 0.5 秒没有新字节就返回。这是最省事的超时接收但精度和灵活性不如内核定时器。3.3 波特率计算与分频设置16550 的波特率分频公式divisor clock_frequency / (16 * baud_rate)比如时钟 1.8432MHz波特率 115200divisor 1843200 / (16 * 115200) 1DLL divisor 0xffDLM (divisor 8) 0xff。static void uart16550_set_baud(struct uart16550_port *p, u32 baud) { u32 divisor p-clk / (16 * baud); u8 lcr readb(p-membase UART_LCR); writeb(lcr | UART_LCR_DLAB, p-membase UART_LCR); writeb(divisor 0xff, p-membase UART_DLL); writeb((divisor 8) 0xff, p-membase UART_DLM); writeb(lcr ~UART_LCR_DLAB, p-membase UART_LCR); }逻辑说明先置 DLAB 才能写 DLL/DLM写完恢复 LCR。注意 divisor 为 0 是非法值实际要检查divisor 1。参数说明p-clk是输入时钟必须和设备树clock-frequency一致。如果实际时钟是 16 倍波特率的整数倍关系分频才准。非标准时钟会导致波特率误差误差超过 2% 就可能丢包。4. 16550 驱动避坑收数据丢失、乱码与中断不触发的排查4.1 现象接收数据偶发丢失尤其高速率下原因FIFO 触发级别设太高或者中断处理太慢FIFO 溢出LSR 的 OE 位置 1。另一个常见原因是接收中断里没有循环读到 LSR.DR 为 0只读一个字节就退出FIFO 里剩余数据等下次中断但下次中断可能因为触发级别没到而不来。解决接收中断里用while (LSR DR)循环读空 FIFO把 FCR 触发级别降到 1 或 4检查中断处理里有没有耗时操作如 printk。如果还丢考虑用 DMA 或增大 FIFO外接 16550 只有 16 字节可换 16950 或带更大 FIFO 的芯片。4.2 现象串口输出乱码波特率看起来对但就是不对原因reg-shift或reg-io-width配错导致读写寄存器偏移错位。或者clock-frequency和实际输入时钟不一致分频算出来偏差大。还有一种情况是 LCR 的数据位、停止位、校验位和对方不一致。解决先用示波器或逻辑分析仪量 TX 波形算实际波特率。核对设备树reg-shift大多数 32 位访问的 SoC 是 28 位访问是 0。确认clock-frequency来源有些 SoC 的 UART 时钟是 PLL 分频出来的要查时钟树。4.3 现象中断完全不触发/proc/interrupts 里计数为 0原因中断号配错或者中断触发方式上升沿/高电平和设备树不一致。也可能是 IER 没使能接收中断或者 IIR 的 NO_INT 位判断反了导致中断处理直接退出。解决先cat /proc/interrupts看有没有对应中断号计数是否增长。检查设备树interrupts属性。确认 IER 里UART_IER_RDI置 1。在 ISR 入口加临时计数确认是否进了中断。4.4 现象超时接收时一帧数据被拆成多帧原因超时时间设太短或者每个字节都重置定时器但定时器精度不够导致帧内字节间隔被误判为帧结束。另一种是中断里处理慢字节到达间隔被拉大。解决把rx_timeout_ms调大到协议帧间隔的 2~3 倍。用hrtimer替代timer_list提高精度。如果协议有固定帧头帧尾改用帧头帧尾解析而不是纯超时。4.5 现象DMA 模式下数据错位或丢失原因DMA 缓冲区和 FIFO 触发级别不匹配或者 DMA 传输完成中断和 UART 中断竞争。16550 本身不支持 DMA需要外接 DMA 控制器配置复杂。解决确认 DMA 通道和 UART 的握手信号正确。DMA 传输长度按 FIFO 触发级别对齐。在 DMA 完成中断里检查 LSR 的 OE 位。如果问题持续先退回中断模式验证功能再逐步加 DMA。5. 用 serpi-master 思路做验证回环测试、压力测试与参数调优serpi-master这类工程的价值在于把 16550 的收发、超时、统计做成可复现的测试集。我一般会按下面三步验证一个 16550 驱动是否靠谱。第一步硬件回环把 TX 和 RX 短接或者用TIOCM置内部回环MCR 的 bit4/* 使能内部回环 */ u8 mcr readb(p-membase UART_MCR); writeb(mcr | UART_MCR_LOOP, p-membase UART_MCR); /* 发一串数据看能否原样收到 */ u8 test[] 16550_loopback_test; for (int i 0; i sizeof(test); i) writeb(test[i], p-membase UART_THR); /* 等待接收比对 */内部回环能验证寄存器配置、FIFO、中断通路不依赖外部接线。如果回环都不通先查寄存器和中断。第二步压力测试用stty配好波特率然后用脚本持续发随机数据统计丢包率stty -F /dev/ttyS0 115200 cs8 -cstopb -parenb raw # 发送端 cat /dev/urandom | head -c 1000000 /dev/ttyS0 # 接收端统计 timeout 30 cat /dev/ttyS0 | wc -c对比发送和接收字节数丢包率应低于 0.01%。如果丢包明显回到第 4 章排查 FIFO 和中断。第三步参数调优表参数保守值激进值影响FIFO 触发级别114高触发减少中断但增延迟rx_timeout_ms505短超时响应快但易拆帧中断合并关开合并减少中断但增延迟DMA 突发长度416长突发效率高但占总线调优没有万能值取决于协议帧长、帧间隔和 CPU 负载。我习惯先用保守值跑通再逐步调激进每调一个参数跑一次压力测试记录丢包率和延迟。最后说个血泪经验16550 的坑大多不在驱动代码本身而在设备树属性、时钟配置和中断触发方式。我现在的习惯是新板子先不写驱动先用devmem直接读写 UART 寄存器确认基址、时钟、中断都对再上驱动。这样能把硬件问题和软件问题分开省下大量后悔药。希望帮到你。本文还有配套的精品资源点击获取