linux驱动开发2

MIO:多功能IO接口,属于Zynq的PS部分,在芯片外部有54个引脚。这些引脚可以用在GPIO、SPI、UART、TIMER、Ethernet、USB等功能上,每个引脚都同时具有多种功能,故叫多功能。
EMIO:扩展MIO,依然属于Zynq的PS部分,只是连接到了PL上,再从PL的引脚连到芯片外面实现数据输入输出
gpio驱动代码分析

#define GPIOLED_CNT		1				/* 设备号个数 */
#define GPIOLED_NAME	"gpioled"		/* 名字 */
/* dtsled设备结构体 */
struct gpioled_dev {
	dev_t devid;			/* 设备号 */
	struct cdev cdev;		/* cdev cdev 结构体表示一个字符设备 */
	struct class *class;	/* 类 */
	struct device *device;	/* 设备 */
	int major;				/* 主设备号 */
	int minor;				/* 次设备号 */
	struct device_node *nd;	/* 设备节点 */
	int led_gpio;			/* LED所使用的GPIO编号 */
};//设置文件私有数据,包括硬件的一些属性
static struct gpioled_dev gpioled;	/* 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 = &gpioled;	/* 设置私有数据 */
	return 0;
}
/*
 * @description		: 从设备读取数据 
 * @param - filp	: 要打开的设备文件(文件描述符)
 * @param - buf		: 返回给用户空间的数据缓冲区
 * @param - cnt		: 要读取的数据长度
 * @param - offt	: 相对于文件首地址的偏移
 * @return			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf,
			size_t cnt, loff_t *offt)
{
	return 0;
}
/** @description		: 向设备写数据 
 * @param - filp	: 设备文件,表示打开的文件描述符
 * @param - buf		: 要写给设备写入的数据
 * @param - cnt		: 要写入的数据长度
 * @param - offt	: 相对于文件首地址的偏移
 * @return			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf,
			size_t cnt, loff_t *offt)
{
	int ret;
	char kern_buf[1];
	ret = copy_from_user(kern_buf, buf, cnt);	// 得到应用层传递过来的数据
	if(0 > ret) {
		printk(KERN_ERR "kernel write failed!\r\n");
		return -EFAULT;
	}
	if (0 == kern_buf[0])
		gpio_set_value(gpioled.led_gpio, 0);	// 如果传递过来的数据是0则关闭led
	else if (1 == kern_buf[0])
		gpio_set_value(gpioled.led_gpio, 1);	// 如果传递过来的数据是1则点亮led
	return 0;
}
/*
 * @description		: 关闭/释放设备
 * @param – filp	: 要关闭的设备文件(文件描述符)
 * @return			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{	return 0;
}/* 设备操作函数 */
static struct file_operations gpioled_fops = {
	.owner		= THIS_MODULE,
	.open		= led_open,
	.read		= led_read,
	.write		= led_write,
	.release	= led_release,
};
static int __init led_init(void)
{
	const char *str;
	int ret;
	/* 1.获取led设备节点 */
	gpioled.nd = of_find_node_by_path("/led");
	if(NULL == gpioled.nd) {
		printk(KERN_ERR "gpioled: Failed to get /led node\n");
		return -EINVAL;
	}
	/* 2.读取status属性 */
	ret = of_property_read_string(gpioled.nd, "status", &str);
	if(!ret) {
		if (strcmp(str, "okay"))
			return -EINVAL;
	}
	/* 2、获取compatible属性值并进行匹配 */
	ret = of_property_read_string(gpioled.nd, "compatible", &str);
	if(0 > ret) {
		printk(KERN_ERR "gpioled: Failed to get compatible property\n");
		return ret;
	}
	if (strcmp(str, "alientek,led")) {
		printk(KERN_ERR "gpioled: Compatible match failed\n");
		return -EINVAL;
	}
	printk(KERN_INFO "gpioled: device matching successful!\r\n");
	/* 4.获取设备树中的led-gpio属性,得到LED所使用的GPIO编号 */
	gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);
	if(!gpio_is_valid(gpioled.led_gpio)) {
		printk(KERN_ERR "gpioled: Failed to get led-gpio\n");
		return -EINVAL;
	}
	printk(KERN_INFO "gpioled: led-gpio num = %d\r\n", gpioled.led_gpio);
	/* 5.向gpio子系统申请使用GPIO */
	ret = gpio_request(gpioled.led_gpio, "LED-GPIO");
	if (ret) {
		printk(KERN_ERR "gpioled: Failed to request led-gpio\n");
		return ret;
	}
	/* 6.将led gpio管脚设置为输出模式 */
	gpio_direction_output(gpioled.led_gpio, 0);
	/* 7.初始化LED的默认状态 */
ret = of_property_read_string(gpioled.nd, "default-state", &str);
	if(!ret) {
		if (!strcmp(str, "on"))
			gpio_set_value(gpioled.led_gpio, 1);
		else
			gpio_set_value(gpioled.led_gpio, 0);
	} else
	gpio_set_value(gpioled.led_gpio, 0);
	/* 8.注册字符设备驱动 */
	 /* 创建设备号 */
	if (gpioled.major) {
		gpioled.devid = MKDEV(gpioled.major, 0);
		ret = register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);
		if (ret)
		goto out1;
	} else {
		ret = alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME);
		if (ret)
			goto out1;
		gpioled.major = MAJOR(gpioled.devid);
		gpioled.minor = MINOR(gpioled.devid);
	}
	printk("gpioled: major=%d,minor=%d\r\n",gpioled.major, gpioled.minor); 
 /* 初始化cdev */
	gpioled.cdev.owner = THIS_MODULE;
	cdev_init(&gpioled.cdev, &gpioled_fops);
	 /* 添加一个cdev */
	ret = cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);
	if (ret)
	goto out2;
 /* 创建类 */
	gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
	if (IS_ERR(gpioled.class)) {
		ret = PTR_ERR(gpioled.class);
		goto out3;
	}
 /* 创建设备 */
	gpioled.device = device_create(gpioled.class, NULL,
				gpioled.devid, NULL, GPIOLED_NAME);
if (IS_ERR(gpioled.device)) {
	ret = PTR_ERR(gpioled.device);
	goto out4;	}
return 0;
out4:
	class_destroy(gpioled.class);
out3:
cdev_del(&gpioled.cdev);
out2:
unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);
out1:
gpio_free(gpioled.led_gpio);
return ret;
}
static void __exit led_exit(void)
{
	/* 注销设备 */
	device_destroy(gpioled.class, gpioled.devid);
	/* 注销类 */
class_destroy(gpioled.class);
	/* 删除cdev */
	cdev_del(&gpioled.cdev);
	/* 注销设备号 */
	unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);
	/* 释放GPIO */
	gpio_free(gpioled.led_gpio);
}
/* 驱动模块入口和出口函数注册 */
module_init(led_init);
module_exit(led_exit);
MODULE_AUTHOR("DengTao <773904075@qq.com>");
MODULE_DESCRIPTION("Alientek ZYNQ GPIO LED Driver");
MODULE_LICENSE("GPL");

第七期Linux 并发与竞争
Linux 系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可相互覆盖这段内存中的数据,造成内存数据混乱。
一般在驱动函数内部都是在读写过程中使用,或者直接在open,read,write函数使用上锁解锁啥的,因为防止其他线程干扰。
处理竞争操作方法:
1.原子操作
首先看一下原子操作,原子操作就是指不能在进一步分割的操作,一般原子操作用于变量或者位操作。因为双进程操作的过程中,线程不同的地方可能会产生并发竞争如下图:在这里插入图片描述
要想保住避免并发竞争就需要将线程a和线程b作为一个整体进行运行,,也就是作为一个原子存在。Linux内核提供了一组原子操作 API 函数来完成此功能,Linux 内核提供了两组原子操作 API 函数,一组是对整形变量进行操作的,一组是对位进行操作的。
1.1原子整形操作 API 函数
Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作

atomic_t b = ATOMIC_INIT(0); //定义原子变量 b 并赋初值为 0

在这里插入图片描述
1.2原子位操作 API 函数
在这里插入图片描述
2.自旋锁
原子操作只能对整形变量或者位进行保护,但是,在实际的使用环境中怎么可能只有整形变量或位这么简单的临界区。举个最简单的例子,设备结构体变量就不是整型变量,我们对于结构体中成员变量的操作也要保证原子性,在线程 A 对结构体变量使用期间,应该禁止其他的线程来访问此结构体变量,这些工作原子操作都不能胜任,需要本节要讲的锁机制,在 Linux内核中就是自旋锁。
当一个线程要访问某个共享资源的时候首先要先获取相应的锁,锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。自旋锁的“自旋”也就是“原地打转”的意思,“原地打转”的目的是为了等待自旋锁可以用,可以访问共享资源。
Linux 内核使用结构体 spinlock_t 表示自旋锁,在使用自旋锁之前,肯定要先定义一个自旋锁变量,定义方法如下所示:

spinlock_t lock; //定义自旋锁

定义好自旋锁变量以后就可以使用相应的 API 函数来操作自旋锁。
在这里插入图片描述在这里插入图片描述
同时中断可能会跟线程抢cpu的所有权,导致死锁的产生,最好的解决方法就是获取锁之前关闭本地中断,Linux 内核提供了相应的 API 函数。
在这里插入图片描述
读写锁
除了自旋锁还有读写锁,读写自旋锁为读和写操作提供了不同的锁,一次只能允许一个写操作,也就是只能一个线程持有写锁,而且不能进行读操作。在这里插入图片描述
在这里插入图片描述
顺序锁
顺序锁在读写锁的基础上衍生而来的,使用读写锁的时候读操作和写操作不能同时进行。使用顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行并发的写操作。
在这里插入图片描述
信号量
相比于自旋锁,信号量可以使线程进入休眠状态,比如 A 与 B、C 合租了一套房子,这个房子只有一个厕所,一次只能一个人使用。某一天早上 A 去上厕所了,过了一会 B 也想用厕所,因为 A 在厕所里面,所以 B 只能等到 A 用来了才能进去。B 要么就一直在厕所门口等着,等 A 出来,这个时候就相当于自旋锁。B 也可以告诉 A,让 A 出来以后通知他一下,然后 B 继续回房间睡觉,这个时候相当于信号量。
信号量有一个信号量值,相当于一个房子有 10 把钥匙,这 10 把钥匙就相当于信号量值为10。因此,可以通过信号量来控制访问共享资源的访问数量,如果要想进房间,那就要先获取一把钥匙,信号量值减 1,直到 10 把钥匙都被拿走,信号量值为 0,这个时候就不允许任何人进入房间了,因为没钥匙了。如果有人从房间出来,那他要归还他所持有的那把钥匙,信号量值加 1,此时有 1 把钥匙了,那么可以允许进去一个人。
①、因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场
合。
②、因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
③、如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。
Linux 内核使用 semaphore 结构体表示信号量。在这里插入图片描述

struct semaphore sem; /* 定义信号量 */
sema_init(&sem, 1); /* 初始化信号量 */
down(&sem); /* 申请信号量 */
/* 临界区 */
up(&sem); /* 释放信号量 */

互斥体
将信号量的值设置为 1 就可以使用信号量进行互斥访问了,虽然可以通过信号量实现互斥,但是 Linux 提供了一个比信号量更专业的机制来进行互斥,它就是互斥体—mutex。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体。在这里插入图片描述

1 struct mutex lock; /* 定义一个互斥体 */
2 mutex_init(&lock); /* 初始化互斥体 */
3
4 mutex_lock(&lock); /* 上锁 */
5 /* 临界区 */
6 mutex_unlock(&lock); /* 解锁 */

原子操作实验:
不管什么类型的锁(原子操作中的也是,lock代表一个钥匙,当lock=1的时候代表设备有一个线程可以操作,当lock=0的时候表示没有线程可以操作)代码如下:

struct gpioled_dev {
....
atomic_t lock; /* 原子变量 */
42 };
if (!atomic_dec_and_test(&gpioled.lock)) {
57 printk(KERN_ERR "gpioled: Device is busy!\n");
58 atomic_inc(&gpioled.lock); /* 小于 0 的话就加 1,使其原子变量等于 0 */
59 return -EBUSY; /* LED 被其他应用使用,返回忙 */
60 }
112 static int led_release(struct inode *inode, struct file *filp)
113 {
114 /* 关闭驱动文件的时候释放原子变量 */
115 atomic_inc(&gpioled.lock);
116
117 return 0;
118 }
atomistatic int __init led_init(void)
130 {
...
c_set(&gpioled.lock, 1); /* 原子变量初始值为 1 *
}

当有led被占用时,lock=0表示钥匙没有了,atomic_dec_and_test函数是自减1为0,返回为真,即当被占用时返回为假,再取反为真,进入if下面语句,当然lock自减1到负一,需要加回来。
具体过程,先在设备操作结构体中定义原子变量lock,再在open函数中每次申请lock,没有被占用就使用。最后关闭驱动的时候释放lock。同时在驱动模块入口设置原子变量为1。
自旋锁实验:

struct gpioled_dev {
int dev_stats; /* 设备状态: 0,设备未使用; >0,设备已经被使用 */
42 spinlock_t lock; /* 自旋锁 */
43 };
 static int led_open(struct inode *inode, struct file *filp)
55 {
56 unsigned long flags;
57 int ret = 0;
58
59 /* 自旋锁上锁 */
60 spin_lock_irqsave(&gpioled.lock, flags);
61
62 /* 如果设备被使用了 */
63 if (gpioled.dev_stats) {
64 printk(KERN_ERR "gpioled: Device is busy!\n");
65 ret = -EBUSY;
66 goto out;
67 }
68
69 /* 如果设备没有打开,那么就标记已经打开了 */
70 gpioled.dev_stats++;
71
72 out:
73 /* 解锁 */
74 spin_unlock_irqrestore(&gpioled.lock, flags);
75 return ret;
76 }
5 static int led_release(struct inode *inode, struct file *filp)
126 {
127 unsigned long flags;
128
129 /* 上锁 */
130 spin_lock_irqsave(&gpioled.lock, flags);
131
132 /* 关闭驱动文件的时候将 dev_stats 减 1 */
133 if (gpioled.dev_stats)
134 gpioled.dev_stats--;
135
136 /* 解锁 */
137 spin_unlock_irqrestore(&gpioled.lock, flags);
138
139 return 0;
140 }
151 static int __init led_init(void)
152 {
 ......
255 /* 9.初始化自旋锁 */
256 spin_lock_init(&gpioled.lock);
 ......
273 }

与前面的原子操作lock不同,前面lock是被使用的时候lock=0,表示没有钥匙,这里用dev_stats表示使用设备数量,为0表示没有设备使用。
自旋锁具体过程:先定义自旋锁结构体,然后在open函数中自旋上锁,然后判断dev_stats状态,如果设备已经被打开了,就进行解锁操作,并且直接返回busy,退出操作,如果没有被使用,则dev_stats++。同时在release函数中,先上锁再自减dev_stats,再解锁。
同时在驱动模块入口初始化自旋锁。自旋锁的工作就是保护 dev_stats 变量,真正实现对设备互斥访问的是 dev_stats。
同时下面的信号量和互斥体和自旋锁都是一样的只有使用完当前文档之后才能解锁,否则不能解锁。
信号量实验:

#include <linux/semaphore.h>
 struct gpioled_dev {
 struct semaphore sem; /* 信号量 */
43 };
  static int led_open(struct inode *inode, struct file *filp)
55 {
56 /* 获取信号量,如果获取不到则会进入休眠状态 */
57 if (down_interruptible(&gpioled.sem))
58 return -ERESTARTSYS;
59
60 #if 0
61 down(&gpioled.sem);
62 #endif
63
64 return 0;
65 }
static int led_release(struct inode *inode, struct file *filp)
115 {
116 /* 释放信号量,信号量值加 1 */
117 up(&gpioled.sem);
118
119 return 0;
131 static int __init led_init(void)
132 {
 ......
235 /* 初始化信号量 */
236 sema_init(&gpioled.sem, 1);
 ......
253 }

这里的信号量跟原子操作中的lock是一样的,表示可以使用的钥匙个数(区别就是原子的lock不会休眠同时不会被唤醒,而信号量可以在大于1的时候被唤醒),具体过程就是,先在open函数中down_interruptible 函数。如果信号量值大于或等于 1 就表示可用,那么应用程序就会开始使用 LED 设备。如果信号量值为 0 就表示应用程序不能使用 LED 设备,此时应用程序就会进入到休眠状态。等到信号量值大于或等于 1 的时候应用程序就会唤醒,申请到信号量,获取 LED设备使用权。
之后在release函数中释放信号量,即up(使sema信号量加1)。最后在驱动入口使信号量值为1.
互斥体使实验:

struct gpioled_dev {
struct mutex lock; /* 互斥体 */
43 };
 static int led_open(struct inode *inode, struct file *filp)
55 {
56 /* 获取互斥体,可以被信号打断 */
57 if (mutex_lock_interruptible(&gpioled.lock))
58 return -ERESTARTSYS;
59
60 #if 0
61 mutex_lock(&gpioled.lock); /* 不能被信号打断 */
62 #endif
63
64 return 0;
65 }
......
114 static int led_release(struct inode *inode, struct file *filp)
115 {
116 /* 释放互斥锁 */
117 mutex_unlock(&gpioled.lock);
118
119 return 0;
120 }
......
131 static int __init led_init(void)
132 {
133 const char *str;
134 int ret
mutex_init(&gpioled.lock);
 ......
252 return ret;
253 }

互斥体具体操作跟信号量是一样的。
两者区别:1. 互斥量用于线程的互斥,信号量用于线程的同步。
这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源
note:信号量可以用来实现互斥量的功能
2. 互斥量值只能为0/1,信号量值可以为非负整数。
也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。
3. 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
linux内核定时器实验:
系统时钟源:Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会将 jiffies 初始化为 0,jiffies/HZ 就是系统运行时间,单位为秒。
绕回:当jiffies(一般是32位,50天一个轮回,即溢出,64位不考虑)绕回即复位。
在这里插入图片描述
如果 unkown 超过 known 的话,time_after 函数返回真,否则返回假。如果 unkown 没有超过 known 的话 time_before 函数返回真,否则返回假。time_after_eq 函数和 time_after 函数类似,只是多了判断等于这个条件。同理,time_before_eq 函数和 time_before 函数也类似。
判断是否超时2s:

unsigned long timeout;
2 timeout = jiffies + (2 * HZ); /* 超时的时间点 */
if(time_before(jiffies, timeout)) {
/* 超时未发生 */
11 } else {
12 /* 超时发生 */
13 }

在这里插入图片描述
定时器:Linux 内核使用 timer_list 结构体表示内核定时器,

struct timer_list {
 struct list_head entry;
 unsigned long expires; /* 定时器超时时间,单位是节拍数 */
 struct tvec_base *base;
 void (*function)(unsigned long); /* 定时处理函数 */
unsigned long data; /* 要传递给 function 函数的参数 */
 int slack;
};

要使用内核定时器首先要先定义一个 timer_list 变量,表示定时器,tiemr_list 结构体的 expires 成员变量表示超时时间,单位为节拍数。
比如我们现在需要定义一个周期为 2 秒的定时器,那么这个定时器的超时时间就是 jiffies+(2HZ),因此expires=jiffies+(2HZ)。
init_timer 函数负责初始化 timer_list 类型变量,当我们定义了一个 timer_list 变量以后一定要先用 init_timer 初始化一下。init_timer 函数原型如下:
void init_timer(struct timer_list *timer)
add_timer 函数用于向 Linux 内核注册定时器,使用 add_timer 函数向内核注册定时器以后,定时器就会开始运行,函数原型如下:
void add_timer(struct timer_list *timer)
同时还有删除定时器int del_timer(struct timer_list * timer)
mod_timer 函数用于修改定时值,如果定时器还没有激活的话,mod_timer 函数会激活定时器!函数原型如下:
int mod_timer(struct timer_list *timer, unsigned long expires)
使用定时器具体代码:

struct timer_list timer; /* 定义定时器 */
3 /* 定时器回调函数 */
4 void function(unsigned long arg)
5 { 
6 /* 
7 * 定时器处理代码
8 */
10 /* 如果需要定时器周期性运行的话就使用 mod_timer
11 * 函数重新设置超时值并且启动定时器。
12 */
13 mod_timer(&dev->timertest, jiffies + msecs_to_jiffies(2000));
14 }
16 /* 初始化函数 */
17 void init(void) 
18 {
19 init_timer(&timer); /* 初始化定时器 */
21 timer.function = function; /* 设置定时处理函数 */
22 timer.expires=jffies + msecs_to_jiffies(2000);/* 超时时间 2 秒 */
23 timer.data = (unsigned long)&dev; /* 将设备结构体作为参数 */
25 add_timer(&timer); /* 启动定时器 */
26 }
28 /* 退出函数 */
29 void exit(void)
30 {
del_timer(&timer); /* 删除定时器 */
32 /* 或者使用 */
33 del_timer_sync(&timer);
34 }

在这里插入图片描述
led闪烁实验代码分析:

/* ioctl 函数命令定义 */
27 #define CMD_LED_CLOSE (_IO(0XEF, 0x1)) /* 关闭 LED */
28 #define CMD_LED_OPEN (_IO(0XEF, 0x2)) /* 打开 LED */
29 #define CMD_SET_PERIOD (_IO(0XEF, 0x3)) /* 设置 LED 闪烁频率 */
struct led_dev {
int led_gpio; /* GPIO 编号 */
42 int period; /* 定时周期,单位为 ms */
43 struct timer_list timer; /* 定义一个定时器 */
44 spinlock_t spinlock; /* 定义自旋锁 */
45 };
static long led_unlocked_ioctl(struct file *filp, unsigned int cmd,
107 unsigned long arg)
108 {
109 unsigned long flags;
110
111 /* 自旋锁上锁 */
112 spin_lock_irqsave(&led.spinlock, flags);
113
114 switch (cmd) {
115
116 case CMD_LED_CLOSE:
117 del_timer_sync(&led.timer);
118 gpio_set_value(led.led_gpio, 0);
119 break;
120
121 case CMD_LED_OPEN:
122 del_timer_sync(&led.timer);
123 gpio_set_value(led.led_gpio, 1);
124 break;
125
126 case CMD_SET_PERIOD:
127 led.period = arg;
128 mod_timer(&led.timer, jiffies + msecs_to_jiffies(arg));
129 break;
default: break;
132 }
133
134 /* 自旋锁解锁 */
135 spin_unlock_irqrestore(&led.spinlock, flags);
136
137 return 0;
138 }
/* 定时器回调函数 */
151 static void led_timer_function(unsigned long arg)
152 {
153 static bool on = 1;
154 unsigned long flags;
155
156 /* 每次都取反,实现 LED 灯反转 */
157 on = !on;
158
159 /* 自旋锁上锁 */
160 spin_lock_irqsave(&led.spinlock, flags);
161
162 /* 设置 GPIO 电平状态 */
163 gpio_set_value(led.led_gpio, on);
164
165 /* 重启定时器 */
166 mod_timer(&led.timer, jiffies + msecs_to_jiffies(led.period));
167
168 /* 自旋锁解锁 */
169 spin_unlock_irqrestore(&led.spinlock, flags);
170 }
static int __init led_init(void)
173 {
/* 8.初始化 timer,绑定定时器处理函数,此时还未设置周期,所以不会激活定时器 */
280 init_timer(&led.timer);
281 led.timer.function = led_timer_function;
 }

ioctr函数先用switch判断app返回值,如果是打开led或者关闭led则先删除定时器,然后置位gpio控制led,app返回的是闪烁命令,则启动定时器(mod和add函数都可以启动:初始化的时候定义好时间,处理函数和定时器也然后调用add就可以启动)
回调函数就是启动定时器后定时时间到了跳到这个函数处,就是简单的翻转led重启定时器。然后驱动模块入口初始化定时器指定回调函数即可。ps在设备结构体中定义的结构体是全局变量在后面的函数中直接使用不需要再定义。
驱动出口函数:调用 del_timer_sync 函数删除定时器,也可以
使用 del_timer 函数。
linux中断实验:
每个中断都有一个中断号,通过中断号即可区分不同的中断,在 Linux 内核中要想使用某个中断是需要申请的,request_irq 函数用于申请中断,request_irq 函数会激活(使能)中断,所以不需要我们手动去使能中断。

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

irq:要申请中断的中断号。handler:中断处理函数,当中断发生以后就会执行此中断处理函数。flags:中断标志
中断标志:即指定触发中断类型
在这里插入图片描述在这里插入图片描述
使用中断的时候需要通过 request_irq 函数申请,使用完成以后就要通过 free_irq 函数释放掉相应的中断。
使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:
irqreturn_t (*irq_handler_t) (int, void *)
第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void 的指针,也就是个通用指针,需要与 request_irq 函数的 dev 参数保持一致
enable_irq 和 disable_irq 用于使能和禁止指定的中断.
local_irq_enable(),local_irq_disable()使能或者关闭全局中断
中断有关的设备树属性信息:
① 、#interrupt-cells,指定中断源的信息 cells(参数)个数。
② 、interrupt-controller,表示当前节点为中断控制器。
③ 、interrupts,指定中断号,触发方式等。
④ 、interrupt-parent,指定父中断,也就是中断控制器。

test-node {
2 compatible = "test-node";
3 reg = <0x1e>;
4 interrupt-parent = <&gpio0>;
5 interrupts = <12 2>;
6 };4 行,interrupt-parent 属性设置中断控制器,这里使用 gpio0 作为中断控制器。第 6 行,interrupts 设置中断信息,12 表示一个具体的 GPIO 引脚编号,也就是对应GPIO0_12;而 2 表示该 GPIO 的中断触发类型为下降沿触发。

上下半部
中断的上半部和下半部:只要中断触发,那么中断处理函数就会执行,但是有些中断函数处理速度较慢影响soc。把中断处理分为两部分,上半部处理速度快的一般是仅响应中断,清除中断标志位,下半部处理具体内容。
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。
软中断:
Linux 内核使用结构体 softirq_action 表示软中断,一般软中断用于处理下半部,一共有 10 个软中断。
要使用软中断,必须先使用 open_softirq函数注册对应的软中断处理函数,void open_softirq(int nr, void (*action)(struct softirq_action *))
nr:要开启的软中断,action:软中断对应的处理函数
nr从下列10中断中选择:

 enum
459 {
460 HI_SOFTIRQ=0,
461 TIMER_SOFTIRQ,
462 NET_TX_SOFTIRQ,
463 NET_RX_SOFTIRQ,
464 BLOCK_SOFTIRQ,
465 IRQ_POLL_SOFTIRQ,
466 TASKLET_SOFTIRQ,
467 SCHED_SOFTIRQ,
468 HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
469 numbering. Sigh! *
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
473 };

注册好软中断以后需要通过 raise_softirq 函数触发void raise_softirq(unsigned int nr)
tasklet
tasklet 是利用软中断来实现的另外一种下半部机制,在软中断和 tasklet 之间,建议大家使用 tasklet。

struct tasklet_struct
485 {
486 struct tasklet_struct *next; /* 下一个 tasklet */
487 unsigned long state; /* tasklet 状态 */
488 atomic_t count; /* 计数器,记录对 tasklet 的引用数 */
489 void (*func)(unsigned long); /* tasklet 执行的函数 */
490 unsigned long data; /* 函数 func 的参数 */
491 };

func 函数就是 tasklet 要执行的处理函数,用户定义函数内容,相当于中断
处理函数。如果要使用 tasklet,必须先定义一个 tasklet,然后使用tasklet_init 函数初始化 tasklet。
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
t:要初始化的 tasklet
func:tasklet 的处理函数。
data:要传递给 func 函数的参数
同时也可以使用宏 DECLARE_TASKLET 来 一 次 性 完 成 tasklet 的 定 义 和 初 始 化DECLARE_TASKLET(name, func, data)
其中 name 为要定义的 tasklet 名字,这个名字就是一个 tasklet_struct 类型的时候变量,func 就是 tasklet 的处理函数,data 是传递给 func 函数的参数。
在上半部,也就是中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运行,tasklet_schedule 函数原型如下:
void tasklet_schedule(struct tasklet_struct *t)
tasklet具体代码分析

/* 定义 taselet */
struct tasklet_struct testtasklet;
/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{
 /* tasklet 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
 ......
 /* 调度 tasklet */
 tasklet_schedule(&testtasklet);
 ......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
 ......
 /* 初始化 tasklet */
 tasklet_init(&testtasklet, testtasklet_func, data);
 /* 注册中断处理函数 */
 request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
 ......
}

工作队列:
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。Linux 内核使用 work_struct 结构体表示一个工作。xxx

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值