嵌入式驱动开发

第一章:字符设备驱动开发

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);
  1. class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。
  2. 设备类名对应 /sys/class 目录的子目录名。
  3. 返回值是个指向结构体 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设备节点的使用

在自己的设备节点中使用属性“[<name>-]gpios”。

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有机会执行,不会卡顿】
  1. 很耗时的中断处理,应该放到线程里去
  2. 可以使用 work、 work queue
  3. 在中断上半部调用 schedule_work 函数,触发 work 的处理
  4.  既然是在线程中运行,那对应的函数可以休眠
  • 新技术 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 中剩下的代码,把数据复制回用户空间,返回用户空间。 

切记:中断处理函数中,不能休眠

处理过程:

  1. 初始化wq队列
    static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);        // 初始化wq队列
  2. 在驱动的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;
    }
  3. 在中断服务程序里:设置 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;
    }
    

  • 14
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
朱有鹏是一位熟悉嵌入式驱动开发的专家。他精通U-Boot、Linux kernel移植及驱动程序开发,并且具备高级语言(如C、C++、Java、C#)和汇编语言(如80C51、PIC、ARM等平台)的技能。他熟悉三星平台(如S3C2440、S3C6410、S5PV210)和全志平台(如A10、A20、A31)下的开发流程,包括Linux和WinCE平台的应用开发。此外,他还具备Windows下C# Winform界面开发和WinCE嵌入式操作系统驱动及应用程序开发的经验。他还熟悉编译技术,是一位全面的嵌入式开发专家。 关于朱有鹏嵌入式驱动开发的具体课程资源,根据引用的内容,视频课程需要转存到自己的网盘后再进行下载。在引用中提到,朱有鹏的嵌入式核心课程由6个部分组成,每个部分又分为若干模块,以构建整个知识体系网络。所以,如果您对朱有鹏的嵌入式驱动开发感兴趣,建议您转存相关课程资源并按照部分和模块的顺序学习,以便全面掌握相关知识。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [朱有鹏嵌入式单片机免费课程](https://blog.csdn.net/weixin_33572836/article/details/116813988)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值