[驱动之路(九)——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;
}
相关推荐
jiuri_121516 分钟前
深入理解 Linux 内核同步机制
linux·内核
郝学胜-神的一滴33 分钟前
Python数据封装与私有属性:保护你的数据安全
linux·服务器·开发语言·python·程序人生
٩( 'ω' )و2601 小时前
linux--库的制作与原理
linux
海盗12341 小时前
VMware 中 CentOS 7 无法使用 yum 安装 wget 的完整解决方案
linux·运维·centos
gtr20202 小时前
Ubuntu24.04 基于 EtherCAT 的 SVD60N 主站
linux·ethercat
Ghost Face...2 小时前
网卡驱动开发与移植实战指南
驱动开发
weixin_462446232 小时前
ubuntu真机安装tljh jupyterhub支持跨域iframe
linux·运维·ubuntu
小码吃趴菜2 小时前
select/poll/epoll 核心区别
linux
Ghost Face...2 小时前
深入解析网卡驱动开发与移植
linux·驱动开发
a41324472 小时前
在CentOS系统上挂载硬盘到ESXi虚拟机
linux·运维·centos