Linux下按键设备驱动开发以及对中断的上半部分和下半部分详细介绍

文章目录

    • 一、编写并且加载设备树插件
      • (1)检测管脚是否占用
      • (2)添加设备树插件
      • (3)加载设备树插件
    • 二、中断相关函数
      • (1)request_irq中断注册函数
      • (2)free_irq中断注销函数
      • (3)中断处理函数
    • 三、编写按键驱动代码
      • (1)编写代码
      • (2)运行结果
    • 四、中断的上半部分和下半部分
      • (1)中断上下半部分介绍
      • (2)软中断
      • (3)tasklet
        • 【1】tasklet_struct 结构体
        • 【2】初始化 tasklet
        • 【3】触发 tasklet
        • 【4】tasklet 测试程序
        • 【5】相关宏定义
          • DECLARE_TASKLET()
          • DECLARE_TASKLET_DISABLED()
          • DECLARE_TASKLET_HI()
      • (4)工作队列(workqueue)
        • 【1】work_struct 结构体
        • 【2】初始化 workqueue
        • 【3】启动/关闭 workqueue
        • 【4】workqueue 测试程序


在做Linux下设备驱动开发之前我们需要掌握的基础知识可以看这篇文章:GIC中断控制器、设备树插件(Device Tree Overlay)以及内核定时器介绍


一、编写并且加载设备树插件

在imx6ull开发板上我们的按键管脚是GPIO4_IO14。

在前面的文章中做LED驱动开发的时候,我们是直接更新设备树的,学了设备树插件的方式,我们可以用打补丁的方式进行添加子节点。

(1)检测管脚是否占用

先检查NAND_CE1_B这个PIN有没有被其他pinctrl节点使用, 如果有使用的话就要屏蔽掉,然后再检查GPIO4_IO14这个GPIO有没有被其他外设使用,如果有的话也要屏蔽掉。

在我的开发板中按键设备默认是这个GPIO,所以被占用了,我们需要屏蔽掉。

/imx6ull/imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts下的igkboard.dts:

        /*keys {
            compatible = "gpio-keys";
            pinctrl-names = "default";
            pinctrl-0 = <&pinctrl_gpio_keys>;
            autorepeat;
            status = "okay";

            key_user {
                lable = "key_user";
                gpios = <&gpio4 14 GPIO_ACTIVE_LOW>;
                linux,code = ;
            };
        };*/
&iomuxc {
    pinctrl-names = "default";

/*pinctrl_gpio_keys: gpio-keys {
                       fsl,pins = <
                           MX6UL_PAD_NAND_CE1_B__GPIO4_IO14     0x17059  
                           >;
                   };*/

(2)添加设备树插件

一般设备树插件都在如下的目录下:

wangdengtao@wangdengtao-virtual-machine:~/imx6ull/imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts/overlays$ ls
adc.dtbo  cam.dts    can2.dtbo  i2c1.dts      lcd_drm.dtbo  lcd.dts        nbiot-4g.dts  pwm8.dtbo  spi1.dts    uart3.dtbo  uart4.dts   w1.dtbo
adc.dts   can1.dtbo  can2.dts   key_irq.dtbo  lcd_drm.dts   Makefile       pwm7.dtbo     pwm8.dts   uart2.dtbo  uart3.dts   uart7.dtbo  w1.dts
cam.dtbo  can1.dts   i2c1.dtbo  key_irq.dts   lcd.dtbo      nbiot-4g.dtbo  pwm7.dts      spi1.dtbo  uart2.dts   uart4.dtbo  uart7.dts

编写key_irq.dts设备树插件(一般不推荐直接这样添加设备树插件,一般推荐在设备树中添加根节点,在设备树插件中添加子节点,为了方便目前使用前者):

/dts-v1/;
/plugin/;

#include 
#include "../imx6ul-pinfunc.h"
#include "dt-bindings/interrupt-controller/irq.h"

/ {
	compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";

	fragment@0 {
		target-path = "/";
		__overlay__ {
key_irq:key_irq{
			compatible = "my-gpio-keys";
			status = "disabled";
		};
		};
	};

	fragment@1 {
		target = <&key_irq>;/*节点名字需要和添加的根节点的名字相同*/
		__overlay__ {
			compatible = "my-gpio-keys";
			pinctrl-names = "default";
			pinctrl-0 = <&pinctrl_my_gpio_keys>;
			gpios = <&gpio4 14 GPIO_ACTIVE_LOW>;
			status = "okay";
			interrupt-parent = <&gpio4>;
			interrupts = <14 IRQ_TYPE_EDGE_RISING>;
		};
	};

	fragment@2 {
		target = <&iomuxc> ;
		__overlay__ {
pinctrl_my_gpio_keys: my-gpio-keys {
						  fsl,pins = <
							  MX6UL_PAD_NAND_CE1_B__GPIO4_IO14 0x17059 /* gpio key */
							  >;
					  };
		};
	};
};

在根路径(/imx6ull/imx6ull/bsp/kernel/linux-imx)下执行make dtbs编译:

wangdengtao@wangdengtao-virtual-machine:~/imx6ull/imx6ull/bsp/kernel/linux-imx$ make dtbs
  DTC     arch/arm/boot/dts/overlays/key_irq.dtbo

我们就可以在/imx6ull/imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts/overlays下看见key_irq.debo设备树补丁文件了:

wangdengtao@wangdengtao-virtual-machine:~/imx6ull/imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts/overlays$ ls
adc.dtbo  cam.dts    can2.dtbo  i2c1.dts      lcd_drm.dtbo  lcd.dts        nbiot-4g.dts  pwm8.dtbo  spi1.dts    uart3.dtbo  uart4.dts   w1.dtbo
adc.dts   can1.dtbo  can2.dts   key_irq.dtbo  lcd_drm.dts   Makefile       pwm7.dtbo     pwm8.dts   uart2.dtbo  uart3.dts   uart7.dtbo  w1.dts
cam.dtbo  can1.dts   i2c1.dtbo  key_irq.dts   lcd.dtbo      nbiot-4g.dtbo  pwm7.dts      spi1.dtbo  uart2.dts   uart4.dtbo  uart7.dts

(3)加载设备树插件

我们将key_irq.debo文件上传到我们的开发板上,然后添加到如下路径下:

root@igkboard:/run/media/mmcblk1p1/overlays# ls
adc.dtbo  can1.dtbo  i2c1.dtbo     lcd.dtbo      nbiot-4g.dtbo  pwm8.dtbo  uart2.dtbo  uart4.dtbo  w1.dtbo
cam.dtbo  can2.dtbo  key_irq.dtbo  lcd_drm.dtbo  pwm7.dtbo      spi1.dtbo  uart3.dtbo  uart7.dtbo

并且在路径(/run/media/mmcblk1p1)下修改配置文件config.txt

# Enable extra overlays
dtoverlay_extra=key_irq

然后重启开发板即可,就可以在路径/proc/device-tree下看见我们的设备树文件,以及里面的属性:

root@igkboard:~# ls /proc/device-tree  
'#address-cells'   3p3v               aliases         clock-cli   clock-osc    key_irq           model     name    pwm-buzzer           regulator-sd1-vmmc   serial-number   timer
'#size-cells'      __local_fixups__   backlight-lcd   clock-di0   compatible   leds              mqs       panel   pxp_v4l2             regulator@0          soc             w1
 1p8v              __symbols__        chosen          clock-di1   cpus         memory@80000000   my_leds   pmu     regulator-peri-3v3   reserved-memory      sound-mqs
root@igkboard:/proc/device-tree/key_irq# ls
compatible  gpios  interrupt-parent  interrupts  name  phandle  pinctrl-0  pinctrl-names  status

如上就添加设备树插件并且加载成功了。


二、中断相关函数

在Linux内核中也提供了大量的中断相关的API函数,每个中断都有一个中断号,通过中断号即可区分不同的中断。在Linux内核中使用一个int变量表示中断号。

(1)request_irq中断注册函数

在linux内核中要想使用某个中断是需要申请的,request_irq函数用于申请中断,request_irq函数可能会导致休眠,因此不能在中断上下文或其他禁止睡眠的代码段中使用request_irq函数。

static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)
  • irq:用于指定“内核中断号”,这个参数我们会从设备树中获取或转换得到。在内核空间中它代表一个唯一的中断编号;
  • handler:用于指定中断处理函数,中断发生后跳转到该函数去执行;
  • flags:中断触发条件,也就是我们常说的上升沿触发、下降沿触发等等 触发方式通过“|”进行组合;
  • name:中断的名字,中断申请成功后会在“/proc/interrupts”目录下看到对应的文件。
  • dev: 如果使用了IRQF_SHARED 宏,则开启了共享中断。“共享中断”指的是多个驱动程序共用同一个中断。 开启了共享中断之后,中断发生后内核会依次调用这些驱动的“中断服务函数”。 这就需要我们在中断服务函数里判断中断是否来自本驱动,这里就可以用dev参数做中断判断。 即使不用dev参数判断中断来自哪个驱动,在申请中断时也要加上dev参数 因为在注销驱动时内核会根据dev参数决定删除哪个中断服务函数。

上面flags的设置会覆盖设备树中的默认设置,宏定义如下:

#define IRQF_TRIGGER_NONE 0x00000000
#define IRQF_TRIGGER_RISING 0x00000001
#define IRQF_TRIGGER_FALLING 0x00000002
#define IRQF_TRIGGER_HIGH 0x00000004
#define IRQF_TRIGGER_LOW 0x00000008
#define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE 0x00000010
#define IRQF_SHARED 0x00000080 ---------①//多个设备共享一个中断
/*-----------以下宏定义省略------------*/
  • 成功:返回0
  • 失败:返回负数。

(2)free_irq中断注销函数

使用中断的时候需要通过request_irq函数申请,使用完成以后就要通过free_irq函数释放掉相应的中断。如果中断不是共享的,那么free_irq会删除中断处理函数并且禁止中断。

void free_irq(unsigned int irq, void *dev);
  • irq:从设备树中得到或者转换得到的中断编号;
  • dev:与request_irq函数中dev传入的参数一致。

(3)中断处理函数

在中断申请时需要指定一个中断处理函数,书写格式如下所示。

irqreturn_t (*irq_handler_t)(int irq, void * dev);
  • irq:用于指定“内核中断号”。
  • dev:在共享中断中,用来判断中断产生的驱动是哪个,具体介绍同上中断注册函数。 不同的是dev参数是内核“带回”的。如果使用了共享中断还得根据dev带回的硬件信息判断中断是否来自本驱动,或者不使用dev,直接读取硬件寄存器判断中断是否来自本驱动。如果不是,应当立即跳出中断服务函数,否则正常执行中断服务函数。

返回值是irqreturn_t类型:枚举类型变量,如下所示。

enum irqreturn {
	IRQ_NONE = (0 << 0),
	IRQ_HANDLED = (1 << 0),
	IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;

如果是“共享中断”并且在中断服务函数中发现中断不是来自本驱动则应当返回 IRQ_NONE , 如果没有开启共享中断或者开启了并且中断来自本驱动则返回 IRQ_HANDLED,表示中断请求已经被正常处理。第三个参数涉及到我们后面会讲到的中断服务函数的“上半部分”和“下半部分”, 如果在中断服务函数是使用“上半部分”和“下半部分”实现,则应当返回IRQ_WAKE_THREAD。


三、编写按键驱动代码

Linux下按键设备驱动开发以及对中断的上半部分和下半部分详细介绍_第1张图片

(1)编写代码

代码中涉及到内核定时器的相关知识,在开头的文章中都有讲解。
按键驱动设备文件(key_irq.c):

/*************************************************************************
  > File Name: key_irq.c
  > Author: WangDengtao
  > Mail: [email protected] 
  > Created Time: 2023年04月01日 星期六 14时55分18秒
 ************************************************************************/

#include                      // 驱动程序必须的头文件
#include                    // 驱动程序必须的头文件
#include                     // ENODEV,ENOMEM存放的头文件
#include                     // u8,u32,dev_t等类型在该头文件中定义          
#include                    // printk(),内核打印函数
#include                        // file_operations,用于联系系统调用和驱动程序
#include                      // cdev_alloc(),分配cdev结构体
#include                    // 用于自动生成设备节点的函数头文件
#include                   // gpio子系统的api
#include           // platform总线驱动头文件
#include 
#include 
#include                     // 内核和用户传输数据的函数
#include                    // 中断相关函数
#include                       // 中断相关函数
#include 
#include                     // 定时器相关函数


#define KEY_NAME                "key_irq"
#define KEY0VALUE               0XF0        // 按键值 
#define INVAKEY                 0X00        // 无效的按键值 

static int                      dev_major = 0;

// 存放key信息的结构体
struct platform_key_data 
{
	char                        name[16];       // 设备名字
	int                         key_gpio;       // gpio编号
	unsigned char                value;         // 按键值

	int                         irq;            // 中断号
	irqreturn_t (*handler)(int, void *);        // 中断处理函数
};

// 存放key的私有属性
struct platform_key_priv 
{
	struct cdev                 cdev;                   // cdev结构体
	struct class                *dev_class;             // 自动创建设备节点的类
	int                         num_key;                // key的数量
	struct platform_key_data    key;                    // 存放key信息的结构体数组

	atomic_t                    keyvalue;               // 有效的按键键值,用于向应用层上报
	atomic_t                    releasekey;             // 标记是否完成一次完成的按键,用于向应用层上报
	struct timer_list           timer;                  // 定时器,用于消抖 
};

// 为key私有属性开辟存储空间的函数
static inline int sizeof_platform_key_priv(int num_key)
{
	return sizeof(struct platform_key_priv) + (sizeof(struct platform_key_data) * num_key);
}

// 中断服务函数,初始化定时器用于消抖
static irqreturn_t key0_handler(int irq, void *dev_id)
{
	struct platform_key_priv *priv = (struct platform_key_priv *)dev_id;

	// 开启定时器
	//priv->timer.data = (volatile long)dev_id;
	mod_timer(&(priv->timer), jiffies + msecs_to_jiffies(10));

	return IRQ_RETVAL(IRQ_HANDLED);
}

// 定时器服务函数,定时器到了后的操作
// 定时器到了以后再次读取按键,如果按键还是按下状态则有效
void timer_function(struct timer_list *t)
{
	unsigned char value;
	struct platform_key_priv *priv = from_timer(priv, t, timer);

	// 读取按下的io值
	value = gpio_get_value(priv->key.key_gpio); 	
	if(value == 0) // 按下按键
	{ 					
		atomic_set(&(priv->keyvalue), value);
		printk("keypress\n");
	}
	else // 按键松开
	{ 									
		atomic_set(&(priv->keyvalue), 0x80 | value);
		atomic_set(&(priv->releasekey), 1); // 标记松开按键,即完成一次完整的按键过程 	
		printk("keyrelease\n");
	}	
}

// 解析设备树,初始化key属性并初始化中断
int parser_dt_init_key(struct platform_device *pdev)
{
	/*struct device_node *of_node;/*存放设备树中匹配的设备节点。当内核使能设备树,总线负责将驱动的of_match_table 以及设备树的 compatible 属性进行比较之后,将匹配的节点保存到该变量。*/
	struct device_node *np = pdev->dev.of_node;     // 当前设备节点
	struct platform_key_priv *priv;                 // 存放私有属性
	int num_key, gpio;                              // key数量和gpio编号      
	int ret;                           

	/* 1)按键初始化 */
	// 获取该节点下子节点的数量
	num_key = 1;
	if(num_key <= 0) 
	{
		dev_err(&pdev->dev, "fail to find node\n");
		return -EINVAL;
	}

	// 分配存储空间用于存储按键的私有数据
	priv = devm_kzalloc(&pdev->dev, sizeof_platform_key_priv(num_key), GFP_KERNEL);
	if (!priv)
	{
		return -ENOMEM;
	}

	// 通过dts属性名称获取gpio编号
	gpio = of_get_named_gpio(np, "gpios", 0);

	// 将子节点的名字,传给私有属性结构体中的key信息结构体中的name属性
	strncpy(priv->key.name, np->name, sizeof(priv->key.name)); 

	// 将gpio编号和控制亮灭的标志传给结构体
	priv->key.key_gpio = gpio; 

	// 申请gpio口,相较于gpio_request增加了gpio资源获取与释放功能
	if( (ret = devm_gpio_request(&pdev->dev, priv->key.key_gpio, priv->key.name)) < 0 ) 
	{
		dev_err(&pdev->dev, "fail to request gpio for %s\n", priv->key.name); 
		return ret;
	}

	// 设置gpio为输入模式,并设置初始状态
	if( (ret = gpio_direction_input(priv->key.key_gpio))< 0 ) 
	{
		dev_err(&pdev->dev, "can't request gpio output for %s\n", priv->key.name); 
	}

	/* 2)中断初始化 */
	// 从设备树中获取中断号
	priv->key.irq = irq_of_parse_and_map(np, 0);
	// 申请中断,并初始化value和中断处理函数
	priv->key.handler = key0_handler;
	priv->key.value = KEY0VALUE;

	ret = request_irq(priv->key.irq, priv->key.handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, priv->key.name, priv);
	if(ret < 0)
	{
		printk("fail to request irq %d\n", priv->key.irq);
		return -EFAULT;
	}

	/*初始化 timer,设置定时器处理函数,还未设置周期,所以不会激活定时器*/
	timer_setup(&(priv->timer), timer_function, 0);
	priv->timer.expires = jiffies + HZ/5;
	priv->timer.function = timer_function;
	add_timer(&priv->timer);

	// 暂时先解决一个按键的问题
	priv->num_key = 1;
	dev_info(&pdev->dev, "success to get %d valid key\n", priv->num_key);

	// 将key的私有属性放入platform_device结构体的device结构体中的私有数据中
	platform_set_drvdata(pdev, priv);

	return 0;
}

static int key_open(struct inode *inode, struct file *file)
{
	struct platform_key_priv *priv;

	priv = container_of(inode->i_cdev, struct platform_key_priv, cdev);
	file->private_data = priv;

	return 0;
}

static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	unsigned char value;
	int ret = 0;

	struct platform_key_priv  *priv;
	priv = filp->private_data;

	if (0 == gpio_get_value(priv->key.key_gpio))    // key0 按下 
	{ 
		while(!gpio_get_value(priv->key.key_gpio)); // 等待按键释放 
		atomic_set(&(priv->keyvalue), KEY0VALUE);
	} 
	else // 无效的按键值
	{
		atomic_set(&(priv->keyvalue), INVAKEY);
	}

	value = atomic_read(&(priv->keyvalue));         // 保存按键值 
	ret = copy_to_user(buf, &value, sizeof(value));

	return ret;
}

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

static struct file_operations key_fops = 
{
	.owner = THIS_MODULE,
	.open = key_open,
	.read = key_read,
	.release = key_release,
};

static int platform_key_probe(struct platform_device *pdev)
{
	struct platform_key_priv    *priv;          // 临时存放私有属性的结构体
	struct device               *dev;           // 设备结构体
	dev_t                       devno;          // 设备的主次设备号
	int                         i, rv = 0;      

	// 1)解析设备树并初始化key状态
	rv = parser_dt_init_key(pdev);
	if( rv < 0 )
		return rv;

	// 将之前存入的私有属性,放入临时的结构体中
	priv = platform_get_drvdata(pdev);

	// 2)分配主次设备号
	if (0 != dev_major) 
	{   
		// 静态分配主次设备号
		devno = MKDEV(dev_major, 0); 	
		rv = register_chrdev_region(devno, priv->num_key, "KEY_NAME"); /*proc/devices/key_irq*/
	}   
	else 
	{   
		// 动态分配主次设备号
		rv = alloc_chrdev_region(&devno, 0, priv->num_key, "KEY_NAME"); 
		dev_major = MAJOR(devno); 
	}   
	if (rv < 0) 
	{   
		dev_err(&pdev->dev, "major can't be allocated\n"); 
		return rv; 
	}   

	// 3)分配cdev结构体
	cdev_init(&priv->cdev, &key_fops);
	priv->cdev.owner  = THIS_MODULE;

	rv = cdev_add (&priv->cdev, devno , priv->num_key); 
	if( rv < 0) 
	{
		dev_err(&pdev->dev, "struture cdev can't be allocated\n");
		goto undo_major;
	}

	// 4)创建类,实现自动创建设备节点
	priv->dev_class = class_create(THIS_MODULE, "key"); /* /sys/class/key */
	if( IS_ERR(priv->dev_class) ) 
	{
		dev_err(&pdev->dev, "fail to create class\n");
		rv = -ENOMEM;
		goto undo_cdev;
	}

	// 5)创建设备
	for(i=0; i<priv->num_key; i++)
	{
		devno = MKDEV(dev_major, i);
		dev = device_create(priv->dev_class, NULL, devno, NULL, "key%d", i);  /* /dev/key0 */
		if( IS_ERR(dev) ) 
		{
			dev_err(&pdev->dev, "fail to create device\n");
			rv = -ENOMEM;
			goto undo_class;
		}
	}

	printk("success to install driver[major=%d]!\n", dev_major);

	return 0;

undo_class:
	class_destroy(priv->dev_class);

undo_cdev:
	cdev_del(&priv->cdev);

undo_major:
	unregister_chrdev_region(devno, priv->num_key);

	return rv;
}

static int platform_key_remove(struct platform_device *pdev)
{
	struct platform_key_priv *priv = platform_get_drvdata(pdev);
	int i;
	dev_t devno = MKDEV(dev_major, 0);

	// 注销设备结构体,class结构体和cdev结构体
	for(i=0; i<priv->num_key; i++)
	{
		devno = MKDEV(dev_major, i);
		device_destroy(priv->dev_class, devno);
	}
	class_destroy(priv->dev_class);

	cdev_del(&priv->cdev); 
	unregister_chrdev_region(MKDEV(dev_major, 0), priv->num_key);

	// 将key的状态设置为0
	for (i = 0; i < priv->num_key; i++) 
	{
		gpio_set_value(priv->key.key_gpio, 0);
	}   

	// 删除定时器
	del_timer_sync(&(priv->timer));

	// 释放中断
	free_irq(priv->key.irq, priv);

	printk("success to remove driver[major=%d]!\n", dev_major);
	return 0;
} 

// 匹配列表
static const struct of_device_id platform_key_of_match[] = {
	{ .compatible = "my-gpio-keys" },
	{}
};

MODULE_DEVICE_TABLE(of, platform_key_of_match);

// platform驱动结构体
static struct platform_driver platform_key_driver = {
	.driver		= {
		.name	= "key_irq",			                // 无设备树时,用于设备和驱动间的匹配
		.of_match_table	= platform_key_of_match,    // 有设备树后,利用设备树匹配表
	},
	.probe		= platform_key_probe,
	.remove		= platform_key_remove,
};

module_platform_driver(platform_key_driver);

/*添加LICENSE和作者信息,是来告诉内核,该模块带有一个自由许可证;没有这样的说明,在加载模块的时内核会“抱怨”.*/
MODULE_LICENSE("Dual BSD/GPL");//许可 GPL、GPL v2、Dual MPL/GPL、Proprietary(专有)等,没有内核会提示.
MODULE_AUTHOR("WangDengtao");//作者
MODULE_VERSION("V1.0");//版本

按键驱动测试文件(key_irq_test.c):

#include 
#include 
#include 
#include 
#include 
#include 
/*
 * ./led_App /dev/key0
 *
 */
#define KEY0VALUE       0XF0
#define INVAKEY         0X00

int main(int argc, char **argv)
{
	int     fd;
	unsigned char keyvalue;
    
	fd = open("/dev/key0", O_RDWR);
	if (fd == -1)
	{
		printf("can not open file %s\n");
		return -1;
	}

	while (1)
	{
		/* 3. 读文件 */
		read(fd, &keyvalue, sizeof(keyvalue));
        if (keyvalue == KEY0VALUE) 
        { 
            printf("KEY0 Press, value = %#X\r\n", keyvalue);
        }
	}
	close(fd);
	return 0;
}

Makefile:

KERNAL_DIR ?= /home/wangdengtao/imx6ull/imx6ull/bsp/kernel/linux-imx
PWD :=$(shell pwd)
obj-m := key_irq.o

CC=arm-linux-gnueabihf-gcc
APP_NAME=key_irq_test

all:
	$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
	@${CC} ${APP_NAME}.c -o ${APP_NAME}

	@make clear


clear:
	@rm -f *.o *.cmd *.mod *.mod.c
	@rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
	@rm -f .*ko.cmd .*.o.cmd .*.o.d
	@rm -f *.unsigned

clean:
	@rm -f *.ko
	@rm -f ${APP_NAME}

make之后可以看见生成驱动文件,一次可执行测试文件:

wangdengtao@wangdengtao-virtual-machine:~/wangdengtao/driver/arm$ ls
key_irq.c  key_irq.ko  key_irq_test  key_irq_test.c   Makefile

(2)运行结果

将我们的驱动加载到我们的开发板,并且测试运行。

root@igkboard:~# tftp -gr key_irq.ko 192.168.137.229
root@igkboard:~# tftp -gr key_irq_test 192.168.137.229      
root@igkboard:~# chmod a+x key_irq_test 
root@igkboard:~# ls
key_irq.ko  key_irq_test

安装我们的驱动,可以看见在 /dev 路径下生成了key0设备文件:

root@igkboard:~# insmod key_irq.ko 
root@igkboard:~# ls -l /dev/key0 
crw------- 1 root root 243, 0 Apr  1 08:49 /dev/key0
root@igkboard:~# lsmod
Module                  Size  Used by
key_irq                16384  0
rtl8188fu             999424  0
imx_rngc               16384  0
rng_core               20480  1 imx_rngc
secvio                 16384  0
error                  20480  1 secvio

执行我们的测试程序,按动开发板上的按键,就可看见按键按下的提示:

root@igkboard:~# ./key_irq_test 
KEY0 Press, value = 0XF0
KEY0 Press, value = 0XF0
KEY0 Press, value = 0XF0
KEY0 Press, value = 0XF0
KEY0 Press, value = 0XF0
root@igkboard:~# dmesg | tail -20
[  211.077576] key_irq key_irq: success to get 1 valid key
[  211.083746] success to install driver[major=243]!
[  211.286380] keyrelease
[  279.566310] keypress
[  279.686329] keyrelease
[  280.156309] keypress
[  280.336327] keyrelease
[  280.696316] keypress
[  280.816326] keyrelease
[  281.156311] keypress
[  281.296311] keyrelease
[  281.556325] keypress
[  281.686326] keyrelease

卸载驱动:

root@igkboard:~# rmmod key_irq
root@igkboard:~# lsmod
Module                  Size  Used by
rtl8188fu             999424  0
imx_rngc               16384  0
rng_core               20480  1 imx_rngc
secvio                 16384  0
error                  20480  1 secvio

四、中断的上半部分和下半部分

(1)中断上下半部分介绍

中断处理程序通常被分为两个部分:顶半部分和底半部分,也称为上半部分和下半部分。

我们在使用request_irq申请中断的中断服务函数属于中断处理的上半部,只要中断触发。那么中断处理函数就会执行。然而一些中断的产生之后需要较长的时间来处理,如由于网络传输产生的中断, 在产生网络传输中断后需要比较长的时间来处理接收或者发送数据,因为在linux下中断并不能被嵌套如果这时有其他中断产生就不能够及时的响应,为了解决这个问题,linux对中断的处理引入了“中断上半部”和 “中断下半部”的概念,在中断的上半部中只对中断做简单的处理,把需要耗时处理的部分放在中断下半部中,使得能够 对其他中断作为及时的响应,提供系统的实时性。这一概念又被称为中断分层

  • 上半部分是中断处理程序的第一部分,它在中断发生后立即执行。上半部分通常处理一些紧急的、高优先级的任务,例如更新硬件状态、保存寄存器、禁用中断等。由于上半部分需要快速执行,因此应该尽可能地简单、短暂和高效。

  • 下半部分是中断处理程序的第二部分,它在上半部分执行完毕后延迟执行。下半部分通常处理一些非紧急的、低优先级的任务,例如访问I/O设备、调度进程、释放锁等。由于下半部分可以在中断返回后再执行,因此可以较长时间地执行复杂的操作,但也需要避免过多的延迟。

上半部分和下半部分之间的通信通常通过共享数据结构或标志位实现。例如,在s上半部分中可以设置一个标志位表示需要执行某个下半部分任务,在下半部分中检查该标志位并执行相应的任务。为了确保数据的一致性和可靠性,上半部分和下半部分之间还需要避免竞态条件和锁等问题。

总之,上半部分和下半部分是中断处理程序的两个重要组成部分,合理使用它们可以提高系统的性能和稳定性。

中断分层实现方法常用的有三种,分别为软中断、tasklet、和工作队列,下面分别介绍这三种方式。

(2)软中断

软中断(Soft Interrupt)是一种由软件触发的中断机制,用于在内核空间中执行高优先级的任务。与硬中断不同,软中断不需要外部硬件的支持,可以在内核线程中实现。

软中断通常用于处理一些紧急的、高优先级的任务,例如网络数据包的接收和发送、磁盘I/O等操作。当需要执行这些任务时,内核会通过软中断机制将任务添加到工作队列中,并向CPU发送一个中断信号。CPU在适当的时候中断当前进程并执行软中断处理程序来处理这些任务。完成任务后,软中断处理程序会立即返回到原来的进程继续执行。

软中断机制存在的优点

  • 可以在内核空间中快速执行紧急任务,提高系统的响应性能和效率。
  • 不需要硬件支持,可以在各种平台上实现。
  • 与硬中断相比,软中断的开销较小,对系统资源的消耗也相对较小。

软中断机制存在的缺点

  • 软中断处理程序需要尽可能地简单和高效,否则会影响整个系统的性能。
  • 频繁地触发软中断会占用大量的CPU时间,导致其他进程的响应时间变慢。

总之,软中断机制是内核中一个重要的中断处理机制,可以在需要高优先级任务处理时快速响应和执行。但为了确保系统的性能和稳定性,开发人员需要合理地使用软中断,并针对具体应用场景进行优化和调整。

(3)tasklet

tasklet是基于软中断实现,如果对效率没有特殊要求推荐是用tasklet实现中断分层。

为什么这么说, 根据之前讲解软中断的中断服务函数是一个全局的数组,在多CPU系统中,所有CPU都可以访问, 所以在多CPU系统中需要用户自己考虑并发、可重入等问题,增加编程负担。 软中断资源非常有限一些软中断是为特定的外设准备的(不是说、只能用于特定外设)。如“NET_TX_SOFTIRQ、NET_RX_SOFTIRQ” 从名字可以看出它们用于网络的TX和RX。像网络这种对效率要求较高的场合还是会使用软中断实现中断分层的。

相比软中断,tasklet使用起来更简单,最重要的一点是在多CPU系统中同一时间只有一个CPU运行tasklet, 所以并发、可重入问题就变得很容易处理(一个tasklet甚至不用去考虑)。而且使用时也比较简单,介绍如下。

在Linux内核中,与Tasklet相关的函数包括:

  • tasklet_init():初始化一个Tasklet;
  • tasklet_schedule():将一个Tasklet加入到任务队列中等待执行;
  • tasklet_hi_schedule():将一个高优先级的Tasklet加入到任务队列中等待执行;
  • tasklet_kill():终止一个正在运行或等待执行的Tasklet;
  • tasklet_disable():禁用一个Tasklet;
  • tasklet_enable():启用一个已被禁用的Tasklet;
  • tasklet_trylock():尝试获取一个Tasklet的锁;
  • tasklet_unlock():释放一个Tasklet的锁。

此外,还有一些与 Tasklet 相关的宏定义和数据结构,例如:

  • struct tasklet_struct:Tasklet数据结构;
  • DECLARE_TASKLET():声明并定义一个Tasklet;
  • DECLARE_TASKLET_DISABLED():声明并定义一个已经禁用的Tasklet;
  • DECLARE_TASKLET_HI():声明并定义一个高优先级的Tasklet;
  • TASKLET_*_NAME():根据Tasklet类型生成名称。

下面简单介绍几个。

【1】tasklet_struct 结构体

Tasklet 数据结构是一个包含回调函数、数据参数和状态信息等成员变量的结构体,用于表示一个 Tasklet。

在驱动中使用tasklet_struct结构体表示一个tasklet,结构体定义如下所示:

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

tasklet_struct 结构体定义了 Tasklet 的五个主要成员变量:

  • next:指向下一个Tasklet的指针;
  • state:Tasklet的状态信息,例如是否被禁用或正在运行等;
  • count:Tasklet的引用计数,用于管理Tasklet的生命周期;
  • func:Tasklet的回调函数,当Tasklet需要被执行时会调用该函数;
  • data:Tasklet的数据参数,传递给回调函数的参数。

【2】初始化 tasklet

要使用tasklet,必须先定义一个tasklet然后使用tasket_init函数初始化tasklet,函数原型如下:

void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned
long data)
{
	t->next = NULL;
	t->state = 0;
	atomic_set(&t->count, 0);
	t->func = func;
	t->data = data;
}

使用 tasklet_init() 函数可以将一个 Tasklet 变量初始化为一个新的、未被安排执行的 Tasklet。在初始化后,Tasklet 变量的默认状态为已经启用并且没有被禁用。

举个栗子:

struct tasklet_struct my_tasklet;
tasklet_init(&my_tasklet, my_tasklet_fn, 0);

这将创建一个名为 my_tasklet 的 Tasklet 变量,并将其回调函数设置为 my_tasklet_fn,数据参数设置为 0,并将其初始化为一个新的可用 Tasklet。

【3】触发 tasklet

在上半部,也就中断处理函数中调用tasklet_schedule函数就能使tasklet在合适的时间运行。

将一个Tasklet加入到任务队列中等待执行:

static inline void tasklet_schedule(struct tasklet_struct *t)
{
	if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
		__tasklet_schedule(t);
}
  • t:tasklet_struct结构体,要调度的tasklet。

【4】tasklet 测试程序

tasklet_test.c代码:

/*************************************************************************
    > File Name: tasklet_test.c
    > Author: WangDengtao
    > Mail: [email protected] 
    > Created Time: 2023年04月01日 星期六 21时23分25秒
 ************************************************************************/

#include 
#include 
#include 
#include 

#define IRQ_NUM 1 // 用于本例子的虚拟中断号

static struct tasklet_struct my_tasklet;

// 中断处理程序
static irqreturn_t my_interrupt(int irq, void *dev_id)
{
    // 执行一些紧急任务
    printk(KERN_INFO "Executing some urgent tasks...\n");
    
    // 调度tasklet
    tasklet_schedule(&my_tasklet);
    return IRQ_HANDLED;
}

// tasklet处理程序
static void my_tasklet_fn(unsigned long data)
{
    printk(KERN_INFO "Tasklet executed\n");
}

static int __init my_tasklet_init(void)
{
    printk(KERN_INFO "Tasklet module loaded\n");
    
    // 初始化tasklet
    tasklet_init(&my_tasklet, my_tasklet_fn, 0);
    
    // 注册中断处理程序
    if (request_irq(IRQ_NUM, my_interrupt, IRQF_SHARED, "my_interrupt", &my_tasklet)) {
        printk(KERN_ERR "Failed to register interrupt\n");
        return -EFAULT;
    }
    
    return 0;
}

static void __exit my_tasklet_exit(void)
{
    printk(KERN_INFO "Tasklet module unloaded\n");
    
    // 停止中断处理程序
    free_irq(IRQ_NUM, &my_tasklet);
    
    // 停止tasklet
    tasklet_kill(&my_tasklet);
}

module_init(my_tasklet_init);
module_exit(my_tasklet_exit);

/*添加LICENSE和作者信息,是来告诉内核,该模块带有一个自由许可证;没有这样的说明,在加载模块的时内核会“抱怨”.*/
MODULE_LICENSE("Dual BSD/GPL");//许可 GPL、GPL v2、Dual MPL/GPL、Proprietary(专有)等,没有内核会提示.
MODULE_AUTHOR("WangDengtao");//作者
MODULE_VERSION("V1.0");//版本

Makefile:

KERNAL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
obj-m := tasklet_test.o

all:
	$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
	@make clear

clean:
	@rm -f *.ko
clear:
	@rm -f *.o *.cmd *.mod *.mod.c
	@rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
	@rm -f .*ko.cmd .*.o.cmd .*.o.d
	@rm -f *.unsigned

结果:

wangdengtao@wangdengtao-virtual-machine:~/wangdengtao/driver/x86$ sudo insmod tasklet_test.ko 
[sudo] wangdengtao 的密码: 
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao/driver/x86$ sudo rmmod tasklet_test 
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao/driver/x86$ dmesg 
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao/driver/x86$ sudo dmesg 
[  160.518190] Tasklet module loaded
[  160.540421] Executing some urgent tasks...
[  160.540427] Tasklet executed
[  162.372465] Executing some urgent tasks...
[  162.372489] Tasklet executed
[  162.452298] Executing some urgent tasks...
[  162.452321] Tasklet executed
[  162.453113] Executing some urgent tasks...
[  162.453116] Tasklet executed
[  162.572318] Executing some urgent tasks...
[  162.572343] Tasklet executed
[  162.748576] Executing some urgent tasks...
......
[  556.656698] Tasklet module unloaded

【5】相关宏定义

DECLARE_TASKLET()

DECLARE_TASKLET() 是一个宏定义,用于声明并定义一个 Tasklet。它的定义如下:

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

使用 DECLARE_TASKLET() 宏可以快速创建一个 Tasklet 变量,并将其回调函数和数据参数设置为指定的值。

该宏包含三个参数:

  • name:Tasklet 变量的名称;
  • func:Tasklet 回调函数的名称;
  • data:Tasklet 数据参数的值。

举个栗子:

DECLARE_TASKLET(my_tasklet, my_tasklet_fn, 0);

这将创建一个名为 my_tasklet 的 Tasklet 变量,并将其回调函数设置为 my_tasklet_fn,数据参数设置为 0。

DECLARE_TASKLET_DISABLED()

DECLARE_TASKLET_DISABLED() 是一个宏定义,用于声明并定义一个已经禁用的 Tasklet。它的定义如下:

#define DECLARE_TASKLET_DISABLED(name, func, data) \
    struct tasklet_struct name = { NULL, TASKLET_STATE_SCHED | TASKLET_STATE_NO_LOCK, ATOMIC_INIT(0), func, data }

使用 DECLARE_TASKLET_DISABLED() 宏可以快速创建一个已经禁用的 Tasklet 变量,并将其回调函数和数据参数设置为指定的值。
该宏包含三个参数:

  • name:Tasklet 变量的名称;
  • func:Tasklet 回调函数的名称;
  • data:Tasklet 数据参数的值。

举个栗子:

DECLARE_TASKLET_DISABLED(my_tasklet, my_tasklet_fn, 0);

这将创建一个名为 my_tasklet 的 Tasklet 变量,并将其回调函数设置为 my_tasklet_fn,数据参数设置为 0。

DECLARE_TASKLET_DISABLED() 宏会自动设置 Tasklet 的状态为 TASKLET_STATE_SCHED | TASKLET_STATE_NO_LOCK,即已经被安排执行但没有被锁定,并且处于禁用状态。

如果要手动将 Tasklet 的状态设置为 TASKLET_STATE_SCHED | TASKLET_STATE_NO_LOCK,可以使用以下代码:

my_tasklet.state = TASKLET_STATE_SCHED | TASKLET_STATE_NO_LOCK;
DECLARE_TASKLET_HI()

DECLARE_TASKLET_HI() 是一个宏定义,用于声明并定义一个高优先级的 Tasklet。它的定义如下:

#define DECLARE_TASKLET_HI(name, func, data) \
    struct tasklet_struct name = { NULL, TASKLET_STATE_SCHED | TASKLET_STATE_NO_LOCK | TASKLET_STATE_HIGHPRI, ATOMIC_INIT(0), func, data }

使用 DECLARE_TASKLET_HI() 宏可以快速创建一个高优先级的 Tasklet 变量,并将其回调函数和数据参数设置为指定的值。

该宏包含三个参数:

  • name:Tasklet 变量的名称;
  • func:Tasklet 回调函数的名称;
  • data:Tasklet 数据参数的值。

例如,以下代码片段演示了如何使用 DECLARE_TASKLET_HI() 宏来创建和初始化一个高优先级的 Tasklet:

DECLARE_TASKLET_HI(my_tasklet, my_tasklet_fn, 0);

(4)工作队列(workqueue)

与软中断和tasklet不同,工作队列运行在内核线程,允许被重新调度和睡眠。 如果中断的下部分能够接受被重新调度和睡眠,推荐使用工作队列。

下面介绍一下有关的结构体以及函数。

【1】work_struct 结构体

在驱动中一个工作结构体代表一个工作,工作结构体如下所示:

struct work_struct {
	atomic_long_t data;
	struct list_head entry;
	work_func_t func; /*工作队列处理函数*/
#ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map;
#endif
};

这些工作组织成工作队列,工作队列使用workqueue_struct结构体表示。

struct workqueue_struct {
	struct list_head pwqs;
	struct list_head list;
	struct mutex mutex;
	int work_color;
	int flush_color;
	atomic_t nr_pwqs_to_flush;
	struct wq_flusher *first_flusher;
	struct list_head flusher_queue;
	struct list_head flusher_overflow;
	struct list_head maydays;
	struct worker *rescuer;
	int nr_drainers;
	int saved_max_active;
	struct workqueue_attrs *unbound_attrs;
	struct pool_workqueue *dfl_pwq;
	char name[WQ_NAME_LEN];
	struct rcu_head rcu;
	unsigned int flags ____cacheline_aligned;
	struct pool_workqueue __percpu *cpu_pwqs;
	struct pool_workqueue __rcu *numa_pwq_tbl[];
};

Linux 内核使用工作者线程(worker thread)来处理工作队列中的各个工作。并且使用 worker 结构体表示工作者线程,worker 结构体内容如下:

struct worker {
	union {
		struct list_head entry;
		struct hlist_node hentry;
};
struct work_struct *current_work;
work_func_t current_func;
struct pool_workqueue *current_pwq;
bool desc_valid;
struct list_head scheduled;
struct task_struct *task;
struct worker_pool *pool;
struct list_head node;
unsigned long last_active;
unsigned int flags;
int id;
char desc[WORKER_DESC_LEN];
struct workqueue_struct *rescue_wq;
};

每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。

【2】初始化 workqueue

#define INIT_WORK(_work, _func)
  • _work:用于指定要初始化的工作结构体;
  • _func:用于指定工作的处理函数。

也可以使用DECLARE_WOEK宏一次性完成工作的创建和初始化:

#define DECLARE_WORK(n,f)
  • n:表示定义的工作
  • f:表示工作对应的处理函数

【3】启动/关闭 workqueue

驱动工作函数执行后相应内核线程将会执行工作结构体指定的处理函数,驱动函数如下所示。

static inline bool schedule_work(struct work_struct *work)

移除工作队列:

static inline cancel_work_sync(struct work_struct *work);
  • work:要调度的工作

【4】workqueue 测试程序

/*************************************************************************
    > File Name: tasklet_test.c
    > Author: WangDengtao
    > Mail: [email protected] 
    > Created Time: 2023年04月01日 星期六 21时23分25秒
 ************************************************************************/

#include 
#include 
#include 
#include 
#include 
#include 

#define IRQ_NUM 1 // 用于本例子的虚拟中断号
	static struct work_struct my_workqueue;

// 中断处理程序
static irqreturn_t my_interrupt(int irq, void *dev_id)
{

    // 执行一些紧急任务
    printk(KERN_INFO "Executing some urgent tasks...\n");
    
    // 调度workqueue
    schedule_work(&my_workqueue);
    return IRQ_HANDLED;
}

// 工作队列处理程序
static void my_workqueue_fn(struct work_struct *t)
{

    printk(KERN_INFO "workqueue executed\n");
}

static int __init my_workqueue_init(void)
{
    printk(KERN_INFO "workqueue module loaded\n");
    
    // 初始化workqueue
    INIT_WORK(&my_workqueue, my_workqueue_fn);
    
    // 注册中断处理程序
    if (request_irq(IRQ_NUM, my_interrupt, IRQF_SHARED, "my_interrupt", &my_workqueue)) 
	{
        printk(KERN_ERR "Failed to register interrupt\n");
        return -EFAULT;
    }
    return 0;
}

static void __exit my_workqueue_exit(void)
{
    printk(KERN_INFO "workqueue module unloaded\n");
    
    // 停止中断处理程序
    free_irq(IRQ_NUM, &my_workqueue_fn);
    
    // 停止workqueue
	cancel_work_sync(&my_workqueue);
}

module_init(my_workqueue_init);
module_exit(my_workqueue_exit);

/*添加LICENSE和作者信息,是来告诉内核,该模块带有一个自由许可证;没有这样的说明,在加载模块的时内核会“抱怨”.*/
MODULE_LICENSE("Dual BSD/GPL");//许可 GPL、GPL v2、Dual MPL/GPL、Proprietary(专有)等,没有内核会提示.
MODULE_AUTHOR("WangDengtao");//作者
MODULE_VERSION("V1.0");//版本 

Makefile:

KERNAL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
obj-m := workqueue_test.o

all:
	$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
	@make clear

clean:
	@rm -f *.ko
clear:
	@rm -f *.o *.cmd *.mod *.mod.c
	@rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
	@rm -f .*ko.cmd .*.o.cmd .*.o.d
	@rm -f *.unsigned

结果:

[  187.083640] workqueue module loaded
[  187.099458] Executing some urgent tasks...
[  187.099491] workqueue executed
[  187.219689] Executing some urgent tasks...
[  187.219715] workqueue executed
[  187.283704] Executing some urgent tasks...
[  187.283713] workqueue executed
[  187.435710] Executing some urgent tasks...
[  187.435768] workqueue executed
[  187.483435] Executing some urgent tasks...
[  187.483443] workqueue executed

你可能感兴趣的:(#,驱动开发,linux,嵌入式硬件,c语言,物联网)