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