韦东山嵌入式Liunx入门驱动开发五

这篇具有很好参考价值的文章主要介绍了韦东山嵌入式Liunx入门驱动开发五。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


本人学习完韦老师的视频,因此来复习巩固,写以笔记记之。
韦老师的课比较难,第一遍不知道在说什么,但是坚持看完一遍,再来复习,基本上就水到渠成了。
看完视频复习的同学观看最佳!
基于 IMX6ULL-PRO
参考视频 Linux快速入门到精通视频
参考资料:01_嵌入式Linux应用开发完全手册V5.1_IMX6ULL_Pro开发板.pdf

一、驱动程序基石

1-1 休眠与唤醒

当应用程序必须等待某个事件发生,比如必须等待按键被按下时, 可以使用休眠-唤醒机制
① APP调用read等函数试图读取数据,比如读取按键;
② APP进入内核态,也就是调用驱动中的对应函数,发现有数据则复制到用户空间并马上返回;
③ 如果APP在内核态,也就是在驱动程序中发现没有数据,则APP休眠;
④ 当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,它会记录数据、唤醒APP
⑤ APP继续运行它的内核态代码,也就是驱动程序中的函数,复制数据到用户空间并马上返回。
韦东山嵌入式Liunx入门驱动开发五,Linux嵌入式,驱动开发,ubuntu,linux,嵌入式,c语言
驱动框架
韦东山嵌入式Liunx入门驱动开发五,Linux嵌入式,驱动开发,ubuntu,linux,嵌入式,c语言
休眠,直到condition 为真;休眠期间是可被打断的,可以被信号打断

wait_event_interruptible(wq, condition)

唤醒wq队列中状态为“ TASK_INTERRUPTIBLE ”的线程,只唤醒其中的一个线程

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得到数据。

韦东山嵌入式Liunx入门驱动开发五,Linux嵌入式,驱动开发,ubuntu,linux,嵌入式,c语言
drv_poll函数需要做的事
① 把当前线程挂入队列wq:poll_wait
②返回设备状态:drv_poll 要返回自己的当前状态:P OLLIN | POLLRDNORM) 或 POLLOUT | POLLWRNORM) 。

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

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) 异步通知程序解析

异步通知流程如下:
韦东山嵌入式Liunx入门驱动开发五,Linux嵌入式,驱动开发,ubuntu,linux,嵌入式,c语言

① APP给SIGIO这个信号注册信号处理函数func,以后APP收到SIGIO信号时,这个函数会被自动调用。
② 把APP的PID(进程 ID)告诉驱动程序,这个调用不涉及驱动程序,在内核的文件系统层次记录PID。

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

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,并发信号给上层应用

kill_fasync(&button_fasync, SIGIO, POLL_IN);

上层应用程序
韦东山嵌入式Liunx入门驱动开发五,Linux嵌入式,驱动开发,ubuntu,linux,嵌入式,c语言

(2) 异步通知机制内核代码详解

上层应用执行fcntl函数,内核会调用fs/fcntl.c 的如下函数。
韦东山嵌入式Liunx入门驱动开发五,Linux嵌入式,驱动开发,ubuntu,linux,嵌入式,c语言
进入do_fcntl函数,flag标志对应函数的cmd
韦东山嵌入式Liunx入门驱动开发五,Linux嵌入式,驱动开发,ubuntu,linux,嵌入式,c语言

以下分别对三种flag进行代码演示
① 当flag是F_SETOWN时,内核do_fcntl中调用f_setown函数,最终将pid给filp

fcntl(fd, F_SETOWN, getpid());

韦东山嵌入式Liunx入门驱动开发五,Linux嵌入式,驱动开发,ubuntu,linux,嵌入式,c语言
在这里插入图片描述
在这里插入图片描述
② 当flag是F_GETFL时,获取文件的状态标志

flags = fcntl(fd, F_GETFL);

在这里插入图片描述
③ 当flag是F_SETFL时,设置文件支持异步通知功能

fcntl(fd, F_SETFL, flags | FASYNC);

在这里插入图片描述
在这里插入图片描述
启动了FASYNC 功能的话,驱动程序的 button_fasync 就被设置了,它指向的 fasync_struct 结构体里含有 filp里含有PID

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结构体, 修改里面的某些成员表示收到了信号。

kill_fasync(&button_fasync, SIGIO, POLL_IN);

在这里插入图片描述

1-4 阻塞与非阻塞

所谓阻塞,就是等待某件事情发生。比如调用read读取按键时,如果没有按键数据则read函数不会返回,它会让线程休眠等待。
使用poll时,传入超时时间不为0(阻塞);设置超时时间为0,没有数据立即返回(非阻塞)
如何设置阻塞与非阻塞呢?
① open时

int fd = open(/dev/xxx”, O_RDWR | O_NONBLOCK); 	/* 非阻塞方式*/
int fd = open(/dev/xxx”, O_RDWR ); 	/* 阻塞方式*/

② open后

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 位,表示非阻塞;也可以清除这个位表示阻塞。

/* 实现对应的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结构体,设置其中的函数、参数。

setup_timer(timer, fn, data);

2、向内核添加定时器。 timer–>expires 表示超时时间。
当超时时间到达,内核就会调用这个函数:timer->function(timer -->data) 。

void add_timer(struct timer_list *timer)

在这里插入图片描述
3、修改定时器的超时时间

int mod_timer(struct timer_list *timer, unsigned long expires):

4、删除定时器

int del_timer(struct timer_list *timer)
(2) 定时器时间单位

这表示内核每秒中会发生100次系统滴答中断 (tick),这是Linux系统的心跳。每发生一次tick中断,全局变量jiffies累加1。即:每个滴答是10ms。

CONFIG_HZ=100

按键触发中断,进入中断处理函数,若不断发生机械振动,会不断进入中断处理函数更新定时器超时时间,时间到后进入定时器处理函数,打印GPIO端口信息
probe函数设置定时器

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);
}

中断处理函数修改定时器超时时间

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信息和唤醒线程

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

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。
count表示该tasklet是否使能,0表示使能,非0表示被禁止。对于c ount非0的tasklet,里面的func函数不会被执行。

1、tasklet_init函数初始化tasklet结构体

extern void tasklet_init(struct tasklet_struct *t,
						void (*func)(unsigned long), unsigned long data);

2、调度tasklet函数

static inline void tasklet_schedule(struct tasklet_struct *t);

tasklet放入链表,设置它的TASKLET_STATE_SCHED状态为1。

3、kill tasklet函数

extern void tasklet_kill(struct tasklet_struct *t);

如果一个tasklet 未被调度, tasklet_kill 会把它的TASKLET_STATE_SCHED 状态清0
如果一个 tasklet 已被调度, tasklet_kill 会等待它执行完华,再
把它的 TASKLET_STATE_SCHED 状态清0

首先在probe函数初始化tasklet结构体,中断函数中调度tasklet,最后在key_tasklet_func实现打印相应GPIO功能,remove函数中卸载

/*probe函数初始化tasklet*/
tasklet_init(&gpio_keys_100ask[i].tasklet, key_tasklet_func, &gpio_keys_100ask[i]);

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);
	//上半部的函数来调度下半部
	tasklet_schedule(&gpio_key->tasklet);
	return IRQ_HANDLED;
}
static void key_tasklet_func(unsigned long data)
{
	struct gpio_key *gpio_key = data;
	int val;
	int key;
	
	val = gpiod_get_value(gpio_key->gpiod);
	printk("key_tasklet_func key %d %d\n", gpio_key->gpio, val);
}
/*remove函数中卸载*/
tasklet_kill(&gpio_keys_100ask[i].tasklet);

tasklet 内部机制
内核源码:kernel\softirq.c
当发生硬件中断时,内核处理完硬件中断后,会处理软件中断。对于TASKLET_SOFTIRQ 软件中断,事先tasklet_schedule调度 tasklet时,其中的函数并不会立刻执行,而只是把
tasklet放入队列,再调用tasklet_action 函数。
在这里插入图片描述

当驱动程序调用tasklet_schedule时,会设置tasklet 的state为TASKLET_STATE_SCHED ,并把它放入某个链表。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

1-7 工作队列

前面的定时器、下半部tasklet ,它们都是在中断上下文中执行,它们无法休眠。当要做的事情比较耗时,甚至可能需要休眠,那么可以使用工作队列。内核初始化工作队列就为它创建了内核线程
内核源码:include\linux\workqueue.h
使用工作队列时,步骤如下:
1、构造一个work_struct结构体,里面有函数;
2、把work_struct 结构体放入工作队列,内核线程就会运行work中的函数。在这里插入图片描述
调用schedule_work时,就会把work_struct 结构体放入队列中,并唤醒对应的内核线程。内核线程就会从队列里把work_struct 结构体取出来,执行里面的函数。

static inline bool schedule_work(struct work_struct *work)
{
	return queue_work(system_wq, work);
}
/*probe函数初始化work*/
INIT_WORK(&gpio_keys_100ask[i].work, key_work_func);

/*中断处理函数*/
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
	struct gpio_key *gpio_key = dev_id;
	//按下按键中断服务程序调用后,执行下面函数,放入队列,唤醒内核线程
	//在合适的时候,当内核线程运行时,就会把work里的函数进行执行
	schedule_work(&gpio_key->work);
	return IRQ_HANDLED;
}

/*key_work_func函数*/
static void key_work_func(struct work_struct *work)
{
	/*得到结构体gpio_key的地址*/
	struct gpio_key *gpio_key = container_of(work, struct gpio_key, work);
	int val;
	
	val = gpiod_get_value(gpio_key->gpiod);
	printk("key_work_func: the process is %s  pid %d\n", current->comm, current->pid);
	printk("key_work_func key %d %d\n", gpio_key->gpio, val);
}
1-8 中断的线程化处理

提供thread_fn函数,系统会为这个函数创建一个内核线程。发生中断时,系统会立刻调用 handler函数,然后唤醒某个内核线程,内核线程再来执行thread_fn函数。
请添加图片描述

request_threaded_irq(unsigned int irq, irq_handler_t handler,
		     irq_handler_t thread_fn,
		     unsigned long flags, const char *name, void *dev);

请添加图片描述文章来源地址https://www.toymoban.com/news/detail-837296.html

/*在probe函数注册一个中断处理函数*/
err = request_threaded_irq(gpio_keys_100ask[i].irq, gpio_key_isr, gpio_key_thread_func, 
							IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpio_keys_100ask[i]);

/* gpio_key_isr中断处理handler函数 需要返回IRQ_WAKE_THREAD关键字*/
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);
	
	return IRQ_WAKE_THREAD;
}

/*gpio_key_thread_func内核线程thread_fn函数 打印进程PID GPIO值*/
static irqreturn_t gpio_key_thread_func(int irq, void *data)
{
	struct gpio_key *gpio_key = data;
	int val;
	int key;
	
	val = gpiod_get_value(gpio_key->gpiod);
	printk("gpio_key_thread_func: the process is %s  pid %d\n", current->comm, current->pid);
	printk("gpio_key_thread_func key %d %d\n", gpio_key->gpio, val);

	return IRQ_HANDLED;
}
1-9 mmap
  • 应用程序不能直接读写驱动程序中的buffer ,需要在用户态buffer和内核态 buffer之间进行一次数据拷贝。
  • 改进的方法就是让程序可以直接读写驱动程序中的buffer,这可以通过mmap实现,把内核的buffer映射到用户态,让APP在用户态直接读写。
  • CPU 发出的地址是虚拟地址,它经过MMU内存管理单元映射到物理地址上,对于不同进程的同一个虚拟地址,MMU会把它们映射到不同的物理地址。

到了这里,关于韦东山嵌入式Liunx入门驱动开发五的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 嵌入式内核及驱动开发高级

    仅devfs,导致开发不方便以及一些功能难以支持: 热插拔 不支持一些针对所有设备的统一操作(如电源管理) 不能自动mknod 用户查看不了设备信息 设备信息硬编码,导致驱动代码通用性差,即没有分离设备和驱动 uevent机制:sysfs + uevent + udevd(上层app) sysfs用途:(类似于

    2024年02月16日
    浏览(65)
  • 全志V3S嵌入式驱动开发(驱动开发准备)

    【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】         之前的文章都是教大家怎么搭建环境、看原理图、编译内核和根文件系统、做镜像,直到现在才进入驱动开发的主题。 毕竟整个专栏的目的,还是希望大家能够学会驱动外部硬件。

    2024年02月13日
    浏览(65)
  • 嵌入式驱动开发需要会哪些技能?

    嵌入式驱动开发是指在嵌入式系统中编写驱动程序,实现设备与计算机之间的通信。嵌入式驱动开发是指编写设备驱动程序,实现设备与计算机之间的通信。 以下是一些嵌入式驱动开发的具体操作方法:  1)了解硬件设备结构: 在进行嵌入式驱动开发之前,需要对所使用的硬

    2024年01月25日
    浏览(58)
  • 嵌入式Linux驱动开发之点灯

      使用驱动开发的方式点亮一个LED灯。看看两者有啥区别不? 首先查看原理图,看看我们的板子上的LED等接在哪一个IO口上面。 好了,看原理图我们知道LED灯接在芯片的GPIO1的第三个引脚上面,也就是GPIO1_IO03。 先掌握三个名词 CCM: Clock Controller Module (时钟控制模块) IOMUXC : I

    2024年02月01日
    浏览(101)
  • 嵌入式Linux开发-USB驱动

    哥们马上就要被裁了,总得整理一下技术方面的积累,准备开始下一轮的面试和找工作之旅了。。。。 通用串行总线(USB)是主机和外围设备之间的一种连接。 从拓扑上来看,是一颗由几个点对点的连接构建而成的树。这些连接是连接设备和集线器(hub)的四线电缆(底线、电源线

    2024年02月20日
    浏览(73)
  • 嵌入式:驱动开发 Day4

    驱动程序:myled.c 应用程序:test.c 头文件:head.h

    2024年02月09日
    浏览(51)
  • 嵌入式Linux驱动开发 04:基于设备树的驱动开发

    前面文章 《嵌入式Linux驱动开发 03:平台(platform)总线驱动模型》 引入了资源和驱动分离的概念,这篇文章将在前面基础上更进一步,引入设备树的概念。 在平台总线驱动模型中资源和驱动已经从逻辑上和代码组织上进行了分离,但每次调整资源还是会涉及到内核,所以现

    2024年02月16日
    浏览(70)
  • 【嵌入式Linux驱动】驱动开发调试相关的关系记录

    https://www.processon.com/mindmap/64537772b546c76a2f37bd2f

    2024年02月02日
    浏览(56)
  • 嵌入式Linux驱动开发——常见框架梳理

    本文主要介绍了Linux驱动开发中一些常用的驱动框架,platform、input、iic、spi等,硬件平台使用的是正点原子的imx6ull开发板。 不管什么框架最后都是要追溯到配置IO的电气属性和复用功能 如果要使用外部中断,设备树节点中还需添加相关信息,什么边沿触发 1:module_init和mod

    2024年02月15日
    浏览(67)
  • 华清远见嵌入式学习——驱动开发——作业1

    通过字符设备驱动分步注册过程实现LED驱动的编写,编写应用程序测试,发布到CSDN

    2024年02月20日
    浏览(57)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包