当前位置: 首页 > article >正文

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

img

目录

  • 往期内容
  • 1.内核代码
  • 2.行规程
  • 3.read过程分析
  • 4.数据源头:中断

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

2.行规程

img

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和信号量保证并发访问的安全性。
  • 数据拷贝:从硬件缓冲区到内核缓冲区,再到用户空间缓冲区,多级拷贝可能会影响性能。

imgimg

\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

\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函数

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:

\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

\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机制处理大批量数据。
    • 中断处理函数需要尽可能简短高效,以避免阻塞其他中断的处理。

img

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

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:

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:

\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
}

http://www.kler.cn/a/411053.html

相关文章:

  • WebSocket详解、WebSocket入门案例
  • 利用Python爬虫获得1688按关键字搜索商品:技术解析
  • vscode利用ofExtensions插件可以调试单进程Openfoam,但是不能调试mpi多进程案例
  • 重新定义社媒引流:AI社媒引流王如何为品牌赋能?
  • STM32WB55RG开发(5)----监测STM32WB连接状态
  • 钉钉免登录接口
  • XG(S)-PON原理
  • 【贪心算法第五弹——300.最长递增子序列】
  • QUICK调试camera-xml解析
  • QT QToolButton控件 全面详解
  • Scala—Collections集合概述
  • goframe框架bug-记录
  • 如何提升编程能力第二篇
  • 关于网络安全攻防知识
  • [CA] 读懂core.cpp -3 Core::decode
  • docker 的各种操作
  • 防御网络攻击的创新策略
  • IP反向追踪技术,了解一下?
  • vim 如何高亮/取消高亮
  • MySQL系列之数据类型(Numeric)
  • 【计算机网络】C/C++实现解析Wireshark离线数据包,附源码
  • Java基础.数组排序(冒泡排序和选择排序)数组与遍历
  • JavaScript HTML DOM 实例
  • windowsC#-在异步任务完成时处理
  • wangEditor富文本插入自定义应用
  • 大厂也在用的分布式链路追踪:TraceIdFilter + MDC + Skywalking