IMX 平台UART驱动情景分析:read篇--从硬件驱动到行规程的全链路剖析

往期内容

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

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

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博客
    -- 末篇,有专栏内容观看顺序

目录

1.内核代码

硬件相关:

串口核心层:

TTY层:

2.行规程

Linux-4.9.88\drivers\tty[📎tty_ldisc.c](https://www.yuque.com/attachments/yuque/0/2024/txt/40457654/1731853957034-ca0a7ae6-b27d-4406-810a-ae72d3c13119.txt)

行规程(TTY Line Discipline)是TTY层的关键模块,用于处理从硬件到用户空间的数据流和控制流。

  • 基本概念:
    • 行规程位于串口驱动和TTY层之间,负责处理数据格式化、缓冲区管理,以及对数据进行规范化处理(如规范模式和原始模式)。
    • 通过tty_ldisc_ops接口,行规程定义了一系列操作函数,比如readwriteioctl等。
  • n_tty规程:
    • n_tty是默认的行规程,主要用于处理规范模式和原始模式的字符数据。
    • 在规范模式下,n_tty_read会等待换行符或缓冲区满时才返回数据。
    • 在原始模式下,数据直接传递到用户空间。

行规程是通过tty_ldisc_ops结构实现扩展的,用户可以根据需要编写自定义的行规程,用于特殊协议或数据处理需求。

具体的相关结构体看之前的文章:理解UART 子系统:Linux Kernel 4.9.88 中的核心结构体与设计详解

具体的tty体系介绍看:深入理解TTY体系:设备节点与驱动程序框架详解

3.read过程分析

read调用涉及从应用程序到硬件的完整数据流,其关键点包括:

  1. 应用程序读取数据
    • 用户空间通过read系统调用读取数据,最终映射到tty_read函数。
    • 如果数据缓冲区为空,调用线程会进入休眠状态。
  2. 中断接收数据
    • 硬件接收到数据后触发中断(如imx_rxint),中断处理函数将数据从硬件FIFO读取到内核缓冲区。
  3. 行规程处理
    • 中断数据通过调用ldisc->ops->receive_buf传递到行规程的接收函数(如n_tty_receive_buf)。
    • 行规程对数据进行处理(如行编辑、缓冲区管理)后,将其存储到行规程的缓冲区中。
  4. 唤醒应用程序
    • 行规程在接收到完整数据或触发条件(如换行符)时,通过wake_up_interruptible唤醒休眠的用户进程。
  5. 返回用户数据
    • 应用程序进程被唤醒后,从行规程缓冲区读取数据并返回给用户。

扩展要点:

  • 锁机制 :通过mutex_lock和信号量保证并发访问的安全性。
  • 数据拷贝:从硬件缓冲区到内核缓冲区,再到用户空间缓冲区,多级拷贝可能会影响性能。

c 复制代码
\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,
};

具体的也不再缀述了,在之前讲解open的时候,如果能看懂的话,那么read和write其实也很好懂,只不过中间过了个行规程而已,直接看tty_read函数,在此之前先来讲一下行规程的结构体tty_ldisc

c 复制代码
\Linux-4.9.88\include\linux\tty_ldisc.h
struct tty_ldisc {
	struct tty_ldisc_ops *ops;  // 指向TTY行规约操作的结构体
	struct tty_struct *tty;      // 指向相关的TTY结构体
};

//其成员:
\Linux-4.9.88\Linux-4.9.88\include\linux\tty_ldisc.h
struct tty_ldisc_ops {
	int magic;  // 用于验证的魔数,确保结构体的正确性
	char *name; // 行规约的名称
	int num;    // 行规约的编号
	int flags;  // 行规约的标志,定义了该行规约的特性

	/*
	 * The following routines are called from above.
	 */
	int (*open)(struct tty_struct *);  // 打开TTY的方法
	void (*close)(struct tty_struct *); // 关闭TTY的方法
	void (*flush_buffer)(struct tty_struct *tty); // 刷新TTY缓冲区的方法
	ssize_t (*read)(struct tty_struct *tty, struct file *file,
			unsigned char __user *buf, size_t nr); // 读取TTY的数据
	ssize_t (*write)(struct tty_struct *tty, struct file *file,
			 const unsigned char *buf, size_t nr); // 写入数据到TTY
	int (*ioctl)(struct tty_struct *tty, struct file *file,
			 unsigned int cmd, unsigned long arg); // 控制TTY的ioctl方法
	long (*compat_ioctl)(struct tty_struct *tty, struct file *file,
				unsigned int cmd, unsigned long arg); // 兼容ioctl方法
	void (*set_termios)(struct tty_struct *tty, struct ktermios *old); // 设置TTY的终端IO设置
	unsigned int (*poll)(struct tty_struct *, struct file *,
			     struct poll_table_struct *); // 查询TTY的状态
	int (*hangup)(struct tty_struct *tty); // 挂起TTY的方法

	/*
	 * The following routines are called from below.
	 */
	void (*receive_buf)(struct tty_struct *, const unsigned char *cp,
			       char *fp, int count); // 接收数据到TTY缓冲区
	void (*write_wakeup)(struct tty_struct *); // 唤醒等待写入的进程
	void (*dcd_change)(struct tty_struct *, unsigned int); // 处理DCD信号变化
	int (*receive_buf2)(struct tty_struct *, const unsigned char *cp,
				char *fp, int count); // 备用接收方法

	struct module *owner; // 所属模块的指针

	int refcount; // 引用计数,管理行规约的生命周期
};

那么正式来分析tty_read函数

c 复制代码
Linux-4.9.88\Linux-4.9.88\drivers\tty\tty_io.c

static ssize_t tty_read(struct file *file, char __user *buf, size_t count,
			loff_t *ppos)
{
	int i;  // 用于存储读取的字节数
	struct inode *inode = file_inode(file);  // 获取与文件关联的inode结构体
	struct tty_struct *tty = file_tty(file);  // 获取与文件关联的tty结构体
	struct tty_ldisc *ld;  // 指向当前行规约的指针

	// 检查TTY设备的状态,确保设备没有故障
	if (tty_paranoia_check(tty, inode, "tty_read"))
		return -EIO;  // 如果有错误,返回输入输出错误
	if (!tty || tty_io_error(tty))
		return -EIO;  // 如果TTY为空或发生IO错误,返回输入输出错误

	/* 我们希望等待行规约完成此时的操作 */
	ld = tty_ldisc_ref_wait(tty);  // 获取行规约的引用并等待
	if (!ld)
		return hung_up_tty_read(file, buf, count, ppos);  // 如果行规约为空,处理挂起的TTY读取
	if (ld->ops->read)  // 检查是否有读取操作
		i = ld->ops->read(tty, file, buf, count);  // 调用行规约的读取方法
	else
		i = -EIO;  // 如果没有读取操作,返回输入输出错误

	tty_ldisc_deref(ld);  // 释放行规约的引用

	if (i > 0)  // 如果读取成功
		tty_update_time(&inode->i_atime);  // 更新文件的访问时间

	return i;  // 返回读取的字节数或错误代码
}

其中调用i = ld->ops->read(tty, file, buf, count)行规约的读取方法,这个方法是什么???可以去看\Linux-4.9.88\drivers\tty\n_tty.c:

c 复制代码
\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_read

c 复制代码
\Linux-4.9.88\Linux-4.9.88\drivers\tty\n_tty.c
static ssize_t n_tty_read(struct tty_struct *tty, struct file *file,
			 unsigned char __user *buf, size_t nr)
{
	struct n_tty_data *ldata = tty->disc_data;  // 获取TTY设备的线性数据结构
	unsigned char __user *b = buf;  // 用户空间缓冲区指针
	DEFINE_WAIT_FUNC(wait, woken_wake_function);  // 定义等待函数
	int c;
	int minimum, time;
	ssize_t retval = 0;  // 返回值初始化为0
	long timeout;
	int packet;
	size_t tail;

	c = job_control(tty, file);  // 控制当前作业的状态
	if (c < 0)
		return c;  // 如果返回值小于0,表示错误,直接返回

	/*
	 *	Internal serialization of reads.
	 */
	if (file->f_flags & O_NONBLOCK) {  // 检查文件是否为非阻塞模式
		if (!mutex_trylock(&ldata->atomic_read_lock))  // 尝试获取锁
			return -EAGAIN;  // 如果获取失败,返回重试错误
	} else {
		if (mutex_lock_interruptible(&ldata->atomic_read_lock))  // 获取锁,可能会被中断
			return -ERESTARTSYS;  // 被中断返回错误
	}

	down_read(&tty->termios_rwsem);  // 获取读取信号量

	minimum = time = 0;  // 最小字符和时间初始化
	timeout = MAX_SCHEDULE_TIMEOUT;  // 超时设为最大值
	if (!ldata->icanon) {  // 检查是否为非规范模式
		minimum = MIN_CHAR(tty);  // 获取最小字符数
		if (minimum) {
			time = (HZ / 10) * TIME_CHAR(tty);  // 计算超时时间
		} else {
			timeout = (HZ / 10) * TIME_CHAR(tty);
			minimum = 1;  // 如果没有最小字符,设置为1
		}
	}

	packet = tty->packet;  // 获取数据包状态
	tail = ldata->read_tail;  // 获取当前读取尾部位置

	add_wait_queue(&tty->read_wait, &wait);  // 添加等待队列以便调度
	while (nr) {  // 循环直到没有读取的字节
		/* First test for status change. */
		if (packet && tty->link->ctrl_status) {  // 检查控制状态
			unsigned char cs;
			if (b != buf)
				break;  // 如果已经读过数据,退出循环
			spin_lock_irq(&tty->link->ctrl_lock);  // 加锁控制状态
			cs = tty->link->ctrl_status;  // 获取控制状态
			tty->link->ctrl_status = 0;  // 清除状态
			spin_unlock_irq(&tty->link->ctrl_lock);  // 解锁
			if (put_user(cs, b)) {  // 将控制状态写入用户缓冲区
				retval = -EFAULT;  // 如果失败,返回错误
				break;
			}
			b++;  // 移动缓冲区指针
			nr--;  // 减少待读取字节数
			break;
		}

		if (!input_available_p(tty, 0)) {  // 检查输入是否可用
			up_read(&tty->termios_rwsem);  // 解锁信号量
			tty_buffer_flush_work(tty->port);  // 刷新TTY缓冲区
			down_read(&tty->termios_rwsem);  // 再次获取信号量
			if (!input_available_p(tty, 0)) {  // 如果仍然不可用
				if (test_bit(TTY_OTHER_CLOSED, &tty->flags)) {  // 检查其他TTY是否关闭
					retval = -EIO;  // 返回输入输出错误
					break;
				}
				if (tty_hung_up_p(file))  // 检查文件是否挂起
					break;
				if (!timeout)  // 检查超时
					break;
				if (file->f_flags & O_NONBLOCK) {  // 非阻塞模式
					retval = -EAGAIN;  // 返回重试错误
					break;
				}
				if (signal_pending(current)) {  // 检查当前进程是否有信号
					retval = -ERESTARTSYS;  // 返回被中断错误
					break;
				}
				up_read(&tty->termios_rwsem);  // 解锁信号量

				timeout = wait_woken(&wait, TASK_INTERRUPTIBLE, timeout);  // 等待被唤醒
				down_read(&tty->termios_rwsem);  // 再次获取信号量
				continue;  // 继续循环
			}
		}

		if (ldata->icanon && !L_EXTPROC(tty)) {  // 检查是否为规范模式
			retval = canon_copy_from_read_buf(tty, &b, &nr);  // 从读取缓冲区拷贝数据
			if (retval)
				break;  // 如果出错,退出
		} else {
			int uncopied;

			/* Deal with packet mode. */
			if (packet && b == buf) {  // 检查数据包模式
				if (put_user(TIOCPKT_DATA, b)) {  // 写入数据包标识
					retval = -EFAULT;  // 如果失败,返回错误
					break;
				}
				b++;  // 移动缓冲区指针
				nr--;  // 减少待读取字节数
			}

			uncopied = copy_from_read_buf(tty, &b, &nr);  // 从读取缓冲区拷贝数据
			uncopied += copy_from_read_buf(tty, &b, &nr);  // 再次拷贝数据
			if (uncopied) {
				retval = -EFAULT;  // 如果出错,返回错误
				break;
			}
		}

		n_tty_check_unthrottle(tty);  // 检查并解除限流

		if (b - buf >= minimum)  // 检查是否已读取到最小字节数
			break;
		if (time)  // 如果设置了超时时间
			timeout = time;  // 更新超时时间
	}
	if (tail != ldata->read_tail)  // 检查读取尾部是否改变
		n_tty_kick_worker(tty);  // 触发工作线程
	up_read(&tty->termios_rwsem);  // 解锁信号量

	remove_wait_queue(&tty->read_wait, &wait);  // 从等待队列中移除
	mutex_unlock(&ldata->atomic_read_lock);  // 解锁互斥量

	if (b - buf)  // 如果有数据被读取
		retval = b - buf;  // 更新返回值为实际读取字节数

	return retval;  // 返回读取字节数或错误码
}

retval = canon_copy_from_read_buf(tty, &b, &nr),无非就是阻塞等待行规有数据,然后唤醒调用该函数从读取缓冲区拷贝数据。

4.数据源头:中断

文件:drivers\tty\serial\imx.c📎imx.c

函数:imx_rxint

数据流的起点是硬件的中断处理。

  • 中断注册
    • serial_imx_probe函数中通过devm_request_irq注册了中断处理函数(如imx_rxint)。
    • 接收中断(RX)用于处理UART接收到的数据;发送中断(TX)则负责处理发送缓冲区的空闲信号。
  • 中断处理逻辑
    • 中断触发后,imx_rxint从硬件寄存器中读取数据,并通过tty_insert_flip_chartty_insert_flip_string将数据填入TTY缓冲区。
    • 使用tty_schedule_flip将缓冲区中的数据传递给行规程。
  • 效率优化
    • 为了避免频繁的中断触发,通常结合DMA或FIFO机制处理大批量数据。
    • 中断处理函数需要尽可能简短高效,以避免阻塞其他中断的处理。

在之前讲解imx.c的probe函数中,就有个注册中断处理函数,有中断,才能将数据传给行规,行规有数据了,才能唤醒休眠等待的read:

c 复制代码
static int serial_imx_probe(struct platform_device *pdev)
{
    if (txirq > 0) {
		ret = devm_request_irq(&pdev->dev, rxirq, imx_rxint, 0,
				       dev_name(&pdev->dev), sport);  //这个是读中断
		if (ret) {
			dev_err(&pdev->dev, "failed to request rx irq: %d\n",
				ret);
			return ret;
		}

		ret = devm_request_irq(&pdev->dev, txirq, imx_txint, 0,
				       dev_name(&pdev->dev), sport);  //这个是写中断
		if (ret) {
			dev_err(&pdev->dev, "failed to request tx irq: %d\n",
				ret);
			return ret;
		}
    //............
}

省略的不相关的,想要具体了解的话可以看之前的关于注册过程的分析。其中读中断处理函数就是imx_rxint:

c 复制代码
static irqreturn_t imx_rxint(int irq, void *dev_id)
{
	struct imx_port *sport = dev_id;  // 获取设备结构
	unsigned int rx, flg, ignored = 0;  // rx: 接收到的数据, flg: 状态标志, ignored: 忽略的错误计数
	struct tty_port *port = &sport->port.state->port;  // 获取 tty_port 结构
	unsigned long flags, temp;  // 用于保存状态和临时变量

	spin_lock_irqsave(&sport->port.lock, flags);  // 加锁并保存当前中断状态

	while (readl(sport->port.membase + USR2) & USR2_RDR) {  // 检查是否有接收数据
		flg = TTY_NORMAL;  // 默认状态标志
		sport->port.icount.rx++;  // 接收计数增加

		rx = readl(sport->port.membase + URXD0);  // 从接收数据寄存器读取数据

		temp = readl(sport->port.membase + USR2);  // 读取状态寄存器
		if (temp & USR2_BRCD) {  // 检查是否为中断信号
			writel(USR2_BRCD, sport->port.membase + USR2);  // 清除中断信号
			if (uart_handle_break(&sport->port))  // 处理断开的连接
				continue;  // 如果处理成功,继续下一个循环
		}

		if (uart_handle_sysrq_char(&sport->port, (unsigned char)rx))  // 检查是否为系统请求字符
			continue;  // 如果处理成功,继续下一个循环

		if (unlikely(rx & URXD_ERR)) {  // 检查是否有错误
			if (rx & URXD_BRK)  // 检查是否是断开信号
				sport->port.icount.brk++;  // 增加断开计数
			else if (rx & URXD_PRERR)  // 检查是否是奇偶校验错误
				sport->port.icount.parity++;  // 增加奇偶校验错误计数
			else if (rx & URXD_FRMERR)  // 检查是否是帧错误
				sport->port.icount.frame++;  // 增加帧错误计数
			if (rx & URXD_OVRRUN)  // 检查是否是溢出错误
				sport->port.icount.overrun++;  // 增加溢出错误计数

			if (rx & sport->port.ignore_status_mask) {  // 如果错误被忽略
				if (++ignored > 100)  // 如果忽略计数超过 100
					goto out;  // 跳转到出错处理
				continue;  // 继续下一个循环
			}

			rx &= (sport->port.read_status_mask | 0xFF);  // 清除不相关的状态位

			if (rx & URXD_BRK)  // 如果是断开信号
				flg = TTY_BREAK;  // 设置状态标志为断开
			else if (rx & URXD_PRERR)  // 如果是奇偶校验错误
				flg = TTY_PARITY;  // 设置状态标志为奇偶校验
			else if (rx & URXD_FRMERR)  // 如果是帧错误
				flg = TTY_FRAME;  // 设置状态标志为帧错误
			if (rx & URXD_OVRRUN)  // 如果是溢出错误
				flg = TTY_OVERRUN;  // 设置状态标志为溢出
		}

#ifdef SUPPORT_SYSRQ
		sport->port.sysrq = 0;  // 重置系统请求标志
#endif
		// 检查是否需要忽略读取
		if (sport->port.ignore_status_mask & URXD_DUMMY_READ)
			goto out;  // 跳转到出错处理

		// 将接收到的字符插入到 tty 缓冲区
		if (tty_insert_flip_char(port, rx, flg) == 0)  
			sport->port.icount.buf_overrun++;  // 如果缓冲区溢出,增加计数
	}

out:
	spin_unlock_irqrestore(&sport->port.lock, flags);  // 解锁并恢复中断状态
	tty_flip_buffer_push(port);  // 将缓冲区中的数据推送到 tty
	return IRQ_HANDLED;  // 返回中断处理成功
}

末尾不就调用到了tty_flip_buffer_push,将缓冲区中的数据推送到 tty:

c 复制代码
\Linux-4.9.88\drivers\tty\tty_buffer.c
void tty_flip_buffer_push(struct tty_port *port)
{
	tty_schedule_flip(port);
}

void tty_schedule_flip(struct tty_port *port)
{
	struct tty_bufhead *buf = &port->buf;

	/* paired w/ acquire in flush_to_ldisc(); ensures
	 * flush_to_ldisc() sees buffer data.
	 */
	smp_store_release(&buf->tail->commit, buf->tail->used);
	queue_work(system_unbound_wq, &buf->work);  //采用工作队列的方式,唤醒等待的read
}
相关推荐
智者知已应修善业1 小时前
【51单片机用数码管显示流水灯的种类是按钮控制数码管加一和流水灯】2022-6-14
c语言·经验分享·笔记·单片机·嵌入式硬件·51单片机
智商偏低8 小时前
单片机之helloworld
单片机·嵌入式硬件
青牛科技-Allen9 小时前
GC3910S:一款高性能双通道直流电机驱动芯片
stm32·单片机·嵌入式硬件·机器人·医疗器械·水泵、
沉在嵌入式的鱼10 小时前
使用nomachine远程连接ARM设备桌面
arm开发·rk3588·远程连接·nomachine
白鱼不小白11 小时前
stm32 USART串口协议与外设(程序)——江协教程踩坑经验分享
stm32·单片机·嵌入式硬件
S,D12 小时前
MCU引脚的漏电流、灌电流、拉电流区别是什么
驱动开发·stm32·单片机·嵌入式硬件·mcu·物联网·硬件工程
芯岭技术15 小时前
PY32F002A单片机 低成本控制器解决方案,提供多种封装
单片机·嵌入式硬件
youmdt15 小时前
Arduino IDE ESP8266连接0.96寸SSD1306 IIC单色屏显示北京时间
单片机·嵌入式硬件
嘿·嘘15 小时前
第七章 STM32内部FLASH读写
stm32·单片机·嵌入式硬件
几个几个n18 小时前
STM32-第二节-GPIO输入(按键,传感器)
单片机·嵌入式硬件