串口驱动开发:从内核源码到调试坑位全解析

张开发
2026/4/21 17:18:48 15 分钟阅读
串口驱动开发:从内核源码到调试坑位全解析
昨天深夜调试现场设备管理器里能看到ttyS0但cat /dev/ttyS0就是没数据。示波器测TX脚明明有波形minicom里却一片死寂。这种“硬件有信号软件没反应”的尴尬十有八九是串口驱动配置出了问题。今天咱们就深挖Linux串口驱动的那些门道。一、串口驱动框架全景Linux的串口子系统是个经典的分层架构。最底层是硬件相关的驱动比如8250.c这种通用驱动或者各家芯片厂商的私有实现。中间层是tty层负责缓冲区和流控。最上层才是我们平时用的/dev/ttyS*设备节点。老规矩先看设备树。现在嵌入式开发几乎都走设备树了但很多老驱动还在用platform_device那套。以ARM平台为例设备树里串口节点长这样uart0:serialfe001000{compatiblevendor,my-uart;reg0xfe0010000x100;interruptsGIC_SPI12IRQ_TYPE_LEVEL_HIGH;clocksuart_clk;clock-frequency115200;statusdisabled;};这里有个坑clock-frequency属性不是必须的但如果你没填有些驱动会默认用9600。结果就是你配置115200实际跑的是9600两边对不上。二、驱动代码实战拆解拿个最简单的串口驱动骨架看看。现在内核推荐用serial_core框架别自己从头造轮子。staticintmy_uart_probe(structplatform_device*pdev){structuart_port*port;structresource*res;// 1. 申请端口结构体portdevm_kzalloc(pdev-dev,sizeof(*port),GFP_KERNEL);if(!port)return-ENOMEM;// 2. 获取内存资源resplatform_get_resource(pdev,IORESOURCE_MEM,0);port-membasedevm_ioremap_resource(pdev-dev,res);if(IS_ERR(port-membase))returnPTR_ERR(port-membase);// 3. 配置端口参数port-lineof_alias_get_id(pdev-dev.of_node,serial);if(port-line0)port-line0;// 没定义别名就用0port-typePORT_MY_UART;// 自定义类型port-iotypeUPIO_MEM;// 内存映射IOport-irqplatform_get_irq(pdev,0);port-uartclkclk_get_rate(clk);// 这里一定要拿到正确的时钟// 4. 关键设置ops操作集port-opsmy_uart_ops;// 5. 注册到串口核心retuart_add_one_port(my_uart_driver,port);if(ret){dev_err(pdev-dev,添加端口失败: %d\n,ret);returnret;}platform_set_drvdata(pdev,port);return0;}重点在ops结构体这是驱动的心脏staticconststructuart_opsmy_uart_ops{.tx_emptymy_tx_empty,.set_mctrlmy_set_mctrl,.get_mctrlmy_get_mctrl,.stop_txmy_stop_tx,.start_txmy_start_tx,.stop_rxmy_stop_rx,.enable_msmy_enable_ms,.break_ctlmy_break_ctl,.startupmy_startup,.shutdownmy_shutdown,.set_termiosmy_set_termios,.typemy_type,.config_portmy_config_port,};.set_termios是最容易出问题的回调。这里要配置波特率、数据位、停止位、校验位。常见错误是没处理好时钟分频导致实际波特率偏差太大。三、调试技巧与坑位记录检查时钟树串口时钟不对一切都白搭。用clk_summary看时钟频率cat /sys/kernel/debug/clk/clk_summary | grep uart确保uartclk和你期望的一致。我遇到过PLL配置被bootloader改掉的情况。DMA还是FIFO高速串口比如3Mbps以上建议开DMA。但DMA配置很讲究缓存对齐不对直接丢数据。用dmaengine框架的话注意dma_alloc_coherent返回的可能是非缓存内存。中断风暴防护有些芯片的串口中断设计有缺陷RX引脚悬空时可能产生连续中断。在中断处理函数里加个计数超过阈值就disable_irq打印警告。procfs调试接口自己加调试节点实时看寄存器状态seq_printf(m,LSR: 0x%02x\n,readb(port-membaseUART_LSR));seq_printf(m,发送队列: %d/%d\n,uart_circ_chars_pending(port-state-xmit),UART_XMIT_SIZE);早期console如果串口要做earlycon实现early_write时别用复杂函数。那时内存管理还没初始化kmalloc都不能用。四、用户空间适配要点驱动写好了用户空间配置不对照样不工作。几个检查点确认设备节点权限crw-rw---- 1 root dialout 4, 64 /dev/ttyS0stty配置stty -F /dev/ttyS0 115200 cs8 -parenb -cstopb如果要用RS485通过ioctl设置RTS方向控制structserial_rs485rs485conf;rs485conf.flags|SER_RS485_ENABLED;ioctl(fd,TIOCSRS485,rs485conf);五、经验之谈串口驱动看似简单但稳定性要求极高。工业现场一个丢包可能就是重大事故。我的习惯是第一上电先做环路测试。短接TX和RX自发自收验证数据通路。第二压力测试用cat /dev/urandom /dev/ttyS0同时另一个终端接收跑24小时看有没有丢帧。第三睡眠唤醒测试特别重要很多驱动在suspend/resume后寄存器状态恢复不全。最后留个思考题为什么有些驱动要在shutdown里关时钟有些却不关这涉及到设备电源管理域的设计。简单说如果这个串口是系统console关了时钟内核panic时连打印都没了。所以看芯片手册的电源域章节比盲目抄代码管用。驱动调试到凌晨三点是常事但看到[ 0.123456] my_uart fe001000.serial: ttyS0 at MMIO 0xfe001000 (irq 12) is a my_uart这句打印出来数据灯开始闪烁那种成就感值了。

更多文章