11.2 Linux串口驱动框架

tty 驱动程序框架

tty 驱动程序从下往上分别是设备驱动层、行规程、终端虚拟化、TTY I/O层,它们的功能如下:

  1. 设备驱动层:用于驱动设备,如串口、显示器、键盘等。
  2. 行规程:用于处理控制字符、回显输入数据、缓存输入数据、显示数据输出等,如果应用层不需要这些处理机制可以将行规程设置为原始模式,设置方法参考11.1Linux串口应用程序开发。
  3. 终端虚拟化:用于对显示器、键盘、鼠标组成的终端进行虚拟化。
  4. TTY I/O层:与应用层进行交互。
    在这里插入图片描述

struct uart_driver 对象

uart_driver 对象表示一个 uart 驱动(一个串口驱动可以由多个串口端口),其核心成员如下:

	//所属模块
	struct module *owner;
	//驱动名称
	const char *driver_name;
	//设备名称,串口设备文件名以此为基础生成
	const char *dev_name;
	//主设备号,为0表示系统自动分配
	int major;
	//起始次设备号,即第一个串口的次设备号
	int minor;
	//此驱动支持的串口个数
	int nr;
	//若驱动支持 console 则指向对应的 serial console ,否则为 NULL
	struct console *cons;
	//串口的状态信息,每个串口端口都有自己的状态信息,其中主要包括 tty_port 和 uart_port
	struct uart_state *state;
	//串口驱动对应的 tty 驱动
	struct tty_driver *tty_driver;

struct console 对象

console 对象用于描述一个控制台驱动,其核心成员如下:

	//该 console 的名称,配合index字段使用,如果name为“ttySTM”,index字段为小于0,则可以和“console=ttySTMn“(n=0,1,2…)来确定index字段的值
	char name[16];
	//写函数
	void (*write)(struct console *, const char *, unsigned);
	//读函数
	int	(*read)(struct console *, char *, unsigned);
	//获取 console 对应的 tty 驱动
	struct tty_driver *(*device)(struct console *, int *);
	//初始化 console
	int (*setup)(struct console *, char *);
	//console 标志
	short flags;
	//console 索引,若小于 0 则由命令行参数确定
	short index;

struct uart_state 对象

uart_state 对象表示一个串口端口的状态信息,其核心成员如下:

	//串口端口所属的 tty 端口,主要包含 tty 端口的 buf 、端口操作函数等信息
	struct tty_port port;
	//串口端口,对应一个串口设备,主要包含串口的硬件操作函数等信息
	struct uart_port *uart_port;

struct uart_port 对象

uart_port 对象表示一个串口端口,其核心成员如下:

	//配置 RS485
	int (*rs485_config)(struct uart_port *, struct serial_rs485 *rs485);
	//中断号
	unsigned int irq;
	//串口基准时钟
	unsigned int uartclk;
	//发送 FIFO 大小
	unsigned int fifosize;
	//流控字符
	unsigned char x_char;
	//IO 类型
	unsigned char iotype;
	//串口标志
	upf_t flags;
	//操作函数集合
	const struct uart_ops *ops;
	//端口索引
	unsigned int line;
	//寄存器逻辑基地址
	resource_size_t mapbase;
	//寄存器映射大小
	resource_size_t mapsize;
	//所属父设备
	struct device *dev;
	//RS485 配置信息
	struct serial_rs485 rs485;

struct tty_driver 对象

tty_driver 对象用于表示一个 tty 驱动,其核心成员如下:

	//幻数,用于检查结构体是否是一个 tty_driver
	int magic;
	//cdev 指针数组,用于关联 tty_port 的字符设备驱动
	struct cdev **cdevs;
	//所属模块
	struct module *owner;
	//驱动名称
	const char *driver_name;
	//设备名称,对于非 TTY_DRIVER_TYPE_PTY 类型的 tty_port ,其设备文件名以此为基础生成
	const char *name;
	//主设备号,为0表示系统自动分配
	int major;
	//起始次设备号,即第一个 tty_port 的次设备号
	int minor_start;
	//此驱动支持的 tty_port 个数
	unsigned int num;
	//tty 类型
	short type;
	//tty 子类型
	short subtype;
	//初始配置参数
	struct ktermios init_termios;
	//tty 驱动标志
	unsigned long flags;
	//tty_struct 指针数组
	struct tty_struct **ttys;
	//tty_port 指针数组
	struct tty_port **ports;
	//ktermios 指针数组
	struct ktermios **termios;
	//tty 驱动操作函数接口
	const struct tty_operations *ops;

串口驱动注册过程

串口驱动注册包括两个主要步骤,分别是注册串口驱动和在串口驱动下添加串口端口。
5. 串口驱动注册过程

构建并初始化 struct uart_driver
	调用 uart_register_driver 函数注册 struct uart_driver
		根据 struct uart_driver 中的端口数量为其分配 struct uart_statestruct uart_state 中包含一个 struct tty_port 和一个 struct uart_port
		调用 alloc_tty_driver 函数分配一个 struct tty_driver
			通过宏定义调用 __tty_alloc_driver 函数
			分配 struct tty_driver,并初始化
			为前面分配的 struct tty_driver 分配 struct tty_structstruct tty_portstruct ktermiosstruct cdev 指针数组(这里只分配了指针)
		将分配的 struct tty_driverstruct uart_driver 关联
		利用 struct uart_driver 初始化前面分配的 struct tty_driver
		调用 tty_set_operationsstruct uart_driver 函数设置 struct tty_driver 的操作函数集合
		循环调用 tty_port_init 函数初始化 struct uart_state *state 中的 struct tty_port port 
		调用 tty_register_driver 函数将 struct uart_driver 中的 struct tty_driver *tty_driver 注册到系统中

注册 uart_driver 的本质就是注册 tty_driver ,只是这个 tty_driver 属于uart_driver ,且对应的的操作函数集合具体操作对象为 uart_driver ( uart_driver 继承于 tty_driver )。
6. 在串口驱动下添加串口端口的过程

先注册 struct uart_driver ,然后构建并初始化 struct uart_port
	调用 uart_add_one_port 函数在 struct uart_driver 中添加一个 struct uart_port
	根据 struct uart_port 中的 unsigned int line 成员从 struct uart_driver 中找到对应的 struct uart_statestruct uart_state 中的struct tty_port
	初始化对应的 struct uart_state ,并将其与 struct uart_port 进行关联
	通过传入的 struct uart_driver 初始化 struct uart_port,其中包括次设备号、端口名称、端口的 console
	调用 tty_port_link_device 函数,将 struct uart_state 中的 struct tty_portstruct uart_driver 中的 struct tty_driver 进行关联
	调用 uart_configure_port 函数对 struct uart_port *uport 进行一些配置
		如果 struct uart_port 的 iobase 、 mapbase 、 membase 均为 0 则直接退出
		如果 struct uart_port 的 flags 设置了 UPF_BOOT_AUTOCONF 则调用 config_port 函数配置串口硬件
		如果 struct uart_portstruct console *cons 不为空,且还未使能则调用 register_console 注册 console
			利用启动命令行参数中的 console_cmdline 参数进行匹配,匹配成功则设置 console 的 index ,然后调用 console 的 setup 函数进行硬件初始化
			将 console 添加到 console_drivers 链表中
	设置 struct tty_port 中的 console 属性(需要 struct uart_port 的 line 与 struct console 的 index 一致才设置,而 struct console 的 index 值可以由 bootargs 参数确定)
	调用 tty_port_register_device_attr_serdev 函数注册 struct tty_port
		调用 tty_register_device_attr 函数注册 tty 设备
			调用 tty_line_name 生成设备文件名
			分配一个 device 对象,并对其初始化(这里配置了设备号和设备名,注册后会创建设备文件)
			调用 device_register 注册分配的 device
			调用 tty_cdev_add 注册字符设备驱动
				分配 cdev 对象,并进行初始化,主要配置操作函数集合
				调用 cdev_add 将分配 cdev 对象注册到内核

在 uart_driver 下添加 uart_port 的本质就是在其对应的 tty_driver 下添加 tty_port ,只不过事先通过 uart_state 将 uart_port 和 tty_port 进行了关联

串口打开过程

添加端口的时注册一个 cdev ,并设置其操作函数集合为 tty_fops ,其中提供了 tty_open 函数,在应用层执行 open 时会通过虚拟文件系统调用到此函数,其执行过程如下:

在应用层执行 open 时会通过虚拟文件系统调用 tty_open
	执行 tty_open_current_tty 函数尝试打开进程所属的 tty (其设备文件名是/dev/tty,设备ID应该是MKDEV(5, 	0)),对于打开串口此函数会执行失败
	tty_open_current_tty 函数执行失败后会执行 tty_open_by_driver 函数来打开 tty
		执行 tty_lookup_driver 函数找到对应的 tty_driver
		执行 tty_driver_lookup_tty 函数从 tty_driver 找到对应的 tty_struct ,对于打开过的 tty 会执行成功,然后对 tty 进行检查和设置,并返回对应的 tty
		tty_driver_lookup_tty 函数未找到 tty 则调用 tty_init_dev 函数分配 tty_struct 并进行相应的配置
			调用 alloc_tty_struct 分配 tty_strct
				调用 kzalloc 分配 tty_strct
				调用 tty_ldisc_init 绑定行规程
			调用 tty_driver_install_tty 将 tty_strct 安装到 tty_driver 中
			调用 tty_ldisc_setup 对配置行规程
	通过 tty->ops->open 指针调用 serial_core.c 中 的 uart_open 函数
		调用 tty_port_open 打开对应端口
			通过 port->ops->activate 调用 serial_core.c 中的 uart_port_activate 函数
				调用 uart_startup 函数启动串口
					调用 uart_port_startup 函数启动串口
						通过 uport->ops->startup 调用串口驱动提供的 startup 函数

串口读过程

读串口数据可分为两部分:
7. 应用程序从行规程中读取数据

与执行 open 过程类似,在应用层执行 read 时会通过虚拟文件系统调用 tty_read 函数,其执行过程如下:
	在应用层执行 read 时会通过虚拟文件系统调用 tty_read
		通过 tty_ldisc_ref_wait 函数得到 tty 的行规程
		通过 ld->ops->read 调用行规程的 read 函数,这里通常是 N_TTY中的 n_tty_read 函数
			在 n_tty_read 函数中无数据则休眠等待数据,有数据则将数据拷贝到应用层
  1. 串口向行规程上报数据
串口收到数据进入中断程序
	中断程序从硬件读取数据
	调用 tty_insert_flip_string 函数将数据存入 tty_port 的 tty_buffer 中
	调用 tty_flip_buffer_push 函数通知行规程处理数据
		调用 tty_schedule_flip 函数启动数据处理
			调用 queue_work 函数启动一个工作队列处理数据,这里的工作队列处理函数为 flush_to_ldisc 函数

串口写过程

在应用层执行 write 时会通过虚拟文件系统调用 tty_write 函数,然后 tty_write 函数通过绑定的行规程调用到行规程的 n_tty_write 函数,接下来由行规程对数据处理后调用 serial_core.c 中的 uart_flush_chars 函数启动发送,如果行规程设置原始模式则不进行处理,直接调用 serial_core.c 的 uart_write 函数启动发送(发送过程由串口的中断或 DMA 完成),其流程如下:

在应用层执行 write 时会通过虚拟文件系统调用 tty_write 函数
	通过 tty_ldisc_ref_wait 函数获得与tty绑定的行规程
	调用 do_tty_write 函数执行数据发送操作(在执行 do_tty_write 函数传入的是行规程 write 函数作为实际的写函数)
		通过 copy_from_user 函数将数据拷贝到 write_buf 中
		通过 write 指针调用行规程的 write 函数,这里对应的是 n_tty_write 函数
			如果设置了 OPOST 标志则对数据进行处理后调用 tty 操作函数集合中的 flush_chars 函数发送数据,这里实际对应的是 uart_flush_chars 函数
				调用 uart_start 启动串口发送
					调用 __uart_start 函数
						调用 uart_port 中的 start_tx 函数,即硬件驱动提供的发送启动函数
			如果没有设置 OPOST 标志则调用 tty 操作函数集合中的 write 函数发送数据,这里实际对应的是 uart_write 函数
				将数据写入到 uart_state 的 buf 中后在调用 __uart_start 启动串口发送
					调用 uart_port 中的 start_tx 函数,即硬件驱动提供的发送启动函数
	通过 tty_ldisc_deref 释放对行规程的占用

console 注册过程

在前面的串口驱动注册过程已经介绍了 console 的注册,这里在简单梳理一下 console 的注册步骤:

构建并初始化 struct console 对象
将 struct console 对象的地址给 struct uart_driver 的 cons 成员
调用 uart_register_driver 函数注册 struct uart_driver
调用 uart_add_one_port 函数添加一个串口端口,此时会顺便完成对 console 的注册
	将 struct uart_driver 的 cons 赋给 struct uart_port 的 cons
	调用 uart_configure_port 函数
		若 struct uart_port 的 cons 成员有效,且未使能则调用 register_console 函数注册 console
			利用启动命令行中的 console_cmdline 参数进行匹配,匹配成功则设置 console 的 index ,然后调用 setup 函数进行配置
			将新注册的 console 添加到 console_drivers 链表中
	当 struct uart_port 的 line 与其 console 的 index 一致则设置 struct tty_port 的 console 标志(因此在多个 uart_port 中只有一个 uart_port 真正的拥有 struct uart_driver 中的 console)

printk 执行流程

printk 的执行流程大致如下:

通过 va_start 取出不定参数列表,然后调用 vprintk_func
	然后调用 vprintk_default 函数
		再调用 vprintk_emit 函数
			调用 vprintk_store 函数
				使用 vscnprintf 进行格式化处理
				调用 log_output 输出数据(并未通过硬件输出,实际上是将数据存储在 log_buf 中)
			调用 console_unlock 函数,可能是直接调用,也可能是通过 wake_up_klogd 函数唤醒工作队列,然后由工作队列处理函数去调用
				通过 log_from_idx 从 log_buf 取出一个 msg
				如果 msg 优先级不够则跳过
				通过 call_console_drivers 将数据从 console 输出,系统中可能会有多个 console

printk 使用

	/*
	 * fmt 格式字符串,其前面还包含描述优先级的字符,它们的定义如下:
	 *     ASCII的标题开始字符(SOH),表示后面是优先级字符
	 *     #define KERN_SOH			"\001"
	 *     不同优先级定义,数字越小优先级越高
	 *     #define KERN_EMERG		KERN_SOH "0"
	 *     #define KERN_ALERT		KERN_SOH "1"
	 *     #define KERN_CRIT		KERN_SOH "2"
	 *     #define KERN_ERR			KERN_SOH "3"
	 *     #define KERN_WARNING		KERN_SOH "4"
	 *     #define KERN_NOTICE		KERN_SOH "5"
	 *     #define KERN_INFO		KERN_SOH "6"
	 *     #define KERN_DEBUG		KERN_SOH "7"
	 * ... 不定参数列表,与格式字符串有关
	 **/
	int printk(const char *fmt, ...)

设置系统的输出优先级:
系统输出等级存储在 /proc/sys/kernel/printk 文件中,它有4个参数,依次是:控制台消息级别、默认信息级别、最小控制台级别、默认控制台级别

early_printk

当在 uboot 的命令行参数中传入 earlyprintk 且在内核配置选项中使能 Early printk 选项后系统会调用 setup_early_printk 函数创建一个 console (此 console 的 write 函数最终会调用 printascii 输出数据, STM 官方内核已经通过汇编实现 printascii 函数),用于在串口初始化完成以前输出调试信息,具体使用步骤如下:

  1. 配置内核
Kernel hacking  ---> 
	[*] Kernel low-level debugging functions (read help!)
	(0x40010000) Physical base address of debug UART (NEW)
	(0xfe010000) Virtual base address of debug UART (NEW)
	[*] Early printk
  1. 在 bootargs 中加入 earlyprintk
    earlyprintk 注册过程:
当 bootargs 中包含 earlyprintk 且内核配置选项中使能 Early printk 时在系统初始化阶段会调用 setup_early_printk
	调用 register_console 注册一个 console ,用于系统早期输出(通过 console 通过的 write 函数输出,在STM32中此 write 函数最终会调用 printascii 输出数据)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/295999.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

矩阵的乘法

首先矩阵的乘法定义如下&#xff1a; #include <stdio.h> int main() { int i 0; int j 0; int arr[20][20] { 0 }; int str[20][20] { 0 }; int s[20][20] { 0 }; int n1 0; int n2 0; int m2 0; int z 0; int m1 0;…

使用IDEA官方docker插件构建镜像

此方法同样适用于jetbrains系列的其他开发软件 在IDEA中&#xff0c;如果是maven项目&#xff0c;可以使用插件 <plugin><groupId>com.spotify</groupId><artifactId>docker-maven-plugin</artifactId><version>1.2.2</version> &…

用于查询性能预测的计划结构深度神经网络模型--大数据计算基础大作业

用于查询性能预测的计划结构深度神经网络模型 论文阅读和复现 24.【X1.1】 在关系数据库查询优化领域&#xff0c;对查询时间的估计准确性直接决定了查询优化结果&#xff0c;进而影响到数据库整体的查询效率。但由于数据库自身的复杂性&#xff0c;查询时间受到数据分布、数据…

Linux操作实例 – 输入输出重定向

Linux操作实例 – 输入输出重定向 Input & Output Redirection Examples in Linux By Jackson 1. 前言 在操作计算机的时候&#xff0c;我们能够很容易通过键盘、鼠标给计算机输入信息&#xff08;例如&#xff1a;写公文、邮件&#xff0c;同时通过显示器得到输出。这就…

【AI视野·今日Sound 声学论文速览 第三十九期】Tue, 2 Jan 2024

AI视野今日CS.Sound 声学论文速览 Tue, 2 Jan 2024 Totally 7 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Sound Papers Enhancing Pre-trained ASR System Fine-tuning for Dysarthric Speech Recognition using Adversarial Data Augmentation Authors Huimen…

数据安全保护体系的设计原则

目录 引言 数据的分类分级原则 数据的分类分级是个长期且动态的过程 数据的分类分级应结合实际应用和业务特性 建立数据分类分级制度和体系也是非常重要的 最小化原则 企业需要对数据访问的用户进行身份验证 企业需要明确用户访问数据的目的是什么 企业需要梳理数据访问…

CentOS 7 安装 PPTP

环境&#xff1a; 阿里云试用机&#xff1a; 外网IP&#xff1a;114.55.80.150 内网IP&#xff1a;172.28.11.92 一、服务器安装 PPTP 1、安装 yum install epel-release -y 2、安装pptp yum install pptpd iptables-services -y 3、修改配置 vim /etc/pptpd.conf# 最…

DS|二叉树

题目一&#xff1a;DS二叉树 -- 二叉树构建与遍历 题目描述&#xff1a; 给定一颗二叉树的逻辑结构如下图&#xff0c;&#xff08;先序遍历的结果&#xff0c;空树用字符‘#’表示&#xff0c;例如AB#C##D##&#xff09;&#xff0c;建立该二叉树的二叉链式存储结构&#xf…

【面试高频算法解析】算法练习5 深度优先搜索

前言 本专栏旨在通过分类学习算法&#xff0c;使您能够牢固掌握不同算法的理论要点。通过策略性地练习精选的经典题目&#xff0c;帮助您深度理解每种算法&#xff0c;避免出现刷了很多算法题&#xff0c;还是一知半解的状态 专栏导航 二分查找回溯&#xff08;Backtracking&…

【代码随想录】刷题笔记Day46

前言 刚考完自辩&#xff0c;Chat回答举例什么的真方便。早上做组会PPT去了&#xff0c;火速来刷题&#xff01; 139. 单词拆分 - 力扣&#xff08;LeetCode&#xff09; 单词是物品&#xff0c;字符串s是背包&#xff0c;单词能否组成字符串s&#xff0c;就是问物品能不能把…

1.3进制,码(8421),化简规则、卡诺图化简、性质,触发器(转换与设计、应用),电路图,电路设计

十进制与原码、反码、补码之间的转换 正数的原码、反码、补码相同&#xff0c;符号位为0 负数的原码为、符号位1&#xff0c;二进制数 反码&#xff0c;符号位不变、其它取反&#xff0c; 补码为&#xff1a;反码最低有效位1 运算 卡诺图化简 奇偶校验码 检查1的个数&…

使用CentOS 7.6搭建HTTP隧道代理服务器

在现代网络环境中&#xff0c;HTTP隧道代理服务器因其灵活性和安全性而受到广泛关注。CentOS 7.6&#xff0c;作为一个稳定且功能强大的Linux发行版&#xff0c;为搭建此类服务器提供了坚实的基础。 首先&#xff0c;我们需要明确HTTP隧道代理的基本原理。HTTP隧道代理允许客户…

字节填充与0比特填充以及数据链路的基本问题

目录 字节填充&#xff1a; 比特填充&#xff1a; 数据链路有三个基本问题 1.封装成帧 2.透明传输 3.差错检测 首先介绍一下PPP的帧结构&#xff1a; 首部的第一个字段和尾部的第二个字段都是标志字段F(Flag)&#xff0c;规定为0x7E (符号“0x”表示它后面的字符是用十六…

python练习3【题解///考点列出///错题改正】

一、单选题 1.【单选题】 ——可迭代对象 下列哪个选项是可迭代对象&#xff08; D&#xff09;&#xff1f; A.(1,2,3,4,5) B.[2,3,4,5,6] C.{a:3,b:5} D.以上全部 知识点补充——【可迭代对象】 可迭代对象&#xff08;iterable&#xff09;是指可以通过迭代&#xff…

发票信息提取v1.2.0

程序介绍 “发票信息提取”是一款用于提取电子发票的PDF、XML文件中的开票信息到excel表格的软件&#xff0c;无需联网及进行复杂配置&#xff0c;打开即用。目前支持增值税电子发票&#xff08;非数电票&#xff09;原始PDF文件&#xff0c;及数电票的XML文件。 更新内容 增加…

【I2C】i2c-tools工具使用,以及开发调试

i2c调试 eeprom 手动创建eeprom设备调试&#xff0c;例如0x50 是FRU的地址&#xff0c;i2c-3是bus 创建设备 echo 24c32 0x50 > /sys/bus/i2c/devices/i2c-4/new_device如果设备正确&#xff0c;将成功被创建&#xff0c;并且生成/sys/bus/i2c/devices/4-0050/eeprom&am…

智能语音机器人NXCallbot

受出海公司业务全球化的影响&#xff0c;智能客服逐渐从便捷应用变为市场刚需。新基建七大领域中&#xff0c;人工智能及场景应用的基础建设是最核心的领域&#xff0c;而智能客服作为商业化实际应用的核心场景之一&#xff0c;能提升企业运营效率&#xff0c;为行业客户赋能。…

智能分析网关V4在工业园区周界防范场景中的应用

一、背景需求分析 在工业产业园、化工园或生产制造园区中&#xff0c;周界防范意义重大&#xff0c;对园区的安全起到重要的作用。常规的安防方式是采用人员巡查&#xff0c;人力投入成本大而且效率低。周界一旦被破坏或入侵&#xff0c;会影响园区人员和资产安全&#xff0c;对…

“编程界的隐形斗篷:C语言作用域与生命周期的喜怒哀乐”

少年们&#xff0c;大家好。我是博主那一脸阳光。 前言&#xff1a;理解C语言作用域与生命周期&#xff0c;犹如掌握了变量在程序中的“活动地带”与“存活时刻”&#xff0c;有助于避免数据冲突、优化内存使用、提升代码质量和模块化程度&#xff0c;增强程序稳定性和安全性…

windows下使用PowerShell切割大数据文件

测试文件为24.4G文件 打开PowerShell窗口&#xff0c;使用以下命令 $filePath 为指向文件路径 $outputPath 输出到指定文件夹 $chunkSize 单个文件控制切割大小 将命令修改完后&#xff0c;直接粘贴到powershell窗口&#xff0c;点击回车即可进行切割 $filePath "D:\…
最新文章