【Linux】ARM篇七--UART串口驱动开发与调试实战

张开发
2026/4/18 14:36:20 15 分钟阅读

分享文章

【Linux】ARM篇七--UART串口驱动开发与调试实战
1. UART串口驱动开发基础第一次接触ARM平台的UART驱动开发时我完全被各种寄存器配置绕晕了。后来发现理解UART驱动其实就像学习一门新的外语——先掌握基本语法寄存器操作再学习高级表达内核框架。在Exynos 4412这类ARM芯片上UART驱动开发可以分为三个层次最底层的寄存器操作、中间层的tty子系统对接以及最上层的用户空间接口。以Exynos 4412的UART2为例我们需要先配置GPA1.CON寄存器将引脚切换为UART模式。这里有个坑引脚复用配置容易出错。我遇到过GPIO模式和UART模式混淆的情况导致数据根本无法传输。正确的配置应该是GPA1.CON (GPA1.CON ~0xFF) | 0x22; // 将GPA1_0和GPA1_1设置为UART模式波特率计算是另一个容易出错的地方。芯片手册给出的公式是DIV_VAL (SCLK_UART / (bps × 16)) − 1假设系统时钟100MHz要配置115200波特率时UART2.UBRDIV2 53; // 整数部分 UART2.UFRACVAL2 4; // 小数部分2. Linux内核驱动框架集成当我们需要将裸机程序升级为内核驱动时首先要理解Linux的tty子系统架构。就像搭积木一样UART驱动需要实现以下几个关键组件struct uart_driver注册驱动到内核struct uart_port描述硬件端口信息struct uart_ops实现具体的操作函数我常用的驱动初始化模板如下static int __init exynos_uart_init(void) { int ret; ret uart_register_driver(exynos_uart_drv); if (ret) { printk(KERN_ERR Failed to register driver\n); return ret; } uart_add_one_port(exynos_uart_drv, exynos_uart_port); return 0; }调试时最有用的是printk的八个日志级别。我习惯在关键路径加KERN_DEBUG级打印出错时改用KERN_ERR。比如检测FIFO状态if (!(readl(port-membase UTRSTAT) UTRSTAT_TX_EMPTY)) { printk(KERN_DEBUG TX FIFO not empty\n); }3. 设备树配置实战现代Linux内核强烈推荐使用设备树来描述硬件。Exynos 4412的UART设备树节点通常长这样uart13820000 { compatible samsung,exynos4210-uart; reg 0x13820000 0x100; interrupts 0 54 0; clocks clock 265, clock 266; clock-names uart, clk_uart_baud0; status okay; };有次调试发现串口无法工作最后发现是clock-names不匹配。通过sysfs查看时钟状态才定位到问题cat /sys/kernel/debug/clk/clk_summary | grep uartDMA配置是提升性能的关键。在设备树中添加dma属性后驱动中需要实现dma_opsdmas pdma0 15, pdma0 16; dma-names tx, rx;4. 用户空间交互实现要让应用层能够使用串口我们需要提供字符设备接口。Linux内核已经帮我们封装好了tty核心层主要工作包括实现tty_driver的open/release方法处理termios设置波特率/数据位等提供write_room和chars_in_buffer回调我常用的数据收发函数对static unsigned int exynos_tx_empty(struct uart_port *port) { return (readl(port-membase UTRSTAT) UTRSTAT_TX_EMPTY) ? TIOCSER_TEMT : 0; } static void exynos_start_tx(struct uart_port *port) { struct circ_buf *xmit port-state-xmit; if (uart_circ_empty(xmit)) return; writel(xmit-buf[xmit-tail], port-membase UTXH); xmit-tail (xmit-tail 1) (UART_XMIT_SIZE - 1); }调试时可以借助proc文件系统暴露内部状态static int exynos_uart_proc_show(struct seq_file *m, void *v) { struct uart_port *port m-private; seq_printf(m, registers:\n); seq_printf(m, ULCON: %08x\n, readl(port-membase ULCON)); seq_printf(m, UCON: %08x\n, readl(port-membase UCON)); return 0; }5. 实战调试技巧用示波器抓取波形是最直接的调试手段。有一次发现数据错误用示波器发现是波特率偏差超过3%。调整UBRDIV后问题解决// 重新计算分频值 div (clk / (16 * baud)) - 1; frac ((clk * 1000 / (16 * baud)) - (div * 1000)) * 16 / 1000; writel(div, port-membase UBRDIV); writel(frac, port-membase UFRACVAL);内核提供的ftrace工具可以跟踪函数调用echo 1 /sys/kernel/debug/tracing/events/uart/enable cat /sys/kernel/debug/tracing/trace_pipe遇到中断不触发时检查GIC配置很重要cat /proc/interrupts | grep uart6. 性能优化方案启用DMA传输能显著降低CPU占用。配置步骤包括在设备树声明DMA通道实现dmaengine回调处理DMA完成中断我优化后的DMA初始化代码port-dmatx.chan dma_request_chan(port-dev, tx); if (IS_ERR(port-dmatx.chan)) { dev_warn(port-dev, DMA tx request failed\n); return PTR_ERR(port-dmatx.chan); } dmaengine_slave_config(port-dmatx.chan, slave_config);环形缓冲区设计对吞吐量影响很大。我通常使用2的幂次方大小方便用位运算优化#define BUF_SIZE 2048 struct exynos_uart_buf { unsigned char buf[BUF_SIZE]; unsigned int head; unsigned int tail; }; static inline unsigned int buf_used(struct exynos_uart_buf *b) { return (b-head - b-tail) (BUF_SIZE - 1); }7. 常见问题排查电气问题是最隐蔽的故障。有一次遇到随机数据错误最终发现是未接上拉电阻。现在我的检查清单包括测量TX/RX电压是否在3.3V检查地线连接阻抗确认流控信号状态时钟配置错误会导致波特率不准。我写的时钟检查脚本#!/bin/bash for clk in $(find /sys/kernel/debug/clk -type d); do echo $clk: $(cat $clk/clk_rate) done | grep uart驱动加载失败时按这个顺序排查dmesg看内核日志检查设备树节点status验证资源冲突ioremap区域等测试probe函数是否执行记得那次调试三天的问题最后发现是设备树里的compatible字符串拼写错误。现在我都用这个命令验证of_node_check() { echo Checking node $1 ls /proc/device-tree/$1 2/dev/null || echo Node not found! }

更多文章