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.1.2. TTY设备分类

  • 物理TTY设备:对应真实UART硬件串口,设备节点为/dev/ttyS0、/dev/ttyUSB0等,用于串口通信、外设数据传输,是本文驱动开发的核心对象。

  • 虚拟控制台TTY:设备节点/dev/tty1~tty63,对应系统本地终端,可通过Ctrl+Alt+Fn切换,用于本地人机交互。

  • 伪终端PTY设备:分为主设备/dev/ptmx和从设备/dev/pts/*,用于远程SSH、图形终端、程序管道交互,无硬件依赖。

6.2. UART基础知识

UART是异步串行全双工通信协议,无需同步时钟线,仅通过TX(发送)、RX(接收)两根数据线即可完成双向通信。 通信双方必须提前约定波特率、数据位、校验位、停止位参数,参数完全一致才能正常解析数据,数据以帧为单位逐位串行传输。

6.2.1. 数据帧格式

标准UART帧结构(从低位到高位传输):起始位 + 数据位 + 校验位(可选) + 停止位

../_images/subsystem_tty_subsystem_0.jpg
  • 起始位:固定1位低电平,标志一帧数据开始,用于同步收发时序。

  • 数据位:5/6/7/8位,常规外设默认8位,传输有效数据。

  • 校验位:可选,无校验/奇校验/偶校验,用于数据纠错。

  • 停止位:1位/2位高电平,标志一帧数据传输结束。

6.2.2. 波特率

波特率定义:每秒传输的码元个数,代表串口传输速率,单位Baud。常见标准波特率:300、9600、19200、38400、115200。

内核波特率计算公式:波特率除数 = 总线时钟 / (16 * 目标波特率)

6.2.3. 流控机制

流控用于防止数据丢失,确保发送端和接收端的数据传输速率匹配。流控分为两种类型:

  • 硬件流控

../_images/subsystem_tty_subsystem_1.jpg

使用RTS(Request To Send,发送请求)和CTS(Clear To Send,发送允许)两个信号线:

  • RTS:输出信号,由本设备的接收缓冲区状态控制,本质是“接收就绪指示”,用来告诉对端“我能不能接收数据”。

  • CTS:输入信号,由本设备的发送逻辑检测,本质是“发送允许判断”,用来确认“我能不能给对端发数据”。

当接收方缓冲区满时,会拉高RTS信号(无效状态)通知发送方停止发送;当缓冲区有空间时,拉低RTS信号(有效状态)允许继续发送。

  • 软件流控

通过发送特殊字符(XON/XOFF)来控制数据流:

  • XON(0x11):通知对端继续发送。

  • XOFF(0x13):通知对端暂停发送。

当接收方无法继续接收时,发送XOFF字符停止传输;当可以继续接收时,发送XON字符恢复传输。

6.3. TTY子系统整体架构

../_images/subsystem_tty_subsystem_2.jpg
  • 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)用于描述驱动全局信息、设备数量、设备名称,是模块注册的核心结构体。

uart_driver结构体(内核源码/include/linux/serial_core.h)
 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硬件端口的所有硬件参数、状态、操作接口,是串口硬件资源的核心载体。

uart_port结构体(内核源码/include/linux/serial_core.h)
 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核心层通过该结构体调用底层硬件逻辑。

uart_ops结构体(内核源码/include/linux/serial_core.h)
 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,支撑串口异步收发与资源安全释放。

uart_state结构体(内核源码/include/linux/serial_core.h)
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驱动向内核注册的载体。

tty_driver结构体(内核源码/include/linux/tty_driver.h)
 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驱动与内核核心层之间的交互入口。

tty_operations结构体(内核源码/include/linux/tty_driver.h)
 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)保存串口终端所有配置参数,包括波特率、数据位、校验位、停止位、流控、字符转换规则等。

ktermios结构体(内核源码/include/uapi/asm-generic/termbits.h)
 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.5.8. 推送TTY缓冲区数据

6.5.8.1. tty_flip_buffer_push函数

tty_flip_buffer_push函数用于将TTY翻转缓冲区中缓存的接收数据,正式提交给线路规程与TTY核心层,最终推送至用户态read接口,完成一次数据接收闭环。

函数原型:

1
int tty_flip_buffer_push(struct tty_port *port);

参数说明:

  • port:TTY端口状态结构体指针。

返回值:推送成功返回0,失败返回负错误码。

6.6. 内核TTY子系统源码解析

内核TTY子系统源码相关位于: 内核源码/drivers/tty/

查看瑞芯微UART相关设备树可知,瑞芯微UART使用的是8250串口通用驱动,位于: 内核源码/drivers/tty/serial/8250/

6.6.1. 设备注册流程

../_images/subsystem_tty_subsystem_3.jpg

6.6.1.1. UART硬件驱动层(8250)

UART硬件驱动层(8250):最底层的硬件驱动层,负责具体8250/16550芯片的初始化和端口注册。

serial8250_init()是8250驱动的模块初始化入口,初始化ISA端口,调用uart_register_driver注册驱动,并注册所有静态配置的端口。

serial8250_init(内核源码/drivers/tty/serial/8250/8250_core.c)
 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将其加入核心层。

serial8250_register_8250_port(内核源码/drivers/tty/serial/8250/8250_core.c)
 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核心层。

uart_register_driver(内核源码/drivers/tty/serial/serial_core.c)
 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完成设备节点的注册。

uart_add_one_port(内核源码/drivers/tty/serial/serial_core.c)
 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模块)。

tty_port_register_device_attr_serdev(内核源码/drivers/tty/tty_port.c)
 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)绑定在一起。

tty_port_link_device(内核源码/drivers/tty/tty_port.c)
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驱动到内核核心,分配设备号,并将其加入全局驱动链表。

tty_register_driver(内核源码/drivers/tty/tty_io.c)
 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)注册一个具体的设备节点。

tty_register_device_attr(内核源码/drivers/tty/tty_io.c)
 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的关键。

tty_cdev_add(内核源码/drivers/tty/tty_io.c)
 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。

tty_open(内核源码/drivers/tty/tty_io.c)
 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)。

tty_read(内核源码/drivers/tty/tty_io.c)
 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设备写入数据。同样,它先将数据交给线路规程处理,再由线路规程交给底层驱动。

tty_write(内核源码/drivers/tty/tty_io.c)
 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设备,清理资源。

tty_release(内核源码/drivers/tty/tty_io.c)
 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)等。

tty_ioctl(内核源码/drivers/tty/tty_io.c)
 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(只写)。

16550 UART 寄存器映射表

地址偏移

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是驱动最频繁访问的状态寄存器,提供收发路径的状态信息,是数据收发流程判断的依据。

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

  • 读取时机:查询 LSRDR 位为 1 时读取,或接收中断触发后读取。

  • FIFO模式:启用FIFO后, RBR 对应FIFO出口,每次读取弹出一个字节。

  • 发送保持寄存器THR(偏移0x00,DLAB=0,只写)

THR 是串口数据发送的入口寄存器。CPU将待发送字节写入 THR 后,硬件将其移入发送移位寄存器 TSR ,逐位串行输出。当 THR 数据移入 TSR 后, LSRTHRE 位置1,表示可以写入下一个字节。

  • 数据去向: THR -> 发送移位寄存器 TSR -> TXD引脚串行输出。

  • 写入时机:查询 LSRTHRE 位为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 控制串行数据帧的物理格式,是串口参数配置的核心寄存器。

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向调制解调器输出的握手信号。

MCR寄存器位定义

位序号

位名称

含义

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输入的握手信号状态。

MSR寄存器位定义

位序号

位名称

含义

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,降低中断频率,提升吞吐量。

FCR寄存器位定义

位序号

位名称

含义

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位保留。

IER寄存器位定义

位序号

缩写

名称

功能说明

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寄存器位定义

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. 驱动代码详解

核心定义与数据结构

核心定义与数据结构(位于linux_driver/40_tty_subsystem/tty_subsystem.c)
 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查询使用。

模块初始化与退出函数

模块初始化与退出函数(位于linux_driver/40_tty_subsystem/tty_subsystem.c)
  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框架的硬件抽象层,每个回调对应一类硬件操作。

UART操作集(位于linux_driver/40_tty_subsystem/tty_subsystem.c)
 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翻转缓冲区上报给用户空间。

接收定时器回调函数(位于linux_driver/40_tty_subsystem/tty_subsystem.c)
 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位。

发送器状态检查回调(位于linux_driver/40_tty_subsystem/tty_subsystem.c)
 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寄存器的对应输入状态。 在回环模式下模拟芯片内部引脚短接,在正常模式下模拟外部设备就绪。

调制解调器控制回调(位于linux_driver/40_tty_subsystem/tty_subsystem.c)
 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层标准标志位返回。

调制解调器状态读取回调(位于linux_driver/40_tty_subsystem/tty_subsystem.c)
 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寄存器的过程。

发送启动回调(位于linux_driver/40_tty_subsystem/tty_subsystem.c)
 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信号。

控制Break信号回调(位于linux_driver/40_tty_subsystem/tty_subsystem.c)
 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寄存器。

终端属性设置回调(位于linux_driver/40_tty_subsystem/tty_subsystem.c)
 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框架使用。

端口启动与关闭回调

端口打开时初始化资源并启动接收定时器,关闭时删除定时器并清理状态。

端口启动与关闭回调(位于linux_driver/40_tty_subsystem/tty_subsystem.c)
 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说明

Makefile(位于linux_driver/40_tty_subsystem/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 转换引入多余字符,导致字符串比较失败。

  • 硬件流控与虚拟状态: 默认非回环模式下,驱动强制将 MSRCTSDSRDCD 置为有效状态,确保上层应用开启硬件流控时不会因等待信号而阻塞发送。

  • 波特率与时钟频率:驱动固定使用1.8432MHz输入时钟,波特率计算公式为 divisor = 1843200 / (16 * baud) ,尝试设置超出支持范围的波特率(如921600)将导致除数为0。