Linux字符时设备驱动 中断处理 按键

1. Linux异常处理体系结构

Linux异常处理体系结构使用主要分成两步:
1、使用函数init_IRQ()初始化中断体系结构,源代码在arch/arm/kernel/irq.c中。
2、用户使用函数request_irq()向内核注册中断处理函数,也就是通过中断号找到irq_desc数组项,将中断函数添加到action链表中。

作者分析的内核版本为2.6.22.6。

1.1 Linux中断处理体系结构初始化

Linux内核将所有中断统一编号,使用一个irq_desc结构数组描述,每个数组项对应一个中断,irq结构数据类型在include/linux/irq.h中定义,如下:

struct irq_desc {
	irq_flow_handler_t	handle_irq;	/* 当前中断处理函数入口 */
	struct irq_chip		*chip;		/* 底层硬件访问 */
	struct msi_desc		*msi_desc;	
	void				*handler_data;
	void				*chip_data;
	struct irqaction	*action;	/* 用户提供的中断处理函数链表 */
	unsigned int		status;		/* IRQ 状态 */

	unsigned int		depth;		/* nested irq disables */
	unsigned int		wake_depth;	/* nested wake enables */
	unsigned int		irq_count;	/* For detecting broken IRQs */
	unsigned int		irqs_unhandled;
	spinlock_t			lock;
#ifdef CONFIG_SMP
	cpumask_t			affinity;
	unsigned int		cpu;
#endif
#if defined(CONFIG_GENERIC_PENDING_IRQ) || defined(CONFIG_IRQBALANCE)
	cpumask_t			pending_mask;
#endif
#ifdef CONFIG_PROC_FS
	struct proc_dir_entry	*dir;
#endif
	const char			*name;	/* 中断名称 */
} ____cacheline_internodealigned_in_smp;

在handle.c文件中(在/kernel/irq中定义),定义了irq_desc结构数组,如下:

struct irq_desc irq_desc[NR_IRQS]

#define NR_IRQS	128

irq_desc结构数组中成员hanle_irq结构,它包括操作底层硬件的函数,如清除、屏蔽或者重新使能中断,其类型在include/linux/irq.h中定义,如下:

struct irq_chip {
	const char		*name;
	unsigned int	(*startup)(unsigned int irq);	/* 启动中断,如果不设置,缺省为enable */
	void			(*shutdown)(unsigned int irq);	/* 关闭中断,如果不设置,缺省为disable */
	void			(*enable)(unsigned int irq);	/* 使能中断,如果不设置,缺省为unmask */
	void			(*disable)(unsigned int irq);	/* 禁止中断,如果不设置,缺省为disable */
	
	void			(*ack)(unsigned int irq);		/* 响应中断,通常是清楚当前中断使得可以接受下一个中断 */
	void			(*mask)(unsigned int irq);		/* 屏蔽中断源 */
	void			(*mask_ack)(unsigned int irq);	/* 屏蔽和响应中断 */
	void			(*unmask)(unsigned int irq);	/* 开启中断源 */
	void			(*eoi)(unsigned int irq);

	void			(*end)(unsigned int irq);
	void			(*set_affinity)(unsigned int irq, cpumask_t dest);
	int				(*retrigger)(unsigned int irq);
	int				(*set_type)(unsigned int irq, unsigned int flow_type);
	int				(*set_wake)(unsigned int irq, unsigned int on);

#ifdef CONFIG_IRQ_RELEASE_METHOD
	void			(*release)(unsigned int irq, void *dev_id);
#endif

	const char		*typename;
};

irq_desc结构数组中成员irqaction结构类型,他用来表示用户注册的中断处理函数,若有多个处理函数,他们链接成一个链表,以action为表头。irqaction结构类型在include/linux/interrupt.h,如下:

struct irqaction {
	irq_handler_t 	 handler;	/* 用户注册的中断处理函数 */
	unsigned long 	 flags;		/* 中断标志,如是否共享中断,电平触发还是边沿触发等 */
	cpumask_t 		 mask;		/* 用于SMP(对称多处理器系统) */
	const char 		 *name;		/* 用户注册中断名字,cat /proc/interrupts 能看到 */
	void 			 *dev_id;	/* 用户传给上面handler的参数,可以区分共享中断 */
	struct irqaction *next;		/**/
	int 			 irq;		/* 中断号 */
	struct proc_dir_entry *dir;	/**/
};

irq_desc结构数组、struct irq_chip *chip、struct irqaction *action这三个数据结构构成中断体系的架构,其关系如下:
Linux字符时设备驱动 中断处理 按键_第1张图片
Linux内核启动后,进入start_kernel函数(源代码在init/main.c),该函数会调用函数init_IRQ()初始化中断处理体系结构(源代码在arch/arm/kernel/irq.c),也就是构造上面的中断体系的架构,init_IRQ()函数源代码如下:

void __init init_IRQ(void)
{
	int irq;

	for (irq = 0; irq < NR_IRQS; irq++)
		irq_desc[irq].status |= IRQ_NOREQUEST | IRQ_NOPROBE;

#ifdef CONFIG_SMP
	bad_irq_desc.affinity = CPU_MASK_ALL;
	bad_irq_desc.cpu = smp_processor_id();
#endif
	init_arch_irq();
}

首先会初始化irq_desc结构数组的中断状态,然后调用函数init_arch_irq(),该函数由不同厂家的芯片决定,不同的芯片会执行不同的函数,作者使用的是开发板JZ2440V3,对应S3C2440芯片,会执行函数s3c24xx_init_irq()(在arch/arm/plat-s3c2440xx/irq.c)
函数s3c24xx_init_irq()会设置跟S3C2440芯片相关的数据结构,如设置irq_desc结构数组中处理函数入口,定义irq_chip 结构体等,并把他们挂接到irq_desc结构数组。

1.2 用户注册中断处理函数

初始化完中断体系架构后,就需要用户注册中断处理函数。用户通过request_irq函数向内核注册中断处理函数,该函数根据中断号找到irq_desc数组项,然后将中断处理函数挂接到在action链表中。
request_irq()函数在kernel/irqmanage.c中定义,函数原型如下:

/* 函数:request_irq()
 * 描述:向内核注册中断处理函数
 * 参数:     irq:中断号
 *       handler:中断处理函数句柄
 *      irqflags:中断触发方式
 *       devname:中断名称
 *        dev_id:用户可以自己指定,也可以为空
 * 返回: 
 */
int request_irq(unsigned int  irq, 
				irq_handler_t handler,
				unsigned long irqflags,
				const char    *devname, 
				void          *dev_id)

当删除一个中断时,也可以使用卸载中断处理函数,在kernel/irq/manage.c中定义,函数原型如下:

/* 函数:request_irq()
 * 描述:向内核注册中断处理函数
 * 参数:     irq:中断号
 *        dev_id:用户可以自己指定,也可以为空
 * 返回: 无
 */
void free_irq(unsigned int irq, void *dev_id)

2. 中断处理函数的过程

ARM架构的异常向量基址可以为0x00000000,也可以为0xFFFF0000,Linux内核采用0xFFFF0000。当发生中断时,CPU会调到相应的异常向量,执行异常向量中的代码后,最终会调用总入口函数,例如图下的asm_do_IRQ、do_DataAbort函数等,异常处理体系结构如下:
Linux字符时设备驱动 中断处理 按键_第2张图片
假设进入c语言总入口函数,如函数asm_do_IRQ()(在arch/arm/kernel/irq.c中定义),源代码如下:

asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
	struct pt_regs *old_regs = set_irq_regs(regs);
	struct irq_desc *desc = irq_desc + irq;

	/*
	 * Some hardware gives randomly wrong interrupts.  Rather
	 * than crashing, do something sensible.
	 */
	if (irq >= NR_IRQS)
		desc = &bad_irq_desc;

	irq_enter();

	desc_handle_irq(irq, desc);

	/* AT91 specific workaround */
	irq_finish(irq);

	irq_exit();
	set_irq_regs(old_regs);
}

最终会执行函数desc_handle_irq(),该函数直接调用desc结构体中处理函数入口,对于电平触发中断,入口函数为handle_level_irq()(在kernel/irq/chip.c中定义);对于边沿触发中断,入口为handle_edge_irq()(在kernel/irq/chip.c中定义)。
不管函数handle_level_irq()还是函数handle_edge_irq(),最后都会调用函数hanle_IRQ_event(),依次执行action链表中用户注册的处理函数。

3. Linux中断方式按键驱动开发

3.1 开发环境

开发板:JZ2440V3
Linux内核版本:2.6.22.6
编译器:arm-linux-gcc-3.4.5-glibc-2.3.6

3.2 开发底层驱动程序

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


#define DEVICE_NAME		"keys"  /* 加载模式后,执行”cat /proc/devices”命令看到的设备名称 */
#define LED_MAJOR		251     /* 主设备号 */
int major;

static struct class *keys_class;
static struct class_device	*keys_class_dev;

volatile unsigned long *GPFCON;
volatile unsigned long *GPFDAT;
volatile unsigned long *GPGCON;
volatile unsigned long *GPGDAT;


static DECLARE_WAIT_QUEUE_HEAD(button_waitq);

/* 中断事件标志, 中断服务程序将它置1,keys_read将它清0 */
static volatile int ev_press = 0;


struct pin_desc{
	unsigned int pin;
	unsigned int key_val;
};


/* 键值: 按下时, 0x01, 0x02, 0x03, 0x04 */
/* 键值: 松开时, 0x81, 0x82, 0x83, 0x84 */
static unsigned char key_val;

struct pin_desc pins_desc[4] = {
	{S3C2410_GPF0, 0x01},
	{S3C2410_GPF2, 0x02},
	{S3C2410_GPG3, 0x03},
	{S3C2410_GPG11, 0x04},
};


/* 函数:buttons_irq()
 * 描述:中断函数
 * 参数:irq:中断号
 *       dev_id:传入的参数dev_id,用户可以指向不同指针数据,也可以为空
 *               该处传入按键引脚和按键值
 * 返回:
 */
static irqreturn_t buttons_irq(int irq, void *dev_id)
{
	struct pin_desc * pindesc = (struct pin_desc *)dev_id;
	unsigned int pinval;

    /* 返回引脚的状态,若引脚为高,则返回1 */
	pinval = s3c2410_gpio_getpin(pindesc->pin);

    /* 引脚为低,怎说明按下按键 */
	if(pinval) /* 松开 */
	{

		key_val = 0x80 | pindesc->key_val;
	}
	else    /* 按下 */
	{
		key_val = pindesc->key_val;
	}

    ev_press = 1;                           /* 表示中断发生了 */
    wake_up_interruptible(&button_waitq);   /* 唤醒休眠的进程 */

	
	return IRQ_RETVAL(IRQ_HANDLED);
}

/* 函数:keys_open()
 * 描述:应用程序执行open(...)时,就会调用该函数
 * 参数:inode:传递给驱动的inode
 *       filp:设备文件,file结构体有个叫做private_data的成员变量
 * 			   一般在open的时候将private_data指向设备结构体。
 * 返回:0 成功;其他 失败
 */
static int keys_open(struct inode *inode, struct file *file)
{
	/* 配置GPF0,2为输入引脚 */
	/* 配置GPG3,11为输入引脚 */
	request_irq(IRQ_EINT0,  buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
	request_irq(IRQ_EINT2,  buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
	request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
	request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);	

	return 0;
}

/* 函数:keys_read()
 * 描述:应用程序执行read(...)时,就会调用该函数,从设备读取数据 
 * 参数:filp:要打开的设备文件(文件描述符)
 *       buf:返回给用户空间的数据缓冲区
 * 	     cnt:要读取的数据长度
 * 		 offt:相对于文件首地址的偏移
 * 返回:读取的字节数,如果为负值,表示读取失败
 */
ssize_t keys_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
	if (size != 1)
		return -EINVAL;

	/* 如果没有按键动作, 休眠 */
	wait_event_interruptible(button_waitq, ev_press);

	/* 如果有按键动作, 返回键值 */
	copy_to_user(buf, &key_val, 1);

	ev_press = 0;
	
	return 1;
}

/* 函数:keys_close()
 * 描述:应用程序执行close(...)时,就会调用该函数,关闭设备 
 * 参数: file:设备文件,表示关闭的文件描述符
 *       inode:传递给驱动的inode
 * 返回:0 成功;其他 失败
 */
int keys_close(struct inode *inode, struct file *file)
{
	free_irq(IRQ_EINT0, &pins_desc[0]);
	free_irq(IRQ_EINT2, &pins_desc[1]);
	free_irq(IRQ_EINT11, &pins_desc[2]);
	free_irq(IRQ_EINT19, &pins_desc[3]);

	return 0;
}


static struct file_operations keys_drv_fops = {
	.owner	 =  THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
	.open	 =  keys_open,     
	.read	 =	keys_read,	   
	.release =  keys_close,	   
};



static int __init keys_init(void)
{
	major = register_chrdev(0, DEVICE_NAME, &keys_drv_fops);

	/* 新建一个类 */
	keys_class = class_create(THIS_MODULE, DEVICE_NAME);

	/* 在类下创建一个设备 */
	keys_class_dev = class_device_create(keys_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME); /* /dev/keys */

	GPFCON = (volatile unsigned long *)ioremap(0x56000050, 16);
	GPFDAT = GPFCON + 1;

	GPGCON = (volatile unsigned long *)ioremap(0x56000060, 16);
	GPGDAT = GPGCON + 1;

	return 0;
}

static void __exit keys_exit(void)
{
	unregister_chrdev(major, DEVICE_NAME);

	class_device_unregister(keys_class_dev);
	class_destroy(keys_class);

	/* 取消映射 */
	iounmap(GPFCON);
	iounmap(GPGCON);

	return 0;
}


module_init(keys_init);
module_exit(keys_exit);

MODULE_LICENSE("GPL");

/* 描述驱动程序的一些信息,不是必须的 */
MODULE_AUTHOR("https://me.csdn.net/qq_31782183");
MODULE_VERSION("1.0.0");
MODULE_DESCRIPTION("S3C2410/S3C2440 Keys Driver");

3.3 开发应用程序

#include 
#include 
#include 
#include 
#include 

int main(int argc, char **argv)
{
	int fd;
	unsigned char key_val;
	
	fd = open("/dev/keys", O_RDWR);
	if (fd < 0)
	{
		printf("can't open!\n");
	}

	while (1)
	{
		read(fd, &key_val, 1);
		printf("key_val = 0x%x\n", key_val);
	}
	
	return 0;
}

3.4 编写Makeile文件

KERN_DIR = /work/system/linux-2.6.22.6

all:
	make -C $(KERN_DIR) M=`pwd` modules 

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order

obj-m	+= keys.o

3.5 编译和测试

3.5.1 编译驱动模块:
make

编译完成,会生成chrleds.ko的驱动模块文件。

3.5.2 编译APP测试程序:
arm-linux-gcc keysApp.c -o keysApp

编译完成后,会生成keysApp应用程序。

3.5.3 挂接nfs文件系统:
mount -t nfs -o tcp,nolock 192.168.1.1:/home/book/linux/nfs/fs_mini_mdev /mnt

启动开发板后,执行该语句,将自己的nfs系统挂接,方便测试。

3.5.4 加载模块:

将keys.ko和keysApp两个文件拷贝到挂接系统的/lib/modules/2.6.22.6 目录下,输入以下命令:

insmod keys.ko

驱动加载成功后,会创建"/dev/keys"设备节点。

3.5.5 输入如下命名测试按键:
./keysApp

通过按开发板上面的按键,会打印相应的内容,如下:

key_val = 0x3
key_val = 0x3
key_val = 0x83
key_val = 0x3
key_val = 0x3
key_val = 0x83
key_val = 0x83

如果要卸载模块,输入如下命令:

rmmod chrleds.ko

你可能感兴趣的:(Linux驱动)