6. Linux内核TTY子系统¶
TTY(Teletype,电传打字机)是Linux系统最古老、最核心的字符设备子系统之一,承担系统人机交互、串口通信、终端会话、日志输出等核心功能。 早期用于物理电传设备,如今演化为一套通用的终端字符传输框架,兼容物理串口、虚拟控制台、伪终端、调试终端等所有字符交互场景。
Linux内核TTY子系统采用分层架构,将上层通用终端协议处理、中层线路规程解析、底层硬件UART驱动完全分离,实现一套框架适配所有串口硬件。 开发者只需适配底层硬件操作接口,即可快速完成UART串口驱动开发,无需关心上层终端协议、字符转换、行编辑等复杂逻辑。
6.1. TTY与UART基础概念¶
6.1.1. 相关名词区分¶
Linux系统中易混淆的四个核心名词:
UART:硬件层通用异步收发传输器,是物理串口硬件,负责底层串行数据收发、电平转换、波特率时序控制,属于硬件外设。
TTY:内核软件子系统框架,统一管理所有终端字符设备,向上提供文件读写接口,向下适配UART硬件驱动。
Console:控制台,用于系统内核日志、启动信息输出,可绑定任意TTY设备。
PTY:伪终端,纯虚拟TTY设备,无硬件对应,用于SSH终端、图形终端、进程交互场景。
6.2. UART基础知识¶
UART是异步串行全双工通信协议,无需同步时钟线,仅通过TX(发送)、RX(接收)两根数据线即可完成双向通信。 通信双方必须提前约定波特率、数据位、校验位、停止位参数,参数完全一致才能正常解析数据,数据以帧为单位逐位串行传输。
6.2.1. 数据帧格式¶
标准UART帧结构(从低位到高位传输):起始位 + 数据位 + 校验位(可选) + 停止位
起始位:固定1位低电平,标志一帧数据开始,用于同步收发时序。
数据位:5/6/7/8位,常规外设默认8位,传输有效数据。
校验位:可选,无校验/奇校验/偶校验,用于数据纠错。
停止位:1位/2位高电平,标志一帧数据传输结束。
6.2.2. 波特率¶
波特率定义:每秒传输的码元个数,代表串口传输速率,单位Baud。常见标准波特率:300、9600、19200、38400、115200。
内核波特率计算公式:波特率除数 = 总线时钟 / (16 * 目标波特率)
6.2.3. 流控机制¶
流控用于防止数据丢失,确保发送端和接收端的数据传输速率匹配。流控分为两种类型:
硬件流控
使用RTS(Request To Send,发送请求)和CTS(Clear To Send,发送允许)两个信号线:
RTS:输出信号,由本设备的接收缓冲区状态控制,本质是“接收就绪指示”,用来告诉对端“我能不能接收数据”。
CTS:输入信号,由本设备的发送逻辑检测,本质是“发送允许判断”,用来确认“我能不能给对端发数据”。
当接收方缓冲区满时,会拉高RTS信号(无效状态)通知发送方停止发送;当缓冲区有空间时,拉低RTS信号(有效状态)允许继续发送。
软件流控
通过发送特殊字符(XON/XOFF)来控制数据流:
XON(0x11):通知对端继续发送。
XOFF(0x13):通知对端暂停发送。
当接收方无法继续接收时,发送XOFF字符停止传输;当可以继续接收时,发送XON字符恢复传输。
6.3. TTY子系统整体架构¶
VFS虚拟文件系统层:向上对应用户态,提供标准文件读写接口(read/write/ioctl),用户操作/dev/ttySx等设备文件即可完成串口收发。
TTY核心层(tty_core):内核通用核心模块,负责设备注册、节点管理、状态维护、资源调度,封装通用TTY操作逻辑,隔离用户态与底层驱动。
线路规程层(Line Discipline):默认N_TTY规程,核心作用:字符过滤、换行转换、回显处理、行编辑、特殊命令解析,是终端交互的核心逻辑层,硬件驱动无需干预。
UART硬件驱动层:开发者编写的驱动层级,适配具体UART硬件,实现硬件初始化、波特率配置、数据收发、中断处理、寄存器操作,对接内核uart_ops接口。
6.4. TTY子系统核心结构体¶
6.4.1. UART驱动结构体¶
UART驱动结构体(struct uart_driver)用于描述驱动全局信息、设备数量、设备名称,是模块注册的核心结构体。
1 2 3 4 5 6 7 8 9 10 11 12 13 | struct uart_driver {
struct module *owner; /* 模块所有者 */
const char *driver_name; /* 驱动名称 */
const char *dev_name; /* 设备文件名 */
int major; /* 主设备号 */
int minor; /* 起始次设备号 */
int nr; /* 端口数量 */
struct console *cons; /* 控制台支持 */
/* 以下字段由串口核心层管理,驱动无需初始化 */
struct uart_state *state; /* 端口状态数组 */
struct tty_driver *tty_driver; /* 对应的TTY驱动 */
};
|
6.4.2. UART端口硬件描述结构体¶
UART端口硬件描述结构体(struct uart_port)用于描述单个UART硬件端口的所有硬件参数、状态、操作接口,是串口硬件资源的核心载体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | struct uart_port {
spinlock_t lock; /* 端口自旋锁 */
unsigned long iobase; /* IO端口基地址 */
unsigned char __iomem *membase; /* 内存映射IO基地址 */
unsigned int irq; /* 中断号 */
unsigned int uartclk; /* UART时钟频率 */
unsigned int fifosize; /* 发送FIFO大小 */
unsigned char x_char; /* 待发送的XON/XOFF字符 */
unsigned char regshift; /* 寄存器偏移移位 */
unsigned int iotype; /* IO访问类型 */
unsigned int read_status_mask; /* 读状态掩码 */
unsigned int ignore_status_mask; /* 忽略状态掩码 */
const struct uart_ops *ops; /* UART操作函数集 */
unsigned int custom_divisor; /* 自定义除数 */
unsigned int line; /* 端口号 */
unsigned int minor; /* 次设备号 */
resource_size_t mapbase; /* 物理地址基址 */
struct device *dev; /* 父设备 */
unsigned char suspended; /* 是否挂起 */
unsigned char console_reinit; /* 控制台重新初始化标志 */
const char *name; /* 端口名称,如"ttyS0" */
struct attribute_group *attr_group; /* 属性组 */
const struct attribute_group **tty_groups;
unsigned int type; /* 端口类型 */
unsigned int flags; /* 端口标志 */
/* 其他成员省略 */
};
|
6.4.3. UART硬件操作回调集结构体¶
UART硬件操作回调集结构体(struct uart_ops)是驱动开发者需要填充所有硬件相关操作函数,内核TTY核心层通过该结构体调用底层硬件逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | struct uart_ops {
/* 查询发送缓冲区是否完全为空 */
unsigned int (*tx_empty)(struct uart_port *);
/* 设置调制解调器控制引脚状态 */
void (*set_mctrl)(struct uart_port *, unsigned int mctrl);
/* 获取当前调制解调器引脚状态 */
unsigned int (*get_mctrl)(struct uart_port *);
/* 停止串口数据发送 */
void (*stop_tx)(struct uart_port *);
/* 启动串口数据发送 */
void (*start_tx)(struct uart_port *);
/* 流控节流:暂停接收上游数据 */
void (*throttle)(struct uart_port *);
/* 流控解除:恢复接收上游数据 */
void (*unthrottle)(struct uart_port *);
/* 发送特殊控制字符(XON/XOFF软件流控) */
void (*send_xchar)(struct uart_port *, char ch);
/* 停止串口数据接收 */
void (*stop_rx)(struct uart_port *);
/* 启动串口数据接收 */
void (*start_rx)(struct uart_port *);
/* 使能调制解调器状态中断检测 */
void (*enable_ms)(struct uart_port *);
/* 发送/停止Break中断信号 */
void (*break_ctl)(struct uart_port *, int ctl);
/* 串口端口初始化、硬件使能 */
int (*startup)(struct uart_port *);
/* 串口端口关闭、硬件失能、资源释放 */
void (*shutdown)(struct uart_port *);
/* 清空收发FIFO缓冲区数据 */
void (*flush_buffer)(struct uart_port *);
/* 配置串口参数:波特率、数据位、校验位、停止位、流控 */
void (*set_termios)(struct uart_port *, struct ktermios *new,
const struct ktermios *old);
/* 设置线路规程(N_TTY等),切换终端解析模式 */
void (*set_ldisc)(struct uart_port *, struct ktermios *);
/* 串口电源管理 */
void (*pm)(struct uart_port *, unsigned int state,
unsigned int oldstate);
/* 获取当前UART硬件型号字符串 */
const char *(*type)(struct uart_port *);
/* 释放串口硬件IO资源 */
void (*release_port)(struct uart_port *);
/* 申请、占用串口硬件IO资源 */
int (*request_port)(struct uart_port *);
/* 端口硬件参数配置与初始化 */
void (*config_port)(struct uart_port *, int);
/* 校验用户态传入的串口参数合法性 */
int (*verify_port)(struct uart_port *, struct serial_struct *);
/* 自定义串口IO控制命令处理 */
int (*ioctl)(struct uart_port *, unsigned int, unsigned long);
#ifdef CONFIG_CONSOLE_POLL
/* 轮询控制台初始化 */
int (*poll_init)(struct uart_port *);
/* 轮询模式下发单个字符 */
void (*poll_put_char)(struct uart_port *, unsigned char);
/* 轮询模式读取单个字符 */
int (*poll_get_char)(struct uart_port *);
#endif
};
|
6.4.4. UART状态管理结构体¶
UART状态管理结构体(struct uart_state)用于维护单个UART端口的TTY状态、发送缓冲区、电源状态、引用计数与等待队列,串联tty_port与uart_port,支撑串口异步收发与资源安全释放。
1 2 3 4 5 6 7 8 9 | struct uart_state {
struct tty_port port; // TTY端口状态与缓冲区管理核心结构体
enum uart_pm_state pm_state; // UART电源管理状态
struct circ_buf xmit; // 串口发送环形缓冲区,缓存上层下发待发送数据
atomic_t refcount; // 设备引用计数,防止资源被异常释放
wait_queue_head_t remove_wait; // 设备销毁等待队列,等待业务进程退出
struct uart_port *uart_port; // 关联底层硬件端口结构体,绑定硬件资源
};
|
6.4.5. TTY驱动结构体¶
TTY驱动结构体(struct tty_driver)用于描述一类TTY设备的全局驱动信息、设备数量、设备号、默认终端参数、文件操作接口,是TTY驱动向内核注册的载体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | struct tty_driver {
struct kref kref; // 驱动引用计数,用于安全资源释放
struct module *owner; // 模块所有者,固定赋值THIS_MODULE
const char *driver_name; // 驱动内核名称
const char *name; // TTY设备节点名称前缀
int name_base; // 设备号名称起始偏移
int major; // 主设备号
int minor_start; // 次设备号起始编号
unsigned int num; // 当前驱动管理的TTY设备数量
short type; // TTY设备类型
short subtype; // TTY设备子类型
struct ktermios init_termios; // 设备初始化默认终端参数
unsigned long flags; // TTY驱动属性标志位
const struct tty_operations *ops; // TTY文件操作回调函数集
struct list_head tty_drivers; // TTY驱动全局链表节点
/* 其他成员省略 */
}
|
6.4.6. TTY驱动操作回调集结构体¶
TTY驱动操作回调集结构体(struct tty_operations)定义了设备打开关闭、数据收发、参数配置、流控控制等所有上层调用的回调函数,是TTY驱动与内核核心层之间的交互入口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | struct tty_operations {
int (*open)(struct tty_struct *tty, struct file *filp); // 打开TTY设备节点
void (*close)(struct tty_struct *tty, struct file *filp); // 关闭TTY设备节点
int (*write)(struct tty_struct *tty, const unsigned char *buf, int count); // 批量发送数据
int (*put_char)(struct tty_struct *tty, unsigned char ch); // 发送单个字符
int (*ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg); // 设备IO控制命令
void (*set_termios)(struct tty_struct *tty, const struct ktermios *old); // 更新终端配置参数
void (*throttle)(struct tty_struct *tty); // 流控节流:暂停上游数据输入
void (*unthrottle)(struct tty_struct *tty); // 流控解除:恢复上游数据输入
void (*hangup)(struct tty_struct *tty); // 挂断终端连接
int (*break_ctl)(struct tty_struct *tty, int state); // Break中断信号控制
int (*tiocmget)(struct tty_struct *tty); // 获取调制解调器引脚状态
int (*tiocmset)(struct tty_struct *tty, unsigned int set, unsigned int clear); // 设置调制解调器引脚
}
|
6.4.7. 终端参数配置结构体¶
终端参数配置结构体(struct ktermios)保存串口终端所有配置参数,包括波特率、数据位、校验位、停止位、流控、字符转换规则等。
1 2 3 4 5 6 7 8 9 10 | struct ktermios {
tcflag_t c_iflag; // 输入模式标志位
tcflag_t c_oflag; // 输出模式标志位
tcflag_t c_cflag; // 控制模式标志位
tcflag_t c_lflag; // 本地模式标志位
cc_t c_line; // 线路规程类型
cc_t c_cc[NCCS]; // 特殊控制字符数组
speed_t c_ispeed; // 输入波特率
speed_t c_ospeed; // 输出波特率
};
|
6.5. TTY子系统核心函数¶
6.5.1. 注册UART驱动¶
6.5.1.1. uart_register_driver函数¶
uart_register_driver函数用于将自定义UART驱动结构体注册到内核TTY子系统,完成驱动框架初始化。
函数原型:
1 | int uart_register_driver(struct uart_driver *drv);
|
参数说明:
drv:初始化完成的uart_driver驱动指针。
返回值:成功返回0,失败返回负错误码。
6.5.2. 注销UART驱动¶
6.5.2.1. uart_unregister_driver函数¶
uart_unregister_driver函数用于卸载UART驱动框架,释放内核驱动资源,模块退出必须调用。
函数原型:
1 | void uart_unregister_driver(struct uart_driver *drv);
|
参数说明:
drv:待注销的uart_driver驱动指针。
6.5.3. 添加单个串口端口¶
6.5.3.1. uart_add_one_port函数¶
uart_add_one_port函数用于向已注册的驱动框架添加一个硬件串口端口,创建设备节点。
函数原型:
1 | int uart_add_one_port(struct uart_driver *drv, struct uart_port *port);
|
参数说明
drv:已注册的UART驱动指针。
port:初始化完成的硬件端口结构体指针。
返回值:成功返回0,失败返回负错误码。
6.5.4. 移除串口端口¶
6.5.4.1. uart_remove_one_port函数¶
uart_remove_one_port函数用于移除串口端口、销毁设备节点、释放硬件资源。
函数原型:
1 | void uart_remove_one_port(struct uart_driver *drv, struct uart_port *port);
|
参数说明
drv:已注册的UART驱动结构体指针。
port:需要移除的UART硬件端口结构体指针。
6.5.5. 获取合法波特率¶
6.5.5.1. uart_get_baud_rate函数¶
uart_get_baud_rate函数用于根据终端配置解析合法波特率,限制波特率上下限,自动适配标准值,过滤非法波特率参数。
函数原型:
1 2 | unsigned int uart_get_baud_rate(struct uart_port *port, struct ktermios *termios,
const struct ktermios *old, unsigned int min, unsigned int max);
|
参数说明:
port:UART硬件端口结构体指针。
termios:新的终端参数配置结构体。
old:旧的终端参数配置结构体,用于对比变更。
min:波特率最小限制值。
max:波特率最大限制值。
返回值:返回校验、适配后的合法标准波特率。
6.5.6. 更新传输超时¶
6.5.6.1. uart_update_timeout函数¶
uart_update_timeout函数用于根据当前波特率、串口控制标志,自动计算并更新串口传输超时时间,适配不同速率的数据收发,防止低速波特率下数据截断、丢包问题。
函数原型:
1 | void uart_update_timeout(struct uart_port *port, unsigned int cflag, unsigned int baud);
|
参数说明:
port:UART硬件端口结构体指针。
cflag:串口控制标志位(数据位、停止位、校验位配置)。
baud:当前生效的串口波特率。
6.5.7. 插入接收字符到TTY缓冲区¶
6.5.7.1. uart_insert_char函数¶
uart_insert_char函数用于将硬件中断接收的单个字符写入TTY翻转缓冲区,同时携带错误标记、状态标记,供上层线路规程解析处理,是串口接收中断的核心上报函数。
函数原型:
1 2 | void uart_insert_char(struct uart_port *port, unsigned int status,
unsigned int err_mask, u8 ch, u16 flag);
|
参数说明:
port:UART硬件端口结构体指针。
status:硬件接收状态寄存器值。
err_mask:需要屏蔽的错误状态位。
ch:硬件读取到的单字节接收数据。
flag:字符属性标记(普通数据/错误数据)。
6.6. 内核TTY子系统源码解析¶
内核TTY子系统源码相关位于: 内核源码/drivers/tty/
查看瑞芯微UART相关设备树可知,瑞芯微UART使用的是8250串口通用驱动,位于: 内核源码/drivers/tty/serial/8250/
6.6.1. 设备注册流程¶
6.6.1.1. UART硬件驱动层(8250)¶
UART硬件驱动层(8250):最底层的硬件驱动层,负责具体8250/16550芯片的初始化和端口注册。
serial8250_init()是8250驱动的模块初始化入口,初始化ISA端口,调用uart_register_driver注册驱动,并注册所有静态配置的端口。
1 2 3 4 5 6 7 8 9 10 11 | // 模块初始化入口
static int __init serial8250_init(void)
{
// ...
serial8250_isa_init_ports(); // 初始化静态端口数组
// ...
ret = uart_register_driver(&serial8250_reg); // 调用serial_core层注册驱动
// ...
serial8250_register_ports(&serial8250_reg, &serial8250_isa_devs->dev); // 注册端口
// ...
}
|
serial8250_register_8250_port()用于动态注册8250端口。它配置端口参数后,调用uart_add_one_port将其加入核心层。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // 注册单个8250端口
int serial8250_register_8250_port(const struct uart_8250_port *up)
{
// ...
// 配置端口参数
uart->port.iobase = up->port.iobase;
uart->port.membase = up->port.membase;
uart->port.irq = up->port.irq;
uart->port.irqflags = up->port.irqflags;
uart->port.uartclk = up->port.uartclk;
uart->port.fifosize = up->port.fifosize;
// ...
// 调用serial_core层添加端口
ret = uart_add_one_port(&serial8250_reg, &uart->port);
// ...
}
|
6.6.1.2. serial_core层¶
serial_core层:对TTY核心层的封装,专门用于处理UART串口设备的通用逻辑。
uart_register_driver():8250驱动在初始化时调用此函数。它分配tty_driver,设置串口专用的操作集uart_ops,并调用tty_register_driver() 将其注册到TTY核心层。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // 注册UART驱动
int uart_register_driver(struct uart_driver *drv)
{
// ...
// 分配tty_driver结构
normal = tty_alloc_driver(drv->nr, TTY_DRIVER_REAL_RAW |
TTY_DRIVER_DYNAMIC_DEV);
// ...
normal->driver_state = drv;
// 设置tty_operations
tty_set_operations(normal, &uart_ops);
// 初始化每个端口的tty_port
for (i = 0; i < drv->nr; i++) {
// ...
tty_port_init(port);
port->ops = &uart_port_ops;
}
retval = tty_register_driver(normal); // 注册到TTY核心层
// ...
}
|
uart_add_one_port():将具体的硬件端口(uart_port)添加到驱动中。它会调用tty_port_register_device_attr_serdev完成设备节点的注册。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // 添加单个UART端口
int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport)
{
// ...
// 链接port到driver
tty_port_link_device(port, drv->tty_driver, uport->line);
// 配置硬件端口参数
uart_configure_port(drv, state, uport);
// ...
// 调用TTY核心层函数注册设备
tty_dev = tty_port_register_device_attr_serdev(port, drv->tty_driver,
uport->line, uport->dev, port, uport->tty_groups);
// ...
}
|
6.6.1.3. TTY核心层¶
TTY核心层:负责将底层的硬件端口(tty_port)与TTY核心层的设备模型关联起来。
tty_port_register_device_attr_serdev():注册一个串口设备。它提供了一个统一的接口,能够根据设备树的配置,自动决定将该端口注册为传统的TTY设备(如/dev/ttyS0)还是serdev设备如蓝牙、GPS模块)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // 注册TTY端口设备
struct device *tty_port_register_device_attr_serdev(
struct tty_port *port, struct tty_driver *driver,
unsigned index, struct device *device, void *drvdata,
const struct attribute_group **attr_grp)
{
// 链接port到driver的ports数组
tty_port_link_device(port, driver, index);
// 尝试注册serdev设备
dev = serdev_tty_port_register(port, device, driver, index);
if (PTR_ERR(dev) != -ENODEV)
return dev;
// 注册为TTY设备
return tty_register_device_attr(driver, index, device,
drvdata, attr_grp);
}
|
tty_port_link_device():将底层的硬件端口(tty_port)与TTY驱动(tty_driver)中的特定索引(index)绑定在一起。
1 2 3 4 5 6 7 8 | // 链接tty_port到tty_driver
void tty_port_link_device(struct tty_port *port,
struct tty_driver *driver, unsigned index)
{
if (WARN_ON(index >= driver->num))
return;
driver->ports[index] = port; // 建立关联
}
|
6.6.1.4. VFS虚拟文件系统层¶
VFS虚拟文件系统层:向上,它向用户空间提供标准的字符设备文件接口(open, read, write, ioctl);向下,它将通用的文件操作路由到具体的TTY驱动和线路规程(Line Discipline)。
tty_register_driver():注册一个TTY驱动到内核核心,分配设备号,并将其加入全局驱动链表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // 以下函数省略源码部分内容
int tty_register_driver(struct tty_driver *driver)
{
// 分配或注册字符设备号区域
if (!driver->major)
error = alloc_chrdev_region(&dev, driver->minor_start, driver->num, driver->name);
else
error = register_chrdev_region(dev, driver->num, driver->name);
// 如果是动态分配cdev模式,注册一个整体的字符设备
if (driver->flags & TTY_DRIVER_DYNAMIC_ALLOC) {
error = tty_cdev_add(driver, dev, 0, driver->num);
}
// 将驱动加入全局tty_drivers链表,供后续open时查找
mutex_lock(&tty_mutex);
list_add(&driver->tty_drivers, &tty_drivers);
mutex_unlock(&tty_mutex);
// 如果驱动没有设置DYNAMIC_DEV标志,则立即为所有端口创建设备节点
if (!(driver->flags & TTY_DRIVER_DYNAMIC_DEV)) {
for (i = 0; i < driver->num; i++)
d = tty_register_device(driver, i, NULL);
}
// 在/proc/tty/driver/下创建对应的proc文件,供用户空间查询状态
proc_tty_register_driver(driver);
driver->flags |= TTY_DRIVER_INSTALLED;
return 0;
}
|
tty_register_device_attr():为驱动的特定索引(如index=0对应ttyS0)注册一个具体的设备节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // 以下函数省略源码部分内容
struct device *tty_register_device_attr(struct tty_driver *driver, unsigned index,
struct device *device, void *drvdata, const struct attribute_group **attr_grp)
{
// 计算设备号
dev_t devt = MKDEV(driver->major, driver->minor_start) + index;
// 生成设备名称,如"ttyS0"
tty_line_name(driver, index, name);
// 分配并初始化struct device
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
dev->devt = devt;
dev->class = tty_class; // 归属到/sys/class/tty
dev->parent = device;
dev_set_name(dev, "%s", name);
// 注册到Linux设备模型
retval = device_register(dev);
// 添加字符设备cdev到内核,绑定VFS操作集
if (!(driver->flags & TTY_DRIVER_DYNAMIC_ALLOC)) {
retval = tty_cdev_add(driver, devt, index, 1);
}
// 发送uevent (KOBJ_ADD),通知用户空间的udev/mdev在/dev下创建/dev/ttyS0节点
dev_set_uevent_suppress(dev, 0);
kobject_uevent(&dev->kobj, KOBJ_ADD);
return dev;
}
|
tty_cdev_add():将TTY驱动与字符设备模型cdev绑定,这是VFS能够拦截open/read/write的关键。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 以下函数省略源码部分内容
static int tty_cdev_add(struct tty_driver *driver, dev_t dev, unsigned int index, unsigned int count)
{
// 分配cdev结构
driver->cdevs[index] = cdev_alloc();
// 绑定VFS标准文件操作集,所有对该设备的open/read/write都会进入tty_fops
driver->cdevs[index]->ops = &tty_fops;
driver->cdevs[index]->owner = driver->owner;
// 将cdev添加到内核的字符设备哈希表中
err = cdev_add(driver->cdevs[index], dev, count);
return err;
}
|
6.6.2. 响应用户态系统调用¶
当用户程序执行open(“/dev/ttyS0”) 时,VFS会根据设备号找到上面注册的cdev,并调用其绑定的tty_fops中的函数。
6.6.2.1. tty_open¶
tty_open():TTY设备打开的总入口,负责查找驱动、分配tty_struct、初始化线路规程,并调用底层硬件驱动的open。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | // 以下函数省略源码部分内容
static int tty_open(struct inode *inode, struct file *filp)
{
dev_t device = inode->i_rdev;
// 分配文件私有数据结构
retval = tty_alloc_file(filp);
// 查找驱动并初始化tty结构
tty = tty_open_by_driver(device, filp);
// 将file与tty关联
tty_add_file(tty, filp);
// 调用底层驱动的具体open方法,如serial_core的uart_open -> tty_port_open
if (tty->ops->open)
retval = tty->ops->open(tty, filp);
// 处理控制终端关联
if (!noctty)
tty_open_proc_set_tty(filp, tty);
return 0;
}
// 辅助函数:查找驱动并初始化
static struct tty_struct *tty_open_by_driver(dev_t device, struct file *filp)
{
// 根据设备号遍历tty_drivers链表,找到匹配的driver和index
driver = tty_lookup_driver(device, filp, &index);
// 检查是否是重新打开
tty = tty_driver_lookup_tty(driver, filp, index);
if (tty) {
// 重新打开逻辑
retval = tty_reopen(tty);
} else {
// 首次打开:分配tty_struct,调用driver->ops->install
// 绑定tty->port,并初始化线路规程
tty = tty_init_dev(driver, index);
}
return tty;
}
|
6.6.2.2. tty_read¶
tty_read():从TTY设备读取数据。TTY层本身不直接读硬件,而是委托给线路规程(如n_tty)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // 以下函数省略源码部分内容
static ssize_t tty_read(struct kiocb *iocb, struct iov_iter *to)
{
struct tty_struct *tty = file_tty(file);
struct tty_ldisc *ld;
// 检查设备是否已挂断
if (!tty || tty_io_error(tty))
return -EIO;
// 获取线路规程的引用
ld = tty_ldisc_ref_wait(tty);
// 调用线路规程的read函数。
// 对于n_tty,它会处理回显、规范模式(回车换行),并从tty翻转缓冲区(tty_flip_buffer)拷贝数据到用户空间
if (ld->ops->read)
i = iterate_tty_read(ld, tty, file, to);
tty_ldisc_deref(ld);
return i;
}
|
6.6.2.3. tty_write¶
tty_write():向TTY设备写入数据。同样,它先将数据交给线路规程处理,再由线路规程交给底层驱动。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // 以下函数省略源码部分内容
static ssize_t tty_write(struct kiocb *iocb, struct iov_iter *from)
{
return file_tty_write(iocb->ki_filp, iocb, from);
}
static ssize_t file_tty_write(struct file *file, struct kiocb *iocb, struct iov_iter *from)
{
struct tty_struct *tty = file_tty(file);
struct tty_ldisc *ld;
// 获取线路规程引用
ld = tty_ldisc_ref_wait(tty);
// 调用 do_tty_write,它会将用户空间数据分块(默认2KB)拷贝到内核临时缓冲区,然后调用线路规程的write方法(如n_tty_write)
// n_tty_write最终会调用tty->ops->write(即uart_write),将数据放入串口的环形发送缓冲区(xmit_buf),并触发硬件发送中断。
ret = do_tty_write(ld->ops->write, tty, file, from);
tty_ldisc_deref(ld);
return ret;
}
|
6.6.2.4. tty_release¶
tty_release():对应用户态close,关闭TTY设备,清理资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // 以下函数省略源码部分内容
int tty_release(struct inode *inode, struct file *filp)
{
struct tty_struct *tty = file_tty(filp);
// 递减引用计数
if (--tty->count < 0) { ... }
// 从tty的文件列表中移除此file
tty_del_file(filp);
// 检查是否是最后一个关闭者(final close)
final = !tty->count && !(o_tty && o_tty->count);
if (!final)
return 0;
// 如果是最后一个关闭者,调用底层驱动的close方法(如uart_close -> tty_port_close)
// tty_port_close会负责刷新缓冲区、降低DTR/RTS信号、关闭硬件等
if (tty->ops->close)
tty->ops->close(tty, filp);
// 释放tty_struct结构体内存
tty_release_struct(tty, idx);
return 0;
}
|
6.6.2.5. tty_ioctl¶
tty_ioctl():处理终端控制命令,如设置波特率(TCSETS)、获取窗口大小(TIOCGWINSZ)、设置线路规程(TIOCSETD)等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // 以下函数省略源码部分内容
long tty_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct tty_struct *tty = file_tty(file);
struct tty_ldisc *ld;
switch (cmd) {
// 处理VFS/TTY 核心层的通用ioctl
case TIOCGWINSZ: return tiocgwinsz(real_tty, p); // 获取窗口大小
case TIOCSETD: return tiocsetd(tty, p); // 设置线路规程
case TIOCSBRK: return tty->ops->break_ctl(tty, -1); // 发送BREAK信号
// ... 其他通用处理
}
// 如果驱动有自定义的ioctl,优先调用驱动的实现
if (tty->ops->ioctl) {
retval = tty->ops->ioctl(tty, cmd, arg);
if (retval != -ENOIOCTLCMD) return retval;
}
// 否则,将ioctl传递给线路规程处理
ld = tty_ldisc_ref_wait(tty);
if (ld->ops->ioctl) {
retval = ld->ops->ioctl(tty, cmd, arg);
}
tty_ldisc_deref(ld);
return retval;
}
|
6.7. 虚拟16550串口驱动实验¶
本实验驱动基于Linux内核标准TTY子系统、serial_core串口框架开发,纯软件模拟16550A通用异步收发器寄存器模型, 实现虚拟串口的数据收发、波特率配置、线路参数设置、调制解调器信号控制等完整功能,实现虚拟串口的自发自收。
本章的示例代码目录为: linux_driver/40_tty_subsystem
6.7.1. 16550 UART通信协议详解¶
16550(Universal Asynchronous Receiver/Transmitter,通用异步收发器)是工业界最经典的串口控制器标准, 定义了完整的寄存器编程接口、数据帧格式、FIFO缓冲机制与中断控制逻辑。 绝大多数PC、嵌入式平台的物理串口芯片均兼容16550寄存器标准,Linux内核serial_core框架也原生基于16550模型设计。
16550 UART通信协议可参考: UART16550 Core Technical Manual
6.7.1.1. 8250和16550比较¶
16550 UART相比8250,最核心的升级是引入了16字节的FIFO(先进先出)缓冲区,这使其能稳定支持更高的通信速率。主要区别如下:
特性 |
8250 UART |
16550 UART |
|---|---|---|
核心区别 |
无FIFO缓冲区 |
内置16字节FIFO缓冲区 |
数据接收方式 |
每接收1个字节数据就产生一次中断,通知CPU处理 |
可以连续接收最多16个字节数据,在缓冲区填满或达到特定条件时才产生中断 |
高速性能 |
在高于9600 baud的速率下,容易因CPU来不及处理而丢失数据 |
能可靠地支持高达115200 bps的速率 |
中断频率 |
高,每个字节都触发中断 |
低,可以批量处理数据,显著减轻CPU负担 |
软硬件兼容性 |
- |
完全向下兼容8250和16450 |
8250的瓶颈:8250是早期IBM PC及其兼容机使用的芯片,它内部只有一个单字节的接收缓冲器,这意味着每收到一个字节,就必须立即中断CPU来读取,否则下一个字节到来时,上一个字节就会被覆盖丢失。在低速(如9600 baud以下)时尚可应对,但当高速Modem出现后,其高频中断使CPU不堪重负。
16550的解决方案:16550正是为解决此问题而设计的,其核心的16字节FIFO缓冲区就像一个“蓄水池”。在高速通信时,数据可以先暂存在FIFO中,等攒够一批(比如14个字节)再一次性通知CPU来取。这极大地减少了中断次数,给CPU留出了处理其他任务的时间,从而保障了高速通信的稳定性和可靠性。
6.7.1.2. 16550标准寄存器集¶
16550通过3位地址线寻址8个寄存器,其中部分寄存器地址重叠,由 LCR 线控寄存器的最高位DLAB(Divisor Latch Access Bit)控制切换。当DLAB=1时,偏移0和1地址映射为波特率除数锁存器;DLAB=0时映射为数据收发与中断使能寄存器。
并且同一个偏移地址可通过“读/写操作方向”区分两个独立寄存器——对偏移0x02执行读操作访问中断标识寄存器IIR(只读),执行写操作访问FIFO控制寄存器FCR(只写)。
地址偏移 |
DLAB=0(读) |
DLAB=0(写) |
DLAB=1 |
寄存器全称 |
功能描述 |
|---|---|---|---|---|---|
0x00 |
RBR |
THR |
DLL |
接收缓冲/发送保持/ 除数锁存低字节 |
接收数据读取、发送数据写入、 波特率除数低8位 |
0x01 |
IER |
IER |
DLM |
中断使能/ 除数锁存高字节 |
中断类型开关、 波特率除数高8位 |
0x02 |
IIR |
FCR |
- |
中断标识/FIFO控制 |
中断优先级识别、FIFO 使能与清空 |
0x03 |
LCR |
LCR |
LCR |
线路控制寄存器 |
数据帧格式配置、DLAB 位控制 |
0x04 |
MCR |
MCR |
MCR |
调制解调器控制寄存器 |
流控信号输出、回环模式 |
0x05 |
LSR |
- |
LSR |
线路状态寄存器 |
收发状态、错误标志 |
0x06 |
MSR |
- |
MSR |
调制解调器状态寄存器 |
流控信号输入状态 |
0x07 |
SCR |
SCR |
SCR |
暂存寄存器 |
通用临时存储 |
线路状态寄存器LSR(偏移0x05,只读)
LSR是驱动最频繁访问的状态寄存器,提供收发路径的状态信息,是数据收发流程判断的依据。
位序号 |
位名称 |
含义 |
驱动中的作用 |
|---|---|---|---|
Bit0 |
DR |
接收数据就绪 |
1 表示 RBR 中有数据,可读取;0 表示无数据 |
Bit1 |
OE |
溢出错误 |
1 表示接收数据未及时读取导致覆盖丢失 |
Bit2 |
PE |
奇偶校验错误 |
1 表示接收到的数据校验位不匹配 |
Bit3 |
FE |
帧错误 |
1 表示未检测到正确的停止位 |
Bit4 |
BI |
间断检测 |
1 表示 RXD 线持续低电平超过一帧时间 |
Bit5 |
THRE |
发送保持寄存器空 |
1 表示 THR 已空,可写入下一字节 |
Bit6 |
TEMT |
发送器空 |
1 表示 THR 和 TSR 均为空,发送完全结束 |
Bit7 |
FIFOE |
FIFO错误 |
FIFO 模式下至少有一个错误数据在 FIFO 中 |
发送流程判断:发送数据前必须检查 LSR 的 Bit5(THRE),为1时才能写入发送保持寄存器 THR ;等待发送完全结束则检查Bit6(TEMT)。
接收流程判断:读取数据前必须检查 LSR 的 Bit0(DR),为1时才能读取接收缓冲寄存器 RBR ;同时检查Bit1~Bit4判断是否发生接收错误。
接收缓冲寄存器RBR(偏移0x00,DLAB=0,只读)
RBR 是串口数据接收的出口寄存器。当UART接收到一帧完整数据并完成校验后,硬件将数据字节移入 RBR ,同时将 LSR 寄存器的 DR 位置1,通知CPU有数据可读。CPU读取 RBR 后, DR 位自动清零。
数据来源:接收移位寄存器
RSR串行接收,串转并后移入RBR。读取时机:查询
LSR的DR位为 1 时读取,或接收中断触发后读取。FIFO模式:启用FIFO后,
RBR对应FIFO出口,每次读取弹出一个字节。
发送保持寄存器THR(偏移0x00,DLAB=0,只写)
THR 是串口数据发送的入口寄存器。CPU将待发送字节写入 THR 后,硬件将其移入发送移位寄存器 TSR ,逐位串行输出。当 THR 数据移入 TSR 后, LSR 的 THRE 位置1,表示可以写入下一个字节。
数据去向:
THR-> 发送移位寄存器TSR-> TXD引脚串行输出。写入时机:查询
LSR的THRE位为1时写入,或发送空中断触发后写入。FIFO模式:启用FIFO后,
THR对应FIFO入口,可连续写入多个字节。
波特率除数锁存器DLL/DLM(偏移0x00,DLAB=1,读写)
波特率由16位除数锁存器(DLL+DLM)控制,计算公式为:
1 | Divisor = 输入时钟频率 / (16 x 目标波特率)
|
标准16550输入时钟为1.8432MHz,因此:
波特率9600:除数 = 1843200 / (16 x 9600) = 12
波特率115200:除数 = 1843200 / (16 x 115200) = 1
波特率921600:除数 = 1843200 / (16 x 921600) = 0.125(不支持,需更高时钟)
配置步骤:
置位
LCR的Bit7(DLAB=1),切换寄存器映射。写入
DLL(偏移0)保存除数低8位。写入
DLM(偏移1)保存除数高8位。清除
LCR的Bit7(DLAB=0),恢复正常寄存器映射。
线路控制寄存器LCR(偏移0x03,读写)
LCR 控制串行数据帧的物理格式,是串口参数配置的核心寄存器。
位序号 |
位名称 |
含义 |
|---|---|---|
Bit1~Bit0 |
WLS |
字长选择:00 = 5位,01 = 6位,10 = 7位,11 = 8位 |
Bit2 |
STB |
停止位:0 = 1位停止位,1 = 1.5个停止位(5位数据)或2个停止位(6/7/8位数据) |
Bit3 |
PEN |
奇偶校验使能:1 = 启用校验位 |
Bit4 |
EPS |
偶校验选择:0 = 奇校验,1 = 偶校验(PEN=1时有效) |
Bit5 |
SP |
粘滞校验:强制校验位固定电平(PEN=1时有效) |
Bit6 |
SBC |
设置间断:1 = 强制TXD输出低电平 |
Bit7 |
DLAB |
除数锁存访问位:1 = 访问DLL/DLM,0 = 访问正常寄存器 |
Bit5~Bit3三位的有效组合规则如下:
xx0(Bit3 = 0):禁用奇偶校验,数据帧仅包含数据位 + 停止位。
001(Bit5=0、Bit4=0、Bit3=1):启用奇校验,数据位 + 校验位中逻辑 1 的总数为奇数。
011(Bit5=0、Bit4=1、Bit3=1):启用偶校验,数据位 + 校验位中逻辑 1 的总数为偶数。
调制解调器控制寄存器MCR(偏移0x04,读写)
控制UART向调制解调器输出的握手信号。
位序号 |
位名称 |
含义 |
|---|---|---|
Bit0 |
DTR |
数据终端就绪:控制 DTR 输出引脚 |
Bit1 |
RTS |
请求发送:控制 RTS 输出引脚 |
Bit2 |
OUT1 |
用户自定义输出 1 |
Bit3 |
OUT2 |
用户自定义输出 2,通常用于全局中断使能 |
Bit4 |
LOOP |
回环模式:1 = 启用内部回环自测 |
Bit5 |
- |
无意义 |
Bit6 |
- |
无意义 |
Bit7 |
- |
无意义 |
LOOP本地回环机制:
当LOOP=1时:芯片进入本地回环测试模式。此时,MCR的输出引脚(DTR, RTS, OUT1, OUT2)在芯片内部物理短接到
MSR的输入引脚(DSR, CTS, RI, DCD)。当LOOP=0时:芯片处于正常通信模式。
MSR反映的是外部物理引脚的真实电平,与MCR的输出没有内部连接关系。
调制解调器状态寄存器MSR(偏移0x06,只读)
读取调制解调器向UART输入的握手信号状态。
位序号 |
位名称 |
含义 |
|---|---|---|
Bit0 |
ΔCTS |
CTS 信号变化标志,读 MSR 后自动清零 |
Bit1 |
ΔDSR |
DSR 信号变化标志,读 MSR 后自动清零 |
Bit2 |
ΔRI |
RI 信号下降沿标志,读 MSR 后自动清零 |
Bit3 |
ΔDCD |
DCD 信号变化标志,读 MSR 后自动清零 |
Bit4 |
CTS |
清除发送输入状态 |
Bit5 |
DSR |
数据设备就绪输入状态 |
Bit6 |
RI |
振铃指示输入状态 |
Bit7 |
DCD |
载波检测输入状态 |
额外说明如下:
CTS(Clear To Send,发送允许):输入信号。由DCE(如Modem)发送给DTE(如计算机),用于告诉DTE:“我已准备好,你可以发送数据了”。它是实现硬件流控的关键信号,用于防止数据丢失。低电平有效。
DSR(Data Set Ready,数据设备就绪):输入信号。由DCE发送给DTE,用于告知DTE:“DCE设备(如Modem)已正常通电并准备就绪”。这是建立通信链路前的“自我介绍”。高电平表示就绪。
RI(Ring Indicator,振铃指示):输入信号。由DCE发送给DTE,用于通知DTE:“检测到振铃信号(有来电)”。它只是一个提醒信号,本身不直接影响数据传输。
DCD(Data Carrier Detect,载波检测):输入信号。由DCE发送给DTE,用于告知DTE:“已检测到对方的载波信号,通信链路已建立”。它帮助DTE判断通信链路是否正常。
在MSR寄存器中,Bit4-Bit7读取的是这些信号的实时状态,而Bit0-Bit3则是用于检测信号是否发生过变化的“变化标志”。
FIFO控制寄存器FCR(偏移0x02,只写)
16550相对前代8250的重要改进就是增加了16字节收发FIFO,降低中断频率,提升吞吐量。
位序号 |
位名称 |
含义 |
|---|---|---|
Bit0 |
FIFO_EN |
FIFO使能:1 = 启用收发FIFO |
Bit1 |
RCVR_RESET |
接收FIFO复位:写1清空接收FIFO,自动清零 |
Bit2 |
XMIT_RESET |
发送FIFO复位:写1清空发送FIFO,自动清零 |
Bit3 |
DMA_MODE |
DMA 模式选择 |
Bit7~Bit6 |
RX_TRIG |
接收触发阈值:00 = 1字节,01 = 4 字节,10 = 8 字节,11 = 14 字节 |
中断使能寄存器IER(偏移0x01,读写)
IER是UART中断系统的分项开关,每一位独立控制一类中断的使能与禁用,低4位有效,高4位保留。
位序号 |
缩写 |
名称 |
功能说明 |
|---|---|---|---|
Bit 0 |
ERBFI |
接收数据就绪中断使能 |
置1后,接收FIFO/寄存器达到触发阈值时触发接收中断,是串口接收数据最核心的中断 |
Bit 1 |
ETBEI |
发送保持寄存器空中断使能 |
置1后,发送FIFO/THR为空时触发中断,用于连续批量发送数据 |
Bit 2 |
ELSI |
接收线路状态中断使能 |
置1后,接收出现溢出错误、奇偶校验错、帧错误、间断检测等异常时触发中断,用于错误处理 |
Bit 3 |
EDSSI |
调制解调器状态中断使能 |
置1后,CTS/DSR/RI/DCD等Modem引脚状态变化时触发中断,用于硬件流控或Modem通信 |
Bit 7~4 |
- |
保留 |
固定写 0 |
当 LCR 寄存器DLAB位(bit7)置1时,偏移0x00和0x01会切换为除数锁存器DLL/DLM,此时无法访问IER;只有DLAB=0时,偏移0x01才对应IER寄存器。
中断标识寄存器IIR(偏移0x02,只读)
读取当前最高优先级待处理中断类型。
Bit0为中断挂起位:0表示有中断待处理,1表示无中断。
Bit1~3:中断类型编码。
Bit4~5:固定为0,Bit6~7:固定为1。
Bit3:1位中断类型编码如下表:
IIR 编码 |
中断优先级 |
中断类型 |
清除方式 |
|---|---|---|---|
011 |
最高(1 级) |
接收线路状态错误 |
读 LSR |
010 |
高(2 级) |
接收数据就绪 |
读 RBR 至低于阈值 |
110 |
高(2 级) |
字符接收超时 |
读 RBR |
001 |
低(3 级) |
发送保持寄存器空 |
写 THR 或读 IIR |
000 |
最低(4 级) |
调制解调器状态变化 |
读 MSR |
6.7.2. serial_reg.h宏定义¶
在Linux内核的串口驱动开发中,<linux/serial_reg.h>提供了所有标准16550兼容寄存器的偏移量、位掩码及扩展功能的宏定义,这些宏使得驱动代码具备良好的可读性。
源码文件位于内核源码/include/uapi/linux/serial_reg.h
以下是在虚拟16550驱动及大多数物理串口驱动中最常用的宏分组。
6.7.2.1. 寄存器偏移宏¶
这些宏定义了8个基本寄存器相对于UART基址的地址偏移,无论DLAB状态如何,偏移量固定:
宏名 |
偏移 |
访问方向 |
说明 |
|---|---|---|---|
UART_RX / UART_TX |
0x00 |
读/写 |
接收缓冲(读)或发送保持(写) |
UART_IER |
0x01 |
读写 |
中断使能寄存器(DLAB=0时) |
UART_DLL / UART_DLM |
0x00/0x01 |
读写 |
波特率除数低/高字节(DLAB=1时) |
UART_IIR / UART_FCR |
0x02 |
读/写 |
中断标识(读)或FIFO控制(写) |
UART_LCR |
0x03 |
读写 |
线路控制寄存器 |
UART_MCR |
0x04 |
读写 |
调制解调器控制寄存器 |
UART_LSR |
0x05 |
只读 |
线路状态寄存器 |
UART_MSR |
0x06 |
只读 |
调制解调器状态寄存器 |
UART_SCR |
0x07 |
读写 |
暂存寄存器 |
6.7.2.2. 常用位定义宏¶
中断使能寄存器IER
1 2 3 4 | #define UART_IER_MSI 0x08 // 启用调制解调器状态中断
#define UART_IER_RLSI 0x04 // 启用接收线路状态中断
#define UART_IER_THRI 0x02 // 启用发送保持寄存器空中断
#define UART_IER_RDI 0x01 // 启用接收数据就绪中断
|
FIFO控制寄存器FCR
1 2 3 4 5 6 7 8 9 10 | #define UART_FCR_ENABLE_FIFO 0x01 // 使能FIFO
#define UART_FCR_CLEAR_RCVR 0x02 // 清空接收FIFO
#define UART_FCR_CLEAR_XMIT 0x04 // 清空发送FIFO
#define UART_FCR_DMA_SELECT 0x08 // DMA模式选择
// 接收触发阈值
#define UART_FCR_R_TRIG_00 0x00 // 1字节
#define UART_FCR_R_TRIG_01 0x40 // 4字节
#define UART_FCR_R_TRIG_10 0x80 // 8字节
#define UART_FCR_R_TRIG_11 0xC0 // 14字节
|
线路控制寄存器LCR
1 2 3 4 5 6 7 8 9 10 | #define UART_LCR_DLAB 0x80 // 除数锁存访问位
#define UART_LCR_SBC 0x40 // 设置间断
#define UART_LCR_SPAR 0x20 // 粘滞校验
#define UART_LCR_EPAR 0x10 // 偶校验选择
#define UART_LCR_PARITY 0x08 // 校验使能
#define UART_LCR_STOP 0x04 // 停止位(0=1位,1=2位)
#define UART_LCR_WLEN5 0x00 // 5位数据
#define UART_LCR_WLEN6 0x01 // 6位数据
#define UART_LCR_WLEN7 0x02 // 7位数据
#define UART_LCR_WLEN8 0x03 // 8位数据
|
调制解调器控制寄存器MCR
1 2 3 4 5 6 | #define UART_MCR_DTR 0x01 // 数据终端就绪
#define UART_MCR_RTS 0x02 // 请求发送
#define UART_MCR_OUT1 0x04 // 用户自定义输出1
#define UART_MCR_OUT2 0x08 // 用户自定义输出2(常作为全局中断使能)
#define UART_MCR_LOOP 0x10 // 回环模式
#define UART_MCR_AFE 0x20 // 自动流控使能
|
线路状态寄存器LSR
1 2 3 4 5 6 7 8 | #define UART_LSR_DR 0x01 // 接收数据就绪
#define UART_LSR_OE 0x02 // 溢出错误
#define UART_LSR_PE 0x04 // 奇偶校验错误
#define UART_LSR_FE 0x08 // 帧错误
#define UART_LSR_BI 0x10 // 间断检测
#define UART_LSR_THRE 0x20 // 发送保持寄存器空
#define UART_LSR_TEMT 0x40 // 发送器空(THR和TSR均为空)
#define UART_LSR_FIFOE 0x80 // FIFO中存在错误
|
调制解调器状态寄存器MSR
1 2 3 4 5 6 7 8 9 | #define UART_MSR_DCTS 0x01 // ΔCTS (CTS变化)
#define UART_MSR_DDSR 0x02 // ΔDSR
#define UART_MSR_TERI 0x04 // ΔRI (RI下降沿)
#define UART_MSR_DDCD 0x08 // ΔDCD
#define UART_MSR_CTS 0x10 // 当前CTS输入
#define UART_MSR_DSR 0x20 // 当前DSR输入
#define UART_MSR_RI 0x40 // 当前RI输入
#define UART_MSR_DCD 0x80 // 当前DCD输入
#define UART_MSR_ANY_DELTA (UART_MSR_DDCD|UART_MSR_TERI|UART_MSR_DDSR|UART_MSR_DCTS) // 任何变化
|
6.7.3. 驱动代码详解¶
核心定义与数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | #define TTYTEST_NR_PORTS 1 /* 串口数量 */
#define TTYTEST_FIFO_SIZE 16 /* FIFO大小,字节 */
#define TTYTEST_RX_BUF_SIZE 512 /* 接收缓冲区大小 */
#define TTYTEST_TIMER_PERIOD (2 * HZ) /* 定时器周期,2秒 */
#define DEFAULT_RX_MSG "Virtual UART: Hello from 16550!\n" /* 默认接收消息 */
/* 驱动私有数据结构体 */
struct ttytest_port {
struct uart_port port; /* UART端口基础结构 */
spinlock_t lock; /* 自旋锁,保护寄存器访问 */
u8 regs[8]; /* 虚拟UART寄存器数组 */
u16 divisor; /* 波特率除数 */
u8 lcr; /* 线路控制寄存器缓存 */
struct timer_list rx_timer; /* 接收定时器,模拟数据接收 */
char rx_buf[TTYTEST_RX_BUF_SIZE]; /* 接收数据缓冲区 */
int rx_len; /* 接收数据长度 */
char tx_accum[TTYTEST_RX_BUF_SIZE]; /* 发送数据累积缓冲区 */
int tx_accum_len; /* 累积数据长度 */
unsigned int baudrate; /* 当前波特率 */
u8 data_bits; /* 当前数据位数 */
u8 parity; /* 当前校验方式 */
u8 stop_bits; /* 当前停止位数 */
};
|
关键说明:
第1行:配置驱动支持的物理串口数量,本实验仅模拟1个端口。
第2行:模拟16550标准16字节硬件FIFO深度。
第3行:软件接收缓冲区大小,用于存储待上报的模拟接收数据。
第4行:定时器触发周期,每2秒模拟一次数据接收上报。
第5行:初始默认接收消息,驱动加载后定时上报该字符串。
第10行:内嵌标准uart_port结构体,接入serial_core框架。
第13行:8字节寄存器数组,模拟16550的8个可寻址寄存器。
第5~6行:波特率除数与LCR缓存,软件侧保存配置值。
第17行:内核定时器,周期性模拟硬件接收数据。
第18~22行:收发软件缓冲区,模拟硬件FIFO功能。
第24~27行:缓存当前串口参数,供termios查询使用。
模块初始化与退出函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | /* UART驱动结构体 */
static struct uart_driver ttytest_driver = {
.owner = THIS_MODULE,
.driver_name = "tty_subsystem", /* 驱动名称 */
.dev_name = "ttyTEST", /* 设备接口名称 */
.major = 0, /* 主设备号,使用自动分配 */
.minor = 0, /* 次设备号起始值 */
.nr = TTYTEST_NR_PORTS, /* 串口数量 */
};
/* 模块初始化函数 */
static int __init ttytest_init(void)
{
/* 返回值 */
int ret;
/* 端口实例指针 */
struct ttytest_port *tp;
/* 注册UART驱动 */
ret = uart_register_driver(&ttytest_driver);
if (ret) {
pr_err("ttytest: uart_register_driver failed, ret=%d\n", ret);
return ret;
}
/* 分配端口实例内存 */
tp = kzalloc(sizeof(struct ttytest_port), GFP_KERNEL);
if (!tp) {
ret = -ENOMEM;
goto err_unreg_driver;
}
/* 保存全局端口实例指针 */
g_ttytest_port = tp;
/* 初始化自旋锁 */
spin_lock_init(&tp->lock);
/* 设置UART操作集指针 */
tp->port.ops = &ttytest_uart_ops;
/* 设置端口号为0 */
tp->port.line = 0;
/* 设置UART类型为16550A */
tp->port.type = PORT_16550A;
/* 设置IO类型为内存映射 */
tp->port.iotype = UPIO_MEM;
/* 设置UART时钟频率为1.8432MHz */
tp->port.uartclk = 1843200;
/* 设置FIFO大小为16字节 */
tp->port.fifosize = TTYTEST_FIFO_SIZE;
/* 设置端口标志 */
tp->port.flags = UPF_BOOT_AUTOCONF;
/* 初始化LSR线路状态寄存器:发送保持寄存器空、发送器空 */
tp->regs[UART_LSR] = UART_LSR_THRE | UART_LSR_TEMT;
/* 初始化MSR调制解调器状态寄存器:CTS、DSR、DCD有效 */
tp->regs[UART_MSR] = UART_MSR_CTS | UART_MSR_DSR | UART_MSR_DCD;
/* 初始化MCR调制解调器控制寄存器:DTR、RTS有效 */
tp->regs[UART_MCR] = UART_MCR_DTR | UART_MCR_RTS;
/* 初始化FIFO控制寄存器:使能FIFO、清除接收FIFO、清除发送FIFO、接收触发级别8字节 */
tp->regs[UART_FCR] = UART_FCR_ENABLE_FIFO |
UART_FCR_CLEAR_RCVR |
UART_FCR_CLEAR_XMIT |
UART_FCR_R_TRIG_10;
/* 初始化LCR线路控制寄存器:8位数据 */
tp->regs[UART_LCR] = UART_LCR_WLEN8;
/* 保存LCR值到缓存 */
tp->lcr = UART_LCR_WLEN8;
/* 初始化接收缓冲区为默认消息 */
strscpy(tp->rx_buf, DEFAULT_RX_MSG, TTYTEST_RX_BUF_SIZE);
/* 设置接收数据长度 */
tp->rx_len = strlen(tp->rx_buf);
/* 初始化定时器 */
timer_setup(&tp->rx_timer, ttytest_rx_timer, 0);
/* 添加UART端口 */
ret = uart_add_one_port(&ttytest_driver, &tp->port);
if (ret) {
pr_err("ttytest: uart_add_one_port failed, ret=%d\n", ret);
goto err_free_port;
}
/* 打印成功信息 */
pr_info("ttytest: virtual 16550 UART driver loaded\n");
pr_info("ttytest: device node: /dev/%s0\n", TTYTEST_DEV_NAME);
return 0;
/* 错误处理:释放端口内存 */
err_free_port:
kfree(tp);
/* 错误处理:注销驱动 */
err_unreg_driver:
uart_unregister_driver(&ttytest_driver);
return ret;
}
/* 模块退出函数 */
static void __exit ttytest_exit(void)
{
/* 获取全局端口实例指针 */
struct ttytest_port *tp = g_ttytest_port;
/* 检查指针是否为空 */
if (!tp)
return;
/* 移除UART端口 */
uart_remove_one_port(&ttytest_driver, &tp->port);
/* 注销UART驱动 */
uart_unregister_driver(&ttytest_driver);
/* 释放端口内存 */
kfree(tp);
/* 打印卸载信息 */
pr_info("ttytest: driver unloaded\n");
}
module_init(ttytest_init);
module_exit(ttytest_exit);
|
第20行:注册UART驱动到serial_core框架,分配主设备号。
第40~52行:初始化uart_port核心参数,声明为16550A型,时钟1.8432MHz。
第55行:初始化LSR寄存器,发送保持寄存器和发送器均为空。
第57行:初始化MSR寄存器,CTS、DSR、DCD信号均有效。
第32行:初始化MCR寄存器,DTR和RTS输出有效。
第61~64行:初始化FCR寄存器,启用FIFO、清空收发FIFO、8字节触发阈值。
第66行:初始化LCR寄存器,默认8位数据位。
第71行:初始化接收缓冲区为默认消息字符串。
第77行:初始化接收定时器,绑定回调函数。
第80行:添加端口到驱动,创建设备节点/dev/ttyTEST0。
第114行:移除端口,销毁设备节点。
第117行:注销UART驱动,释放主设备号。
第120行:释放端口实例内存。
UART操作集
uart_ops结构体是serial_core框架的硬件抽象层,每个回调对应一类硬件操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /* UART操作集 */
static const struct uart_ops ttytest_uart_ops = {
.tx_empty = ttytest_tx_empty, /* 检查发送器是否为空 */
.set_mctrl = ttytest_set_mctrl, /* 设置调制解调器控制信号 */
.get_mctrl = ttytest_get_mctrl, /* 获取调制解调器控制信号 */
.stop_tx = ttytest_stop_tx, /* 停止发送 */
.start_tx = ttytest_start_tx, /* 开始发送 */
.stop_rx = ttytest_stop_rx, /* 停止接收 */
.start_rx = ttytest_start_rx, /* 开始接收 */
.break_ctl = ttytest_break_ctl, /* 控制中断信号 */
.set_termios = ttytest_set_termios, /* 设置终端属性 */
.ioctl = ttytest_ioctl, /* 处理IO控制命令 */
.startup = ttytest_startup, /* 启动UART端口 */
.shutdown = ttytest_shutdown, /* 关闭UART端口 */
};
|
接收定时器回调函数
模拟硬件接收数据机制,定时器周期性触发,将rx_buf中的数据通过TTY翻转缓冲区上报给用户空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | /* 定时器回调:模拟接收数据 */
static void ttytest_rx_timer(struct timer_list *t)
{
/* 从定时器结构获取ttytest_port指针 */
struct ttytest_port *tp = from_timer(tp, t, rx_timer);
/* 获取UART端口指针 */
struct uart_port *up = &tp->port;
/* 保存中断状态的标志变量 */
unsigned long flags;
/* 循环计数器 */
int i;
/* 保存中断状态并获取自旋锁 */
spin_lock_irqsave(&tp->lock, flags);
/* 遍历接收缓冲区中的所有字符 */
for (i = 0; i < tp->rx_len; i++) {
/* 将字符插入到TTY翻转缓冲区 */
/* 参数: UART端口, LSR状态, 错误掩码, 字符, TTY标志 */
uart_insert_char(up, UART_LSR_DR, 0, tp->rx_buf[i], TTY_NORMAL);
}
/* 释放自旋锁并恢复中断状态 */
spin_unlock_irqrestore(&tp->lock, flags);
/* 将翻转缓冲区的数据推送到TTY子系统 */
tty_flip_buffer_push(&up->state->port);
/* 重新设置定时器,2秒后再次触发 */
mod_timer(&tp->rx_timer, jiffies + TTYTEST_TIMER_PERIOD);
}
|
关键说明:
第5行:通过from_timer从定时器指针反查私有结构体。
第14行:加锁保护寄存器与缓冲区,防止并发访问。
第17~21行:遍历接收缓冲区,逐字符调用uart_insert_char插入翻转缓冲区。
第27行:推送翻转缓冲区数据到TTY层,用户空间read即可获取。
第30行:重新设置定时器,实现周期性循环触发。
发送器状态检查回调
返回发送器是否完全空闲,对应硬件LSR寄存器的TEMT位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /* UART操作回调:检查发送器是否为空 */
static unsigned int ttytest_tx_empty(struct uart_port *port)
{
/* 获取ttytest_port指针 */
struct ttytest_port *tp = to_ttytest_port(port);
/* 保存中断状态的标志变量 */
unsigned long flags;
/* 返回值 */
unsigned int ret;
/* 保存中断状态并获取自旋锁 */
spin_lock_irqsave(&tp->lock, flags);
/* 检查LSR线路状态寄存器的TEMT位(Bit6)判断发送器是否完全空 */
ret = (tp->regs[UART_LSR] & UART_LSR_TEMT) ? TIOCSER_TEMT : 0;
/* 释放自旋锁并恢复中断状态 */
spin_unlock_irqrestore(&tp->lock, flags);
return ret;
}
|
关键说明:
第7行:读取虚拟LSR寄存器的Bit6(TEMT)位,TEMT为1表示THR和TSR均为空,发送完全结束。
调制解调器控制回调
设置MCR寄存器输出信号,并严格根据LOOP位同步更新MSR寄存器的对应输入状态。 在回环模式下模拟芯片内部引脚短接,在正常模式下模拟外部设备就绪。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | /*
* ttytest_set_mctrl - 设置调制解调器控制信号
* @port: UART端口指针
* @mctrl: 控制信号位图(TIOCM_DTR/TIOCM_RTS等)
*/
static void ttytest_set_mctrl(struct uart_port *port, unsigned int mctrl)
{
/* 获取ttytest_port指针 */
struct ttytest_port *tp = to_ttytest_port(port);
/* 保存中断状态的标志变量 */
unsigned long flags;
/* 调制解调器控制寄存器值 */
u8 mcr = 0;
/* 保存中断状态并获取自旋锁 */
spin_lock_irqsave(&tp->lock, flags);
/* 将TIOCM控制信号转换为MCR调制解调器控制寄存器位 */
if (mctrl & TIOCM_DTR) mcr |= UART_MCR_DTR; /* 数据就绪 */
if (mctrl & TIOCM_RTS) mcr |= UART_MCR_RTS; /* 请求发送 */
if (mctrl & TIOCM_OUT1) mcr |= UART_MCR_OUT1; /* 输出1 */
if (mctrl & TIOCM_OUT2) mcr |= UART_MCR_OUT2; /* 输出2 */
if (mctrl & TIOCM_LOOP) mcr |= UART_MCR_LOOP; /* 回环模式 */
/* 更新MCR调制解调器控制寄存器 */
tp->regs[UART_MCR] = mcr;
/* 检查是否处于回环模式 (LOOP位) */
if (mcr & UART_MCR_LOOP) {
/* 回环模式:MCR输出直接反馈到MSR输入 */
/* 必须先清除MSR中的相关状态位,防止状态残留 */
tp->regs[UART_MSR] &= ~(UART_MSR_CTS | UART_MSR_DSR | UART_MSR_RI | UART_MSR_DCD);
/* 16550硬件规范:在本地回环模式下,MCR的输出引脚在芯片内部直接短接到MSR的输入引脚 */
if (mcr & UART_MCR_RTS) tp->regs[UART_MSR] |= UART_MSR_CTS; /* RTS(请求发送) 环回到 CTS(允许发送) */
if (mcr & UART_MCR_DTR) tp->regs[UART_MSR] |= UART_MSR_DSR; /* DTR(终端就绪) 环回到 DSR(设备就绪) */
if (mcr & UART_MCR_OUT1) tp->regs[UART_MSR] |= UART_MSR_RI; /* OUT1(输出1) 环回到 RI(振铃指示) */
if (mcr & UART_MCR_OUT2) tp->regs[UART_MSR] |= UART_MSR_DCD; /* OUT2(输出2) 环回到 DCD(载波检测) */
} else {
/* 非回环模式:MSR反映外部状态。
* 在虚拟驱动中,通常模拟一个“外部设备已连接且就绪”的状态,或者直接清零。
* 这里模拟CTS/DSR/DCD始终有效,以保证正常通信,防止硬件流控死锁 */
tp->regs[UART_MSR] = UART_MSR_CTS | UART_MSR_DSR | UART_MSR_DCD;
/* RI振铃指示通常保持无效 */
tp->regs[UART_MSR] &= ~UART_MSR_RI;
}
/* 释放自旋锁并恢复中断状态 */
spin_unlock_irqrestore(&tp->lock, flags);
}
|
关键说明:
第19~23行:将TTY层的TIOCM标志转换为16550 MCR寄存器位,其中TIOCM_LOOP标志是区分工作模式的关键。
第29~38行:回环模式:当LOOP位置1时,模拟16550硬件的内部回环机制。将MCR的4个输出位(RTS/DTR/OUT1/OUT2)分别映射到MSR的4个输入位(CTS/DSR/RI/DCD),确保标准串口回环测试工具能正确验证所有引脚状态。
第39~46行:正常模式:当LOOP位为0时,断开内部映射。由于是虚拟设备没有真实外部对端,此处主动将MSR的CTS/DSR/DCD置位,模拟“外部设备已就绪”。这能有效防止上层应用开启硬件流控(如stty crtscts)时,因等待CTS信号而导致发送队列死锁。
调制解调器状态读取回调
读取MSR寄存器,转换为TTY层标准标志位返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | /*
* ttytest_get_mctrl - 获取调制解调器控制信号状态
* @port: UART端口指针
*
* 返回: 当前调制解调器控制信号状态
*/
static unsigned int ttytest_get_mctrl(struct uart_port *port)
{
/* 获取ttytest_port指针 */
struct ttytest_port *tp = to_ttytest_port(port);
/* 保存中断状态的标志变量 */
unsigned long flags;
/* 返回的控制信号状态 */
unsigned int mctrl = 0;
/* 调制解调器状态寄存器值 */
u8 msr;
/* 保存中断状态并获取自旋锁 */
spin_lock_irqsave(&tp->lock, flags);
/* 读取MSR调制解调器状态寄存器 */
msr = tp->regs[UART_MSR];
/* 将MSR位转换为TIOCM控制信号 */
if (msr & UART_MSR_CTS) mctrl |= TIOCM_CTS; /* 清除发送 */
if (msr & UART_MSR_DSR) mctrl |= TIOCM_DSR; /* 数据就绪 */
if (msr & UART_MSR_RI) mctrl |= TIOCM_RI; /* 振铃指示 */
if (msr & UART_MSR_DCD) mctrl |= TIOCM_CD; /* 载波检测 */
/* 清除MSR中的delta标志位 */
tp->regs[UART_MSR] &= ~(UART_MSR_DDCD | UART_MSR_TERI |
UART_MSR_DDSR | UART_MSR_DCTS);
/* 释放自旋锁并恢复中断状态 */
spin_unlock_irqrestore(&tp->lock, flags);
/* 返回结果 */
return mctrl;
}
|
关键说明:
第25~28行:将MSR寄存器的状态位映射为TTY标准TIOCM标志。
第31~32行:读取后清零MSR的delta变化标志位。
发送启动回调
serial_core框架调用此函数启动发送,从发送环形缓冲区读取数据,模拟写入THR寄存器的过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | /*
* ttytest_start_tx - 开始发送
* @port: UART端口指针
*
* 从UART发送环形缓冲区读取数据,累积到tx_accum中。
* 当收到换行符时,比较累积内容与当前rx_buf,如果不同则更新rx_buf为新内容。
*/
static void ttytest_start_tx(struct uart_port *port)
{
/* 获取ttytest_port指针 */
struct ttytest_port *tp = to_ttytest_port(port);
/* 获取UART发送环形缓冲区指针 */
struct circ_buf *xmit = &port->state->xmit;
/* 保存中断状态的标志变量 */
unsigned long flags;
/* 当前读取的字符 */
unsigned char ch;
/* 保存中断状态并获取自旋锁 */
spin_lock_irqsave(&tp->lock, flags);
/* 循环读取发送缓冲区中的数据 */
while (!uart_circ_empty(xmit) && tp->tx_accum_len < TTYTEST_RX_BUF_SIZE - 1) {
/* 从发送缓冲区读取一个字符 */
ch = xmit->buf[xmit->tail];
/* 移动发送缓冲区尾指针 */
xmit->tail = (xmit->tail + 1) & (UART_XMIT_SIZE - 1);
/* 增加发送计数 */
port->icount.tx++;
/* 跳过回车字符'\r',避免规范模式下ONLCR转换导致的内容污染 */
if (ch == '\r')
continue;
/* 将字符累积到tx_accum缓冲区 */
tp->tx_accum[tp->tx_accum_len++] = ch;
/* 检查是否收到换行符 */
if (ch == '\n') {
/* 添加字符串结束符 */
tp->tx_accum[tp->tx_accum_len] = '\0';
/* 比较累积内容与当前rx_buf */
/* 如果内容不同,更新rx_buf为新内容 */
if (strcmp(tp->tx_accum, tp->rx_buf) != 0) {
memcpy(tp->rx_buf, tp->tx_accum, tp->tx_accum_len + 1);
tp->rx_len = tp->tx_accum_len;
}
/* 重置累积缓冲区长度 */
tp->tx_accum_len = 0;
}
}
/* 设置LSR线路状态寄存器的THRE(Bit5)和TEMT位(Bit6),表示发送器为空 */
tp->regs[UART_LSR] |= UART_LSR_THRE | UART_LSR_TEMT;
/* 释放自旋锁并恢复中断状态 */
spin_unlock_irqrestore(&tp->lock, flags);
/* 唤醒等待写入的进程 */
uart_write_wakeup(port);
}
|
关键说明:
第13行:获取serial_core的发送环形缓冲区。
第23行:循环读取发送缓冲区数据,直到为空或累积缓冲区满。
第32~33行:跳过回车符,避免规范模式下ONLCR转换污染数据。
第36~51行:遇到换行符时,将累积的整行内容更新到接收缓冲区,实现自发自收闭环。
第56行:置位LSR寄存器的THRE和TEMT位,模拟发送完成。
第62行:唤醒等待发送的上层进程。
控制Break信号回调
操作16550 LCR寄存器Bit6(SBC),置1时强制TXD持续输出低电平,产生串口Break信号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | /*
* ttytest_break_ctl - 控制Break信号
* @port: UART端口指针
* @break_state: Break状态(非零表示开启Break,零表示关闭Break)
*/
static void ttytest_break_ctl(struct uart_port *port, int break_state)
{
/* 获取ttytest_port指针 */
struct ttytest_port *tp = to_ttytest_port(port);
/* 保存中断状态的标志变量 */
unsigned long flags;
/* 保存中断状态并获取自旋锁 */
spin_lock_irqsave(&tp->lock, flags);
/* 根据break_state设置或清除LCR线路控制寄存器的SBC位(Bit6) */
if (break_state)
tp->lcr |= UART_LCR_SBC; /* 开启Break */
else
tp->lcr &= ~UART_LCR_SBC; /* 关闭Break */
/* 更新LCR线路控制寄存器 */
tp->regs[UART_LCR] = tp->lcr;
/* 释放自旋锁并恢复中断状态 */
spin_unlock_irqrestore(&tp->lock, flags);
}
|
关键说明:
第17行:用户空间应用调用TIOCSBRK ioctl:开启Break,传入break_state=1;用户空间应用调用TIOCCBRK ioctl:关闭Break,传入break_state=0。
第18行:置Bit6为1,开启Break信号:强制TXD持续输出低电平,产生串口断开信号。
第20行:清空Bit6,关闭Break信号:TXD恢复串口默认空闲高电平,正常收发数据。
第23行:同步写入虚拟寄存器数组regs,更新模拟硬件寄存器状态。
终端属性设置回调
响应用户空间的termios设置,解析波特率、数据位、停止位、校验位参数,更新虚拟LCR寄存器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | /*
* ttytest_set_termios - 设置终端属性
* @port: UART端口指针
* @termios: 新的终端属性
* @old: 旧的终端属性
*
* 设置波特率、数据位、校验位、停止位等参数
*/
static void ttytest_set_termios(struct uart_port *port, struct ktermios *termios,
const struct ktermios *old)
{
/* 获取ttytest_port指针 */
struct ttytest_port *tp = to_ttytest_port(port);
/* 保存中断状态的标志变量 */
unsigned long flags;
/* 计算后的波特率 */
unsigned int baud;
/* 线路控制寄存器值 */
u8 lcr = 0;
/* 获取波特率,范围300-4000000 */
baud = uart_get_baud_rate(port, termios, old, 300, 4000000);
/* 保存中断状态并获取自旋锁 */
spin_lock_irqsave(&tp->lock, flags);
/* 保存当前波特率 */
tp->baudrate = baud;
/* 根据termios设置LCR线路控制寄存器数据位数(Bit1和Bit0) */
switch (termios->c_cflag & CSIZE) {
case CS5: lcr |= UART_LCR_WLEN5; tp->data_bits = 5; break; /* 5位数据 */
case CS6: lcr |= UART_LCR_WLEN6; tp->data_bits = 6; break; /* 6位数据 */
case CS7: lcr |= UART_LCR_WLEN7; tp->data_bits = 7; break; /* 7位数据 */
case CS8:
default: lcr |= UART_LCR_WLEN8; tp->data_bits = 8; break; /* 8位数据 */
}
/* 设置停止位数(Bit2) */
if (termios->c_cflag & CSTOPB) {
lcr |= UART_LCR_STOP; /* 设置停止位为2位 */
tp->stop_bits = 2;
} else {
tp->stop_bits = 1; /* 默认1位停止位 */
}
/* 设置校验方式(Bit5~Bit3) */
tp->parity = 0; /* 默认无校验 */
if (termios->c_cflag & PARENB) {
lcr |= UART_LCR_PARITY; /* 使能校验 */
if (termios->c_cflag & PARODD) {
tp->parity = 1; /* 奇校验 */
} else {
lcr |= UART_LCR_EPAR; /* 偶校验 */
tp->parity = 2;
}
}
/* 计算波特率除数:Divisior=输入时钟频率/(16×所需波特率) */
if (baud > 0) {
tp->divisor = 1843200 / (16 * baud);
} else {
tp->divisor = 0;
}
/* 保存LCR基础值 */
tp->lcr = lcr;
/* 设置LCR的DLAB位(Bit7)为1,将偏移0和1切换为除数锁存器 */
tp->regs[UART_LCR] = lcr | UART_LCR_DLAB;
/* 将波特率除数分别写入DLL(偏移0)和DLM(偏移1) */
tp->regs[UART_DLL] = tp->divisor & 0xff; /* 写入除数低 8 位 */
tp->regs[UART_DLM] = (tp->divisor >> 8) & 0xff; /* 写入除数高 8 位 */
/* 清除DLAB位,将偏移0和1恢复为数据收发(RBR/THR)与中断使能(IER)寄存器,并写入最终的LCR配置 */
tp->regs[UART_LCR] = lcr;
/* 释放自旋锁并恢复中断状态 */
spin_unlock_irqrestore(&tp->lock, flags);
/* 更新超时参数 */
uart_update_timeout(port, termios->c_cflag, baud);
}
|
关键说明:
第22行:从termios结构解析目标波特率,限制范围300~4000000。
第31~37行:根据CSIZE标志设置数据位宽,对应LCR的WLS位。
第40~45行:根据CSTOPB标志设置停止位数,对应LCR的STB位。
第48~57行:根据PARENB和PARODD标志设置校验方式,对应LCR的PEN和EPS位。
第60~64行:按标准公式计算波特率除数,更新divisor缓存。
第67-77行:先设置LCR的DLAB位(Bit7)为1,切换为除数锁存器,写入波特率除数,写入完成后清除LCR的DLAB位,偏移0和1恢复为数据收发(RBR/THR)与中断使能(IER)寄存器。
第83行:更新串口超时参数,供serial_core框架使用。
端口启动与关闭回调
端口打开时初始化资源并启动接收定时器,关闭时删除定时器并清理状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | /*
* ttytest_startup - 启动UART端口
* @port: UART端口指针
*/
static int ttytest_startup(struct uart_port *port)
{
/* 获取ttytest_port指针 */
struct ttytest_port *tp = to_ttytest_port(port);
/* 保存中断状态的标志变量 */
unsigned long flags;
/* 保存中断状态并获取自旋锁 */
spin_lock_irqsave(&tp->lock, flags);
/* 重置发送累积缓冲区长度 */
tp->tx_accum_len = 0;
/* 释放自旋锁并恢复中断状态 */
spin_unlock_irqrestore(&tp->lock, flags);
/* 设置接收定时器,2秒后触发 */
mod_timer(&tp->rx_timer, jiffies + TTYTEST_TIMER_PERIOD);
return 0;
}
/*
* ttytest_shutdown - 关闭UART端口
* @port: UART端口指针
*/
static void ttytest_shutdown(struct uart_port *port)
{
/* 获取ttytest_port指针 */
struct ttytest_port *tp = to_ttytest_port(port);
/* 删除定时器并等待其完成 */
del_timer_sync(&tp->rx_timer);
}
|
关键说明:
第22行:首次打开设备时启动接收定时器,开始周期性模拟数据上报。
第37行:关闭设备时同步删除定时器,确保回调不再执行。
6.7.4. Makefile说明¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #指定内核路径,可以是相对路径或绝对路径
KERNEL_DIR=../../kernel/
# KERNEL_DIR=/home/guest/LubanCat_Linux_Generic_Full_SDK/kernel-6.1
# KERNEL_DIR=/home/guest/LubanCat_Linux_rk3588_SDK/kernel
#指定目标架构为arm64
ARCH=arm64
#指定交叉编译工具链的前缀
CROSS_COMPILE=aarch64-linux-gnu-
# CROSS_COMPILE=/home/guest/LubanCat_Linux_Generic_Full_SDK/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
#导出为环境变量
export ARCH CROSS_COMPILE
#指定要编译的内核模块目标文件
obj-m := tty_subsystem.o
#all :默认目标,执行时会编译驱动模块
#$(MAKE) :调用make工具
#-C $(KERNEL_DIR) :指定的内核源码目录
#M=$(CURDIR) :模块的源码位于当前目录
#modules :编译模块
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
.PHONE:clean
#清理编译生成的文件
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
|
6.7.5. 编译驱动¶
在实验目录下输入 make 即可编译驱动,编译得到内核模块tty_subsystem.ko。
6.7.6. 程序运行结果¶
如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。
6.7.6.1. 实验操作¶
使用以下命令加载驱动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #加载驱动
sudo insmod tty_subsystem.ko
#信息输出如下
[ 26.546032] ttytest: virtual 16550 UART driver loaded
[ 26.546068] ttytest: device node: /dev/TEST0
#查看设备节点
ls -l /dev/ttyTEST*
#信息输出如下
crw-rw---- 1 root dialout 236, 0 Jul 14 17:18 /dev/ttyTEST0
#查看通信参数
stty -F /dev/ttyTEST0
#信息输出如下
speed 9600 baud; line = 0;
-brkint -imaxbel
|
可以看到成功注册/dev/ttyTEST0,主设备号236由内核动态分配,次设备号0,波特率9600,line = 0表示终端行规程编号为0,-brkint表示关闭BREAK中断信号产生功能,-imaxbel表示关闭输入缓冲区满时的振铃提示功能。
驱动每2秒自动上报一次接收缓冲区内容,可以使用cat命令读取设备节点观察数据:
1 2 3 4 5 6 7 | #读取虚拟串口数据
cat /dev/ttyTEST0
#信息输出如下,每2秒打印一次
Virtual UART: Hello from 16550!
Virtual UART: Hello from 16550!
Virtual UART: Hello from 16550!
|
向串口写入数据后,内容会被更新到接收缓冲区,后续定时上报将返回新内容:
1 2 3 4 5 6 7 8 9 10 | #向串口写入一行数据
sudo sh -c "echo Hello TTY Subsystem! > /dev/ttyTEST0"
#再次读取观察数据变化
cat /dev/ttyTEST0
#信息输出如下,每2秒打印一次
Hello TTY Subsystem!
Hello TTY Subsystem!
Hello TTY Subsystem!
|
修改波特率、数据位、停止位测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | #查看当前所有参数
stty -F /dev/ttyTEST0 -a
#信息输出如下
speed 9600 baud; rows 0; columns 0; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>;
eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R;
werase = ^W; lnext = ^V; discard = ^O; min = 1; time = 0;
-parenb -parodd -cmspar cs8 hupcl -cstopb cread clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff
-iuclc -ixany -imaxbel -iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt
echoctl echoke -flusho -extproc
#修改波特率115200
stty -F /dev/ttyTEST0 115200
#修改数据位,cs5~cs8
stty -F /dev/ttyTEST0 cs7
#修改停止位
#设置2位停止位
stty -F /dev/ttyTEST0 cstopb
###恢复为1位停止位
###stty -F /dev/ttyTEST0 -cstopb
#修改奇偶校验
#奇校验
stty -F /dev/ttyTEST0 parenb parodd
#偶校验
stty -F /dev/ttyTEST0 parenb -parodd
#无校验
stty -F /dev/ttyTEST0 -parenb
#组合设置一条命令,115200 7E2(7位数据,偶校验,2停止位)
stty -F /dev/ttyTEST0 115200 cs7 cstopb parenb -parodd
#再查看当前所有参数
stty -F /dev/ttyTEST0 -a
#信息输出如下
speed 115200 baud; rows 0; columns 0; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>;
eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R;
werase = ^W; lnext = ^V; discard = ^O; min = 1; time = 0;
parenb -parodd -cmspar cs7 hupcl cstopb cread clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff
-iuclc -ixany -imaxbel -iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt
echoctl echoke -flusho -extproc
|
修改参数后再看当前所有参数可以看到输出值和配置的一样。
6.7.7. 实验注意事项¶
定时器与并发安全:接收定时器在软中断上下文中运行,所有访问共享数据的操作均需使用spin_lock_irqsave保护,防止与进程上下文产生竞态条件。
数据发送与换行符处理:驱动通过检测换行符
\n来触发接收缓冲区更新,因此写入的数据必须以换行符结尾,否则内容不会被更新。写操作时\r被自动跳过,避免规范模式下ONLCR转换引入多余字符,导致字符串比较失败。硬件流控与虚拟状态: 默认非回环模式下,驱动强制将
MSR的CTS、DSR、DCD置为有效状态,确保上层应用开启硬件流控时不会因等待信号而阻塞发送。波特率与时钟频率:驱动固定使用1.8432MHz输入时钟,波特率计算公式为
divisor = 1843200 / (16 * baud),尝试设置超出支持范围的波特率(如921600)将导致除数为0。