文章目录
-
-
- 一、驱动程序基石
-
- [1-1 休眠与唤醒](#1-1 休眠与唤醒)
- [1-2 POLL机制](#1-2 POLL机制)
- [1-3 异步通知](#1-3 异步通知)
-
- [(1) 异步通知程序解析](#(1) 异步通知程序解析)
- [(2) 异步通知机制内核代码详解](#(2) 异步通知机制内核代码详解)
- [1-4 阻塞与非阻塞](#1-4 阻塞与非阻塞)
- [1-5 定时器](#1-5 定时器)
-
- [(1) 内核函数](#(1) 内核函数)
- [(2) 定时器时间单位](#(2) 定时器时间单位)
- [1-6 中断下半部 tasklet](#1-6 中断下半部 tasklet)
-
本人学习完韦老师的视频,因此来复习巩固,写以笔记记之。
韦老师的课比较难,第一遍不知道在说什么,但是坚持看完一遍,再来复习,基本上就水到渠成了。
看完视频复习的同学观看最佳!
基于 IMX6ULL-PRO
参考视频 Linux快速入门到精通视频
参考资料 :01_嵌入式Linux应用开发完全手册V5.1_IMX6ULL_Pro开发板.pdf
一、驱动程序基石
1-1 休眠与唤醒
当应用程序必须等待某个事件发生,比如必须等待按键被按下时, 可以使用休眠-唤醒 机制
① APP调用read等函数试图读取数据,比如读取按键;
② APP进入内核态,也就是调用驱动中的对应函数,发现有数据则复制到用户空间并马上返回;
③ 如果APP在内核态,也就是在驱动程序中发现没有数据,则APP休眠;
④ 当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,它会记录数据、唤醒APP
⑤ APP继续运行它的内核态代码,也就是驱动程序中的函数,复制数据到用户空间并马上返回。
驱动框架
休眠,直到condition 为真;休眠期间是可被打断的,可以被信号打断
c
wait_event_interruptible(wq, condition)
唤醒wq队列中状态为" TASK_INTERRUPTIBLE "的线程,只唤醒其中的一个线程
c
wake_up_interruptible(wq)
要休眠的线程,放在wq 队列里,中断处理函数从wq队列里把它取出来唤
醒。
① 初始化wq队列
② 在驱动的read函数中,调用 wait_event_interruptible。它本身会判断
event是否为 FALSE ,如果为FASLE表示无数据,则休眠。
当从wait_event_interruptible 返回后,把数据复制回用户空间。
③ 在中断服务程序里:设置event 为TRUE,并调用wake_up_interruptible 唤醒线程。
1-2 POLL机制
使用休眠唤醒的方式等待某个事件发生时,有一个缺点:等待的时间可能
很久 。我们可以加上一个超时时间,这时就可以使用poll 机制。
① APP不知道驱动程序中是否有数据,可以先调用 poll函数查询一下, poll函数可以传入超时时间
② APP进入内核态,调用驱动程序的poll函数,有数据的话立刻返回;
③ 如果发现没有数据时就休眠一段时间
④ 当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,它会记录数据、唤醒APP
⑤ 当超时时间到了之后,内核也会唤醒APP
⑥ APP根据poll函数的返回值就可以知道是否有数据,如果有数据就调用
read得到数据。
drv_poll函数需要做的事
① 把当前线程挂入队列wq:poll_wait
②返回设备状态:drv_poll 要返回自己的当前状态:P OLLIN | POLLRDNORM) 或 POLLOUT | POLLWRNORM) 。
c
static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
poll_wait(fp, &gpio_key_wait, wait);
return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}
button_test.c
c
int main(int argc, char **argv)
{
int fd;
int val;
struct pollfd fds[1];
int timeout_ms = 5000;
int ret;
/* 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;
}
fds[0].fd = fd;
fds[0].events = POLLIN;
while (1)
{
/* 3. 读文件 */
ret = poll(fds, 1, timeout_ms);
if((ret == 1) && (fds[0].revents & POLLIN)){
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}
else{
printf("timeout\n");
}
}
close(fd);
return 0;
}
1-3 异步通知
(1) 异步通知程序解析
异步通知流程如下:
① APP给SIGIO这个信号注册信号处理函数func,以后APP收到SIGIO信号时,这个函数会被自动调用。
② 把APP的PID(进程 ID)告诉驱动程序,这个调用不涉及驱动程序,在内核的文件系统层次记录PID。
c
filp->f_owner.pid = get_pid(pid);
③ 读取驱动程序文件Flag
④ 设置Flag里面的FASYNC位为1;当 FASYNC位发生变化时,会导致驱动程序的fasync被调用
⑤ 调用faync_helper ,它会根据 FAYSNC的值决定是否设置
button_async -->fa_file=filp(内含PID);open文件时,会在内核文件系统中有一个struct file *filp结构体,filp->f_owner.pid 里面含有之前设置的PID 。
⑥ APP做其他事;当按下按键,发生中断,驱动程序的中断服务程序被调用,里面调用kill_fasync 发信号
⑦ APP收到信号后,它的信号处理函数被自动调用,可以在里面调用
read函数读取按键。
驱动程序 中提供对应的drv_fasync
函数,并在FAYNC变化时,调用fasync_helper
函数,使得button_fasync->fa_file = filp或者NULL
c
struct fasync_struct *button_fasync;
static int gpio_key_drv_fasync(int fd, struct file *file, int on)
{
if (fasync_helper(fd, file, on, &button_fasync) >= 0)
return 0;
else
return -EIO;
}
在GPIO中断服务程序中,若button_fasync->fa_file非空,则获得PID,并发信号给上层应用
c
kill_fasync(&button_fasync, SIGIO, POLL_IN);
上层应用程序
(2) 异步通知机制内核代码详解
上层应用执行fcntl 函数,内核会调用fs/fcntl.c 的如下函数。
进入do_fcntl函数,flag标志对应函数的cmd
以下分别对三种flag进行代码演示
① 当flag是F_SETOWN
时,内核do_fcntl中调用f_setown函数,最终将pid给filp
c
fcntl(fd, F_SETOWN, getpid());
② 当flag是F_GETFL
时,获取文件的状态标志
c
flags = fcntl(fd, F_GETFL);
③ 当flag是F_SETFL时,设置文件支持异步通知功能
c
fcntl(fd, F_SETFL, flags | FASYNC);
启动了FASYNC 功能的话,驱动程序的 button_fasync 就被设置了,它指向的 fasync_struct 结构体里含有 filp里含有PID
c
static int gpio_key_drv_fasync(int fd, struct file *file, int on)
{
if (fasync_helper(fd, file, on, &button_fasync) >= 0)
return 0;
else
return -EIO;
}
从button_fasync 指针中,取出 fasync_struct 结构体,从这个结构体的 fa_file 中得到接收方的PID ,然后使用 send_sigio函数发送信号。根据 PID找到进程在内核的 task_struct结构体, 修改里面的某些成员表示收到了信号。
c
kill_fasync(&button_fasync, SIGIO, POLL_IN);
1-4 阻塞与非阻塞
所谓阻塞,就是等待某件事情发生。比如调用read读取按键时,如果没有按键数据则read函数不会返回,它会让线程休眠等待。
使用poll时,传入超时时间不为0(阻塞);设置超时时间为0,没有数据立即返回(非阻塞)
如何设置阻塞与非阻塞呢?
① open时
c
int fd = open("/dev/xxx", O_RDWR | O_NONBLOCK); /* 非阻塞方式*/
int fd = open("/dev/xxx", O_RDWR ); /* 阻塞方式*/
② open后
c
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK); /* 非阻塞方式*/
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); /* 阻塞方式*/
驱动程序中,当APP打开某个驱动时,在内核中会有一个struct file 结构体的f _flags对应打开文件时的标记位;可以设置f _flasgs 的O_NONBLOCK 位,表示非阻塞;也可以清除这个位表示阻塞。
c
/* 实现对应的open/read/write等函数,填入file_operations结构体*/
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;
int key;
if(is_key_buf_empty() && (file->f_flags & O_NONBLOCK))
return -EAGAIN;
else{
wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
key = get_key();
err = copy_to_user(buf, &key, 4);
return 4;
}
}
1-5 定时器
(1) 内核函数
所谓定时器,就是闹钟,时间到后你就要做某些事。有2个要素:时间、做事;换成程序员的话就是:超时时间、函数。
内核源码:include\linux\timer.h
1、设置定时器,主要是初始化timer_list结构体,设置其中的函数、参数。
c
setup_timer(timer, fn, data);
2、向内核添加定时器。 timer-->expires 表示超时时间。
当超时时间到达,内核就会调用这个函数:timer->function(timer -->data) 。
c
void add_timer(struct timer_list *timer)
3、修改定时器的超时时间
c
int mod_timer(struct timer_list *timer, unsigned long expires):
4、删除定时器
c
int del_timer(struct timer_list *timer)
(2) 定时器时间单位
这表示内核每秒中会发生100次系统滴答中断 (tick),这是Linux系统的心跳。每发生一次tick 中断,全局变量jiffies累加1。即:每个滴答是10ms。
c
CONFIG_HZ=100
按键触发中断,进入中断处理函数,若不断发生机械振动,会不断进入中断处理函数更新定时器超时时间,时间到后进入定时器处理函数,打印GPIO端口信息
probe函数设置定时器
c
static int gpio_key_probe(struct platform_device *pdev)
{
/* 设置定时器*/
setup_timer(&gpio_keys_100ask[i].key_timer, key_timer_expire, &gpio_keys_100ask[i]);
/*设置超时时间*/
gpio_keys_100ask[i].key_timer.expires = ~0;
add_timer(&gpio_keys_100ask[i].key_timer);
}
中断处理函数修改定时器超时时间
c
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
struct gpio_key *gpio_key = dev_id;
printk("gpio_key_isr %d irq happened\n", gpio_key->gpio);
mod_timer(&gpio_key->key_timer, jiffies + HZ/50); //20ms HZ = 1s
return IRQ_HANDLED;
}
定时器处理函数中打印GPIO信息和唤醒线程
c
struct timer_list key_timer;
/*定时器处理函数*/
static void key_timer_expire(unsigned long data)
{
/*data ==> gpio*/
struct gpio_key *gpio_key = data;
int val;
int key;
val = gpiod_get_value(gpio_key->gpiod);
printk("key_timer_expire %d %d\n", gpio_key->gpio, val);
key = (gpio_key->gpio << 8) | val;
put_key(key);
wake_up_interruptible(&gpio_key_wait); /*唤醒线程*/
kill_fasync(&button_fasync, SIGIO, POLL_IN); /*发信号*/
}
1-6 中断下半部 tasklet
在上半部处理紧急的事情时,在处理过程中,中断是被禁止 的;在下半部处理耗时的事情时,在处理过程中,中断是使能 的。
内核源码:include\linux\interrupt.h
c
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
state用于表示 tasklet 的状态,一共有2位。
bit0 表示 TASKLET_STATE_SCHED
等于1时,表示已经执行了 tasklet_schedule把tasklet放入队列;
bit1 表示 TASKLET_STATE_RUN
等于1时,表示正在运行 tasklet 中的func函数;函数执行完后内核会把该位清0。