2024-05-14-嵌入式驱动开发
嵌入式驱动开发
第一章:字符设备驱动开发
1.insmod和modprobe的区别
insmod 命令不能解决模块的依赖关系,比如 drv.ko 依赖 first.ko 这个模块,就必须先使用insmod 命令加载 first.ko 这个模块,然后再加载 drv.ko 这个模块。但是 modprobe 就不会存在这个问题, modprobe 会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此modprobe 命令相比 insmod 要智能一些。
2.设备号的组成
为了方便管理, Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
Linux 提供了一个名为 dev_t 的数据类型表示设备号,dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号, 低 20 位为次设备号。
MAJOR // 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
MINOR //用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
MKDEV //用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。
3.printk和printf
这里使用了 printk 来输出信息,而不是 printf!因为在 Linux 内核中没有 printf 这个函数。 printk 相当于 printf 的孪生兄妹, printf运行在用户态, printk 运行在内核态。在内核中想要向控制台输出或显示一些内容,必须使用printk 这个函数。不同之处在于, printk 可以根据日志级别对消息进行分类。
4.copy_to_user函数
因为内核空间不能直接操作用户空间的内存,因此需要借助 copy_to_user 函数来完成内核空间的数据到用户空间的复制。
第二章:嵌入式Linux LED驱动开发
1.ioremap和iounmap函数
ioremap 函数用于获取指定物理地址空间对应的虚拟地址空间。 我们对寄存器进行配置时,需要对寄存器的物理地址进行操作,在开启MMU的情况下,直接操作寄存器地址物理地址是不成功的,需要使用函数得到物理地址的虚拟地址后进行操作。
2.io空间
IO空间是一个与CPU内存空间独立的用于输入输出设备与CPU进行数据交换的地址空间,而IO内存和IO端口是这个空间中的两种不同的访问方式。
首先,IO内存通常被称为Memory-Mapped I/O (MMIO),它位于CPU地址空间内。这意味着IO内存与普通内存在访问机制上没有区别,都是通过CPU的地址总线、控制总线发送信号进行读写操作。要访问IO内存,需要将其映射到CPU地址中,之后便可以像访问普通内存一样对其进行操作。
其次,IO端口,也称为Port I/O,它的空间与CPU的内存空间相互独立。在X86架构中较为常见,拥有独立的地址范围,因此CPU需要通过特定的函数或指令来访问这些端口。例如,X86架构中有专门的IN和OUT指令用于与IO端口交互。
最后,IO空间是指专门为输入输出设备设置的地址空间,在某些架构下(如x86),这个空间和内存空间是分开的。IO空间可以进一步分为IO端口和IO内存,前者是通过专用指令访问,后者则是映射到内存空间中,可以通过内存访问的方式进行读取写入。
总的来说,这三者虽然都属于计算机中用于处理输入输出的部分,但它们所处的位置不同,以及被CPU访问的方式也有所区别。
3.I/O内存访问函数
使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
4.应用层和内核层传递数据
应用层和内核层是不能直接进行数据传输的。 要想进行数据传输, 要借助下面的这两个函数
static inline long copy_from_user(void *to, const void __user * from, unsigned long n)
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
第三章:字符设备基本驱动框架
1.注册字符设备驱动函数
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备。卸载驱动模块的时也需要注销掉字符设备。
1.1分配设备号函数
static inline int register_chrdev(unsigned int major,
const char *name,
const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major,
const char *name)
这种注册函数会 将后面所有的次设备号全部占用 ,而且 主设备号需要我们自己去设置 ,现在不推荐这样使用(weidongshan驱动入门使用的函数)。
Linux驱动推荐使用 动态分配设备号 。
- 动态申请设备号
// 该函数可以用来申请一段连续的多个设备号
int alloc_chrdev_region(dev_t *dev, // 保存申请到的设备号
unsigned baseminor, // 次设备号起始地址
unsigned count, // 要申请的设备号数量
const char *name) // 设备名字
- 静态申请设备号
int register_chrdev_region(dev_t from, // 要申请的起始设备号
unsigned count, // 设备号个数
const char *name); //设备号在内核中名称
- 释放设备号
void unregister_chrdev_region(dev_t from, // 要释放的设备号
unsigned count) // 要释放的设备号数量
- 申请设备号模板
//创建设备号
if (newchrled.major) //定义了设备号就静态申请
{
newchrled.devid = MKDEV(newchrled.major, 0);
register_chrdev_region(newchrled.devid,
NEWCHRLED_CNT,
NEWCHRLED_NAME);
}
else //没有定义设备号就动态申请
{
alloc_chrdev_region(&newchrled.devid,
0,
NEWCHRLED_CNT,
NEWCHRLED_NAME);//申请设备号
newchrled.major = MAJOR(newchrled.devid); //获取分配号的主设备号
newchrled.minor = MINOR(newchrled.devid); // 获取分配号的次设备号
}
1.2初始化cdev
在 Linux 中使用 cdev 结构体 表示一个字符设备
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;//操作函数集合
struct list_head list;
dev_t dev;//设备号
unsigned int count;
};
在 cdev 中有两个重要的成员变量: ops 和 dev ,字符设备文件操作函数集合 file_operations 以及 设备号 dev_t 。
void cdev_init(struct cdev *cdev, // cdev结构体
const struct file_operations *fops); // ops结构体
1.3将设备添加到内核
cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备 。
int cdev_add(struct cdev *p, // 要添加的cdev结构
dev_t dev, // 绑定的起始设备号
unsigned count) // 设备号个数
register_chrdev = alloc_chrdev_region/register_chrdev_region + cdev_init + cdev_add
2.自动生成设备节点
在驱动中实现 自动创建设备节点 的功能以后,使用 modprobe 加载驱动模块成功的话就会 自动在/dev 目录下创建 对应的设备文件。
自动创建设备节点的工作是在 驱动程序的入口函数中完成 的,一般在 cdev_add 函数后面添加 自动创建设备节点相关代码。
2.1创建一个类
- 类的创建函数
struct class *class_create(struct module *owner, const char *name);
- class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。
- 设备类名对应 /sys/class 目录的子目录名。
- 返回值是个指向结构体 class 的指针,也就是创建的类。
- 删除一个类
void class_destroy(struct class *cls); // cls要删除的类
2.2创建设备节点
还需要在 类下创建一个设备 ,使用 device_create 函数在类下面创建设备。
- 设备节点的创建
struct device *device_create(struct class *class, // 设备类指针
struct device *parent, // 父设备指针
dev_t devt, // 设备号
void *drvdata, // 额外数据
const char *fmt, ...) //设备文件名称
- 设备节点的删除
void device_destroy(struct class *class, dev_t devt);
2.3文件私有属性
- 每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)
- 一个设备的 所有属性信息将其做成一个结构体
- 编写驱动 open 函数的时候将 设备结构体作为私有数据添加到设备文件中
- 在 write、 read、 close 函数中直接读取 private_data即可得到设备结构体
/* newchrled设备结构体 */
struct newchrled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct newchrled_dev newchrled; /* led设备 */
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchrled; /* 设置私有数据 */
return 0;
}
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
struct newchrled_dev *dev = (struct newchrled_dev *)filp->private_data;
return 0;
}
3.新字符设备驱动框架
#define NEWCHR_CNT 1
#define NEWCHR_NAME "NEWCHR"
//内核缓存区
static char readbuf[100]; //读数据缓存
static char writebuf[100]; //写数据缓存
static char kerneldata[] = {"kernel data!"}; //测试数据
//硬件寄存器
#define GPIO_TEST_BASE (0x01234567) //宏定义寄存器映射地址
static void __iomem *GPIO_TEST; // __iomem 类型的指针,指向映射后的虚拟空间首地址
/* newchr设备结构体 */
struct newchr_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct newchr_dev newchr; /* newchr设备 */
//打开设备
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchr; /* 设置私有数据 */
return 0;
}
// 从设备读取数据
static ssize_t chrdevbase_read(struct file *filp , char __user *buf , size_t cnt , loff_t *offt)
{
int retvalue = 0;
unsigned char databuf[1];
//读取私有数据
struct newchr_dev *dev = (struct newchr_dev *)filp->private_data;
// 读取硬件寄存器
#if 0
//读取寄存器状态
databuf[0] = readl(GPIO_TEST);
retvalue = copy_to_user(buf , databuf, cnt);
//读取内核内存
#else
//测试数据拷贝到读数据缓存中
memcpy(readbuf , kerneldata , sizeof(kerneldata));
//内核中数据(读缓存)拷贝到用户空间
retvalue = copy_to_user(buf , readbuf , cnt);
#endif
if(retvalue == 0) printk("kernel senddate ok!\n");
else printk("kernel senddate failed!\n");
return 0;
}
//向设备写数据
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt , loff_t *offt)
{
int retvalue = 0;
//读取私有数据
struct newchr_dev *dev = (struct newchr_dev *)filp->private_data;
//写硬件寄存器
#if 0
writel(buf[0],GPIO_TEST);
//写内核缓存
#else
//用户数据拷贝到内核空间(写缓存)
retvalue = copy_from_user(writebuf , buf ,cnt);
#endif
if(retvalue == 0) printk("kernel recevdate : %s\n",writebuf);
else printk("kernel recevdate failed!");
return 0;
}
//关闭/释放设备
static int chrdevbase_release(struct inode *inode , struct file *filp)
{
return 0;
}
//设备操作函数
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/* 驱动入口函数 */
static int __init chrdevbase_init(void)
{
int retvalue = 0;
//寄存器物理映射,物理地址映射到虚拟地址指针
GPIO_TEST= ioremap(GPIO_TEST_BASE, 4);
//申请设备号
if(newchr.major) //静态申请
{
newchr.devid = MKDEV(newchr.major , 0);
register_chrdev_region(newchr.devid, NEWCHR_CNT,NEWCHR_NAME);
}else //动态申请
{
alloc_chrdev_region(&newchr.devid , 0 , NEWCHR_CNT , NEWCHR_NAME);
newchr.major = MAJOR(newchr.devid);
newchr.minor = MINOR(newchr.devid);
}
printk("newche major=%d,minor=%d\r\n",newchr.major , newchr.minor);
//字符串设备初始化、注册添加到内核
newchr.cdev.owner = THIS_MODULE;
cdev_init(&newchr.cdev , &newchr_fops);
cdev_add(&newchr.cdev , newchr.devid ,NEWCHR_LED_CNT);
//创建设备类
newchr.class = class_create(THIS_MODULE , NEWCHR_NAME);
if(IS_ERR(newchr.class))
{
return PTR_ERR(newchr.class);
}
//创建类的实例化设备 ,dev下面创建文件
newchr.device = device_create(newchr.class , NULL , newchr.devid ,NULL ,NEWCHR_NAME);
if(IS_ERR(newchr.device))
{
return PTR_ERR(newchr.device);
}
return 0;
}
/* 驱动出口函数 */
static void __exit chrdevbase_exit(void)
{
//解除寄存器映射
iounmap(GPIO_TEST);
//删除cdev字符串设备
cdev_del(&newchr.cdev);
//释放设备号
unregister_chrdev_region(newchr.devid , NEWCHR_CNT);
//具体设备注销
device_destroy(newchr.class, newchr.devid);
//类注销
class_destroy(newchr.class);
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
MODULE_LICENSE("GPI");//GPL模块许可证
MODULE_AUTHOR("songwei");//作者信息
第四章:pintrl子系统
Linux 内核提供了 pinctrl 子系统和 gpio 子系统 用于 GPIO 驱动 。
- 获取设备树中 pin 信息, 管理系统中所有的可以控制的 pin , 在系统初始化的时候, 枚举所有可以控制的 pin, 并标识这些 pin
- 根据获取到的 pin 信息来 设置 pin 的复用功能 ,对于 SOC 而言, 其引脚除了配置成普通的 GPIO 之外,若干个引脚还可以组成一个 pin group, 形成特定的功能
- 根据获取到的 pin 信息来 设置 pin 的电气特性 ,比如上/下拉、速度、驱动能力等
1.pinctrl设备树节点设置
在设备树里面创建一个节点来描述 PIN 的配置信息。pinctrl 子系统一般在 iomuxc子节点下 ,所有需要配置用户自己的pinctrl需要在该节点下添加。
2.添加设备节点
在根节点“/”下创建驱动设备节点,例如“gpioled”。
2.1设备节点的client device属性
test {
pinctrl-names = "default", "wake up";
pinctrl-0 = <&pinctrl_test>;
pinctrl-1 = <&pinctrl_test_2>;
/* 其他节点内容 */
};
- pinctrl-names = “default”, “wake up”; 设备的状态, 可以有多个状态, default 为状态 0, wake up 为状态 1。
- pinctrl-0 = <&pinctrl_test>;第 0 个状态所对应的引脚配置, 也就是 default 状态对应的引脚在 pin controller 里面定义好的节点 pinctrl_test里面的管脚配置。
- pinctrl-1 = <&pinctrl_test_2>;第 1 个状态所对应的引脚配置, 也就是 wake up 状态对应的引脚在 pin controller 里面定义好的节点 pinctrl_test_2里面的管脚配置。
2.2pinctrl和client device的关系
第五章:GPIO子系统
当使用 pinctrl 子系统将引脚的复用设置为 GPIO,可以使用 GPIO 子系统来操作GPIO。
1.GPIO工作内容
- 在 设备树节点中 指定使用的gpio引脚和触发模式
gpioled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "songwei-gpioled";
// client device设备节点
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led>;
//gpio信息
led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
- 在 驱动代码中 配置引脚功能(获取gpio引脚(probe函数)、设置方向(open函数)、读写(read/write函数))。
2.GPIO子系统设备树的设置
2.1设备树中的GPIO控制器
在几乎所有 ARM 芯片中, GPIO 都分为几组,每组中有若干个引脚。 所以在使用 GPIO 子系统之前,就要先确定:它是哪组的?组里的哪一个?
在设备树中, “ GPIO 组”就是一个 GPIO Controller , 这通常都由芯片厂家设置好。我们要做的是找到它名字,比如“ gpio1”,然后指定要用它里面的哪个引脚,比如<&gpio1 0>。
- gpio-controller:表示这一节点为GPIO Contorller,下面有很多引脚。
- #gpio-cell = <2> :表示这个控制器下每一个引脚要用2个32位(cell)来描述。第一个指定引脚,第二个指定触发方式。
2.2 GPIO设备节点的使用
在自己的设备节点中使用属性“[
3.pinctrl和gpio子系统使用程序框架
框架可以分布在不同的函数中进行实现
int ret = 0;
/* 1 、获取设备节点:alphaled */
gpio_led.nd = of_find_node_by_path("/gpioled");
if(gpio_led.nd == NULL)
{
printk("songwei_led node can not found!\r\n");
return -EINVAL;
}else
{
printk("songwei_led node has been found!\r\n");
}
/* 2、 获取设备树中的 gpio 属性,得到 LED 所使用的 LED 编号 */
gpio_led.led_gpio = of_get_named_gpio(gpio_led.nd,"led-gpio",0);
if(gpio_led.led_gpio < 0)
{
printk("can't get led-gpio");
return -EINVAL;
}
printk("led-gpio num = %d\r\n", gpio_led.led_gpio);
/* 3、设置 GPIO1_IO03 为输出,并且输出高电平,默认关闭 LED 灯 */
ret = gpio_direction_output(gpio_led.led_gpio, 1);
if(ret < 0)
{
printk("can't set gpio!\r\n");
}
第六章:内核定时器
1.内核时间管理
内核必须管理系统的运行时间以及当前的日期和时间。
硬件为内核提供了一个系统定时器用以计算流逝的时间, 系统定时器以 某种频率自行触发时钟中断 ,该频率可以通过编程预定, 称 节拍率 。
当时钟中断发生时, 内核就通过一种特殊中断处理程序对其进行处理。 内核知道连续两次时钟中断的间隔时间。 这个间隔时间称为 节拍(tick) 。内核就是靠这种已知的时钟中断来计算墙上时间和系统运行时间。
节拍率
系统定时器频率是通过静态预处理定义的(HZ), 在系统启动时按照 Hz 对硬件进行设置。一般 ARM 体系结构的节拍率多数都等于 100。
- 高节拍优点:提高系统时间精度,如果采用 100Hz 的节拍率,时间精度就是 10ms,采用1000Hz 的话时间精度就是 1ms。能够以更高的精度运行,时间测量也更加准确。
- 高节拍缺点:高节拍率会导致中断的产生更加频繁,频繁的中断会加剧系统的负担, 1000Hz 和 100Hz的系统节拍率相比,系统要花费 10 倍的精力去处理中断, 中断服务函数占用处理器的时间增加。
jiffies: 全局变量 jiffies 用来 记录自系统启动以来产生的节拍的总数 。 启动时, 内核将该变量初始化为 0, 每次时钟中断处理程序都会增加该变量的值。
因为一秒内时钟中断的次数等于 Hz, 所以 jiffes 一秒内增加的值为 Hz, 系统运行时间以秒为单位计算, 就等于time = jiffes/Hz ( jiffes = seconds*HZ)
当 jiffies 变量的值超过它的最大存放范围后就会发生溢出, 对于 32 位无符号长整型, 最大取值为 2^32-1,在溢出前, 定时器节拍计数最大为 4294967295 , 如果节拍数达到了最大值后还要继续增加的话, 它的值会回绕到 0。
第七章:Linux的中断处理
在任何中断上下文中,不能执行任何带有休眠属性的代码。
因为在中断时,休眠会导致中断关闭后无法打开导致这个核无法执行其他代码
1.中断的分类
- 硬件中断:
对于按键中断等硬件产生的中断,称之为“硬件中断” (hard irq)。每个硬件中断都有对应的处理函数,比如按键中断、网卡中断的处理函数肯定不一样。
为方便理解,你可以先认为对硬件中断的处理是用数组来实现的,数组里存放的是函数指针:
当发生 A 中断时,对应的 irq_function_A 函数被调用。硬件导致该函数被调用。
- 软件中断
相对的,还可以人为地制造中断:软件中断(soft irq),如下图所示:
软件中断在flag置1时代表着发生了该中断,并在硬件中断处理完后进行处理。
2.中断处理
2.1中断处理原则
- 不能嵌套
中断的现场保护都是通过栈进行保存,防止中断突然爆发导致栈用尽,规定中断不能嵌套
- 越快越好
中断处理期间,该 CPU 是不能进行进程调度的,所以中断的处理要越快越好,尽早让其他中断能被处理──进程调度靠定时器中断来实现。(中断处理期间关闭中断,定时器中断被屏蔽,进程无法进行调度)
2.2中断分段
为保证系统实时性, 中断服务程序必须足够简短,如果都在中断服务程序中完成, 则会严重降低中断的实时性,
所以, linux 系统提出了一个概念: 把中断服务程序分为两部分: 上半部-下半部 。主要目的就是 实现中断处理函数的快进快出
中断服务程序分为上半部(top half)和下半部(bottom half),上 半部负责读中断源,并在清中断后登记中断下半部,而耗时的工作在下半部处理。
上半部只能通过中断处理程序实现, 下半部的实现目前有 3 种实现方式, 分别为: 软中断、
- tasklet 【耗时不长】
同一个中断的上半部和下半部,在执行时是多对一的关系
不同中断的上半部和下半部,在执行时也是多对一的关系
中断上下半部不能休眠
- 工作队列(work queues)【太耗时,在中断执行期间,app无法执行,通过内核线程进行调度执行,使得app有机会执行,不会卡顿】
- 很耗时的中断处理,应该放到线程里去
- 可以使用 work、 work queue
- 在中断上半部调用 schedule_work 函数,触发 work 的处理
- 既然是在线程中运行, 那对应的函数可以休眠
- 新技术 thread_irq
以前用 work 来线程化地处理中断,一个 worker 线程只能由一个 CPU 执行,多个中断的 work 都由同一个 worker 线程来处理,在单 CPU 系统中也只能忍着了。但是在 SMP 系统中, 明明有那么多 CPU 空着,你偏偏让多个中断挤在这个CPU 上
新技术 threaded irq,为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个 CPU 上执行,这提高了效率。
2.3中断处理中重要的结构体
第八章:驱动基石
1.休眠与唤醒
- 休眠
APP 调用 read 等函数试图读取数据, 比如读取按键;
APP 进入内核态,也就是调用驱动中的对应函数,发现有数据则复制到用户空间并马上返回;
如果 APP 在内核态,也就是在驱动程序中发现没有数据,则 APP 休眠;
- 唤醒
当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,它会记录数据、唤醒 APP;
APP 继续运行它的内核态代码,也就是驱动程序中的函数,复制数据到用户空间并马上返回。
驱动中没有数据则执行到drv_read会休眠,休眠就是把自己的状态改为非 RUNNING,这样内核的调度器就不会让它运行。
按下按键后,驱动程序中断被调用,记录数据并唤醒app。所谓唤醒就是把程序的状态改为 RUNNING,这样内核的调度器有合适的时间就会让它运行。当 APP1 再次运行时,就会继续执行 drv_read 中剩下的代码,把数据复制回用户空间,返回用户空间。
切记:中断处理函数中,不能休眠
处理过程:
初始化wq队列
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait); // 初始化wq队列
在驱动的read函数中:调用wait_event_interruptible。它本身会判断 event 是否为 FALSE,如果为 FASLE 表示无数据,则休眠。当从 wait_event_interruptible 返回后,把数据复制回用户空间。第 49 行并不一定会进入休眠,它会先判断 g_key 是否为 TRUE。执行到第 50 行时,表示要么有了数据(g_key 为 TRUE),要么有信号等待处理(本节课程不涉及信号)。假设 g_key 等于 0,那么 APP 会执行到上述代码第 49 行时进入休眠状态。
static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset) { //printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); int err; wait_event_interruptible(gpio_key_wait, g_key); err = copy_to_user(buf, &g_key, 4); g_key = 0; return 4; }
在中断服务程序里:设置 event 为 TRUE,并调用 wake_up_interruptible 唤醒线程。上述代码中,第 72 行确定按键值 g_key, g_key 也就变为 TRUE 了。然后在第 73 行唤醒 gpio_key_wait 中的第 1 个线程。
static irqreturn_t gpio_key_isr(int irq, void *dev_id) { struct gpio_key *gpio_key = dev_id; int val; val = gpiod_get_value(gpio_key->gpiod); printk("key %d %d\n", gpio_key->gpio, val); g_key = (gpio_key->gpio << 8) | val; wake_up_interruptible(&gpio_key_wait); return IRQ_HANDLED; }
68747470733a2f2f626c6f672e:6373646e2e6e65742f77656978696e5f36363435313231332f:61727469636c652f64657461696c732f313338373931343930