按鍵驅動程序設計---混雜設備、中斷分層處理、工作隊列、阻塞型驅動

混雜設備

概念:
在Linux系統中,存在一類字符設備,它們擁有相同的主設備號主設備號都爲10,但次設備號不同,我們稱這類設備爲混雜設備(miscdevice)。所有的混雜設備形成一個鏈表,對設備訪問時內核根據次設備號查找到相應的混雜設備。

設備描述符:
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;
};

設備註冊和註銷
Linux中使用 misc_ register 函數 和 misc_ dregister函數 來註冊或註銷一個混雜設備驅動。

//註冊:
int misc_register(struct miscdevice * misc)

//註銷:
int misc_dregister(struct miscdevice * misc)

中斷處理程序

中斷處理流程:
這裏寫圖片描述

首先獲取中斷號,根據中斷號來找到中斷處理程序,一箇中斷號可以對應多箇中斷處理程序,通過dev_id來區分。

中斷註冊:
request_irq函數用於註冊中斷。

int request_irq
(
    unsigned int irq,
    //中斷號

    void (*handler)(int, void*, struct  pt_regs *), 
    //中斷處理函數

    unsigned long flags,
    //與中斷管理有關的各種選項

    const char *devname,
    //設備名

    void *dev_id
    //共享中斷時使用
)

函數返回0表示成功,或者返回一個錯誤碼。

在flags參數中,可以選擇一些與中斷管理有關的選項,如:

IRQF_ DISABLED(SA_INTERRUPT)
如果設置該位,表示是一個“快速”中斷處理程序;如果沒有設置這位,那麼是一個“慢速”中斷處理程序。

快/慢速中斷的主要區別在於:快速中斷保證中斷處理的原子性(不被打斷),而慢速中斷則不保證。換句話說,也就是“開啓中斷”標誌位(處理器IF)在運行快速中斷處理程序時是關閉的,因此在服務該中斷時,不會被其他類型的中斷打斷;而調用慢速中斷處理時,其它類型的中斷仍可以得到服務。

IRQF_ SHARED(SA_SHIRQ)
該位表明該中斷號是多個設備共享的。

中斷處理:
中斷處理程序的特別之處是在中斷上下文中運行的,它的行爲受到某些限制:
1、不能使用可能引起阻塞的函數
2、不能使用可能引起調度的函數

這裏寫圖片描述

註銷中斷:
當設備不再需要使用中斷時(通常在驅動卸載時),應當把它們註銷,使用函數:

void free_irq(unsigned int irq, void *dev_id)
//irq指定了中斷號,然後用 dev_id 來區分共享中斷。

中斷分層處理

在慢速中斷的情況下,如果正在處理某一中斷,此時,發生了相同類型的中斷,CPU會忽略後面發生的中斷。在快速中斷的情況下,CPU會忽略其他任何類型的中斷。因此,在處理中斷時,無論快速還是慢速中斷都會產生忽略其他中斷的情況,如果可以儘可能的縮短中斷處理的時間,則可以讓忽略的中斷數量儘可能的減少。

因此,可以對中斷進行分層的處理:
這裏寫圖片描述
上半部:當中斷髮生時,它進行相應地硬件讀寫,並“登記”該中斷。通常由中斷處理程序充當上半部。
下半部:在系統空閒的時候對上半部“登記”的中斷進行後續處理。

工作隊列:
可以採用工作隊列的的方式實現中斷處理的分層:

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

這裏寫圖片描述
掛載的時候並不是work執行的時候,是內核線程來執行的work的工作。

Linux內核使用 struct work_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;
};
typedef void (*work_func_t)(struct work_struct *work);

工作流程:
1、創建工作隊列 create_workqueue
2、創建工作 INIT_WORK
3、提交工作 queue_work

代碼範例:

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

struct workqueue_struct *my_wq; //工作隊列結構體
struct work_struct *work1;      //工作結構體
struct work_struct *work2;

MODULE_LICENSE("GPL");

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

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

int init_que(void)
{   
    //1. 創建工作隊列
    my_wq = create_workqueue("my_que");

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

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

    //2. 創建工作(創建第二個work)
    work2 = kmalloc(sizeof(struct work_struct),GFP_KERNEL);
    INIT_WORK(work2, work2_func);

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

    return 0;
}

void clean_que(){ }

module_init(init_que);
module_exit(clean_que);

在大多數情況下, 驅動並不需要自己建立工作隊列,只需定義工作, 然後將工作提交到內核已經定義好的工作隊列 keventd_wq 中。
可以採用下面的函數,提交工作到默認隊列: schedule_work 函數

使用默認工作隊列範例代碼:

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

struct work_struct *work1;
struct work_struct *work2;

MODULE_LICENSE("GPL");

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

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

int init_que(void)
{   
    //2. 創建工作
    work1 = kmalloc(sizeof(struct work_struct),GFP_KERNEL);
    INIT_WORK(work1, work1_func);

    //3. 掛載(提交)工作,採用默認的工作隊列,不需要創建工作隊列
    schedule_work(work1);

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

    //3. 掛載(提交)工作
    schedule_work(work2);

    return 0;
}

void clean_que(){ }

module_init(init_que);
module_exit(clean_que);

採用定時器來按鍵去抖

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

按鍵去抖動的方法主要有二種,一種是硬件電路去抖;另一種就是軟件延時去抖。而延時又一般分爲二種,一種是for循環等待,另一種是定時器延時。在操作系統中,由於效率方面的原因,一般不允許使用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;
};

這裏寫圖片描述

定時器範例代碼見下方。

阻塞型驅動程序設計

阻塞必要性
當一個設備無法立刻滿足用戶的讀寫請求時應當如何處理? 例如:調用read時,設備沒有數據提供, 但以後可能會有;或者一個進程試圖向設備寫入數據,但是設備暫時沒有準備好接收數據。當上述情況發生的時候,驅動程序應當(缺省地)阻塞進程,使它進入等待(睡眠)狀態,直到請求可以得到滿足。

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

1、定義等待隊列

wait_queue_head_t my_queue

2、初始化等待隊列

init_waitqueue_head(&my_queue)

3、定義+初始化等待隊列

DECLARE_WAIT_QUEUE_HEAD(my_queue)

4、進入等待隊列,睡眠

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參數所指定的等待隊列上。
*/

5、從等待隊列中喚醒進程

wake_up(wait_queue_t *q)
/*
從等待隊列q中喚醒狀態爲TASK_UNINTERRUPTIBLE,
TASK_INTERRUPTIBLE , TASK_KILLABLE 的所有進程。
*/
wake_up_interruptible(wait_queue_t *q)
/*
從等待隊列q中喚醒狀態爲TASK_INTERRUPTIBLE 的進程。
*/

範例代碼

驅動程序代碼:

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

#define GPFCON 0x56000050   //寄存器地址
#define GPFDAT 0x56000054

struct work_struct *work1;       //工作結構體
struct timer_list buttons_timer; //定時器結構體

unsigned int *gpio_data;
unsigned int key_num = 0;

wait_queue_head_t  key_q;  //定義等待隊列

void work1_func(struct work_struct *work)
{
    mod_timer(&buttons_timer, jiffies + (HZ /10)); //啓動定時器,100ms
    //jiffies是全局變量,表示當前時間。HZ 是 1 秒鐘
}

void buttons_timer_function(unsigned long data)  //定時器超時函數
{
    unsigned int key_val;

    key_val = readw(gpio_data)&0x1; //定義兩個按鍵
    if (key_val == 0)
       key_num = 4;

    key_val = readw(gpio_data)&0x4;
    if (key_val == 0)
        key_num = 3;

    wake_up(&key_q);  //喚醒等待隊列中的進程
} 

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(GPFCON,4);//轉化爲虛擬地址
    data = readw(gpio_config);      //讀出數據
    data &= ~0b110011;              //設置中斷模式
    data |= 0b100010;                

    writew(data,gpio_config);       //寫回寄存器

    gpio_data = ioremap(GPFDAT,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)
//支持應用程序對設備文件的讀取
{ 
    wait_event(key_q,key_num); //進入等待隊列,睡眠
    //在讀數據時,如果沒有數據可讀,就需要進入等待隊列

    printk("in kernel :key num is %d\n",key_num);
    copy_to_user(buf, &key_num, 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 button_init()
{
    int ret;
    ret = misc_register(&key_miscdev); //註冊混雜設備

    if (ret !=0)
        printk("register fail!\n");

    //註冊中斷處理程序,兩個按鍵分屬不同的中斷
    request_irq(IRQ_EINT0,key_int,IRQF_TRIGGER_FALLING,"key",(void *)4);//下降沿
    request_irq(IRQ_EINT2,key_int,IRQF_TRIGGER_FALLING,"key",(void *)3);

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

    //創建工作,將剩餘的中斷處理工作放到work中
    work1 = kmalloc(sizeof(struct work_struct),GFP_KERNEL);
    INIT_WORK(work1, work1_func);

    /* 初始化定時器 */  
    init_timer(&buttons_timer);   
    buttons_timer.function = buttons_timer_function;  //設置超時函數

    /* 向內核註冊一個定時器 */  
    add_timer(&buttons_timer);  

    /* 初始化等待隊列 */
    init_waitqueue_head(&key_q);

    return 0;   
}

static void button_exit()
{
    misc_deregister(&key_miscdev);  //註銷混雜設備
}

module_init(button_init);
module_exit(button_exit);

應用程序代碼:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int main(int argc, char **argv)
{
    int fd;
    int key_num;

    fd = open("/dev/2440key", 0);
    //需要在相應目錄下創建設備文件,
    //混雜設備主設備號10,次設備號在驅動程序中設爲200

    if (fd<0)
        printf("open fail\n");

    read(fd, &key_num, 4);
    printf("key is %d\n",key_num);
    close(fd);

    return 0;
}

makefile文件

obj-m := key.o
KDIR := /home/S5-driver/lesson7/linux-tq2440/
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
發佈了5 篇原創文章 · 獲贊 60 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章