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中断一般分三种:

  1. 接收中断 —— 有数据来了,赶紧读走
  2. 发送中断 —— FIFO空了,可以继续塞数据
  3. 错误中断 —— 帧错误、溢出、校验错等

中断处理函数里,第一步是读中断状态寄存器,判断是哪种中断。我见过新手直接一股脑全处理,结果中断嵌套把自己搞死。正确的做法是:

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驱动的三个核心模块串起来了。你写驱动时脑子里要有这个框架:

UART驱动核心架构 设备树节点 compatible / reg / interrupts uart_ops 回调 startup / shutdown / set_termios 中断处理 RX / TX / Error 处理 时钟管理 寄存器读写 FIFO 控制 DMA 传输 数据流路径 硬件FIFO → 中断处理 → uart_insert_char → tty_flip_buffer → 用户空间 用户空间 → uart_write → 环形缓冲区 → start_tx → 中断发送 → 硬件FIFO 💡 调试建议:先验证设备树节点能被驱动匹配到 再测试寄存器读写,最后才上中断。分步验证,别一口吃成胖子

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。结果高速传输时丢数据丢到怀疑人生。