前言:学东西的时候总是喜欢碎碎念,去思考该怎么学。关于嵌入式开发,以前就觉得嵌入式只是一个工具,关键还是结合专业知识赋能,比如控制、信号处理、神经网络、计算机网络、各种协议比如蓝牙、wifi,音视频,当然,如果能够把内核学的很透彻,那也是很了不起的。现在越学越觉得这个东西应该就是一个工具,并且在基于具体的平台去学习的时候,应该重点学习的是思想和框架,以及嵌入式中共性的东西。在学习的过程中,以驱动开发为例,首要的是理解和掌握驱动开发的框架,重点就在于platform_driver结构体、platform_device结构体(现在一般通过设备树由内核处理)和file_operation结构体,从这个点出发再考虑到结构体中的probe函数,remove函数,在里面要做什么工作,比如注册驱动,获取设备树信息等等。在学习的过程没必要特意去记住所有细节,而要先专注于整体的框架和逻辑,理解整个过程内核是怎么工作的,你的驱动代码是怎么工作的,内核和你的驱动代码是如何交互的。具体的API用的多了自然就记住了,如果专门记住但是后面不用,也很快就会忘记了。理解了所学内容的逻辑,在实际工作中再参考别人的代码,就可以理解别人的代码,就可以在别人的基础上写出自己的代码。本来很多东西,就是模仿,改造,再创新的过程。高中学习新内容的时候,也是理解了概念知识点后就去模仿别人的解题思路,然后经过自己的思考后可以进行一定的改造,最后才形成自己的解题风格,做其它事情也是一样。在韦东山的视频教程里,也反复强调的是可以参考别人的代码去写自己的代码,一直在教我们如何锻炼自己的开发能力。
所以,作为一名工程师的话,就要有记录自己的学习、项目的习惯。通过记录,即是一次对所学所做的重新思考和复习,也便于未来需要用到的时候可以快速复习快速上手,甚至可以代码复用,提高工作效率。记录,既是复盘也是便于今后需要的时候重新回忆,自己写的东西,符合自己的语言习惯,肯定是比去看别人总结的更好理解。
接下来介绍另一种休眠唤醒1机制-poll机制
1 poll机制介绍
poll机制简单概括就是在原来的有数据就唤醒的基础上加入了超时唤醒机制,这主要是为了克服休眠-唤醒方式中可能等待时间很久的缺点。
poll机制的运行流程如下:
(1)应用程序调用poll函数查询一下是否有数据,同时传入设置的超时时间;
(2)应用程序调用的poll函数会引起软中断进入内核态调用驱动对应的系统调用函数,如果有数据就立即返回;
(3)如果没有数据就休眠;
(4)有数据时,比如按键中断发生会记录按键值,然后唤醒进程
(5)或者超时时间到了也会唤醒进程
(6)再次使用poll函数查询是否有数据;
上面的总结还是比较模糊的,总的来说在poll机制中会查询两次是否有数据,并根据查询结果进行下一步动作
从上面的poll机制的函数流程图也可以看出来,poll机制的两次查询主要是在for循环中会至少两次调用drv_poll:第一次进入for循环时会调用drv_poll,如果返回值表示没有数据,那么会陷入休眠,然后等待超时或者有按键按下唤醒进程后会再次进入for循环,再次指向drv_poll,此时无论有无数据都会跳出循环。在上图中,drv_poll就是程序员需要自己编写的驱动函数。
特别注意的是,在使用poll机制时,进程并不是在drv_poll中休眠的,它只是把进程放入了队列,之后由操作系统负责确定是否休眠,并且drv_poll需要返回状态。这个线程唤醒要不是有数据了(按键按下),要不是因为发生超时。
下面更加详细的介绍poll机制的内核代码
应用程序使用系统调用时,会调用应用程序提供API封装系统调用编号然后出发软中断进入内核,在内核中再执行对应的系统调用函数,具体可以参考哈工大操作系统课程的实验三系统调用。
对于poll,它从应用层到内核执行的过程为:poll->sys_poll(内核中对应的函数)->do_sys_poll->do_poll->do_pollfd->drv_pooll(自己写的poll驱动函数)。
SYSCALL_DEFINE3是一个宏,通过宏展开就是sys_poll函数的定义。在sys_poll函数中会对超时参数做简单处理然后调用do_sys_poll。
在do_sys_poll中初始化poll_wqueues变量table后会接着调用do_poll
分析上面的do_poll函数以及图片解释的文字,在do_poll函数中有一个for循环,进入for循环时会执行if判断中的函数do_pollfd,在do_pollfd函数中有一个判断if(f.file->f_op->poll),这里判断传入的文件操作符结构体中的file_operations字段是否有poll函数,如果有那么执行,而这个poll函数就是我们写的drv_poll函数。在drv_poll函数中会调用poll_wait函数,poll_wait函数中会判断p->_proc & p & wait_address然后通过回调函数p->_qproc把线程放入队列中。dev_poll函数中执行完poll_wait后该线程就被放入了某个队列。然后执行一些条件判断来确定返回0还是POLLIN | POLLRDNORM。显然,在该函数中,执行判断缓存是否为空后如果是空的话,返回0,那么在上一个do_pollfd函数中mask就是0,最后do_poll函数中for循环内的条件判断接收到的就是0,不会执行count++,假设此时还没有超时,那么在if(count || time_out)就不会成立,接下来的if(poll_schedule_timeout())中就会把线程休眠。休眠可以通过中断或者超时唤醒,幻想乡后重新执行第二次for循环,再次执行前面的流程。如果此时缓存中有数据,在执行drv_poll时就会返回POLLIN | POLLRDNORM,在do_pollfd中接收返回值后mask和POLLIN | POLLRDNORM逻辑与,返回mask,那么在do_poll中count就会加1,如果没有数据,说明是超时唤醒的,count不会加1,但是此时time_out = 1,因此无论如何,在接下来的if中break一定执行,跳出for循环。从上面的分析,drv_poll会可能执行两次,进行两次缓存检查。还需要注意的是,在第一次for循环执行后,pt->qproc被赋值NULL,因此第二次执行时在poll_wait函数中不会执行p->qproc(),因此不会再把线程放入队列,从而避免了重复放入队列。并且后续线程被唤醒后,再次执行poll_wait时,也不会把线程再放入队列中了。
说明:POLLIN其实就是一个宏,猜测它就是某一位为1的整数,drv_poll的返回值和POLLIN逻辑与得到的结果要不0要不POLLIN非零,从而可以作为条件判断。并且,drv_poll返回的状态值会在返回do_pollfd函数中被赋值给pollfd(一个和poll有关的结构体)的revents字段,在应用层中就可以通过这个字段来判断缓冲区是否有值。
关于poll机制在内核中的执行过程就介绍到这里,关键是do_poll函数,还是需要好好理解。更详细的剖析的话,后续可以看看韦东山的视频。
2 poll的使用方法
2.1 驱动层
在驱动层,代码的核心就是提供poll函数并放入file_operations结构体中,在函数中需要做两个事情:
(1)把当前线程放入队列。poll函数可能会被调用两次,但是线程不需要被放入队列两次,因此可以使用内核提供的poll_wait函数把线程放入队列,如果线程已经在队列中,poll_wait函数不会重复把线程再放入队列。
(2)驱动层中自己写的poll函数要返回状态,因为调用它的得到函数需要用到返回的状态值。
2.2 应用层
应用程序需要定义poll相关得到结构体变量struct pollfd,然后直接调用poll系统调用就可以。
特别说明:在poll机制中,定义pollfd结构体变量fd后(可以是多个,那就监测多个事件),需要初始化pollfd结构体中的event,表示要等待的是什么事件,然后在驱动里我们写的drv_poll函数中要返回对应的状态或者0,比如events初始化为POLLIN,那么drv_poll中要返回0或者POLLIN,因为内核会把drv_poll的返回值写入fd的revents中然后在应用层才可以根据revents来判断是否满足条件。因此只有event和drv_poll返回的类型一致,对于缓冲区是否能写入(也即是否已经满了),完全可以用POLLIN代替POLLOUT,只要程序员能够理解内核处理poll机制的过程即可。当然,正常情况是不会用POLLOUT代替POLLIN的,这只是我突然想到。
3 基于poll机制的按键驱动程序代码
本次驱动程序在原来休眠-等待的基础上使用了poll机制,并添加了循环缓冲区来记录按键的值,驱动代码如下:
点击查看代码
/*
使用循环缓冲区的按键中断驱动程序,发生按键中断时将按键记录到循环缓冲区中
基于总线设备驱动模型进行驱动开发,platform_device由设备树产生,并且使用GPIO子系统和pinctr子系统
在该驱动程序中需要自己构造platform_driver结构体并且要和设备树生成的platform_device结构体匹配
*/
#include "asm-generic/errno-base.h"
#include "asm-generic/poll.h"
#include "asm/uaccess.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/gpio/driver.h"
#include "linux/irqreturn.h"
#include "linux/kdev_t.h"
#include "linux/nfs_fs.h"
#include "linux/of.h"
#include "linux/wait.h"
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/ktime.h>
#include <linux/delay.h>
#include <linux/poll.h>
static int major; //主设备号
static struct class *key_class;
static int count; //引脚树,也即按键数量
/*
设置按键中断时,会使用到引脚的引脚编号(老版本),引脚描述结构体,引脚中断号,引脚标志位
因此这些信息可以使用一个结构体来保存,一个引脚按键就对应一个,如果有多个按键就可以用结构体数组
按键中断本质是引脚输入电平变化引起的中断,因此如果有外设在完成工作后发出电平变化信号的话,可以使用这种中断方式
比如AD7606在完成转换后BUSY引脚会变成持续一段时间的高电平,那么可以使用这个高电平产生引脚中断然后去读取AD7606的AD转换值
*/
struct gpio_interrupt{
int gpio_num; //GPIO引脚编号,老版本
struct gpio_desc *desc; //gpio引进描述结构体
int irq; //中断号
int flag;
};
struct gpio_interrupt *key_gpio; //定义一个结构体指针,在probe函数中再根据设备树中定义的引脚数量动态分配内存
#define BUF_LEN 128 //缓冲区大小
static int key_buf[BUF_LEN];
static int r, w; //定义缓冲区写指针,r=w时表示缓冲区空,(w+1) % BUF_LEN = r时表示缓冲区满
//实际上BUF_LEN个缓存单元只能用BUF_LEN-1个,因为要判断是否满/空
#define next_position(x) (((x) + 1) % BUF_LEN)
static bool is_buf_empty(void)
{
// printk("%s %s line %d, w = %d, r = %d\n", __FILE__, __FUNCTION__, __LINE__, w, r);
return (r == w);
}
static bool is_buf_full(void)
{
// printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return (r == next_position(w));
}
static void put_key_value(int key)
{
// printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
if(!is_buf_full())
{
key_buf[w] = key;
w = next_position(w); //更新写指针
}
}
static int get_key_value(void)
{
int key;
if(!is_buf_empty())
{
key = key_buf[r];
r = next_position(r); //更新指针
}
else
{
key = -1;
printk("%s %s line %d, buf is empty! failed to read\n", __FILE__, __FUNCTION__, __LINE__);
}
return key;
}
int key_open(struct inode *node, struct file *file)
{
/*打开文件时会执行,在这里可以做一些初始化操作
设置GPIO为输入
*/
int i;
r = 0;
w = 0;
for(i = 0; i < count; i++)
{
gpiod_direction_input(key_gpio[i].desc);
}
return 0;
}
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);
ssize_t key_read(struct file *file, char __user *buf, size_t count, loff_t *offset)
{
/*用户程序使用read系统调用时会调用,返回值最终会返回给用户程序中的read
读取按键值,如果循环缓冲区有内容,那么直接读取缓冲区内容,否则休眠
因此可以使用缓冲区是否为空作为休眠的信号
*/
int err;
int key;
// printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
//如果buf空,is_buf_empty()返回1, !is_buf_empty()是0,那么陷入睡眠
// wait_event_interruptible(gpio_key_wait, !is_buf_empty()); //在poll机制中,休眠的工作由内核自动进行,只需要在poll函数调用poll_wait并指定队列就可以了
//如果没有休眠或者从休眠唤醒,说明缓冲区有内容了,读缓冲区
key = get_key_value();
//把按键值写入用户空间
err = copy_to_user(buf, &key, sizeof(key));
if(err)
{
printk("failed copy data to user\n");
return -EFAULT;
}
return sizeof(key);
}
unsigned int key_poll(struct file *fd, struct poll_table_struct *wait)
{
/*poll函数
调用poll_wait将线程放入等待队列,注意是放入等待队列,不是休眠
检查条件并返回结果
*/
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); //通过这个可以检验函数被调用两次
poll_wait(fd, &gpio_key_wait, wait); //将线程放入gpio_key_wait等待队列,内核会根据监测的事件确定是否休眠
return is_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}
int key_close(struct inode *node, struct file *file)
{
/*用户程序调用close时会执行*/
int i;
for(i = 0; i < count; i++)
{
gpiod_direction_output(key_gpio[i].desc, 0);
}
return 0;
}
struct file_operations key_opr = {
.owner = THIS_MODULE,
.open = key_open,
.read = key_read,
.poll = key_poll,
.release = key_close,
};
static irqreturn_t gpio_isr(int irq, void *dev_id)
{
/*中断处理函数上半部
发生中断后,会执行相应的中断处理函数,在内核中已经提供了相应的中断处理函数
在里面会判断中断号,关中断,我们所写的中断处理函数被注册进内核后就作为内核中断
处理函数的回调函数被调用,在里面我们可以写上我们想执行的操作
*/
struct gpio_interrupt *key = dev_id; //获取引脚信息
int val; //引脚状态
int key_val; //引脚编号 | 引脚状态
val = gpiod_get_value(key->desc); //获取引脚电平状态
val ^= (1); //因为设备树中设置低电平有效,因此引脚电平的逻辑值和真实值是相反的
key_val = (key->gpio_num << 8) | val; //记录引脚状态和引脚编号
// printk("%s %s line %d: key_val: %d\n", __FILE__, __FUNCTION__, __LINE__, key_val);
put_key_value(key_val); //把按键状态放入缓冲区
wake_up_interruptible(&gpio_key_wait); //唤醒线程。
return IRQ_HANDLED; //返回IRQ_HANDLED表示已经处理中断,否则内核还会继续执行action链表中其他函数
}
int key_probe(struct platform_device *pdev)
{
/*
驱动和设备节点匹配时执行
在这里会做一些初始化操作:获取设备树信息,设置中断,注册驱动file_operation结构体
*/
struct device_node *node = pdev->dev.of_node; //获取设备树中引脚对应的device_node结构体
int err;
int i;
enum of_gpio_flags flag;
// count = of_gpio_count(node); //设备树中定义了多少个节点,根据key-gpio来计算,这个函数计算的不对,好像是因为我没有用gpios属性
count = of_gpio_named_count(node, "key-gpios");
printk("count = %d\n", count);
if(count <= 0)
{
printk("%s %s line %d, No available gpio, count = %d\n", __FILE__, __FUNCTION__, __LINE__, count);
return -1;
}
key_gpio = kzalloc(sizeof(struct gpio_interrupt) * count, GFP_KERNEL);
//获取设备树中定义的节点信息
for(i = 0; i < count; i++)
{
//获取引脚编号
// key_gpio[i].gpio_num = of_get_gpio_flags(node, i, &flag); //以后尽量使用命名式的函数
key_gpio[i].gpio_num = of_get_named_gpio_flags(node, "key-gpios", 0, &flag);
if(key_gpio[i].gpio_num < 0)
{
printk("%s %s line %d, of_get_gpio_flags failed\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
//获取引脚描述结构体
key_gpio[i].desc = gpiod_get_index(&pdev->dev, "key", i); //这里使用get,那么在remove函数里就需要使用put释放引脚
key_gpio[i].flag = flag & OF_GPIO_ACTIVE_LOW;
key_gpio[i].irq = gpiod_to_irq(key_gpio[i].desc); //获取对应的中断号
}
//注册中断
for(i = 0; i < count; i++)
{
err = request_irq(key_gpio[i].irq, gpio_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "gpio_key", &key_gpio[i]);
printk("%d gpio irq_request\n", key_gpio[i].gpio_num);
}
/*注册file_operaton结构体,创建类和设备*/
major = register_chrdev(0, "key_interrupt", &key_opr);
key_class = class_create(THIS_MODULE, "key_class");
if(IS_ERR(key_class))
{
printk("%s %s line %d, class create failed\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "key_interrupt");
return PTR_ERR(key_class);
}
//创建设备节点,不需要为每个引脚都创建设备节点,所有引脚(按键)共用一个设备节点
device_create(key_class, NULL, MKDEV(major, 0), NULL, "key");
return 0;
}
int key_remove(struct platform_device *pdev)
{
int i;
/*卸载驱动时执行,在这里要撤销中断,注销驱动、类和设备节点,释放动态分配的内存*/
//删除设备节点
device_destroy(key_class, MKDEV(major, 0));
//删除类
class_destroy(key_class);
//撤销驱动
unregister_chrdev(major, "key_interrupt");
//撤销中断,释放GPIO
for(i = 0; i < count; i++)
{
free_irq(key_gpio[i].irq, &key_gpio[i]);
gpiod_put(key_gpio[i].desc);
}
kfree(key_gpio); //释放内存
return 0;
}
static const struct of_device_id keys_match_table[] = {
{
.compatible = "mykey_driver_interrupt"
}
};
struct platform_driver key_driver = {
.probe = key_probe,
.remove = key_remove,
.driver = {
.name = "key_interrupt",
.of_match_table = keys_match_table,
},
};
static int __init key_drv_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = platform_driver_register(&key_driver);
return err;
}
static void __exit key_drv_exit(void)
{
platform_driver_unregister(&key_driver);
}
module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");
应用程序代码如下:
点击查看代码
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/poll.h>
int main(int argc, char** argv)
{
int fd_button;
int val;
struct pollfd fds[1]; //使用poll机制需要定义pollfd结构体变量,需要检测几个事件就定义几个
int ret;
int timeout_ms = 50000; //定义poll的超时时间5s,超过5s没有数据就唤醒进程
/*判断参数*/
if(argc != 2)
{
printf("Usage: %s <buton_dev>\n", argv[0]);
}
fd_button = open(argv[1], O_RDWR);
if(fd_button == -1)
{
printf("can not open file %s\n", argv[1]);
return -1;
}
fds[0].fd = fd_button;
fds[0].events = POLLIN; //初始化fds,明确需要检测的事件
while(1)
{
ret = poll(fds, 1, timeout_ms);
if((ret == 1) && (fds[0].revents & POLLIN)) //如果返回1同时发生了等待的时间,就可以进行下一步操作
{
read(fd_button, &val, sizeof(val));
printf("key_value: %d\n", val);
}
else
{
printf("time out\n");
}
}
close(fd_button);
return 0;
}