Skip to content

GPIO 按键驱动 - 我们终于要从输出走向输入了

前面几章我们都在折腾输出设备——LED、蜂鸣器,这些有个共同点:CPU 控制它们,想让它们干嘛就干嘛。说干就干,写个寄存器,LED 就亮了;再写一次,蜂鸣器就叫了。这种掌控感确实让人上瘾。

但现实世界不全是输出设备。我们还需要处理输入设备——按键、传感器、触摸屏,这些设备不听 CPU 的指挥,反过来是它们告诉 CPU 发生了什么。这种角色的转换一开始让人有点不适应,我承认刚开始学的时候确实折腾了好几天。

输入和输出的本质区别

输出设备和输入设备的区别说起来简单,但理解透彻不容易:

c
/* 输出设备:CPU 写 GPIO */
writel(GPIO_DR, val);  // LED 亮了

/* 输入设备:CPU 读 GPIO */
u32 val = readl(GPIO_DR);  // 按键状态

区别就在这一读一写。输出是 CPU 主动的,输入是 CPU 被动的。输出什么时候发生由代码决定,输入什么时候发生由用户决定(用户什么时候按键谁知道)。

这个"谁说了算"的问题,直接影响了驱动的设计。

按键驱动要解决的问题

说实话,第一次写按键驱动的时候,我以为很简单。不就是读个 GPIO 吗?能有多复杂。结果现实给了我一记响亮的耳光,坑真的不少。

第一个问题是:怎么知道按键被按下了?

你说我读 GPIO 状态吧,什么时候读?一直读?那 CPU 不就空转了吗?间隔着读?那按键刚好在两次读取之间按下怎么办?

第二个问题是:按键会抖动

这是一个真实的坑

机械按键的触点在接触瞬间会抖动。你按一下,GPIO 电平可能会跳变好几次:

理想波形(以为):
    ┌───────────────────
────┘                   └─────

实际波形(真实):
    ┌─┬───┬─────┬────────
────┘ │   │     │        └──
    └─┘   └─┬─┬─┘
             └─┘
    ←──── 5-50ms ────→

我第一次测试驱动的时候,按一下按键,应用程序居然收到好多次事件。查了半天才发现是这个抖动问题。真的,这种硬件特性只有踩过坑才会印象深刻。

第三个问题是:CPU 占用的问题

如果你用轮询方式——就是不停地读 GPIO 状态——CPU 占用率会很高。这不像 LED,设置一下就完事了,按键驱动需要持续监控。

我们先学轮询方式

这些问题的终极解决方案是用中断。但中断有自己的复杂度,需要理解中断系统、中断处理函数、下半部机制这些概念。

所以我们决定分两步走:先学轮询方式,再学中断方式。

轮询方式虽然效率低,但有几个好处:

轮询方式的教学价值

  1. 简单直接——代码逻辑一目了然,没有隐藏的魔法
  2. 容易调试——出问题了直接看循环里的状态就行
  3. 建立概念——先理解"等待事件"的概念,再学中断会轻松很多

说实话,我觉得如果一开始就学中断,很容易迷失在各种机制里。先跑通轮询,建立信心,再学中断,这条路更平滑。

按键的硬件连接

我们的 Alpha 开发板上有一个按键,连接方式是经典的低电平触发:

     +3.3V
       |
       <
       > 10kΩ 上拉电阻
       <
       |
       +---- GPIO1_IO18
       |
    按键开关
       |
      GND

这个电路的工作原理:

  • 按键松开时,上拉电阻把 GPIO 拉到高电平(3.3V)
  • 按键按下时,开关导通,GPIO 被拉到地(0V)

为什么用上拉而不是下拉?

这是个设计选择问题。上拉的好处是功耗低——按键松开时几乎没有电流。下拉的话,按键按下时会有电流从 VCC 流到 GND。对于电池供电的设备,这个差异可能很重要。

另外,很多传统设计习惯用上拉,可能历史原因多一些。

设备树配置

设备树里这样描述这个按键:

c
imxaes_key_gpio: key-gpio {
    compatible = "imxaes-key-gpio";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_key>;
    gpios = <&gpio1 18 GPIO_ACTIVE_LOW>;
    status = "okay";
};

这里的 GPIO_ACTIVE_LOW 很关键。它告诉内核:这个按键是低电平触发的。当按键按下时,GPIO 物理上是低电平,但逻辑上应该解释为"按键按下"(1)。

这个反转逻辑在驱动层会自动处理,我们写代码的时候不用管。

本章教程的结构

为了把这个主题讲清楚,我们把内容拆成了几个小节:

  1. GPIO 输入机制——怎么配置 GPIO 为输入,怎么读取状态
  2. 轮询实现——在 read() 函数里循环等待按键事件
  3. 抖动现象——为什么按键会抖,轮询方式怎么处理
  4. 编译测试——跑起来看看真实效果

每一节都比较短,可以慢慢消化。说实话,输入设备确实比输出设备复杂一点,但一步一步来,没什么搞不定的。

为什么值得学这个

你可能会问:现在都有现成的按键驱动框架(gpio-keys),为什么还要自己从头写?

学习价值大于实用价值

确实,实际工程里你会直接用 gpio-keys 这个通用驱动,配置好设备树就行了。但自己写一遍轮询驱动,你会理解:

  • GPIO 输入的底层机制
  • 轮询方式的工作原理和局限性
  • 为什么需要中断机制

有了这些理解,以后遇到更复杂的输入设备——触摸屏、加速度计——你就知道从哪里下手了。

好了,背景介绍差不多了。下一节我们开始看代码,先从 GPIO 输入的机制说起。


下一章: GPIO 输入机制

Built with VitePress