Bootstrap

IMX 平台UART驱动情景分析:write篇--从 TTY 层到硬件驱动的写操作流程解析

往期内容

本专栏往期内容:Uart子系统

  1. UART串口硬件介绍
  2. 深入理解TTY体系:设备节点与驱动程序框架详解
  3. Linux串口应用编程:从UART到GPS模块及字符设备驱动
  4. 解UART 子系统:Linux Kernel 4.9.88 中的核心结构体与设计详解
  5. IMX 平台UART驱动情景分析:注册篇
  6. IMX 平台UART驱动情景分析:open篇
  7. IMX 平台UART驱动情景分析:read篇–从硬件驱动到行规程的全链路剖析

interrupt子系统专栏:

  1. 专栏地址:interrupt子系统
  2. Linux 链式与层级中断控制器讲解:原理与驱动开发
    – 末片,有专栏内容观看顺序

pinctrl和gpio子系统专栏:

  1. 专栏地址:pinctrl和gpio子系统

  2. 编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用

    – 末片,有专栏内容观看顺序

input子系统专栏:

  1. 专栏地址:input子系统
  2. input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序
    – 末片,有专栏内容观看顺序

I2C子系统专栏:

  1. 专栏地址:IIC子系统
  2. 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
    – 末篇,有专栏内容观看顺序

总线和设备树专栏:

  1. 专栏地址:总线和设备树
  2. 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
    – 末篇,有专栏内容观看顺序

img

1.内核代码

硬件相关:

串口核心层:

TTY层:

本文深入剖析了 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 总框图

img

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->opstty_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.硬件相关的发送

有兴趣的可以继续看看它是怎么实现硬件发送的

img

\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 则在硬件准备好时进行实际的数据发送。

;