2. UART驱动架构:Linux下串口驱动框架、tty子系统、核心数据结构
好,咱们进入第二章。这一章我打算把Linux下UART驱动的骨架给你拆开看看。很多工程师调了几年串口,遇到个波特率不准、丢数据的问题,第一反应就是改设备树、调应用层参数——其实根子往往在驱动框架里。
我个人习惯,学任何驱动,先看它挂在哪个子系统下面。UART在Linux里属于tty子系统。tty是什么?说白了就是“终端”。你想想看,从早期的电传打字机,到现在的串口终端、虚拟控制台、SSH会话,Linux统统把它们抽象成tty设备。UART只是tty家族里最古老、最基础的一员。
2.1 tty子系统架构概览
tty子系统分三层,我画个图你一看就明白:
这张图你得记住。以后遇到任何串口问题,先定位问题出在哪一层:
- 用户空间:应用程序调用read/write,或者用termios调参数。这一层出问题,通常是波特率设错、数据格式不匹配。
- tty核心层:线路规程(Line Discipline)在这里干活。它负责数据缓冲、流量控制、行编辑。默认的n_tty规程会把回车换行、Ctrl+C这些特殊字符处理掉——嗯,这里要注意,如果你传的是二进制数据,得关掉这些处理。
- tty驱动层:串口驱动真正和硬件打交道的地方。我们调优、加DMA,主要改的就是这一层。
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相关回调 */
};
这几个回调的执行顺序,我建议你记一下:
- startup:打开串口时调用。申请中断、复位FIFO、使能接收。我一般在这里初始化DMA描述符。
- set_termios:应用程序调用tcsetattr时触发。设置波特率、数据位、停止位、校验。这里要重新计算波特率分频系数。
- start_tx / stop_tx:tty核心要发数据时,调用start_tx。驱动从环形缓冲区取数据,写入发送FIFO。如果FIFO满了,等中断再继续。
- shutdown:关闭串口时调用。释放中断、清空FIFO、停止DMA。
2.5 数据流全景:从write到硬件
咱们把整个数据流串起来走一遍。假设应用程序调用 write(fd, buf, 100):
- tty核心层:收到write请求,把数据拷贝到tty_struct的缓冲区。
- 线路规程:n_tty对数据做处理(比如把换行符转成回车换行)。如果关掉了ICANON和ECHO,这一步基本就是透传。
- tty驱动层:线路规程调用驱动层的
write回调,数据被放入uart_port的环形缓冲区circ_buf。 - start_tx:驱动检查环形缓冲区有数据,调用
start_tx。驱动从环形缓冲区取出数据,写入UART硬件的发送FIFO。 - 硬件发送:UART控制器逐位将数据从TX引脚发出。发送完一个字节或FIFO空时,触发发送中断。
- 中断处理:中断服务程序检查环形缓冲区是否还有数据。有则继续填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的基础。你先把这些结构体在代码里找到,对照着看一遍,心里就有底了。