4. UART驱动实战:基于Linux内核的UART驱动开发
好,咱们进入实战环节。前面几章把UART协议和硬件时序讲透了,这一章咱们直接撸代码——在Linux内核里写一个UART驱动。我个人习惯是从设备树开始,再到ops实现,最后搞定中断。这个顺序其实也是驱动加载的流程,你跟着走一遍就全通了。
4.1 设备树:告诉内核你的UART长什么样
设备树(Device Tree)说白了就是硬件的“身份证”。内核启动时通过它知道:哪个地址是UART寄存器、用哪条中断线、时钟频率多少。没有设备树,驱动就是瞎子。
我在项目中遇到过好几次设备树写错导致驱动加载失败的情况。最坑的一次是reg属性少写了一个地址单元,结果内核把控制寄存器和数据寄存器映射到了同一个位置,收发全乱套。
下面是一个典型的UART设备树节点:
uart0: serial@10000000 {
compatible = "mycompany,my-uart";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
clocks = <&uart_clk>;
clock-names = "uart_clk";
fifo-size = <16>;
current-speed = <115200>;
status = "okay";
};
| 属性 | 含义 | 避坑点 |
|---|---|---|
compatible |
驱动匹配字符串 | 必须与驱动中of_match_table一致 |
reg |
寄存器基地址和范围 | 地址和长度要成对出现 |
interrupts |
中断号、类型、触发方式 | 不同SoC的编码方式不同 |
clocks |
时钟源句柄 | 别忘了配clock-names |
fifo-size |
硬件FIFO深度 | 自定义属性,驱动里要解析 |
我曾经踩过的坑:interrupts属性里第三个数字是触发方式,ARM GIC用4表示高电平触发,但有些文档写的是3。一定要查你的SoC手册,别想当然。
4.2 ops实现:驱动与内核的契约
Linux串口驱动核心是struct uart_ops。这个结构体定义了十几个回调函数,但真正必须实现的其实就几个。我一般先实现这五个:
- .startup —— 打开串口时调用,初始化硬件、申请中断
- .shutdown —— 关闭串口时调用,释放中断、复位硬件
- .set_termios —— 设置波特率、数据位、校验位等
- .start_tx —— 启动发送,通常使能发送中断
- .stop_tx —— 停止发送,关闭发送中断
来看一个简化的startup实现:
static int my_uart_startup(struct uart_port *port)
{
struct my_uart_data *data = container_of(port, struct my_uart_data, port);
int ret;
// 使能UART时钟
clk_prepare_enable(data->clk);
// 复位UART控制器
writel(0x01, port->membase + UART_CR);
// 配置FIFO触发阈值
writel(0x07, port->membase + UART_FCR);
// 申请中断
ret = request_irq(port->irq, my_uart_isr, IRQF_TRIGGER_HIGH,
"my_uart", port);
if (ret) {
dev_err(port->dev, "failed to request irq %d\n", port->irq);
clk_disable_unprepare(data->clk);
return ret;
}
return 0;
}
小技巧:set_termios里计算波特率时,我习惯用
uart_get_baud_rate()这个内核辅助函数。它会自动处理边界情况,比你自己算除数靠谱得多。
4.3 中断处理:让CPU不再傻等
轮询方式太浪费CPU了。真正产品里必须用中断。UART中断一般分三种:
- 接收中断 —— 有数据来了,赶紧读走
- 发送中断 —— FIFO空了,可以继续塞数据
- 错误中断 —— 帧错误、溢出、校验错等
中断处理函数里,第一步是读中断状态寄存器,判断是哪种中断。我见过新手直接一股脑全处理,结果中断嵌套把自己搞死。正确的做法是:
static irqreturn_t my_uart_isr(int irq, void *dev_id)
{
struct uart_port *port = dev_id;
unsigned int iir;
// 读中断标识寄存器
iir = readl(port->membase + UART_IIR);
// 按优先级处理
if (iir & IIR_RX_TIMEOUT || iir & IIR_RX_AVAIL) {
// 接收:读到FIFO空为止
while (readl(port->membase + UART_LSR) & LSR_RX_READY) {
unsigned char ch = readl(port->membase + UART_RBR);
uart_insert_char(port, ch);
}
// 通知上层
tty_flip_buffer_push(&port->state->port);
}
if (iir & IIR_TX_EMPTY) {
// 发送:从环形缓冲区取数据写FIFO
my_uart_tx_chars(port);
}
if (iir & IIR_RLS) {
// 错误处理:读LSR清除状态
unsigned int lsr = readl(port->membase + UART_LSR);
// 记录错误统计
if (lsr & LSR_OE) port->icount.overrun++;
if (lsr & LSR_FE) port->icount.frame++;
}
return IRQ_HANDLED;
}
核心原则:中断处理函数要快进快出。接收数据用uart_insert_char放到缓冲区就行,别在里面做复杂处理。真正的协议解析交给workqueue或tasklet。
4.4 知识结构总览
下面这张图把UART驱动的三个核心模块串起来了。你写驱动时脑子里要有这个框架:
4.5 注册与匹配:让内核找到你的驱动
驱动写好了,怎么让内核加载它?靠platform_driver结构体。这里有个关键点:of_match_table必须和设备树的compatible属性完全一致,大小写都不能错。
static const struct of_device_id my_uart_of_match[] = {
{ .compatible = "mycompany,my-uart" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_uart_of_match);
static struct platform_driver my_uart_driver = {
.probe = my_uart_probe,
.remove = my_uart_remove,
.driver = {
.name = "my_uart",
.of_match_table = my_uart_of_match,
},
};
module_platform_driver(my_uart_driver);
我个人的调试习惯:在probe函数里加一句
dev_info(dev, "UART probe success, irq=%d\n", port->irq);。内核启动时看dmesg输出,能快速确认驱动是否加载成功。如果没看到这条日志,八成是设备树匹配出了问题。
4.6 避坑指南:那些年我踩过的UART驱动坑
- 中断号搞错 —— 设备树里写的中断号跟硬件手册对不上,驱动申请中断失败。解决办法:查SoC的interrupt-map,别信二手资料。
- 波特率不准 —— 时钟源频率算错了,导致实际波特率偏差超过3%。我遇到过9600波特率实际只有9200,传一帧错一帧。用示波器量一下最靠谱。
- FIFO溢出 —— 中断处理太慢,数据来不及读就被新数据覆盖了。解决办法:把中断优先级调高,或者在中断里只做快速拷贝,复杂处理挪到下半部。
- 流控没配 —— 硬件支持硬件流控,但驱动里没使能RTS/CTS。结果高速传输时丢数据丢到怀疑人生。