2、Linux串口子系统框架:TTY子系统架构、串口驱动分层(UART/线路规程/TTY)、核心数据结构(uart_port/uart_ops)
好,咱们今天来聊聊Linux串口子系统的框架。说实话,我刚接触这块的时候,也被那一堆抽象层搞得有点懵。TTY、线路规程、UART驱动……这些到底是怎么串起来的?
别急,我带你一层层剥开它。你想想看,串口驱动其实没那么神秘,它就是一个典型的分层架构。每一层各司其职,上层不用管底层怎么发数据,底层也不用关心上层怎么处理数据。
2.1 TTY子系统架构:三层的“流水线”
Linux的串口子系统,说白了就是一条三层流水线:
- TTY层:最上面,面向用户空间。你用的
open()、read()、write(),最终都会落到这一层。 - 线路规程(Line Discipline):中间层,负责数据加工。比如你按了回车,它要不要帮你转成换行?数据要不要回显?这些都是线路规程干的活。
- UART层:最底层,直接跟硬件打交道。它负责往寄存器里写数据,或者从FIFO里读数据。
我习惯把这层关系想象成一个“数据管道”。用户写数据,经过TTY层、线路规程,最后到UART层发出去。反过来,硬件收到数据,也是从UART层往上冒,经过线路规程处理,再到TTY层,最后交给用户程序。
核心要点:这三层之间通过函数指针和回调机制通信。上层调用下层的接口,下层通过回调通知上层“数据来了”。
嗯,这里要注意:TTY层和线路规程是内核通用的,跟具体硬件无关。只有最底层的UART驱动,才是我们移植时需要动手改的地方。
2.2 串口驱动分层:UART / 线路规程 / TTY
咱们再细看一下每一层的职责。
2.2.1 TTY层
TTY层是用户空间的“门面”。它实现了字符设备驱动的那一套:file_operations。你打开/dev/ttyS0,实际上就是在操作TTY层。它负责维护一个环形缓冲区,用户读写的数据都在这里暂存。
2.2.2 线路规程
线路规程是Linux串口设计里很巧妙的一环。它默认的线路规程是N_TTY,负责处理终端协议。比如:
- 回车换行转换(
ONLCR、INLCR等) - 数据回显(
ECHO) - 信号生成(比如Ctrl+C发送SIGINT)
我在项目中遇到过一种情况:用串口跟一个老式设备通信,对方只认\r\n,但Linux默认只发\n。折腾了半天,最后发现是线路规程的配置问题。你想想看,如果不懂这一层,光在应用层加\r,那多别扭。
2.2.3 UART层
UART层就是咱们驱动工程师的主战场了。它直接操作硬件寄存器,负责:
- 波特率设置
- 数据位、停止位、校验位配置
- 中断处理(收数据、发数据、错误处理)
- DMA传输(如果硬件支持)
我的经验:移植串口驱动时,90%的精力都花在UART层。TTY层和线路规程基本不用动,除非你要实现特殊的协议(比如蓝牙HCI、PPP等)。
2.3 核心数据结构:uart_port 和 uart_ops
好,接下来是硬核内容。这两个结构体,是串口驱动的“身份证”和“操作手册”。
2.3.1 uart_port:硬件的“代言人”
uart_port结构体描述了一个串口端口的所有硬件信息。我每次写驱动,第一件事就是填充这个结构体。它包含:
| 成员 | 说明 |
|---|---|
base_addr |
寄存器基地址(内存映射IO) |
irq |
中断号 |
uartclk |
串口时钟频率(算波特率用) |
fifosize |
硬件FIFO深度 |
ops |
指向uart_ops的指针 |
line |
端口号(对应/dev/ttyS0的0) |
说白了,uart_port就是硬件在软件世界里的“替身”。你告诉内核:我的串口基地址是0x1000,中断号是42,时钟是24MHz……内核就知道怎么跟它打交道了。
2.3.2 uart_ops:驱动的“操作手册”
uart_ops是一组函数指针,定义了UART驱动需要实现的所有操作。我把它叫做“驱动必须填的坑”。核心函数有:
struct uart_ops {
// 端口启动/停止
int (*startup)(struct uart_port *port);
void (*shutdown)(struct uart_port *port);
// 参数设置(波特率、数据位等)
void (*set_termios)(struct uart_port *port,
struct ktermios *new,
struct ktermios *old);
// 数据发送
void (*start_tx)(struct uart_port *port);
void (*stop_tx)(struct uart_port *port);
// 中断处理
irqreturn_t (*handle_irq)(int irq, void *dev_id);
// 其他辅助函数
const char *(*type)(struct uart_port *port);
void (*config_port)(struct uart_port *port, int flags);
};
嗯,这里要注意:start_tx和stop_tx是UART层跟TTY层通信的关键。TTY层要发数据时,会调用start_tx,驱动收到后,就从环形缓冲区里取数据往硬件FIFO里塞。
我曾经踩过的坑:在start_tx里一次性把环形缓冲区的数据全塞进FIFO,结果FIFO满了,数据丢了。正确的做法是:每次只塞FIFO能容纳的量,然后等发送完成中断来了,再继续塞剩下的。
2.4 框架图:串口子系统数据流
为了让你更直观地理解这三层怎么配合,我画了一张数据流图。从用户写数据到硬件发出去,再到硬件收到数据返回给用户,整个路径一目了然。
你看,写数据是从上往下流,读数据是从下往上流。每一层都只跟相邻层打交道,互不越界。这种设计的好处是:你想换一个串口芯片,只需要重写最底层的UART驱动,上面的TTY层和线路规程完全不用动。
2.5 小结
这一章我们聊了串口子系统的三层架构。说白了,就是:
- TTY层:面向用户,提供标准字符设备接口
- 线路规程:数据加工厂,处理终端协议
- UART层:直面硬件,操作寄存器
核心数据结构uart_port和uart_ops,是驱动开发的“地基”。你写任何串口驱动,都绕不开这两个家伙。下一章我们会动手写一个实际的串口驱动,到时候你会更深刻地理解它们的作用。
个人建议:如果你刚开始学串口驱动,别急着看代码。先把这三层的关系理清楚。我当年就是太着急,一上来就啃8250的驱动代码,结果越看越糊涂。后来静下心来画了张数据流图,一下子就通了。
公众号:蓝海资料掘金营,微信deep3321