韦东山嵌入式linux系列-驱动程序基石

1 休眠与唤醒

1.1 适用场景

在前面引入中断时,我们曾经举过一个例子

妈妈怎么知道卧室里小孩醒了?

① 时不时进房间看一下: 查询方式
        ◼ 简单,但是累
② 进去房间陪小孩一起睡觉,小孩醒了会吵醒她: 休眠-唤醒
        ◼ 不累,但是妈妈干不了活了
③ 妈妈要干很多活,但是可以陪小孩睡一会,定个闹钟: poll 方式
        ◼ 要浪费点时间,但是可以继续干活。
        ◼ 妈妈要么是被小孩吵醒,要么是被闹钟吵醒。
④ 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈: 异步通知
        ◼ 妈妈、小孩互不耽误。

这 4 种方法没有优劣之分,在不同的场合使用不同的方法。

查询方式

以前是直接返回引脚电平,然后比较前后电平是否有变化,有变化就是有按键发生了。

现在用“休眠-唤醒”机制

① APP 调用 read 等函数试图读取数据,比如读取按键;
② APP 进入内核态,也就是调用驱动中的对应函数,发现有数据则复制到用户空间并马上返回;
③ 如果 APP 在内核态,也就是在驱动程序中发现没有数据,则 APP 休眠;
④ 当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,它会记录数据、唤醒 APP;
⑤ APP 继续运行它的内核态代码,也就是驱动程序中的函数,复制数据到用户空间并马上返回。

驱动中有数据时, 图 中红线就是 APP1 的执行过程,涉及用户态、内核态

有应用层发起的,再回到应用层。处于进程/线程上下文可以调用复杂/耗时的函数。

在中断上下文则不可,中断不能休眠。

驱动中没有数据时, APP1在内核态执行到 drv_read 时会休眠。所谓休眠就是把自己的状态改为非RUNNING,这样内核的调度器就不会让它运行。当按下按键,驱动程序中的中断服务程序被调用,它会记录数据,并唤醒 APP1。所以唤醒就是把程序的状态改为 RUNNING,这样内核的调度器有合适的时间就会让它运行。当 APP1 再次运行时,就会继续执行 drv_read 中剩下的代码,把数据复制回用户空间,返回用户空间。 APP1 的执行过程如下图的红色实线所示,它被分成了 2 段:

值得注意的是,上面 2个红线部分都属于 APP1 的“上下文”,或者这样说:红线所涉及的代码,都是 APP1调用的。但是按键的中断服务程序,不属于 APP1的“上下文”,这是突如其来的,当中断发生时, APP1正在休眠呢。

在 APP1的“上下文”,也就是在 APP1的执行过程中,它是可以休眠的。

在中断的处理过程中,也就是 gpio_key_irq 的执行过程中,它不能休眠:“中断”怎么能休眠?“中断”休眠了,谁来调度其他 APP 啊?

所以,请记住: 在中断处理函数中,不能休眠,也就不能调用会导致休眠的函数。

1.2 内核函数

1 休眠函数

参考内核源码: include\linux\wait.h

函数说明
wait_event_interruptible(wq, condition)休眠,直到 condition 为真;
休眠期间是可被打断的,可以被信号打断
wait_event(wq, condition)休眠,直到 condition 为真;
退出的唯一条件是 condition 为真,信号也不好使
wait_event_interruptible_timeout(wq,
condition, timeout)
休眠,直到 condition 为真或超时;
休眠期间是可被打断的,可以被信号打断
wait_event_timeout(wq, condition,
timeout)
休眠,直到 condition 为真;
退出的唯一条件是 condition 为真,信号也不好使
#define wait_event_interruptible(wq_head, condition)				\
({										\
	int __ret = 0;								\
	might_sleep();								\
	if (!(condition))							\
		__ret = __wait_event_interruptible(wq_head, condition);		\
	__ret;									\
})


#define wait_event(wq_head, condition)						\
do {										\
	might_sleep();								\
	if (condition)								\
		break;								\
	__wait_event(wq_head, condition);					\
} while (0)



#define wait_event_interruptible_timeout(wq_head, condition, timeout)		\
({										\
	long __ret = timeout;							\
	might_sleep();								\
	if (!___wait_cond_timeout(condition))					\
		__ret = __wait_event_interruptible_timeout(wq_head,		\
						condition, timeout);		\
	__ret;									\
})


#define wait_event_timeout(wq_head, condition, timeout)				\
({										\
	long __ret = timeout;							\
	might_sleep();								\
	if (!___wait_cond_timeout(condition))					\
		__ret = __wait_event_timeout(wq_head, condition, timeout);	\
	__ret;									\
})

比较重要的参数就是

① wq: waitqueue,等待队列
        ◼ 休眠时除了把程序状态改为非 RUNNING 之外,还要把进程/进程放入wq 中,以后中断服务程序要从 wq 中把它取出来唤醒。
        ◼ 没有 wq 的话,茫茫人海中,中断服务程序去哪里找到你?
② condition
        ◼ 这可以是一个变量,也可以是任何表达式。表示“一直等待,直到condition 为真”。

2 唤醒函数

参考内核源码: include\linux\wait.h

函数说明
wake_up_interruptible(x)唤醒 x 队列中状态为“ TASK_INTERRUPTIBLE”的线程,只唤
醒其中的一个线程
wake_up_interruptible_nr(x, nr)唤醒 x 队列中状态为“ TASK_INTERRUPTIBLE”的线程,只唤
醒其中的 nr 个线程
wake_up_interruptible_all(x)唤醒 x 队列中状态为“ TASK_INTERRUPTIBLE”的线程,唤醒
其中的所有线程
wake_up(x)唤 醒 x 队 列 中 状 态 为 “ TASK_INTERRUPTIBLE ” 或
“TASK_UNINTERRUPTIBLE”的线程,只唤醒其中的一个线程
wake_up_nr(x, nr)唤 醒 x 队 列 中 状 态 为 “ TASK_INTERRUPTIBLE ” 或
“TASK_UNINTERRUPTIBLE”的线程,只唤醒其中 nr 个线程
wake_up_all(x)唤 醒 x 队 列 中 状 态 为 “ TASK_INTERRUPTIBLE ” 或
“TASK_UNINTERRUPTIBLE”的线程,唤醒其中的所有线程

1.3 驱动框架

驱动框架如下

要休眠的线程,放在 wq 队列里,中断处理函数从 wq 队列里把它取出来唤醒。

所以,要做这几件事:
① 初始化 wq 队列
② 在驱动的read函数中,调用wait_event_interruptible:
        ◼ 它本身会判断 event 是否为 FALSE,如果为 FASLE 表示无数据,则休眠。
        当从 wait_event_interruptible 返回后,把数据复制回用户空间。
③ 在中断服务程序里:
        ◼ 设置 event 为 TRUE,并调用 wake_up_interruptible 唤醒线程。

1.4 代码

在上一节的基础上修改:韦东山嵌入式linux系列-Linux 系统对中断的处理-CSDN博客

在驱动的读函数里调用 wait_event_interruptible:

// 实现自己的read函数,等待按键按下
static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	// 当g_key为真时,休眠才被打断
	wait_event_interruptible(gpio_key_wait, g_key);
	err = copy_to_user(buf, &g_key, 4);
	// 再次被置为0,进入休眠状态,等待下次唤醒
	g_key = 0;
	return 4;
}

假设 g_key 等于 0,那么 APP 会执行到上述代码进入休眠状态。它被谁唤醒?被控制的中断服务程序:

// 中断服务程序,唤醒线程
static irqreturn_t gpio_key_irq_winter(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);
	// 哪一个按键放在高8位,按下/松开是val
	g_key = (gpio_key->gpio << 8) | val;
	// 唤醒g_key队列
	wake_up_interruptible(&gpio_key_wait);
	return IRQ_HANDLED;
}

注意这 2 个函数,一个没有使用“ &”,另一个使用了“ &”:
 

wait_event_interruptible(gpio_key_wait, g_key);
wake_up_interruptible(&gpio_key_wait);

应用程序并不复杂,调用 open、 read 即可,代码在 button_test.c 中

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

/*
 * ./button_test /dev/100ask_button0
 *
 */
int main(int argc, char **argv)
{
	int fd;
	int val;
	
	/* 1. 判断参数 */
	if (argc != 2) 
	{
		printf("Usage: %s <dev>\n", argv[0]);
		return -1;
	}

	/* 2. 打开文件 */
	fd = open(argv[1], O_RDWR);
	if (fd == -1)
	{
		printf("can not open file %s\n", argv[1]);
		return -1;
	}

	while (1)
	{
		/* 3. 读文件 */
		read(fd, &val, 4);
		printf("get button : 0x%x\n", val);
	}
	
	close(fd);
	
	return 0;
}

编译

1.5 测试

在开发板挂载 Ubuntu 的NFS目录

mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs/ /mnt

使用上一节的设备树文件:韦东山嵌入式linux系列-Linux 系统对中断的处理-CSDN博客

先确定boot分区挂载在哪里

cat /proc/mounts

这里没问题,如果不是/boot分区,重新挂载

mount /dev/mmcblk2p2 /boot

把编译出来的设备树文件拷贝到/boot目录下

reboot重启

安装驱动,强制安装

insmod -f gpio_key_drv.ko

按按键测试,按下是1,松开是0

利用top查看占资源情况

缺点是只能读最新的一个数据,因为按键数据放在全局变量g_key中了

1.6 使用环形缓冲区改进驱动程序

利用唤醒缓冲区存储数据

// 环形缓冲区
#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;

// 下一个位置
#define NEXT_POS(x) ((x+1) % BUF_LEN)

// 判断环形缓冲区是否为空
static int is_key_buf_empty(void)
{
	// 相等时为空
	return (r == w);
}

// 判断缓冲区是否满
static int is_key_buf_full(void)
{
	return (r == NEXT_POS(w));
}

// 写数据进去
static void put_key(int key)
{
	if (!is_key_buf_full())
	{
		g_keys[w] = key;
		// w后移
		w = NEXT_POS(w);
	}
}

// 获得数据
static int get_key(void)
{
	int key = 0;
	if (!is_key_buf_empty())
	{
		key = g_keys[r];
		// r后移
		r = NEXT_POS(r);
	}
	return key;
}

使用环形缓冲区之后,休眠函数可以这样写

// 实现自己的read函数,等待按键按下
static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	int err;
	int key;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	// 当is_key_buf_empty为假时,也就是不空,休眠才被打断,才能get数据
	wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
	key = get_key();
	err = copy_to_user(buf, &key, 4);
	return 4;
}

唤醒函数可以这样写

// 中断服务程序,唤醒线程
static irqreturn_t gpio_key_irq_winter(int irq, void* dev_id)
{
	struct gpio_key *gpio_key = dev_id;
	int val;
	int key;
	val = gpiod_get_value(gpio_key->gpiod);
	printk("key %d %d\n", gpio_key->gpio, val);
	// 哪一个按键放在高8位,按下/松开是val
	key = (gpio_key->gpio << 8) | val;
	// 加入数据
	put_key(key);
	
	// 唤醒g_key队列
	wake_up_interruptible(&gpio_key_wait);
	return IRQ_HANDLED;
}

测试过程同1.5

可以存储多个数据

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值