混雜設備
概念:
在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