嵌入式C++工程实践第20篇:GPIO 输入模式内部电路 —— 芯片是如何“听“到外部信号的

嵌入式C++工程实践第20篇:GPIO 输入模式内部电路 ------ 芯片是如何"听"到外部信号的

仓库已经开源!仍然在持续建设中,喜欢的话点个⭐!相关的链接如下:
https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeModernCPP
静态网页直接阅览:https://awesome-embedded-learning-studio.github.io/Tutorial_AwesomeModernCPP/
承接上一篇:按钮比 LED 难在三个地方------读取而非写入、物理噪声、时序管理。这一篇我们解决第一个问题:GPIO 输入模式到底是怎么工作的。
PS! 今天或者是明天会发下本仓库的新Release宣传!笔者有一小段时间断更就是再忙这些事情!笔者大幅度重写了以下基本C++的内容,感兴趣的朋友可以直接先到上面的仓库链接转一转!


从输出路径到输入路径

在 LED 教程里,我们花了很多时间理解 GPIO 输出模式的内部电路。输出模式的核心信号路径是:

text 复制代码
CPU 写入 ODR → 输出驱动器(推挽/开漏) → GPIO 引脚 → 外部电路

CPU 往 ODR(Output Data Register,输出数据寄存器)的某个 bit 写 1,对应的推挽驱动器就把引脚拉到 VDD(高电平);写 0 就拉到 VSS(低电平)。信号从芯片内部流向外部世界------芯片是主动方。

现在我们要把这条路径反过来。按钮模式下,信号从外部世界流向芯片内部:

text 复制代码
GPIO 引脚 → 保护二极管 → 上拉/下拉电阻(可选) → 施密特触发器 → IDR → CPU 可读

注意信号方向变了。引脚上的电压不再是 CPU 控制的------它由外部电路决定(在我们的场景中,由按钮的闭合/断开决定)。CPU 的角色从"写 ODR"变成了"读 IDR"------被动地观察引脚电平的变化。


输入路径上的每一站

让我们沿着信号路径,从引脚开始往里走,看看每一站做了什么。

第一站:保护二极管

引脚后面紧接着的是两个保护二极管,一个接 VDD,一个接 VSS。它们的作用是钳位(clamp)------如果引脚上的电压超过 VDD + 0.6V,上面的二极管导通,把多余电压泄放到 VDD;如果低于 VSS - 0.6V,下面的二极管导通,泄放到 VSS。

这层保护对按钮场景来说不是重点------按钮的电压就是 0V 或 3.3V,不会超出范围。但如果你接的是传感器或其他可能产生异常电压的器件,这两个二极管就是芯片不被烧坏的第一道防线。STM32 的引脚能承受的电压范围是 -0.3V 到 VDD + 0.3V(超出这个范围保护二极管开始工作),绝对最大值是 4.0V(超出就真的坏了)。

第二站:上拉/下拉电阻

过了保护二极管,信号来到了一个岔路口。这里有三个选项:

  • 浮空(No Pull):上下拉电阻都断开。引脚电平完全由外部电路决定。如果外部什么都没接(引脚悬空),电平是不确定的------受电磁干扰影响,可能在高和低之间随机跳变。
  • 上拉(Pull-Up):一个内部电阻(大约 30-50kΩ)把信号线接到 VDD。没有外部信号时,引脚被"拉"到高电平。
  • 下拉(Pull-Down):一个内部电阻接到 VSS。没有外部信号时,引脚被"拉"到低电平。

用 ASCII 图来画更直观:

text 复制代码
浮空输入:              上拉输入:              下拉输入:

  引脚 ──→ 后级          引脚 ──→ 后级          引脚 ──→ 后级
                         │                      │
                        [R] ~40kΩ              [R] ~40kΩ
                         │                      │
                        VDD                    VSS

  引脚悬空时:            引脚悬空时:            引脚悬空时:
  电平不确定              高电平                  低电平

⚠️ 注意这些电阻的阻值。根据 STM32F103 数据手册,内部上拉/下拉电阻的范围是 25-60kΩ,典型值约 40kΩ。这个阻值不算小------它只够在没有外部驱动时提供一个"默认电平",不能用来驱动任何负载。但对我们来说,40kΩ 的上拉电阻配合按钮完全够用。

第三站:施密特触发器

信号经过上拉/下拉电阻之后,来到施密特触发器。这是输入路径中最精巧的一站。

施密特触发器本质上是一个带迟滞(hysteresis)的比较器。普通比较器只有一个阈值------输入超过阈值就输出高,低于阈值就输出低。问题在于,如果输入信号恰好在阈值附近晃动(哪怕只有几毫伏的噪声),输出就会在 0 和 1 之间快速跳变------这就是所谓的"振铃"(ringing)。

施密特触发器用两个阈值解决了这个问题:

  • 上升阈值 VT+:信号从低到高变化时,必须超过这个阈值才被认为是"高"。STM32F103 在 3.3V 供电时,数据手册保证 VIH(min) = 0.49×VDD ≈ 1.62V,VT+ 约在 1.6V 左右。
  • 下降阈值 VT-:信号从高到低变化时,必须低于这个阈值才被认为是"低"。数据手册保证 VIL(max) = 0.35×VDD ≈ 1.16V。实际的迟滞(VT+ - VT-)典型值约为 0.06×VDD ≈ 200mV,所以 VT- 约在 1.4V 左右。

两个阈值之间有一个约 200mV 的"迟滞窗口"。在这个窗口内,输出保持上一次的状态不变:

text 复制代码
VT+ ≈ 1.6V
        ──────────────  上升时,超过此阈值 → 输出变高
        | 迟滞窗口 |
        |  ≈ 200mV |
        ──────────────  下降时,低于此阈值 → 输出变低
        VT- ≈ 1.4V

  输入电压:  0V ─────────── 1.4V ── 1.6V ────── 3.3V
  输出:      低              保持    保持         高

这有什么用?想象一个 1.2V 的输入信号恰好在两个阈值之间。普通比较器可能会因为几毫伏的噪声不停翻转输出。但施密特触发器不会------它在 1.2V 时保持上一次的状态。信号必须明确地升过 1.64V 或者降到 0.82V 以下,输出才会改变。这就是"迟滞"的意思------系统有一定的"惯性",不会对小幅度的波动做出反应。

施密特触发器的迟滞和按钮的机械抖动是两个不同层面的问题。施密特触发器消除的是阈值附近的电噪声(毫伏级),而按钮抖动是整个信号在 0V 和 3.3V 之间的大幅度震荡(伏特级)。施密特触发器帮不了按钮抖动------信号在抖动期间在高电平和低电平之间来回跳,每次都明确超过了两个阈值。软件消抖是必须的,这一点我们后面会详细讲。

第四站:IDR 寄存器

施密特触发器的输出最终连接到 GPIOx_IDR(Input Data Register,输入数据寄存器)。IDR 是一个 16 位的只读寄存器,bit 0 对应 Pin 0,bit 1 对应 Pin 1,以此类推到 bit 15 对应 Pin 15。每个 bit 的值就是对应引脚经过施密特触发器整形后的电平------1 表示高电平,0 表示低电平。

CPU 在任何时候都可以读取 IDR 来获知所有引脚的当前输入状态。HAL 库的 HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) 底层就是读取 IDR 寄存器并做位与操作------IDR & Pin 提取对应引脚的电平值。非常快,一个时钟周期就能完成。下一篇我们会完整拆解这个函数。


三种输入模式的选择

理解了输入路径上每一站的作用,现在的问题是:我们的按钮该选哪种输入模式?

浮空输入 --- 不推荐

浮空输入不启用内部上下拉电阻。按钮松开时,PA0 引脚悬空,电平不确定。可能是高,可能是低,可能因为你的手靠近了引脚(人体是导体)而发生变化。这种不确定性意味着你无法区分"按钮松开"和"按钮处于不确定状态"------读出来的值不可靠。

浮空输入适合什么场景?适合外部电路自己提供了明确的电平驱动。比如另一个芯片的输出引脚直接连接过来,它自己会驱动高或低,不需要 STM32 提供默认电平。

上拉输入 --- 我们的选择

上拉输入启用内部上拉电阻。按钮松开时,PA0 通过 40kΩ 电阻接到 VDD,读出来是高电平(1)。按钮按下时,PA0 直接接 GND,电流从 VDD 经 40kΩ 电阻流向 GND,PA0 电压被拉到接近 0V,读出来是低电平(0)。

松开 = 高,按下 = 低。这就是所谓"低电平有效"(Active Low),对应我们代码中的 ButtonActiveLevel::Low。绝大部分 MCU 的按钮方案都用上拉输入,因为 GND 接线比 VCC 方便------Blue Pill 板上 GND 引脚很多,随手就能接。

下拉输入 --- 备选方案

下拉输入启用内部下拉电阻。按钮松开时引脚为低电平,按钮按下时(接 VCC)引脚为高电平。松开 = 低,按下 = 高,即"高电平有效"(Active High),对应 ButtonActiveLevel::High

我们的按钮教程不用下拉方案。但我们的 Button 模板类同时支持两种极性------如果你以后遇到一个高电平有效的按钮,只需要把模板参数改成 ButtonActiveLevel::High 就行。

总结表格

模式 内部电阻 默认电平 适用场景
浮空 不确定 外部有确定性信号源
上拉 接 VDD ~40kΩ 高电平 按钮→GND(Active Low)
下拉 接 VSS ~40kΩ 低电平 按钮→VCC(Active High)

CRL/CRH 寄存器:底层配置

HAL 库把底层寄存器操作封装成了 HAL_GPIO_Init(),你不需要直接操作寄存器。但理解底层有助于调试------当引脚行为不符合预期时,看寄存器配置往往能快速定位问题。

STM32F103 的每个 GPIO 端口有两个配置寄存器:CRL 控制 Pin 0-7,CRH 控制 Pin 8-15。每个 pin 占 4 位:MODE[1:0](2 位)+ CNF[1:0](2 位)。

输入模式下的配置:

MODE[1:0] CNF[1:0] 含义
00 00 模拟输入(ADC 用)
00 01 浮空输入
00 10 上拉/下拉输入(方向由 ODR 对应位决定)

上拉输入的完整配置:MODE=00, CNF=10, ODR bit=1(ODR=1 表示上拉,ODR=0 表示下拉)。

注意一个容易混淆的地方:输入模式下 ODR 的 bit 用来选择上拉还是下拉方向,而不是控制输出电平。这个 bit 在输出模式下控制输出电平,在输入模式下控制上下拉方向------同一个寄存器,不同模式下有不同的含义。

PA0 配置为上拉输入时,GPIOA->CRL 的低 4 位应该是 1000(CNF=10, MODE=00),GPIOA->ODR 的 bit 0 应该是 1。HAL 的 HAL_GPIO_Init() 会帮你处理这些位域操作,你只需要传入正确的 GPIO_InitTypeDef 结构体。


和 gpio.hpp 的对应关系

让我们把硬件知识和代码对应起来。在 device/gpio/gpio.hpp 中,GPIO 模板的 setup() 方法负责配置引脚:

cpp 复制代码
void setup(Mode gpio_mode, PullPush pull_push = PullPush::NoPull, Speed speed = Speed::High) {
    GPIOClock::enable_target_clock();
    GPIO_InitTypeDef init_types{};
    init_types.Pin = PIN;
    init_types.Mode = static_cast<uint32_t>(gpio_mode);
    init_types.Pull = static_cast<uint32_t>(pull_push);
    init_types.Speed = static_cast<uint32_t>(speed);
    HAL_GPIO_Init(native_port(), &init_types);
}

按钮使用时调用 setup(Mode::Input, PullPush::PullUp, Speed::Low)Mode::Input 对应 GPIO_MODE_INPUT(0x00),PullPush::PullUp 对应 GPIO_PULLUP(0x01)。HAL 内部会把这两个值翻译成上面说的 CRL/CRH 位域配置。

而新增的 read_pin_state() 方法直接封装了 IDR 的读取:

cpp 复制代码
[[nodiscard]] State read_pin_state() const {
    return static_cast<State>(HAL_GPIO_ReadPin(native_port(), PIN));
}

HAL_GPIO_ReadPin()IDRstatic_castGPIO_PIN_SET/GPIO_PIN_RESET 转成我们的 State::Set/State::UnSet 枚举。加了 [[nodiscard]] 是因为读引脚状态的结果你不使用的话,这个调用就毫无意义------多半是忘写赋值了。


我们回头看

这一篇我们从引脚出发,沿着保护二极管、上下拉电阻、施密特触发器、IDR 寄存器的路径,搞清楚了 GPIO 输入模式的完整信号链。三个要点:

  1. 上拉输入是我们的按钮方案------松开时高电平,按下时低电平
  2. 施密特触发器消除阈值附近的电噪声,但不能消除按钮的机械抖动
  3. IDR 寄存器 是 CPU 读取引脚状态的窗口,HAL_GPIO_ReadPin() 底层就是读它

下一篇我们把 GPIO 输入的知识用到实际的按钮电路上------画接线图、算电流、看抖动波形。硬件知识准备就绪后,我们就可以开始写代码了。


相关阅读

  1. 现代Qt开发教程(新手篇)1.1------QObject 与元对象系统 - 相似度 100%
  2. 现代Qt开发教程(新手篇)1.2------信号与槽 - 相似度 100%
  3. 通用GUI编程技术------图形渲染实战(二十八)------图像格式与编解码:PNG/JPEG全掌握 - 相似度 100%
相关推荐
xinhuanjieyi1 小时前
极语言让ai学习的方法
开发语言·学习
三佛科技-134163842122 小时前
PD65W快充电源方案LP8841SD+LP35118N(高频QR反激、BOM简洁,小体积,过认证)
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
xiaogutou11212 小时前
2026年历史课件PPT模板选购指南:教师备课效率与精度的平衡方案
开发语言·c#
StockTV2 小时前
印度股票实时数据 NSE和BSE的实时行情、K 线及指数数据
java·开发语言·spring boot·python
chaofan9802 小时前
GPT-5.5 领衔 Image 2.0:像素级控制时代,AI 绘图告别开盲盒
开发语言·人工智能·python·gpt·自动化·api
三佛科技-187366133972 小时前
LP9962AA 集成PFC和高压半桥驱动的LLC谐振控制器(内置碳化硅芯片)
单片机·嵌入式硬件
爱码小白3 小时前
Python 异常处理 完整学习笔记
开发语言·python
c++之路3 小时前
C++20概述
java·开发语言·c++20
芝士就是力量啊 ೄ೨3 小时前
Python如何编写一个简单的类
开发语言·python