前言:
本章我们就来学习一 下如何在 Linux 下使用中断。在linux内核里面使用中断,不同于我们以往在别的裸机开发一样,需要进行各种寄存器的配置,中断使能之类的,而在Linux 内核中,提供了完善的中断框架,我们只需要申请中断,然后注 册中断处理函数即可,使用非常方便。
一、Linux 中断 API 函数
1、中断号
每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中断线。在 Linux 内核中使用一个 int 变量表示中断号。
2、request_irq函数:
函数原型:
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev);
参数分析:
- irq:要申请中断的中断号。
- handler:中断处理函数,当中断发生以后就会执行此中断处理函数。
- flags:中断标志,可以在文件 include/linux/interrupt.h 里面查看所有的中断标志
- name:中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字。
- dev:如果将 flags 设置为 IRQF_SHARED 的话,dev 用来区分不同的中断,一般情况下将 dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
- 返回值:0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY 的话表示中断已经 被申请了。
顺便介绍几个常用的中断标志,如表所示:
3.free_irq 函数:
函数原型:
void free_irq(unsigned int irq, void *dev);
函数参数和返回值含义如下:
- irq:要释放的中断。
- dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断 只有在释放最后中断处理函数的时候才会被禁止掉。
- 返回值:无。
4.中断处理函数:
使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:
irqreturn_t (*irq_handler_t) (int, void *);
- 第一个参数是要中断处理函数要相应的中断号。
- 第二个参数是一个指向 void 的指针,也就是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备, dev 也可以指向设备数据结构。
- 中断处理函数的返回值为 irqreturn_t 类型
二、了解中断初始化驱动的步骤。
1 .修改设备树
这里是基于之前按键的程序修改的,也就是在之前设备树的基础上修改就好。这里是在根节点下添加的。
key {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
status = "okay";
};
在上面基础上添加如下:
(1)设置 interrupt-parent 属性值,例如按键的中断,设置属性值为”gpio1”,因为 KEY0 所使用的 GPIO 为GPIO1_IO18,也就是设置 KEY0 的 GPIO 中断控制器为 gpio1。
(2)设置 interrupts 属性,也就是设置中断源。
2.编写字符型驱动框架。
- (1)直接选取之前的字符设备驱动框架,在那份代码的基础上来进行修改。
3.定义按键信息结构体
- (1)首先定义一个结构体,来保存有关key中断的一些信息。
/* 中断IO描述结构体 */
struct irq_keydesc {
int gpio; /* gpio *
int irqnum; /* 中断号 */
unsigned char value; /* 按键对应的键值 */
char name[10]; /* 名字 */
irqreturn_t (*handler)(int, void *); /* 中断服务函数 */
};
-
4.编写按键中断初始化程序。
- 步骤如下:
- ①使用of_find_node_by_path函数通过环境名获取节点。
- ②使用of_get_named_gpio函数通过节点得到gpio的name编号。
- ③使用gpio_request函数通过gpio的name编号去注册gpio
- ④使用gpio_direction_input函数去设置gpio的方向,是输入还是输出啊。
- ⑤使用irq_of_parse_and_map函数从 interupts 属性中提取到对应的中断号
- ⑥使用request_irq函数去申请注册中断。同时指定我们的中断处理函数。
-
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
- ⑦编写中断处理函数,函数原型:
-
static irqreturn_t key0_handler(int irq, void *dev_id);
5.编写按键释放程序
①使用free_irq函数去释放中断。
三、驱动代码的编写。
1.修改设备树
key {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* FALLING RISING */
status = "okay";
};
2.编写字符型驱动框架。
这里就直接使用之前的字符设备驱动的框架就可以了,把那些函数名,还有结构体名字之类的改一改就好,后面也会贴出完整的代码。
3.首先定义一个结构体,来保存有关key中断的一些信息。
struct irq_keydesc
{
int gpio_num; /* gpio号 */
int irq_num; /* 中断号 */
char gpio_name[10]; /* gpio名字 */
unsigned char key_value; /*按键对应键值*/
irqreturn_t (*handle)(int ,void *); /*终端服务函数 */
};
-
4.编写按键中断初始化程序。
- 步骤如下:
- ①使用of_find_node_by_path函数通过环境名获取节点。
-
/*使用of_find_node_by_path函数通过环境名获取节点。*/ dev->nd = of_find_node_by_path("/key"); if(dev->nd == NULL) { printk("key node not find!\r\n"); return -EINVAL; }
- ②使用of_get_named_gpio函数通过节点得到gpio的name编号。
-
/*使用of_get_named_gpio函数通过节点得到gpio的name编号。*/ dev->irqkeydesc[0].gpio_num = of_get_named_gpio(dev->nd,"gpios",0); if(dev->irqkeydesc[0].gpio_num < 0){ printk("can't get key0\r\n"); }
- ③使用gpio_request函数通过gpio的name编号去注册gpio
-
/*使用gpio_request函数通过gpio的name编号,以及设置中断名去注册gpio*/ memset(dev->irqkeydesc[0].gpio_name,0,sizeof(dev->irqkeydesc[0].gpio_name)); sprintf(dev->irqkeydesc[0].gpio_name, "KEY0"); gpio_request(dev->irqkeydesc[0].gpio_num,dev->irqkeydesc[0].gpio_name);
- ④使用gpio_direction_input函数去设置gpio的方向,是输入还是输出啊。
-
/*使用gpio_direction_input函数去设置gpio的方向,是输入还是输出啊。*/ gpio_direction_input(dev->irqkeydesc[0].gpio_num);
- ⑤使用irq_of_parse_and_map函数从 interupts 属性中提取到对应的中断号。
-
/*使用irq_of_parse_and_map函数从 interupts 属性中提取到对应的中断号*/ dev->irqkeydesc[0].irq_num=irq_of_parse_and_map(dev->nd,0); printk("key0:gpio=%d, irqnum=%d\r\n", dev->irqkeydesc[0].gpio_num, dev->irqkeydesc[0].irq_num);
- ⑥使用request_irq函数去申请注册中断。同时指定我们的中断处理函数。
-
/*使用request_irq函数去申请注册中断。同时指定我们的中断处理函数。*/ dev->irqkeydesc[0].handle = key0_handler; dev->irqkeydesc[0].key_value = KEY0_VALUE; ret = request_irq(dev->irqkeydesc[0].irq_num, dev->irqkeydesc[0].handle, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, dev->irqkeydesc[0].gpio_name, &imx6uirq); if(ret < 0){ printk("irq %d request failed!\r\n", dev->irqkeydesc[0].irq_num); return -EFAULT; }
5.编写中断释放程序
①使用free_irq函数去释放中断。
free_irq(imx6uirq.irqkeydesc[0].irq_num,&imx6uirq);
gpio_free(imx6uirq.irqkeydesc[0].gpio_num);
6.完整驱动代码:
/**************头文件区域*********************************************************/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/major.h>
#include <linux/gfp.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/string.h>
/**********************************************************************************/
/************************宏定义-begin***********************************************/
#define DEVICE_CNT 1 /* 设备号个数 */
#define DEVICE_NAME "imx6uirq" /* 名字 */
#define KEY_CNT 1 /* 按键数量*/
#define KEY0_VALUE 0X01 /*按键0对应键值*/
#define INVAKEY_VALUE 0XFF /*无按键对应键值*/
/************************宏定义-end********************************************/
/************************结构体定义-begin***********************************************/
struct irq_keydesc
{
int gpio_num; /* gpio号 */
int irq_num; /* 中断号 */
char gpio_name[10]; /* gpio名字 */
unsigned char key_value; /*按键对应键值*/
irqreturn_t (*handle)(int ,void *); /*终端服务函数 */
};
/* imx6uirql设备信息结构体 */
struct char_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct device_node *nd; /* 设备节点 */
struct irq_keydesc irqkeydesc[KEY_CNT]; /*按键信息结构体数组*/
struct timer_list timer; /*timer结构体*/
atomic_t keyvalue; /* 有效的按键键值 */
atomic_t releasekey; /* 标记是否完成一次完成的按键,包括按下和释放 */
};
struct char_dev imx6uirq;
/************************结构体定义-end***********************************************/
/*----------------------------------------------------------------------------------------------------*/
/************************按键中断服务函数***********************************************/
static irqreturn_t key0_handler(int irq, void *dev_id)
{
struct char_dev *dev = (struct char_dev *)dev_id;
dev->timer.data = (volatile long)dev_id;
mod_timer(&dev->timer,jiffies + msecs_to_jiffies(10));
return IRQ_RETVAL(IRQ_HANDLED);
}
/************************定时器中断服务函数***********************************************/
void timer_function(unsigned long arg)
{
unsigned char value;
struct char_dev *dev=(struct char_dev *)arg;
struct irq_keydesc*keydesc;
keydesc = &dev->irqkeydesc[0];
value=gpio_get_value(keydesc->gpio_num);
if(value == 0){
atomic_set(&dev->keyvalue,keydesc->key_value);
}
else{
atomic_set(&dev->keyvalue,0x80 | keydesc->key_value);
atomic_set(&dev->releasekey, 1); /* 标记松开按键,即完成一次完整的按键过程 */
}
}
/*************按键gpio初始化函数**************************/
static int keyio_init(struct char_dev *dev)
{
int ret;
/*使用of_find_node_by_path函数通过环境名获取节点。*/
dev->nd = of_find_node_by_path("/key");
if(dev->nd == NULL)
{
printk("key node not find!\r\n");
return -EINVAL;
}
/*使用of_get_named_gpio函数通过节点得到gpio的name编号。*/
dev->irqkeydesc[0].gpio_num = of_get_named_gpio(dev->nd,"gpios",0);
if(dev->irqkeydesc[0].gpio_num < 0){
printk("can't get key0\r\n");
}
/*使用gpio_request函数通过gpio的name编号,以及设置中断名去注册gpio*/
memset(dev->irqkeydesc[0].gpio_name,0,sizeof(dev->irqkeydesc[0].gpio_name));
sprintf(dev->irqkeydesc[0].gpio_name, "KEY0");
gpio_request(dev->irqkeydesc[0].gpio_num,dev->irqkeydesc[0].gpio_name);
/*使用gpio_direction_input函数去设置gpio的方向,是输入还是输出啊。*/
gpio_direction_input(dev->irqkeydesc[0].gpio_num);
/*使用irq_of_parse_and_map函数从 interupts 属性中提取到对应的中断号*/
dev->irqkeydesc[0].irq_num=irq_of_parse_and_map(dev->nd,0);
printk("key0:gpio=%d, irqnum=%d\r\n", dev->irqkeydesc[0].gpio_num,
dev->irqkeydesc[0].irq_num);
/*使用request_irq函数去申请注册中断。同时指定我们的中断处理函数。*/
dev->irqkeydesc[0].handle = key0_handler;
dev->irqkeydesc[0].key_value = KEY0_VALUE;
ret = request_irq(dev->irqkeydesc[0].irq_num,
dev->irqkeydesc[0].handle,
IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
dev->irqkeydesc[0].gpio_name, &imx6uirq);
if(ret < 0){
printk("irq %d request failed!\r\n", dev->irqkeydesc[0].irq_num);
return -EFAULT;
}
return 0;
}
/*************定时器初始化函数********************************************/
void mytimer_init(struct char_dev *dev)
{
init_timer(&dev->timer);
dev->timer.function = timer_function;
}
/*****实现对应的open/read/write等函数,填入file_operations结构体************/
/******************实现对应的read函数************************************/
static ssize_t imx6uirq_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
int ret = 0;
unsigned char keyvalue = 0;
unsigned char releasekey = 0;
struct char_dev *dev = (struct char_dev *)file->private_data;
keyvalue = atomic_read(&dev->keyvalue);
releasekey = atomic_read(&dev->releasekey);
if(releasekey)
{
if(keyvalue & 0x80)
{
keyvalue &= ~0x80;
ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
}
else
{
goto data_error;
}
atomic_set(&dev->releasekey, 0);/* 按下标志清零 */
}
else
{
goto data_error;
}
return 0;
data_error:
return -EINVAL;
}
/******************实现对应的write函数************************************/
static ssize_t imx6uirq_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
return 0;
}
/******************实现对应的open函数************************************/
static int imx6uirq_drv_open (struct inode *node, struct file *file)
{
file->private_data = &imx6uirq; /* 设置私有数据 */
return 0;
}
/******************实现对应的close函数************************************/
static int imx6uirq_drv_close (struct inode *node, struct file *file)
{
return 0;
}
/********************定义自己的file_operations结构体****************************/
static struct file_operations imx6uirq_drv = {
.owner = THIS_MODULE,
.open = imx6uirq_drv_open,
.read = imx6uirq_drv_read,
.write = imx6uirq_drv_write,
.release = imx6uirq_drv_close,
};
/**************************注册驱动程序******************************************/
/* 把file_operations结构体告诉内核:注册驱动程序 */
/* 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */
static int __init imx6uirq_init(void)
{
/* 动态注册字符设备的流程一般如下:
1.调用 alloc_chrdev_region() 函数申请设备编号。
2.使用 cdev_init() 函数初始化设备描述结构体。
3.使用 cdev_add() 函数将设备号与设备描述结构体关联起来,注册字符设备驱动。
4.使用 class_create() 函数创建一个设备类.
5.使用 device_create() 函数创建一个设备
*/
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
/*1 创建设备号
根据是否定义了设备号,通过条件判断选择不同的创建方式。
如果定义了设备号,则使用MKDEV宏将主设备号和次设备号合成为设备号,并调用register_chrdev_region()函数注册字符设备号。
如果没有定义设备号,则使用alloc_chrdev_region()函数动态分配设备号,并通过MAJOR和MINOR宏获取分配得到的主设备号和次设备号。*/
if (imx6uirq.major)
{ /* 定义了设备号 */
imx6uirq.devid = MKDEV(imx6uirq.major, 0);
/*register_chrdev_region() 是 Linux 内核中用于向系统申请指定范围内的字符设备编号的函数*/
register_chrdev_region(imx6uirq.devid, DEVICE_CNT, DEVICE_NAME);
}
else
{ /* 没有定义设备号 */
alloc_chrdev_region(&imx6uirq.devid, 0, DEVICE_CNT, DEVICE_NAME); /* 申请设备号 */
imx6uirq.major = MAJOR(imx6uirq.devid); /* 获取分配号的主设备号 */
imx6uirq.minor = MINOR(imx6uirq.devid); /* 获取分配号的次设备号 */
}
/* 2 初始化cdev
设置cdev结构体的拥有者为当前模块(THIS_MODULE),然后使用 cdev_init() 函数初始化cdev结构体。
参数包括待初始化的cdev结构体和用于操作该设备的file_operations结构体(imx6uirq_drv) */
imx6uirq.cdev.owner = THIS_MODULE;
cdev_init(&imx6uirq.cdev, &imx6uirq_drv);
/* 3、添加一个cdev */
cdev_add(&imx6uirq.cdev, imx6uirq.devid, DEVICE_CNT);
/*4 创建设备类
使用 class_create() 函数创建一个设备类,设备类用于在/sys/class目录下创建子目录,以组织同一类设备的相关信息。
该函数的参数包括所属的模块(THIS_MODULE)和设备类的名称(imx6uirq_NAME)。
如果创建失败,IS_ERR() 函数将返回true,表示出错,此时使用 PTR_ERR() 函数返回错误码。 */
imx6uirq.class = class_create(THIS_MODULE, DEVICE_NAME);
if (IS_ERR(imx6uirq.class)) {
return PTR_ERR(imx6uirq.class);
}
/*5 创建设备
使用 device_create() 函数创建一个设备,并在/dev目录下创建相应的设备节点。
参数包括设备所属的类(imx6uirq.class)、父设备(NULL,如果没有父设备)、设备号(imx6uirq.devid)、设备私有数据(NULL,一般为设备驱动程序提供一个指针)和设备名称(imx6uirq_NAME)。
如果创建失败,IS_ERR() 函数将返回true,表示出错,此时使用 PTR_ERR() 函数返回错误码。 */
imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, DEVICE_NAME);
if (IS_ERR(imx6uirq.device)) {
return PTR_ERR(imx6uirq.device);
}
atomic_set(&imx6uirq.keyvalue, 0XFF);
atomic_set(&imx6uirq.releasekey, 0);
keyio_init(&imx6uirq);
mytimer_init(&imx6uirq);
printk("imx6uirq init!\r\n");
return 0;
}
/************有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数****************/
static void __exit imx6uirq_exit(void)
{
del_timer_sync(&imx6uirq.timer);
free_irq(imx6uirq.irqkeydesc[0].irq_num,&imx6uirq);
gpio_free(imx6uirq.irqkeydesc[0].gpio_num);
/*在模块卸载时,使用 cdev_del() 函数注销字符设备驱动,并使用 unregister_chrdev_region() 函数释放设备号资源。*/
/* 注销字符设备驱动 */
cdev_del(&imx6uirq.cdev);/* 删除cdev */
unregister_chrdev_region(imx6uirq.devid, DEVICE_CNT); /* 注销设备号 */
device_destroy(imx6uirq.class, imx6uirq.devid);// 销毁设备,删除相应的设备节点
class_destroy(imx6uirq.class);// 销毁设备类,释放相关资源
printk("imx6uirq_drv exit!\r\n");
}
/*****************其他完善:提供设备信息**********************/
module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("oudafa");
MODULE_LICENSE("GPL");
四、应用程序的编写.
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "linux/ioctl.h"
/*
* @description : main主程序
* @param - argc : argv数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd;
int ret = 0;
char *filename;
unsigned char data;
if (argc != 2) {
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if (fd < 0) {
printf("Can't open file %s\r\n", filename);
return -1;
}
while (1) {
ret = read(fd, &data, sizeof(data));
if (ret < 0) { /* 数据读取错误或者无效 */
} else { /* 数据读取正确 */
if (data) /* 读取到数据 */
printf("key value = %#X\r\n", data);
}
}
close(fd);
return ret;
}
五、实验测试
测试流程和之前一样,先使用make命令生成.ko文件还有APP这 两 个 文 件,然后拷贝到nfs挂载的rootfs/lib/modules/4.1.15 目录中,重启开发板。
先进入/lib/modules/4.1.15 目录,输入如下命令加载驱动模块:
insmod imx6uirq.ko
驱动加载成功以后可以通过查看/proc/interrupts 文件来检查一下对应的中断有没有被注册 上,输入如下命令:
cat /proc/interrupts
可以看到如下:
从图可以看出 imx6uirq.c 驱动文件里面的 KEY0 中断已经存在了,触发方式为跳边沿(Edge),中断号为 49。
接下来使用如下命令来测试中断:
./imx6uirqApp /dev/imx6uirq
按下开发板上的 KEY0 键,终端就会输出按键值,如图所示: