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,负责处理终端协议。比如:

  • 回车换行转换(ONLCRINLCR等)
  • 数据回显(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_txstop_tx是UART层跟TTY层通信的关键。TTY层要发数据时,会调用start_tx,驱动收到后,就从环形缓冲区里取数据往硬件FIFO里塞。

我曾经踩过的坑:在start_tx里一次性把环形缓冲区的数据全塞进FIFO,结果FIFO满了,数据丢了。正确的做法是:每次只塞FIFO能容纳的量,然后等发送完成中断来了,再继续塞剩下的。

2.4 框架图:串口子系统数据流

为了让你更直观地理解这三层怎么配合,我画了一张数据流图。从用户写数据到硬件发出去,再到硬件收到数据返回给用户,整个路径一目了然。

Linux串口子系统数据流图 用户空间 应用程序:open("/dev/ttyS0") / read() / write() TTY层 file_operations / 环形缓冲区 / tty_struct 线路规程(Line Discipline) N_TTY:回车换行转换 / 回显 / 信号生成 UART层(串口驱动) uart_port / uart_ops / 中断处理 / 寄存器操作 硬件:UART控制器 / 串口芯片 写数据路径 读数据路径(反向)

你看,写数据是从上往下流,读数据是从下往上流。每一层都只跟相邻层打交道,互不越界。这种设计的好处是:你想换一个串口芯片,只需要重写最底层的UART驱动,上面的TTY层和线路规程完全不用动。

2.5 小结

这一章我们聊了串口子系统的三层架构。说白了,就是:

  • TTY层:面向用户,提供标准字符设备接口
  • 线路规程:数据加工厂,处理终端协议
  • UART层:直面硬件,操作寄存器

核心数据结构uart_portuart_ops,是驱动开发的“地基”。你写任何串口驱动,都绕不开这两个家伙。下一章我们会动手写一个实际的串口驱动,到时候你会更深刻地理解它们的作用。

个人建议:如果你刚开始学串口驱动,别急着看代码。先把这三层的关系理清楚。我当年就是太着急,一上来就啃8250的驱动代码,结果越看越糊涂。后来静下心来画了张数据流图,一下子就通了。


公众号:蓝海资料掘金营,微信deep3321