使用定時器實作按鍵防抖
發生抖動現象:
按下0x1,松開0x81,一一對應。但是出現“按下0x1,按下0x1,松開0x81”。按下操作,會出現兩個值,說明出現了抖動。
抖動如何産生的?
按鍵是一個機械的開關,當我們按下按鍵的時候,裡面的金屬彈片可能會抖動了幾次,就産生了些脈沖,脈沖就會産生中斷。一次按下,可能出現多次中斷。
目的:
防抖動。
定時器的兩要素:
1、逾時時間
2、處理函數
如何使用定時器防抖動?
以前,一産生中斷,就在中斷處理函數裡面來确定按鍵值,上報按鍵值。
※※※
目的:在中斷裡面,修改定時器,10ms後才執行處理函數(确定按鍵值,上報)。
第一個中斷産生後,10ms後才處理。
馬上來了第二個中斷,機械抖動非常快,根本到不了10ms。修改同一個定時器,從目前時刻起,10s後才處理。之前的“10ms後才處理”就取消了,因為是同一個定時器。
馬上來了第三個中斷,機械抖動非常快,根本到不了10ms。修改同一個定時器,從目前時刻起,10s後才處理。之前的“10ms後才處理”又取消了,因為是同一個定時器。
…… ……
上面的操作,因為是修改同一個定時器的逾時時間。就像調鬧鐘一樣,把鬧鐘的時間往後面調,最終隻會導緻鬧鐘響一次。
前方高能預警
定時器可以仿照aha152x.c檔案中的static int aha152x_device_reset函數來寫。
1、定義一個timer定時器結構體:static struct timer_list buttons_timer;
2、初始化:
static int sixth_drv_init(void)
{
init_timer(&buttons_timer);
buttons_timer.data = (unsigned long) SCpnt;
buttons_timer.expires = jiffies + 100*HZ; (逾時時間)
buttons_timer.function = buttons_timer_function;(當定時器的逾時時間到了,就會調用函數buttons_timer_function)
}
3、把定時器告訴核心:add_timer(&buttons_timer);
4、修改驅動程式裡面的代碼:把中斷裡面的東西,移動到定時器buttons_timer_function函數裡面。
5、全局變量jiffies用來記錄自系統啟動以來産生的節拍的總數。啟動時,核心将該變量初始化為0,此後,每次時鐘中斷處理程式都會增加該變量的值。一秒内時鐘中斷的次數等于Hz,是以jiffies一秒内增加的值也就是Hz。系統運作時間以秒為機關,等于jiffies/Hz。
6、逾時時間就是鬧鐘什麼時候到,修改定時器的逾時時間:mod_timer()。逾時時間是基于jiffies這個值,jiffies是一個全局變量,系統每隔10ms,就會産生一個系統時鐘中斷,jiffies就會累加,可以使用指令cat /proc/interrupts 檢視jiffies的值。
1秒就是HZ,HZ=100。說明1秒内,jiffies增加100。10ms後啟動定時器,HZ=1s,(1s)/100=10ms.。
static irqreturn_t button_irq(int irq, void *dev_id) //中斷處理函數
{
//為了防抖動,修改定時器的逾時時間10ms,逾時時間就是鬧鐘什麼時候到
//逾時時間基于jiffies
//1秒就是HZ,HZ=100。說明1秒内,jiffies增加100
//10ms後啟動定時器
irq_pd = (struct pin_desc *)dev_id;//把它記錄下來
mod_timer(&buttons_timer, jiffies+HZ/100);//HZ=1s,1s/100=10ms
return IRQ_RETVAL(IRQ_HANDLED);
}
7、※※※假設目前的jiffies=50,HZ=100。buttons_timer.expine(逾時時間)=50+100/100=51。每隔10ms就會産生系統時鐘中斷,在系統中斷裡面,這個jiffies值會累加(jiffies++)。下一個系統時鐘後,jiffies的值就會變成51,在中斷處理函數裡面,會從定時器連結清單裡面,把定時器給找出來,看一下哪個時間到了,發現已經jiffies≥逾時間expine,就會調用它的處理函數。
8、定義:static struct pin_desc *irq_pd; //發生中斷時的引腳描述
9、irq_pd = (struct pin_desc *)dev_id;//把它記錄下來
在中斷處理函數就可以用起來了:struct pin_desc * pindesc = irq_pd;
10、因為在驅動程式入口函數sixth_drv_init,沒有設定逾時時間,一開始的逾時時間為0。是以jiffies>=0,它就會立刻調用處理函數,但是這個時候,還沒按鍵中斷産生。進行處理:
void buttons_timer_function(unsigned long data)
{
if (!pinval) //如果按鍵值是空的,直接return
return;
}
11、如果有多個中斷,不斷有抖動,總是把時間基于目前時間推遲10ms。這樣就可以把多個中斷,合成一個定時器處理。
驅動程式buttons.c
/*
一、驅動架構:
1.先定義file_operations結構體,其中有對裝置的打開,讀和寫的操作函數。
2.分别定義相關的操作函數
3.定義好對裝置的操作函數的結構體(file_operations)後,将其注冊到核心的file_operations結構數組中。
此設定的主裝置号為此結構在數組中的下标。
4.定義出口函數:解除安裝注冊到核心中的裝置相關資源
5.修飾 入口 和 出口函數
6.給系統提供更多的核心消息,在sys目錄下提供裝置的相關資訊。應用程式udev可以據此自動建立裝置節點,
建立一個class裝置類,在此類下建立裝置
*/
#include <linux/module.h> //内涵頭檔案,含有一些核心常用函數的原形定義。
#include <linux/kernel.h> //最基本的檔案,支援動态添加和解除安裝子產品。Hello World驅動要這一個檔案就可以。
#include <linux/fs.h> //包含了檔案操作相關的struct的定義,例如struct file_operations
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/irq.h>
#include <asm/uaccess.h> //包含了copy_to_user、copy_from_user等核心通路使用者程序記憶體位址的函數定義
#include <asm/irq.h>
#include <asm/io.h> //包含了ioremap、ioread等核心通路IO記憶體等函數的定義
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
#include <linux/poll.h>
static struct class *sixthdrv_class; //一個類
static struct class_device *sixthdrv_class_dev; //一個類裡面再建立一個裝置
volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;
volatile unsigned long *gpgcon;
volatile unsigned long *gpgdat;
static struct timer_list buttons_timer;
/* 下面兩個是定義休眠函數的參數 */
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
/* 中斷時間标志,中斷服務程式将它置1,sixth_drv_read将它清0 */
static volatile int ev_press=0;
static struct fasync_struct *button_async;
/* 引腳描述的結構體 */
struct pin_desc{
unsigned int pin;
unsigned int key_val;
};
/* 鍵值:按下時,0x01,0x02,0x03,0x04 */
/* 鍵值:松開時,0x81,0x82,0x83,0x84 */
static unsigned char keyval; //鍵值
/* 在request_irq函數中把結構體傳進去 */
struct pin_desc pins_desc[4] = { //鍵值先賦初始值0x01,0x02,0x03,0x04
{S3C2410_GPF0, 0x01}, //pin=S3C2410_GPF0, key_val(按鍵值)=0x01
{S3C2410_GPF2, 0x02}, //pin=S3C2410_GPF2, key_val(按鍵值)=0x02
{S3C2410_GPG3, 0x03}, //pin=S3C2410_GPF3, key_val(按鍵值)=0x03
{S3C2410_GPG11, 0x04}, //pin=S3C2410_GPF11, key_val(按鍵值)=0x04
};
static struct pin_desc *irq_pd; //發生中斷時的引腳描述
//static atomic_t canopen = ATOMIC_INIT(1); //定義原子變量并初始化為1
static DECLARE_MUTEX(button_lock); //定義互斥鎖
/*
* 确定按鍵值
*/
static irqreturn_t button_irq(int irq, void *dev_id) //中斷處理函數
{
//為了防抖動,修改定時器的逾時時間10ms,逾時時間就是鬧鐘什麼時候到
//逾時時間基于jiffies
//1秒就是HZ,HZ=100。說明1秒内,jiffies增加100
//10ms後啟動定時器
irq_pd = (struct pin_desc *)dev_id;//把它記錄下來
mod_timer(&buttons_timer, jiffies+HZ/100);//HZ=1s,1s/100=10ms
return IRQ_RETVAL(IRQ_HANDLED);
}
static int sixth_drv_open(struct inode *inode, struct file *file)
{
#if 0
//自減。如果等于0,還沒有打開它
if(!atomic_dec_and_test(&canopen) != 0)
{
atomic_inc(&canopen);
return -EBUSY;
}
#endif
if (file->f_flags & O_NONBLOCK)//标記位擷取,如果flags是O_NONBLOCK
{
//非阻塞
if( down_trylock(&button_lock))//如果open打不開就傳回錯誤,
{
//down_trylock獲得信号量,傳回0,否則傳回非0值
return -EBUSY;
}
}
else
{
//阻塞
/* 擷取信号量 */
down(&button_lock);//無法擷取信号量,休眠
}
/* 配置GPF0,2為輸入引腳 */
/* 配置GPF3,11為輸入引腳 */
/* request_irq函數的第五個參數是void *,為無類型指針,可以指向任何資料類型 */
request_irq(IRQ_EINT0, button_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
request_irq(IRQ_EINT2, button_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
request_irq(IRQ_EINT11, button_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
request_irq(IRQ_EINT19, button_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);
return 0;
}
ssize_t sixth_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
if (size != 1)
return -EINVAL;
if( file->f_flags & O_NONBLOCK)//如果open打不開就傳回錯誤,
{
//非阻塞
if (!ev_press)
return -EAGAIN;
}
else//阻塞
{
/* 如果沒有按鍵動作,休眠,休眠:讓出CPU */
/* 休眠時,把程序挂在button_wq 隊列裡 */
/* 如果休眠後被喚醒,就會從這裡繼續往下執行 */
/* 一開始沒有按鍵按下,ev_press = 0 */
wait_event_interruptible(button_waitq, ev_press);//ev_press=0,休眠,讓我們的測試程式休眠;ev_press!=0,直接往下運作
}
/* 如果有按鍵動作,傳回鍵值 */
copy_to_user(buf, &keyval, 1); //把鍵值 拷回去
ev_press = 0; //清零,如果不清零,下次再讀,立馬往下執行,傳回原來的值
return 1;
}
int sixth_drv_close(struct inode *inode, struct file *file)
{
//atomic_inc(&canopen);
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]);
up(&button_lock);
return 0;
}
static unsigned sixth_drv_poll(struct file *file, poll_table *wait)
{
unsigned int mask = 0;
//poll_wait會調用sys_poll的__pollwait函數
poll_wait(file, &button_waitq, wait); //不會立即休眠,這隻是讓程序挂到隊列裡面去。
//休眠是在"do_poll"中的"schedule_timeout()"
//ev_press=0,休眠,ev_press=1,喚醒
if (ev_press) //如果目前有資料可以傳回應用程式,否則mask=0
mask |= POLLIN | POLLRDNORM;
return mask;//如果傳回0,do_poll的count++就不會執行,往下就會休眠schedule_timeout()
}
static int sixth_drv_fasync(int fd, struct file * filp, int on) //設定信号發給誰
{
//測試,是否應用程式調用fcntl(fd, F_SETFL, Oflags | FASYNC)會執行到這裡去
printk("driver: sixth_drv_fasync\n");
//fasync_helper初始化或者釋放button_async
return fasync_helper(fd, filp, on, &button_async); //初始化button_async結構體後,中斷服務程式才能使用kill_fasync發信号
}
static struct file_operations sixth_drv_fops = {
.owner = THIS_MODULE, /* 這是一個宏,推向編譯子產品時自動建立的__this_module變量 */
.open = sixth_drv_open,
.read = sixth_drv_read,
.release = sixth_drv_close,
.poll = sixth_drv_poll,
.fasync = sixth_drv_fasync,
};
int major;
void buttons_timer_function(unsigned long data)
{
/* irq = IRQ_EINT0 …… */
/* dev_id = 結構體struct pins_desc */
struct pin_desc * pindesc = irq_pd;
unsigned int pinval;
if (!pinval)//如果按鍵值是空的,直接return
return;
/* 讀取引腳PIN值 */
pinval = s3c2410_gpio_getpin(pindesc->pin);
/* 确定按鍵值,按下管腳低電平,松開管腳高電平 */
if(pinval)
{
/* 松開 */
keyval = 0x80 | pindesc->key_val; //規定的:0x8X
}
else
{
/* 按下 */
keyval = pindesc->key_val; //0x0X
}
/* 喚醒 */
ev_press = 1; /* 表示中斷發生了 */
wake_up_interruptible(&button_waitq); /* 喚醒休眠的程序,去button_wq隊列,把挂在隊列下的程序喚醒 */
//發送信号SIGIO信号給fasync_struct結構體所描述的PID,觸發應用程式的SIGIO信号處理函數
kill_fasync(&button_async, SIGIO, POLL_IN); //有按鍵按下,就發出信号給應用程式(發給誰,是在button_async結構中定義的)
}
static int sixth_drv_init(void)
{
//定時器timer初始化
init_timer(&buttons_timer);
//設定處理函數
buttons_timer.function = buttons_timer_function;//當定時器的逾時時間到了之後,就會調用此函數buttons_timer_function
//buttons_timer.expires=0;
add_timer(&buttons_timer);//把定時器告訴核心/* jiffies>=0 */
major = register_chrdev(0, "sixth_drv", &sixth_drv_fops);
//建立一個類
sixthdrv_class = class_create(THIS_MODULE, "firstdrv");
//在這個類下面再建立一個裝置
//mdev是udev的一個簡化版本
//mdev應用程式,就會被核心調用,會根據類和類下面的裝置這些資訊
sixthdrv_class_dev = class_device_create(sixthdrv_class, NULL, MKDEV(major, 0), NULL, "buttons");/* /dev/buttons */
//建立位址映射:實體位址->虛拟位址
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16); //指向的是虛拟位址,第一個參數是實體開始位址,第二個是長度(位元組)
gpfdat = gpfcon + 1; //加1,實際加4個位元組
gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16); //指向的是虛拟位址,第一個參數是實體開始位址,第二個是長度(位元組)
gpgdat = gpgcon + 1; //加1,實際加4個位元組
return 0;
}
static void sixth_drv_exit(void)
{
unregister_chrdev(major, "sixth_drv");
class_device_unregister(sixthdrv_class_dev);
class_destroy(sixthdrv_class);
iounmap(gpfcon);
iounmap(gpgcon);
return 0;
}
module_init(sixth_drv_init);
module_exit(sixth_drv_exit);
MODULE_LICENSE("GPL");
測試應用程式:buttons_test.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <poll.h>
#include <signal.h> //信号處理需要這個頭檔案
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
/* sixthdrvtest
*/
int fd;
//信号處理函數會在驅動中斷服務程式裡,如果有按鍵按下,通過kill_fasync發送信号
void my_signal_fun(int signum)
{
unsigned char key_val;
read(fd, &key_val, 1); //讀取按鍵值,這就是為什麼把fd作為全局變量的原因
printf("key_val:0x%x\n", key_val);
}
int main(int argc, char **argv)
{
unsigned char key_val;
int ret;
int Oflags;
//做什麼事情,需要一個信号處理函數
//在應用程式中捕捉SIGIO信号(由驅動程式發送),接受到SIGIO信号時,執行my_signal_fun函數
//signal(SIGIO, my_signal_fun); //SIGIO:表示IO口有資料讓你讀和寫
fd = open("/dev/buttons", O_RDWR);
if (fd < 0)
{
printf("can't open!\n");
return -1;
}
//把程序ID号告訴給驅動程式,否則驅動不知道發給誰
//fcntl(fd, F_SETOWN, getpid()); // 告訴核心,發給誰
//擷取fd的打開方式
//Oflags = fcntl(fd, F_GETFL);//通過F_GETFL讀出flags,在flags上置上"FASYNC"位
//支援F_SETFL指令的處理,每當FASYNC标志改變時, ※※驅動程式中的fasync()函數将得以執行
//将fd的打開方式設定為FASYNC---即 支援異步通知
//該行代碼執行會觸發 驅動程式中 file_operation->fasync函數 ---函數
//調用fasync_helper初始化一個fasync_struct結構體,該結構體描述了将要發送信号的程序PID(fasync_struct->fa_file->f_fowner->p_id)
//fcntl(fd, F_SETFL, Oflags | FASYNC); // 改變fasync标記,最終會調用到驅動的fasync > fasync_helper:初始化/釋放fasync_struct
/* 主函數隻用sleep,所有工作在"信号處理函數"中實作 */
while (1)
{
ret = read(fd, &key_val, 1); //讀取按鍵值,這就是為什麼把fd作為全局變量的原因
printf("key_val:0x%x, ret = %d\n", key_val, ret);
//sleep(5);
}
return 0;
}
Makefile檔案
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 += buttons.o