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 驱动发送给硬件设备。
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] 值。
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 串口核心层初始化流程
- 注册 uart_driver: 调用
uart_register_driver(),向 TTY 核心注册驱动,分配设备号。 - 添加 uart_port: 调用
uart_add_one_port(),将具体的串口端口(如 UART0、UART1)绑定到驱动。 - 硬件初始化: 在
startup回调中配置波特率、数据位、停止位、流控,并申请中断。 - 数据收发: 通过
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() |
释放中断、关闭硬件、清空缓冲区。 |