星空网 > 软件开发 > 操作系统

驱动02.按键

一.以查询方式实现

1.写出驱动框架
  1.1 仿照其他程序加一些必要的头文件
  1.2 构造一个结构体file_operations
  1.3 根据file_operations的所选项写出所需的函数,并构建出来
  1.4 入口函数、出口函数的注册和卸载
  1.5 修饰入口函数和出口函数
  1.6 给sysfs提供更多的信息,并有udev机制自动创建/dev/xxx设备节点
2.硬件操作
  2.1 sch原理图
  2.2 2440手册
  2.3 编程:物理地址到虚拟地址的映射

驱动02.按键images/loading.gif' data-original="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif" >驱动02.按键
 1 /* 2  *以查询的方式实现按键驱动程序 3  */ 4  5 #include <linux/module.h> 6 #include <linux/kernel.h> 7 #include <linux/fs.h> 8 #include <linux/init.h> 9 #include <linux/delay.h> 10 #include <asm/uaccess.h> 11 #include <asm/irq.h> 12 #include <asm/io.h> 13 #include <asm/arch/regs-gpio.h> 14 #include <asm/hardware.h> 15  16  17 static struct class *g_ptSeconddrvCls; 18 static struct class_device *g_ptSeconddrvClsDev; 19  20 volatile unsigned long *gpfcon; 21 volatile unsigned long *gpfdat; 22  23 volatile unsigned long *gpgcon; 24 volatile unsigned long *gpgdat; 25  26 static int seconddrv_open(struct inode *inode, struct file *file) 27 { 28   /* 配置GPF0,2为输入引脚 */ 29   *gpfcon &= ~((0x3<<(0*2)) | (0x3<<(2*2))); 30  31   /* 配置GPG3,11为输入引脚 */ 32   *gpgcon &= ~((0x3<<(3*2)) | (0x3<<(11*2))); 33  34   return 0; 35 } 36  37 static ssize_t seconddrv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos) 38 { 39   /* 返回4个引脚的电平 */ 40   unsigned char key_vals[4]; 41   int regval; 42  43   if (size != sizeof(key_vals)) 44     return -EINVAL; 45  46   /* 读GPF0,2 */ 47   regval = *gpfdat; 48   key_vals[0] = (regval & (1<<0)) ? 1 : 0; 49   key_vals[1] = (regval & (1<<2)) ? 1 : 0; 50    51  52   /* 读GPG3,11 */ 53   regval = *gpgdat; 54   key_vals[2] = (regval & (1<<3)) ? 1 : 0; 55   key_vals[3] = (regval & (1<<11)) ? 1 : 0; 56  57   copy_to_user(buf, key_vals, sizeof(key_vals)); 58    59   return sizeof(key_vals); 60 } 61  62 static struct file_operations seconddrv_fops = { 63   .owner = THIS_MODULE, 64   .open  = seconddrv_open,    65   .read  =  seconddrv_read, 66 }; 67  68  69 int g_iMajor; 70 static int seconddrv_init(void) 71 { 72   g_iMajor = register_chrdev(0, "second_drv", &seconddrv_fops); 73  74   g_ptSeconddrvCls = class_create(THIS_MODULE, "second_drv"); 75  76   g_ptSeconddrvClsDev = class_device_create(g_ptSeconddrvCls, NULL, MKDEV(g_iMajor, 0), NULL, "buttons"); /* /dev/buttons */ 77  78   gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16); 79   gpfdat = gpfcon + 1; 80  81   gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16); 82   gpgdat = gpgcon + 1; 83  84   return 0; 85 } 86  87 static int seconddrv_exit(void) 88 { 89   unregister_chrdev(g_iMajor, "second_drv"); 90   class_device_unregister(g_ptSeconddrvClsDev); 91   class_destroy(g_ptSeconddrvCls); 92   iounmap(gpfcon); 93   iounmap(gpgcon); 94   return 0; 95 } 96  97 module_init(seconddrv_init); 98  99 module_exit(seconddrv_exit);100 MODULE_LICENSE("GPL");

查询方式的按键驱动程序

驱动02.按键驱动02.按键
 1 #include <sys/types.h> 2 #include <sys/stat.h> 3 #include <fcntl.h> 4 #include <stdio.h> 5  6 /* 2nddrvtest  7  */ 8 int main(int argc, char **argv) 9 {10   int fd;11   unsigned char key_vals[4];12   int cnt = 0;13   14   fd = open("/dev/buttons", O_RDWR);15   if (fd < 0)16   {17     printf("can't open!\n");18   }19 20   while (1)21   {22     read(fd, key_vals, sizeof(key_vals));23     if (!key_vals[0] || !key_vals[1] || !key_vals[2] || !key_vals[3])24     {25       printf("%04d key pressed: %d %d %d %d\n", cnt++, key_vals[0], key_vals[1], key_vals[2], key_vals[3]);26     }27   }28   29   return 0;30 }

测试程序

 二、使用中断方式实现按键驱动程序

使用查询方式实现时,程序一直占用CPU资源,CPU利用率低。

改进的办法:使用中断的方式实现

1.linux中断异常体系分析

  1.1 异常向量的设置trap_init()
    trap_init被用来设置各种异常的处理向量,trap_init函数将异常向量复制到0xffff0000处
    trap_init
        memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
            vector_irq
                vector_stub(宏)//计算返回地址,保存一些寄存器
                    __irq_usr     //根据被中断的工作模式进入相应的某个跳转分支->进入管理模式
                        usr_entry(宏)//保存一些寄存器的值
                            .maro irq_handler(宏)
                                asm_do_IRQ //异常处理函数
  1.2 异常处理函数asm_do_IRQ
  asm_do_IRQ
      定义了一个结构体数组desc[NR_IRQS]
          irq_enter
          desc_handle_irq:desc->handle_irq
        
  1.3 结构数组desc[NR_IRQS]的构建    
  s3c24xx_init_irq
      set_irq_chip    //chip=s3c_irq_eint0t4,可设置触发方式,使能中断,禁止中断等底层芯片相关的操作
      set_irq_flags
      set_irq_handler(irqno, handle_edge_irq)//handle_irq=handle_edge_irq
          __set_irq_handler
              desc_handle_irq
  

  中断处理函数handle_edge_irq
    handle_edge_irq
        desc->chip->ack(irq)//清中断
        handle_IRQ_event//取出action链表中的成员,执行action->handler
  1.4 用户注册中断处理函数的过程request_irq
  request_irq(unsigned int irq, irq_handler_t handler,unsigned long irqflags, const char *devname, void *dev_id)
          //用户通过request_irq向内核注册中断处理函数:a.分配了irqaction结构b.调用setup_irq函数
      setup_irq(irq,action)//将新建的irqaction结构链入irq_desc[irq]结构的action链表中P405
      desc->chip->settype//定义引脚为中断引脚
      desc->chip->startup/enable//引脚使能
  1.5 free_irq(irq,dev_id)//卸载中断服务程序
      a.出链
      b.禁止中断

  1.6 总结:handle_irq是这个中断的处理函数入口,发生中断时,总入口函数asm_do_IRQ函数将根据中断号调用相应irq_desc数组项的handle_irq。handle_irq使用chip结构体中的函数来清除、屏蔽或者重新enable中断,还一一调用用户在action链表中注册的中断处理函数。

2.编写代码

驱动02.按键驱动02.按键
 1 /* 2  *用中断方式实现按键驱动程序 3  */ 4  5 #include <linux/module.h> 6 #include <linux/kernel.h> 7 #include <linux/fs.h> 8 #include <linux/init.h> 9 #include <linux/delay.h> 10 #include <linux/irq.h> 11 #include <asm/uaccess.h> 12 #include <asm/irq.h> 13 #include <asm/io.h> 14 #include <asm/arch/regs-gpio.h> 15 #include <asm/hardware.h> 16  17  18 static struct class *g_ptThirddrvCls; 19 static struct class_device *g_ptThirddrvClsDev; 20  21 volatile unsigned long *gpfcon; 22 volatile unsigned long *gpfdat; 23  24 volatile unsigned long *gpgcon; 25 volatile unsigned long *gpgdat; 26  27 static DECLARE_WAIT_QUEUE_HEAD(button_waitq); 28  29 /* 中断事件标志, 中断服务程序将它置1,third_drv_read将它清0 */ 30 static volatile int ev_press = 0; 31  32 struct pin_desc{ 33   unsigned int pin; 34   unsigned int key_val; 35 }; 36  37 static unsigned char key_val; 38  39 struct pin_desc pins_desc[4] = { 40   {S3C2410_GPF0, 0x01}, 41   {S3C2410_GPF2, 0x02}, 42   {S3C2410_GPG3, 0x03}, 43   {S3C2410_GPG11, 0x04}, 44 }; 45  46  47 /* 48  * 确定按键值 49  */ 50 static irqreturn_t buttons_irq(int irq, void *dev_id) 51 { 52   struct pin_desc * pindesc = (struct pin_desc *)dev_id; 53   unsigned int pinval; 54    55   pinval = s3c2410_gpio_getpin(pindesc->pin); 56  57   if (pinval) 58   { 59     /* 松开 */ 60     key_val = 0x80 | pindesc->key_val; 61   } 62   else 63   { 64     /* 按下 */ 65     key_val = pindesc->key_val; 66   } 67  68   ev_press = 1;         /* 表示中断发生了 */ 69   wake_up_interruptible(&button_waitq);  /* 唤醒休眠的进程 */ 70  71    72   return IRQ_RETVAL(IRQ_HANDLED); 73 } 74  75 static int thirddrv_open(struct inode *inode, struct file *file) 76 { 77   request_irq(IRQ_EINT0, buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]); 78   request_irq(IRQ_EINT2, buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]); 79   request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]); 80   request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);   81  82   return 0; 83 } 84  85 static ssize_t thirddrv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos) 86 { 87    88   if (size != 1) 89     return -EINVAL; 90  91   /* 如果没有按键动作, 休眠 */ 92   wait_event_interruptible(button_waitq, ev_press); 93  94   /* 如果有按键动作, 返回键值 */ 95   copy_to_user(buf, &key_val, 1); 96   ev_press = 0; 97    98   return 1; 99 }100 101 static int thirddrv_close (struct inode * inode, struct file * file)102 {103   free_irq(IRQ_EINT0,&pins_desc[0]);104   free_irq(IRQ_EINT2,&pins_desc[1]);105   free_irq(IRQ_EINT11,&pins_desc[2]);106   free_irq(IRQ_EINT19,&pins_desc[3]);107 108   return 0;109 }110 111 static struct file_operations thirddrv_fops = {112   .owner = THIS_MODULE,113   .open  = thirddrv_open,   114   .read  =  thirddrv_read,115   .release = thirddrv_close,116 };117 118 119 int g_iMajor;120 static int thirddrv_init(void)121 {122   g_iMajor = register_chrdev(0, "third_drv", &thirddrv_fops);123 124   g_ptThirddrvCls = class_create(THIS_MODULE, "third_drv");125 126   g_ptThirddrvClsDev = class_device_create(g_ptThirddrvCls, NULL, MKDEV(g_iMajor, 0), NULL, "buttons"); /* /dev/buttons */127 128   gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);129   gpfdat = gpfcon + 1;130 131   gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);132   gpgdat = gpgcon + 1;133 134   return 0;135 }136 137 static int thirddrv_exit(void)138 {139   unregister_chrdev(g_iMajor, "third_drv");140   class_device_unregister(g_ptThirddrvClsDev);141   class_destroy(g_ptThirddrvCls);142   iounmap(gpfcon);143   iounmap(gpgcon);144   return 0;145 }146 147 module_init(thirddrv_init);148 149 module_exit(thirddrv_exit);150 MODULE_LICENSE("GPL");

驱动程序
驱动02.按键驱动02.按键
 1 #include <sys/types.h> 2 #include <sys/stat.h> 3 #include <fcntl.h> 4 #include <stdio.h> 5 #include <unistd.h> 6  7 /* thirddrvtest  8  */ 9 int main(int argc, char **argv)10 {11   int fd;12   unsigned char key_val;13   14   fd = open("/dev/buttons", O_RDWR);15   if (fd < 0)16   {17     printf("can't open!\n");18   }19 20   while (1)21   {22     //read(fd, &key_val, 1);23     //printf("key_val = 0x%x\n", key_val);24     sleep(5);25   }26   27   return 0;28 }

测试程序

三、在上述基础上添加poll机制

  虽然我们实现了中断式的按键驱动,但是我们发觉,测试程序是一个无限的循环。在实际应用中并不存在这样的情况,所以我们要进一步优化——poll机制。

1.poll机制的分析

1.1 函数原型

int poll(struct pollfd *fds,nfds_t nfds, int timeout)
//Poll机制会判断fds中的文件是否可读,如果可读则会立即返回,返回的值就是可读fd的数量,如果不可读,那么就进程就会
休眠timeout这么长的时间,然后再来判断是否有文件可读,如果有,返回fd的数量,如果没有,则返回0.  
1.2 poll机制在内核实现分析:
应用程序调用poll
    sys_poll
        do_sys_poll
            poll_initwait(&table)//table->pt->qproc = qproc = __pollwait
            do_poll(nfds, head, &table, timeout)
                for (;;) {
                if (do_pollfd(pfd, pt)) {     //return mask;mask=file->f_op->poll(file,pwait)
                    count++;
                    pt = NULL;
                    }
                if (count || !*timeout || signal_pending(current))
                        break;//break的条件是:count非0,超时,有信号在等待处理
                    __timeout = schedule_timeout(__timeout)//休眠
                    }
1.3 总结
①poll > sys_poll > do_sys_poll >poll_initwait,poll_initwait函数注册一下回调函数__pollwait,它就是我们的驱动程序执行poll_wait时,真正被调用的函数。
②接下来执行file->f_op->poll,即我们驱动程序里自己实现的poll函数它会调用poll_wait把自己挂入某个队列,这个队列也是我们的驱动自己定义的;它还判断一下设备是否就绪。
③如果设备未就绪,do_sys_poll里会让进程休眠一定时间,这个时间是应用提供的“超时时间”
④进程被唤醒的条件有2:一是上面说的“一定时间”到了,二是被驱动程序唤醒。驱动程序发现条件就绪时,就把“某个队列”上挂着的进程唤醒,这个队列,就是前面通过poll_wait把本进程挂过去的队列。
⑤如果驱动程序没有去唤醒进程,那么schedule_timeout(__timeout)超时后,会重复2、3动作1
次,然后返回。

 2.写代码
①在测试程序中加入以下代码
while (1)
    {
        ret = poll(fds, 1, 5000);
        if (ret == 0)
        {
            printf("time out\n");
        }
int main()
{
    int ret;
    struct pollfd fds[1]
}
②在file_operations中增加一行
.poll    =  forth_drv_poll
③写函数forth_drv_poll
forth_drv_poll
    poll_wait
        p->qproc(file,wait_address,p)//相当于调用了__pollwait(file,&button_waitq,p)
                                        //把当前进程挂到button_waitq队列里去
④在增加如下代码
if (ev_press)
        mask |= POLLIN | POLLRDNORM;

驱动02.按键驱动02.按键
 1 /* 2  *使用poll 机制实现按键驱动程序 3  */ 4  5 #include <linux/module.h> 6 #include <linux/kernel.h> 7 #include <linux/fs.h> 8 #include <linux/init.h> 9 #include <linux/delay.h> 10 #include <linux/irq.h> 11 #include <asm/uaccess.h> 12 #include <asm/irq.h> 13 #include <asm/io.h> 14 #include <asm/arch/regs-gpio.h> 15 #include <asm/hardware.h> 16  17  18 static struct class *g_ptForthdrvCls; 19 static struct class_device *g_ptForthdrvClsDev; 20  21 volatile unsigned long *gpfcon; 22 volatile unsigned long *gpfdat; 23  24 volatile unsigned long *gpgcon; 25 volatile unsigned long *gpgdat; 26  27 static DECLARE_WAIT_QUEUE_HEAD(button_waitq); 28  29 /* 中断事件标志, 中断服务程序将它置1,forth_drv_read将它清0 */ 30 static volatile int ev_press = 0; 31  32 struct pin_desc{ 33   unsigned int pin; 34   unsigned int key_val; 35 }; 36  37 static unsigned char key_val; 38  39 struct pin_desc pins_desc[4] = { 40   {S3C2410_GPF0, 0x01}, 41   {S3C2410_GPF2, 0x02}, 42   {S3C2410_GPG3, 0x03}, 43   {S3C2410_GPG11, 0x04}, 44 }; 45  46  47 /* 48  * 确定按键值 49  */ 50 static irqreturn_t buttons_irq(int irq, void *dev_id) 51 { 52   struct pin_desc * pindesc = (struct pin_desc *)dev_id; 53   unsigned int pinval; 54    55   pinval = s3c2410_gpio_getpin(pindesc->pin); 56  57   if (pinval) 58   { 59     /* 松开 */ 60     key_val = 0x80 | pindesc->key_val; 61   } 62   else 63   { 64     /* 按下 */ 65     key_val = pindesc->key_val; 66   } 67  68   ev_press = 1;         /* 表示中断发生了 */ 69   wake_up_interruptible(&button_waitq);  /* 唤醒休眠的进程 */ 70  71    72   return IRQ_RETVAL(IRQ_HANDLED); 73 } 74  75 static int forthdrv_open(struct inode *inode, struct file *file) 76 { 77   request_irq(IRQ_EINT0, buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]); 78   request_irq(IRQ_EINT2, buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]); 79   request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]); 80   request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);   81  82   return 0; 83 } 84  85 static ssize_t forthdrv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos) 86 { 87    88   if (size != 1) 89     return -EINVAL; 90  91   /* 如果没有按键动作, 休眠 */ 92   wait_event_interruptible(button_waitq, ev_press); 93  94   /* 如果有按键动作, 返回键值 */ 95   copy_to_user(buf, &key_val, 1); 96   ev_press = 0; 97    98   return 1; 99 }100 101 static int forthdrv_close (struct inode * inode, struct file * file)102 {103   free_irq(IRQ_EINT0,&pins_desc[0]);104   free_irq(IRQ_EINT2,&pins_desc[1]);105   free_irq(IRQ_EINT11,&pins_desc[2]);106   free_irq(IRQ_EINT19,&pins_desc[3]);107 108   return 0;109 }110 111 static unsigned int forthdrv_poll(struct file *file, struct poll_table_struct *wait)112 {113   unsigned int mask = 0;114   poll_wait(file, &button_waitq, wait); 115 116   if (ev_press)117     mask |= POLLIN | POLLRDNORM;118 119   return mask;120 }121 122 static struct file_operations forthdrv_fops = {123   .owner  = THIS_MODULE,124   .open  = forthdrv_open,   125   .read   = forthdrv_read,126   .release = forthdrv_close,127   .poll   = forthdrv_poll,128 };129 130 131 int g_iMajor;132 static int forthdrv_init(void)133 {134   g_iMajor = register_chrdev(0, "forth_drv", &forthdrv_fops);135 136   g_ptForthdrvCls = class_create(THIS_MODULE, "forth_drv");137 138   g_ptForthdrvClsDev = class_device_create(g_ptForthdrvCls, NULL, MKDEV(g_iMajor, 0), NULL, "buttons"); /* /dev/buttons */139 140   gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);141   gpfdat = gpfcon + 1;142 143   gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);144   gpgdat = gpgcon + 1;145 146   return 0;147 }148 149 static int forthdrv_exit(void)150 {151   unregister_chrdev(g_iMajor, "forth_drv");152   class_device_unregister(g_ptForthdrvClsDev);153   class_destroy(g_ptForthdrvCls);154   iounmap(gpfcon);155   iounmap(gpgcon);156   return 0;157 }158 159 module_init(forthdrv_init);160 161 module_exit(forthdrv_exit);162 MODULE_LICENSE("GPL");

使用poll机制实现按键驱动程序
驱动02.按键驱动02.按键
 1 #include <sys/types.h> 2 #include <sys/stat.h> 3 #include <fcntl.h> 4 #include <stdio.h> 5 #include <poll.h> 6 /* forthdrvtest  7  */ 8 int main(int argc, char **argv) 9 {10   int fd;11   unsigned char key_val;12   int ret;13 14   struct pollfd fds[1];15   16   fd = open("/dev/buttons", O_RDWR);17   if (fd < 0)18   {19     printf("can't open!\n");20   }21 22   fds[0].fd   = fd;23   fds[0].events = POLLIN;24   while (1)25   {26     ret = poll(fds, 1, 5000);27     if (ret == 0)28     {29       printf("time out\n");30     }31     else32     {33       read(fd, &key_val, 1);34       printf("key_val = 0x%x\n", key_val);35     }36   }37   return 0;38 }

测试程序

四、异步通知机制

之前的的驱动都是应用程序主动去读取驱动程序传来的数据;有没有一直机制,就是一旦有数据,驱动程序主动通知应用程序,让应用程序来获取数据。这种机制就是异步通知。

1.异步通知的简介

异步通知是file_operations中的一个设备方法,定义为
int (*fsync) (struct file *, struct dentry *, int datasync)

1.1 四个要点

要点:
注册信号处理函数:由应用程序注册信号处理函数
谁发信号?——驱动
发给谁?——应用程序的进程ID(如何获取应用程序的进程ID?——fasync_helper函数)
怎么发?——kill_fasync函数(在中断服务程序实现)

1.2 总结

为了使设备支持异步通知机制,驱动程序中涉及以下3项工作:
①支持F_SETOWN命令,能在这个控制命令处理中设置filp->f_owner为对应进程ID。(不过此项工作已由内核完成,设备驱动无须处理)
②支持F_SETFL命令的处理,每当FASYNC标志改变时,驱动程序中的fasync()函数将得以执行。
   驱动中应该实现fasync()函数。
③在设备资源可获得时,调用kill_fasync()函数激发相应的信号

应用程序:
fcntl(fd, F_SETOWN, getpid());  // 告诉内核,发给谁

Oflags = fcntl(fd, F_GETFL);   
fcntl(fd, F_SETFL, Oflags | FASYNC);  // 改变fasync标记,最终会调用到驱动的faync > fasync_helper:初始化/释放fasync_struct

 

驱动02.按键驱动02.按键
 1 /* 2  *异步通知机制实现按键驱动程序 3  */ 4  5 #include <linux/module.h> 6 #include <linux/kernel.h> 7 #include <linux/fs.h> 8 #include <linux/init.h> 9 #include <linux/delay.h> 10 #include <linux/irq.h> 11 #include <asm/uaccess.h> 12 #include <asm/irq.h> 13 #include <asm/io.h> 14 #include <asm/arch/regs-gpio.h> 15 #include <asm/hardware.h> 16 #include <linux/poll.h> 17  18  19 static struct class *g_ptFifthdrvCls; 20 static struct class_device *g_ptFifthdrvClsDev; 21  22 volatile unsigned long *gpfcon; 23 volatile unsigned long *gpfdat; 24  25 volatile unsigned long *gpgcon; 26 volatile unsigned long *gpgdat; 27  28 static DECLARE_WAIT_QUEUE_HEAD(button_waitq); 29  30 /* 中断事件标志, 中断服务程序将它置1,forth_drv_read将它清0 */ 31 static volatile int ev_press = 0; 32 static struct fasync_struct *fifthdrv_async; 33  34 struct pin_desc{ 35   unsigned int pin; 36   unsigned int key_val; 37 }; 38  39 static unsigned char key_val; 40  41 struct pin_desc pins_desc[4] = { 42   {S3C2410_GPF0, 0x01}, 43   {S3C2410_GPF2, 0x02}, 44   {S3C2410_GPG3, 0x03}, 45   {S3C2410_GPG11, 0x04}, 46 }; 47  48  49 /* 50  * 确定按键值 51  */ 52 static irqreturn_t buttons_irq(int irq, void *dev_id) 53 { 54   struct pin_desc * pindesc = (struct pin_desc *)dev_id; 55   unsigned int pinval; 56    57   pinval = s3c2410_gpio_getpin(pindesc->pin); 58  59   if (pinval) 60   { 61     /* 松开 */ 62     key_val = 0x80 | pindesc->key_val; 63   } 64   else 65   { 66     /* 按下 */ 67     key_val = pindesc->key_val; 68   } 69  70   ev_press = 1;         /* 表示中断发生了 */ 71   wake_up_interruptible(&button_waitq);  /* 唤醒休眠的进程 */ 72  73   kill_fasync(&fifthdrv_async,SIGIO, POLL_IN); 74   return IRQ_RETVAL(IRQ_HANDLED); 75 } 76  77 static int fifthdrv_open(struct inode *inode, struct file *file) 78 { 79   request_irq(IRQ_EINT0, buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]); 80   request_irq(IRQ_EINT2, buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]); 81   request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]); 82   request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);   83  84   return 0; 85 } 86  87 static ssize_t fifthdrv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos) 88 { 89    90   if (size != 1) 91     return -EINVAL; 92  93   /* 如果没有按键动作, 休眠 */ 94   wait_event_interruptible(button_waitq, ev_press); 95    96   /* 如果有按键动作, 返回键值 */ 97   copy_to_user(buf, &key_val, 1); 98   ev_press = 0; 99   100   return 1;101 }102 103 static int fifthdrv_close (struct inode * inode, struct file * file)104 {105   free_irq(IRQ_EINT0,&pins_desc[0]);106   free_irq(IRQ_EINT2,&pins_desc[1]);107   free_irq(IRQ_EINT11,&pins_desc[2]);108   free_irq(IRQ_EINT19,&pins_desc[3]);109 110   return 0;111 }112 113 static unsigned int fifthdrv_poll(struct file *file, struct poll_table_struct *wait)114 {115   unsigned int mask = 0;116   poll_wait(file, &button_waitq, wait); 117 118   if (ev_press)119     mask |= POLLIN | POLLRDNORM;120 121   return mask;122 }123 124 static int fifthdrv_fasync(int fd, struct file *file, int on)125 {  126   printk("driver: fifthdrv_fasync\n");127   return fasync_helper(fd, file, on, &fifthdrv_async);128 }129 130 131 static struct file_operations fifthdrv_fops = {132   .owner  = THIS_MODULE,133   .open  = fifthdrv_open,   134   .read   = fifthdrv_read,135   .release = fifthdrv_close,136   .poll   = fifthdrv_poll,137   .fasync = fifthdrv_fasync,138 };139 140 141 int g_iMajor;142 static int fifthdrv_init(void)143 {144   g_iMajor = register_chrdev(0, "fifth_drv", &fifthdrv_fops);145 146   g_ptFifthdrvCls = class_create(THIS_MODULE, "fifth_drv");147 148   g_ptFifthdrvClsDev = class_device_create(g_ptFifthdrvCls, NULL, MKDEV(g_iMajor, 0), NULL, "buttons"); /* /dev/buttons */149 150   gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);151   gpfdat = gpfcon + 1;152 153   gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);154   gpgdat = gpgcon + 1;155 156   return 0;157 }158 159 static int fifthdrv_exit(void)160 {161   unregister_chrdev(g_iMajor, "fifth_drv");162   class_device_unregister(g_ptFifthdrvClsDev);163   class_destroy(g_ptFifthdrvCls);164   iounmap(gpfcon);165   iounmap(gpgcon);166   return 0;167 }168 169 module_init(fifthdrv_init);170 171 module_exit(fifthdrv_exit);172 MODULE_LICENSE("GPL");

异步通知机制
驱动02.按键驱动02.按键
 1 #include <sys/types.h> 2 #include <sys/stat.h> 3 #include <fcntl.h> 4 #include <stdio.h> 5 #include <poll.h> 6 #include <signal.h> 7 #include <sys/types.h> 8 #include <unistd.h> 9 #include <fcntl.h>10 11 12 /* fifthdrvtest 13  */14 int fd;15 16 void my_signal_fun(int signum)17 {18   unsigned char key_val;19   read(fd, &key_val, 1);20   printf("key_val: 0x%x\n", key_val);21 }22 23 int main(int argc, char **argv)24 {25   unsigned char key_val;26   int ret;27   int Oflags;28 29   signal(SIGIO, my_signal_fun);30   31   fd = open("/dev/buttons", O_RDWR);32   if (fd < 0)33   {34     printf("can't open!\n");35   }36 37   fcntl(fd, F_SETOWN, getpid());38   39   Oflags = fcntl(fd, F_GETFL); 40   41   fcntl(fd, F_SETFL, Oflags | FASYNC);42 43 44   while (1)45   {46     sleep(1000);47   }48   49   return 0;50 }

测试程序

 五、同步,互斥,阻塞

linux内核是多线程的内核,当有多个应用程序来访问同一个驱动程序时必将出错,这是系统所不允许的。所以,同一时刻只能有一个应用程序打开驱动程序。

实现原理:

static int canopen = 1;
在open函数内添加如下代码
if(--canopen != 0){
    canopen++;
    return -EBUSY;
}

在close函数内添加如下代码
canopen++;

但是,linux是多任务系统,进行某一操作的同时有可能被切换,从而导致两进程都打开了设备。

解决方案:

1.把上述步骤设置为原子操作。

open函数:
static atomic_t canopen = ATOMIC_INIT(1);
if(!atomic_desc_and_test(&canopen)){
    atomic_inc(canopen);
    return -EBUSY;
    }

close函数:
atomic_inc(canopen);

2.使用信号量实现。

定义信号量init_MUTEX(button_lock)
获得信号量down(&button_lock)
释放信号量up(&button_lock)

 

3.阻塞

open函数添加以下代码:

if (file->f_flags & O_NONBLOCK)
    {
        if (down_trylock(&button_lock))
                return -EBUSY;
    }
    else
    {
        /* 获取信号量 */
        down(&button_lock);
    }
在read函数添加以下代码:

   if (file->f_flags & O_NONBLOCK)
    {
        if (!ev_press)
            return -EAGAIN;
    }
    else
    {
        /* 如果没有按键动作, 休眠 */
        wait_event_interruptible(button_waitq, ev_press);
    }

测试方法:

当多次执行./sixth_test &时,S即睡眠状态,D即僵死状态

驱动02.按键驱动02.按键
 1 /* 2  *阻塞方式实现按键驱动程序 3  */ 4  5 #include <linux/module.h> 6 #include <linux/kernel.h> 7 #include <linux/fs.h> 8 #include <linux/init.h> 9 #include <linux/delay.h> 10 #include <linux/irq.h> 11 #include <asm/uaccess.h> 12 #include <asm/irq.h> 13 #include <asm/io.h> 14 #include <asm/arch/regs-gpio.h> 15 #include <asm/hardware.h> 16 #include <linux/poll.h> 17  18  19 static struct class *g_ptFifthdrvCls; 20 static struct class_device *g_ptFifthdrvClsDev; 21  22 volatile unsigned long *gpfcon; 23 volatile unsigned long *gpfdat; 24  25 volatile unsigned long *gpgcon; 26 volatile unsigned long *gpgdat; 27  28 static DECLARE_WAIT_QUEUE_HEAD(button_waitq); 29  30 /* 中断事件标志, 中断服务程序将它置1,forth_drv_read将它清0 */ 31 static volatile int ev_press = 0; 32 static struct fasync_struct *fifthdrv_async; 33  34 struct pin_desc{ 35   unsigned int pin; 36   unsigned int key_val; 37 }; 38  39 static unsigned char key_val; 40  41 struct pin_desc pins_desc[4] = { 42   {S3C2410_GPF0, 0x01}, 43   {S3C2410_GPF2, 0x02}, 44   {S3C2410_GPG3, 0x03}, 45   {S3C2410_GPG11, 0x04}, 46 }; 47  48 static DECLARE_MUTEX(button_lock);  49 /* 50  * 确定按键值 51  */ 52 static irqreturn_t buttons_irq(int irq, void *dev_id) 53 { 54   struct pin_desc * pindesc = (struct pin_desc *)dev_id; 55   unsigned int pinval; 56    57   pinval = s3c2410_gpio_getpin(pindesc->pin); 58  59   if (pinval) 60   { 61     /* 松开 */ 62     key_val = 0x80 | pindesc->key_val; 63   } 64   else 65   { 66     /* 按下 */ 67     key_val = pindesc->key_val; 68   } 69  70   ev_press = 1;         /* 表示中断发生了 */ 71   wake_up_interruptible(&button_waitq);  /* 唤醒休眠的进程 */ 72  73   kill_fasync(&fifthdrv_async,SIGIO, POLL_IN); 74   return IRQ_RETVAL(IRQ_HANDLED); 75 } 76  77 static int fifthdrv_open(struct inode *inode, struct file *file) 78 { 79  80 #if 0   81   if (!atomic_dec_and_test(&canopen)) 82   { 83     atomic_inc(&canopen); 84     return -EBUSY; 85   } 86 #endif     87  88   if (file->f_flags & O_NONBLOCK) 89   { 90     if (down_trylock(&button_lock)) 91       return -EBUSY; 92   } 93   else 94   { 95     /* 获取信号量 */ 96     down(&button_lock); 97   } 98    99   request_irq(IRQ_EINT0, buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);100   request_irq(IRQ_EINT2, buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);101   request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);102   request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);  103 104   return 0;105 }106 107 static ssize_t fifthdrv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)108 {109   110   if (size != 1)111     return -EINVAL;112 113   if (file->f_flags & O_NONBLOCK)114   {115     if (!ev_press)116       return -EAGAIN;117   }118   else119   {120     /* 如果没有按键动作, 休眠 */121     wait_event_interruptible(button_waitq, ev_press);122   }123 124   /* 如果有按键动作, 返回键值 */125   copy_to_user(buf, &key_val, 1);126   ev_press = 0;127   128   return 1;129 }130 131 static int fifthdrv_close (struct inode * inode, struct file * file)132 {133   free_irq(IRQ_EINT0,&pins_desc[0]);134   free_irq(IRQ_EINT2,&pins_desc[1]);135   free_irq(IRQ_EINT11,&pins_desc[2]);136   free_irq(IRQ_EINT19,&pins_desc[3]);137   up(&button_lock);138   return 0;139 }140 141 static unsigned int fifthdrv_poll(struct file *file, struct poll_table_struct *wait)142 {143   unsigned int mask = 0;144   poll_wait(file, &button_waitq, wait); 145 146   if (ev_press)147     mask |= POLLIN | POLLRDNORM;148 149   return mask;150 }151 152 static int fifthdrv_fasync(int fd, struct file *file, int on)153 {  154   printk("driver: fifthdrv_fasync\n");155   return fasync_helper(fd, file, on, &fifthdrv_async);156 }157 158 159 static struct file_operations fifthdrv_fops = {160   .owner  = THIS_MODULE,161   .open  = fifthdrv_open,   162   .read   = fifthdrv_read,163   .release = fifthdrv_close,164   .poll   = fifthdrv_poll,165   .fasync = fifthdrv_fasync,166 };167 168 169 int g_iMajor;170 static int fifthdrv_init(void)171 {172   g_iMajor = register_chrdev(0, "fifth_drv", &fifthdrv_fops);173 174   g_ptFifthdrvCls = class_create(THIS_MODULE, "fifth_drv");175 176   g_ptFifthdrvClsDev = class_device_create(g_ptFifthdrvCls, NULL, MKDEV(g_iMajor, 0), NULL, "buttons"); /* /dev/buttons */177 178   gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);179   gpfdat = gpfcon + 1;180 181   gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);182   gpgdat = gpgcon + 1;183 184   return 0;185 }186 187 static int fifthdrv_exit(void)188 {189   unregister_chrdev(g_iMajor, "fifth_drv");190   class_device_unregister(g_ptFifthdrvClsDev);191   class_destroy(g_ptFifthdrvCls);192   iounmap(gpfcon);193   iounmap(gpgcon);194   return 0;195 }196 197 module_init(fifthdrv_init);198 199 module_exit(fifthdrv_exit);200 MODULE_LICENSE("GPL");

信号量+阻塞方式
驱动02.按键驱动02.按键
 1 #include <sys/types.h> 2 #include <sys/stat.h> 3 #include <fcntl.h> 4 #include <stdio.h> 5 #include <poll.h> 6 #include <signal.h> 7 #include <sys/types.h> 8 #include <unistd.h> 9 #include <fcntl.h>10 11 12 /* sixthdrvtest 13  */14 int fd;15 16 void my_signal_fun(int signum)17 {18   unsigned char key_val;19   read(fd, &key_val, 1);20   printf("key_val: 0x%x\n", key_val);21 }22 23 int main(int argc, char **argv)24 {25   unsigned char key_val;26   int ret;27   int Oflags;28 29   //signal(SIGIO, my_signal_fun);30   31   fd = open("/dev/buttons", O_RDWR | O_NONBLOCK);32   if (fd < 0)33   {34     printf("can't open!\n");35     return -1;36   }37 38   //fcntl(fd, F_SETOWN, getpid());39   40   //Oflags = fcntl(fd, F_GETFL); 41   42   //fcntl(fd, F_SETFL, Oflags | FASYNC);43 44 45   while (1)46   {47     ret = read(fd, &key_val, 1);48     printf("key_val: 0x%x, ret = %d\n", key_val, ret);49     sleep(5);50   }51   52   return 0;53 }

测试程序

最后科普一下,异步通知和阻塞的区别:

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
阻塞调用是指调用结果返回之前,当前线程会被挂起(休眠)。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

异步关注的是消息通信机制,当一个异步过程调用发出后,调用者不会立刻得到结果,
而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用(主动通知调用者)。

异步通知是相对与同步通知来说的。
这里阻塞与非阻塞与是否同步异步无关

比如:你问朋友问题,如果你是阻塞式调用,你会一直把自己“挂起”,直到得到问题有没有解决的结果,
如果是非阻塞式调用,你不管朋友有没有告诉你,你先去忙其他事,当然你也要偶尔看一下朋友有没有返回结果。

异步通知:朋友看到你问的问题,直接告诉你他想一下,(此时没有返回结果给你),当他想到了,会主动通知你

六、定时器防抖动

1.按键去抖动的方法
    a.硬件电路去抖动
    b.软件延时去抖动,有两种方式,其中一种是什么事都不做,死循环;另一种是利用定时器延时。

本程序采用的的定时器延时的方式。

2.定时器延时的两个要素:超时时间,处理函数。

3.定时器相关的处理函数
    a.初始化定时器init_timer
    b.处理函数timer_list.function
    c.向内核注册一个定时器结构体add_timer
    d.修改定时器时间并启动定时器mod_timer
    
4.mod_timer(&timer, jiffies+HZ/100)的定时时间为10ms,为什么?
 HZ为100,所以HZ/100 = 1,变成了jiffies+1,而jiffies的单位为10ms,所以每次定时的时间自然就为10ms了。
 

驱动02.按键驱动02.按键
 1 /* 2  *定时器去抖动 3  */ 4  5 #include <linux/module.h> 6 #include <linux/kernel.h> 7 #include <linux/fs.h> 8 #include <linux/init.h> 9 #include <linux/delay.h> 10 #include <linux/irq.h> 11 #include <asm/uaccess.h> 12 #include <asm/irq.h> 13 #include <asm/io.h> 14 #include <asm/arch/regs-gpio.h> 15 #include <asm/hardware.h> 16 #include <linux/poll.h> 17  18  19 static struct timer_list timer; 20  21 static struct pin_desc *irq_pd; 22  23 static struct class *g_pbuttonsdrvCls; 24 static struct class_device *g_pbuttonsdrvClsDev; 25  26 volatile unsigned long *gpfcon; 27 volatile unsigned long *gpfdat; 28  29 volatile unsigned long *gpgcon; 30 volatile unsigned long *gpgdat; 31  32 static DECLARE_WAIT_QUEUE_HEAD(button_waitq); 33  34 /* 中断事件标志, 中断服务程序将它置1,forth_drv_read将它清0 */ 35 static volatile int ev_press = 0; 36 static struct fasync_struct *buttonsdrv_async; 37  38 struct pin_desc{ 39   unsigned int pin; 40   unsigned int key_val; 41 }; 42  43 static unsigned char key_val; 44  45 struct pin_desc pins_desc[4] = { 46   {S3C2410_GPF0, 0x01}, 47   {S3C2410_GPF2, 0x02}, 48   {S3C2410_GPG3, 0x03}, 49   {S3C2410_GPG11, 0x04}, 50 }; 51  52 static DECLARE_MUTEX(button_lock);  53 /* 54  * 确定按键值 55  */ 56 static irqreturn_t buttons_irq(int irq, void *dev_id) 57 { 58   /* 10ms后启动定时器 */ 59   irq_pd = (struct pin_desc *)dev_id; 60   mod_timer(&timer, jiffies+HZ/100); 61   return IRQ_RETVAL(IRQ_HANDLED); 62 } 63  64 static int buttonsdrv_open(struct inode *inode, struct file *file) 65 { 66  67 #if 0   68   if (!atomic_dec_and_test(&canopen)) 69   { 70     atomic_inc(&canopen); 71     return -EBUSY; 72   } 73 #endif     74  75   if (file->f_flags & O_NONBLOCK) 76   { 77     if (down_trylock(&button_lock)) 78       return -EBUSY; 79   } 80   else 81   { 82     /* 获取信号量 */ 83     down(&button_lock); 84   } 85    86   request_irq(IRQ_EINT0, buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]); 87   request_irq(IRQ_EINT2, buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]); 88   request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]); 89   request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);   90  91   return 0; 92 } 93  94 static ssize_t buttonsdrv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos) 95 { 96    97   if (size != 1) 98     return -EINVAL; 99 100   if (file->f_flags & O_NONBLOCK)101   {102     if (!ev_press)103       return -EAGAIN;104   }105   else106   {107     /* 如果没有按键动作, 休眠 */108     wait_event_interruptible(button_waitq, ev_press);109   }110 111   /* 如果有按键动作, 返回键值 */112   copy_to_user(buf, &key_val, 1);113   ev_press = 0;114   115   return 1;116 }117 118 static int buttonsdrv_close (struct inode * inode, struct file * file)119 {120   free_irq(IRQ_EINT0,&pins_desc[0]);121   free_irq(IRQ_EINT2,&pins_desc[1]);122   free_irq(IRQ_EINT11,&pins_desc[2]);123   free_irq(IRQ_EINT19,&pins_desc[3]);124   up(&button_lock);125   return 0;126 }127 128 static unsigned int buttonsdrv_poll(struct file *file, struct poll_table_struct *wait)129 {130   unsigned int mask = 0;131   poll_wait(file, &button_waitq, wait); 132 133   if (ev_press)134     mask |= POLLIN | POLLRDNORM;135 136   return mask;137 }138 139 static int buttonsdrv_fasync(int fd, struct file *file, int on)140 {  141   printk("driver: buttonsdrv_fasync\n");142   return fasync_helper(fd, file, on, &buttonsdrv_async);143 }144 145 146 static void buttons_timer_func(unsigned long data)147 {148   struct pin_desc * pindesc = irq_pd;149   unsigned int pinval;150   151   pinval = s3c2410_gpio_getpin(pindesc->pin);152 153   if (pinval)154   {155     /* 松开 */156     key_val = 0x80 | pindesc->key_val;157   }158   else159   {160     /* 按下 */161     key_val = pindesc->key_val;162   }163 164   ev_press = 1;         /* 表示中断发生了 */165   wake_up_interruptible(&button_waitq);  /* 唤醒休眠的进程 */166 167   kill_fasync(&buttonsdrv_async,SIGIO, POLL_IN);168 //  return IRQ_RETVAL(IRQ_HANDLED);169 }170 171 static struct file_operations buttonsdrv_fops = {172   .owner  = THIS_MODULE,173   .open  = buttonsdrv_open,   174   .read   = buttonsdrv_read,175   .release = buttonsdrv_close,176   .poll   = buttonsdrv_poll,177   .fasync = buttonsdrv_fasync,178 };179 180 181 int g_iMajor;182 static int buttonsdrv_init(void)183 {184 185   init_timer(&timer);186 //  timer.expires = jiffies + media_tbl[dev->if_port].wait;187   timer.function = &buttons_timer_func;  /* timer handler */188   add_timer(&timer);189 190   191   g_iMajor = register_chrdev(0, "buttons_drv", &buttonsdrv_fops);192 193   g_pbuttonsdrvCls = class_create(THIS_MODULE, "buttons_drv");194 195   g_pbuttonsdrvClsDev = class_device_create(g_pbuttonsdrvCls, NULL, MKDEV(g_iMajor, 0), NULL, "buttons"); /* /dev/buttons */196 197   gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);198   gpfdat = gpfcon + 1;199 200   gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);201   gpgdat = gpgcon + 1;202 203   return 0;204 }205 206 static int buttonsdrv_exit(void)207 {208   unregister_chrdev(g_iMajor, "buttons_drv");209   class_device_unregister(g_pbuttonsdrvClsDev);210   class_destroy(g_pbuttonsdrvCls);211   iounmap(gpfcon);212   iounmap(gpgcon);213   return 0;214 }215 216 module_init(buttonsdrv_init);217 218 module_exit(buttonsdrv_exit);219 MODULE_LICENSE("GPL");

定时器防抖动
驱动02.按键驱动02.按键
 1 #include <sys/types.h> 2 #include <sys/stat.h> 3 #include <fcntl.h> 4 #include <stdio.h> 5 #include <poll.h> 6 #include <signal.h> 7 #include <sys/types.h> 8 #include <unistd.h> 9 #include <fcntl.h>10 11 12 /* sixthdrvtest 13  */14 int fd;15 16 void my_signal_fun(int signum)17 {18   unsigned char key_val;19   read(fd, &key_val, 1);20   printf("key_val: 0x%x\n", key_val);21 }22 23 int main(int argc, char **argv)24 {25   unsigned char key_val;26   int ret;27   int Oflags;28 29   //signal(SIGIO, my_signal_fun);30   31   fd = open("/dev/input/event0", O_RDWR);32   if (fd < 0)33   {34     printf("can't open!\n");35     return -1;36   }37 38   //fcntl(fd, F_SETOWN, getpid());39   40   //Oflags = fcntl(fd, F_GETFL); 41   42   //fcntl(fd, F_SETFL, Oflags | FASYNC);43 44 45   while (1)46   {47     ret = read(fd, &key_val, 1);48     printf("key_val: 0x%x, ret = %d\n", key_val, ret);49     //sleep(5);50   }51   52   return 0;53 }

测试程序

 

修改于2017-01-08 21:05:56

 

 



原标题:驱动02.按键

关键词:

*特别声明:以上内容来自于网络收集,著作权属原作者所有,如有侵权,请联系我们: admin#shaoqun.com (#换成@)。
相关文章
我的浏览记录
最新相关资讯
海外公司注册 | 跨境电商服务平台 | 深圳旅行社 | 东南亚物流