4. UART驱动实战:串口硬件原理、tty驱动框架、串口设备树配置
串口,UART,搞嵌入式的人手必备。说它简单吧,确实就两根线来回倒腾;说它复杂吧,tty 那一套框架能把人绕晕。今天咱们就把这三块掰开揉碎——硬件原理、驱动框架、设备树配置,一个一个来。
4.1 串口硬件原理:就两根线的事?
UART 全称是 Universal Asynchronous Receiver/Transmitter。异步,意味着没有时钟线。收发双方得事先约定好波特率,不然数据就乱套了。
硬件上就三根线:TX(发送)、RX(接收)、GND(地)。全双工,能同时发和收。我见过不少新手把 TX 和 RX 接反,然后在那调半天波特率——嗯,我也干过这事。
关键参数:
- 波特率:每秒传输的符号数,常见 9600、115200、921600
- 数据位:5~8 位,通常 8 位
- 校验位:无校验、奇校验、偶校验
- 停止位:1 位或 2 位
数据帧长这样:起始位(0)→ 数据位(LSB 先发)→ 校验位(可选)→ 停止位(1)。
举个例子,发送字符 'A'(0x41),波特率 115200,8N1:
起始位 | 1 0 0 0 0 0 1 0 | 停止位
LSB ────────────────── MSB
硬件上,UART 外设内部有移位寄存器和 FIFO。FIFO 深度一般是 16 字节或 64 字节。数据来了先存 FIFO,CPU 有空了再读。这样能减少中断频率,提升效率。
我的习惯:调试阶段把 FIFO 关掉,用单字节中断模式。这样每个字节都能抓到,排查问题方便。量产再开 FIFO 提升吞吐。
4.2 tty 驱动框架:Linux 串口驱动的灵魂
Linux 里,串口驱动属于 tty 子系统。tty 是 Teletype 的缩写,历史遗留名字。说白了,tty 就是一套字符设备框架,专门处理终端类设备。
整个框架分三层:
| 层级 | 职责 | 关键结构体 |
|---|---|---|
| tty 核心层 | 提供统一字符设备接口,管理线路规程 | struct tty_driver, struct tty_port |
| 线路规程层 | 处理数据格式、流控、回显等 | struct tty_ldisc_ops |
| 硬件驱动层 | 直接操作 UART 寄存器,收发数据 | struct uart_ops, struct uart_port |
用户空间看到的是 /dev/ttyS0、/dev/ttyAMA0 这类设备文件。应用层调用 open/read/write,最终会落到硬件驱动层的回调函数里。
我个人觉得,理解 tty 框架的关键是搞清楚 数据流:
- 应用层 write → tty 核心层 → 线路规程层(N_TTY) → 硬件驱动层 → UART 寄存器 → TX 引脚
- RX 引脚收到数据 → 硬件中断 → 驱动层读寄存器 → 推入 tty 缓冲区 → 线路规程层处理 → 应用层 read
我曾经踩过的坑:线路规程层默认是 N_TTY,它会把收到的数据做回显处理。如果你写一个自定义协议,不想让数据回显,得用 ioctl 把线路规程改成 N_RAW 或者自己注册一个。不然调试时发现发什么回什么,还以为硬件有问题。
硬件驱动层最核心的结构体是 struct uart_ops,里面全是函数指针:
struct uart_ops {
unsigned int (*tx_empty)(struct uart_port *);
void (*set_mctrl)(struct uart_port *, unsigned int);
unsigned int (*get_mctrl)(struct uart_port *);
void (*stop_tx)(struct uart_port *);
void (*start_tx)(struct uart_port *);
void (*stop_rx)(struct uart_port *);
void (*enable_ms)(struct uart_port *);
void (*break_ctl)(struct uart_port *, int);
int (*startup)(struct uart_port *);
void (*shutdown)(struct uart_port *);
void (*set_termios)(struct uart_port *, struct ktermios *, struct ktermios *);
void (*pm)(struct uart_port *, unsigned int, unsigned int);
const char *(*type)(struct uart_port *);
void (*release_port)(struct uart_port *);
int (*request_port)(struct uart_port *);
void (*config_port)(struct uart_port *, int);
int (*verify_port)(struct uart_port *, struct serial_struct *);
void (*poll_put_char)(struct uart_port *, unsigned char);
int (*poll_get_char)(struct uart_port *);
};
写驱动时,你主要实现 startup(初始化硬件)、shutdown(关闭硬件)、start_tx(启动发送)、set_termios(设置波特率等参数)。
我的建议:先实现 poll_put_char 和 poll_get_char,这两个函数用于早期调试。内核还没完全启动时,串口控制台就靠它们输出信息。你想想看,如果这两个函数没写好,连内核启动 log 都看不到,后面怎么调?
4.3 串口设备树配置:让内核认识你的硬件
设备树是描述硬件信息的。串口设备树节点告诉内核:UART 外设在哪个地址、用哪个中断、时钟频率多少、引脚怎么配置。
一个典型的串口设备树节点:
&uart1 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = &uart1_pins;
clocks = <&clk_uart1>;
clock-names = "uartclk";
current-speed = <115200>;
dmas = <&dma 0 0x40000000>, <&dma 1 0x40000000>;
dma-names = "tx", "rx";
};
关键属性说明:
| 属性 | 含义 | 说明 |
|---|---|---|
| status | 设备状态 | "okay" 启用,"disabled" 禁用 |
| pinctrl-0 | 引脚复用配置 | 指向 pinctrl 子系统的节点 |
| clocks | 时钟源 | UART 模块的工作时钟 |
| current-speed | 默认波特率 | 驱动初始化时设置 |
| dmas | DMA 通道 | 可选,用于 DMA 模式收发 |
引脚配置通常在 pinctrl 节点里定义:
uart1_pins: uart1-pins {
mux {
function = "uart1";
groups = "uart1_tx", "uart1_rx";
};
conf {
pins = "uart1_tx", "uart1_rx";
drive-strength = <2>;
bias-pull-up;
};
};
驱动里通过 of_property_read_u32 等 API 读取设备树属性。比如读 current-speed:
struct device_node *np = pdev->dev.of_node;
u32 baud;
if (of_property_read_u32(np, "current-speed", &baud))
baud = 115200; // 默认值
我曾经踩过的坑:设备树里配了 DMA,但 DMA 驱动没加载。结果串口驱动 probe 时申请 DMA 通道失败,直接返回 -EPROBE_DEFER。内核会反复重试 probe,但 DMA 驱动一直没加载,系统就卡在这了。解决办法:要么确保 DMA 驱动先加载,要么在设备树里先去掉 dmas 属性,等 DMA 调通了再加。
还有一个容易忽略的点:时钟频率。UART 的波特率发生器需要知道输入时钟频率,才能算出分频系数。如果设备树里 clocks 配错了,波特率就会不准。我遇到过客户反馈串口丢数据,最后发现是时钟频率差了 3%,累积到长数据帧就出错了。
4.4 整体框架图
下面这张图把串口驱动从硬件到应用层的完整路径画出来了,你一看就明白:
从这张图能清楚看到:应用层通过设备文件访问,经过 tty 核心层和线路规程层,最终落到硬件驱动层操作寄存器。设备树和引脚配置在底层提供硬件描述信息。
4.5 实战要点总结
- 硬件层面:确认 TX/RX 没接反,波特率一致,GND 共地。这是最基础的,也是最容易出问题的。
- 驱动层面:先实现 poll 模式保证能输出,再实现中断模式,最后考虑 DMA。一步一步来,别想一口吃成胖子。
- 设备树层面:时钟频率必须准确,引脚复用配置要跟原理图一致。我习惯在设备树里加
current-speed属性,这样驱动初始化时就能设好波特率,不用等应用层来配置。
调试小技巧:用逻辑分析仪抓 TX/RX 引脚波形,比用示波器方便。设置好波特率,看起始位和停止位对不对,数据位是不是反了(LSB/MSB 顺序)。一眼就能看出问题。
串口驱动看起来简单,但 tty 框架的复杂度不低。把硬件原理搞明白,把框架层次理清楚,把设备树配准确,剩下的就是水到渠成的事。