OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 9088|回复: 1
打印 上一主题 下一主题

【正点原子Linux连载】第五十八章Linux INPUT子系统实验--摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0

[复制链接]

1312

主题

1328

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
5613
金钱
5613
注册时间
2019-5-8
在线时间
1498 小时
跳转到指定楼层
楼主
发表于 2020-3-13 11:05:19 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 正点原子01 于 2020-3-13 11:10 编辑

1)实验平台:正点原子阿尔法Linux开发板
2)平台购买地址::https://item.taobao.com/item.htm?id=603672744434
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html
4)本章实例源码下载: Linux INPUT子系统实验.zip (5.57 KB, 下载次数: 4)
5)对正点原子Linux感兴趣的同学可以加群讨论:935446741
6)关注正点原子公众号,获取最新资料更新


第五十八章Linux INPUT子系统实验

       按键、鼠标、键盘、触摸屏等都属于输入(input)设备,Linux内核为此专门做了一个叫做input子系统的框架来处理输入事件。输入设备本质上还是字符设备,只是在此基础上套上了input框架,用户只需要负责上报输入事件,比如按键值、坐标等信息,input核心层负责处理这些事件。本章我们就来学习一下Linux内核中的input子系统。

58.1 input子系统58.1.1input子系统简介
       input就是输入的意思,因此input子系统就是管理输入的子系统,和pinctrl和gpio子系统一样,都是Linux内核针对某一类设备而创建的框架。比如按键输入、键盘、鼠标、触摸屏等等这些都属于输入设备,不同的输入设备所代表的含义不同,按键和键盘就是代表按键信息,鼠标和触摸屏代表坐标信息,因此在应用层的处理就不同,对于驱动编写者而言不需要去关心应用层的事情,我们只需要按照要求上报这些输入事件即可。为此input子系统分为input驱动层、input核心层、input事件处理层,最终给用户空间提供可访问的设备节点,input子系统框架如图58.1.1.1所示:
图58.1.1.1input子系统结构图
       图58.1.1中左边就是最底层的具体设备,比如按键、USB键盘/鼠标等,中间部分属于Linux内核空间,分为驱动层、核心层和时间层,最右边的就是用户空间,所有的输入设备以文件的形式供用户应用程序使用。可以看出input子系统用到了我们前面讲解的驱动分层模型,我们编写驱动程序的时候只需要关注中间的驱动层、核心层和事件层,这三个层的分工如下:
       驱动层:输入设备的具体驱动程序,比如按键驱动程序,向内核层报告输入内容。
       核心层:承上启下,为驱动层提供输入设备注册和操作接口。通知事件层对输入事件进行处理。
       事件层:主要和用户空间进行交互。
58.1.2input驱动编写流程
       input核心层会向Linux内核注册一个字符设备,大家找到drivers/input/input.c这个文件,input.c就是input输入子系统的核心层,此文件里面有如下所示代码:
示例代码58.1.2.1 input核心层创建字符设备过程
  1. struct class input_class ={
  2. .name       ="input",
  3. .devnode    = input_devnode,
  4. };
  5. ......
  6. staticint __init input_init(void)
  7. {
  8. int err;

  9.     err = class_register(&input_class);
  10. if(err){
  11.         pr_err("unable to register input_devclass\n");
  12. return err;
  13. }

  14.     err = input_proc_init();
  15. if(err)
  16. goto fail1;

  17.     err = register_chrdev_region(MKDEV(INPUT_MAJOR,0),
  18.                      INPUT_MAX_CHAR_DEVICES,"input");
  19. if(err){
  20.         pr_err("unable to register char major %d", INPUT_MAJOR);
  21. goto fail2;
  22. }

  23. return0;

  24.   fail2:   input_proc_exit();
  25.   fail1:   class_unregister(&input_class);
  26. return err;
  27. }
复制代码
    第2418行,注册一个input类,这样系统启动以后就会在/sys/class目录下有一个input子目录,如图58.1.2.1所示:
图58.1.2.1input类
       第2428~2429行,注册一个字符设备,主设备号为INPUT_MAJOR,INPUT_MAJOR定义在include/uapi/linux/major.h文件中,定义如下:
  1. #define INPUT_MAJOR              13
复制代码
       因此,input子系统的所有设备主设备号都为13,我们在使用input子系统处理输入设备的时候就不需要去注册字符设备了,我们只需要向系统注册一个input_device即可。
       1、注册input_dev
       在使用input子系统的时候我们只需要注册一个input设备即可,input_dev结构体表示input设备,此结构体定义在include/linux/input.h文件中,定义如下(有省略):
示例代码58.1.2.2 input_dev结构体
  1. struct input_dev {
  2. constchar*name;
  3. constchar*phys;
  4. constchar*uniq;
  5. struct input_id id;

  6. unsignedlong propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];

  7. unsignedlong evbit[BITS_TO_LONGS(EV_CNT)]; /* 事件类型的位图 */
  8. unsignedlong keybit[BITS_TO_LONGS(KEY_CNT)];  /* 按键值的位图   */
  9. unsignedlong relbit[BITS_TO_LONGS(REL_CNT)];  /* 相对坐标的位图  */
  10. unsignedlong absbit[BITS_TO_LONGS(ABS_CNT)];  /* 绝对坐标的位图  */
  11. unsignedlong mscbit[BITS_TO_LONGS(MSC_CNT)];  /* 杂项事件的位图  */
  12. unsignedlong ledbit[BITS_TO_LONGS(LED_CNT)];  /*LED相关的位图    */
  13. unsignedlong sndbit[BITS_TO_LONGS(SND_CNT)];/* sound有关的位图 */
  14. unsignedlong ffbit[BITS_TO_LONGS(FF_CNT)]; /* 压力反馈的位图  */
  15. unsignedlong swbit[BITS_TO_LONGS(SW_CNT)]; /*开关状态的位图   */
  16. ......
  17. bool devres_managed;
  18. };
复制代码
    第129行,evbit表示输入事件类型,可选的事件类型定义在include/uapi/linux/input.h文件中,事件类型如下:
示例代码58.1.2.3 事件类型
  1. #define EV_SYN            0x00    /* 同步事件     */
  2. #define EV_KEY            0x01    /* 按键事件     */
  3. #define EV_REL            0x02    /* 相对坐标事件  */
  4. #define EV_ABS            0x03    /* 绝对坐标事件  */
  5. #define EV_MSC            0x04    /* 杂项(其他)事件   */
  6. #define EV_SW             0x05    /* 开关事件     */
  7. #define EV_LED            0x11    /* LED        */
  8. #define EV_SND            0x12    /* sound(声音)     */
  9. #define EV_REP            0x14    /* 重复事件     */
  10. #define EV_FF             0x15    /* 压力事件     */
  11. #define EV_PWR            0x16    /* 电源事件     */
  12. #define EV_FF_STATUS     0x17    /* 压力状态事件  */
复制代码
    比如本章我们要使用到按键,那么就需要注册EV_KEY事件,如果要使用连按功能的话还需要注册EV_REP事件。
       继续回到示例代码58.1.2.2中,第129行~137行的evbit、keybit、relbit等等都是存放不同事件对应的值。比如我们本章要使用按键事件,因此要用到keybit,keybit就是按键事件使用的位图,Linux内核定义了很多按键值,这些按键值定义在include/uapi/linux/input.h文件中,按键值如下:
示例代码58.1.2.4 按键值
  1. #defineKEY_RESERVED        0
  2. #defineKEY_ESC               1
  3. #define KEY_1                 2
  4. #define KEY_2                 3
  5. #define KEY_3                 4
  6. #define KEY_4                 5
  7. #define KEY_5                 6
  8. #define KEY_6                 7
  9. #define KEY_7                 8
  10. #define KEY_8                 9
  11. #define KEY_9                 10
  12. #define KEY_0                 11
  13. ......
  14. #defineBTN_TRIGGER_HAPPY39 0x2e6
  15. #defineBTN_TRIGGER_HAPPY40 0x2e7
复制代码
    我们可以将开发板上的按键值设置为示例代码58.1.2.4中的任意要一个,比如我们本章实验会将I.MX6U-ALPHA开发板上的KEY按键值设置为KEY_0。
       在编写input设备驱动的时候我们需要先申请一个input_dev结构体变量,使用input_allocate_device函数来申请一个input_dev,此函数原型如下所示:
  1. struct input_dev*input_allocate_device(void)
复制代码
函数参数和返回值含义如下:
       参数:无。
       返回值:申请到的input_dev。
       如果要注销的input设备的话需要使用input_free_device函数来释放掉前面申请到的input_dev,input_free_device函数原型如下:
  1. void input_free_device(struct input_dev *dev)
复制代码
函数参数和返回值含义如下:
       dev:需要释放的input_dev。
       返回值:无。
       申请好一个input_dev以后就需要初始化这个input_dev,需要初始化的内容主要为事件类型(evbit)和事件值(keybit)这两种。input_dev初始化完成以后就需要向Linux内核注册input_dev了,需要用到input_register_device函数,此函数原型如下:
  1. int input_register_device(struct input_dev *dev)
复制代码
函数参数和返回值含义如下:
       dev:要注册的input_dev 。
       返回值:0,input_dev注册成功;负值,input_dev注册失败。
       同样的,注销input驱动的时候也需要使用input_unregister_device函数来注销掉前面注册的input_dev,input_unregister_device函数原型如下:
  1. void input_unregister_device(struct input_dev*dev)
复制代码
函数参数和返回值含义如下:
       dev:要注销的input_dev 。
       返回值:无。
       综上所述,input_dev注册过程如下:
       ①、使用input_allocate_device函数申请一个input_dev。
       ②、初始化input_dev的事件类型以及事件值。
       ③、使用input_unregister_device函数向Linux系统注册前面初始化好的input_dev。
       ④、卸载input驱动的时候需要先使用input_unregister_device函数注销掉注册的input_dev,然后使用input_free_device函数释放掉前面申请的input_dev。input_dev注册过程示例代码如下所示:
示例代码58.1.2.5 input_dev注册流程
  1. struct input_dev *inputdev;/* input结构体变量 */

  2. /* 驱动入口函数 */
  3. staticint __init xxx_init(void)
  4. {
  5.    ......
  6.    inputdev = input_allocate_device();    /* 申请input_dev      */
  7.    inputdev->name ="test_inputdev";       /* 设置input_dev名字  */

  8.   /*********第一种设置事件和事件值的方法***********/
  9.   __set_bit(EV_KEY, inputdev->evbit);    /* 设置产生按键事件     */
  10.   __set_bit(EV_REP, inputdev->evbit);    /* 重复事件        */
  11.   __set_bit(KEY_0, inputdev->keybit);    /*设置产生哪些按键值 */
  12.   /************************************************/

  13.   /*********第二种设置事件和事件值的方法***********/
  14.   keyinputdev.inputdev->evbit[0]= BIT_MASK(EV_KEY)|
  15. BIT_MASK(EV_REP);
  16.   keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)]|=
  17. BIT_MASK(KEY_0);
  18.   /************************************************/

  19.   /*********第三种设置事件和事件值的方法***********/
  20.   keyinputdev.inputdev->evbit[0]= BIT_MASK(EV_KEY)|
  21. BIT_MASK(EV_REP);
  22.   input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);
  23.   /************************************************/

  24.   /* 注册input_dev */
  25.   input_register_device(inputdev);
  26.   ......
  27.   return0;
  28. }

  29. /* 驱动出口函数 */
  30. staticvoid __exit xxx_exit(void)
  31. {
  32.   input_unregister_device(inputdev); /* 注销input_dev  */
  33.   input_free_device(inputdev);       /* 删除input_dev  */
  34. }
复制代码
    第1行,定义一个input_dev结构体指针变量。
       第4~30行,驱动入口函数,在此函数中完成input_dev的申请、设置、注册等工作。第7行调用input_allocate_device函数申请一个input_dev。第10~23行都是设置input设备事件和按键值,这里用了三种方法来设置事件和按键值。第27行调用input_register_device函数向Linux内核注册inputdev。
       第33~37行,驱动出口函数,第35行调用input_unregister_device函数注销前面注册的input_dev,第36行调用input_free_device函数删除前面申请的input_dev。
       2、上报输入事件
       当我们向Linux内核注册好input_dev以后还不能高枕无忧的使用input设备,input设备都是具有输入功能的,但是具体是什么样的输入值Linux内核是不知道的,我们需要获取到具体的输入值,或者说是输入事件,然后将输入事件上报给Linux内核。比如按键,我们需要在按键中断处理函数,或者消抖定时器中断函数中将按键值上报给Linux内核,这样Linux内核才能获取到正确的输入值。不同的事件,其上报事件的API函数不同,我们依次来看一下一些常用的事件上报API函数。
       首先是input_event函数,此函数用于上报指定的事件以及对应的值,函数原型如下:
  1. void input_event(struct input_dev        *dev,
  2.                 unsigned int                     type,
  3.                 unsigned int                     code,
  4.                 int                                  value)
复制代码
函数参数和返回值含义如下:
       dev:需要上报的input_dev。
       type:上报的事件类型,比如EV_KEY。
       code事件码,也就是我们注册的按键值,比如KEY_0、KEY_1等等。
       value:事件值,比如1表示按键按下,0表示按键松开。
       返回值:无。
       input_event函数可以上报所有的事件类型和事件值,Linux内核也提供了其他的针对具体事件的上报函数,这些函数其实都用到了input_event函数。比如上报按键所使用的input_report_key函数,此函数内容如下:
例代码58.1.2.6 input_report_key函数
  1. static inline void input_report_key(struct input_dev *dev,
  2. unsignedint code,int value)
  3. {
  4.     input_event(dev, EV_KEY, code,!!value);
  5. }
复制代码
    从示例代码58.1.2.6可以看出,input_report_key函数的本质就是input_event函数,如果要上报按键事件的话还是建议大家使用input_report_key函数。
       同样的还有一些其他的事件上报函数,这些函数如下所示:
  1. void input_report_rel(struct input_dev *dev,unsigned int code, int value)
  2. void input_report_abs(struct input_dev *dev,unsigned int code, int value)
  3. void input_report_ff_status(struct input_dev *dev,unsigned int code, int value)
  4. void input_report_switch(struct input_dev *dev,unsigned int code, int value)
  5. void input_mt_sync(struct input_dev *dev)
复制代码
       当我们上报事件以后还需要使用input_sync函数来告诉Linux内核input子系统上报结束,input_sync函数本质是上报一个同步事件,此函数原型如下所示:
  1. void input_sync(struct input_dev *dev)
复制代码
函数参数和返回值含义如下:
       dev:需要上报同步事件的input_dev。
       返回值:无。
       综上所述,按键的上报事件的参考代码如下所示:
示例代码58.1.2.7 事件上报参考代码
  1. <blockquote> /* 用于按键消抖的定时器服务函数 */
复制代码
       第6行,获取按键值,判断按键是否按下。
第9~10行,如果按键值为0那么表示按键被按下了,如果按键按下的话就要使用input_report_key函数向Linux系统上报按键值,比如向Linux系统通知KEY_0这个按键按下了。
第12~13行,如果按键值为1的话就表示按键没有按下,是松开的。向Linux系统通知KEY_0这个按键没有按下或松开了。
58.1.3 input_event结构体
       Linux内核使用input_event这个结构体来表示所有的输入事件,input_envent结构体定义在include/uapi/linux/input.h文件中,结构体内容如下:
示例代码58.1.3.1 input_event结构体
  1. struct input_event {
  2.   struct timeval time;
  3.   __u16 type;
  4.   __u16 code;
  5.   __s32 value;
  6. };
复制代码
       我们依次来看一下input_event结构体中的各个成员变量:
       time:时间,也就是此事件发生的时间,为timeval结构体类型,timeval结构体定义如下:
示例代码58.1.3.2 timeval结构体
  1. typedeflong      __kernel_long_t;
  2. typedef __kernel_long_t     __kernel_time_t;
  3. typedef __kernel_long_t     __kernel_suseconds_t;

  4. struct timeval {
  5.    __kernel_time_t           tv_sec;    /* 秒   */
  6.    __kernel_suseconds_t      tv_usec;   /* 微秒 */
  7. };
复制代码
       从示例代码58.1.3.2可以看出,tv_sec和tv_usec这两个成员变量都为long类型,也就是32位,这个一定要记住,后面我们分析event事件上报数据的时候要用到。
       type事件类型,比如EV_KEY,表示此次事件为按键事件,此成员变量为16位。
       code事件码,比如在EV_KEY事件中code就表示具体的按键码,如:KEY_0、KEY_1等等这些按键。此成员变量为16位。
       value值,比如EV_KEY事件中value就是按键值,表示按键有没有被按下,如果为1的话说明按键按下,如果为0的话说明按键没有被按下或者按键松开了。
       input_envent这个结构体非常重要,因为所有的输入设备最终都是按照input_event结构体呈现给用户的,用户应用程序可以通过input_event来获取到具体的输入事件或相关的值,比如按键值等。关于input子系统就讲解到这里,接下来我们就以开发板上的KEY0按键为例,讲解一下如何编写input驱动。
58.2 硬件原理图分析
本章实验硬件原理图参考15.2小节即可。
58.3实验程序编写
本实验对应的例程路径为:开发板光盘->2、Linux驱动例程->20_input。
58.3.1 修改设备树文件
       直接使用49.3.1小节创建的key节点即可。
58.3.2 按键input驱动程序编写
新建名为“20_input”的文件夹,然后在20_input文件夹里面创建vscode工程,工作区命名为“keyinput”。工程创建好以后新建keyinput.c文件,在keyinput.c里面输入如下内容:
示例代码58.3.2.1 keyinput.c文件代码段
#include <linux/types.h>
  1.    #include <linux/kernel.h>
  2.    #include <linux/delay.h>
  3.    #include <linux/ide.h>
  4.    #include <linux/init.h>
  5.    #include <linux/module.h>
  6.    #include <linux/errno.h>
  7.    #include <linux/gpio.h>
  8.    #include <linux/cdev.h>
  9.   #include <linux/device.h>
  10.   #include <linux/of.h>
  11.   #include <linux/of_address.h>
  12.   #include <linux/of_gpio.h>
  13.   #include <linux/input.h>
  14.   #include <linux/semaphore.h>
  15.   #include <linux/timer.h>
  16.   #include <linux/of_irq.h>
  17.   #include <linux/irq.h>
  18.   #include <asm/mach/map.h>
  19.   #include <asm/uaccess.h>
  20.   #include <asm/io.h>
  21. /***************************************************************
  22.   Copyright ? ALIENTEK Co., Ltd. 1998-2029. Allrights reserved.
  23. 文件名       : keyinput.c
  24. 作者 : 左忠凯
  25. 版本 : V1.0
  26. 描述 : Linux按键input子系统实验
  27. 其他 : 无
  28. 论坛 : www.openedv.com
  29. 日志 : 初版V1.02019/8/21 左忠凯创建
  30. ***************************************************************/
  31.   #define KEYINPUT_CNT     1   /* 设备号个数   */
  32.   #define KEYINPUT_NAME    "keyinput" /* 名字 */
  33.   #define KEY0VALUE         0X01/* KEY0按键值 */
  34.   #define INVAKEY            0XFF/* 无效的按键值 */
  35.   #define KEY_NUM            1   /* 按键数量 */

  36. /* 中断IO描述结构体 */
  37. struct irq_keydesc {
  38. int gpio;   /* gpio           */
  39. int irqnum; /* 中断号      */
  40. unsignedchar value;    /* 按键对应的键值   */
  41. char name[10];  /* 名字        */
  42.       irqreturn_t (*handler)(int,void*);/* 中断服务函数 */
  43. };

  44. /* keyinput设备结构体 */
  45. struct keyinput_dev{
  46.       dev_t devid;   /* 设备号      */
  47. struct cdev cdev;  /* cdev            */
  48. struct class *class;   /* 类          */
  49. struct device *device; /* 设备        */
  50. struct device_node  *nd;/* 设备节点    */
  51. struct timer_list timer;  /* 定义一个定时器   */
  52. struct irq_keydesc irqkeydesc[KEY_NUM]; /* 按键描述数组 */
  53. unsignedchar curkeynum;       /* 当前的按键号  */
  54. struct input_dev *inputdev;             /* input结构体 */
  55. };

  56. struct keyinput_dev keyinputdev;/* key input设备 */

  57. /*@description        : 中断服务函数,开启定时器,延时10ms,
  58.    *                     定时器用于按键消抖。
  59.    * @param - irq        : 中断号
  60.    * @param - dev_id    : 设备结构。
  61.    * @return           :中断执行结果
  62.    */
  63. static irqreturn_t key0_handler(int irq,void*dev_id)
  64. {
  65. struct keyinput_dev *dev =(struct keyinput_dev *)dev_id;

  66.       dev->curkeynum =0;
  67.       dev->timer.data =(volatilelong)dev_id;
  68.       mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));
  69. return IRQ_RETVAL(IRQ_HANDLED);
  70. }

  71. /* @description    : 定时器服务函数,用于按键消抖,定时器到了以后
  72.    *                 再次读取按键值,如果按键还是处于按下状态就表示按键有效。
  73.    * @param - arg   : 设备结构变量
  74.    * @return        : 无
  75.    */
  76. void timer_function(unsignedlong arg)
  77. {
  78. unsignedchar value;
  79. unsignedchar num;
  80. struct irq_keydesc *keydesc;
  81. struct keyinput_dev *dev =(struct keyinput_dev *)arg;

  82.       num = dev->curkeynum;
  83.       keydesc =&dev->irqkeydesc[num];
  84.       value = gpio_get_value(keydesc->gpio); /* 读取IO值    */
  85. if(value ==0){     /* 按下按键    */
  86. /* 上报按键值 */
  87. //input_event(dev->inputdev,EV_KEY, keydesc->value, 1);
  88.           input_report_key(dev->inputdev, keydesc->value,1);/*1,按下*/
  89.           input_sync(dev->inputdev);
  90. }else{   /* 按键松开 */
  91. //input_event(dev->inputdev,EV_KEY, keydesc->value, 0);
  92.          input_report_key(dev->inputdev, keydesc->value,0);
  93.          input_sync(dev->inputdev);
  94. }
  95. }

  96. /*
  97.   * @description   : 按键IO初始化
  98.   * @param         : 无
  99.   * @return        : 无
  100.   */
  101. staticint keyio_init(void)
  102. {
  103. unsignedchar i =0;
  104. char name[10];
  105. int ret =0;

  106.      keyinputdev.nd = of_find_node_by_path("/key");
  107. if(keyinputdev.nd==NULL){
  108.          printk("key node not find!\r\n");
  109. return-EINVAL;
  110. }

  111. /* 提取GPIO */
  112. for(i =0; i < KEY_NUM; i++){
  113.          keyinputdev.irqkeydesc[i].gpio =  of_get_named_gpio(keyinputdev.nd,"key-gpio", i);
  114. if(keyinputdev.irqkeydesc[i].gpio <0){
  115.              printk("can't get key%d\r\n", i);
  116. }
  117. }

  118. /* 初始化key所使用的IO,并且设置成中断模式 */
  119. for(i =0; i < KEY_NUM; i++){
  120.          memset(keyinputdev.irqkeydesc[i].name,0,sizeof(name));
  121.          sprintf(keyinputdev.irqkeydesc[i].name,"KEY%d", i);
  122.          gpio_request(keyinputdev.irqkeydesc[i].gpio, name);
  123.          gpio_direction_input(keyinputdev.irqkeydesc[i].gpio);
  124.          keyinputdev.irqkeydesc[i].irqnum =
  125. irq_of_parse_and_map(keyinputdev.nd, i);
  126. }
  127. /* 申请中断 */
  128.      keyinputdev.irqkeydesc[0].handler = key0_handler;
  129.      keyinputdev.irqkeydesc[0].value = KEY_0;

  130. for(i =0; i < KEY_NUM; i++){
  131.          ret = request_irq(keyinputdev.irqkeydesc[i].irqnum,
  132. keyinputdev.irqkeydesc[i].handler,
  133.                            IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
  134. keyinputdev.irqkeydesc[i].name,&keyinputdev);
  135. if(ret <0){
  136.              printk("irq %d request failed!\r\n",
  137. keyinputdev.irqkeydesc[i].irqnum);
  138. return-EFAULT;
  139. }
  140. }

  141. /* 创建定时器 */
  142.      init_timer(&keyinputdev.timer);
  143.      keyinputdev.timer.function = timer_function;

  144. /* 申请input_dev */
  145.      keyinputdev.inputdev = input_allocate_device();
  146.      keyinputdev.inputdev->name = KEYINPUT_NAME;
  147. #if0
  148. /* 初始化input_dev,设置产生哪些事件 */
  149.      __set_bit(EV_KEY, keyinputdev.inputdev->evbit);/*按键事件   */
  150.      __set_bit(EV_REP, keyinputdev.inputdev->evbit);/* 重复事件  */

  151. /* 初始化input_dev,设置产生哪些按键 */
  152.      __set_bit(KEY_0, keyinputdev.inputdev->keybit);
  153. #endif

  154. #if0
  155.      keyinputdev.inputdev->evbit[0]= BIT_MASK(EV_KEY)|
  156. BIT_MASK(EV_REP);
  157.      keyinputdev.inputdev->keybit[BIT_WORD(KEY_0)]|=
  158. BIT_MASK(KEY_0);
  159. #endif

  160.      keyinputdev.inputdev->evbit[0]= BIT_MASK(EV_KEY)|
  161. BIT_MASK(EV_REP);
  162.      input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);

  163. /* 注册输入设备 */
  164.      ret = input_register_device(keyinputdev.inputdev);
  165. if(ret){
  166.          printk("register input device failed!\r\n");
  167. return ret;
  168. }
  169. return0;
  170. }

  171. /*
  172.   * @description   : 驱动入口函数
  173.   * @param         : 无
  174.   * @return        : 无
  175.   */
  176. staticint __init keyinput_init(void)
  177. {
  178.      keyio_init();
  179. return0;
  180. }

  181. /*
  182.   * @description   : 驱动出口函数
  183.   * @param         : 无
  184.   * @return        : 无
  185.   */
  186. staticvoid __exit keyinput_exit(void)
  187. {
  188. unsigned i =0;
  189. /* 删除定时器 */
  190.      del_timer_sync(&keyinputdev.timer);

  191. /* 释放中断 */
  192. for(i =0; i < KEY_NUM; i++){
  193.          free_irq(keyinputdev.irqkeydesc[i].irqnum,&keyinputdev);
  194. }
  195. /* 释放input_dev */
  196.      input_unregister_device(keyinputdev.inputdev);
  197.      input_free_device(keyinputdev.inputdev);
  198. }

  199. module_init(keyinput_init);
  200. module_exit(keyinput_exit);
  201. MODULE_LICENSE("GPL");
  202. MODULE_AUTHOR("zuozhongkai");
复制代码
       keyinput.c文件内容其实就是实验“13_irq”中的imx6uirq.c文件中修改而来的,只是将其中与字符设备有关的内容进行了删除,加入了input_dev相关的内容,我们简单来分析一下示例代码58.3.2.1中的程序。
       第57行,在设备结构体中定义一个input_dev指针变量。
       第93~102行,在按键消抖定时器处理函数中上报输入事件,也就是使用input_report_key函数上报按键事件以及按键值,最后使用input_sync函数上报一个同步事件,这一步一定得做!
       第156~180行,使用input_allocate_device函数申请input_dev,然后设置相应的事件以及事件码(也就是KEY模拟成那个按键,这里我们设置为KEY_0)。最后使用input_register_device函数向Linux内核注册input_dev。
       第211~212行,当注销input设备驱动的时候使用input_unregister_device函数注销掉前面注册的input_dev,最后使用input_free_device函数释放掉前面申请的input_dev。
58.3.3 编写测试APP
新建keyinputApp.c文件,然后在里面输入如下所示内容:
示例代码58.3.3.1 keyinputApp.c文件代码段
  1. #include "stdio.h"
  2. #include "unistd.h"
  3. #include "sys/types.h"
  4. #include "sys/stat.h"
  5. #include "sys/ioctl.h"
  6. #include "fcntl.h"
  7. #include "stdlib.h"
  8. #include "string.h"
  9. #include <poll.h>
  10. #include <sys/select.h>
  11. #include <sys/time.h>
  12. #include <signal.h>
  13. #include <fcntl.h>
  14. #include <linux/input.h>
  15. /***************************************************************
  16. Copyright ?ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
  17. 文件名   : keyinputApp.c
  18. 作者 : 左忠凯
  19. 版本 : V1.0
  20. 描述 : input子系统测试APP。
  21. 其他 : 无
  22. 使用方法     :./keyinputApp/dev/input/event1
  23. 论坛 : www.openedv.com
  24. 日志 : 初版V1.02019/8/26 左忠凯创建
  25. ***************************************************************/

  26. /* 定义一个input_event变量,存放输入事件信息 */
  27. staticstruct input_event inputevent;

  28. /*
  29.   * @description    : main主程序
  30.   * @param - argc   : argv数组元素个数
  31.   * @param - argv   : 具体参数
  32.   * @return           :0 成功;其他失败
  33.   */
  34. int main(int argc,char*argv[])
  35. {
  36.   int fd;
  37.   int err =0;
  38.   char*filename;

  39.   filename = argv[1];

  40.   if(argc !=2){
  41.   printf("Error Usage!\r\n");
  42.   return-1;
  43.   }

  44.   fd = open(filename, O_RDWR);
  45.   if(fd <0){
  46.   printf("Can't open file %s\r\n", filename);
  47.   return-1;
  48.   }

  49.      while(1){
  50.   err = read(fd,&inputevent,sizeof(inputevent));
  51.   if(err >0){/* 读取数据成功 */
  52.      switch(inputevent.type){
  53.      case EV_KEY:
  54.      if(inputevent.code < BTN_MISC){/* 键盘键值 */
  55.      printf("key %d %s\r\n", inputevent.code,
  56. inputevent.value ?"press":"release");
  57.      }else{
  58.      printf("button %d %s\r\n", inputevent.code,
  59. inputevent.value ?"press":"release");
  60.      }
  61.      break;

  62.      /* 其他类型的事件,自行处理 */
  63.      case EV_REL:
  64.      break;
  65.      case EV_ABS:
  66.      break;
  67.      case EV_MSC:
  68.      break;
  69.      case EV_SW:
  70.      break;
  71.      }
  72.   }else{
  73.      printf("读取数据失败\r\n");
  74.   }
  75.   }
  76.   return0;
  77. }
复制代码
    第58.1.3小节已经说过了,Linux内核会使用input_event结构体来表示输入事件,所以我们要获取按键输入信息,那么必须借助于input_event结构体。第28行定义了一个inputevent变量,此变量为input_event结构体类型。
       第56行,当我们向Linux内核成功注册input_dev设备以后,会在/dev/input目录下生成一个名为“eventX(X=0….n)”的文件,这个/dev/input/eventX就是对应的input设备文件。我们读取这个文件就可以获取到输入事件信息,比如按键值什么的。使用read函数读取输入设备文件,也就是/dev/input/eventX,读取到的数据按照input_event结构体组织起来。获取到输入事件以后(input_event结构体类型)使用switch case语句来判断事件类型,本章实验我们设置的事件类型为EV_KEY,因此只需要处理EV_KEY事件即可。比如获取按键编号(KEY_0的编号为11)、获取按键状态,按下还是松开的?
58.4 运行测试58.4.1 编译驱动程序和测试APP
1、编译驱动程序
编写Makefile文件,本章实验的Makefile文件和第四十章实验基本一样,只是将obj-m变量的值改为“keyinput.o”,Makefile内容如下所示:
示例代码58.4.1.1 Makefile文件
  1.   KERNELDIR:=/home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
  2. ......
  3.   obj-m := keyinput.o
  4. ......
  5. clean:
  6. $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
复制代码
    第4行,设置obj-m变量的值为“keyinput.o”。
       输入如下命令编译出驱动模块文件:
  1. make-j32
复制代码
       编译成功以后就会生成一个名为“keyinput.ko”的驱动模块文件。
2、编译测试APP
       输入如下命令编译测试keyinputApp.c这个测试程序:
  1. arm-linux-gnueabihf-gcc keyinputApp.c -o keyinputApp
复制代码
       编译成功以后就会生成keyinputApp这个应用程序。
58.4.2 运行测试
       将上一小节编译出来keyinput.kokeyinputApp这两个文件拷贝到rootfs/lib/modules/4.1.15目录中,重启开发板,进入到目录lib/modules/4.1.15中。在加载keyinput.ko驱动模块之前,先看一下/dev/input目录下都有哪些文件,结果如图58.4.2.1所示:
58.4.2.1 当前/dev/input目录文件
       从图58.4.2.1可以看出,当前/dev/input目录只有event0mice这两个文件。接下来输入如下命令加载keyinput.ko这个驱动模块。
  1. depmod                        //第一次加载驱动的时候需要运行此命令
  2. modprobe keyinput.ko   //加载驱动模块
复制代码
       当驱动模块加载成功以后再来看一下/dev/input目录下有哪些文件,结果如图58.4.2.2所示:
58.4.2.2 加载驱动以后的/dev/input目录
       从图58.4.2.2可以看出,多了一个event1文件,因此/dev/input/event1就是我们注册的驱动所对应的设备文件。keyinputApp就是通过读取/dev/input/event1这个文件来获取输入事件信息的,输入如下测试命令:
  1. ./keyinputApp /dev/input/event1
复制代码
       然后按下开发板上的KEY按键,结果如图58.4.2.3所示:
58.4.2.3 测试结果
       从图58.4.2.3可以看出,当我们按下或者释放开发板上的按键以后都会在终端上输出相应的内容,提示我们哪个按键按下或释放了,在Linux内核中KEY_011
       另外,我们也可以不用keyinputApp来测试驱动,可以直接使用hexdump命令来查看/dev/input/event1文件内容,输入如下命令:
  1. hexdump /dev/input/event1
复制代码
       然后按下按键,终端输出如图58.4.2.4所示信息:
58.4.2.4 原始数据值
       58.4.2.4就是input_event类型的原始事件数据值,采用十六进制表示,这些原始数据的含义如下:
示例代码58.4.2.1 input_event类型的原始事件值
  1.     /*****************input_event类型********************/
  2. /* 编号 */   /* tv_sec */    /* tv_usec */   /* type */  /* code */   /* value */
  3. 0000000    0c410000   d7cd 000c      0001       000b        0001 0000
  4. 0000010    0c41 0000  d7cd000c   00000000   00000000
  5. 0000020    0c420000   54bb0000   0001 000b  00000000
  6. 0000030    0c420000   54bb0000   00000000   00000000
复制代码
    type为事件类型,查看示例代码58.1.2.3可知,EV_KEY事件值为1EV_SYN事件值为0。因此第1行表示EV_KEY事件,第2行表示EV_SYN事件。code为事件编码,也就是按键号,查看示例代码58.1.2.4可以,KEY_0这个按键编号为11,对应的十六进制为0xb,因此第1行表示KEY_0这个按键事件,最后的value就是按键值,为1表示按下,为0的话表示松开。综上所述,示例代码58.4.2.1中的原始事件值含义如下:
       1行,按键(KEY_0)按下事件。
       2行,EV_SYN同步事件,因为每次上报按键事件以后都要同步的上报一个EV_SYN事件。
       3行,按键(KEY_0)松开事件。
       4行,EV_SYN同步事件,和第2行一样。
58.5 Linux自带按键驱动程序的使用58.5.1 自带按键驱动程序源码简析
       Linux内核也自带了KEY驱动,如果要使用内核自带的KEY驱动的话需要配置Linux内核,不过Linux内核一般默认已经使能了KEY驱动,但是我们还是要检查一下。按照如下路径找到相应的配置选项:
  1. -> Device Drivers                                                                                          
  2.        ->Input device support                                                                                   
  3.        ->Generic input layer (needed for keyboard, mouse, ...) (INPUT [=y])   
  4.        -> Keyboards (INPUT_KEYBOARD[=y])  
  5.                             ->GPIOButtons
复制代码
       选中“GPIO Buttons”选项,将其编译进Linux内核中,如图58.5.1.1所示:
58.5.1.1 内核自带KEY驱动使能选项
       选中以后就会在.config文件中出现“CONFIG_KEYBOARD_GPIO=y”这一行,Linux内核就会根据这一行来将KEY驱动文件编译进Linux内核。Linux内核自带的KEY驱动文件为drivers/input/keyboard/gpio_keys.cgpio_keys.c采用了platform驱动框架,在KEY驱动上使用了input子系统实现。在gpio_keys.c文件中找到如下所示内容:
示例代码58.5.1.1 gpio_keys文件代码段
  1. staticconststruct of_device_id gpio_keys_of_match[]={
  2. {.compatible ="gpio-keys",},
  3. {},
  4. };
  5. ......
  6. staticstruct platform_driver gpio_keys_device_driver ={
  7. .probe      = gpio_keys_probe,
  8. .remove     = gpio_keys_remove,
  9. .driver     ={
  10. .name   ="gpio-keys",
  11. .pm =&gpio_keys_pm_ops,
  12. .of_match_table = of_match_ptr(gpio_keys_of_match),
  13. }
  14. };

  15. staticint __init gpio_keys_init(void)
  16. {
  17. return platform_driver_register(&gpio_keys_device_driver);
  18. }

  19. staticvoid __exit gpio_keys_exit(void)
  20. {
  21.      platform_driver_unregister(&gpio_keys_device_driver);
  22. }
复制代码
       从示例代码58.5.1.1可以看出,这就是一个标准的platform驱动框架,如果要使用设备树来描述KEY设备信息的话,设备节点的compatible属性值要设置为“gpio-keys”。当设备和驱动匹配以后gpio_keys_probe函数就会执行,gpio_keys_probe函数内容如下(为了篇幅有缩减):
示例代码58.5.1.2 gpio_keys_probe函数代码段
  1. staticint gpio_keys_probe(struct platform_device *pdev)
  2. {
  3. struct device *dev =&pdev->dev;
  4. conststruct gpio_keys_platform_data *pdata =
  5. dev_get_platdata(dev);
  6. struct gpio_keys_drvdata *ddata;
  7. struct input_dev *input;
  8. size_t size;
  9. int i, error;
  10. int wakeup =0;

  11. if(!pdata){
  12.          pdata = gpio_keys_get_devtree_pdata(dev);
  13. if(IS_ERR(pdata))
  14. return PTR_ERR(pdata);
  15. }
  16. ......
  17.      input = devm_input_allocate_device(dev);
  18. if(!input){
  19.          dev_err(dev,"failed to allocate input device\n");
  20. return-ENOMEM;
  21. }

  22.      ddata->pdata = pdata;
  23.      ddata->input = input;
  24.      mutex_init(&ddata->disable_lock);

  25.      platform_set_drvdata(pdev, ddata);
  26.      input_set_drvdata(input, ddata);

  27.      input->name = pdata->name ?: pdev->name;
  28.      input->phys ="gpio-keys/input0";
  29.      input->dev.parent =&pdev->dev;
  30.      input->open = gpio_keys_open;
  31.      input->close = gpio_keys_close;

  32.      input->id.bustype = BUS_HOST;
  33.      input->id.vendor =0x0001;
  34.      input->id.product =0x0001;
  35.      input->id.version =0x0100;

  36. /* Enable autorepeat feature of Linux input subsystem */
  37. if(pdata->rep)
  38.          __set_bit(EV_REP, input->evbit);

  39. for(i =0; i < pdata->nbuttons; i++){
  40. conststruct gpio_keys_button *button =&pdata->buttons[i];
  41. struct gpio_button_data *bdata =&ddata->data[i];

  42.          error = gpio_keys_setup_key(pdev, input, bdata, button);
  43. if(error)
  44. return error;

  45. if(button->wakeup)
  46.              wakeup =1;
  47. }
  48. ......
  49.      error = input_register_device(input);
  50. if(error){
  51.          dev_err(dev,"Unable to register input device, error:%d\n",
  52.              error);
  53. goto err_remove_group;
  54. }
  55. ......
  56. }
复制代码
    第700行,调用gpio_keys_get_devtree_pdata函数从设备树中获取到KEY相关的设备节点信息。
第713行,使用devm_input_allocate_device函数申请input_dev。
       第726~735,初始化input_dev。
       第739行,设置input_dev事件,这里设置了EV_REP事件。
       第745行,调用gpio_keys_setup_key函数继续设置KEY,此函数会设置input_dev的EV_KEY事件已经事件码(也就是KEY模拟为哪个按键)。
       第760行,调用input_register_device函数向Linux系统注册input_dev。
       我们接下来再来看一下gpio_keys_setup_key函数,此函数内容如下:
示例代码58.5.1.3gpio_keys_setup_key函数代码段
  1. staticint gpio_keys_setup_key(struct platform_device *pdev,
  2. struct input_dev *input,
  3. struct gpio_button_data *bdata,
  4. conststruct gpio_keys_button *button)
  5. {
  6. constchar*desc = button->desc ? button->desc :"gpio_keys";
  7. struct device *dev =&pdev->dev;
  8.      irq_handler_t isr;
  9. unsignedlong irqflags;
  10. int irq;
  11. int error;

  12.      bdata->input = input;
  13.      bdata->button = button;
  14.      spin_lock_init(&bdata->lock);

  15. if(gpio_is_valid(button->gpio)){

  16.          error = devm_gpio_request_one(&pdev->dev, button->gpio,
  17.                            GPIOF_IN, desc);
  18. if(error <0){
  19.              dev_err(dev,"Failed to request GPIO %d, error%d\n",
  20.                  button->gpio, error);
  21. return error;
  22. ......
  23.          isr = gpio_keys_gpio_isr;
  24.          irqflags = IRQF_TRIGGER_RISING |IRQF_TRIGGER_FALLING;

  25. }else{
  26. if(!button->irq){
  27.              dev_err(dev,"No IRQ specified\n");
  28. return-EINVAL;
  29. }
  30.          bdata->irq = button->irq;
  31. ......

  32.          isr = gpio_keys_irq_isr;
  33.          irqflags =0;
  34. }

  35.      input_set_capability(input, button->type ?: EV_KEY,
  36. button->code);
  37. ......
  38. return0;
  39. }
复制代码
    第511行,调用input_set_capability函数设置EV_KEY事件以及KEY的按键类型,也就是KEY作为哪个按键?我们会在设备树里面设置指定的KEY作为哪个按键。
       一切都准备就绪以后剩下的就是等待按键按下,然后向Linux内核上报事件,事件上报是在gpio_keys_irq_isr函数中完成的,此函数内容如下:
示例代码58.5.1.4gpio_keys_irq_isr函数代码段
  1. static irqreturn_t gpio_keys_irq_isr(int irq,void*dev_id)
  2. {
  3. struct gpio_button_data *bdata = dev_id;
  4. conststruct gpio_keys_button *button = bdata->button;
  5. struct input_dev *input = bdata->input;
  6. unsignedlong flags;

  7.      BUG_ON(irq != bdata->irq);

  8.      spin_lock_irqsave(&bdata->lock, flags);

  9. if(!bdata->key_pressed){
  10. if(bdata->button->wakeup)
  11.              pm_wakeup_event(bdata->input->dev.parent,0);

  12.          input_event(input, EV_KEY, button->code,1);
  13.          input_sync(input);

  14. if(!bdata->release_delay){
  15.              input_event(input, EV_KEY, button->code,0);
  16.              input_sync(input);
  17. goto out;
  18. }

  19.          bdata->key_pressed = true;
  20. }

  21. if(bdata->release_delay)
  22.          mod_timer(&bdata->release_timer,
  23.              jiffies + msecs_to_jiffies(bdata->release_delay));
  24. out:
  25.      spin_unlock_irqrestore(&bdata->lock, flags);
  26. return IRQ_HANDLED;
  27. }
复制代码
    gpio_keys_irq_isr是按键中断处理函数,第407行向Linux系统上报EV_KEY事件,表示按键按下。第408行使用input_sync函数向系统上报EV_REP同步事件。
       综上所述,Linux内核自带的gpio_keys.c驱动文件思路和我们前面编写的keyinput.c驱动文件基本一致。都是申请和初始化input_dev,设置事件,向Linux内核注册input_dev。最终在按键中断服务函数或者消抖定时器中断服务函数中上报事件和按键值。
58.5.2 自带按键驱动程序的使用
       要使用Linux内核自带的按键驱动程序很简单,只需要根据Documentation/devicetree/bindings/input/gpio-keys.txt这个文件在设备树中添加指定的设备节点即可,节点要求如下:
       ①、节点名字为“gpio-keys”。
       ②、gpio-keys节点的compatible属性值一定要设置为“gpio-keys”。
       ③、所有的KEY都是gpio-keys的子节点,每个子节点可以用如下属性描述自己:
       gpiosKEY所连接的GPIO信息。
       interruptsKEY所使用GPIO中断信息,不是必须的,可以不写。
       labelKEY名字
       linux,codeKEY要模拟的按键,也就是示例代码58.1.2.4中的这些按键。
       ④、如果按键要支持连按的话要加入autorepeat
       打开imx6ull-alientek-emmc.dts,根据上面的要求创建对应的设备节点,设备节点内容如下所示:
示例代码58.5.2.1 gpio-keys节点内容
  1.   gpio-keys {
  2.       compatible ="gpio-keys";
  3.       #address-cells =<1>;
  4.       #size-cells =<0>;
  5.       autorepeat;
  6.       key0 {
  7.           label ="GPIO Key Enter";
  8.           linux,code =<KEY_ENTER>;
  9.           gpios =<&gpio1 18 GPIO_ACTIVE_LOW>;
  10. };
  11. };
复制代码
    5行,autorepeat表示按键支持连按。
       6~10行,ALPHA开发板KEY按键信息,名字设置为“GPIO Key Enter”,这里我们将开发板上的KEY按键设置为“EKY_ENTER”这个按键,也就是回车键,效果和键盘上的回车键一样。后面学习LCD驱动的时候需要用到此按键,因为Linux内核设计的10分钟以后LCD关闭,也就是黑屏,就跟我们用电脑或者手机一样,一定时间以后关闭屏幕。这里将开发板上的KEY按键注册为回车键,当LCD黑屏以后直接按一下KEY按键即可唤醒屏幕,就跟当电脑熄屏以后按下回车键即可重新打开屏幕一样。
       最后设置KEY所使用的IOGPIO1_IO18,一定要检查一下设备树看看此GPIO有没有被用到其他外设上,如果有的话要删除掉相关代码!
重新编译设备树,然后用新编译出来的imx6ull-alientek-emmc.dtb启动Linux系统,系统启动以后查看/dev/input目录,看看都有哪些文件,结果如图58.5.2.1所示:
58.5.2.1 /dev/input目录文件
       从图58.5.2.1可以看出存在event1这个文件,这个文件就是KEY对应的设备文件,使用hexdump命令来查看/dev/input/event1文件,输入如下命令:
  1. hexdump /dev/input/event1
复制代码
       然后按下ALPHA开发板上的按键,终端输出图58.5.2.2所示内容:
58.5.2.2 按键信息
       如果按下KEY按键以后会在终端上输出图58.5.2.2所示的信息那么就表示Linux内核的按键驱动工作正常。至于图58.5.2.2中内容的含义大家就自行分析,这个已经在58.4.2小节详细的分析过了,这里就不再讲解了。
       大家如果发现按下KEY按键以后没有反应,那么请检查一下三方面:
       ①、是否使能Linux内核KEY驱动。
       ②、设备树中gpio-keys节点是否创建成功。
       ③、在设备树中是否有其他外设也使用了KEY按键对应的GPIO,但是我们并没有删除掉这些外设信息。检查Linux启动log信息,看看是否有类似下面这条信息:
  1. gpio-keys gpio_keys:FailedtorequestGPIO 18, error -16
复制代码

       上述信息表示GPIO 18申请失败,失败的原因就是有其他的外设正在使用此GPIO

回复

使用道具 举报

86

主题

984

帖子

0

精华

论坛大神

Rank: 7Rank: 7Rank: 7

积分
1851
金钱
1851
注册时间
2013-4-15
在线时间
164 小时
2#
发表于 2023-9-25 09:46:27 | 只看该作者
楼主,问个问题,我看你的按键对应的IO初始化为 上升沿/下降沿 触发的,而且在定时器中断服务函数里面上报的是EV_KEY事件,请问,为啥在当按键按下不放的时候,为啥在应用程序里面可以一直检测到按键按下?定时器服务函数里面你也没有上报 “重复”事件
合肥-文盲
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /1 下一条

正点原子公众号

如发现本坛存在违规或侵权内容, 请点击这里发送邮件举报 (或致电020-38271790)。请提供侵权说明和联系方式。我们将及时审核依法处理,感谢配合。

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2026-4-23 00:23

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表