简介
中断这块是个重点,结合了很多之前已经讨论过的知识点,个人觉得是一个大的应用,这里把逻辑和代码都捋一下,留个记录。
驱动框架
框架部分还是Linux底层驱动中的字符设备框架,然后在此基础上添加中断部分,在中断中为了消抖,使用了定时器。同时,为了保护数据,使用了原子操作。下面就是具体实现。代码部分参考了原子的教程,特此注明一下。
字符设备驱动
字符设备驱动框架就是一入一出,非常的简单:
module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
传入的参数就是个函数,框架内这样:
static int __init imx6uirq_init(void);
static void __exit imx6uirq_exit(void);
注意分别有关键字**__init以及__exit**修饰。之后开始写这入口和出口函数。
驱动入口
驱动入口就是
static int __init imx6uirq_init(void);
的实现。
框架中,驱动入口需要做5个事情,分别是
- 注册字符设备驱动;
- 初始化cdev
- 添加cdev
- 创建类
- 创建设备
当然,在框架中,我们需要一个结构体来描述字符设备的属性,所以框架实际还需要包含字符设备使用的结构体:
sturct imx6uirq_dev{
dev_t devid; //设备号
int major; //主设备号,devid的高12位
int minor; //次设备号,devid的低20位
struct cdev cdev; //字符设备结构体
struct class* class; //类结构体
struct device* device; //设备结构体
struct device_node* nd; //节点结构体
};
入口函数要做的事情,就是初始化这个结构体里的每个成员变量,让设备可以使用。之后就是一步一步实现。
注册字符设备驱动
直接上代码:
if(imx6uirq.major){
imx6uirq.devid = MKDEV(imx6uirq.major, 0);
ret = register_chrdev_region(imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
} else {
ret = alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
imx6uirq.major = MAJOR(imx6uirq.devid);
imx6uirq.minor = MINOR(imx6uirq.devid);
}
if (ret < 0){
goto fail_devid;
}
这里处理了一下给定设备号和未给定设备号的情况。其实主设备号和次设备号可以不用处理,之前说过主设备号是devid的高12位,次设备号是低20位。一旦通过alloc_chrdev_region
注册了设备号,major
和minor
都被隐形的确定了。这里这么写是为了分析。实际工作中不需要。
初始化cdev
这个实际就是初始化字符设备结构体成员cdev的过程。注意cdev本身也是个结构体。代码很简单,有接口函数,调用一下就可以了。
imx6uirq.cdev.owner = THIS_MODULE;
cdev_init(&imx6uirq.cdev, &imx6uirq_fops);
添加cdev
初始化完成后,需要把cdev添加到系统中,也是使用API
ret = cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);
if(ret < 0){
goto fail_cdevadd;
}
添加类
所谓的类,实际是内核中的一个概念,它所扮演的角色是设备的大管家,位置在内核中。class
可以包含多个具体设备,因此就是设备类。
imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME);
if(IS_ERR(imx6uirq.class)){
ret = PTR_ERR(imx6uirq.class);
goto fail_class;
}
需要访问设备的时候,必须指定类
添加设备
device用于表示一个具体的设备结构体,通常就是个硬件设备。需要创建一个具体的设备时,必须先创建好对应的类。
imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME);
if(IS_ERR(imx6uirq.device)){
ret = PTR_ERR(imx6uirq.device);
goto fail_device;
}
至此,一个简单的字符设备框架的入口就完成了。
字符设备出口
出口,就是实现
static void __exit imx6uirq_exit(void);
依据入口中各个模块的初始化和添加顺序,反过来就可以:
static void __exit imx6uirq_exit(void)
{
device_destroy(imx6uirq.class, imx6uirq.devid);
class_destroy(imx6uirq.class);
cdev_del(&imx6uirq.cdev);
unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);
}
中断部分
裸机里处理中断是这样:
- 使能中断,初始化对应的寄存器
- 注册中断服务函数,也就是向irqtable数值的指定标号处吸入中断服务函数
- 中断发生后进入IRQ中断服务函数,然后执行
设备树中断信息节点
Linux中,需要把中断先添加进设备树,然后再编写中断驱动。所以在实际写驱动之前,需要先在设备树内添加对应的中断。例如,需要使用按键KEY0的中断,需要添加:
key{
#address-cells = <1>;
#size-cells = <1>;
compatible = "Femtomes-key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
status = "okay"
};
感兴趣的可以自行查阅中断控制器对应的设备树(intc节点)。
中断添加和处理
以下的代码均是需要添加在上面的字符设备驱动框架中的。在添加设备完成后,需要初始化IO。初始化IO的过程比较复杂,写成独立的函数,然后在驱动入口调用即可。
在做处理之前,首先添加一下需要用到的宏定义:
#define KEY_NUM 1
#define KEY0VALUE 0x01
#define INVAKEY 0xFF
然后在字符设备结构体中添加:
struct irq_keydesc irqkey[KEY_NUM];
struct timer_list timer;
atomic_t keyvalue;
atomic_t releasekey;
由于可能会有多个按键,每个按键也可能需要不同的irq_keydesc来描述:
struct irq_keydesc{
int gpio; //IO编号
int irqnum; //中断号
unsigned char value; //键值
char name[10]; //名称
irqreturn_t(*handler)(int,void*); //中断处理函数
}
这里注意,irq_keydesc
是作为imx6uirq_dev
的成员变量存在的!
添加到入口的代码:
ret = keyio_init(&imx6uirq);
函数实现:
static int keyio_init(struct imx6uirq_dev* dev)
{
int ret = 0;
int i = 0;
/*1. 按键初始化 */
dev->nd = of_find_node_by_path("/key");
if(dev->nd == NULL){
ret = -EINVAL;
goto fail_nd;
}
for(i = 0; i < KEY_NUM; i++)
{
dev->irqkey[i].gpio = of_get_named_gpio(dev->nd, "key-gpios", i);
if(dev->irqkey[i].gpio < 0){
printk(" Can't get key%d\r\n",i);
}
}
for(i = 0; i < KEY_NUM ; i++)
{
memset(dev->irqkey[i].name, 0, sizeof(dev->irqkey[i].name));
sprintf(dev->irqkey[i].name, "KEY%d", i);
gpio_request(imx6uirq.irqkey[i].gpio, dev->irqkey[i].name);
gpio_direction_input(dev->irqkey[i].gpio);
dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio); /* 获取中断号 */
//dev->irqkey[i].irqnum = irq_of_parse_and_map(dev->nd, i); /* 通用获取函数 */
}
dev->irqkey[0].handler = key0_handler;
dev->irqkey[0].value = KEY0VALUE;
/*2. 按键中断初始化 */
for(i = 0; i< KEY_NUM; i++)
{
ret = request_irq(dev->irqkey[i].irqnum, dev->irqkey[i].handler,
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
dev->irqkey[i].name, &imx6uirq);
if(ret){
printk("irq %d request error!\r\n", dev->irqkey[i].irqnum);
goto fail_irq;
}
}
/* 3. 初始化定时器 */
init_timer(&imx6uirq.timer);
imx6uirq.timer.function = timer_func;
return 0;
fail_irq:
for(i = 0; i < KEY_NUM ;i++){
gpio_free(imx6uirq.irqkey[i].gpio);
}
fail_nd:
return ret;
}
keyio_init
中调用的key0_handler
,也就是中断服务函数:
static irqreturn_t key0_handler(int irq, void* dev_id)
{
#if 0
int val = 0;
struct imx6uirq_dev *dev = dev_id;
val = gpio_get_value(dev->irqkey[0].gpio);
if(val == 0){ /* 按下 */
printk("KEY0 push!\r\n");
} else if(val == 1){ /**/
printk("KEY0 release!\r\n");
}
#endif
struct imx6uirq_dev *dev = dev_id;
dev->timer.data = (volatile unsigned long)dev_id;
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));
return IRQ_HANDLED;
}
keyio_init
中调用的timer_func
,也就是定时器服务函数:
static void timer_func(unsigned long arg)
{
int val = 0;
struct imx6uirq_dev* dev = (struct imx6uirq_dev*)arg;
val = gpio_get_value(dev->irqkey[0].gpio);
if(val == 0){ /* 按下 */
atomic_set(&dev->keyvalue, dev->irqkey[0].value);
printk("KEY0 push!\r\n");
} else if(val == 1){ /* 释放 */
atomic_set(&dev->keyvalue, 0x80 | (dev->irqkey[0].value));
atomic_set(&dev->releasekey, 1);
printk("KEY0 release!\r\n");
}
}
这里简单描述一下逻辑。我们要实现定时器消抖,那么在按下按键的时候,定时器开始工作,延迟一段时间后(代码中是10ms)读取键值,如果还是按下,那么就认为是一次有效的按压。