1. 程式人生 > >老查的ARM學習筆記:chapter-1(按鍵驅動程式設計)

老查的ARM學習筆記:chapter-1(按鍵驅動程式設計)

前面的部落格中,有一篇通過按鍵玩中斷的文章,不過那裡的程式是裸機,也就是沒有加系統下設計的程式,也就和在微控制器中設計的程式一樣比較簡單。現在我們來看看按鍵的驅動程式在linux系統下是如何設計的。

1 混雜裝置驅動模型**

1 混雜裝置驅動描述
首先我們先來了解一下什麼是混雜裝置驅動模型。混雜裝置其實是字元裝置中的一種,主裝置號是10,次裝置號不同的裝置稱為混雜裝置,在linux中,用struct miscdevice來描述一個混雜裝置,從核心原始碼中複製過來結構原型為

struct miscdevice  {
    int minor;              /*次裝置號*/
const char *name; /*裝置名*/ const struct file_operations *fops; /*檔案操作*/ struct list_head list; struct device *parent; struct device *this_device; const char *nodename; mode_t mode; };

2 註冊混雜裝置驅動
linux中使用misc_register函式來註冊一個混雜裝置驅動
int misc_register(struct miscdevice *misc)
3 登出混雜裝置驅動
misc_deregister(struct miscdevice *misc)
通過上面的概述,可以通過下面的導圖來搭建一個簡單的按鍵模型驅動key.c。
這裡寫圖片描述

#include<linux/module.h>
#include<linux/init.h>
#include<linux/miscdevice.h>

MODULE_LICENSE("GPL");

int key_open(struct inode *node,struct file *filp)
{
    return 0;
}

struct file_operations key_fops=
{
    .open = key_open,   
};

struct miscdevice key_miscdev = {
     .minor = 200
, .name = "key", .fops = &key_fops, }; static int key_init() { misc_register(&key_miscdev); return 0; } static void key_exit() { misc_deregister(&key_miscdev); //登出混雜裝置 } module_init(key_init); module_exit(key_exit);

2 中斷處理流程分析

按鍵驅動程式中,一般採用中斷的方式去處理,那我們就先來看看中斷處理的步驟是什麼樣的,在裸機程式部分當中,步驟可分為以下三步。
1 中斷存在統一入口,這個入口在start.S彙編檔案中,每當中斷髮生的時候,都會把irq傳送給pc。
2 註冊中斷處理程式。
3 根據中斷源的編號來呼叫中斷處理程式。
在linux中,統一的入口也是存在的,在entry-arm.S檔案當中的irq_svc。這裡我就來簡單說一下這個流程是怎麼工作的
1 首先,程式通過irq_svc找到中斷入口。
2 其次拿到產生中斷源的編號,也就是中斷號。在裸機中,直接調取中斷源和中斷號就直接去操作了,但是在linux系統中,引入了irq_desc資料結構,其中含有已經有註冊好的處理函式。
3 最後根據取出來事先註冊好的中斷處理函式來執行。

上面分析的就是為了說明在驅動中如果要用中斷,驅動程式該幹嘛。驅動程式有兩個作用,第一是實現中斷處理程式,第二個是註冊中斷到linux系統中。
中斷處理程式設計步驟為:
這裡寫圖片描述

1 註冊中斷

這步是在按鍵中斷初始化中進行的,request_irq函式用於註冊中斷。

int request_irq(unsigned int irq,void (*handler)(int, void*, struct pt_regs *),
unsigned long flags,
const char *devname,
void *dev_id)

引數說明:
unsigned int irq :中斷號
void(handler)(int,void ):中斷處理函式
unsigned long flags:與中斷管理有關的各種選項
const char *devname:裝置名
void *dev_id:共享中斷時使用
在flags引數中, 可以選擇一些與中斷管理有關的選項,如:
. IRQF_DISABLED(SA_INTERRUPT) 快速中斷
如果設定該位,表示是一個“快速”中斷處理程式;如果沒有設定該位,那麼就是一個“慢速”中斷處理程式。
. IRQF_SHARED(SA_SHIRQ) 共享中斷該位表明該中斷號是多個裝置共享的。對於共享中斷,dev_id不同對應不同的裝置中斷,這就是dev_id 的作用。
快/慢速中斷的主要區別在於:快速中斷保證中斷處理的原子性(不被打斷),而慢速中斷則不保證。換句話說,也就是“開啟中斷”標誌位(處理器IF)在執行快速中斷處理程式時是關閉的,因此在服務該中斷時,不會被其他型別的中斷打斷;而呼叫慢速中斷處理時,其他型別的中斷仍可以得到服務。

2 中斷處理

中斷處理函式的原型是
irqreturn_t(int ib_irq, void *dev_id,)
這步是獨立定義的,處理程式還包括
1檢查裝置是否產生了中斷
2 清除中斷產生的標誌
3 相應的硬體操作

3 登出處理

當裝置不再需要使用中斷時(通常在驅動解除安裝時), 應當把它們登出, 使用函式:

void free_irq(unsigned int irq, void *dev_id)  

這步是在按鍵模組解除安裝時進行的

3 按鍵驅動程式設計

在Linux中,硬體的初始化通常在兩個地方進行。一個是在open函式實現,另外一個是在模組的初始化中實現。習慣性的在模組的初始化中實現硬體的初始化void key_hw_init()。按鍵的初始化要參考之前裸機程式,硬體原理圖以及相關GPIO設定這裡先貼上OK6410開發板上的按鍵硬體原理圖部分:
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
從圖中可以看出ok6410的按鍵中斷是通過GPNCON控制暫存器來確定其功能的。
首先定義GPNCON的巨集#define GPNCON 0x7f008830,S2按鍵,也就是KEYINT對應的位為GPN0,設定為10為外部中斷。由於在linux中,不能直接使用實體地址,需要先將其轉化為虛擬地址,使用的函式是gpio_config=ioremap(GPNCON,4),引數4意為分配給地址的虛擬地址4個位元組,從原有暫存器中讀取值使用readw(gpio_config)。
在request_irq()註冊中,因為當按鍵按下去的時候觸發中斷,所以這裡把第三個引數設定為下降沿觸發中斷,IRQF_TRIGGER_FALLING,那麼第一個引數中斷號irqnumber是怎麼獲得的呢?
在核心原始碼irqs.h中可以找到與晶片手冊中一一對應的中斷號,這個中斷號就是註冊函式中第一個引數。
這裡寫圖片描述
從圖中可以看出,外部中斷0—4使用的中斷源為INT_EINT0。
這裡寫圖片描述
這裡寫圖片描述
在OK6410的核心原始碼irqs.h可以看出,中斷號需要加上一個偏移,這個偏移是32,當然在2410上也需要加上一個偏移,只不過2410上的偏移是16。這樣的方式稱為軟中斷。硬體產生的中斷號只是一個序號,和晶片手冊上是對應的序號,這個序號需要加上32才成為linux當中的中斷號。不過核心程式碼已經幫我們加上了這個偏移,所以我們在填寫引數的時候只需要把相應的中斷名放進去就行了,由於這裡採用的是S2按鍵對應的是EINT0中斷,所以第一個引數就是IRQ_EINT0
根據以上分析,可以在原有程式碼上增加中斷處理部分,以下為增加按鍵中斷功能後的程式碼。

#include<linux/module.h>
#include<linux/init.h>
#include<linux/miscdevice.h>
#include<linux/interrupt.h>
#include<linux/io.h>
#include<linux/fs.h>

#define GPNCON 0x7f008830
MODULE_LICENSE("GPL");

irqreturn_t key_int(int irq,void *dev_id)
{
    //1 檢測中斷是否產生

    //2 清除已經發生的按鍵中斷

    //3. 硬體相關操作
    printk(KERN_WARNING"key down!\n"); // 執行列印的時候,必須加上KERN_WARNING字首

    return 0; 
}

void key_hw_init(void)
{
    unsigned int *gpio_config;
    unsigned short data;

    gpio_config=ioremap(GPNCON,4);
    data=readw(gpio_config);
    data&=~0b00;//先清零  
    data |= 0b10;//後兩位設定成0b10 
    writew(data,gpio_config);   
    printk(KERN_WARNING"init ...!\n"); 
}

int key_open(struct inode *node,struct file *filp)
{
    return 0;
}

struct file_operations key_fops=
{
    .open = key_open,   
};

struct miscdevice key_miscdev = {
     .minor = 200,
     .name = "key",
     .fops = &key_fops, 
};

static int key_init(void)
{
    misc_register(&key_miscdev);

    //按鍵初始化 硬體初始化部分一般可一放在模組初始化部分或者open函式中 這裡放在模組初始化部分 
    key_hw_init();

    //註冊中斷處理程式
    request_irq(IRQ_EINT(0),key_int,IRQF_TRIGGER_FALLING,"key",0);

    return 0;
}

static void key_exit(void)
{
    misc_deregister(&key_miscdev); //登出混雜裝置

    //登出中斷處理程式
    free_irq(IRQ_EINT(0),0);  
    printk(KERN_WARNING"key up!"); 
}

module_init(key_init);
module_exit(key_exit);

通過將上述程式碼下載到開發板中去,然後載入insmod key.ko,按下按鍵S2會在開發板上列印key down!
這裡寫圖片描述

4 中斷分層技術

首先來介紹一下中斷分層,中斷分層含有三種,分別是中斷巢狀,中斷分層方式,使用工作佇列實現分層。為什麼要引入中斷分層,這裡還得再提兩點,慢速中斷和快速中斷。
1 慢速中斷概述 當A執行過程中,有B中斷產生,則會執行B中斷,執行完之後執行A中斷,但是當A執行過程中有另外一個同類型的A1中斷產生,則A1會被忽略,一直在執行A。
2 快速中斷概述 當A執行過程中,有B中斷產生,B則會被忽略,同樣A1會被忽略
對比1和2,會發現不管是快/慢速中斷,是否為同類型的中斷,都會產生中斷丟失現象的可能,這不是我們想看到的,所以引入中斷分層技術。
中斷處理程式會做關於硬體和無關硬體的兩個工作,中斷處理程式隨之可以分為兩部分,一部分處理和硬體相關的,另外一部分處理其他無關硬體的,這就是中斷分層,來減少中斷處理程式執行的時間減少丟失的可能性。
中斷分層方式分為
軟中斷
tasklet
工作佇列

其中工作佇列使用的較多
我們可以利用導圖來設計工作佇列
這裡寫圖片描述
工作佇列是怎麼描述的呢?
工作佇列使用struct workqueue_struct來描述,

struct workqueue_struct {
struct cpu_workqueue_struct *cpu_wq;
struct list_head list;
const char *name; /*workqueue name*/
int singlethread;
int freezeable; /* Freeze threads during suspend */
int rt;
};

Linux核心使用struct work_struct來描述一個工作項:
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
};
其中,func成員是最重要的成員。
使用工作佇列
要使用工作佇列,同樣也是要當做一個模組來進行程式設計的,建立一個模組命名為queue.c
同樣要搭建字元裝置模型
1 建立工作佇列,使用create_workqueue,create_workqueue函式只有一個引數,引數為工作佇列的名字,返回的為建立好的一個工作佇列指標。
2 建立工作 使用的是INIT_WORK(*work func),它有兩個引數,第一個是要初始化的工作work指標,第二個是執行的函式。首先要定義一個work_struct函式描述一個工作項,其次是給指標分配空間,使用kmalloc,最後是定義所執行的函式。
3 掛載工作佇列 使用的是queue_work(*workqueue_struct,*work_struct)函式,有兩個引數,第一個是工作佇列的指標,第二個是要掛載的工作,就是建立好工作的指標。
下面根據上面的分析這裡貼出一個示例小程式:

#include<linux/module.h>  
#include<linux/init.h>  
#include <linux/slab.h> /* for kmalloc */  

struct workqueue_struct *my_wq; //定義一個工作佇列指標  
struct work_struct *work1; //定義一項工作  
struct work_struct *work2; //定義一項工作  

MODULE_LICENSE("GPL");  

void work1_func(struct work_struct *work)  
{  
    printk(KERN_WARNING"this is work1>\n");  
}  

void work2_func(struct work_struct *work)  
{  
    printk(KERN_WARNING"this is work2>\n");  
}  

int init_que(void)  
{  
    //1. 建立工作佇列  
    my_wq = create_workqueue("my_queue");  

    //2. 建立工作  
    //work1 = kmalloc(sizeof(struct work_struct), GFP_KERNEL);  
      work1 = kmalloc(sizeof(struct work_struct),GFP_KERNEL);  
    INIT_WORK(work1 , work1_func );  

    //3. 掛載(提交)提交工作  
    queue_work(my_wq, work1);  

    //2. 建立工作  
    work2 = kmalloc(sizeof(struct work_struct), GFP_KERNEL);  
    INIT_WORK(work2 , work2_func );  

    //3. 掛載(提交)提交工作  
    queue_work(my_wq, work2);  


    return 0;  
}  

void clean_que(void)  
{  

}  

module_init(init_que);  
module_exit(clean_que); 

這裡寫圖片描述
將queue.c在linux中make後,把queue.ko檔案複製到根檔案系統中去,在開發板上安裝queue.ko模組,實驗結果如下。
這裡寫圖片描述
程式在執行的時候,linux核心會主動的去建立核心執行緒,所以程式碼中並沒有建立執行緒。在linux系統中,需要自己去建立工作佇列的情況不多,linux核心為我們建立好了工作佇列,我們只要建立工作,並把工作掛載到工作佇列即可。
linux核心為我們建立好的佇列是keventd_wq,我們自己做兩件事就可以了。
1 建立工作
2 提交工作到預設佇列
schedule_work(struct work_sruct *work),它只有一個引數,要提交工作的指標

現在在key.c中定義一項工作,把中斷改為分層處理的方式。
按鍵中斷處理程式 硬體處理部分比較簡單,中斷上半部 硬體中斷處理基本可以不做。在原來程式碼中把建立工作的部分放到模組的初始化中,把定義的工作佇列和工作項workqueue_struct, work_struct以及func都複製到key.c中,再把原來中斷處理程式的第三步執行硬體操作改為提交下半部使用schedule_work(work1);這樣就把和硬體無關的工作提取出來了,為系統節省更多的時間出來,避免應為中斷程式處理部分耗時過長造成中斷丟失!

#include <linux/module.h>  
#include <linux/init.h>  
#include <linux/miscdevice.h> /* for struct miscdevice*/  
#include <linux/interrupt.h>  
#include <linux/fs.h> /* for iormap */  
#include <linux/io.h>  
#include <linux/slab.h> /* for kmalloc */  

#define GPNCON 0x7F008830  

struct work_struct *work1;//定義一項工作  

void work1_func(struct work_struct *work)  
{  
    printk(KERN_WARNING"key down!\n");  
}  

irqreturn_t key_int(int irq, void *dev_id)  
{  
    //1. 檢測是否發生了按鍵中斷 這裡可以暫時不做,因為這裡沒有使用共享中斷  

    //2. 清除已經發生的按鍵中斷 這個是指硬體內部處理,按鍵CPU內部不需要做處理  

    //3. 提交下半部  
    schedule_work(work1);  

    return 0;  
}  

void key_hw_init(void) //按鍵硬體初始化部分  
{  
    unsigned int *gpio_config;  
    unsigned short data;  

    gpio_config = ioremap(GPNCON, 4);//將實體地址轉化為虛擬地址  
    data = readw(gpio_config);  
    data &= ~0b11; //先清零  
    data |= 0b10;  //後兩位設定成0b10  
    writew(data, gpio_config);  
    printk(KERN_WARNING"init ...!\n");  
}  

int key_open(struct inode *node, struct file *filp)  
{  
    printk(KERN_WARNING"open ...!\n");  

    return 0;  
}  

struct file_operations key_fops =   
{  
    .open = key_open,  
};  

struct miscdevice key_miscdev = //定義一個misdevice結構  
{  
    .minor = 200,  
    .name = "key",  
    .fops = &key_fops,//這裡key_fops是一個struct file_operations結構  
};  

static int key_init(void)
{
    misc_register(&key_miscdev);

    //按鍵硬體初始化
    key_hw_init();

    //註冊中斷處理程式
    request_irq(IRQ_EINT(0)),key_int,IRQF_TRIGGER_FALLING,"key",0);
     //建立工作
    work1=kmalloc(sizeof(struct work_struct),GFP_KERNEL);
    INIT_WORK(work1,work1_func);

    return 0;
}

static void key_exit(void)
{
    misc_deregister(&key_miscdev); //登出混雜裝置

    //登出中斷處理程式
    free_irq(IRQ_EINT(0),0);  
}

module_init(key_init);  
module_exit(key_exit);  
MODULE_LICENSE("GPL");  
MODULE_DESCRIPTION("key driver"); 

同樣把key.ko檔案放到6410板子上,可以發現當按鍵按下的時候,會列印key down!

5 按鍵定時器去抖

學過微控制器的同學都知道,在按鍵部分的學習中,必學按鍵消抖,最簡單的方法是延時判斷,就是利用for迴圈等待消抖,但是作業系統不採用這種方法,因為它太佔資源,作業系統採用定時器來進行消抖。
下面我們根據導圖來設計程式碼。
這裡寫圖片描述
linux核心採用struct timer_list來描述一個定時器
struct timer_list {
struct list_head entry;
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
struct tvec_base *base;
};
其中,主要的引數是expire和function
expires 說明定時器定時多長時間
function 是一個指標,若定時器超時的時候,去執行什麼工作,需要自己去設定

1 初始化定時器init_timer只有一個引數,init_timer是timer_list型別的,引數為timer_list的地址。
2 設定並且定義一個超時函式
key_timer.function=key_timer_func;
void key_timer_func(unsigned long data)
{
}
3 註冊定時器
add_timer(&key_timer)
啟動定時器
應在中斷處理程式當中啟動定時器,使用的函式為
mod_timer(&key_timer,jiffer+HZ/10)
定時器在100ms後超時,HZ代表的是1s。jiffer代表的是當前時間,HZ/10代表是100ms.

定時器超時的時候需要定義一個超時函式
void key_timer_func()

具體程式碼如下

#include<linux/module.h>
#include<linux/init.h>
#include<linux/miscdevice.h>
#include<linux/interrupt.h>
#include<linux/io.h>
#include<linux/fs.h>/* for iormap */ 
#include <linux/slab.h> /* for kmalloc */ 

#define GPNCON 0x7f008830
#define GPNDAT 0x7f008834

unsigned int *gpio_data;

struct work_struct *work1;

struct timer_list key_timer;

void key_timer_func(unsigned long data)
{
    unsigned int key_val;

    key_val = readw(gpio_data)&0x1;//因為是S2按鍵,只讀取GPNCON的第0位

    if (key_val == 0)
        printk(KERN_WARNING"key down!\n");
}

void work1_func(struct work_struct *work)
{
    mod_timer(&key_timer, jiffies + (HZ /10));  
}

irqreturn_t key_int(int irq,void *dev_id)
{
    //1 檢測中斷是否產生

    //2 清除已經發生的按鍵中斷

    //3 提交下半部
     schedule_work(work1); 
    return 0;
}

void key_hw_init(void)
{
    unsigned int *gpio_config;
    unsigned short data;

    gpio_config=ioremap(GPNCON,4);
    data=readw(gpio_config);
    data&=~0b00;
    data |= 0b10;
    writew(data,gpio_config);   

    gpio_data=ioremap(GPNDAT,4);
    printk(KERN_WARNING"init ...!\n"); 
}

int key_open(struct inode *node,struct file *filp)
{   
    return 0;
}

struct file_operations key_fops=
{
    .open = key_open,   
};

struct miscdevice key_miscdev = {
     .minor = 200,
     .name = "key",
     .fops = &key_fops, 
};

static int key_init(void)
{
    misc_register(&key_miscdev);//註冊一個混雜裝置驅動裝置  

    //按鍵硬體初始化
    key_hw_init();

     //註冊中斷處理程式
    request_irq(IRQ_EINT(0),key_int,IRQF_TRIGGER_FALLING,"key",0);

     //建立工作
    work1=kmalloc(sizeof(struct work_struct),GFP_KERNEL);
    INIT_WORK(work1,work1_func);
    //初始化定時器
    init_timer(&key_timer);
    key_timer.function = key_timer_func;
    //註冊定時器
    add_timer(&key_timer);

    return 0;
}

static void key_exit(void)
{
    misc_deregister(&key_miscdev); //登出混雜裝置

    //登出中斷處理程式
    free_irq(IRQ_EINT(0),0);  
}

module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");  
MODULE_DESCRIPTION("key driver"); 

同樣,將上面的程式碼make後,把.ko檔案放到開發板上去,按下按鍵的同時可以列印key down的效果。

這裡寫圖片描述

6 驅動支援多按鍵優化

由於上面只針對一個按鍵進行實驗,現在要多增加按鍵。這裡除了S2按鍵,再增加一個按鍵S3,根據上面的講解,增加按鍵部分就比較簡單了,僅僅需要修改下判斷部分和註冊中斷處理部分以及登出部分就可以了。下面提供上程式碼

#include<linux/module.h>
#include<linux/init.h>
#include<linux/miscdevice.h>
#include<linux/interrupt.h>
#include<linux/io.h>
#include<linux/fs.h>
#include<asm/uaccess.h>
#include<linux/slab.h>

#define GPNCON 0x7f008830
#define GPNDAT 0x7F008834

unsigned int *gpio_data;
unsigned int key_num = 0;

struct work_struct *work1;

struct timer_list key_timer;


void key_timer_func(unsigned long data)
{
    unsigned int key_val;

    key_val = readw(gpio_data)&0x1;//只讀取最後一位  

    if (key_val == 0)
     printk(KERN_WARNING"OK6410 S2 key down!\n");  

    key_val = readw(gpio_data)&0x2;//只讀取倒數第二位 

    if (key_val == 0)
    printk(KERN_WARNING"OK6410 S3 key down!\n");  


}


void work1_func(struct work_struct *work)
{
    mod_timer(&key_timer, jiffies + (HZ /10));  
}


irqreturn_t key_int(int irq,void *dev_id)
{
    //1 檢測中斷是否產生

    //2 清除已經發生的按鍵中斷

    //3. 提交下半部  
    schedule_work(work1); 
    return 0;
}

void key_hw_init()
{
    unsigned int *gpio_config;
    unsigned short data;

    gpio_config=ioremap(GPNCON,4);
    data=readw(gpio_config);
    data&=~0b00;
    data |= 0b10;
    writew(data,gpio_config);   

    gpio_data=ioremap(GPNDAT,4);

}

int key_open(struct inode *node,struct file *filp)
{
    return 0;
}

struct file_operations key_fops=
{
    .open = key_open,

};

struct miscdevice key_miscdev = {
     .minor = 200,
     .name = "key",
     .fops = &key_fops, 
};

static int key_init()
{
    misc_register(&key_miscdev);

    //按鍵硬體初始化
    key_hw_init();

    //註冊中斷處理程式
    request_irq(IRQ_EINT(0),key_int,IRQF_TRIGGER_FALLING,"key",0);
    request_irq(IRQ_EINT(1),key_int,IRQF_TRIGGER_FALLING,"key",1);
     //建立工作
    work1=kmalloc(sizeof(struct work_struct),GFP_KERNEL);
    INIT_WORK(work1,work1_func);
    //初始化定時器
    init_timer(&key_timer);
    key_timer.function = key_timer_func;
    //註冊定時器
    add_timer(&key_timer);

    return 0;
}

static void key_exit()
{
    misc_deregister(&key_miscdev); //登出混雜裝置

    //登出中斷處理程式
    free_irq(IRQ_EINT(0),0);  
    free_irq(IRQ_EINT(1),1);  
}

module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");  
MODULE_DESCRIPTION("key driver");  

執行的效果如圖,當按下按鍵時,相應的按鍵就會被打印出來。
這裡寫圖片描述

7 阻塞型驅動設計

1 阻塞必要性
在正規驅動程式碼編寫過程中,對於裝置方法的讀和寫兩個方法,阻塞是必要的。
例如:A程序想從裝置B讀資料,B中此時還沒有資料,B的驅動程式先讓A程序睡眠,等到B有資料的時候,B驅動程式再把A程序喚醒,讓A繼續讀取資料。簡單的說就是驅動程式阻塞程序,使它進入等待狀態。
2 核心等待佇列
驅動程式把程序睡眠的時候是通過把程序放到等待佇列中
定義等待佇列
wait_queue_head_t my_queue
初始化等待佇列
init_waitqueue_head(&my_queue)
定義+初始化等待佇列
DECLARE_WAIT_QUEUE_HEAD(my_queue)
進入等待佇列,睡眠
wait_event(queue,condition)

當condition(布林表示式)為真時,立即返回;否則讓程序進入TASK_UNINTERRUPTIBLE模式的睡眠,並掛在queue引數所指定的等待佇列上。
wait_event_interruptible(queue,condition)
當condition(布林表示式)為真時,立即返回;否則讓程序進入TASK_INTERRUPTIBLE的睡眠,並掛在queue引數所指定的等待佇列上。
兩者的區別在於進入睡眠狀態不一樣,前者進入不可中斷,後者進入可中斷的狀態。
int wait_event_killable(queue, condition)
當condition(一個布林表示式)為真時,立即返回;否則讓程序進入TASK_KILLABLE的睡眠,並掛在queue引數所指定的等待佇列上。
從等待佇列中喚醒程序
wake_up(wait_queue_t *q)

從等待佇列q中喚醒狀態為TASK_UNINTERRUPTIBLE,TASK_INTERRUPTIBLE,TASK_KILLABLE 的所有程序。
下面通過導圖來進行阻塞型前驅動的設計。
這裡寫圖片描述
下面是驅動程式碼key.c

#include<linux/module.h>
#include<linux/init.h>
#include<linux/miscdevice.h>
#include<linux/interrupt.h>
#include<linux/io.h>
#include<linux/fs.h>
#include<asm/uaccess.h>
#include<linux/slab.h>
#include <linux/sched.h>

#define GPNCON 0x7f008830
#define GPNDAT 0x7F008834

unsigned int *gpio_data;
unsigned int key_num = 0;

struct work_struct *work1;

wait_queue_head_t key_q;//定義等待佇列

struct timer_list key_timer;

void key_timer_func(unsigned long data)
{
    unsigned int key_val;

    key_val = readw(gpio_data)&0x1;

    if (key_val == 0)
     //printk(KERN_WARNING"OK6410 S2 key down!\n");  
        key_num=2;

    key_val = readw(gpio_data)&0x2;

    if (key_val == 0)
     //printk(KERN_WARNING"OK6410 S3 key down!\n");  
        key_num=3;

    wake_up(&key_q);//喚醒等待佇列
}


void work1_func(struct work_struct *work)
{
    mod_timer(&key_timer, jiffies + (HZ /10)); //設定100ms超時 1HZ=1S   
}


irqreturn_t key_int(int irq,void *dev_id)
{
    //1 檢測中斷是否產生

    //2 清除已經發生的按鍵中斷

     //3. 提交下半部  
    schedule_work(work1);  

    //return 0;  
    return IRQ_HANDLED; 
}

void key_hw_init()
{
    unsigned int *gpio_config;
    unsigned short data;

    gpio_config=ioremap(GPNCON,4);
    data=readw(gpio_config);
    data&=~0b00;
    data |= 0b10;
    writew(data,gpio_config);   

    gpio_data=ioremap(GPNDAT,4);

}

int key_open(struct inode *node,struct file *filp)
{
    return 0;
}

ssize_t key_read(struct file *filp, char __user *buf, size_t size, loff_t *pos)//應用程式讀取按鍵的時候,要實現read的裝置方法被應用程式呼叫
{
    wait_event(key_q,key_num); //若key_num為0,就是沒有資料的時候,進入睡眠,並掛在key_q這個等待佇列上

    printk(KERN_WARNING"in kernel :key num is %d\n",key_num);   
    copy_to_user(buf, &key_num, 4);//提供4個位元組給應用程式
    key_num=0;//此時清零用來下次讀取

    return 4;
}

struct file_operations key_fops=
{
    .open = key_open,
    .read = key_read,   
};

struct miscdevice key_miscdev = {
     .minor = 200, //次裝置號
     .name = "key",
     .fops = &key_fops, 
};

static int keys_init()
{
    misc_register(&key_miscdev);

    //按鍵硬體初始化
    key_hw_init();

    //註冊中斷處理程式
    request_irq(IRQ_EINT(0),key_int,IRQF_TRIGGER_FALLING,"key",0);
    request_irq(IRQ_EINT(1),key_int,IRQF_TRIGGER_FALLING,"key",1);
     //建立工作
    work1=kmalloc(sizeof(struct work_struct),GFP_KERNEL);
    INIT_WORK(work1,work1_func);
    //初始化定時器
    init_timer(&key_timer);
    key_timer.function = key_timer_func;
    //註冊定時器
    add_timer(&key_timer);
    //初始化等待佇列
    init_waitqueue_head(&key_q);

    return 0;
}

static void key_exit()
{
    misc_deregister(&key_miscdev); //登出混雜裝置

    //登出中斷處理程式
    free_irq(IRQ_EINT(0),0);  
    free_irq(IRQ_EINT(1),1);  
}

module_init(keys_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");  
MODULE_DESCRIPTION("key driver");  

下面是應用程式程式碼key_app

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>  
#include<sys/types.h>  
#include<sys/stat.h>  
#include<fcntl.h>  

int main()
{
    int fd;
    int key_num;
    int ret; 

    fd=open("/dev/6410key",0); //在開發板上要建立的裝置名字
    if (fd<0)
      printf(" open device fail!\n");

    ret= read(fd,&key_num,4);
    if(ret==-1)
    {
     printf("read fail\n",key_num);
    }

     printf("key is %d\n", key_num);
     close(fd);     
    return 0;
}

在linux中,先將應用程式部分編譯好了放入開發板上去
這裡寫圖片描述

接著把key.ko複製到開發板上去,建立一個裝置mknode /dev/6410key c 10 200
其中,/dev/6410key 是應用程式中設定好的,10是主裝置號 200是驅動程式中設定的次裝置號,執行key_app,的時候,程式會等待響應,即進入睡眠狀態,當按下一個按鍵的時候,就會列印相應的按鍵值。
在以後的程式碼中,阻塞型的驅動程式才是正規的程式碼,所以這節就顯得尤為重要。

下面附上本文驅動程式設計的總體導圖
這裡寫圖片描述

按鍵驅動到此就講解完了,驅動的學習道路非常艱辛,本文花費一整天的時間來整理,難免有不足之處,希望各路大神可以指明本文的不足缺陷。