[驱动之路(九)——UART(串口)子系统]学习总结,万字长篇,一文彻底搞懂UART(串口)子系统(含串口数据收发流程解析)

一、UART是什么?

UART(通用异步收发传输器)是一种异步串行通信接口,常用于嵌入式系统和计算机之间传输数据。 它结构简单、稳定可靠,是嵌入式开发中最重要的调试和外接模块的接口之一。

串口的主要用途

打印调试信息(如通过串口助手查看程序输出)。

连接外设模块:GPS、蓝牙、Wi-Fi、GSM模块等。

设备间通信:两个嵌入式设备之间的数据传输。

二、串口的基本连接方式

只需要三根线即可通信:

1、TxD:发送数据线

2、RxD:接收数据线

3、GND:地线(统一电平参考)

三、串口的通信参数(必须一致才能通信)

1、波特率 表示每秒传输的比特数(bps),常见值:9600、19200、115200等。

2、起始位 每帧数据的开始标志,固定为1个比特的低电平

3、数据位 实际要传输的数据,一般为5~8位,如ASCII码(7位)。

4、校验位(可选) 用于检测数据传输是否正确,常见有奇校验或偶校验。

5、停止位 表示一帧数据的结束,通常为1~2位高电平。

四、数据传输示例(以发送字符'A'为例)

'A'的ASCII值为 0x41,二进制为 01000001。 传输过程:

1、发送方先拉低电平1位时间(起始位)。

2、接收方检测到低电平后开始计时。

3、依次发送8位数据(从最低位开始)。

4、最后发送停止位(高电平)。

五、串口电平标准(重要!)

UART通信有两种常见的电平标准:

电平标准 逻辑1电压范围 逻辑0电压范围 特点
TTL/CMOS 高电平(如3.3V/5V) 低电平(如0V) 常用在芯片内部或短距离通信
RS-232 -12V ~ -3V +3V ~ +12V 抗干扰强,适合工业、长距离通信

注意:

  • 大多数ARM芯片的串口是TTL电平

  • 如果连接PC的RS-232串口,需要使用电平转换芯片(如MAX232)。

  • 现在多数PC没有串口接口 ,常用USB转TTL串口模块(如CH340、CP2102)通过USB连接。
  • 多数ARM芯片有多个串口,一般串口0用于调试,其他串口用于外接模块。

六、TTY/终端/控制台/串口到底是什么?

这些术语看起来很相似,但实际上有不同的含义:

术语 含义 简单理解
TTY Teletype(电传打字机)的缩写,现在是Linux内核中的一套驱动系统 历史遗留的名字,现在泛指Linux中处理字符输入输出的子系统
Terminal 终端,指输入输出设备(键盘+显示器),可能是真实的也可能是虚拟的 你与计算机交互的"窗口"
Console 控制台,是一种特殊的终端,有更高权限(能看到内核消息) 系统管理员的"超级终端"
UART 串口硬件接口,TTY子系统的一部分 物理的串行通信接口,用于连接外部设备

七、设备节点详解(/dev目录下的文件)

Linux中"一切都是文件",设备也被抽象成文件。下面是常见TTY设备节点的区别:

1、串口设备节点

  • /dev/ttyS0、/dev/ttyS1... 标准串口设备(如COM1、COM2)

  • /dev/ttySAC0、/dev/ttySAC1... 某些ARM平台特定的串口命名

用途 :连接真实的串口设备,如通过USB转串口线连接开发板(串口没有虚拟终端的概念,一个设备节点就对应一个真实的硬件)。

实际开发中:嵌入式常用串口作为控制台,PC常用虚拟终端作为控制台

2、虚拟终端设备节点

  • /dev/tty1、/dev/tty2、/dev/tty3... 虚拟终端(按Ctrl+Alt+F1~F6切换的就是这些)

特点

  • 每个对应一个独立的登录会话

  • 一套键盘显示器模拟多个独立终端(多个设备节点对应一个真实的硬件

  • 即使没有图形界面也能使用多个"命令行窗口"

3、特殊设备节点

设备节点 含义 示例
/dev/tty0 前台终端:当前正在显示的虚拟终端 切换tty1→tty2,/dev/tty0就从tty1变成tty2
/dev/tty 当前进程的终端:程序自身所在的终端,不会随前台切换改变 在tty3运行的程序,无论前台切到哪个终端,它的/dev/tty始终是tty3
/dev/console 控制台 :接收内核消息的设备,由内核启动参数console=指定 可以是串口(console=ttyS0)或虚拟终端(console=tty0)

八、重要概念澄清

1、终端 vs 控制台

  • 所有控制台 都是终端

  • 但并非所有终端 都是控制台

  • 控制台能看到内核消息(如系统启动时的打印)

  • 可以通过内核参数选择哪个设备作为控制台

2、内核参数 "console="

系统启动时可以指定:

bash 复制代码
# 让串口ttyS0作为控制台(嵌入式常用)
console=ttyS0,115200

# 让第一个虚拟终端作为控制台(PC常用)
console=tty0

# 可以指定多个,只有最后一个生效
console=ttyS0 console=tty0  # tty0生效

3、图形界面下的终端

  • 在图形界面(如Ubuntu桌面)打开的终端窗口

  • 不是**/dev/ttyN** ,而是**/dev/pts/N**(伪终端)

  • 原理类似,但实现方式不同

九、实际应用场景

场景1:嵌入式开发调试

  • 开发板通过串口连接电脑

  • 内核参数设置 console=ttyS0

  • 系统启动信息、printk打印都从串口输出

  • 开发者通过串口工具(如xshell)查看和交互

场景2:系统维护

  • 系统图形界面崩溃时

  • 按 Ctrl+Alt+F2 切换到tty2

  • 登录后可以修复系统

  • 这里tty2就是你的控制台

场景3:多用户服务器

  • 多个用户通过SSH远程登录

  • 每个用户获得一个伪终端(/dev/pts/X)

  • 各自独立工作,互不干扰

十、TTY驱动框架工作流程

1、数据流向图:

css 复制代码
用户空间程序
    ↓
read()/write()系统调用
    ↓
TTY核心层(/dev/ttyS0)
    ↓
行规程层(编辑、转换、缓冲)
    ↓
TTY驱动层(UART驱动、键盘显示器驱动)
    ↓
硬件层(串口芯片)

2、详细流程示例:用户输入"hello"

1、用户输入:在键盘上依次按下h、e、l、l、o

2、TTY驱动层:接收键盘扫描码,转换为字符

3、行规程层

  • 回显每个字符到屏幕

  • 将字符存入行缓冲区

  • 等待回车键

4、用户按回车

5、行规程层

  • 将回车转换为换行(\n

  • 将整行数据 "hello\n" 提交给TTY核心层(没输入换行符,应用程序就不会接收到数据

6、TTY核心层:将数据传递给用户空间程序(如shell)

7、用户程序:收到"hello\n",执行相应命令

十一、UART子系统的分层结构

css 复制代码
用户空间程序
      ↓
┌─────────────────────────────────┐
│     第1层:tty_io层            │ ← 系统调用入口,管理tty_struct
├─────────────────────────────────┤
│     第2层:行规程层            │ ← 数据处理中心(编辑、回显、缓冲)
├─────────────────────────────────┤
│     第3层:串口核心层          │ ← 通用串口逻辑,缓冲区管理
├─────────────────────────────────┤
│     第4层:硬件驱动层          │ ← 具体硬件操作(键盘/UART)
└─────────────────────────────────┘
      ↓
   硬件设备

第1层:tty_io层(设备文件接口层)

位置:drivers/tty/tty_io.c

作用用户空间与内核的桥梁

核心功能

1、设备文件操作:实现file_operations结构体

cpp 复制代码
const struct file_operations tty_fops = {
    .llseek = no_llseek,
    .read = tty_read,
    .write = tty_write,
    .poll = tty_poll,
    .unlocked_ioctl = tty_ioctl,
    .compat_ioctl = tty_ioctl,
    .open = tty_open,
    .release = tty_release,
    .fasync = tty_fasync,
};

2、管理tty_struct:每个打开的终端对应一个tty_struct

cpp 复制代码
struct tty_struct {
    struct tty_driver *driver;    // 指向tty驱动
    struct tty_ldisc *ldisc;      // 指向行规程
    struct tty_port *port;        // 指向端口
    struct tty_buffer *buf;       // 缓冲区
    // ... 其他字段
};

3、系统调用入口

cpp 复制代码
// 当应用程序调用 read(fd, buf, count) 时:
SYSCALL_DEFINE3(read, ...) → vfs_read() → tty_read()

// 当应用程序调用 write(fd, buf, count) 时:
SYSCALL_DEFINE3(write, ...) → vfs_write() → tty_write()

工作流程

cpp 复制代码
应用程序:open("/dev/ttyS0", O_RDWR)
          ↓
      系统调用
          ↓
    tty_open() 
          ↓
    分配tty_struct
          ↓
    关联行规程(N_TTY)
          ↓
    关联串口驱动

第2层:行规程层(数据处理层)

位置:drivers/tty/n_tty.c(默认行规程)

作用数据的智能处理器

核心数据结构

cpp 复制代码
struct n_tty_data {
    unsigned char read_buf[N_TTY_BUF_SIZE];  // 读缓冲区(4096字节)
    unsigned char echo_buf[N_TTY_BUF_SIZE];  // 回显缓冲区
    size_t read_head, read_tail;             // 读写指针
    size_t echo_head, echo_tail;             // 回显指针
    wait_queue_head_t read_wait;             // 读等待队列
    wait_queue_head_t write_wait;            // 写等待队列
    unsigned char lnext:1;                   // 下一个字符字面值
    // ... 其他标志位
};

关键处理功能

1、行编辑:退格(Backspace)、清除单词(Ctrl+W)、清除行(Ctrl+U)

2、回显控制:显示输入字符,或隐藏(如密码输入)

3、特殊字符处理

  • Ctrl+C → SIGINT信号

  • Ctrl+Z→ SIGTSTP信号

  • Ctrl+\ → SIGQUIT信号

4、字符转换\r\n\n 转换

第3层:串口核心层(通用逻辑层)

位置:drivers/tty/serial/serial_core.c

作用所有串口共享的通用逻辑

核心数据结构

cpp 复制代码
struct uart_state {
    struct tty_port port;           // tty端口
    struct circ_buf xmit;           // 发送环形缓冲区
    struct uart_port *uart_port;    // 硬件端口指针
    struct uart_icount icount;      // 统计信息
    // ...
};

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

主要功能

1、缓冲区管理:环形缓冲区操作

2、中断处理框架:统一的中断处理入口

3、波特率计算:将波特率转换为分频值

4、流控支持:RTS/CTS硬件流控

5、状态管理:跟踪串口状态(打开、关闭、挂起)

关键函数

cpp 复制代码
// 数据流向管理
uart_write()      // 写入数据到发送缓冲区
uart_start()      // 启动发送
uart_handle_sysrq_char()  // 处理SysRq键
uart_insert_char() // 插入接收到的字符

// 配置管理
uart_get_baud_rate()  // 计算波特率
uart_update_timeout() // 更新超时设置
uart_set_options()    // 设置串口选项

第4层:硬件驱动层(硬件操作层)

位置:drivers/tty/serial/imx.c(IMX6ULL)

作用与具体硬件交互

核心操作集

cpp 复制代码
static const struct uart_ops imx_uart_ops = {
    .tx_empty	= imx_tx_empty,       // 检查发送是否完成
    .set_mctrl	= imx_set_mctrl,      // 设置MODEM控制线
    .get_mctrl	= imx_get_mctrl,      // 获取MODEM状态
    .stop_tx	= imx_stop_tx,        // 停止发送
    .start_tx	= imx_start_tx,       // 开始发送
    .stop_rx	= imx_stop_rx,        // 停止接收
    .enable_ms	= imx_enable_ms,      // 使能MODEM状态中断
    .break_ctl	= imx_break_ctl,      // 控制Break信号
    .startup	= imx_startup,        // 启动串口
    .shutdown	= imx_shutdown,       // 关闭串口
    .set_termios = imx_set_termios,   // 设置终端属性
    .type		= imx_type,           // 返回类型字符串
    .request_port = imx_request_port, // 请求端口资源
    .config_port = imx_config_port,   // 配置端口
    .verify_port = imx_verify_port,   // 验证端口
};

十二、行规程(Line Discipline)详解

1、什么是行规程?

行规程是TTY子系统中的数据处理层,负责在用户输入输出时进行"智能处理"。

2、行规程的三种模式:

模式 函数处理 典型应用
规范模式(cooked) 行缓冲、编辑、回显、信号处理 命令行交互
非规范模式(cbreak) 即时响应,无行缓冲 vim编辑器
原始模式(raw) 无任何处理,直接传递 串口通信

3、行规程的主要功能:

a、行编辑功能

  • 退格键处理:删除前一个字符

  • 清除单词:Ctrl+W

  • 清除整行:Ctrl+U

  • 重新打印:Ctrl+R

css 复制代码
# 在规范模式下,输入时可以使用这些编辑功能
# 输入 "hello",按退格键删除最后的'o',变成"hell"

b、回显控制

  • 将用户输入的字符显示在屏幕上

  • 可以关闭回显(输入密码时)

c、特殊字符转换

  • 将回车(\r)转换为换行(\n)

  • 将 Ctrl+C 转换为中断信号(SIGINT)

d、行缓冲

  • 等待用户按下回车键才提交整行数据

  • 允许在提交前进行编辑

十三、注册字符设备驱动的两种方法

1、字符设备驱动的基本框架:

  • 确定主设备号(可以静态指定或动态分配)。
  • 定义file_operations结构体,并实现其中的操作函数(如open、read、write等)。
  • 将file_operations结构体注册到内核中。
  • 创建设备节点(可以通过class_create和device_create自动创建)。

2、两种注册方法对比

Linux内核提供了两种字符设备驱动注册方法,下面是它们的对比:

对比项 旧方法(register_chrdev) 新方法(cdev_xxx系列)
出现时间 早期方法,简单直接 较新方法,更灵活
资源使用 一次注册256个次设备号,可能浪费 只注册需要的设备号范围,更节省
灵活性 较低,适合简单驱动 高,适合复杂驱动
现代驱动 较少使用,逐渐淘汰 推荐使用,更标准
代码复杂度 简单,一个函数搞定 稍复杂,需要多步骤

3、旧方法详解:register_chrdev

a、工作原理

cpp 复制代码
// 一个函数搞定所有事情
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
  • 一次性注册:主设备号 + 0~255的所有次设备号

  • 自动分配:如果major=0,内核自动分配主设备号

  • 简单粗暴:适合初学者,但不够灵活

b、优点与缺点

优点

  • 代码简单,容易理解

  • 一个函数完成注册

缺点

  • 资源浪费:注册256个次设备号,即使只用一个

  • 不够灵活:无法精确控制设备号范围

  • 不推荐使用:新驱动开发中已逐渐淘汰

4、新方法详解:cdev_xxx系列函数

a、工作原理(三步法)

新方法将注册过程分解为三个步骤:

cpp 复制代码
// 第一步:注册设备号范围
alloc_chrdev_region(&dev_id, first_minor, count, name);

// 第二步:初始化cdev结构
cdev_init(&my_cdev, &fops);

// 第三步:添加cdev到系统
cdev_add(&my_cdev, dev_id, count);

b、核心概念理解

设备号(dev_t)
  • 包含主设备号次设备号

  • 32位整数:高12位为主设备号,低20位为次设备号

cpp 复制代码
dev_t dev_id;  // 设备号
MAJOR(dev_id);  // 提取主设备号
MINOR(dev_id);  // 提取次设备号
MKDEV(major, minor);  // 合成设备号

c、函数详解

cpp 复制代码
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);
  • 动态分配:内核自动分配一个可用的主设备号

  • 精确控制:可以指定起始次设备号和设备数量

  • 示例alloc_chrdev_region(&dev_id, 0, 3, "mydev")

    分配一个主设备号,次设备号0、1、2共3个设备,设备名为"mydev"

cpp 复制代码
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
  • 初始化cdev结构体

  • 关联file_operations操作函数

cpp 复制代码
int cdev_add(struct cdev *p, dev_t dev, unsigned int count);
  • 将cdev添加到系统,设备生效

  • 可以指定多个设备(count>1)

5、为什么推荐新方法?

场景1:多个相似设备

cpp 复制代码
// 创建一个主设备号,5个次设备号
alloc_chrdev_region(&dev_id, 0, 5, "multi_device");
// 对应5个设备文件:/dev/multi_device0, /dev/multi_device1...

场景2:避免设备号冲突

旧方法需要手动指定主设备号,可能与其他驱动冲突。 新方法自动分配,避免冲突。

场景3:节省资源

只注册实际需要的设备号,不浪费。

场景4:与sysfs集成更好

方便创建设备节点,管理更规范。

十四、场景分析:应用程序通过串口发送"Hello"

css 复制代码
应用程序: write(fd, "Hello", 5);
           ↓
   系统调用进入内核
           ↓
    TTY层 (tty_io.c)
       ↓ tty_write()
           ↓
   行规程处理(如需要)
           ↓
   serial_core.c
       ↓ uart_write()
           ↓
   数据放入发送缓冲区
           ↓
   imx.c硬件驱动
       ↓ imx_start_tx()
           ↓
   使能发送中断/直接发送
           ↓
   硬件UART控制器
       ↓ 通过TxD引脚发出

1、用户空间到TTY层

cpp 复制代码
// tty_io.c
ssize_t tty_write(struct file *file, const char __user *buf, size_t count)
{
    // 1. 获取当前tty结构
    // 2. 调用行规程的write函数
    // 3. 行规程可能进行数据处理(规范模式下)
    // 4. 调用uart_write
}

2、串口核心层处理

cpp 复制代码
// serial_core.c
int uart_write(struct uart_port *port, const unsigned char *buf, int count)
{
    // 1. 将数据放入环形缓冲区
    // 2. 调用硬件驱动的start_tx函数
    // 3. 返回写入的字节数
}

3、硬件驱动层发送

cpp 复制代码
// imx.c
static void imx_start_tx(struct uart_port *port)
{
    // 1. 检查发送缓冲区是否有数据
    // 2. 使能发送中断或直接发送
    // 3. 写入UART数据寄存器
}

// 发送中断处理函数
static irqreturn_t imx_txint(int irq, void *dev_id)
{
    // 1. 检查发送缓冲区
    // 2. 从缓冲区取出下一个字符
    // 3. 写入数据寄存器
    // 4. 如果缓冲区空,禁用发送中断
}

十五、UART读写总体流程图

css 复制代码
读取流程(接收数据):
硬件UART收到数据 → 产生接收中断 → 硬件驱动读取数据 → 存入串口缓冲区 → 
通知行规程 → 行规程处理数据 → 唤醒等待的应用程序 → 应用从行规程缓冲区读取数据

写入流程(发送数据):
应用程序调用write → TTY层处理 → 行规程处理 → 存入发送缓冲区 → 
串口核心层启动发送 → 硬件驱动开始发送 → 中断方式发送剩余数据 → 发送完成

1、读取数据(Read)流程详解

a、应用程序发起读取

cpp 复制代码
// 用户程序
char buffer[100];
int count = read(fd, buffer, sizeof(buffer));  // 从串口读取数据

b、TTY层处理(tty_io.c)

cpp 复制代码
// drivers/tty/tty_io.c
ssize_t tty_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    struct tty_struct *tty = file->private_data;
    
    // 1. 获取当前tty结构
    // 2. 检查行规程是否可用
    // 3. 调用行规程的读取函数
    return tty->ldisc->ops->read(tty, file, buf, count);
}

关键点

  • tty_read是read系统调用的内核实现

  • 调用行规程的read函数(默认为N_TTY行规程)

c、行规程处理(n_tty.c)

cpp 复制代码
// drivers/tty/n_tty.c
static ssize_t n_tty_read(struct tty_struct *tty, struct file *file,
                         unsigned char *kbuf, size_t nr)
{
    // 1. 检查是否有足够数据可读
    // 2. 如果无数据,让进程休眠(等待数据到达)
    // 3. 从行规程缓冲区复制数据到用户空间
    // 4. 返回读取的字节数
}

d、数据如何到达行规程缓冲区?

中断处理流程
css 复制代码
硬件接收数据 → 产生中断 → 中断处理函数读取数据 → 存入串口缓冲区 → 通知行规程 → 行规程处理存入自己的缓冲区 → 唤醒等待进程
以IMX6ULL为例(中断方式)
cpp 复制代码
// drivers/tty/serial/imx.c
static irqreturn_t imx_rxint(int irq, void *dev_id)
{
    struct imx_port *sport = dev_id;
    unsigned int rx, flg;
    
    // 1. 读取状态寄存器
    unsigned int status = imx_uart_read(sport, USR1);
    
    // 2. 读取数据寄存器获取接收到的字符
    rx = imx_uart_read(sport, URXD0);
    
    // 3. 检查错误(奇偶校验、帧错误等)
    flg = TTY_NORMAL;
    if (status & USR1_BRCD)
        flg = TTY_BREAK;
    
    // 4. 将数据存入tty缓冲区
    tty_insert_flip_char(&sport->port.state->port, rx, flg);
    
    // 5. 通知行规程处理新数据
    tty_flip_buffer_push(&sport->port.state->port);
    
    return IRQ_HANDLED;
}

2、写入数据(Write)流程详解

a、应用程序发起写入

cpp 复制代码
// 用户程序
const char *msg = "Hello UART!\n";
int count = write(fd, msg, strlen(msg));  // 向串口写入数据

b、TTY层处理(tty_io.c)

cpp 复制代码
// drivers/tty/tty_io.c
ssize_t tty_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    struct tty_struct *tty = file->private_data;
    
    // 1. 获取当前tty结构
    // 2. 检查行规程是否可用
    // 3. 调用行规程的写入函数
    return tty->ldisc->ops->write(tty, file, buf, count);
}

c、行规程处理(n_tty.c)

cpp 复制代码
// drivers/tty/n_tty.c
static ssize_t n_tty_write(struct tty_struct *tty, struct file *file,
                          const unsigned char *buf, size_t nr)
{
    // 1. 如果需要,进行数据处理(如规范模式下的转换)
    // 2. 调用uart_write将数据传递给串口核心层
    // 3. 返回写入的字节数
}

行规程处理的内容

  • 规范模式:特殊字符处理(如回车换行转换)

  • 回显控制:是否将输入字符显示到终端

  • 信号生成:如Ctrl+C生成SIGINT信号

d、串口核心层处理(serial_core.c)

cpp 复制代码
// drivers/tty/serial/serial_core.c
static int uart_write(struct uart_port *port, const unsigned char *buf, int count)
{
    struct circ_buf *circ = &port->state->xmit;
    int c, ret = 0;
    
    // 1. 检查发送缓冲区是否有空间
    // 2. 将数据放入环形发送缓冲区
    while (1) {
        c = CIRC_SPACE_TO_END(circ->head, circ->tail, UART_XMIT_SIZE);
        if (count < c)
            c = count;
        
        memcpy(circ->buf + circ->head, buf, c);
        circ->head = (circ->head + c) & (UART_XMIT_SIZE - 1);
        
        buf += c;
        count -= c;
        ret += c;
        
        if (count == 0)
            break;
    }
    
    // 3. 通知硬件驱动开始发送
    port->ops->start_tx(port);
    
    return ret;
}

发送缓冲区结构

cpp 复制代码
struct uart_state {
    struct tty_port port;           // tty端口
    struct circ_buf xmit;           // 发送环形缓冲区
    struct uart_port *uart_port;    // 串口端口
};

e、 硬件驱动发送数据

发送的两种方式
  1. 中断方式:数据放入缓冲区,使能发送中断,在中断中发送

  2. DMA方式:配置DMA自动发送数据

中断方式(以IMX6ULL为例)
cpp 复制代码
// drivers/tty/serial/imx.c
static void imx_start_tx(struct uart_port *port)
{
    struct imx_port *sport = (struct imx_port *)port;
    unsigned int temp;
    
    // 1. 检查发送缓冲区是否有数据
    if (!uart_circ_empty(&sport->port.state->xmit)) {
        // 2. 使能发送中断
        temp = imx_uart_read(sport, UCR1);
        temp |= UCR1_TXMPTYEN;
        imx_uart_write(sport, temp, UCR1);
    }
}

// 发送中断处理函数
static irqreturn_t imx_txint(int irq, void *dev_id)
{
    struct imx_port *sport = dev_id;
    struct circ_buf *xmit = &sport->port.state->xmit;
    
    // 1. 检查发送缓冲区是否还有数据
    while (!uart_circ_empty(xmit)) {
        // 2. 从缓冲区取出一个字符
        unsigned char c = xmit->buf[xmit->tail];
        
        // 3. 写入UART数据寄存器,硬件会自动发送
        imx_uart_write(sport, c, URTX0);
        
        // 4. 更新缓冲区指针
        xmit->tail = (xmit->tail + 1) & (UART_XMIT_SIZE - 1);
        
        // 5. 更新统计信息
        sport->port.icount.tx++;
        
        // 6. 如果缓冲区空了,停止发送中断
        if (uart_circ_empty(xmit)) {
            temp = imx_uart_read(sport, UCR1);
            temp &= ~UCR1_TXMPTYEN;
            imx_uart_write(sport, temp, UCR1);
            break;
        }
    }
    
    return IRQ_HANDLED;
}
相关推荐
A小辣椒2 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言