4. Linux下UART驱动框架:tty子系统架构、uart_driver注册流程、串口设备节点创建
好,我们进入第四章。这一章我打算把Linux下UART驱动的骨架给你拆开看看。很多初学者一上来就翻内核源码,结果被tty层、线路规程、uart_ops这些概念绕晕。其实没那么复杂——你只要抓住一条主线:应用层读写串口,最终是怎么落到硬件寄存器上的。
我个人习惯把UART驱动分成三层来看:tty核心层、线路规程层、uart驱动层。这三层各司其职,咱们一个一个说。
4.1 tty子系统架构:三层模型
先看一张整体架构图,我画了张SVG帮你理清脉络。
这张图我建议你多看两眼。数据从应用层往下走,经过tty核心层、线路规程层,最后到uart驱动层操作硬件。反过来,硬件收到数据后,中断一路往上送,最终让应用层的read()返回。
4.2 各层职责速览
| 层级 | 核心文件/结构 | 主要职责 |
|---|---|---|
| tty核心层 | drivers/tty/tty_io.c struct tty_driver |
设备节点管理、open/close、read/write系统调用分发 |
| 线路规程层 | drivers/tty/n_tty.c struct tty_ldisc_ops |
数据缓冲、行规则、信号处理(如Ctrl+C) |
| uart驱动层 | drivers/tty/serial/serial_core.c struct uart_driver |
波特率配置、中断处理、寄存器读写、DMA管理 |
说白了,tty核心层管的是“文件操作”,线路规程层管的是“数据怎么加工”,uart驱动层管的是“怎么跟硬件对话”。
4.3 uart_driver注册流程:从模块加载到设备就绪
接下来我们看看一个UART驱动是怎么注册进内核的。我以最常用的8250驱动为例,把关键步骤拆开讲。
4.3.1 第一步:定义并注册uart_driver
每个UART驱动首先要定义一个struct uart_driver,然后调用uart_register_driver()把它注册到tty核心层。这个结构体里最重要的几个字段:
static struct uart_driver serial8250_reg = {
.owner = THIS_MODULE,
.driver_name = "serial",
.dev_name = "ttyS",
.major = TTY_MAJOR,
.minor = 64,
.nr = UART_NR, // 支持的最大端口数
.cons = SERIAL8250_CONSOLE,
};
这里有个细节:dev_name是"ttyS",所以设备节点就是/dev/ttyS0、/dev/ttyS1这样。如果你写的是USB转串口驱动,dev_name可能是"ttyUSB",节点就是/dev/ttyUSB0。
4.3.2 第二步:为每个端口分配并初始化uart_port
注册完uart_driver之后,每个物理串口对应一个struct uart_port。这个结构体里存放了端口号、I/O基地址、中断号、波特率、FIFO大小等信息。
struct uart_port port;
port.line = 0; // 对应ttyS0
port.iotype = UPIO_MEM; // 内存映射I/O
port.mapbase = 0x3F8; // 物理基地址(经典COM1)
port.irq = 4; // 中断号
port.ops = &serial8250_pops; // 操作函数集
port.fifosize = 16; // 16550A FIFO深度
嗯,这里要注意:uart_port的初始化必须在探测函数(probe)里完成,而且每个端口都要单独做。我见过有人图省事,多个端口共用一个uart_port结构体,结果中断上下文里数据全乱套了。
4.3.3 第三步:添加端口到驱动
初始化好uart_port后,调用uart_add_one_port()把它挂到之前注册的uart_driver上。这一步会触发tty核心层创建设备节点。
ret = uart_add_one_port(&serial8250_reg, &port);
if (ret)
dev_err(dev, "failed to add port\n");
- module_init → uart_register_driver() → 注册到tty核心
- probe → uart_port初始化 → uart_add_one_port() → 创建设备节点
- 设备节点出现后,应用层就可以open/read/write了
4.4 串口设备节点创建:udev与devtmpfs的配合
设备节点是怎么出现在/dev/ttyS0的?这背后是内核的设备模型在起作用。
当你调用uart_add_one_port()时,内核内部会做两件事:
- 在sysfs中创建对应的设备目录,比如
/sys/devices/platform/serial8250/tty/ttyS0/ - 通过devtmpfs自动在
/dev/下创建设备节点
如果你用的是udev(现在大部分发行版都用systemd-udevd),它还会根据规则设置设备节点的权限、属组、符号链接。比如:
# udev规则示例:/etc/udev/rules.d/99-serial.rules
KERNEL=="ttyS0", GROUP="dialout", MODE="0660"
KERNEL=="ttyUSB*", GROUP="dialout", MODE="0666"
我曾经被这个问题坑过:板子启动后/dev/ttyS0确实存在,但普通用户打不开,报"Permission denied"。查了半天,就是udev规则没配,默认权限是600,只有root能用。加上MODE="0666"就好了。
mknod /dev/ttyS0 c 4 64
4.5 uart_ops:驱动与硬件的接口契约
最后,我们看看uart驱动层最核心的东西——struct uart_ops。这个结构体定义了驱动必须实现的回调函数,tty核心层通过它来操作硬件。
| 回调函数 | 调用时机 | 典型实现 |
|---|---|---|
| startup | open()时调用 | 申请中断、复位FIFO、使能收发 |
| shutdown | close()时调用 | 释放中断、关闭收发、清FIFO |
| set_termios | tcsetattr()时调用 | 配置波特率、数据位、校验位、停止位 |
| start_tx | 有数据要发送时调用 | 使能发送中断、写入发送FIFO |
| stop_tx | 发送缓冲区空时调用 | 关闭发送中断 |
| tx_empty | 查询发送是否完成 | 读取LSR寄存器,检查THR/TEMT位 |
| get_mctrl | 查询modem信号状态 | 读取MSR寄存器,返回CTS/DSR等状态 |
| set_mctrl | 设置modem信号 | 写MCR寄存器,控制RTS/DTR |
这里面set_termios是最容易出错的。波特率计算、分频系数设置、数据位/校验位/停止位的寄存器配置,全在这里。我建议你实现这个函数时,先把芯片手册的寄存器定义打印出来,对着写,别凭记忆。
static void serial8250_set_termios(struct uart_port *port,
struct ktermios *termios,
struct ktermios *old)
{
unsigned int baud, quot;
// 1. 从termios中提取波特率
baud = uart_get_baud_rate(port, termios, old, 0, 115200);
// 2. 计算分频系数
quot = uart_get_divisor(port, baud);
// 3. 写寄存器:设置波特率、数据格式
serial_out(port, UART_LCR, UART_LCR_DLAB); // 使能分频锁存
serial_out(port, UART_DLL, quot & 0xff);
serial_out(port, UART_DLM, (quot >> 8) & 0xff);
serial_out(port, UART_LCR, lcr_value); // 设置数据格式
// 4. 更新硬件FIFO触发阈值
serial8250_update_fifos(port);
}
4.6 本章小结
这一章我们把Linux UART驱动的骨架过了一遍。记住三个核心点:
- tty子系统三层架构:tty核心层管文件操作,线路规程层管数据加工,uart驱动层管硬件交互
- uart_driver注册流程:先注册驱动,再初始化端口,最后添加端口触发设备节点创建
- uart_ops回调函数:startup/shutdown/set_termios/start_tx是必须实现的四个核心函数