1.Linux内核微线程tasklet特征

  1. tasklet内核微线程是一种较为特殊的软中断。
  2. tasklet内核微线程是中断处理底半部分最常用的一种方法。
  3. tasklet绑定的函数在同一时刻只能在一个CPU上运行,在SMP系统(多处理器系统)上不会出现并发问题。

2.Linux内核tasklet应用于中断底半部

将中断处理程序拆分为两个部分,其中顶半部仅负责处理紧急事务,同时通过tasklet_schedule函数调度底半部代码。调用tasklet_schedule后,其关联的处理函数不会立即执行,而是会在中断处理完成后,经过一段不确定但通常较短的时间间隔才会被系统调用执行。

3.Linux内核tasklet数据结构:

内核微线使用tasklet_struct结构描述,结构定义在:include/interrupt.h文件中。

struct tasklet_struct
{
	struct tasklet_struct *next;
	unsigned long state;
	atomic_t count;
	void (*func)(unsigned long);
	unsigned long data;
};

成员:

next:内核为了把所有tasklet使用链表统一管理而设置的一个成员。

count:用来表示tasklet是否处于激活状态。0时表示处于激活状态,非0时表示处于非激活状态

当处于激活状态的tasklet在调度后才会执行绑定函数。

state:表示当前tasklet状态,指示正在执行绑定函数,开发者不需要直接访问这个成员。

functasklet绑定函数

data:函数执行时传递的参数。

4.Linux内核tasklet内核API

4.1 静态初始化:DECLARE_TASKLET

头文件

#include <linux/interrupt.h>

原型

#define DECLARE_TASKLET(name, func, data) struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

参数

func:tasklet绑定函数;

data:函数执行时传递的参数。

功能

定义一个名字为name的tasklet,并且初始化为激活状态。

4.2 静态初始化:DECLARE_TASKLET_DISABLED

头文件

#include <linux/interrupt.h>

原型

#define DECLARE_TASKLET_DISABLED(name, func, data) struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

参数

func:tasklet绑定函数;

data:函数执行时传递的参数。

功能

定义一一个名字为name的tasklet,并且初始化为非激活状态。

说明:一开始非激活状态à调度à某时刻count修改为0à不确定后à绑定func就会执行不需要重新调度

4.3 动态初始化:tasklet_init

头文件

#include <linux/interrupt.h>

原型

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

参数

t:任务核心结构体指针

func:tasklet绑定函数;

data:函数执行时传递的参数。

功能

运行时动态初始化tasklet,一般写模块初始化函数中

返回值

4.4 tasklet禁止:tasklet_disable

头文件

#include <linux/interrupt.h>

原型

static inline void tasklet_disable(struct tasklet_struct *t)

参数

t:任务核心结构体指针

功能

禁止任务

返回值

4.5 tasklet使能:tasklet_enable

头文件

#include <linux/interrupt.h>

原型

static inline void tasklet_enable(struct tasklet_struct *t)

参数

t:任务核心结构体指针

功能

使能任务

返回值

4.6 tasklet调度:tasklet_schedule

头文件

#include <linux/interrupt.h>

原型

void tasklet_schedule(struct tasklet_struct *t)

参数

t:任务核心结构体指针

功能

调度tasklet,调度后,绑定的函数会在不确定时间后被调用(前提条件是tasklet处于激活状态)。

返回值

注意:如果tasklet已经调度,但是还没有被响应(绑定的函数已经开始执行,就算没有执行完成也算是响应了),重复调度是无效。

4.7 tasklet取消:tasklet_kill

头文件

#include <linux/interrupt.h>

原型

void tasklet_kill(struct tasklet_struct *t)

参数

t:任务核心结构体指针

功能

取消一个已经调试的tasklet。这个函数会等待已经绑定函数执行完成

返回值

注意:你不能取消一个处于非激活状态,但是已经被调度的tasklet,否则会出现死锁情况

5.Linux内核tasklet使用示例

  • 使用步骤:
    1. 编写一个tasklet任务函数
    2. 定义struct tasklet_struct结构变量
    3. 初始化上一步定义的结构变量
    4. 调度tasklet
    5. 取消调度tasklet
  • 示例代码
#include<linux/module.h>
#include<linux/init.h>
#include<linux/interrupt.h>
#include<linux/delay.h>

//定义全局驱动数据结构体
struct my_driver_data {
    struct tasklet_struct mytasklet;	// 任务结构体
    char *data;		                    // 消息字符串
};

static struct my_driver_data mydrvdata;  // 声明全局驱动数据实例

void mytask_func(unsigned long data)
{
	struct my_driver_data *pdata = (struct my_driver_data *)data;
	printk("data:%s\r\n",pdata->data);
	//循环测试
	//tasklet_schedule(&pdata->mytasklet);

	//休眠测试
	//msleep(1000);	//不能调用休眠函数,否则内核会异常

	//udelay(500);	//这个函数不休眠,相当于while(t--);
}

static int __init mytasklet_init(void)
{
	mydrvdata.data = "LIU";

	tasklet_init(&mydrvdata.mytasklet, mytask_func, (unsigned long)&mydrvdata);

	tasklet_disable(&mydrvdata.mytasklet);//禁止调度测试
	
	printk("-----------begin------------\r\n");
	//多次调度只算一次
	tasklet_schedule(&mydrvdata.mytasklet);
	tasklet_schedule(&mydrvdata.mytasklet);
	tasklet_schedule(&mydrvdata.mytasklet);
	printk("------------end-----------\r\n");
    return 0;
}

static void __exit mytasklet_exit(void)
{
	tasklet_enable(&mydrvdata.mytasklet);//禁止/重新使能调度测试
	tasklet_kill(&mydrvdata.mytasklet);
}

module_init(mytasklet_init);			// 指定模块初始化函数 
module_exit(mytasklet_exit);			// 指定模块清理函数 
MODULE_LICENSE("GPL");					// 声明模块许可证(GPL v2) 

6.Linux内核tasklet实现按键中断底半部

在原来按键驱动基础上修改,把按键中断程序分为顶半部和底半部。

  • 设计思路
    1. 在按键结构中增加一个tasklet_struct结构成员
    2. 在注册中断的循环初始化tasklet_struct成员
    3. 把原来读取按键状态的代码从中断程序转移到tasklet任务函数中
    4. 在中断程序中调度tasklet,这一步就称为启动中断底部分
    5. 在模块卸载函数取消tasklet

drv_btn.c

//增加内核定时器功能
#include<linux/module.h>
#include<linux/fs.h>
#include<linux/miscdevice.h>
#include<asm/uaccess.h>
#include<linux/interrupt.h>
#include<linux/irq.h>
#include<linux/gpio.h>
#include<linux/slab.h>

#define LEDS_MINOR	255
#define DEVICE_NAME  "my-buttons" // 设备节点名称,将在/dev目录下创建 

#ifndef ARRAYSIZE
#define ARRAYSIZE(a)	(sizeof(a)/sizeof(a[0]))
#endif

//按键数量,在模块初始化函数中进行计算
static int key_size;
//按键缓冲区,一个元素存放一个按键值,'1'表示按下,'0'表示松开
//在模块的初始化函数中分配缓冲区空间
static char *keys_buf;	

//使用面向对象思想设计按键,把一个按键信息进行封装
struct key_info{
	int id;							//按键编号
	int gpio;						//统一的GPIO编号
	unsigned long flags;			//触发方式
	char *name;						//按键名
	int irq;						//中断编号
	struct tasklet_struct tasklet;	// 任务结构体
};

//实例化对象
static struct key_info keys[]={
	[0]={
		.id 	= 0,
		.gpio 	= 5,
		.flags 	= IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
		.name 	= "key-0",
	},
	[1]={
		.id 	= 0,
		.gpio 	= 54,
		.flags 	= IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
		.name 	= "key-1",
	},	
};

static int key_open (struct inode *pinode, struct file *pfile)
{
    printk("line:%d,%s is call\n",__LINE__,__FUNCTION__);
    return 0;    
}

loff_t key_llseek (struct file *pfile, loff_t offset, int whence)
{
    return 0;    
}

static ssize_t key_read (struct file *pfile, char __user *buf, size_t count, loff_t *offset)    
{
    int ret;
	if(count > key_size)	count = key_size;
    if(count == 0)			return 0;
	
	//准备数据,但是按键数据在中断中实时更新,不需要在这里读取
	//复制数据到用户空间
	ret = copy_to_user(buf, keys_buf, count);
	if(ret){
		printk("error:copy_to_user\r\n");
		ret = -EFAULT;
		goto errot_copy_to_user;
	}
	return count;  // 返回成功读取的字节数  
errot_copy_to_user:
	return ret;
}

int key_release (struct inode *pinode, struct file *pfile)
{
    return 0;
}

// 文件操作结构体(关联实现的操作函数) 
static const struct file_operations dev_fops = {
    .open = key_open,      // 打开设备 
    .read = key_read,      // 读取设备状态 
	.owner = THIS_MODULE,
};

// 杂项设备定义 
static struct miscdevice key_device = {
    .name = DEVICE_NAME,	// 设备名称(出现在/dev目录下) 
    .minor = LEDS_MINOR, 	// 次设备号(建议使用动态分配) 
    .fops = &dev_fops,		// 关联文件操作函数集 
};

//tasklet处理函数
void btn_tasklet(unsigned long data)
{

	int s;
	struct key_info *pdata = (struct key_info *)data;

	//检测当前的电平状态
	s = !gpio_get_value(pdata->gpio);
	keys_buf[pdata->id]='0'+s;	//保存状态
}

//按键中断函数
//设置了双边触发,按下和松开都会进入这个函数
irqreturn_t btns_irq_handler(int irq,void *devid)
{
	struct key_info *pdata = (struct key_info *)devid;

	tasklet_schedule(&pdata->tasklet);//调度tasklet

	return IRQ_HANDLED;
}

static int __init my_btn_init(void)
{
    int ret,i;

	key_size = ARRAY_SIZE(keys);	//计算按键数量

	//分配按键缓冲区
	keys_buf = kzalloc(key_size,GFP_KERNEL);
	if(keys_buf == NULL){
		return -EFAULT;
	}
	
	//循环注册中断
	for(i = 0;i < key_size;i++){
		keys[i].irq = gpio_to_irq(keys[i].gpio);
		if(keys[i].irq < 0){
			printk("error:gpio_to_irq\r\n");
			goto error_gpio_to_irq;  
		}
		printk("irq:%d\r\n",keys[i].irq);
		
		//传递每个按键结构变量地址,发生中断时可以通过参数取得
		ret = request_irq(keys[i].irq, btns_irq_handler, keys[i].flags, keys[i].name, (void *)&keys[i]);
		if(ret < 0){
			printk("error:request_irq\r\n");
			goto error_request_irq;  
		}

		//初始化tasklet
		tasklet_init(&keys[i].tasklet, btn_tasklet, (unsigned long)&keys[i]);
	}
    ret = misc_register(&key_device);
    if(ret < 0){
        printk("error:misc_register\r\n");
        goto error_misc_register;  
    }
    return 0;

error_misc_register:
error_request_irq:
	while(--i >= 0){
		free_irq(keys[i].irq, (void *)&keys[i]);	//注销中断
	}
	
error_gpio_to_irq:
	kfree(keys_buf);		//释放按键缓冲区空间
    return ret; 
}

static void __exit my_btn_exit(void)
{
	int i = key_size;
	while(--i >= 0){
		tasklet_kill(&keys[i].tasklet);	//取消tasklet
	}
	misc_deregister(&key_device);	//注销杂项设备
	kfree(keys_buf);				//释放按键缓冲区空间
}

module_init(my_btn_init);			// 指定模块初始化函数 
module_exit(my_btn_exit);			// 指定模块清理函数 
MODULE_LICENSE("GPL");				// 声明模块许可证(GPL v2) 

app.c

#include<stdio.h>              // 标准输入输出库(printf, perror等)
#include<stdlib.h> 
#include<string.h> 
#include<sys/types.h>          // 系统数据类型定义(如dev_t)
#include<sys/stat.h>           // 文件状态信息(文件模式等)
#include<fcntl.h>              // 文件控制选项(open等)
#include<unistd.h>             // 系统调用封装(lseek, read, write, sleep等)
#include<sys/ioctl.h>          // I/O控制操作(ioctl)
#include<errno.h> 

#define BTN_SIZE	1					// 按键数量
#define DEV_NAME 	"/dev/my-buttons"  	// 默认设备名

int main(int argc, char **argv)
{
    int fd,ret, i;                
    const char *devname; 		// 设备路径指针(初始化为默认路径)
    unsigned char pre_buf[BTN_SIZE+1],recv_buf[BTN_SIZE+1];

	memset(pre_buf,'0',BTN_SIZE);
	memset(recv_buf,'0',BTN_SIZE);
	
    if(argc == 1)
        devname = DEV_NAME;
    else if(argc == 2)
        devname= argv[1];
    else {
        printf("Usage:%s [/dev/devname]\r\n", argv[0]);
        return 0;
    }

    fd = open(devname, O_RDWR);  			// O_RDWR:以读写模式打开
    if(fd < 0) {
        perror("open");   					// 打印系统错误信息
        printf("fd=%d\r\n", fd);
        return -1;            				// 打开失败退出程序
    }
    printf("fd=%d\r\n", fd);  			// 成功打开后输出fd值

    while(1) {
		ret = read(fd,recv_buf,BTN_SIZE);	//读取按键数据
		if(ret < 0){
			if(errno != EAGAIN){
				perror("read");
				exit(-1);
			}else	continue;
		}

		//只在状态发生变化时候才输出
		for(i = 0;i < BTN_SIZE;i++){
			//分别判断每一个按键状态是否发生变化
			if(recv_buf[i] != pre_buf[i]){
				//更新当前状态为上一次状态
				pre_buf[i] = recv_buf[i];

				//判断这次变化是按下还是松开
				if(pre_buf[i] == '1')
					printf("KEY%d is press!\r\n",i+1);
				else
					printf("KEY%d is up!\r\n",i+1);
			}
		}
    }    
    return 0;
}

现象

7.按键消抖

把定时器和tasklet结合实现按键驱动实现消抖和底半部。

驱动代码

//增加内核定时器功能
#include<linux/module.h>
#include<linux/fs.h>
#include<linux/miscdevice.h>
#include<asm/uaccess.h>
#include<linux/interrupt.h>
#include<linux/irq.h>
#include<linux/gpio.h>
#include<linux/slab.h>

#define LEDS_MINOR	255
#define DEVICE_NAME  "my-buttons" // 设备节点名称,将在/dev目录下创建 

#ifndef ARRAYSIZE
#define ARRAYSIZE(a)	(sizeof(a)/sizeof(a[0]))
#endif

//按键数量,在模块初始化函数中进行计算
static int key_size;
//按键缓冲区,一个元素存放一个按键值,'1'表示按下,'0'表示松开
//在模块的初始化函数中分配缓冲区空间
static char *keys_buf;	

//使用面向对象思想设计按键,把一个按键信息进行封装
struct key_info{
	int id;							//按键编号
	int gpio;						//统一的GPIO编号
	unsigned long flags;			//触发方式
	char *name;						//按键名
	int irq;						//中断编号
	struct timer_list timer;		//增加一个定时器作消抖
	struct tasklet_struct tasklet;	// 任务结构体
};

//实例化对象
static struct key_info keys[]={
	[0]={
		.id 	= 0,
		.gpio 	= 5,
		.flags 	= IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
		.name 	= "key-0",
	},
	[1]={
		.id 	= 1,
		.gpio 	= 54,
		.flags 	= IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
		.name 	= "key-1",
	},	
};

static int key_open (struct inode *pinode, struct file *pfile)
{
    printk("line:%d,%s is call\n",__LINE__,__FUNCTION__);
    return 0;    
}

loff_t key_llseek (struct file *pfile, loff_t offset, int whence)
{
    return 0;    
}

int key_release (struct inode *pinode, struct file *pfile)
{
    return 0;
}

static ssize_t key_read (struct file *pfile, char __user *buf, size_t count, loff_t *offset)    
{
    int ret;
	if(count > key_size)	count = key_size;
    if(count == 0)			return 0;
	
	//准备数据,但是按键数据在中断中实时更新,不需要在这里读取
	//复制数据到用户空间
	ret = copy_to_user(buf, keys_buf, count);
	if(ret){
		printk("error:copy_to_user\r\n");
		ret = -EFAULT;
		goto errot_copy_to_user;
	}
	return count;  // 返回成功读取的字节数  
errot_copy_to_user:
	return ret;
}

// 文件操作结构体(关联实现的操作函数) 
static const struct file_operations dev_fops = {
    .open = key_open,      // 打开设备 
    .read = key_read,      // 读取设备状态 
	.owner = THIS_MODULE,
};

// 杂项设备定义 
static struct miscdevice key_device = {
    .name = DEVICE_NAME,	// 设备名称(出现在/dev目录下) 
    .minor = LEDS_MINOR, 	// 次设备号(建议使用动态分配) 
    .fops = &dev_fops,		// 关联文件操作函数集 
};

//tasklet处理函数
void btn_tasklet(unsigned long data)
{

	int state;
	struct key_info *pdata = (struct key_info *)data;

	//检测当前的电平状态
	state = !gpio_get_value(pdata->gpio);
	// 确保ID在有效范围内
	if (pdata->id >= 0 && pdata->id < key_size) {
		keys_buf[pdata->id] = '0' + state;
		printk("Key %d state: %d\n", pdata->id+1, state);
	} else {
		printk("Invalid key ID: %d\n", pdata->id+1);
	}
}

//超时处理函数
void btn_timer(unsigned long data)
{
	struct key_info *pdata = (struct key_info *)data;
	tasklet_schedule(&pdata->tasklet);			//调度tasklet
}

//按键中断函数
//设置了双边触发,按下和松开都会进入这个函数
irqreturn_t btns_irq_handler(int irq,void *devid)
{
	struct key_info *pdata = (struct key_info *)devid;
	//启动新一次定时
	mod_timer(&pdata->timer,jiffies + msecs_to_jiffies(20));
	return IRQ_HANDLED;
}

static int __init my_btn_init(void)
{
    int ret,i;

	key_size = ARRAY_SIZE(keys);	//计算按键数量

	//分配按键缓冲区
	keys_buf = kzalloc(key_size,GFP_KERNEL);
	if(keys_buf == NULL)	return -EFAULT;
	
	//循环注册中断
	for(i = 0;i < key_size;i++){
		keys[i].irq = gpio_to_irq(keys[i].gpio);
		if(keys[i].irq < 0){
			printk("error:gpio_to_irq\r\n");
			goto error_gpio_to_irq;  
		}
		printk("irq:%d\r\n",keys[i].irq);
		
		//传递每个按键结构变量地址,发生中断时可以通过参数取得
		ret = request_irq(keys[i].irq, btns_irq_handler, keys[i].flags, keys[i].name, (void *)&keys[i]);
		if(ret < 0){
			printk("error:request_irq\r\n");
			goto error_request_irq;  
		}
		//初始化tasklet
		tasklet_init(&keys[i].tasklet, btn_tasklet, (unsigned long)&keys[i]);
		//初始化按键的定时器,参数是按键自身的结构内存地址
		setup_timer(&keys[i].timer,btn_timer, (unsigned long)&keys[i]);
	}
    ret = misc_register(&key_device);
    if(ret < 0){
        printk("error:misc_register\r\n");
        goto error_misc_register;  
    }
    return 0;  								// 初始化成功 

error_misc_register:
error_request_irq:
	while(--i >= 0){
		free_irq(keys[i].irq, (void *)&keys[i]);	//注销中断
	}
error_gpio_to_irq:
	kfree(keys_buf);		//释放按键缓冲区空间
    return ret; 
}

static void __exit my_btn_exit(void)
{
	int i = key_size;
	while(--i >= 0){
		tasklet_kill(&keys[i].tasklet);	//取消tasklet
		free_irq(keys[i].irq, (void *)&keys[i]);	//注销中断
		del_timer_sync(&keys[i].timer);
	}
	misc_deregister(&key_device);	//注销杂项设备
	kfree(keys_buf);				//释放按键缓冲区空间
}

module_init(my_btn_init);				// 指定模块初始化函数 
module_exit(my_btn_exit);				// 指定模块清理函数 
MODULE_LICENSE("GPL");					// 声明模块许可证(GPL v2) 

应用程序

#include<stdio.h>              // 标准输入输出库(printf, perror等)
#include<stdlib.h> 
#include<string.h> 
#include<sys/types.h>          // 系统数据类型定义(如dev_t)
#include<sys/stat.h>           // 文件状态信息(文件模式等)
#include<fcntl.h>              // 文件控制选项(open等)
#include<unistd.h>             // 系统调用封装(lseek, read, write, sleep等)
#include<sys/ioctl.h>          // I/O控制操作(ioctl)
#include<errno.h> 

#define BTN_SIZE	2					// 按键数量
#define DEV_NAME 	"/dev/my-buttons"  	// 默认设备名

int main(int argc, char **argv)
{
    int fd,ret, i;                
    const char *devname; 		// 设备路径指针(初始化为默认路径)
    unsigned char pre_buf[BTN_SIZE+1],recv_buf[BTN_SIZE+1];

	memset(pre_buf,'0',BTN_SIZE);
	memset(recv_buf,'0',BTN_SIZE);
	
    if(argc == 1)
        devname = DEV_NAME;
    else if(argc == 2)
        devname= argv[1];
    else {
        printf("Usage:%s [/dev/devname]\r\n", argv[0]);
        return 0;
    }

    /* 打开设备文件(字符设备)*/
    fd = open(devname, O_RDWR);  	// O_RDWR:以读写模式打开
    if(fd < 0) {
        perror("open");   			// 打印系统错误信息
        printf("fd=%d\r\n", fd);
        return -1;            		// 打开失败退出程序
    }
    printf("fd=%d\r\n", fd);  // 成功打开后输出fd值

    while(1) {
		ret = read(fd,recv_buf,BTN_SIZE);	//读取按键数据
		if(ret < 0){
			if(errno != EAGAIN){
				perror("read");
				exit(-1);
			}else	continue;
		}

		//只在状态发生变化时候才输出
		for(i = 0;i < BTN_SIZE;i++){
			//分别判断每一个按键状态是否发生变化
			if(recv_buf[i] != pre_buf[i]){
				//更新当前状态为上一次状态
				pre_buf[i] = recv_buf[i];

				//判断这次变化是按下还是松开
				if(pre_buf[i] == '1')
					printf("KEY%d is press!\r\n",i+1);
				else
					printf("KEY%d is up!\r\n",i+1);
			}
		}
    }    
    return 0;
}

现象

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐