2. UART驱动架构:Linux下串口驱动框架、tty子系统、核心数据结构

好,咱们进入第二章。这一章我打算把Linux下UART驱动的骨架给你拆开看看。很多工程师调了几年串口,遇到个波特率不准、丢数据的问题,第一反应就是改设备树、调应用层参数——其实根子往往在驱动框架里。

我个人习惯,学任何驱动,先看它挂在哪个子系统下面。UART在Linux里属于tty子系统。tty是什么?说白了就是“终端”。你想想看,从早期的电传打字机,到现在的串口终端、虚拟控制台、SSH会话,Linux统统把它们抽象成tty设备。UART只是tty家族里最古老、最基础的一员。

2.1 tty子系统架构概览

tty子系统分三层,我画个图你一看就明白:

Linux tty 子系统三层架构 用户空间 应用程序(open/read/write/ioctl) tty核心层(tty_io.c / n_tty.c) 线路规程(Line Discipline) 数据缓冲、流量控制、行编辑、回显 tty驱动层(串口驱动) uart_ops:.startup / .shutdown / .set_termios / .start_tx uart_port:寄存器操作、中断处理、DMA配置 硬件层(UART控制器 / 物理串口) 数据流向:用户空间 → tty核心 → 线路规程 → tty驱动 → 硬件

这张图你得记住。以后遇到任何串口问题,先定位问题出在哪一层:

  • 用户空间:应用程序调用read/write,或者用termios调参数。这一层出问题,通常是波特率设错、数据格式不匹配。
  • tty核心层:线路规程(Line Discipline)在这里干活。它负责数据缓冲、流量控制、行编辑。默认的n_tty规程会把回车换行、Ctrl+C这些特殊字符处理掉——嗯,这里要注意,如果你传的是二进制数据,得关掉这些处理。
  • tty驱动层:串口驱动真正和硬件打交道的地方。我们调优、加DMA,主要改的就是这一层。
我的经验: 有一次客户说串口收数据总丢第一个字节。我查了半天,发现是线路规程的缓冲区在驱动还没准备好时就把数据丢了。最后在tty驱动层的startup回调里加了接收缓冲区预分配,问题解决。所以别小看这三层之间的交互时序。

2.2 核心数据结构

驱动代码里,几个关键结构体你得烂熟于心。我列个表,你对照着看:

结构体 所在文件 核心作用 关键字段(我常用的)
struct tty_driver tty_driver.h 描述一个tty设备驱动,注册到tty核心 ops(操作函数表)、type、subtype
struct tty_struct tty.h 每个打开的tty设备对应一个实例 driver_data(指向uart_state)、ldisc(线路规程)
struct uart_driver serial_core.h 串口驱动的顶层封装,管理多个uart_port dev_name、nr(端口数)、cons(控制台)
struct uart_port serial_core.h 每个物理串口的抽象,包含硬件资源 membase(寄存器基址)、irq、uartclk、ops
struct uart_ops serial_core.h 串口硬件操作函数表 startup、shutdown、set_termios、start_tx、stop_tx
struct circ_buf serial_core.h 发送环形缓冲区,驱动和tty核心共享 buf、head、tail

这里我重点说两个:uart_port 和 uart_ops。你写驱动或者调优,90%的时间都在跟这两个结构体打交道。

2.3 uart_port:串口的“身份证”

struct uart_port 是每个物理串口的完整描述。我调驱动时,第一件事就是看这个结构体里的参数配没配对:

struct uart_port {
    unsigned long   iobase;          /* IO端口基地址 */
    unsigned char   __iomem *membase; /* 内存映射基地址 */
    unsigned int    irq;             /* 中断号 */
    unsigned int    uartclk;         /* UART输入时钟频率(Hz) */
    unsigned char   fifosize;        /* 硬件FIFO深度 */
    unsigned char   x_char;          /* 待发送的XON/XOFF字符 */
    unsigned int    flags;           /* UPF_* 标志位 */
    struct uart_ops *ops;            /* 硬件操作函数表 */
    struct circ_buf  state->xmit;    /* 发送环形缓冲区 */
    /* ... 还有不少字段,但上面这些是核心 */
};

举个例子,uartclk 这个字段特别容易配错。我遇到过一块板子,芯片手册说UART时钟是24MHz,结果实际量出来是26MHz。驱动里按24MHz算波特率,115200怎么都配不准。后来我在 set_termios 回调里加了调试打印,才发现时钟源不对。

避坑指南: 我曾经在移植驱动时,忘了设置 uart_port->fifosize,结果驱动默认FIFO深度为1。中断频繁触发,CPU占用率直接飙到80%。后来加上 port->fifosize = 16;,世界清净了。所以FIFO深度一定要和硬件匹配。

2.4 uart_ops:驱动的心脏

struct uart_ops 定义了串口驱动必须实现的回调函数。我把它叫做“驱动的心脏”——没有这些函数,串口就是个摆设。

struct uart_ops {
    unsigned int (*tx_empty)(struct uart_port *);
    void        (*set_mctrl)(struct uart_port *, unsigned int mctrl);
    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 ctl);
    int         (*startup)(struct uart_port *);
    void        (*shutdown)(struct uart_port *);
    void        (*set_termios)(struct uart_port *, struct ktermios *new,
                               struct ktermios *old);
    void        (*set_ldisc)(struct uart_port *, struct ktermios *);
    void        (*pm)(struct uart_port *, unsigned int state,
                      unsigned int oldstate);
    int         (*poll_init)(struct uart_port *);
    void        (*poll_put_char)(struct uart_port *, unsigned char);
    int         (*poll_get_char)(struct uart_port *);
    /* 新内核还有DMA相关回调 */
};

这几个回调的执行顺序,我建议你记一下:

  1. startup:打开串口时调用。申请中断、复位FIFO、使能接收。我一般在这里初始化DMA描述符。
  2. set_termios:应用程序调用tcsetattr时触发。设置波特率、数据位、停止位、校验。这里要重新计算波特率分频系数。
  3. start_tx / stop_tx:tty核心要发数据时,调用start_tx。驱动从环形缓冲区取数据,写入发送FIFO。如果FIFO满了,等中断再继续。
  4. shutdown:关闭串口时调用。释放中断、清空FIFO、停止DMA。
调优关键点: start_tx 和中断处理函数之间的配合,是UART性能的瓶颈。如果每次只发一个字节就等中断,效率极低。我习惯在start_tx里尽量填满FIFO,减少中断次数。后面讲DMA时,这个思路会进一步延伸。

2.5 数据流全景:从write到硬件

咱们把整个数据流串起来走一遍。假设应用程序调用 write(fd, buf, 100):

  1. tty核心层:收到write请求,把数据拷贝到tty_struct的缓冲区。
  2. 线路规程:n_tty对数据做处理(比如把换行符转成回车换行)。如果关掉了ICANON和ECHO,这一步基本就是透传。
  3. tty驱动层:线路规程调用驱动层的 write 回调,数据被放入 uart_port 的环形缓冲区 circ_buf。
  4. start_tx:驱动检查环形缓冲区有数据,调用 start_tx。驱动从环形缓冲区取出数据,写入UART硬件的发送FIFO。
  5. 硬件发送:UART控制器逐位将数据从TX引脚发出。发送完一个字节或FIFO空时,触发发送中断。
  6. 中断处理:中断服务程序检查环形缓冲区是否还有数据。有则继续填FIFO,没有则调用 stop_tx 关闭发送中断。

接收路径反过来,但多了个接收超时中断的处理。嗯,这里要注意,很多丢数据的问题就出在接收中断处理不及时上。

我的调试习惯: 在 start_tx 和中断处理函数里加trace_printk,打印环形缓冲区的head/tail指针。这样能直观看到数据是否被及时消费。如果head追上了tail,说明发送太慢;如果tail一直不动,说明中断没触发。

2.6 驱动注册与初始化流程

最后,咱们看看驱动怎么注册到系统里。流程不复杂,但顺序错了就会出问题:

/* 伪代码,展示核心流程 */
static struct uart_driver my_uart_drv = {
    .owner          = THIS_MODULE,
    .driver_name    = "my_uart",
    .dev_name       = "ttyMY",
    .nr             = 4,        /* 4个串口 */
};

static int __init my_uart_init(void)
{
    int ret;

    /* 1. 注册uart_driver到tty核心 */
    ret = uart_register_driver(&my_uart_drv);
    if (ret)
        return ret;

    /* 2. 为每个端口添加uart_port */
    for (i = 0; i < 4; i++) {
        my_uart_ports[i].uartclk = 24000000;
        my_uart_ports[i].ops = &my_uart_ops;
        /* ... 其他初始化 ... */
        uart_add_one_port(&my_uart_drv, &my_uart_ports[i]);
    }

    return 0;
}

这里有个坑:uart_register_driver 必须在 uart_add_one_port 之前调用。我曾经把顺序搞反了,结果端口注册失败,dmesg里报“tty core not ready”。

好了,这一章的内容就是这些。tty子系统的三层架构、核心数据结构、数据流走向、驱动注册流程——这些东西是后面调优和加DMA的基础。你先把这些结构体在代码里找到,对照着看一遍,心里就有底了。