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內核啓動後,進入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函數等,異常處理體系結構如下:
在這裏插入圖片描述
假設進入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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/irq.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>


#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 <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章