DIY编程器网

标题: 嵌入式Linux设备驱动开发之:按键驱动程序实例 [打印本页]

作者: admin    时间: 2014-10-10 07:33
标题: 嵌入式Linux设备驱动开发之:按键驱动程序实例
        11.6  按键驱动程序实例

        11.6.1  按键工作原理

        LED和蜂鸣器是最简单的GPIO的应用,都不需要任何外部输入或控制。按键同样使用GPIO接口,但按键本身需要外部的输入,即在驱动程序中要处理外部中断。按键硬件驱动原理图如图11-7所示。在图11-7的4×4矩阵按键(K1~K16)电路中,使用4个输入/输出端口(EINT0、EINT2、EINT11和EINT19)和4个输出端口(KSCAN0~KSCAN3)。
         
       

        图11.7  按键驱动电路原理图

         
        按键驱动电路使用的端口和对应的寄存器如表11-18所示。
        表11.18 按键电路的主要端口
                                                                        管    脚
                       
                                                                        端    口
                       
                                                                        输入/输出
                       
                                                                         
                       
                                                                        管    脚
                       
                                                                        端    口
                       
                                                                        输入/输出
                       
                                                                        KEYSCAN0
                       
                                                                        GPE11
                       
                                                                        输出
                       
                                                                        EINT0
                       
                                                                        EINIT0/GPF0
                       
                                                                        输入/输出
                       
                                                                        KEYSCAN1
                       
                                                                        GPG6
                       
                                                                        输出
                       
                                                                        EINT2
                       
                                                                        EINT2/GPF2
                       
                                                                        输入/输出
                       
                                                                        KEYSCAN2
                       
                                                                        GPE13
                       
                                                                        输出
                       
                                                                        EINT11
                       
                                                                        EINT11/GPG3
                       
                                                                        输入/输出
                       
                                                                        KEYSCAN3
                       
                                                                        GPG2
                       
                                                                        输出
                       
                                                                        EINT19
                       
                                                                        EINT19/GPG11
                       
                                                                        输入/输出
                       
         
        因为通常中断端口是比较珍贵且有限的资源,所以在本电路设计中,16个按键复用了4个中断线。那怎么样才能及时而准确地对矩阵按键进行扫描呢?
         
        某个中断的产生表示,与它所对应的矩阵行的4个按键中,至少有一个按键被按住了。因此可以通过查看产生了哪个中断,来确定在矩阵的哪一行中发生了按键操作(按住或释放)。例如,如果产生了外部2号线中断(EINT2变为低电平),则表示K7、K8、K9和K15中至少有一个按键被按住了。这时候4个EINT端口应该通过GPIO配置寄存器被设置为外部中断端口,而且4个KSCAN端口的输出必须为低电平。
         
        在确定按键操作所在行的位置之后,我们还得查看按键操作所在列的位置。此时要使用KSCAN端口组,同时将4个EINT端口配置为通用输入端口(而不是中断端口)。在4个KSCAN端口中,轮流将其中某一个端口的输出置为低电平,其他3个端口的输出置为高电平。这样逐列进行扫描,直到按键所在列的KSCAN端口输出为低电平,此时按键操作所在行的EINT管脚的输入端口的值会变成低电平。例如,在确认产生了外部2号中断之后,进行逐列扫描。若发现在KSCAN1为低电平时(其他端口输出均为高电平),GPF2(EINT2管脚的输入端口)变为低电平,则可以断定按键K8被按住了。
         
        以上的讨论都是在按键的理想状态下进行的,但实际的按键动作会在短时间(几毫秒至几十毫秒)内产生信号抖动。例如,当按键被按下时,其动作就像弹簧的若干次往复运动,将产生几个脉冲信号。一次按键操作将会产生若干次按键中断,从而会产生抖动现象。因此驱动程序中必须要解决去除抖动所产生的毛刺信号的问题。
         
        11.6.2  按键驱动程序

        首先按键设备相关的数据结构的定义如下所示:
         
        /* butt_drv.h */
        ……
        typedef struct _st_key_info_matrix            /* 按键数据结构 */
        {
            unsigned char    key_id;                    /* 按键ID */
            unsigned int    irq_no;                    /* 对应的中断号 */
            unsigned int    irq_gpio_port;            /* 对应的中断线的输入端口地址*/
            unsigned int    kscan_gpio_port;        /* 对应的KSCAN端口地址 */
        } st_key_info_matrix;
         
        typedef struct _st_key_buffer                /* 按键缓冲数据结构 */
        {
            unsigned long jiffy[MAX_KEY_COUNT];    /* 按键时间, 5s以前的铵键作废*/
            unsigned char buf[MAX_KEY_COUNT];            /* 按键缓冲区 */
            unsigned int head,tail;                    /* 按键缓冲区头和尾 */
        } st_key_buffer;
        ……
         
        下面是矩阵按键数组的定义,数组元素的信息(一个按键信息)按照0行0列,0行1列,…,3行2列,3行3列的顺序逐行排列。
         
        static st_key_info_matrix key_info_matrix[MAX_COLUMN][MAX_ROW] =
        {
            {{10,    IRQ_EINT0,  S3C2410_GPF0,   S3C2410_GPE11},     /* 0行0列 */
            {11,    IRQ_EINT0,  S3C2410_GPF0,   S3C2410_GPG6},
            {12,    IRQ_EINT0,  S3C2410_GPF0,   S3C2410_GPE13},
            {16,    IRQ_EINT0,  S3C2410_GPF0,   S3C2410_GPG2}},
         
            {{7,    IRQ_EINT2,  S3C2410_GPF2, S3C2410_GPE11},     /* 1行0列 */
            {8,     IRQ_EINT2,  S3C2410_GPF2,  S3C2410_GPG6},
            {9,     IRQ_EINT2,  S3C2410_GPF2,   S3C2410_GPE13},
            {15,    IRQ_EINT2,  S3C2410_GPF2,   S3C2410_GPG2}},
         
            {{4,    IRQ_EINT11, S3C2410_GPG3,  S3C2410_GPE11},       /* 2行0列 */
            {5,     IRQ_EINT11, S3C2410_GPG3,  S3C2410_GPG6},
            {6,     IRQ_EINT11, S3C2410_GPG3,  S3C2410_GPE13},
            {14,    IRQ_EINT11, S3C2410_GPG3,   S3C2410_GPG2}},
         
            {{1,    IRQ_EINT19, S3C2410_GPG11, S3C2410_GPE11},      /* 3行0列 */
            {2,     IRQ_EINT19, S3C2410_GPG11, S3C2410_GPG6},
            {3,     IRQ_EINT19, S3C2410_GPG11, S3C2410_GPE13},
            {13,    IRQ_EINT19, S3C2410_GPG11, S3C2410_GPG2}},
        };
         
        下面是与按键相关的端口的初始化函数。这些函数已经在简单的GPIO字符设备驱动程序里被使用过。此外,set_irq_type()函数用于设定中断线的类型,在本实例中通过该函数将4个中断线的类型配置为下降沿触发式。
         
        static void init_gpio(void)
        {
            s3c2410_gpio_cfgpin(S3C2410_GPE11, S3C2410_GPE11_OUTP); /* GPE11 */
            s3c2410_gpio_setpin(S3C2410_GPE11, 0);
            s3c2410_gpio_cfgpin(S3C2410_GPE13, S3C2410_GPE13_OUTP); /* GPE13 */
            s3c2410_gpio_setpin(S3C2410_GPE13, 0);
            s3c2410_gpio_cfgpin(S3C2410_GPG2, S3C2410_GPG2_OUTP); /* GPG2 */
            s3c2410_gpio_setpin(S3C2410_GPG2, 0);
            s3c2410_gpio_cfgpin(S3C2410_GPG6, S3C2410_GPG6_OUTP); /* GPG6 */
            s3c2410_gpio_setpin(S3C2410_GPG6, 0);
         
            s3c2410_gpio_cfgpin(S3C2410_GPF0, S3C2410_GPF0_EINT0); /* GPF0 */
            s3c2410_gpio_cfgpin(S3C2410_GPF2, S3C2410_GPF2_EINT2); /* GPF2 */
            s3c2410_gpio_cfgpin(S3C2410_GPG3, S3C2410_GPG3_EINT11); /* GPG3 */
            s3c2410_gpio_cfgpin(S3C2410_GPG11, S3C2410_GPG11_EINT19); /* GPG11 */
         
            set_irq_type(IRQ_EINT0, IRQT_FALLING);
            set_irq_type(IRQ_EINT2, IRQT_FALLING);
            set_irq_type(IRQ_EINT11, IRQT_FALLING);
            set_irq_type(IRQ_EINT19, IRQT_FALLING);
        }
         
        下面讲解按键驱动的主要接口,以下为驱动模块的入口和卸载函数。
         
        /* 初始化并添加struct cdev结构到系统之中 */
        static void button_setup_cdev(struct cdev *dev,
                                int minor, struct file_operations *fops)
        {
            int err;
            int devno = MKDEV(button_major,minor);
            cdev_init(dev, fops); /* 初始化结构体struct cdev */
            dev->owner = THIS_MODULE;
            dev->ops = fops; /* 关联到设备的file_operations结构 */
            err = cdev_add(dev, devno, 1); /* 将struct cdev结构添加到系统之中 */
            if (err)
            {
                printk(KERN_INFO"Error %d adding button %d\n",err, minor);
            }
        }
        ……
        /* 驱动初始化 */
        static int  button_init(void)
        {
            int ret;
            /* 将主设备号和次设备号定义到一个dev_t数据类型的结构体之中 */
            dev_t dev = MKDEV(button_major, 0);
            if (button_major)
            {/* 静态注册一个设备,设备号先前指定好,并设定设备名,用cat /proc/devices来查看 */
                ret = register_chrdev_region(dev, 1, BUTTONS_DEVICE_NAME);
            }
            else
            { /*由系统动态分配主设备号 */
                ret = alloc_chrdev_region(&dev, 0, 1, BUTTONS_DEVICE_NAME);
                button_major = MAJOR(dev); /* 获得主设备号 */
            }
         
            if (ret < 0)
            {
                printk(KERN_WARNING"Button:unable to get major %d\n",button_major);
               return ret;
            }
            /* 初始化和添加结构体struct cdev到系统之中 */
            button_setup_cdev(&button_dev, 0, &button_fops);
            printk("Button driver initialized.\n");
            return 0;
        }
        /* 驱动卸载 */
        static void __exit button_exit(void)
        {
            cdev_del(&button_dev); /* 删除结构体struct cdev */
            /* 卸载设备驱动所占有的资源 */
            unregister_chrdev_region(MKDEV(button_major, 0), 1);
            printk("Button driver uninstalled\n");
        }
        module_init(button_init); /* 初始化设备驱动程序的入口 */
        module_exit(button_exit); /* 卸载设备驱动程序的入口 */
        MODULE_AUTHOR("David");
        MODULE_LICENSE("Dual BSD/GPL");
         
        按键字符设备的file_operations结构定义为:
         
        static struct file_operations button_fops =
        {
            .owner = THIS_MODULE,
            .ioctl = button_ioctl,
            .open = button_open,
            .read = button_read,
            .release = button_release,
        };
         
        以下为open和release函数接口的实现。
         
        /* 打开文件, 申请中断 */
        static int button_open(struct inode *inode,struct file *filp)
        {
            int ret = nonseekable_open(inode, filp);
            if (ret < 0)
            {
                return ret;
            }
         
            init_gpio();                /* 相关GPIO端口的初始化*/
            ret = request_irqs();     /* 申请4个中断 */
            if (ret < 0)
            {
                return ret;
            }
            init_keybuffer();            /* 初始化按键缓冲数据结构 */
            return ret;
        }
         
        /* 关闭文件, 屏蔽中断 */
        static int button_release(struct inode *inode,struct file *filp)
        {
            free_irqs();                /* 屏蔽中断 */
            return 0;
        }
         
        在open函数接口中,进行了GPIO端口的初始化、申请硬件中断以及按键缓冲的初始化等工作。在以前的章节中提过,中断端口是比较宝贵而且数量有限的资源。因此需要注意,最好要在第一次打开设备时申请(调用request_irq函数)中断端口,而不是在驱动模块加载的时候申请。如果已加载的设备驱动占用而在一定时间段内不使用某些中断资源,则这些资源不会被其他驱动所使用,只能白白浪费掉。而在打开设备的时候(调用open函数接口)申请中断,则不同的设备驱动可以共享这些宝贵的中断资源。
         
        以下为中断申请和释放的部分以及中断处理函数。
         
        /* 中断处理函数,其中irq为中断号 */
        static irqreturn_t button_irq(int irq, void *dev_id, struct pt_regs *regs)
        {
            unsigned char ucKey = 0;
         
            disable_irqs();        /* 屏蔽中断 */
            /* 延迟50ms, 屏蔽按键毛刺 */
            udelay(50000);
            ucKey = button_scan(irq);    /* 扫描按键,获得进行操作的按键的ID */
            if ((ucKey >= 1) && (ucKey <= 16))
                {
                /* 如果缓冲区已满, 则不添加 */
                if (((key_buffer.head + 1) & (MAX_KEY_COUNT - 1)) != key_buffer.tail)
                {
                    spin_lock_irq(&buffer_lock);
                    key_buffer.buf[key_buffer.tail] = ucKey;
                     key_buffer.jiffy[key_buffer.tail] = get_tick_count();
                    key_buffer.tail ++;
                    key_buffer.tail &= (MAX_KEY_COUNT -1);
                    spin_unlock_irq(&buffer_lock);
                }
            }
            init_gpio();        /* 初始化GPIO端口,主要是为了恢复中断端口配置 */
            enable_irqs();      /* 开启中断 */
            return IRQ_HANDLED;/* 2.6内核返回值一般是这个宏 */
        }
        /* 申请4个中断 */
        static  int request_irqs(void)
        {
            int ret, i, j;
            for (i = 0; i < MAX_COLUMN; i++)
            {
                ret = request_irq(key_info_matrix[0].irq_no,
        button_irq, SA_INTERRUPT, BUTTONS_DEVICE_NAME, NULL);
                if (ret < 0)
                {
                    for (j = 0; j < i; j++)
                    {
                        free_irq(key_info_matrix[j][0].irq_no, NULL);
                    }
                    return -EFAULT;
                }
            }
            return 0;
        }
        /* 释放中断 */
        static __inline void free_irqs(void)
        {
            int i;
            for (i = 0; i < MAX_COLUMN; i++)
            {
                free_irq(key_info_matrix[0].irq_no, NULL);
            }
        }
         
        中断处理函数在每次中断产生的时候会被调用,因此它的执行时间要尽可能得短。通常中断处理函数只是简单地唤醒等待资源的任务,而复杂且耗时的工作则让这个任务去完成。中断处理函数不能向用户空间发送数据或者接收数据,不能做任何可能发生睡眠的操作,而且不能调用schedule()函数。
         
        为了简单起见,而且考虑到按键操作的时间比较长,在本实例中的中断处理函数button_irq()里,通过调用睡眠函数来消除毛刺信号。读者可以根据以上介绍的对中断处理函数的要求改进该部分代码。
         
        按键扫描函数如下所示。首先根据中断号确定操作按键所在行的位置,然后采用逐列扫描法最终确定操作按键所在的位置。
         
        /*
        ** 进入中断后, 扫描铵键码
        ** 返回: 按键码(1~16), 0xff表示错误
        */
        static __inline unsigned char button_scan(int irq)
        {
            unsigned char key_id = 0xff;
            unsigned char column = 0xff, row = 0xff;   
            
            s3c2410_gpio_cfgpin(S3C2410_GPF0, S3C2410_GPF0_INP); /* GPF0 */
            s3c2410_gpio_cfgpin(S3C2410_GPF2, S3C2410_GPF2_INP); /* GPF2 */
            s3c2410_gpio_cfgpin(S3C2410_GPG3, S3C2410_GPG3_INP); /* GPG3 */
            s3c2410_gpio_cfgpin(S3C2410_GPG11, S3C2410_GPG11_INP); /* GPG11 */
            
            switch (irq)
            { /* 根据irq值确定操作按键所在行的位置*/
                case IRQ_EINT0:
                {
                    column = 0;
                }
                break;
                case IRQ_EINT2:
                {
                    column = 1;
                }
                break;
                case IRQ_EINT11:
                {
                    column = 2;
                }
                break;
                case IRQ_EINT19:
                {
                    column = 3;
                }
                break;
            }   
            if (column != 0xff)
            { /* 开始逐列扫描, 扫描第0列 */
                s3c2410_gpio_setpin(S3C2410_GPE11, 0); /* 将KSCAN0置为低电平 */
                s3c2410_gpio_setpin(S3C2410_GPG6, 1);
                s3c2410_gpio_setpin(S3C2410_GPE13, 1);
                s3c2410_gpio_setpin(S3C2410_GPG2, 1);
               if(!s3c2410_gpio_getpin(key_info_matrix[column][0].irq_gpio_port))
                { /* 观察对应的中断线的输入端口值 */
                    key_id = key_info_matrix[column][0].key_id;
                    return key_id;
                }
                /* 扫描第1列*/
                s3c2410_gpio_setpin(S3C2410_GPE11, 1);
                s3c2410_gpio_setpin(S3C2410_GPG6, 0); /* 将KSCAN1置为低电平 */
                s3c2410_gpio_setpin(S3C2410_GPE13, 1);
                s3c2410_gpio_setpin(S3C2410_GPG2, 1);
                if(!s3c2410_gpio_getpin(key_info_matrix[column][1].irq_gpio_port))
                {
                    key_id = key_info_matrix[column][1].key_id;
                    return key_id;
                }
                /* 扫描第2列*/
                s3c2410_gpio_setpin(S3C2410_GPE11, 1);
                s3c2410_gpio_setpin(S3C2410_GPG6, 1);
                s3c2410_gpio_setpin(S3C2410_GPE13, 0); /* 将KSCAN2置为低电平 */
                s3c2410_gpio_setpin(S3C2410_GPG2, 1);
                if(!s3c2410_gpio_getpin(key_info_matrix[column][2].irq_gpio_port))
                {
                    key_id = key_info_matrix[column][2].key_id;
                    return key_id;
                }
                /* 扫描第3列*/
                s3c2410_gpio_setpin(S3C2410_GPE11, 1);
                s3c2410_gpio_setpin(S3C2410_GPG6, 1);
                s3c2410_gpio_setpin(S3C2410_GPE13, 1);
                s3c2410_gpio_setpin(S3C2410_GPG2, 0); /* 将KSCAN3置为低电平 */
                if(!s3c2410_gpio_getpin(key_info_matrix[column][3].irq_gpio_port))
                {
                    key_id = key_info_matrix[column][3].key_id;
                    return key_id;
                }
            }
            return key_id;
        }
         
        以下是read函数接口的实现。首先在按键缓冲中删除已经过时的按键操作信息,接下来,从按键缓冲中读取一条信息(按键ID)并传递给用户层。
         
        /* 从缓冲删除过时数据(5s前的按键值) */
        static void remove_timeoutkey(void)
        {
            unsigned long tick;
            spin_lock_irq(&buffer_lock); /* 获得一个自旋锁 */
            while(key_buffer.head != key_buffer.tail)
            {
                tick = get_tick_count() - key_buffer.jiffy[key_buffer.head];
                if (tick  < 5000)    /* 5s */
                    break;
                key_buffer.buf[key_buffer.head] = 0;
                key_buffer.jiffy[key_buffer.head] = 0;
                key_buffer.head ++;
                key_buffer.head &= (MAX_KEY_COUNT -1);
            }
            spin_unlock_irq(&buffer_lock); /* 释放自旋锁 */
        }
         
        /* 读键盘 */
        static ssize_t button_read(struct file *filp,
                                    char *buffer, size_t count, loff_t *f_pos)
        {
            ssize_t ret = 0;
            remove_timeoutkey(); /* 删除过时的按键操作信息 */
            spin_lock_irq(&buffer_lock);
            while((key_buffer.head != key_buffer.tail) && (((size_t)ret) < count))
            {
                put_user((char)(key_buffer.buf[key_buffer.head]), &buffer[ret]);
                key_buffer.buf[key_buffer.head] = 0;
                key_buffer.jiffy[key_buffer.head] = 0;
                key_buffer.head ++;
                key_buffer.head &= (MAX_KEY_COUNT -1);
                ret ++;
            }
            spin_unlock_irq(&buffer_lock);
            return ret;
        }
         
        以上介绍了按键驱动程序中的主要内容。
         
        11.6.3  按键驱动的测试程序

        按键驱动程序的测试程序所下所示。在测试程序中,首先打开按键设备文件和gpio设备(包括4个LED和蜂鸣器)文件,接下来,根据按键的输入值(按键ID)的二进制形式,LED D9~D12发亮(例如,按下11号按键,则D9、D10和D12会发亮),而蜂鸣器当每次按键时发出声响。
         
        /* butt_test.c */
        #include <sys/stat.h>
        #include <fcntl.h>
        #include <stdio.h>
        #include <sys/time.h>
        #include <sys/types.h>
        #include <unistd.h>
        #include <asm/delay.h>
        #include "butt_drv.h"
        #include "gpio_drv.h"
         
        main()
        {
            int butt_fd, gpios_fd, i;
            unsigned char key = 0x0;
            butt_fd = open(BUTTONS_DEVICE_FILENAME, O_RDWR); /* 打开按钮设备 */
            if (butt_fd == -1)
            {
                printf("Open button device button errr!\n");
                return 0;
            }
            
            gpios_fd = open(GPIO_DEVICE_FILENAME, O_RDWR); /* 打开GPIO设备 */
            if (gpios_fd == -1)
            {
                printf("Open button device button errr!\n");
                return 0;
            }
         
            ioctl(butt_fd, 0);    /* 清空键盘缓冲区, 后面参数没有意义 */
            printf("Press No.16 key to exit\n");
            do
            {   
                if (read(butt_fd, &key, 1) <= 0) /* 读键盘设备,得到相应的键值 */
                {
                    continue;
                }
            
                printf("Key Value = %d\n", key);
                for (i = 0; i < LED_NUM; i++)
                {
                    if ((key & (1 << i)) != 0)
                    {
                        ioctl(gpios_fd, LED_D09_SWT + i, LED_SWT_ON); /* LED发亮*/
                    }
                }
                ioctl(gpios_fd, BEEP_SWT, BEEP_SWT_ON); /* 发声*/
         
                sleep(1);
                for (i = 0; i < LED_NUM; i++)
                {
                    ioctl(gpios_fd, LED_D09_SWT + i, LED_SWT_OFF);    /* LED熄灭*/
                }
                ioctl(gpios_fd, BEEP_SWT, BEEP_SWT_OFF);
         
            } while(key != 16); /* 按16号键则退出 */
            close(gpios_fd);
            close(butt_fd);
            return 0;
        }
         
        首先编译和加载按键驱动程序,而且要创建设备文件节点。
         
        $ make clean;make    /* 驱动程序的编译*/
        $ insmod butt_dev.ko /* 加载buttons设备驱动 */
        $ cat /proc/devices  /* 通过这个命令可以查到buttons设备的主设备号 */
        $ mknod /dev/buttons  c  252  0  /* 假设主设备号为252, 创建设备文件节点*/
         
        接下来,编译和加载GPIO驱动程序,而且要创建设备文件节点。
         
        $ make clean;make /* 驱动程序的编译*/
        $ insmod gpio_drv.ko /* 加载GPIO驱动 */
        $ cat /proc/devices /* 通过这个命令可以查到GPIO设备的主设备号 */
        $ mknod /dev/gpio  c  251  0  /* 假设主设备号为251, 创建设备文件节点*/
         
        然后编译并运行驱动测试程序。
         
        $ arm-linux-gcc &ndash;o butt_test  butt_test.c
        $ ./butt_test




欢迎光临 DIY编程器网 (http://diybcq.com/) Powered by Discuz! X3.2