昨天深夜调试现场,设备管理器里能看到ttyS0,但cat /dev/ttyS0就是没数据。示波器测TX脚明明有波形,minicom里却一片死寂。这种“硬件有信号,软件没反应”的尴尬,十有八九是串口驱动配置出了问题。今天咱们就深挖Linux串口驱动的那些门道。
一、串口驱动框架全景
Linux的串口子系统是个经典的分层架构。最底层是硬件相关的驱动,比如8250.c这种通用驱动,或者各家芯片厂商的私有实现。中间层是tty层,负责缓冲区和流控。最上层才是我们平时用的/dev/ttyS*设备节点。
老规矩,先看设备树。现在嵌入式开发几乎都走设备树了,但很多老驱动还在用platform_device那套。以ARM平台为例,设备树里串口节点长这样:
uart0:serial@fe001000{compatible="vendor,my-uart";reg=<0xfe0010000x100>;interrupts=<GIC_SPI12IRQ_TYPE_LEVEL_HIGH>;clocks=<&uart_clk>;clock-frequency=<115200>;status="disabled";};这里有个坑:clock-frequency属性不是必须的,但如果你没填,有些驱动会默认用9600。结果就是你配置115200,实际跑的是9600,两边对不上。
二、驱动代码实战拆解
拿个最简单的串口驱动骨架看看。现在内核推荐用serial_core框架,别自己从头造轮子。
staticintmy_uart_probe(structplatform_device*pdev){structuart_port*port;structresource*res;// 1. 申请端口结构体port=devm_kzalloc(&pdev->dev,sizeof(*port),GFP_KERNEL);if(!port)return-ENOMEM;// 2. 获取内存资源res=platform_get_resource(pdev,IORESOURCE_MEM,0);port->membase=devm_ioremap_resource(&pdev->dev,res);if(IS_ERR(port->membase))returnPTR_ERR(port->membase);// 3. 配置端口参数port->line=of_alias_get_id(pdev->dev.of_node,"serial");if(port->line<0)port->line=0;// 没定义别名就用0port->type=PORT_MY_UART;// 自定义类型port->iotype=UPIO_MEM;// 内存映射IOport->irq=platform_get_irq(pdev,0);port->uartclk=clk_get_rate(clk);// 这里一定要拿到正确的时钟// 4. 关键!设置ops操作集port->ops=&my_uart_ops;// 5. 注册到串口核心ret=uart_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_empty=my_tx_empty,.set_mctrl=my_set_mctrl,.get_mctrl=my_get_mctrl,.stop_tx=my_stop_tx,.start_tx=my_start_tx,.stop_rx=my_stop_rx,.enable_ms=my_enable_ms,.break_ctl=my_break_ctl,.startup=my_startup,.shutdown=my_shutdown,.set_termios=my_set_termios,.type=my_type,.config_port=my_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->membase+UART_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/ttyS0 - stty配置:
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这句打印出来,数据灯开始闪烁,那种成就感,值了。