3. Linux内核TTY子系统

在 Android 系统中,UART 串口驱动并非独立存在,而是作为 Linux 内核 TTY 子系统的一部分被集成。理解 TTY 子系统的分层架构,是掌握串口调试、log 输出以及用户空间与硬件交互的关键。本章将深入剖析 TTY 架构的三层模型、线路规程(Line Discipline)的核心作用以及串口核心层(Serial Core)的抽象机制。

3.1 TTY 架构分层模型

Linux 内核的 TTY 子系统采用经典的分层设计,从上到下依次为:TTY 核心层(TTY Core)、线路规程(Line Discipline) 和 TTY 驱动层(TTY Driver)。这种分层实现了数据流处理与硬件控制的解耦。

核心概念: TTY 架构的核心思想是“数据流处理管道”。用户空间通过文件操作(open/read/write)与 TTY 核心交互,数据经过线路规程的格式化处理后,最终由 TTY 驱动发送给硬件设备。
Linux TTY 子系统分层架构 Linux TTY 子系统分层架构 用户空间 (User Space) — /dev/ttyS*, /dev/ttyGS*, /dev/pts/* TTY 核心层 (TTY Core) — tty_io.c, tty_ldisc.c 线路规程 (Line Discipline) — n_tty.c, ldisc 框架 TTY 驱动层 (TTY Driver) — 串口核心层 (Serial Core) 硬件层 (Hardware) — UART 控制器, DMA, GPIO 数据流方向:用户空间 → TTY核心 → 线路规程 → TTY驱动 → 硬件

3.1.1 各层职责与数据流

层级 核心文件/模块 主要职责
TTY 核心层 drivers/tty/tty_io.c 提供统一的字符设备接口(tty_fops),管理 tty_struct 结构体,处理 open/close/read/write 系统调用,维护 tty 端口与线路规程的绑定。
线路规程 drivers/tty/n_tty.c 实现数据格式化处理,如行编辑(echo、erase)、信号生成(Ctrl+C)、规范模式与非规范模式转换、输入输出缓冲区管理。
TTY 驱动层 drivers/tty/serial/serial_core.c 封装底层硬件操作,提供 uart_ops 回调函数集(startup/shutdown/tx_empty/set_termios),管理串口控制器状态与中断。
调试要点: 当用户空间的 log 输出(如 printk 通过 /dev/ttyS0)出现乱码或丢失时,问题通常出在 线路规程 的缓冲区管理或 TTY 驱动 的流控配置上。使用 cat /proc/tty/driver/serial 可以快速查看串口驱动状态。

3.2 线路规程(Line Discipline)深度解析

线路规程是 TTY 子系统中最为灵活和关键的中间层。它决定了数据如何在用户空间和硬件之间进行“翻译”。默认的线路规程是 N_TTY,但内核支持多种规程,如 N_PPP(点对点协议)、N_IRDA(红外协议)等。

3.2.1 线路规程的核心数据结构

每个线路规程通过 struct tty_ldisc_ops 结构体注册到内核,该结构体定义了规程的操作函数指针:

// include/linux/tty_ldisc.h
struct tty_ldisc_ops {
    int  magic;
    char *name;
    int  num;                       // 规程编号,如 N_TTY = 0
    int  (*open)(struct tty_struct *tty);
    void (*close)(struct tty_struct *tty);
    void (*flush_buffer)(struct tty_struct *tty);
    ssize_t (*read)(struct tty_struct *tty, struct file *file,
                    unsigned char __user *buf, size_t nr);
    ssize_t (*write)(struct tty_struct *tty, struct file *file,
                     const unsigned char *buf, size_t nr);
    int  (*ioctl)(struct tty_struct *tty, struct file *file,
                  unsigned int cmd, unsigned long arg);
    // ... 其他回调
};

3.2.2 规范模式 vs 非规范模式

线路规程根据 termios 配置,工作在两种模式下:

  • 规范模式(Canonical Mode): 数据按行处理,直到遇到换行符(\n)或 EOF 才返回给用户空间。支持行编辑(退格、删除)。适用于交互式终端。
  • 非规范模式(Non-Canonical Mode): 数据按字节或定时器阈值返回,不进行行编辑。适用于串口传感器、二进制数据传输。
常见陷阱: Android 的 logcat 输出通常使用非规范模式,但如果串口驱动初始化时未正确设置 termios 的 MIN 和 TIME 参数,可能导致 log 输出延迟或阻塞。检查 struct ktermios 中的 c_cc[VMIN] 和 c_cc[VTIME] 值。
线路规程数据流处理流程 线路规程 (N_TTY) 数据流处理流程 用户空间 write() 线路规程 write() 回调 TTY 驱动写硬件 硬件接收中断 翻转缓冲区 (Flip Buffer) 线路规程 read() 回调 用户空间 read() 写路径:用户 → 线路规程 → 驱动 → 硬件;读路径:硬件 → 翻转缓冲 → 线路规程 → 用户

3.3 串口核心层(Serial Core)

串口核心层是 TTY 驱动层中专门为 UART 设备设计的通用框架,位于 drivers/tty/serial/ 目录。它通过 struct uart_driver 和 struct uart_port 抽象了不同硬件平台的串口控制器,极大简化了驱动开发。

3.3.1 核心数据结构与注册流程

一个典型的串口驱动需要实现以下核心结构体:

// include/linux/serial_core.h
struct uart_driver {
    struct module        *owner;
    const char          *driver_name;    // 如 "serial"
    const char          *dev_name;       // 如 "ttyS"
    int                  major;          // 主设备号
    int                  minor;          // 起始次设备号
    int                  nr;             // 支持的端口数
    struct uart_state   *state;          // 端口状态数组
    // ...
};

struct uart_port {
    spinlock_t           lock;           // 端口自旋锁
    unsigned long        iobase;         // IO 基地址
    unsigned char __iomem *membase;      // 内存映射基地址
    unsigned int         irq;            // 中断号
    unsigned int         uartclk;        // UART 时钟频率
    unsigned int         fifosize;       // FIFO 大小
    struct uart_ops      *ops;           // 硬件操作函数集
    // ...
};

struct uart_ops {
    unsigned int (*tx_empty)(struct uart_port *);
    void         (*set_mctrl)(struct uart_port *, unsigned int);
    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 *);
    int          (*startup)(struct uart_port *);
    void         (*shutdown)(struct uart_port *);
    void         (*set_termios)(struct uart_port *, struct ktermios *new,
                                struct ktermios *old);
    // ...
};

3.3.2 串口核心层初始化流程

  1. 注册 uart_driver: 调用 uart_register_driver(),向 TTY 核心注册驱动,分配设备号。
  2. 添加 uart_port: 调用 uart_add_one_port(),将具体的串口端口(如 UART0、UART1)绑定到驱动。
  3. 硬件初始化: 在 startup 回调中配置波特率、数据位、停止位、流控,并申请中断。
  4. 数据收发: 通过 start_tx 启动发送,通过中断处理函数接收数据并调用 tty_insert_flip_char 送入翻转缓冲区。
Android 平台差异: 高通平台通常使用 msm_serial.c 驱动,MTK 平台使用 mtk_uart.c。两者都基于串口核心层,但硬件寄存器布局和时钟管理差异较大。调试时需重点关注 set_termios 回调中的波特率计算。

3.3.3 串口核心层与硬件交互的典型时序

阶段 调用函数 说明
打开端口 uart_open() → uart_startup() 初始化硬件、申请中断、启动接收。
配置参数 uart_set_termios() 设置波特率、校验、流控,更新硬件寄存器。
发送数据 uart_write() → uart_start_tx() 将数据写入发送 FIFO,触发发送中断或 DMA。
接收数据 中断处理 → tty_insert_flip_char() 从接收 FIFO 读取数据,送入 TTY 翻转缓冲区。
关闭端口 uart_close() → uart_shutdown() 释放中断、关闭硬件、清空缓冲区。
串口核心层与硬件交互时序 串口核心层 (Serial Core) 与硬件交互时序 用户空间 串口核心层 硬件驱动 open() uart_startup() 硬件初始化 ioctl(TCSETS) set_termios() 配置波特率 write() uart_write() 写 FIFO 接收中断 翻转缓冲区 read() 返回

3.4 本章小结