一、前言
如何VSCode搭建Linux驱动开发环境 参考:
Visual Studio Code搭建linux驱动开发环境_vscode linux driver 驱动配置-CSDN博客
Linux驱动开发入门(二)VS code驱动开发配置-CSDN博客
linux驱动开发 | 新建工程和vscode配置_vscode开发linux驱动,如何将源码添加到工程中-CSDN博客
Visual Studio Code是一个很优秀的开发软件,但我们并不能直接用来驱动开发。因为驱动开发所需要的头文件来自Linux内核,这就要求我们自己添加内核文件。
Linux 头文件大全(作用介绍)_#include "config.h-CSDN博客
Linux上使用VSCode无法打开源文件_linux vscode无法打开源文件iostream-CSDN博客
#include<>和#include“”的区别_include <>-CSDN博客
【C语言指针】char* argv[] 、char **argv、命令行传参_char**argv_郭同学如是说的博客-CSDN博客
二、嵌入式Linux LED驱动开发实验
Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器,与裸机实验不同的是,在 Linux 下编写驱动要符合 Linux的驱动框架。开发板上的 LED0 连接到 RV1126 的 GPIO3_D4 这个引脚上。
在驱动程序中直接写入寄存器地址,进行配置
一、前言知识:
1.地址映射
利用内存管理单元MMU完成虚拟空间到物理空间的映射;
ioremap 函数用于获取指定物理地址空间对应的虚拟地址空间;
卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射。
2.I/O内存访问函数(访问映射后的虚拟空间内存地址)
读操作函数:readb、readw 和 readl 三个函数分别对应 8bit、16bit 和 32bit 读操作;
写操作函数:writeb、writew 和 writel 这三个函数分别对应 8bit、16bit 和 32bit 写操作。
二、驱动代码
1.#define 宏定义中包括
- 主设备号、设备名字
- LEDON、LEDOFF
- GPIO3_D4的引脚复用寄存器地址(设置为GPIO功能)
- 引脚驱动能力寄存器地址(设置为0级)
- GPIO输入输出寄存器地址(设置为输出)
- GPIO引脚高低电平寄存器地址(设置为高/低电平)
2./*LED打开、关闭*/
void led_switch(u8 sta)
通过设置改变GPIO引脚高低电平寄存器的值:
3./*物理地址映射*/
void led_remap(void)
通过ioremap函数获取寄存器对应的虚拟地址:
......
4./*取消映射*/
void led_remap(void)
通过iounmap函数在卸载驱动时,取消映射:
......
5./*打开设备*/
static int led_open(struct inode *inode, struct file *filp)
空函数,一般在此函数中将设备结构体作为参数 filp 的私有数据(后续实验)
6./*从设备读数据*/
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
空函数,若想在应用程序中读取 LED 的状态
7./*向设备写数据*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
根据应用程序中写入的参数,打开/关闭LED灯
8./*释放设备*/
static int led_release(struct inode *inode, struct file *filp)
9./* 设备操作函数 */
static struct file_operations led_fops = {......}
10./*驱动入口函数*/
static int __init led_init(void)
/* 1 、寄存器地址映射 */
/* 2 、设置 GPIO3_D4 为 GPIO 功能 */
/* 3 、设置 GPIO3_D4 驱动能力为 level0 */
/* 4 、设置 GPIO3_D4 为输出 */
/* 5 、设置 GPIO3_D4 为低电平,关闭 LED 灯。 */
/* 6 、注册字符设备驱动 */
11./*驱动出口函数*/
static void __exit led_exit(void)
/* 1. 取消映射 */
/* 2. 注销字符设备驱动 */
三、新字符设备驱动实验
一、前言知识:
1.分配和释放设备号
register_chrdev 和 unregister_chrdev 这两个函数是老版本驱动使用的函数,现在新的
字符设备驱动已经不再使用这两个函数。
使用 register_chrdev 函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会
带来两个问题:
①、需要我们事先确定好哪些主设备号没有使用。
②、会将一个主设备号下的所有次设备号都使用掉。
解决方法: 使用设备号的时候向 Linux 内核申请,由 Linux 内核分配设备可以使用的设备号
1.如果没有指定设备号的话就使用如下函数来申请设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
2.如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
3.统一使用如下释放函数:
void unregister_chrdev_region(dev_t from, unsigned count)
2.新的字符设备注册方法
在 Linux 中使用 cdev 结构体表示一个字符设备,struct cdev test_cdev,定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化,cdev_add 函数用于向 Linux 系统添加字符设备,卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备。
3.自动创建设备节点
当我们使用 modprobe 加载驱动程序以后还需要使用命令 “mknod ”手动创建设备节点,在驱动中实现 自动创建设备节点的功能以后,使用 modprobe 加载驱动模块成功的话就会自动在 /dev 目录下 创建对应的设备文件。
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。
4.设置文件私有数据
对于一个设备的所有属性信息我们最好将其做 成一个结构体。编写驱动 open 函数的时候将设备结构体作为私有数据添加到设备文件中。
在 open 函数里面设置好私有数据以后,在 write 、 read 、 close 等函数中直接读取 private_data 即可得到设备结构体
二、驱动代码
1.#define 宏定义中包括:
与实验一相同,仍需在驱动代码中直接写入寄存器地址,来进行GPIO设置
2./* newchrled 设备结构体 */
3./*LED打开、关闭*/
4./*物理地址映射*/
5./*取消映射*/
6./*打开设备*/
file 结构体有个叫做 private_data 的成员变量 一般在 open 的时候将 private_data 指向设备结构体,设置私有数据 。
7./*从设备读数据*/
8./*向设备写数据*/
9./*释放设备*/
10./* 设备操作函数 */
11./*驱动入口函数*/
/* 1 、寄存器地址映射 */
/* 2 、设置 GPIO3_D4 为 GPIO 功能 */
/* 3 、设置 GPIO3_D4 驱动能力为 level0 */
/* 4 、设置 GPIO3_D4 为输出 */
/* 5 、设置 GPIO3_D4 为低电平,关闭 LED 灯。 */
/* 6 、注册字符设备驱动 */
/* 1 、创建设备号 */
...... /* 有 定义设备号 */
newchrled . devid = MKDEV ( newchrled . major , 0 );
ret = register_chrdev_region ( newchrled . devid , NEWCHRLED_CNT , NEWCHRLED_NAME );
......
else { /* 没有定义设备号 */
ret = alloc_chrdev_region (& newchrled . devid , 0 , NEWCHRLED_CN NEWCHRLED_NAME );
......
newchrled . major = MAJOR ( newchrled . devid ); /* 获取主设备号 */
newchrled . minor = MINOR ( newchrled . devid ); /* 获取次设备号 */
/* 2、新的字符设备注册方法:初始化 cdev ,添加cdev*/
/* 4、自动创建设备节点方法:创建类 ,创建设备*/
12.*驱动出口函数*/
/* 1. 取消映射 */
/* 2.注销字符设备驱动 删除cdev,释放newchrled 设备,删除设备device,删除class类*/
四、设备树
一、前言知识
在新版本的 Linux 中,ARM 相关的驱动全部采用了设备树。
设备树(Device Tree),描述设备树的文件叫做 DTS(Device Tree Source),这个 DTS 文件采用树形结构描述板级设备,也就是开发板上的设备信息,从 Linux 内中分离开来,用一个专属的文件格式来描述,文件扩展名为.dts。
一个 SOC 可以作出很多不同的板子, 这些不同的板子肯定是有共同的信息,将这些共同的信息提取出来作为一个通用的文件,其他 的.dts 文件直接引用这个通用文件即可,这个通用文件是 .dtsi 文件,类似于 C 语言中的头文件。
一般.dts 描述板级信息 ( 也就是开发板上有哪些 IIC 设备、 SPI 设备等 ) , .dtsi 描述 SOC 级信息( 也就是 SOC 有几个 CPU 、主频是多少、各个外设控制器信息等 ) 。
DTS 是设备树源码文件, DTB 是将 DTS 编译以后得到的二进制文件,将.dts 编译为.dtb需要用到 DTC 工具,使用 RV1126 新做了一个板子,只需要 新建一个此板子对应的.dts 文件,然后将对应的.dtb 文件名添加到 dtb-$( CONFIG_ARCH_ROCKCHIP)下,这样在 编译设备树的时候就会将对应的.dts 编译为二进制的.dtb 文件。
1.DTS语法
和 C 语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi,也可以引用 .h 文件头件,甚至也可以引用 .dts 文件,只是,我们在编写设备树头文件的时候最好选择.dtsi 后缀。
一般.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART 、 IIC 等等。
rv1126.dtsi 文 件中不仅仅描述了CPU 信息, RV1126 这颗 SOC 所有的外设都描述的清清楚楚。
“ / ”是根节点,每个设备树文件只有一个根节点。在 rv1126.dtsi、 rv1126-alientek.dts 和 rv1126-alientek.dtsi 这三个文件都有一个“ /”根节点,这些“ / ”根节点的内容会合并成一个根节点。
设备树中节点命名格式如下:
node-name@unit-address :cpu@f00
label: node-name@unit-address :
cpu0:cpu@f00
引入 label 的目的就是为了方便访问节点,可以直接通过 &label 来访问这个节点,比如通过 &cpu0 就可以访问“ cpu@f00 ”这个节点。
每个节点都有不同属性,属性都是键值对,值可以为空或任意的字节流(字符串 、32 位无符号整数、字符串列表)
2.标准属性
1.compatible 属性
值是 一个字符串列表,compatible 属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序,格式如下所示:"manufacturer,model" ——“ 厂商, 模块对应的驱动名字”
compatible = "sony,imx415";
compatible = "ilitek,ili9881d", "simple-panel-dsi";
一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,用于与其匹配。
2.model 属性
描述开发板的名字或者设备模块信息:
model = "Rockchip RV1126 EVB DDR3 V13 Board";
3.status 属性
和设备状态有关的:
4.#address-cells 和#size-cells 属性
都是无符号 32 位整形,可以用在任何
拥有子节点的设备中,用于描述子节点的地址信息,
#address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位 ) , #size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位 ) 。
5.reg 属性
一般用于描 述设备地址空间资源信息或者设备地址信息。
和地址相关的信息有两种:起始地址和地址长度, reg 属性的格式为:
6.ranges 属性
ranges 是一个地址映射 / 转换表, ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成:
child-bus-address :子总线地址空间的物理地址,由父节点的 #address-cells 确定此物理地址
所占用的字长。
parent-bus-address : 父总线地址空间的物理地址,同样由父节点的 #address-cells 确定此物理地址所占用的字长。
length : 子地址空间的长度,由父节点的 #size-cells 确定此地址长度所占用的字长
7.name属性
name 属性用于记录节点名字, name 属性已经被弃用
8、device_type 属性
IEEE 1275 会用到此属性,用于描述设备的 FCode,但是设备树没有 FCode,所以被抛弃,此属性只能用于 cpu 节点或者 memory 节点。
3.向节点追加或修改内容
一旦硬件修改了,我们就要同步的修改设备树文件,毕竟设备树是描述板子硬件信息的文件。
在 i2c1 这个节点上添加一个 fxls8471子节点,打开文件 rv1126.dtsi 文件,最简单的方法就是在 i2c1 下直接添加一个名为 fxls8471 的子节点。
i2c1节点是定义rv1126.dtsi 文件中的,而 rv1126.dtsi 是共有的设备树头文件,其他所有使用到 RV1126 这颗 SOC 的板子都会引用 rv1126.dtsi 这个文件。
ATK-DLRV1126 开发板使用的设备树文件为 rv1126-alientek.dts 和 rv1126-alientek.dtsi,正确的做法就是在 rv1126-alientek.dts 中,向 i2c1 节点追加 fxls8471 相关的信息。
4.设备树在系统中的体现
Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的 /proc/devicetree 目录下根据节点名字创建不同文件夹。
/proc/device-tree 目录下是根节点“ /”的 所有属性和子节点,表现为一个个的文件,如“#address-cells”、“#size-cells”、“compatible”、“model”和“name” 5 个文件。
在根节点“ / ”中有两个特殊的子节点: aliases 和 chosen,aliases 节点的主要功能就是定义别名,目的就是为了方便访问节点;chosen 并不是一个真实的设备,chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。
5.设备树常用OF操作函数
想在 驱动中读取设备树的属性值,那么就必须使用 Linux 内核提供的众多的 OF 函数。
1.查找节点的 OF 函数:
- of_find_node_by_name 函数通过节点名字查找指定的节点;
- of_find_node_by_type 函数通过 device_type 属性查找指定的节点;
- of_find_compatible_node 函数根据 device_type 和 compatible 两个属性查找指定的节点;
- of_find_matching_node_and_match 函数通过 of_device_id 匹配表来查找指定的节点;
- of_find_node_by_path 函数通过路径来查找指定的节点;
2.查找父/子节点的 OF 函数:
- of_get_parent 函数用于获取指定节点的父节点(如果有父节点的话);
- of_get_next_child 函数用迭代的查找子节点;
3.提取属性值的 OF 函数 :
- of_find_property 函数用于查找指定的属性;
- of_property_count_elems_of_size 函数用于获取属性中元素的数量;
- of_property_read_u32_index 函数用于从属性中获取指定标号的 u32 类型数据值;
- of_property_read_u8_array 函数 of_property_read_u16_array 函数of_property_read_u32_array 函数 of_property_read_u64_array 函数 这 4 个函数分别是读取属性中 u8、u16、u32 和 u64 类型的数组数据;
-
of_property_read_u8 函数 of_property_read_u16 函数 of_property_read_u32 函数
of_property_read_u64 函数 有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整形值的属性,分别用于读取 u8 、 u16 、 u32 和 u64 类型属性值;
-
of_property_read_string 函数用于读取属性中字符串;
-
of_n_size_cells 函数用于获取 #size-cells 属性值;
-
of_n_addr_cells 函数用于获取#address-cells 属性值;
4.其他常用的 OF 函数
- of_device_is_compatible 函数用于查看节点的 compatible 属性是否有包含 name 指定的字符串;
- of_get_address 函数用于获取地址相关属性;
- of_translate_address 函数负责将从设备树读取到的物理地址转换为虚拟地址;
- of_address_to_resource 函数,本质上就是提取 reg 属性值,然后将其转换为 resource 结构体类型;
- of_iomap 函数用于直接内存映射,以前我们会通过 ioremap 函数来完成物理地址到虚拟地 址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址, 不需要使用 ioremap 函数了
五、设备树下的LED驱动实验
我们使用设备树来向 Linux 内核传递相关的寄存器物理地址, Linux 驱动文件使用上一章讲解的 OF 函数从设备树中获取所需的属性值,然后使用获取到的属性值来初始化相关的 IO 。
一、修改设备树文件,创建设备节点
打开 rv1126-alientek.dts 文件,在根节点“/ ”最后面输入:
reg 属性设置了驱动里面所要使用的寄存器物理地址;
./build.sh kernel 编译内核完成以后得到 zboot.img,进入 LOADER 模式进行烧写。
Linux 启动成功以后进入到 /proc/device-tree/目录中查看是否有 “rv1126_led”这个节点,进入/proc/device-tree/rv1126_led 目录中,可以查看一下都有哪些属性文件。
二、驱动代码编写
1.#define 宏定义中包括:
/* 设备号个数 */
/* 名字*/
LEDOFF、LEDON
此时不需要定义寄存器地址,而是在代码中进入设备树查找
2./* newchrled 设备结构体 */
3./*LED打开、关闭*/
5./*取消映射*/
不用ioremap 函数,直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址!
6./*打开设备*/
7./*从设备读数据*/
8./*向设备写数据*/
9./*释放设备*/
10./* 设备操作函数 */
11./*驱动入口函数*/
/* 获取设备树中的属性数据 */
/* 1 、获取设备节点: rv1126_led */
/* 2 、获取 compatible 属性内容 */
/* 4、获取 reg 属性内容 */
寄存器地址在reg属性中
/* 1 、寄存器地址映射 */
/* 2 、设置 GPIO3_D4 为 GPIO 功能 */
/* 3 、设置 GPIO3_D4 驱动能力为 level0 */
/* 4 、设置 GPIO3_D4 为输出 */
/* 5 、设置 GPIO3_D4 为低电平,关闭 LED 灯。 */
/* 6 、注册新字符设备驱动 */
/* 1 、创建设备号 */
/* 2 、初始化 cdev */
/* 3 、添加一个 cdev */
/* 4 、创建类 */
/* 5 、创建设备 */
12.*驱动出口函数*/
/* 1. 取消映射 */
/* 2.注销字符设备驱动 删除cdev,释放newchrled 设备,删除设备device,删除class类*/
六、pinctrl 和 gpio 子系统实验
Linux 是一个庞大而完善的系统, 尤其是驱动框架,像 GPIO 这种最基本的驱动不可能采用“原始”的裸机驱动开发方式。
Linux 内核提供了 pinctrl 和 gpio 子系统用于 GPIO 驱动。
一、pinctrl 子系统
Linux 驱动讲究 驱动分离与分层, 就是按照面向对象编程的设计思想而设计的设备驱动框架。
对于大多数的 32 位 SOC 而言,引脚的设置基本都是这两方面:
①、设置 PIN 的复用功能。
②、如果 PIN 复用为 GPIO 功能,设置 GPIO 相关属性。
因此 Linux 内核针对 PIN 推出了 pinctrl 子系统,对于 GPIO 的电气属性配置推出了 gpio 子系统。
大多数 SOC 的 pin 都是支持复用的。此 外我们还需要配置 pin 的电气特性,比如上 / 下拉、驱动能力等等。传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置方式比较繁琐、而且容易出问题( 比如 pin 功能冲突 ) 。
pinctrl 子系统就是为了解决这个问题而引入的, pinctrl 子系统主要工作内容如下:
①、获取设备树中 pin 信息。
②、根据获取到的 pin 信息来设置 pin 的复用功能
③、根据获取到的 pin 信息来设置 pin 的电气特性,如驱动能力。
对于我们使用者来讲,只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始
化工作均由 pinctrl 子系统来完成, pinctrl 子系统源码目录为 drivers/pinctrl 。
1、PIN 配置信息详解
要使用 pinctrl 子系统,我们需要在设备树里面设置 PIN 的配置信息。
打开 rv1126.dtsi 文件,找到一个叫做 pinctrl 的节点。
#address-cells 属性值为 1 和 #size-cells 属性值为 1 ,也就是说 pinctrl 下的
所有子节点的 reg 第一位是起始地址,第二位为长度。
reg 属性描述了GPIO3对应的寄存器地址,基地址就是 0XFF640000 ,驱动会得到 基地址 , 然后加上偏移就得到了 GPIO3 的其他寄存器地址。
看起来,根本没有 PIN 相关的具体配置,打开 rv1126-pinctrl.dtsi 文件,此文件需要编译内核以后才能得到,我们能找到如下内容:
示例代码 10.1.2.2 就是向 pinctrl 节点追加数据,不同的外设使用的 PIN 不同、其配置也不同,
每个 pincrtl 节点必须至少包含一个子节点来存放 pincrtl 相关信息,也就 pinctrl 集,这个 集合里面存放当前外设用到哪些引脚(PIN) 、复用配置、上下拉、驱动能力等。
一般这个存放pincrtl集的子节点名字是“rockchip,pins ”
引脚复用设置的格式如下:
1 、 PIN_BANK
就是 PIN 所属的组,设置 GPIO3_D4 这个 PIN ,那 PIN_BANK 就是 3 。
2 、 PIN_BANK_IDX
是组内的编号,以 GPIO3 组为例,共有 A0~A7 、 B0~B7 、 C0~C7、 D0~D7,
这 32 个 PIN。
要设置 GPIO3_D4 ,那么 PIN_BANK_IDX 就要设置为 RK_PD4
3 、 MUX
是设置 PIN 的复用功能,一个 PIN 最多有 16 个复用功能
要将 GPIO3_D4 设置为 GPIO 功能,那么 MUX 就设置 0或 RK_FUNC_GPIO
4 、 phandle
描述一些引脚的通用配置信息
GPIO3_D4 这个 PIN 用作普通的 GPIO ,不需要配置驱动能力,那么就可以使用 pcfg_pull_none 。
最总,如果要将 GPIO3_D4 设置为 GPIO 功能,那么配置就是:
rockchip,pins =<3 RK_PD4 RK_FUNC_GPIO &pcfg_pull_none>;
2 设备树中添加 pinctrl 节点模板
1 、创建对应的节点
2 、添加 “ rockchip,pins ” 属性
添加一个“ rockchip,pins ”属性,这个属性是真正用来描述 PIN 配置信息的
对于 RV1126 而言,如果一个 PIN 用作 GPIO 功能 的时候不需要创建对应的 pinctrl 节点!
2. gpio 子系统
pinctrl 子系统重点是设置 PIN 的复用和电气属性,如果 pinctrl 子系统将一个 PIN 复用为 GPIO 的话,那么接下来就要用到 gpio 子系统了。
gpio 子系统用于初始化 GPIO 并且提供相应的 API 函数,主要目的就是方便驱动开发者使用 gpio ,驱动 开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API 函数来操作 GPIO 。
1、设备树中的 gpio 信息
打开 rv1126.dtsi,在里面找到如下所示内容:
- compatible 属性值为“rockchip,gpio-bank”
- reg 属性设置了 GPIO3 控制器的寄存器基地址为 0XFF640000。
- interrupts 属性描述 GPIO3 控制器对应的中断信息。
- clocks 属性指定这个 GPIO3 控制器的时钟。
- “gpio-controller”表示 gpio3 节点是个 GPIO 控制器,每个 GPIO 控制器节点必
- 须包含“gpio-controller”属性。
- “#gpio-cells”属性和“#address-cells”类似,#gpio-cells 应该为 2,表示一共有两个 cell,第一个 cell 为 GPIO 编号,第二个 cell 表 示 GPIO 极 性。
将 GPIO4_A1 用作 CSI1 摄像头的 RESET 引脚,GPIO4_A1 复用为 GPIO 功能,通过控制这个 GPIO 的高低电平就可以复位 CSI1 摄像头。
这里肯定需要设备树来告诉驱动,在设备树中的 CSI1 摄像头节点下添加一个属性来描述摄像头的 RESET 引脚就行了,CSI1 摄像头驱动直接读取这个属性值就知道摄像头的 RESET 引脚使用的是哪个 GPIO 了。
在rv1126-alientek.dts 中找到名为“ i2c5 ”的节点,这个节点包含了所有连接到 I2C5 接口上的设备, 如下所示:
属性“ reset-gpios ”描述了 IMX415 这个摄像头的 RESET 引脚使用的哪个 IO 。
“ &gpio4 ”表示 RESET 引脚所使用的 IO 属于 GPIO4 组,
“ RK_PA1 ”表示 GPIO4 组的 AP1
“ GPIO_ACTIVE_LOW ”
2. gpio 子系统 API 函数
gpio 子系统向驱动开发人员屏蔽了具体的读写寄存器过程。这就是驱动分层与分离的好。
gpio 子系统提供的常用的 API 函数有下面几个:
1 、 gpio_request 函数
用于申请一个 GPIO 管脚
int gpio_request(unsigned gpio, const char *label)
2 、 gpio_free 函数
调用 gpio_free 函数进行释放。
void gpio_free(unsigned gpio)
3 、 gpio_direction_input 函数
此函数用于设置某个 GPIO 为输入。
int gpio_direction_input(unsigned gpio)
4 、 gpio_direction_output 函数
此函数用于设置某个 GPIO 为输出,并且设置默认输出值
int gpio_direction_output(unsigned gpio, int value)
5 、 gpio_get_value 函数
此函数用于获取某个 GPIO 的值 (0 或 1)
int gpio_get_value(unsigned int gpio)
。
6 、 gpio_set_value 函数
此函数用于设置某个 GPIO 的值
void gpio_set_value(unsigned int gpio, int value)
3. 设备树中添加 gpio 节点模板
LED0 连接到了 GPIO3_D4 引脚上,首先创建一个“ led ”设备节点。
1 、创建 led 设备节点
在根节点“ / ”下创建 led 设备子节点,如下所示:
2 、添加 GPIO 属性信息
在 gpioled 节点中添加 GPIO 属性信息,表明所使用的 GPIO 是哪个引脚
4. 与 gpio 相关的 OF 函数
在驱动程序中需要读取 gpio 属性内容, Linux 内核提供了几个与 GPIO 有关的 OF 函数,常用的几个 OF 函数如下所示:
1 、 of_gpio_named_count 函数
用于获取设备树某个属性里面定义了几个 GPIO 信息,要注意的是空的 GPIO 信息也会被统计到。
int of_gpio_named_count(struct device_node *np, const char *propname)
2 、 of_gpio_count 函数
此函数统计的是“ gpios ”这个属 性的 GPIO 数量,
int of_gpio_count(struct device_node *np)
3 、 of_get_named_gpio 函数
此函数获取 GPIO 编号
此函数会将设备树中类似 <&gpio4 RK_PA1 GPIO_ACTIVE_LOW> 的属性信息转换为对应的 GPIO 编号,此函数在驱动中使用很频繁!
函数原型如下:
int of_get_named_gpio(struct device_node *np, const char *propname, int index)
3 驱动程序编写
首先我们要检查一下 GPIO3_D4 对应的 GPIO 有没有被其他外设使用,如这个 GPIO 已经被分配给了其他外设,那么我们驱动在申请这个 GPIO 就会失败。
因为当前开发板系统将 GPIO3_D4 这个 IO 分配给了内核自带的 LED 驱动做心跳灯了。所以需要先关闭 GPIO3_D4 作为心跳灯这个功能,也就是将 GPIO3_D4 对应的 GPIO 释放出来。打开 rx1126-alientek.dtsi 文件
设备树编写完成以后使用“ ./build.sh kernel ”命令重新编译并打包内核,然后将新编译出来的 zboot.img 烧写到开发板中。启动成功以后进入“ /proc/device-tree ”目录中查看“ gpioled ”节 点是否存在,如果存在的话就说明设备树基本修改成功( 具体还要驱动验证 )
1.#define 宏定义中包括:
/* 设备号个数 */
/* 名字*/
LEDOFF、LEDON
此时不需要定义寄存器地址,而是在代码中进入设备树查找
2./* gpioled 设备结构体 */
3./*打开设备*/
4./*从设备读数据*/
5./*向设备写数据*/
利用gpio子系统的API函数
6./*释放设备*/
7./* 设备操作函数 */
8./*驱动入口函数*/
/* 设置 LED 所使用的 GPIO*/
/* 1、获取设备节点:gpioled */
/* 2 、获取 compatible 属性内容 */
/* 4、 获取设备树中的 gpio 属性,得到 LED 所使用的 LED 编号 */
/* 5.向 gpio 子系统申请使用 GPIO */
/* 6 、设置 GPIO 为输出,并且输出低电平,默认关闭 LED 灯 */
不需要在设备树中去找led节点的reg属性,然后取reg属性中的寄存器地址进行GPIO的配置操作,而是直接获取LED gpio编号,然后利用gpio子系统,提供的API函数直接进行设置,这样可以屏蔽掉寄存器的设置过程。
/* 6 、注册新字符设备驱动 */
/* 1 、创建设备号 */
/* 2 、初始化 cdev */
/* 3 、添加一个 cdev */
/* 4 、创建类 */
/* 5 、创建设备 */
9.*驱动出口函数*/
/*注销字符设备驱动 删除cdev,释放gpioled 设备,删除设备device,删除class类*/
内容和上一章的设备树led驱动开发 dtsled.c 差不多,只是取消掉了配置寄存器的过程,改为使用 Linux 内核提供的 API 函数。在 GPIO 操作上更加的规范化,符合 Linux 代码框架,而且也简化了 GPIO 驱动开发的难度。
七、Linux 并发与竞争
在 Linux 驱动编写过程中对于并发控制的管理非常重要。
Linux 系统并发产生的原因很复杂,总结一下有下面几个主要原因:
①、多线程并发访问
②、抢占式并发访问,从 2.6 版本内核开始, Linux 内核支持抢占,也就是说调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。
③、中断程序并发访问
④、 SMP( 多核 ) 核间并发访问,现在 ARM 架构的多核 SOC 很常见,多核 CPU 存在核间并发访问。
并发访问带来的问题就是竞争,临界区就是共享数据段,对于临界区必须保证一次只有一个线程访问,也就是要保证临界区是原子访问的,学习 Linux 内核提供的几种并发和竞争的处理方法。
1. 原子操作
原子操作就是指不能再进一步分割的操作,一般原子操作用于变量或者位操作。
1.原子整形操作 API 函数:
Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量。
atomic_t a; atomic_t b = ATOMIC_INIT(0); // 定义原子变量 b 并赋初值为 0
原子变量有了,接下来就是对原子变量进行操作,比如读、写、增加、减少等等, Linux 内核提供了大量的原子操作 API 函数
2. 原子位操作 API 函数
原子位操作不像原子整形变量那样有个 atomic_t 的数据结构,原子位操作是直接对内存进行操作。
2.自旋锁
原子操作只能对整形变量或者位进行保护,但是,设备结构体变量就不是整型变量,我们对于结构体中成员变量的操作也要保证原子性,在线程 A 对结构体变量使用期间,应该禁止其他的线程来访问此结构体变量。
当一个线程要访问某个共享资源的时候首先要先获取相应的锁,锁只能被一个线程持有, 只要此线程不释放持有的锁,那么其他的线程就不能获取锁, 就会处于忙循环 - 旋转-等待状态。
自旋锁的“自旋”也就是“原地打转”的意思,“原地打转”的目的是为了等待自旋锁可以用,可以访问共享资源。
从这里我们可以看到自旋锁的一个缺点:那就 等待自旋锁的线程会一直处于自旋状态,这样会浪费处理器时间,降低系统性能,所以自旋锁 的持有时间不能太长。
自旋锁适用于短时期的轻量级加锁,如果遇到需要长时间持有锁的场景 那就需要换其他的方法了。
Linux 内核使用结构体 spinlock_t 表示自旋锁
spinlock_t lock; // 定义自旋锁
最基本的自旋锁 API 函数如表
自旋锁 API 函数适用于 线程与线程之间,被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的 API 函数,否则的话会可能会导致死锁现象的发生。
中断里面可以使用自旋锁,但是在中断里 面使用自旋锁的时候,在获取锁之前一定要先禁止本地中断( 也就是本 CPU 中断,对于多核 SOC 来说会有多个 CPU 核 ) ,否则可能导致锁死现象的发生。
建议使用 spin_lock_irqsave/ spin_unlock_irqrestore,因为这一组函数会保存中断状态,在释放锁的时候会恢复中断状态。
一般在线程中使用 spin_lock_irqsave/ spin_unlock_irqrestore,在中断中使用 spin_lock/spin_unlock
如果要在下半部里面使用自旋锁,可以使用表 11.3.2.3 中的 API 函数:
注意事项:
①、因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要短, 否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式,比如信号量和互斥体。
②、自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能导致死锁。
③、不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就 必须“自旋”,等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。结果就是自己把 自己锁死了!
④、在编写驱动程序的时候我们必须考虑到驱动的可移植性,因此不管你用的是单核的还 是多核的 SOC ,都将其当做多核 SOC 来编写驱动程序。
3.其他类型的锁
这些锁在驱动中其实用的不多,更多的是在 Linux 内核中使用
1、读写自旋锁
使用自旋锁对其进行保护。每次只能一个读操作或者写操作,但是实际上此表是可以并发读取的。
读写自旋锁为读和写操作提供了不同的锁,一次只能允许一个写操作,也就是只能一个线程持有写锁,而且不能进行读操作。
但是当没有写操作的时候允许一个或多个线程持有读锁,可以进行并发的读操作。
Linux 内核使用 rwlock_t 结构体表示读写锁,读写锁操作 API 函数分为两部分,一个是给读使用的,一个是给写使用的
2、顺序锁
顺序锁在读写锁的基础上衍生而来的,使用读写锁的时候读操作和写操作不能同时进行。
使用顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行
并发的写操作。
虽然顺序锁的读和写操作可以同时进行,但是如果在读的过程中发生了写操作, 最好重新进行读取,保证数据完整性。
顺序锁保护的资源不能是指针,因为如果在写操作的时
候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生,比如读
取野指针导致系统崩溃。
Linux 内核使用 seqlock_t 结构体表示顺序锁
4 信号量
信号量是同步的一种方式。Linux 内核也提供了信号量机制,信号量常常用于控制对共享资源的访问。
相比于自旋锁,信号量可以使线程进入休眠状态,使用信号量会提高处理器的使用效率,毕竟不用一直傻乎乎的在那里“自旋”等待。但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。
总结一下信号量的特点:
①、因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。
②、因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
③、如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。
信号量有一个信号量值,可以通过信号量来控制访问共享资源的访问数量,如果要想进房间,那就要先获取一把钥匙,信号量值减 1 ,直到 10 把钥匙都被拿走,信号量值为 0 ,这个时候就不允许任何人进入房间了,因为没钥匙了。如果有人从房间出来,那他要归还他所持有的那把钥匙,信号量 值加 1 ,此时有 1 把钥匙了,那么可以允许进去一个人。
相当于通过信号量控制访问资源的线 程数,在初始化的时候将信号量值设置的大于 1,那么这个信号量就是计数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。如果要互斥的访问共享资源那么信号量的值就不能大于 1 ,此时的信号量就是一个二值信号量。
信号量 API 函数
Linux 内核使用 semaphore 结构体表示信号量
5 互斥体
将信号量的值设置为 1 就可以使用信号量进行互斥访问了,虽然可以通过信号量实现互斥,但是 Linux 提供了一个比信号量更专业的机制来进行 互斥,它就是互斥体—mutex 。
互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体。
Linux 内核使用 mutex 结构体表示互斥体
在使用 mutex 之前要先定义一个 mutex 变量。在使用 mutex 的时候要注意如下几点:
①、 mutex 可以导致休眠,因此不能在中断中使用 mutex ,中断中只能使用自旋锁。
②、和信号量一样, mutex 保护的临界区可以调用引起阻塞的 API 函数。
③、因为一次只有一个线程可以持有 mutex ,因此,必须由 mutex 的持有者释放 mutex 。并
且 mutex 不能递归上锁和解锁。
互斥体 API 函数
6.原子操作驱动程序编写
修改设备树文件:基于上一章gpioled的设备树节点设置
1.#define 宏定义中包括:
/* 设备号个数 */
/* 名字*/
LEDOFF、LEDON
此时不需要定义寄存器地址,而是在代码中进入设备树查找
2./* gpioled 设备结构体 */
定义原子变量
3./*打开设备*/
/* 通过判断原子变量的值来检查 LED 有没有被别的应用使用 */
调用 open 函数打开驱动设备的时候先申请 lock,先使用 atomic_dec_and_test 函数将 lock 减 1 ,
如果 atomic_dec_and_test 函数返回值为真就表示 lock 当前值为 0 ,说明设备可以使用。
如果 函数返回值为假,就表示 lock 当前值为负数 , 其他设备正在使用 LED 。
在退出之前调用函数 atomic_inc 将 lock 加 1
4./*从设备读数据*/
5./*向设备写数据*/
gpio的API函数
6./*释放设备*/
/* 关闭驱动文件的时候释放原子变量 */
7./* 设备操作函数 */
8./*驱动入口函数*/
/* 1、初始化原子变量 */
/* 2 、原子变量初始值为 1 */
/* 设置 LED 所使用的 GPIO*/
/* 1、获取设备节点:gpioled */
/* 2 、获取 compatible 属性内容 */
/* 4、 获取设备树中的 gpio 属性,得到 LED 所使用的 LED 编号 */
/* 5.向 gpio 子系统申请使用 GPIO */
/* 6 、设置 GPIO 为输出,并且输出低电平,默认关闭 LED 灯 */
不需要在设备树中去找led节点的reg属性,然后取reg属性中的寄存器地址进行GPIO的配置操作,而是直接获取LED gpio编号,然后利用gpio子系统,提供的API函数直接进行设置,这样可以屏蔽掉寄存器的设置过程。
/* 6 、注册新字符设备驱动 */
/* 1 、创建设备号 */
/* 2 、初始化 cdev */
/* 3 、添加一个 cdev */
/* 4 、创建类 */
/* 5 、创建设备 */
9.*驱动出口函数*/
/*注销字符设备驱动 删除cdev,释放gpioled 设备,删除设备device,删除class类*/
/* 释放 GPIO */
原子变量 lock ,用来实现一次只能允许一个应用访问 LED 灯, led_init 驱动入口函数会将 lock 的值设置为 1。
每次open函数打开驱动设备的时候先使用 atomic_dec_and_test 函数将 lock 减 1,若无设备用就不管,有设备用再加一,然后退出。
led_release 函数执行,调用 atomic_inc 释放 lcok,也就是将 lock 加 1。
7.自旋锁驱动程序编程
我们通过定义一个变量 dev_stats 表示设备的使用情况, dev_stats 为 0 的时候表示设备没有被使用, dev_stats 大于 0 的时候表示设备被使用。
因此真正实现设备互斥访问的是变量 dev_stats ,但是我们要使用自旋锁对 dev_stats 来做保护。
1.#define 宏定义中包括:
/* 设备号个数 */
/* 名字*/
LEDOFF、LEDON
此时不需要定义寄存器地址,而是在代码中进入设备树查找
2./* gpioled 设备结构体 */
定义自旋锁,用于保护设备使用状态变量
3./*打开设备*/
驱动 open 函数中先判断 dev_stats 是否为 0 ,也就是判断设备是否可用,如果为 0 的话就使用设备,并且将 dev_stats 加 1 ,表示设备被使用了。使用完以后在 release 函数中将 dev_stats 减 1 ,表示设备没有被使用了。
判断过程需要对变量上锁。
4./*从设备读数据*/
5./*向设备写数据*/
gpio的API函数
6./*释放设备*/
/* 关闭驱动文件的时候释放原子变量 */
7./* 设备操作函数 */
8./*驱动入口函数*/
/* 初始化自旋锁 */
/* 设置 LED 所使用的 GPIO*/
/* 1、获取设备节点:gpioled */
/* 2 、获取 compatible 属性内容 */
/* 4、 获取设备树中的 gpio 属性,得到 LED 所使用的 LED 编号 */
/* 5.向 gpio 子系统申请使用 GPIO */
/* 6 、设置 GPIO 为输出,并且输出低电平,默认关闭 LED 灯 */
不需要在设备树中去找led节点的reg属性,然后取reg属性中的寄存器地址进行GPIO的配置操作,而是直接获取LED gpio编号,然后利用gpio子系统,提供的API函数直接进行设置,这样可以屏蔽掉寄存器的设置过程。
/* 6 、注册新字符设备驱动 */
/* 1 、创建设备号 */
/* 2 、初始化 cdev */
/* 3 、添加一个 cdev */
/* 4 、创建类 */
/* 5 、创建设备 */
9.*驱动出口函数*/
/*注销字符设备驱动 删除cdev,释放gpioled 设备,删除设备device,删除class类*/
/* 释放 GPIO */
8.信号量实验
使用信号量来实现一次只能有一个应用程序访问 LED 灯,信号量可以导致休眠, 因此信号量保护的临界区没有运行时间限制,可以在驱动的 open 函数申请信号量,然后在 release 函数中释放信号量。
但是信号量不能用在中断中,本节实验我们不会在中断中使用信号量。
1.#define 宏定义中包括:
/* 设备号个数 */
/* 名字*/
LEDOFF、LEDON
此时不需要定义寄存器地址,而是在代码中进入设备树查找
2./* gpioled 设备结构体 */
定义互斥体
3./*打开设备*/
在 open 函数中调用 mutex_lock_interruptible 或者 mutex_lock 获取 mutex ,成 功的话就表示可以使用 LED 灯,失败的话就会进入休眠状态,和信号量一样
4./*从设备读数据*/
5./*向设备写数据*/
gpio的API函数
6./*释放设备*/
7./* 设备操作函数 */
8./*驱动入口函数*/
/* 初始化互斥体 */
/* 设置 LED 所使用的 GPIO*/
/* 1、获取设备节点:gpioled */
/* 2 、获取 compatible 属性内容 */
/* 4、 获取设备树中的 gpio 属性,得到 LED 所使用的 LED 编号 */
/* 5.向 gpio 子系统申请使用 GPIO */
/* 6 、设置 GPIO 为输出,并且输出低电平,默认关闭 LED 灯 */
不需要在设备树中去找led节点的reg属性,然后取reg属性中的寄存器地址进行GPIO的配置操作,而是直接获取LED gpio编号,然后利用gpio子系统,提供的API函数直接进行设置,这样可以屏蔽掉寄存器的设置过程。
/* 6 、注册新字符设备驱动 */
/* 1 、创建设备号 */
/* 2 、初始化 cdev */
/* 3 、添加一个 cdev */
/* 4 、创建类 */
/* 5 、创建设备 */
9.*驱动出口函数*/
/*注销字符设备驱动 删除cdev,释放gpioled 设备,删除设备device,删除class类*/
前面我们使用原子操作、自旋锁和信号量实现了对 LED 灯的互斥访问,但是最适合互斥的就是互斥体 mutex 了。
1.#define 宏定义中包括:
/* 设备号个数 */
/* 名字*/
LEDOFF、LEDON
此时不需要定义寄存器地址,而是在代码中进入设备树查找
2./* gpioled 设备结构体 */
定义信号量
3./*打开设备*/
在 open函数中申请信号量。如果信号量值大于等于 1 就表示可用,那么应用程序就会开始使用 LED 灯。如果信号量 值为 0 就表示应用程序不能使用 LED 灯,此时应用程序就会进入到休眠状态。
等到信号量值大于 1 的时候应用程序就会唤醒,申请信号量,获取 LED 灯使用权。
4./*从设备读数据*/
5./*向设备写数据*/
gpio的API函数
6./*释放设备*/
/* 关闭驱动文件的时候释放原子变量 */
7./* 设备操作函数 */
8./*驱动入口函数*/
/* 初始化信号量 */
初始化信号量 sem 的值为 1 ,相当于 sem 是个二值信号量。
/* 设置 LED 所使用的 GPIO*/
/* 1、获取设备节点:gpioled */
/* 2 、获取 compatible 属性内容 */
/* 4、 获取设备树中的 gpio 属性,得到 LED 所使用的 LED 编号 */
/* 5.向 gpio 子系统申请使用 GPIO */
/* 6 、设置 GPIO 为输出,并且输出低电平,默认关闭 LED 灯 */
不需要在设备树中去找led节点的reg属性,然后取reg属性中的寄存器地址进行GPIO的配置操作,而是直接获取LED gpio编号,然后利用gpio子系统,提供的API函数直接进行设置,这样可以屏蔽掉寄存器的设置过程。
/* 6 、注册新字符设备驱动 */
/* 1 、创建设备号 */
/* 2 、初始化 cdev */
/* 3 、添加一个 cdev */
/* 4 、创建类 */
/* 5 、创建设备 */
12.*驱动出口函数*/
/*注销字符设备驱动 删除cdev,释放gpioled 设备,删除设备device,删除class类*/
/* 释放 GPIO */
八、Linux 按键输入实验
在 Linux 下编写 GPIO 输入驱动程序。开发板上JP2 这个双排排针引出了 18 个 IO ,我们使用 GPIO2_D4 这个引出的 IO 来完成GPIO 输入驱动程序。
Linux 下按键驱动原理
按键驱动和 LED 驱动原理上来讲基本都是一样的,都是操作 GPIO ,只不过一个是读取GPIO 的高低电平,一个是从 GPIO 输出高低电平。
在驱动程序中使用一个整形变量来表示按键值,应用程序通过 read 函数来读取按键值,判断按键有没有按下。
这个保存按键值的变量就是个共享资源,驱动程序要向其写入按键值,应用程序要读取按键值。所以我们要对其进行保护,对于整形变量而言我们首选的就是原子操作,使用原子操 作对变量进行赋值以及读取。
实际中的按键驱动并不会采用此方法,Linux 下的 input 子系统专门用于输入设备!
1.pinctrl 设置
首先打开 rv1126-pinctrl.dtsi 文件,在 pinctrl 节点下添加 GPIO2_D4 的 pinctrl 信息
打开 rv1126-alientek.dts 文件,在根节点“ / ”下创建 KEY 节点,节点名“ key ”,
2. 按键驱动程序编写
1.#define 宏定义中包括:
/* 设备号个数 */
/* 名字*/
KEY0VALUE 0XF0 /* 按键值*/
INVAKEY 0X00 /* 无效的按键值 */
此时不需要定义寄存器地址,而是在代码中进入设备树查找
2./* gpioled 设备结构体 */
定义原子变量
3.初始化按键 IO
/* 1、获取设备节点:gpioled */
/* 2 、获取 compatible 属性内容 */
/* 4、获取设备树中的 gpio 属性,得到 KEY0 所使用的 KYE 编号 */
/* 5.向 gpio 子系统申请使用 GPIO */
/* 6、设置 GPIO2_D4 输入模式 */
4./*打开设备*/
open 函数打开驱动的时候初始化按键所使用的 GPIO 引脚
5./*从设备读数据*/
应用程序通过 read 函数读取按键值的时候此函数就会执行。
读取按键 IO 的电平,如果为 1 的话就表示按键按下了,如果按键按下的话 等
待按键释放。按键释放以后标记按键值为 0XF0
6./*向设备写数据*/
7./*释放设备*/
/* 关闭驱动文件的时候释放原子变量 */
8./* 设备操作函数 */
9./*驱动入口函数*/
/* 1、初始化原子变量 */
/* 2、原子变量初始值为 INVAKEY */
/* 6 、注册新字符设备驱动 */
/* 1 、创建设备号 */
/* 2 、初始化 cdev */
/* 3 、添加一个 cdev */
/* 4 、创建类 */
/* 5 、创建设备 */
10.*驱动出口函数*/
/*注销字符设备驱动 删除cdev,释放gpioled 设备,删除设备device,删除class类*/
函数 keyio_init 用于初始化按键,从设备树中获取按键的 gpio 信息,然后设
置为输入。这里将按键的初始化代码提取出来,将其作为独立的一个函数有利于提高程序的模块化设计。
key_open 函数通过调用 keyio_init 函数来始化按键所使用的 IO,应用程序
每次打开按键驱动文件的时候都会初始化一次按键 IO 。
十、Linux 内核定时器实验
Linux 内核提供的定时器 API 函数 。
Linux 内核也提供了短延时函数,比如微秒、纳秒、毫秒延时函数。
1.内核时间管理简介
Linux 要运行需要一个系统时钟,这个系统时钟是由哪个定时器提供的,对于 Linux 驱动编写者来说,不需要深入研究这些具体的实现,只需要掌握相应的 API 函数。
硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以后就周期性的产生定时中断,系统使用定时中断来计时。
中断周期性产生的频率就是系统频率 ,比如 100Hz 、 1000Hz 等等说的就是系统节拍率。
RV1126系统节拍率默认为 300Hz,
①、高节拍率会提高系统时间精度。
②、高节拍率会导致中断的产生更加频繁,频繁的中断会加剧系统的负担, 但是现在的处理器性能都很强大,所以采用 1000Hz 的系统节拍率并不会增加太大的负载压力。
Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会将 jiffies 初始化为 0。
不管是 32 位还是 64 位的 jiffies ,都有溢出的风险,溢出以后会重新从 0 开始计数,相当于绕回来了,也叫做绕回。处理 32 位 jiffies 的绕回显得尤为重要。
Linux 内核提供了 几个 API 函数来处理绕回
如果 unkown 没有超过 known 的话 time_before 函数返回真,否则返回假。
我们要判断某段代码执行时间有没有超时,此时就可以使用如下所示代码:
为了方便开发, Linux 内核提供了几个 jiffies 和 ms 、 us 、 ns 之间的转换函数
2.内核定时器简介
Linux 内核定时器使用很简单,只需要提供超时时间 ( 相当于定时值 )和定时处理函数即可,当超时时间到了以后设置的定时处理函数就会执行。
在使用内核定时器的时候要注意一点,内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。
Linux 内核使用 timer_list 结构体表示内核定时器,使用内核定时器首先要先定义一个 timer_list 变量,表示定时器,
tiemr_list 结构体的expires 成员变量表示超时时间,单位为节拍数,定义好定时器以后还需要通过一系列的 API 函数来初始化此定时器。
1 、 timer_setup 函数
负责初始化 timer_list 类型变量,
void timer_setup(struct timer_list *timer, void (*func)(struct timer_list *), unsigned int flags)
2 、 add_timer 函数
用于向 Linux 内核注册定时器,使用 add_timer 函数向内核注册定时器以后,
定时器就会开始运行
void add_timer(struct timer_list *timer)
3 、 del_timer 函数
用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。
在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。del_timer 函数原型如下:
int del_timer(struct timer_list * timer)
4 、 del_timer_sync 函数
是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除,
del_timer_sync 不能使用在中断上下文中
int del_timer_sync(struct timer_list *timer)
5 、 mod_timer 函数
用于修改定时值,如果定时器还没有激活的话, mod_timer 函数会激活定时
器!函数原型如下:
int mod_timer(struct timer_list *timer, unsigned long expires)
3 Linux 内核短延时函数
4.驱动程序编写
1.#define 宏定义中包括:
/* 设备号个数 */
/* 名字*/
CLOSE_CMD ( _IO ( 0XEF , 0x1 )) /* 关闭定时器 */
OPEN_CMD ( _IO ( 0XEF , 0x2 )) /* 打开定时器 */
SETPERIOD_CMD ( _IO ( 0XEF , 0x3 )) /* 设置定时器周期命令 */
LEDON、LEDOFF
2./* timer 设备结构体 */
3.初始化 LED 灯 IO
/* 1、获取设备节点:gpioled */
/* 2 、获取 compatible 属性内容 */
/* 4、获取设备树中的 gpio 属性,得到 led-gpio 所使用的 led 编号 */
/* 5.向 gpio 子系统申请使用 GPIO */
/* 6、设置 PI0 为输出,并且输出高电平,默认关闭 LED 灯 */
4./*打开设备*/
初始化定时周期默认为 1 秒,最后调用 led_init 函数初始化 LED 所使用的 IO 。
5.ioctl 函数 timer_unlocked_ioctl
处理应用程序发送过来的命令
函数 timer_unlocked_ioctl ,对应应用程序的 ioctl 函数,应用程序调用 ioctl
函数向驱动发送控制信息,此函数响应并执行。
此函数有三个参数: filp , cmd 和 arg ,其中 filp是对应的设备文件,cmd 是应用程序发送过来的命令信息, arg 是应用程序发送过来的参数,在本章例程中 arg 参数表示定时周期。
CLOSE_CMD : 关闭定时器命令 , 调用 del_timer_sync 函数关闭定时器。
OPEN_CMD :打开定时器命令,调用 mod_timer 函数打开定时器,定时周期为 timerdev 的timeperiod 成员变量,定时周期默认是 1 秒。
SETPERIOD_CMD :设置定时器周期命令,参数 arg 就是新的定时周期,设置 timerdev 的 timeperiod 成员变量为 arg 所表示定时周期值。并且使用 mod_timer 重新打开定时器,使定时器 以新的周期运行。
6./*释放设备*/
7./* 定时器回调函数 */
需要 定义的 timerdev 变量。
这里使用 from_timer 函数来根据 arg 参数反推出 timerdev 变量地址。
比如 timerdev 这个结构体变量,类型为 timer_dev ,而 timer_dev 中有个成员变量 timer , timer 是 timer_list 类型。因此当我们知道了 timer 这个成员变
量的具体地址以后,就可以根据 container_of 函数来反推出 timer 这个成员变量所属的 timer_dev 结构体变量首地址,在这里就是得到 timerdev 地址。
最后在 timer_function 函数中将 LED 灯的状态取反,实现 LED 灯闪烁的效果。
因为内核定时器不是循环的定时器,执行一次以后就结束了,因此又调用了 mod_timer 函数重新开启定时器。
8./* 设备操作函数 */
9./*驱动入口函数*/
/* 初始化自旋锁 */
/* 6 、注册新字符设备驱动 */
/* 1 、创建设备号 */
/* 2 、初始化 cdev */
/* 3 、添加一个 cdev */
/* 4 、创建类 */
/* 5 、创建设备 */
/* 6 、初始化 timer ,设置定时器处理函数 , 还未设置周期,所以不会激活定时器 */
10.*驱动出口函数*/
/*注销字符设备驱动 删除cdev,释放gpioled 设备,删除设备device,删除class类*/
函数 timer_init ,驱动入口函数, 初始化定时器,设置定时器的 定时处理函数为 timer_function ,标志位为 0 。在此函数中并没有调用 timer_add 函数来开启定时器, 不会激活定时器。 因此定时器默认是关闭的,除非应用程序发送打开命令!
十一、Linux 中断实验
不管是单片机还是 Linux 下的驱动实验,中断都是频繁使用的功能,在单片机中使用中断我们需要做一大堆的工作,比如配置寄存器,使能 IRQ 等等。
但是 Linux 内核提供了完善的中断框架,我们只需要申请中断,然后注册中断处理函数即可,使用非常方便,不需要一系列复杂的寄存器配置。
1. Linux 中断 API 函数
中断的处理方法:
①、使能中断,初始化相应的寄存器。
②、编写中断服务函数,中断发生以后相应的中断服务函数就会执行。
中断有关的 API 函数:
1 、中断号
每个中断都有一个中断号,通过中断号即可区分不同的中断,在 Linux 内核中使用一个 int 变量表示中断号。
2 、 request_irq 函数
用于申请中断, request_irq 函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函数。
request_irq 函数会激活 ( 使能 ) 中断,所以不需要我们手动去使能中断, request_irq 函数原型如下:
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
3 、 free_irq 函数
使用完成以后就要通过 free_irq 函数释放掉相应的中断。
如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断。 free_irq 函数原型如下所示:
void free_irq(unsigned int irq, void *dev_id)
4 、中断处理函数
使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:
irqreturn_t (*irq_handler_t) (int, void *)
5 、中断使能与禁止函数
常用的中断使用和禁止函数如下所示:
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
enable_irq 和 disable_irq 用于使能和禁止指定的中断
disable_irq 函数要 等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中 断,并且确保所有已经开始执行的中断处理程序已经全部退出。
在这种情况下,可以使用另外 一个中断禁止函数:
void disable_irq_nosync(unsigned int irq)
函数调用以后立即返回,不会等待当前中断处理程序执行完毕。
上面三个函数都是使能或者禁止某一个中断,有时候我们需要关闭当前处理器的整个中断系统,也就是在学习 STM32 单片机的时候常说的关闭全局中断,这个时候可以使用如下两个函数:
local_irq_enable()
local_irq_disable()
2.上半部与下半部
在有些资料中也将上半部和下半部称为顶半部和底半部。
我们在使用request_irq 申请中断的时候注册的中断服务函数属于中断处理的上半部,只要中断触发,那么 中断处理函数就会执行。
我们必须要缩小中断处理函数的执行时间。
比如电容触摸屏通过中断通知 SOC 有触摸事件发生, SOC 响应中断,然后
通过 IIC 接口读取触摸坐标值并将其上报给系统。但是我们都知道 IIC 的速度最高也只有 400Kbit/S,所以在中断中通过 IIC 读取数据就会浪费时间。我们可以将通过 IIC 读取触摸数据的操作暂后执行,中断处理函数仅仅相应中断,然后清除中断标志位即可。
这个时候中断处理过程就分为了两部分:
上半部: 上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部: 如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
因此, Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所 有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。
一些可以借鉴的参考点:
①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部
④、除了上述三点以外的其他任务,优先考虑放到下半部。
下半部机制:
1 、软中断
一开始 Linux 内核提供了“ bottom half ”机制来实现下半部,简称“ BH ”。后面引入了软中
断和 tasklet 来替代“ BH ”机制, 从 2.5 版本的 Linux内核开始 BH 已经被抛弃了。
Linux 内核使用结构体 softirq_action 表示软中断
在 kernel/softirq.c 文件中一共定义了 10 个软中断
每个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个 CPU 所执行的软中断服务函数确是相同的,都是数组 softirq_vec 中定义的 action 函数。要使用软中断,必须先使用 open_softirq 函数注册对应的软中断处理函数,open_softirq 函数原型如下:
void open_softirq(int nr, void (*action)(struct softirq_action *))
注册好软中断以后需要通过 raise_softirq 函数触发, raise_softirq 函数原型如下:
void raise_softirq(unsigned int nr)
2 、 tasklet
tasklet 是利用软中断来实现的另外一种下半部机制,在软中断和 tasklet 之间,建议大家使
用 tasklet 。
3 、工作队列
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的
工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重
新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软
中断或 tasklet。
3 设备树中断信息节点
1、GIC 中断控制器
GIC 全称为: Generic Interrupt Controller,是 ARM 公司给 Cortex-A/R 内核提供的一个中断控制器,类似 Cortex-M 内核中的 NVIC。 当 GIC 接收到外部中断信号以后就会报给 ARM 内核,但是
ARM 内核只提供了四个信号给 GIC 来汇报中断情况: VFIQ 、 VIRQ 、 FIQ 和 IRQ ,他们之间的
关系如图 15.1.3.1 所示
这四个信号的含义如下:
VFIQ : 虚拟快速 FIQ 。
VIRQ : 虚拟快速 IRQ 。
FIQ : 快速中断 IRQ 。
IRQ : 外部中断 IRQ 。
VFIQ 和 VIRQ 是针对虚拟化的,我们讨论不虚拟化,剩下的就是 FIQ 和 IRQ 了,本教程
我们只使用 IRQ 。所以相当于 GIC 最终向 ARM 内核就上报一个 IRQ 信号。
GIC 将众多的中断源分为分为三类:
①、 SPI(Shared Peripheral Interrupt),共享中断,那些外部中断都属于 SPI 中断
②、 PPI(Private Peripheral Interrupt) ,私有中断,我们说了 GIC 是支持多核的,每个核肯定
有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。
③、 SGI(Software-generated Interrupt) ,软件中断,由软件触发引起的中断,通过向寄存器
GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信。
2 、中断 ID
中断源有很多,为了区分这些不同的中断源肯定要给他们分配一个唯一 ID ,这些 ID 就是
中断 ID 。每一个 CPU 最多支持 1020 个中断 ID ,中断 ID 号为 ID0~ID1019 。这 1020 个 ID 包
含了 PPI 、 SPI 和 SGI
ID0~ID15 :这 16 个 ID 分配给 SGI 。
ID16~ID31 :这 16 个 ID 分配给 PPI 。
ID32~ID1019 :这 988 个 ID 分配给 SPI
3 、 GIC 控制器节点
打开 rv1126.dtsi 文件,其中的 gic 节点就是 GIC 的中断控制器节点
compatible 属性值为“ arm,gic-400”
interrupt-controller 节点为空,表示当前节点是中断控制器。
#interrupt-cells 和 #address-cells 、 #size-cells
这三个 cells 的含义如下:
第一个 cells :中断类型, 0 表示 SPI 中断, 1 表示 PPI 中断。
第二个 cells:中断号
第三个 cells :标志, bit[3:0] 表示中断触发类型,为 1 的时候表示上升沿触发,为 2 的时候
表示下降沿触发,为 4 的时候表示高电平触发,为 8 的时候表示低电平触发。 bit[15:8] 为 PPI 中
断的 CPU 掩码。
找到 SPI0 对应的中断号
SPI0 的中断号为 42 ,注意这里是加上了前面 32 个 PPI 中断号,
如果不算前面 32 个 PPI 中断号的话就是就是 42-32=10 。
打开 rv1126.dtsi ,找到 SPI0 节点内容
interrupts 描述中断中断源的信息,
第一个表示中断类型,为 GIC_SPI ,也就是共享中断。
第二个表示中断号为 10 ,来源就是图 15.1.3.4 中的 42-32=10 。
第三个表示中断触发类型是高电平触发。
我们来看一个具体的应用,打开 rv1126-alientek.dtsi 文件
RK809 是正点原子 ATK-DLRV1126 开发板上核心板的 PMIC 芯片,上述代码就是 RK809 的节点信息,RK809 芯片有一个中断,此引脚链接到了 RV1126 的 GPIO0_B1 上,此中断是电平触发。
interrupt-parent 属性设置中断控制器为 GPIO0 。
interrupts 设置中断信息, 9 表示 GPIO0_B1 属于 GPIO0 组的第 9 个 IO ,前 8 个为 A0~A7 和 B0 。IRQ_TYPE_LEVEL_LOW 表示 下降沿触发 。
目的就是设置 GPIO0_B1 为下低电平触发。可以看出使用起来是非常
的简单,在我们实际编写代码的时候,只需要通过 interrupt-parent 和 interrupts 这两个属性即可
设置某个 GPIO 的中断功能。
4 获取中断号
编写驱动的时候需要用到中断号,我们用到的中断号,中断信息已经写到了设备树里面,
因此可以通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号,函数原型如 下:
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
5 实验程序编写
修改设备树文件
创建的 key 节点基础上补充,添加 GPIO2_D4 这个引脚相关的中断属性
设置 interrupt-parent 属性值为“ gpio2 ”。
设置 interrupts 属性,也就是设置中断源, 表示 GPIO2 组的 28 号 IO ,也就是 PD4,
IRQ_TYPE_EDGE_BOTH 表示上升沿和下降沿同时有效, 相当于按下和释放都会触发中断。
设备树修改完成以后,重新编译内核,并且烧写到开发板
1.#define 宏定义中包括:
/* 设备号个数 */
/* 名字*/
/* 定义按键状态 */
定义了一个枚举类型,包含 3 个常量 KEY_PRESS 、 KEY_RELEASE 、
KEY_KEEP ,分别用来表示按键的 3 种不同的状态,即按键按下、按键松开以及按键状态保持
2./* KEY 设备结构体 */
定义一个 int 类型的静态全局变量 status 用来表示按键的状态
3.初始化 KEY IO
/* 1、获取设备节点:KEY*/
/* 2 、获取 compatible 属性内容 */
/* 4、获取设备树中的 gpio 属性,得到 KEY 所使用的 led 编号 */
* 5.、获取 GPIO 对应的中断号 */
后面就可以使用这个中断号去申请以及释放中断了
4./*GPIO 中断初始化 */
/* 获取设备树中指定的中断触发类型 */
/* 申请中断 */
key_gpio_init 函数中主要对 GPIO 以及中断进行了相关的初始化。使用
gpio_request 函数申请 GPIO 使用权,通过 gpio_direction_input 将 GPIO 设置为输入模式;
irq_get_trigger_type 函数可以获取到 key 节点中定义的中断触发类型,最后使用 request_irq 申请中断,并设置 key_interrupt 函数作为我们的按键中断处理函数,当按键中断发生之后便会跳转 到该函数执行; request_irq 函数会默认使能中断,所以不需要 enable_irq 来使能中断,
当然,我们也可以在申请成功之后先使用 disable_irq 函数禁用中断,等所有工作完成之后再来使能中断,这样会比较安全,建议大家这样使用。
5./*key_timer_function*/
/* 读取按键值并判断按键当前状态 */
/* 自旋锁解锁 */
key_timer_function 函数为定时器定时处理函数,它的参数 arg 在本驱动程
序中我们并没有使用到;该函数中定义了一个静态局部变量 last_val 用来保存按键上一次读取到的值,变量 current_val 用来存放当前按键读取到的值。
当状态判断完成之后,会将 current_val 的值赋值给 last_val 。
本函数中也使用自旋锁对全局变量 status 进行加锁保护!
6./*打开设备*/
7.read设备
/* 自旋锁上锁 */
/* 将按键状态信息发送给应用程序 */
/* 状态重置 */
/* 自旋锁解锁 */
key_read 函数,对应应用程序的 read 函数。此函数向应用程序返回按键状
态信息数据;这个函数其实很简单,使用 copy_to_user 函数直接将 statue 数据发送给应用程序
status 变量保存了按键当前的状态,发送完成之后再将按键状态重置即可!需要注意的是,该函数中使用了自旋锁进行保护。
8./*向设备写数据 */
9./*释放设备*/
10./* 设备操作函数 */
11./*驱动入口函数*/
/* 初始化自旋锁 */
/* 1 、初始化 timer ,设置定时器处理函数 , 还未设置周期,所以不会激活定时器 */
/* 6 、注册新字符设备驱动 */
/* 1 、创建设备号 */
/* 2 、初始化 cdev */
/* 3 、添加一个 cdev */
/* 4 、创建类 */
/* 5 、创建设备 */
mykey_init 是驱动入口函数, 调用 spin_lock_init 初始化自旋锁变量, 对定时器进行初始化并将 key_timer_function 函数绑定为定时器定时处理函数,当定
时时间到了之后便会跳转到该函数执行
12.*驱动出口函数*/
/*注销字符设备驱动 删除cdev,释放设备,删除 timer,注销设备device,注销class类*/
十二、Linux 阻塞和非阻塞 IO 实验
阻塞和非阻塞 IO 是 Linux 驱动开发里面很常见的两种设备访问模式,在编写驱动的时候 一定要考虑到阻塞和非阻塞。
这里的 IO 指的是 Input/Output ,也就是输入 /输出,并不是“ GPIO ”, 是应用程序对驱动设备的输入/ 输出操作。
- 当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程序对应的线程挂起,会进入到休眠态,直到设备资源可以获取为止。
- 对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。
对于设备驱动文件的默认读取方式就是阻塞式的
使用 open 函数打开“ /dev/xxx_dev ”设备文件的时候添加了参数“ O_NONBLOCK ”, 表示以非阻塞方式打开设备,这样从设备中读取数据的时候就是非阻塞方式的了
1.等待队列
1、等待队列头
阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将 CPU 资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。
Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作,如果我们要 在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体 wait_queue_head 表示,
最好使用 wait_queue_head_t 表示等待队列头
初始化,使用 init_waitqueue_head 函数初始化等待队列头,函数原型如下: void init_waitqueue_head(struct wait_queue_head *wq_head)
2、等待队列项
等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的队列项添加到等待队列里面。在以前的 linux 版本中使用结构wait_queue_t 表示,取而代之的是wait_queue_entry 结构体
使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项,宏的内容如下: DECLARE_WAITQUEUE(name, tsk)
3、将队列项添加/移除等待队列头
当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中, 只有添加到等待队列头中以后进程才能进入休眠态。当设备可以访问以后再将进程对应的等待 队列项从等待队列头中移除即可,等待队列项添加 API 函数如下:
void add_wait_queue(struct wait_queue_head *wq_head,
struct wait_queue_entry *wq_entry)
4、等待唤醒
当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用如下两个函数:
void wake_up(struct wait_queue_head *wq_head)
void wake_up_interruptible(struct wait_queue_head *wq_head)
会将这个等待队列头中的所有进程都唤醒
5、等待事件
除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程
2. 轮询
如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,也就是轮询。
poll 、 epoll 和 select 可以用于处理轮询,应用程序通过 select 、 epoll 或 poll 函数来
查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调
用 select 、 epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动
程序中编写 poll 函数。
1、select 函数
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
2、poll 函数
在单个线程中, select 函数能够监视的文件描述符数量有最大的限制,一般为 1024 ,可以
修改内核将监视的文件描述符数量改大,但是这样会降低效率!这个时候就可以使用 poll 函数,
poll 函数本质上和 select 没有太大的差别,但是 poll 函数没有最大文件描述符限制, Linux 应用
程序中 poll 函数原型如下所示:
int poll(struct pollfd *fds, nfds_t nfds, int timeout)
3、epoll 函数
传统的 selcet 和 poll 函数都会随着所监听的 fd 数量的增加,出现效率低下的问题,而且
poll 函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。为此, epoll
应运而生, epoll 就是为处理大并发而准备的,一般常常在网络编程中使用 epoll 函数。应用程
序需要先使用 epoll_create 函数创建一个 epoll 句柄, epoll_create 函数原型如下:
int epoll_create(int size)
4. Linux 驱动下的 poll 操作函数
以驱动程序的编写者需要提供对应的 poll 函 数,poll 函数原型如下所示:
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)
3.阻塞 IO 实验
在上一章 Linux 中断实验中,我们直接在应用程序中通过 read 函数不断的读取按键状态,
当按键有效的时候就打印出按键值。这种方法有个缺点,那就是 keyirqApp 这个测试应用程序
拥有很高的 CPU 占用率。
最好的方法就是在没有有效的按键事件发生的时候, keyirqApp 这个应用程序应该处于休眠状态,当有按键事件发生以后 keyirqApp 这个应用程序才运行,打印出按键值,这样就会降低 CPU 使用率,本小节我们就使用阻塞 IO 来实现此能。主要是对中断实验添加阻塞访问相关的代码
1.#define 宏定义中包括:
/* 设备号个数 */
2./* KEY 设备结构体 */
删除了 自旋锁变量,而改成 原子变量来实现对相应变量的保护操作。 添加了一个等待 队列头 r_wait ,因为在 Linux 驱动中处理阻塞 IO 需要用到等待队列,而等待队列会使进程进入 休眠状态,所以不能使用自旋锁!
3.初始化 KEY IO
/* 1、获取设备节点:KEY*/
/* 2 、获取 compatible 属性内容 */
/* 4、获取设备树中的 gpio 属性,得到 KEY 所使用的 led 编号 */
* 5.、获取 GPIO 对应的中断号 */
后面就可以使用这个中断号去申请以及释放中断了
4./*GPIO 中断初始化 */
/* 获取设备树中指定的中断触发类型 */
/* 申请中断 */
5./*key_timer_function*/
/* 读取按键值并判断按键当前状态 */
如果是按 下动作或是松开动作则会使用 wake_up_interruptible 函数唤醒等待队列,并且将 status 变量设置 为 KEY_PRESS 或 KEY_RELEASE;这样在 key_read 函数中阻塞的进程就会解除阻塞继续进行下面的操作。
6./*打开设备*/
7.read设备
/* 加入等待队列,当有按键按下或松开动作发生时,才会被唤醒 */
/* 将按键状态信息发送给应用程序 */
/* 状态重置 */
如果等待队列被唤醒并且条件“KEY_KEEP != atomic_read(&status) ”成立,则解除阻塞,读取按键状态数据将其发送给应用程序。
因为采用了 wait_event_interruptible 函数,因此进入休眠态的进程可以被信号打断。在该函数中我们读取 status 原子变量必须要使用atomic_read 函数进行操作。
8./*向设备写数据 */
9./*释放设备*/
10./* 设备操作函数 */
11./*驱动入口函数*/
/* 初始化等待队列头 */
/* 1 、初始化 timer ,设置定时器处理函数 , 还未设置周期,所以不会激活定时器 */
/* 6 、注册新字符设备驱动 */
/* 1 、创建设备号 */
/* 2 、初始化 cdev */
/* 3 、添加一个 cdev */
/* 4 、创建类 */
/* 5 、创建设备 */
在驱动入口函数中我们会调用 init_waitqueue_head 初始化等待队列头; 使用原子操作 atomic_set 设置按键的初始状态 status 为 KEY_KEEP
12.*驱动出口函数*/
/*注销字符设备驱动 删除cdev,释放设备,删除 timer,注销设备device,注销class类*/
1.#define 宏定义中包括:
/* 设备号个数 */
2./* KEY 设备结构体 */
3.初始化 KEY IO
4./*GPIO 中断初始化 */
6./*打开设备*/
7.read设备
key_read 函数中判断是否为非阻塞式读取访问,如果是非阻塞的话就判断按键状 态是否有效,也就是判断是否产生了按下或松开这样的动作,如果没有的话就返回-EAGAIN 。
8.poll 函数,用于处理非阻塞访问
当应用程序调用 select 或者 poll 函数的时候 key_poll 函数就会执行。
调用 poll_wait 函数将等待队列头添加到 poll_table 中,
判断按键是否有效,如果按键有效的话就向应用程序返回POLLIN 这个事件,表示有数据可以读取。
9./*向设备写数据 */
10./*释放设备*/
11./* 设备操作函数 */
12./*驱动入口函数*/
13.*驱动出口函数*/
在测试程序中我们使用 select 函数来实现非阻塞访问,在 for 循环中使用
select 函数不断的轮询,检查驱动程序是否有数据可以读取,如果可以读取的话就调用 read 函 数读取按键数据。大家也可以试试使用 poll 函数来实现
十三、 异步通知实验
在前面使用阻塞或者非阻塞的方式来读取驱动中按键值都是应用程序主动读取的,对于非阻塞方式来说还需要应用程序通过 poll 函数不断的轮询。
最好的方式就是驱动程序能主动向应用程序发出通知,报告自己可以访问,然后应用程序再从驱动程序中读取或写入数据,类似于中断。Linux 提供了异步通知这个机制来完成此功能。
1.异步通知简介
中断是处理器提供的一种异步机制,配置好中断以后就可以让处理器去处理其他的事情了,当中断发生以后会触发我们事先设置好的中断服务函数 , Linux 应用程序可以通过阻塞或者非阻塞这两种方式来访问驱动设备
这两种方式都需要应用程序主动的去查询设备的使用情况,如果能提供一种类似中断的机制,当驱动程序 可以访问的时候主动告诉应用程序那就最好了。
“信号”为此应运而生,信号类似于我们硬件上使用的“中断”,只不过信号是软件层次上的。算是在软件层次上对中断的一种模拟,驱动可以通过主动向应用程序发送信号的方式来报告自己可以访问了,应用程序获取到信号以后就可以从驱动设备中读取或者写入数据了。
整个过程就相当于应用程序收到了驱动发送过来了的一个中断,然后应用程序去响应这个中断
异步通知的核心就是信号,在 arch/xtensa/include/uapi/asm/signal.h 文件中定义了 Linux 所支持的所有信号
这些信号就相当于中断号,不同的中断号代表了不同的中断,
不同的中断所做的处理不同,因此,驱动程序可以通过向应用程序发送不同的信号来实现不同的功能。
我们使用中断的时候需要设置中断处理函数,同样的,如果要在应用程序中使用信号,那么就必须设置信号所使用的信号处理函数,在应用程序中使用 signal 函数来设置指定信号的处理函数,signal 函数原型如下所示:
sighandler_t signal(int signum, sighandler_t handler)
2 驱动中的信号处理
1、fasync_struct 结构体
首先我们需要在驱动程序中定义一个 fasync_struct 结构体指针变量,将 fasync_struct 结构体指针变量定义到设备结构体中
2、fasync 函数
如果要使用异步通知,需要在设备驱动中实现 file_operations 操作集中的 fasync 函数,此函数格式如下所示:
int (*fasync) (int fd, struct file *filp, int on)
fasync 函数里面一般通过调用 fasync_helper 函数来初始化前面定义的 fasync_struct 结构体指针,fasync_helper 函数原型如下:
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
在关闭驱动文件的时候需要在 file_operations 操作集中的 release 函数中释放 fasync_struct , fasync_struct 的释放函数同样为 fasync_helper
3、kill_fasync 函数
当设备可以访问的时候,驱动程序需要向应用程序发出信号,相当于产生“中断”。 kill_fasync 函数负责发送指定的信号,kill_fasync 函数原型如下所示:
void kill_fasync(struct fasync_struct **fp, int sig, int band)
3 应用程序对异步通知的处理
应用程序对异步通知的处理包括以下三步:
1 、注册信号处理函数
应用程序根据驱动程序所使用的信号来设置信号的处理函数,应用程序使用 signal 函数来设置信号的处理函数。
2 、将本应用程序的进程号告诉给内核
使用 fcntl(fd, F_SETOWN, getpid()) 将本应用程序的进程号告诉给内核。
3 、开启异步通知
使用如下两行程序开启异步通知:
flags = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC); /* 开启当前进程异步通知功能 */
重点就是通过 fcntl 函数设置进程状态为 FASYNC ,经过这一步,驱动程序中的 fasync 函数就会执行。
4 实验程序编写
在上一章实验 基础上完成,在其中加入异步通知相关内容即可,当按键按下以后驱动程序向应用程序发送 SIGIO 信号,应用程序获取到 SIGIO 信号以后 读取并且打印出按键值.
1.#define 宏定义中包括:
/* 设备号个数 */
2./* KEY 设备结构体 */
3.初始化 KEY IO
4./*GPIO 中断初始化 */
5./*key_timer_function*/
当按键按下或松开动作发生时调用 kill_fasync 函数向应用程序发送 SIGIO 信号,通知应用程序按键数据可以进行读取了
6./*打开设备*/
7.read设备
8.poll 函数,用于处理非阻塞访问
9./*向设备写数据 */
10.key_fasync fasync 函数,用于处理异步通知
设备操作函数集 file_operations 结构体中的 fasync 函数 key_fasync ,该函数
中直接调用 fasync_helper 函数进行相关处理
10./*释放设备*/
11./* 设备操作函数 */
12./*驱动入口函数*/
13.*驱动出口函数*/