天天看點

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。

混雜裝置驅動模型:

1. 混雜裝置描述

        在Linux系統中,存在一類字元裝置,它們擁有相同的主裝置号(10),單次裝置号不同,我們稱這類裝置為混            雜裝置(miscdevice).所有的混雜裝置形成一個連結清單,對裝置通路時核心根據次裝置号查到相應的混雜裝置。

         混雜裝置也是字元裝置!

     linux中使用struct miscdevice來描述一個混雜裝置。

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

2. 混雜驅動注冊

    Linux中使用misc_register函數來注冊一個混雜裝置驅動。

    int  misc_register(struct miscdev *misc)

3. 範例驅動分析

     3.1 初始化miscdevice(minor、name、fops)

     3.2 注冊miscdevice (通過misc_register函數實作)

這裡安照上面的分析,先來搭建一個最簡單隻有一個open操作的混雜按鍵裝置驅動模型,後邊逐漸深入分析逐漸完善代碼。

key.c

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

#include<linux/module.h>  

#include<linux/init.h>  

#inlcude<linux/miscdevice.h> /* for struct miscdevice*/  

int key_open(struct inode *node, struct file *filp)  

{  

    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()  

    misc_register(&key_miscdev);//注冊一個混雜裝置驅動裝置  

static void key_exit()  

    misc_deregister(&key_miscdev);//登出一個混雜裝置驅動  

module_init(key_init);  

module_exit(key_exit);  

2. Linux 中斷處理流程分析

下面先來分析寫好按鍵驅動的一些準備工作!按鍵一般用中斷的模式來處理,這裡先分析linux中斷處理程式:

1. 裸機中斷處理流程分析

    1.1 中斷有一個統一的入口 irq:

    ......

    第一步: 保護現場(中斷部分執行完畢後要恢複之前的狀态繼續執行)

    第二步: 跳轉到hand_ini處執行中斷程式

                先事先注冊中斷程式,然後根據相應的中斷找到對應的中斷處理程式

    第三步:恢複現場,

在Linux作業系統中,irq中斷的統一入口其實也是這樣的(entry-armv.S檔案中)

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

這裡的irq_hander其實是一個宏定義:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

而arch_irq_hander_default這個宏是在entry-macro-multi.S這個檔案中

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

拿到中斷号,然後設定相關寄存器并且調到asm_do_IRQ進行中斷

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

看看generic_handle_irq(irq)這個函數:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

然後函數又跳到這裡了:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

最後調到了handle_irq這個結構中。

這裡總結一下上面函數跳轉的分析過程:

第一步:根據中斷産生的統一入口進入中斷處理程式,拿到産生中斷源的中斷号

第二步:根據這個中斷号irq找到irq_desc結構, 在這個irq結構中就會有一個action選項,在這個action結構中就是使用者事先填寫的中斷處理程式handler,這裡用一張圖來說明:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

上面分析了那麼多,其實就是為了說明在驅動中如果要用中斷,驅動程式該幹嘛?

第一點:實作中斷處理程式

第二點:當我們的中斷産生了,能夠被linux作業系統調用到使用者事先定義好的中斷處理程式,還需要把中斷處理程式               注冊到Linux作業系統中來,簡單的來說就是注冊中斷

3. Linux 中斷處理程式設計

    3.1 注冊中斷

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

參數說明:

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)  共享中斷該位表明該中斷号是多個裝置共享的。

快/慢速中斷的主要差別在于:快速中斷保證中斷處理的原子性(不被打斷),而慢速中斷則不保證。換句話說,也就是“開啟中斷”标志位(處理器IF)在運作快速中斷處理程式時是關閉的,是以在服務該中斷時,不會被其他類型的中斷打斷;而調用慢速中斷處理時,其他類型的中斷仍可以得到服務。

    3.2 中斷處理

中斷處理程式的特别之處是在中斷上下文中運作的,它的行為為受到某些限制:

1. 不能使用可能引起阻塞的函數

2. 不能使用可能引起排程的函數

處理流程:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

    3.3 登出處理

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

void free_irq(unsigned int irq, void *dev_id)  // 參數dev_id 可以結和上面那張圖來看,就是共享中斷中的那個中斷

結和上面的分析在之前的代碼基礎上加入下面的部分:

中斷處理函數部分:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】
Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

下面來分析按鍵硬體部分的相關知識!硬體原理圖以及相關GPIO設定

這裡先貼上OK6410開發闆上的按鍵硬體原理圖部分:

這裡KEYINT1是和GPN0相連,

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】
Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

對應的CPU引腳是GPN組,下面檢視下GPN引腳datasheet的相關部分:

由下面的圖這裡可以看到将GPNCON寄存器的最後兩位設定為0b10(外部中斷模式)

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

GPN0對應的外部中斷号查晶片手冊可以看到為:XEINT0

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

這裡看看OK6410核心源碼部分關于中斷号的宏定義:

這個在Irqs.h檔案中:要與自己使用的硬體平台對應,我這裡是OK6410

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

這裡對應的裝置中斷号為S3C_EINT(0)或者寫出IRQ_EINT(0)都是一樣的

這個檔案源碼中還有一句#define S3C_IRQ_OFFSET(32)

中斷号偏移 其中前面的32個中斷号是留給使用者程式作為軟中斷來使用, 

這裡貼出在前面的基礎上加的key.c的代碼:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

#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>  

#define GPNCON 0x7F008830  

irqreturn_t key_int(int irq, void *dev_id)  

    //1. 檢測是否發生了按鍵中斷 這裡可以暫時不做,因為這裡沒有使用共享中斷  

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

         //比如如果是網卡驅動 就要處理  

    //3. 列印按鍵值  

    printk(KERN_WARNING"key down!\n");  

void key_hw_init(void) //按鍵硬體初始化部分  

    unsigned int *gpio_config;  

    unsigned short data;  

    //第一步:設定GPNCON寄存器設定GPIO為輸入  

    gpio_config = ioremap(GPNCON, 4);//将實體位址轉化為虛拟位址  

    data = readw(gpio_config);  

    data &= ~0b11; //先清零  

    data |= 0b10;  //後兩位設定成0b10  

    writew(data, gpio_config);  

    printk(KERN_WARNING"init ...!\n");  

    //第二步: 按鍵中斷部分相應處理 注冊中斷 登出等等  

    printk(KERN_WARNING"open ...!\n");  

struct miscdevice key_miscdev = //定義一個misdevice結構  

    .minor = 200,  

    .name = "key",  

    .fops = &key_fops,//這裡key_fops是一個struct file_operations結構  

static int key_init(void)  

    int err;  

    //按鍵初始化 硬體初始化部分一般可一放在子產品初始化部分或者open函數中 這裡放在子產品初始化部分  

    key_hw_init();  

    //由高電平變為低電平産生中斷 IRQF_TRIGGER_FALLING  

    if( (err = request_irq(S3C_EINT(0),key_int, IRQF_TRIGGER_FALLING, "key", 0)) < 0 )//注冊中斷處理程式 5個參數  

    {  

         printk(KERN_WARNING"err = %d\n", err);  

         goto irq_err;  

    }  

irq_err:  

        misc_deregister(&key_miscdev);    

    return -1;  

static void key_exit(void)  

    free_irq(S3C_EINT(0), 0);//登出中斷 這裡irqnumber參數暫時用一個變量來表示(中斷号)  

    printk(KERN_WARNING"key up!");  

MODULE_LICENSE("GPL");  

MODULE_DESCRIPTION("key driver");  

這裡貼一個代碼編譯後在開發闆上運作,按下按鍵的效果截圖:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

中斷分層設計:

1. 中斷嵌套

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

2. 中斷分層方式

    2.1 軟中斷

    2.2 tasklet

    2.3 工作隊列(使用更廣泛)

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

工作隊列是一種将任務推後執行的形式,他把推後的任務交由一個核心線程去執行。這樣下半部會在程序上下文執行,它允許重新排程甚至睡眠。每個被推後的任務叫做“工作”,由這些工作組成的隊列稱為工作隊列

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】
Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

這裡應該是用struct  workqueue_struct:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】
Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】
Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

2.1. 從核心源碼檢視create_workqueue函數的用法:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

這是核心源碼裡面找到的這個函數用法示例,這裡可以看到create_workqueue函數隻有一個參數,參數為工作隊列的名字,傳回的為建立好的一個工作隊列指針,下面第三個箭頭所指向的部分就是這個指針的類型!

用法示例:

struct workqueue_struct *my_wq;//定義一個工作隊列指針

my_wq = create_workqueue("my_queue");

2.2. 下面去核心源碼中查找一下init_work這個函數的用法:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

兩個參數:

work :要初始化的工作work指針

func  :工作要執行的函數

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

void work1_func(struct work_struct *work)

{

printk(KERN_WARNING"this is work1>\n");

}

work1 = kmalloc(sizeof(struct work_struct), GFP_KERNEL);

INIT_WORK(work1 , work1_func );

2.3. queue_work函數用法示例:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

也是兩個參數:

一個是工作隊列指針 struct workqueue_struct *wq

一個是工作指針

queue_work(my_wq, work1);

下面根據上面的分析這裡貼出一個示例小程式:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

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

struct workqueue_struct *my_wq; //定義一個工作隊列指針  

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

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

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);  

    work2 = kmalloc(sizeof(struct work_struct), GFP_KERNEL);  

    INIT_WORK(work2 , work2_func );  

    queue_work(my_wq, work2);  

void clean_que(void)  

module_init(init_que);  

module_exit(clean_que);  

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

3. 使用工作隊列實作分層

在大多數情況下,驅動并不需要自己建立工作隊列,隻需定義工作,然後将工作送出到核心已經定義好的工作隊列keventd_wq中。

3.1 送出工作到預設隊列

schedule_work

在上面的代碼這樣修改也是同樣的效果:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

有了上面的基礎,然後對之前的按鍵驅動進行改進!通過中斷分層來實作按鍵驅動

按鍵中斷處理程式 硬體處理部分比較簡單,中斷上半部 硬體中斷處理基本可以不做

下半部 和硬體沒有什麼關系的部分,就是下面列印按鍵值部分 可以放到按鍵中斷以外來處理,為系統節省更多的時間出來,避免應為中斷程式處理部分耗時過長造成中斷丢失!

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

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

    //3. 送出下半部  

    schedule_work(work1);  

    work1 = kmalloc(sizeof(struct work_struct),GFP_KERNEL);  

編譯并且insmod安裝這個驅動子產品,同樣可以看到按鍵列印的效果!不過本質上驅動處理效率上提高了!當然這裡隻是一個很簡單的例程!

按鍵定時器去抖:

按鍵所用開關為機械彈性開關,當機械觸點斷開、閉合時,由于機械觸點的彈性作用,開關不會馬上穩定接通或斷開。因而在閉合及斷開的瞬間總是伴随有一連串的抖動。

按鍵去抖動的方法主要有兩種,一種是硬體電路去抖動;另一種就是軟體延時去抖。而延時一般由分為兩種,一種是for循環等待,另一種是定時器延時,在作業系統中,由于效率方面的原因,一般不允許使用for循環來等待,隻能使用定時器。

核心定時器:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

上面兩個重要的成員(紅色部分)

expires: 逾時也就是定時多長時間

function: 函數指針

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

這之間的函數就不細說了,還是同樣的方法不會就檢視核心代碼!上面的按鍵驅動實際上是不完善的,按一下會列印好幾個按鍵按下的資訊,這裡利用上面介紹到的核心定時器知識優化上面的按鍵程式:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

#define GPNCON  0x7F008830  

#define GPNDAT  0x7F008834  

unsigned int *gpio_data;  

struct timer_list key_timer; //定義一個定時器key_timer  

    //啟動定時器 jiffies是全局變量,用來表示目前系統時間 1S=1000個滴答數  

    mod_timer(&key_timer,jiffies + HZ/10); //設定100ms逾時 1HZ=1S  

void key_timer_func(unsigned long data)  

    unsigned int key_val;  

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

    if(key_val == 0)  

        printk(KERN_WARNING"OK6410 key0 down!\n");  

    gpio_data = ioremap(GPNDAT, 4);//将實體位址轉化為虛拟位址  

    //初始化定時器  

    init_timer(&key_timer);  

    key_timer.function = key_timer_func; //将定義的函數指派給函數指針  

    //注冊定時器  

    add_timer(&key_timer);  

    if( (err = request_irq(S3C_EINT(0),key_int, IRQF_TRIGGER_FALLING, "key", 0)) < 0 )  

編譯運作可以看到:按一下按鍵 隻列印一個OK6410 key0 down!

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

在上面的基礎上繼續優化,實作多按鍵驅動這裡增加key5按鍵!(結合上邊的原理圖部分)

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

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

        printk(KERN_WARNING"OK6410 key5 down!\n");  

    data &= ~0b110000000011; //先清零  

    data |= 0b100000000010;  //後兩位設定成0b10  

    if( (err = request_irq(S3C_EINT(5),key_int, IRQF_TRIGGER_FALLING, "key", 0)) < 0 )  

運作效果:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

阻塞型驅動設計:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

阻塞的必要性:

1. 當一個裝置無法立即滿足使用者的讀寫請求時應當如何處理?例如: 調用read時,裝置沒有資料提供,但以後可能會有:或者一個程序試圖向裝置寫入資料,但是裝置暫時沒有準備好接受資料。當上述情況發生的時候,驅動程式應當阻塞程序,當它進入等待(睡眠)狀态,直到請求可以得到滿足。

2. 在實作阻塞型驅動的過程中,也需要有一個“候車室”來安排被阻塞的程序“休息”,當喚醒它們的條件成熟時,則可以從“候車室”中将這些程序喚醒。而這個“候車室”就是等待隊列。

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】
Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】
Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】
Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

這裡結合阻塞型驅動的知識點繼續優化程式代碼!這裡順便寫個應用測試程式來測試按鍵驅動!

key.c代碼

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

#include<linux/uaccess.h> /* for copy_to_usr */  

#include <linux/sched.h>  

unsigned int key_num = 0;  

wait_queue_head_t key_q; //定義一個等待隊列  

        //printk(KERN_WARNING"OK6410 key0 down!\n");  

        key_num = 1;  

        //printk(KERN_WARNING"OK6410 key5 down!\n");  

        key_num = 6;  

    wake_up(&key_q);  

    //return 0;  

    return IRQ_HANDLED;  

ssize_t key_read(struct file *filp, char __user *buf, size_t size, loff_t *pos)  

    wait_event(key_q,key_num);//休眠 沒有按下為0  

    //将key_value值傳回給使用者空間  

    printk(KERN_WARNING"in kernel :key num is %d\n",key_num);  

    copy_to_user(buf, &key_num, 4); //buf為使用者空間傳過來的位址  

    key_num = 0;  

    return 4;  

    .read = key_read,  

    .name = "6410key",  

static int key_init11(void)  

    if( (err = request_irq(S3C_EINT(0),key_int, IRQF_TRIGGER_FALLING, "6410key", 0)) < 0 )  

    if( (err = request_irq(S3C_EINT(5),key_int, IRQF_TRIGGER_FALLING, "6410key", 0)) < 0 )  

    //初始化一個等待隊列  

    init_waitqueue_head(&key_q);  

    free_irq(S3C_EINT(5), 0);//登出中斷 這裡irqnumber參數暫時用一個變量來表示(中斷号)  

module_init(key_init11);  

key_app.c

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

#include<stdio.h>  

#include<stdlib.h>  

#include<unistd.h>  

#include<sys/types.h>  

#include<sys/stat.h>  

#include<fcntl.h>  

int main(void)  

    int fd;  

    int key_num;  

    int ret;  

    //1. 打開裝置  

    fd = open("/dev/ok6410key", 0);  

    if(fd < 0)  

        printf("open key_device fail!\n");  

    //2. 讀取裝置  

    ret = read(fd, &key_num, 4);  

    if(ret == -1)  

        printf("read fail\n");  

    printf("key is %d\n", key_num);  

    //3. 關閉裝置  

    close(fd);  

Makefile

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

obj-m := key.o  

KDIR := /home/kernel/linux-ok6410  

all:  

    make -C $(KDIR) M=$(PWD) modules CROSS_COMPILE=arm-linux- ARCH=arm  

clean:  

    rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.bak *.order  

編譯:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

同步到開發上,安裝驅動子產品 insmod key.ko

然後mknod /dev/ok6410key  c   10  200 

這一行的指令作用是産生裝置結點供應用程式通路 ,ok6410key為裝置名字 c表示這個是字元裝置 混雜裝置也是字元裝置 10 是混雜字元裝置的統一裝置号 200是在驅動程式中定義的次裝置号.

運作應用程式按下按鍵效果截圖:

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

終于搞定了!

Linux按鍵驅動程式設計詳解---從簡單到不簡單【轉】

(曆時兩天半)

【新浪微網誌】 張昺華--sky

【twitter】 @sky2030_

【facebook】 張昺華 zhangbinghua

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利.

繼續閱讀