UART串口驱动框架:从一次深夜调试说起
凌晨两点示波器上的波形还在跳串口就是不出数据。同事把逻辑分析仪往我桌上一放“115200波特率8N1配置绝对没错但tty设备就是没反应。” 我盯着内核日志里那句“ttyS0: tx fifo empty”突然意识到问题不在硬件——驱动框架没吃透调死都是白搭。串口驱动的三层结构Linux的UART驱动像个三明治。最上层是tty核心提供/dev/ttyS*设备节点处理行规程和用户空间IO。中间层是uart核心实现统一的注册接口和tty操作集。最底层才是我们常写的平台相关驱动负责操作具体的硬件寄存器。// 典型的驱动初始化模板staticintmy_uart_probe(structplatform_device*pdev){structuart_port*port;// 1. 从设备树获取硬件信息portdevm_kzalloc(pdev-dev,sizeof(*port),GFP_KERNEL);// 这里踩过坑MEM资源要用devm_ioremap_resource自己手写ioremap容易漏释放// 2. 填充port结构体port-opsmy_uart_ops;// 关键操作集port-lineof_alias_get_id(pdev-dev.of_node,serial);// line号别硬编码设备树里用aliases节点分配更稳妥// 3. 注册到uart核心uart_add_one_port(my_uart_driver,port);// 注册完不代表就能用了还要看console是否配置正确}那个让我熬通宵的fifo问题回到开头的问题。硬件fifo大小为16字节但驱动里写的fifosize 1。为什么因为早年抄的某个bsp模板就这么写的。结果就是tx中断疯狂触发实际只发了1字节就报empty。staticstructuart_opsmy_uart_ops{.tx_emptymy_tx_empty,// 这个函数要真实反映硬件状态.start_txmy_start_tx,// 启动发送前一定要清空状态寄存器里的错误标志};正确的做法是查芯片手册找到fifo深度寄存器或者直接写死真实的硬件值。更专业的做法是在probe里动态检测先写满fifo再读可用空间。中断处理里的门道串口中断处理函数最容易写崩。有人图省事在一个ISR里既处理接收又处理发送还处理错误最后spin_lock没用好系统偶尔卡死。staticirqreturn_tmy_uart_interrupt(intirq,void*dev_id){structuart_port*portdev_id;unsignedintstatus;spin_lock(port-lock);statusreadl(port-membaseREG_STATUS);// 顺序很重要先错误再接收最后发送if(statusREG_STATUS_ERR){handle_errors(port,status);// 清错误标志要趁早}if(statusREG_STATUS_RX_READY){my_rx_chars(port);// 这里记得用tty_insert_flip_char}if(statusREG_STATUS_TX_READY){my_tx_chars(port);// 发完记得关tx中断等下次start_tx再打开}spin_unlock(port-lock);returnIRQ_HANDLED;}控制台的那些坑想让内核早期printk从你的串口输出得实现console_write。这里有个细节控制台写函数不能依赖中断必须是纯轮询。因为早期中断系统还没初始化。staticvoidmy_console_write(structconsole*co,constchar*s,unsignedintcount){structuart_port*portmy_ports[co-index];// 关中断这里用_local_irq_saveunsignedlongflags;local_irq_save(flags);// 直接操作硬件寄存器输出字符while(count--){while(!(readl(port-membaseREG_STATUS)TX_READY))cpu_relax();// 忙等别无选择writel(*s,port-membaseREG_TX);}local_irq_restore(flags);}设备树里要配好stdout-path内核命令行要有consolettyS0,115200。两个地方对不上启动信息就可能跑到别的串口去。流控不是摆设产品到了现场偶尔丢包。加打印发现是缓冲区溢出。硬件流控RTS/CTS没启用软件流控XON/XOFF也没配。驱动里config_termios函数要认真实现staticvoidmy_config_port(structuart_port*port,intflags){// 检查硬件是否支持流控if(port-flagsUPF_HARD_FLOW){// 配置RTS/CTS引脚复用pinctrl_select_state(pinctrl,flow_ctrl_state);}// 软件流控更简单但占用带宽if(termios-c_iflagIXON){// 响应XON/XOFF字符}}调试技巧不止看日志echo 1 /sys/module/uart_core/parameters/debug打开uart核心调试信息。cat /proc/tty/driver/serial查看所有串口状态。用stty -F /dev/ttyS0检查当前终端设置。这些命令比重新编译内核快得多。个人经验写驱动前先当用户用minicom或picocom测试硬件是否正常排除硬件问题再动代码设备树是双刃剑配置灵活但容易写错用dtc -I dtb -O dts反编译确认别迷信波特率115200不是万能的长距离传输要降速高频时钟要精确计算分频休眠唤醒要测试串口唤醒系统是常见需求pm_runtimeAPI用对了省电用错了叫不醒保留原始寄存器值readl和writel之间可能被中断打断关键操作关中断或锁自旋锁那个凌晨的问题最后发现是时钟配置问题波特率发生器用的PLL未锁定实际波特率漂移了15%。硬件工程师指着原理图说“这芯片的UART时钟独立你没配置。” 所以啊驱动工程师得懂点硬件最好能看懂示波器波形——数据位宽度对不对停止位是不是1.5倍逻辑分析仪一抓便知。串口驱动看似简单但想稳定跑在生产环境每个细节都得抠。下次遇到“怎么调都不通”的情况先别怀疑人生从最基础的时钟、电源、引脚复用查起。硬件正常了软件才有发挥的余地。