往期内容
本专栏往期内容:Uart子系统
- UART串口硬件介绍
- 深入理解TTY体系:设备节点与驱动程序框架详解
- Linux串口应用编程:从UART到GPS模块及字符设备驱动
- 解UART 子系统:Linux Kernel 4.9.88 中的核心结构体与设计详解
- IMX 平台UART驱动情景分析:注册篇
- IMX 平台UART驱动情景分析:open篇
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
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
接口,行规程定义了一系列操作函数,比如read
、write
、ioctl
等。
- n_tty规程:
n_tty
是默认的行规程,主要用于处理规范模式和原始模式的字符数据。- 在规范模式下,
n_tty_read
会等待换行符或缓冲区满时才返回数据。 - 在原始模式下,数据直接传递到用户空间。
行规程是通过tty_ldisc_ops
结构实现扩展的,用户可以根据需要编写自定义的行规程,用于特殊协议或数据处理需求。
具体的相关结构体看之前的文章:理解UART 子系统:Linux Kernel 4.9.88 中的核心结构体与设计详解
具体的tty体系介绍看:深入理解TTY体系:设备节点与驱动程序框架详解
3.read过程分析
read
调用涉及从应用程序到硬件的完整数据流,其关键点包括:
- 应用程序读取数据 :
- 用户空间通过
read
系统调用读取数据,最终映射到tty_read
函数。 - 如果数据缓冲区为空,调用线程会进入休眠状态。
- 用户空间通过
- 中断接收数据 :
- 硬件接收到数据后触发中断(如
imx_rxint
),中断处理函数将数据从硬件FIFO读取到内核缓冲区。
- 硬件接收到数据后触发中断(如
- 行规程处理 :
- 中断数据通过调用
ldisc->ops->receive_buf
传递到行规程的接收函数(如n_tty_receive_buf
)。 - 行规程对数据进行处理(如行编辑、缓冲区管理)后,将其存储到行规程的缓冲区中。
- 中断数据通过调用
- 唤醒应用程序 :
- 行规程在接收到完整数据或触发条件(如换行符)时,通过
wake_up_interruptible
唤醒休眠的用户进程。
- 行规程在接收到完整数据或触发条件(如换行符)时,通过
- 返回用户数据 :
- 应用程序进程被唤醒后,从行规程缓冲区读取数据并返回给用户。
扩展要点:
- 锁机制 :通过
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_char
或tty_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
}