往期内容
本专栏往期内容:Uart子系统
- UART串口硬件介绍
- 深入理解TTY体系:设备节点与驱动程序框架详解
- Linux串口应用编程:从UART到GPS模块及字符设备驱动
- 解UART 子系统:Linux Kernel 4.9.88 中的核心结构体与设计详解
- IMX 平台UART驱动情景分析:注册篇
- IMX 平台UART驱动情景分析:open篇
- IMX 平台UART驱动情景分析:read篇–从硬件驱动到行规程的全链路剖析
interrupt子系统专栏:
- 专栏地址:interrupt子系统
- Linux 链式与层级中断控制器讲解:原理与驱动开发
– 末片,有专栏内容观看顺序pinctrl和gpio子系统专栏:
专栏地址:pinctrl和gpio子系统
编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用
– 末片,有专栏内容观看顺序
input子系统专栏:
- 专栏地址:input子系统
- input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序
– 末片,有专栏内容观看顺序I2C子系统专栏:
- 专栏地址:IIC子系统
- 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
– 末篇,有专栏内容观看顺序总线和设备树专栏:
- 专栏地址:总线和设备树
- 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
– 末篇,有专栏内容观看顺序
1.内核代码
硬件相关:
-
drivers/tty/serial/imx.c📎imx.c — imx系列的
-
drivers/tty/serial/stm32-usart.c📎stm32-usart.c — stm32系列的
串口核心层:
- drivers/tty/serial/serial_core.c📎serial_core.c
TTY层:
- drivers/tty/tty_io.c📎tty_io.c
本文深入剖析了 Linux 串口子系统中的数据写入过程,重点涵盖 TTY 层、行规程、核心层及硬件驱动层的协作机制。通过对 tty_write、do_tty_write 等关键函数的详细代码解析,逐步追踪数据从用户空间到硬件层的传递路径。文章还探讨了数据写入的分块机制、线程安全处理以及中断与 DMA 方式的硬件发送逻辑。
看之前建议看一下之前关于UART驱动相关结构体的介绍文章:解UART 子系统:Linux Kernel 4.9.88 中的核心结构体与设计详解
对于TTY体系不了解的可以看:深入理解TTY体系:设备节点与驱动程序框架详解
1.write过程分析
1.1 流程
流程为:
-
APP写
- 使用行规程来写
- 数据最终存入uart_state->xmit的buffer里
-
硬件发送:怎么发送数据?
- 使用硬件驱动中uart_ops->start_tx开始发送
- 具体的发送方法有2种:通过DMA,或通过中断
-
中断方式
- 方法1:直接使能 tx empty中断,一开始tx buffer为空,在中断里填入数据
- 方法2:写部分数据到tx fifo,使能中断,剩下的数据再中断里继续发送
1.2 总框图
1.3 代码详情分析
1.3.1 tty层
\Linux-4.9.88\drivers\tty\tty_io.c
static 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_compat_ioctl,
.open = tty_open,
.release = tty_release,
.fasync = tty_fasync,
};
和read差不多,直接锁定file_operations中的write函数,也就是tty_write
:
\Linux-4.9.88\drivers\tty\tty_io.c
/**
* tty_write - tty 设备文件的写入方法
* @file: 指向 tty 文件的指针
* @buf: 要写入的用户数据缓冲区
* @count: 要写入的数据字节数
* @ppos: 未使用的文件偏移指针
*
* 通过行规程(line discipline)将数据写入 tty 设备。
*
* 锁机制:
* - 根据需要对行规程加锁。
* - 通过 atomic_write_lock 使写入 tty 驱动的操作串行化。
* - 每个设备的行规程写入方法不会并行调用,写入操作被分块处理。
*/
static ssize_t tty_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
struct tty_struct *tty = file_tty(file); // 从文件结构中获取 tty 结构
struct tty_ldisc *ld; // tty 的行规程结构指针
ssize_t ret; // 返回值:表示写入的字节数或错误码
// 检查 tty 的有效性和可能的错误
if (tty_paranoia_check(tty, file_inode(file), "tty_write"))
return -EIO; // 如果检测到错误,返回 -EIO
// 如果 tty 无效、无写入操作或存在 I/O 错误,返回 -EIO
if (!tty || !tty->ops->write || tty_io_error(tty))
return -EIO;
// 临时调试信息:检查是否定义了 write_room 方法
if (tty->ops->write_room == NULL)
tty_err(tty, "missing write_room method\n");
// 获取 tty 的行规程(line discipline)引用
ld = tty_ldisc_ref_wait(tty);
if (!ld)
return hung_up_tty_write(file, buf, count, ppos); // 如果行规程不可用,处理挂起情况
// 如果行规程未定义写入方法,返回 -EIO;否则调用写入操作
if (!ld->ops->write)
ret = -EIO;
else
ret = do_tty_write(ld->ops->write, tty, file, buf, count);
// 释放行规程引用
tty_ldisc_deref(ld);
return ret; // 返回写入的字节数或错误码
}
其中对行规进行写入操作就是do_tty_write(ld->ops->write, tty, file, buf, count)
函数,检查行规程的 write
方法是否存在。如果存在,则调用 do_tty_write
函数执行写入操作,成功时返回写入的字节数,否则返回 -EIO。
其中又涉及到了struct tty_ldisc ld->ops->write
,在read也有提到过,就是下下面的n_tty_write
函数。
\Linux-4.9.88\drivers\tty\n_tty.c
static struct tty_ldisc_ops n_tty_ops = {
.magic = TTY_LDISC_MAGIC,
.name = "n_tty",
.open = n_tty_open,
.close = n_tty_close,
.flush_buffer = n_tty_flush_buffer,
.read = n_tty_read,
.write = n_tty_write,
.ioctl = n_tty_ioctl,
.set_termios = n_tty_set_termios,
.poll = n_tty_poll,
.receive_buf = n_tty_receive_buf,
.write_wakeup = n_tty_write_wakeup,
.receive_buf2 = n_tty_receive_buf2,
};
暂时不进入n_tty_write
去看其实现,先继续进入do_tty_write
函数:
\Linux-4.9.88\drivers\tty\tty_io.c
/*
* 将写操作拆分成合适的块大小,以避免
* 拒绝服务(denial-of-service)类型的攻击
*/
static inline ssize_t do_tty_write(
ssize_t (*write)(struct tty_struct *, struct file *, const unsigned char *, size_t),
struct tty_struct *tty,
struct file *file,
const char __user *buf,
size_t count)
{
ssize_t ret, written = 0; // ret 存储返回值,written 存储已写入的字节数
unsigned int chunk; // 当前写入块的大小
// 锁定 tty 以保证线程安全,获取写锁
ret = tty_write_lock(tty, file->f_flags & O_NDELAY);
if (ret < 0)
return ret; // 如果获取锁失败,返回错误码
/*
* 将写操作分成临时缓冲区。这
* 简化了低级驱动的操作,因为它们
* 不再需要处理锁问题和用户模式访问。
*
* 但如果设置了 TTY_NO_WRITE_SPLIT,则应使用
* 更大的块大小。
*
* 默认块大小为 2kB,因为 NTTY
* 层对更大块有问题。它会声称能够处理
* 更多字符,但实际上不能。
*
* FIXME:这个限制可能可以去掉,但 64K 的块
* 仍然可能会失败,除非切换到 vmalloc...
*/
chunk = 2048; // 默认块大小为 2KB
if (test_bit(TTY_NO_WRITE_SPLIT, &tty->flags))
chunk = 65536; // 如果设置了标志,块大小为 64KB
if (count < chunk)
chunk = count; // 确保块大小不超过要写入的字节数
// write_buf/write_cnt 受 atomic_write_lock 互斥锁保护
if (tty->write_cnt < chunk) {
unsigned char *buf_chunk;
if (chunk < 1024)
chunk = 1024; // 确保块大小至少为 1KB
buf_chunk = kmalloc(chunk, GFP_KERNEL); // 分配临时缓冲区
if (!buf_chunk) {
ret = -ENOMEM; // 如果内存分配失败,返回 -ENOMEM
goto out; // 跳转到清理代码
}
kfree(tty->write_buf); // 释放旧的缓冲区
tty->write_cnt = chunk; // 更新当前写入块大小
tty->write_buf = buf_chunk; // 设置新的写入缓冲区
}
// 开始写入操作
for (;;) {
size_t size = count; // 当前要写入的大小
if (size > chunk)
size = chunk; // 确保不超过块大小
ret = -EFAULT; // 默认错误码为 EFAULT
if (copy_from_user(tty->write_buf, buf, size)) // 从用户空间复制数据到缓冲区
break; // 如果失败,跳出循环
ret = write(tty, file, tty->write_buf, size); // 调用写入函数
if (ret <= 0) // 如果写入失败或没有写入字节,跳出循环
break;
written += ret; // 累加已写入字节数
buf += ret; // 更新用户缓冲区指针
count -= ret; // 更新剩余字节数
if (!count) // 如果没有剩余字节,结束写入
break;
ret = -ERESTARTSYS; // 设置重启错误
if (signal_pending(current)) // 如果有信号挂起,跳出循环
break;
cond_resched(); // 允许调度其他任务
}
if (written) {
tty_update_time(&file_inode(file)->i_mtime); // 更新文件的最后修改时间
ret = written; // 设置返回值为已写入字节数
}
out:
tty_write_unlock(tty); // 解锁 tty
return ret; // 返回结果
}
do_tty_write
函数负责将用户数据写入 tty 设备。为了避免拒绝服务(DoS)攻击,该函数将写操作拆分成适当大小的块。这种分块机制可以防止过大的写入请求消耗过多资源。 其中调用了 ret = write(tty, file, tty->write_buf, size)
写入操作,这个write函数就是传进来的ld->ops->write
,也就是n_tty_write
:
\Linux-4.9.88\drivers\tty\n_tty.c
/**
* n_tty_write - tty 设备的写函数
* @tty: tty 设备
* @file: 文件对象
* @buf: 用户空间缓冲区指针
* @nr: I/O 的大小
*
* 终端设备的写函数。此函数与其他写调用是串行化的,
* 但不与 termios 变化、读取和其他事件串行化。
* 由于接收代码会回显字符,从而调用驱动的写方法,
* 因此在此处调用的输出处理函数以及回显处理函数中使用 output_lock 来保护列状态和缓冲区剩余空间。
*
* 这段代码必须确保在挂起时绝对不会进入睡眠状态。
*
* 锁定: output_lock 保护列状态和剩余空间
* (注意,process_output*() 函数本身会获取此锁)
*/
static ssize_t n_tty_write(struct tty_struct *tty, struct file *file,
const unsigned char *buf, size_t nr)
{
const unsigned char *b = buf; // 将用户缓冲区指针赋值给 b
DEFINE_WAIT_FUNC(wait, woken_wake_function); // 定义等待队列
int c; // 当前字符
ssize_t retval = 0; // 返回值初始化
/* 作业控制检查 -- 必须在开始时进行 (POSIX.1 7.1.1.4). */
if (L_TOSTOP(tty) && file->f_op->write != redirected_tty_write) {
retval = tty_check_change(tty); // 检查 tty 状态变化
if (retval)
return retval; // 如果检查失败,返回错误
}
down_read(&tty->termios_rwsem); // 获取读取锁,保护 termios 数据结构
/* 写出任何尚未回显的字符 */
process_echoes(tty); // 处理待回显的字符
add_wait_queue(&tty->write_wait, &wait); // 将当前进程添加到写等待队列
while (1) {
if (signal_pending(current)) { // 检查当前进程是否有挂起信号
retval = -ERESTARTSYS; // 设置重启错误
break; // 跳出循环
}
if (tty_hung_up_p(file) || (tty->link && !tty->link->count)) {
retval = -EIO; // 如果 tty 设备挂起或无有效链接,返回 I/O 错误
break;
}
if (O_OPOST(tty)) { // 检查是否启用了输出处理
while (nr > 0) {
ssize_t num = process_output_block(tty, b, nr); // 处理输出块
if (num < 0) {
if (num == -EAGAIN)
break; // 如果返回值是 EAGAIN,跳出循环
retval = num; // 记录错误返回值
goto break_out; // 跳转到退出处理
}
b += num; // 更新缓冲区指针
nr -= num; // 减少待写入的字节数
if (nr == 0)
break; // 如果没有剩余字节,结束循环
c = *b; // 获取当前字符
if (process_output(c, tty) < 0) // 处理当前字符输出
break; // 如果处理出错,跳出循环
b++; nr--; // 更新缓冲区指针和剩余字节数
}
if (tty->ops->flush_chars) // 如果定义了字符冲刷函数
tty->ops->flush_chars(tty); // 调用冲刷函数
} else {
struct n_tty_data *ldata = tty->disc_data; // 获取 n_tty 数据
while (nr > 0) {
mutex_lock(&ldata->output_lock); // 获取输出锁
c = tty->ops->write(tty, b, nr); // 调用 tty 写操作
mutex_unlock(&ldata->output_lock); // 释放输出锁
if (c < 0) {
retval = c; // 记录错误返回值
goto break_out; // 跳转到退出处理
}
if (!c) // 如果没有写入字符,结束循环
break;
b += c; // 更新缓冲区指针
nr -= c; // 更新剩余字节数
}
}
if (!nr) // 如果没有剩余字节,结束写入
break;
if (file->f_flags & O_NONBLOCK) { // 如果是非阻塞模式
retval = -EAGAIN; // 设置 EAGAIN 错误
break; // 跳出循环
}
up_read(&tty->termios_rwsem); // 释放读取锁
wait_woken(&wait, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT); // 允许其他任务调度
down_read(&tty->termios_rwsem); // 重新获取读取锁
}
break_out:
remove_wait_queue(&tty->write_wait, &wait); // 从等待队列中移除当前进程
if (nr && tty->fasync) // 如果有剩余字节并且异步写入被启用
set_bit(TTY_DO_WRITE_WAKEUP, &tty->flags); // 设置写入唤醒标志
up_read(&tty->termios_rwsem); // 释放读取锁
return (b - buf) ? b - buf : retval; // 返回实际写入的字节数或错误码
}
其中c = tty->ops->write(tty, b, nr)
,调用tty写操作,此时的tty是tty_struct
结构体类型,tty->ops
是tty_operations
结构体类型,此时已经对行规进行过写入操作,那么可以猜到tty->ops->write(tty, b, nr)
肯定就是将数据写入到tty设备。
1.3.2 核心层
可是在tty层我找不到这个函数定义???此时就要去核心层了,因为对行规的写操作是已经结束了,在\Linux-4.9.88\Linux-4.9.88\drivers\tty\serial\serial_core.c搜索可以找到:
\Linux-4.9.88\Linux-4.9.88\drivers\tty\serial\serial_core.c
static const struct tty_operations uart_ops = {
.open = uart_open,
.close = uart_close,
.write = uart_write,
.put_char = uart_put_char,
.flush_chars = uart_flush_chars,
.write_room = uart_write_room,
.chars_in_buffer= uart_chars_in_buffer,
.flush_buffer = uart_flush_buffer,
.ioctl = uart_ioctl,
.throttle = uart_throttle,
.unthrottle = uart_unthrottle,
.send_xchar = uart_send_xchar,
.set_termios = uart_set_termios,
.set_ldisc = uart_set_ldisc,
.stop = uart_stop,
.start = uart_start,
.hangup = uart_hangup,
.break_ctl = uart_break_ctl,
.wait_until_sent= uart_wait_until_sent,
#ifdef CONFIG_PROC_FS
.proc_fops = &uart_proc_fops,
#endif
.tiocmget = uart_tiocmget,
.tiocmset = uart_tiocmset,
.get_icount = uart_get_icount,
#ifdef CONFIG_CONSOLE_POLL
.poll_init = uart_poll_init,
.poll_get_char = uart_poll_get_char,
.poll_put_char = uart_poll_put_char,
#endif
};
那么就可以知道tty->ops->write
调用的就是uart_write
函数:
static int uart_write(struct tty_struct *tty,
const unsigned char *buf, int count)
{
struct uart_state *state = tty->driver_data; // 从 tty 结构获取 uart_state
struct uart_port *port; // 声明 uart_port 指针
struct circ_buf *circ; // 声明循环缓冲区指针
unsigned long flags; // 用于保存锁定标志
int c, ret = 0; // c 用于记录当前写入的字节数,ret 用于返回总写入字节数
/*
* 这意味着你在端口关闭后调用了此函数。
* 没有机会进行写入操作。
*/
if (!state) {
WARN_ON(1); // 打印警告信息
return -EL3HLT; // 返回错误,表示端口已关闭
}
circ = &state->xmit; // 获取当前 uart_state 的发送循环缓冲区
if (!circ->buf) // 检查缓冲区是否有效
return 0; // 如果无效,返回 0
port = uart_port_lock(state, flags); // 锁定 uart_port,并获取相关标志
while (port) { // 进入循环,处理写入操作
c = CIRC_SPACE_TO_END(circ->head, circ->tail, UART_XMIT_SIZE); // 计算可用的缓冲区空间
if (count < c) // 如果待写入字节数小于可用空间
c = count; // 将 c 设置为待写入字节数
if (c <= 0) // 如果没有可写字节
break; // 跳出循环
memcpy(circ->buf + circ->head, buf, c); // 将数据复制到循环缓冲区
circ->head = (circ->head + c) & (UART_XMIT_SIZE - 1); // 更新循环缓冲区头指针
buf += c; // 更新源缓冲区指针
count -= c; // 更新待写入字节数
ret += c; // 更新总写入字节数
}
__uart_start(tty); // 启动 UART 传输
uart_port_unlock(port, flags); // 解锁 uart_port
return ret; // 返回成功写入的字节数
}
对数据处理完后,就调用了__uart_start(tty);
启动uart传输,在之前对open进行分析过,其实是类似的,最后肯定是会进入到硬件驱动相关程序,调用struct uart_ops XXX中的函数去启动uart传输:
\Linux-4.9.88\drivers\tty\serial\serial_core.c
static void __uart_start(struct tty_struct *tty)
{
struct uart_state *state = tty->driver_data;
struct uart_port *port = state->uart_port;
if (port && !uart_tx_stopped(port))
port->ops->start_tx(port); //这里,不就调用到了
}
1.3.3 硬件层
port->ops->start_tx(port)
可以看出符合猜想了,那么就进入相关硬件层看看,\Linux-4.9.88\Linux-4.9.88\drivers\tty\serial\imx.c:
static const struct uart_ops imx_pops = {
.tx_empty = imx_tx_empty,
.set_mctrl = imx_set_mctrl,
.get_mctrl = imx_get_mctrl,
.stop_tx = imx_stop_tx,
.start_tx = imx_start_tx, //这个函数
.stop_rx = imx_stop_rx,
.enable_ms = imx_enable_ms,
.break_ctl = imx_break_ctl,
.startup = imx_startup,
.shutdown = imx_shutdown,
.flush_buffer = imx_flush_buffer,
.set_termios = imx_set_termios,
.type = imx_type,
.config_port = imx_config_port,
.verify_port = imx_verify_port,
#if defined(CONFIG_CONSOLE_POLL)
.poll_init = imx_poll_init,
.poll_get_char = imx_poll_get_char,
.poll_put_char = imx_poll_put_char,
#endif
};
port->ops->start_tx(port)
调用的就是imx_start_tx
。
imx_start_tx
函数用于启动 UART 设备的发送操作。根据 RS485 和 DMA 的设置,它配置 UART 硬件,准备发送数据。
分析到这里就差不多了,从tty层,到核心层,再到硬件层,其实很好懂的,主要牢记这个顺序就行了。
2.硬件相关的发送
有兴趣的可以继续看看它是怎么实现硬件发送的
\Linux-4.9.88\drivers\tty\serial\imx.c
/*
* interrupts disabled on entry
*/
static void imx_start_tx(struct uart_port *port)
{
struct imx_port *sport = (struct imx_port *)port; // 将 uart_port 强制转换为 imx_port 类型
unsigned long temp; // 临时变量用于存储寄存器值
// 检查 RS485 相关设置
if (port->rs485.flags & SER_RS485_ENABLED) {
temp = readl(port->membase + UCR2); // 读取 UCR2 寄存器的当前值
if (port->rs485.flags & SER_RS485_RTS_ON_SEND)
imx_port_rts_inactive(sport, &temp); // 设置 RTS 为非活动状态
else
imx_port_rts_active(sport, &temp); // 设置 RTS 为活动状态
// 如果不允许在发送期间接收,则禁用接收功能
if (!(port->rs485.flags & SER_RS485_RX_DURING_TX))
temp &= ~UCR2_RXEN; // 清除 RXEN 位以禁用接收
writel(temp, port->membase + UCR2); // 写回修改后的值到 UCR2 寄存器
// 启用发送器和移位器空的中断
temp = readl(port->membase + UCR4); // 读取 UCR4 寄存器
temp |= UCR4_TCEN; // 设置 TCEN 位以启用空闲中断
writel(temp, port->membase + UCR4); // 写回修改后的值到 UCR4 寄存器
}
// 检查 DMA 是否被启用
if (!sport->dma_is_enabled) {
temp = readl(sport->port.membase + UCR1); // 读取 UCR1 寄存器
writel(temp | UCR1_TXMPTYEN, sport->port.membase + UCR1); // 启用 TXMPTYEN 位
}
if (sport->dma_is_enabled) {
if (sport->port.x_char) {
/* 如果有 X-char 要发送,启用 TX IRQ 并禁用 TX DMA */
temp = readl(sport->port.membase + UCR1); // 读取 UCR1 寄存器
temp &= ~UCR1_TDMAEN; // 清除 TDMAEN 位以禁用 DMA
temp |= UCR1_TXMPTYEN; // 设置 TXMPTYEN 位以启用空闲中断
writel(temp, sport->port.membase + UCR1); // 写回修改后的值到 UCR1 寄存器
return; // 发送 X-char 后返回
}
// 如果发送缓冲区不为空且发送未停止,则调度 DMA 任务
if (!uart_circ_empty(&port->state->xmit) &&
!uart_tx_stopped(port))
schedule_work(&sport->tsk_dma_tx);
return; // 返回
}
}
启用中断以发送 X-char。如果发送缓冲区不为空且发送未停止,则调度 DMA 任务。该函数用于启动 UART 的发送操作。它根据当前的发送状态和配置(如 RS-485 的设置和 DMA 是否启用)配置 UART 硬件,准备好进行数据发送。 通常在发送数据的初始阶段调用,例如在 UART 驱动程序准备发送数据之前,或者在设置完 UART 的发送相关寄存器后。
那么既然是写,那肯定也需要中断,就有对应的中断的处理函数:
\Linux-4.9.88\drivers\tty\serial\imx.c
static irqreturn_t imx_txint(int irq, void *dev_id)
{
struct imx_port *sport = dev_id; // 获取传递的设备指针
unsigned long flags;
spin_lock_irqsave(&sport->port.lock, flags); // 上锁以保护共享资源
imx_transmit_buffer(sport); // 调用函数发送数据
spin_unlock_irqrestore(&sport->port.lock, flags); // 解锁
return IRQ_HANDLED; // 返回中断处理完成
}
static inline void imx_transmit_buffer(struct imx_port *sport)
{
struct circ_buf *xmit = &sport->port.state->xmit; // 获取发送缓冲区
unsigned long temp;
if (sport->port.x_char) {
/* 如果有字符待发送 */
writel(sport->port.x_char, sport->port.membase + URTX0); // 发送该字符
sport->port.icount.tx++; // 发送计数加1
sport->port.x_char = 0; // 清空待发送字符
return;
}
// 如果发送缓冲区为空或发送被停止,停止发送
if (uart_circ_empty(xmit) || uart_tx_stopped(&sport->port)) {
imx_stop_tx(&sport->port);
return;
}
// 处理 DMA 发送
if (sport->dma_is_enabled) {
/*
* 刚刚发送了 X-char,确保启用 TX DMA,并禁用 TX IRQ。
*/
temp = readl(sport->port.membase + UCR1); // 读取 UCR1 寄存器
temp &= ~UCR1_TXMPTYEN; // 清除 TXMPTYEN 位
if (sport->dma_is_txing) {
temp |= UCR1_TDMAEN; // 启用 DMA 传输
writel(temp, sport->port.membase + UCR1); // 写回 UCR1 寄存器
} else {
writel(temp, sport->port.membase + UCR1); // 仅写回
schedule_work(&sport->tsk_dma_tx); // 调度 DMA 任务
}
}
// 从发送缓冲区发送数据
while (!uart_circ_empty(xmit) &&
!(readl(sport->port.membase + uts_reg(sport)) & UTS_TXFULL)) {
/* 发送 xmit->buf[xmit->tail] 到端口 */
writel(xmit->buf[xmit->tail], sport->port.membase + URTX0); // 发送数据
xmit->tail = (xmit->tail + 1) & (UART_XMIT_SIZE - 1); // 更新尾部索引
sport->port.icount.tx++; // 发送计数加1
}
// 如果发送缓冲区中待发送字符少于阈值,则唤醒写入操作
if (uart_circ_chars_pending(xmit) < WAKEUP_CHARS)
uart_write_wakeup(&sport->port);
// 如果发送缓冲区为空,停止发送
if (uart_circ_empty(xmit))
imx_stop_tx(&sport->port);
}
imx_txint
该函数是一个中断处理程序,它在 UART 发送中断触发时被调用。主要职责是调用 imx_transmit_buffer
函数来处理发送缓冲区的数据。当 UART 控制器准备好发送下一个字符时,生成一个中断,触发 imx_txint
函数执行。它会确保在发送操作中对共享资源进行适当的锁定。
- 流程:
imx_start_tx
负责准备发送操作并可能启动 DMA 或设置相关寄存器,而imx_txint
则在 UART 发送中断到达时执行,负责从发送缓冲区中提取数据并进行发送。 - 调用链:在某些情况下,
imx_start_tx
会在数据准备好发送时被调用,而imx_txint
会响应 UART 发送硬件的状态变化(如发送缓冲区的空闲),从而进行数据发送。
因此,这两个函数一起工作,以确保 UART 数据发送的顺畅和高效。imx_start_tx
用于设置和启动发送,而 imx_txint
则在硬件准备好时进行实际的数据发送。