4、GPIO与数字信号采集:LED控制、按键输入、数字信号读取、中断基础

各位同学,欢迎来到第四章。这一章我们聊聊GPIO——也就是通用输入输出口。说白了,它就是芯片和外界打交道的最基本方式。你想想看,一个单片机要控制世界,总得有个“手脚”吧?GPIO就是那个手脚。

我个人习惯把GPIO比作一个“智能开关”。它既能输出高低电平去点亮LED、驱动继电器,也能读取外部信号是高还是低。嗯,这里要注意:GPIO虽然基础,但坑也不少。我在项目中遇到过因为GPIO配置不当导致整块板子电流倒灌烧芯片的事,所以咱们得认真对待。

核心概念:GPIO(General Purpose Input/Output)是微控制器最基础的引脚功能。每个引脚可以独立配置为输入或输出模式。输出模式下,我们可以控制引脚输出高电平(通常3.3V或5V)或低电平(0V);输入模式下,我们可以读取外部信号的电平状态。

4.1 LED控制:从点亮到PWM调光

先来个最简单的——点亮LED。这几乎是每个嵌入式工程师的“Hello World”。

电路上,LED正极通过一个限流电阻接到GPIO引脚,负极接地。为什么需要电阻?我曾经见过新手直接把LED怼到引脚上,结果LED瞬间烧了。因为LED内阻很小,不加限流的话电流会很大。一般用220Ω到1kΩ的电阻就行。

代码实现其实就几行:

// 配置GPIO为输出模式
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;          // 引脚0
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL;        // 不上拉也不下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// 点亮LED
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);

// 熄灭LED
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);

// 翻转LED状态
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);

这里有个小细节:推挽输出和开漏输出的区别。推挽输出能主动拉高和拉低电平,驱动能力强;开漏输出只能拉低,拉高要靠外部上拉电阻。我建议初学者一律用推挽输出,除非你明确知道要用开漏(比如I2C总线)。

再进阶一点,用PWM(脉冲宽度调制)控制LED亮度。说白了就是快速开关LED,通过调整占空比让人眼感觉亮度变化。STM32的定时器可以硬件生成PWM,不需要软件去翻转引脚,效率高得多。

// 初始化定时器PWM
TIM_OC_InitTypeDef sConfigOC = {0};
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500;  // 占空比,范围0-999
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);

// 动态调整亮度
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 750); // 75%亮度

经验之谈:PWM频率不要低于100Hz,否则人眼能感觉到闪烁。我一般用1kHz,既省电又平滑。如果驱动电机,频率要更高,通常10kHz以上。

4.2 按键输入:去抖是必修课

按键输入看起来简单——读引脚电平就行。但实际项目中,按键按下和松开时会产生机械抖动,电平会在几毫秒内反复跳变。如果不处理,一次按键可能被误判成十几次。

我曾经在一个工业控制面板上吃过这个亏。操作员按一次“启动”,机器抖动了七八次,差点出事故。从那以后,我所有按键都加了去抖处理。

去抖有两种方式:硬件去抖(加RC滤波电路)和软件去抖(延时采样)。嵌入式开发中,软件去抖更常用:

// 按键去抖检测
uint8_t Key_Scan(void)
{
    static uint8_t last_state = 1;  // 上次状态,默认高电平(未按下)
    static uint16_t count = 0;
    uint8_t current_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);
    
    if (current_state != last_state) {
        // 状态变化,开始计数
        count++;
        if (count >= 5) {  // 连续5次采样一致,认为稳定
            last_state = current_state;
            count = 0;
            if (current_state == 0) {  // 低电平表示按下
                return 1;  // 有效按键
            }
        }
    } else {
        count = 0;  // 状态一致,清零计数
    }
    return 0;
}

这个代码的核心思想:不立即相信引脚电平变化,而是连续采样多次,确认电平稳定后才认为按键有效。采样间隔一般用10ms,5次就是50ms的去抖时间,足够滤掉机械抖动。

注意:按键电路要接上拉电阻(内部或外部),确保未按下时引脚为高电平。如果悬空,引脚电平不确定,会随机触发。我见过有人省掉上拉电阻,结果按键完全没法用。

4.3 数字信号读取:不只是读电平

数字信号读取,听起来就是读0和1。但实际项目中,信号可能来自各种传感器:霍尔开关、光电传感器、编码器等等。这些信号往往有特定的时序要求。

举个例子,读取一个旋转编码器的AB相脉冲。A相和B相输出有90度相位差,通过判断哪个信号先变化,就能知道旋转方向。这需要同时读取两个引脚,并且快速判断。

// 读取编码器方向
int8_t Encoder_ReadDir(void)
{
    uint8_t a = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
    uint8_t b = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1);
    
    static uint8_t last_ab = 0;
    uint8_t current_ab = (a << 1) | b;  // 合并为2位值
    
    // 查表法判断方向
    const int8_t dir_table[4][4] = {
        {0, 1, -1, 0},
        {-1, 0, 0, 1},
        {1, 0, 0, -1},
        {0, -1, 1, 0}
    };
    
    int8_t dir = dir_table[last_ab][current_ab];
    last_ab = current_ab;
    return dir;
}

这里用了查表法,比if-else判断快得多。我在做电机控制时,这个函数要在中断里每10微秒调用一次,查表法省下的时间很可观。

4.4 中断基础:别让CPU空等

前面讲的都是轮询方式——CPU不断去读引脚状态。但你想过没有,如果按键10分钟才按一次,CPU这10分钟就一直在空转,啥也干不了。中断就是解决这个问题的。

中断的原理很简单:外部信号变化时,硬件自动暂停当前程序,跳到一个专门的中断服务函数(ISR)去处理,处理完再回来继续执行。这样CPU平时可以干别的事,有事件才响应。

配置一个外部中断的步骤:

  1. 使能GPIO时钟和SYSCFG时钟
  2. 配置GPIO为输入模式
  3. 将GPIO连接到外部中断线(EXTI)
  4. 配置中断触发方式(上升沿、下降沿或双边沿)
  5. 使能中断并编写ISR
// 配置PA0为外部中断,下降沿触发
void EXTI_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_SYSCFG_CLK_ENABLE();
    
    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;  // 下降沿触发
    GPIO_InitStruct.Pull = GPIO_PULLUP;           // 内部上拉
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}

// 中断服务函数
void EXTI0_IRQHandler(void)
{
    if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET) {
        // 清除中断标志位
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
        
        // 处理按键事件
        // 注意:ISR里不要做耗时操作,只设置标志位
        key_pressed_flag = 1;
    }
}

中断编程铁律:ISR要短小精悍,只做最必要的事。我一般只在ISR里设置一个标志位或计数,具体处理放到主循环里。如果在ISR里做延时、打印、复杂计算,整个系统会卡死。

4.5 知识体系总览

下面这张图把本章的核心逻辑串起来了。从GPIO的基本配置,到输出控制LED,再到输入读取按键和数字信号,最后用中断提升效率。每一步都是递进的。

GPIO与数字信号采集知识体系 GPIO 核心 输出模式 推挽输出 / 开漏输出 LED控制:电平翻转 PWM调光:占空比控制 输入模式 按键输入:软件去抖 数字信号:编码器读取 中断触发:EXTI配置 轮询 vs 中断:根据场景选择合适方式

4.6 避坑指南与实用技巧

最后,分享几个我踩过的坑,希望能帮你少走弯路:

  • 引脚复用冲突:有些GPIO引脚同时连接着调试接口(如SWD)或晶振。我曾在某个项目里把SWD的时钟引脚配置成普通GPIO,结果程序烧录一次后就再也连不上调试器了。配置前一定先看数据手册的引脚功能表。
  • 电流驱动能力:单个GPIO引脚一般只能输出几毫安到20毫安。如果需要驱动继电器、电机这类大电流设备,必须加三极管或MOS管驱动。直接接上去,芯片会过热保护甚至烧毁。
  • 中断优先级:多个中断同时触发时,优先级低的会被打断。我建议把时间敏感的中断(如编码器读取)设高优先级,按键这类可以设低一些。
  • 浮空输入:输入引脚如果不接上拉或下拉电阻,电平会随电磁环境飘忽不定。内部上拉电阻一般在40kΩ左右,如果要求更稳定的电平,外部加一个10kΩ上拉电阻更可靠。

调试小技巧:用逻辑分析仪观察GPIO波形,比用万用表直观得多。我调试PWM时,一眼就能看出占空比和频率对不对。几十块钱的USB逻辑分析仪就够用。

好了,这一章的内容就到这里。GPIO看似简单,但它是整个嵌入式系统的基石。把LED控制、按键输入、中断这些基本功练扎实了,后面学传感器通信、外设驱动就会轻松很多。记住:越是基础的东西,越值得花时间吃透。


公众号:蓝海资料掘金营,微信deep3321