3、GPIO基础与LED指示:学习GPIO的输入输出模式,编写代码控制LED灯,作为系统运行状态指示。

好,咱们开始第三章。这一章要聊的是GPIO——说白了,就是芯片上那些可以编程控制的小引脚。你想想看,一个嵌入式系统要跟外界打交道,靠的就是这些引脚。而LED灯,就是我们跟系统“打招呼”最直接的方式。

我个人习惯,每接触一个新平台,第一件事就是点亮一个LED。这就像程序员学新语言先写“Hello World”一样。灯亮了,说明你的代码、编译器、下载器、芯片,这一整条链路都通了。心里就有底了。

3.1 GPIO是什么?输入输出模式怎么理解?

GPIO,全称General Purpose Input Output,通用输入输出口。每个GPIO引脚,你可以把它配置成两种基本模式:输入模式输出模式

  • 输出模式:芯片通过引脚对外输出高电平(通常是3.3V或5V)或低电平(0V)。用来控制LED、继电器、蜂鸣器这类外设。
  • 输入模式:芯片读取引脚上的电平状态,是高还是低。用来检测按键按下、传感器信号等。

我在项目中遇到过一个问题:有个同事把本该配置成输入的引脚配成了输出,结果一上电,两个芯片的输出端直接怼在一起,冒烟了。嗯,这里要注意——配置模式之前,一定要想清楚这个引脚要干什么

核心概念:GPIO的输入输出模式,本质上是控制引脚内部的一个“开关”。输出模式时,开关连接到芯片内部的电压源或地;输入模式时,开关断开,引脚处于高阻态,只负责“监听”外部电平。

3.2 输出模式:点亮LED,让系统“说话”

咱们先来写一个最简单的程序:让LED闪烁。这背后其实就两个动作:拉高电平拉低电平,中间加个延时。

我建议你手边准备一个开发板,跟着敲一遍。别光看,代码这东西,看十遍不如写一遍。

// 伪代码示例,以STM32为例
#include "stm32f1xx_hal.h"

void LED_Init(void)
{
    __HAL_RCC_GPIOB_CLK_ENABLE();   // 开启GPIOB时钟,别忘了这一步

    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = GPIO_PIN_0;       // PB0引脚
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;  // 推挽输出
    GPIO_InitStruct.Pull = GPIO_NOPULL;     // 不上拉也不下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 低速就够了
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}

void delay_ms(uint32_t ms)
{
    // 简单的延时函数,实际项目中建议用定时器
    for(uint32_t i = 0; i < ms * 1000; i++);
}

int main(void)
{
    HAL_Init();
    LED_Init();

    while(1)
    {
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);   // 点亮LED
        delay_ms(500);                                         // 保持500ms
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); // 熄灭LED
        delay_ms(500);                                         // 保持500ms
    }
}

小技巧:我习惯把LED初始化和控制逻辑分开。初始化函数只做配置,控制逻辑放在主循环里。这样后期要改成呼吸灯或者PWM调光,改起来特别方便。

3.3 输入模式:读取按键,让系统“听”你说话

光让系统输出还不够,有时候我们需要它“听”我们的指令。比如按一下按键,LED就切换状态。这就用到输入模式了。

输入模式有个坑,我踩过。就是引脚悬空的问题。如果引脚什么都没接,它的电平是不确定的,读出来的值可能一会儿高一会儿低。解决办法就是加上拉或下拉电阻。

配置 内部电阻状态 引脚悬空时电平 适用场景
上拉输入 连接到VCC 高电平 按键接地
下拉输入 连接到GND 低电平 按键接VCC
浮空输入 断开 不确定 外部已有上/下拉

我曾经在一个产品上,因为偷懒用了浮空输入,结果按键在没按下的时候,系统时不时误触发。排查了两天才找到原因。从那以后,我只要用输入模式,一定会明确配置上拉或下拉。

// 按键控制LED翻转
void KEY_Init(void)
{
    __HAL_RCC_GPIOA_CLK_ENABLE();

    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = GPIO_PIN_0;       // PA0引脚
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 输入模式
    GPIO_InitStruct.Pull = GPIO_PULLUP;     // 内部上拉,按键接地
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

int main(void)
{
    HAL_Init();
    LED_Init();
    KEY_Init();

    uint8_t last_key_state = 1; // 上拉,默认高电平
    uint8_t led_state = 0;

    while(1)
    {
        uint8_t current_key_state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);

        // 检测下降沿:按键按下时,电平从高变低
        if(last_key_state == 1 && current_key_state == 0)
        {
            led_state = !led_state;
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, led_state);
        }

        last_key_state = current_key_state;
        delay_ms(10); // 简单消抖
    }
}

警告:按键按下和松开时,电平会有抖动。如果不做消抖处理,一次按下可能会被误判成多次。软件消抖最简单的方法就是延时10-20ms再读一次。硬件上也可以加一个RC滤波电路。

3.4 用LED指示系统运行状态

回到咱们的能耗监测系统。LED灯不只是用来“亮着玩”的,它要承载信息。我一般会这样设计状态指示:

  • 常亮:系统正常运行,无异常。
  • 慢闪(1秒一次):系统处于待机或低功耗模式。
  • 快闪(0.2秒一次):系统正在采集数据或通信中。
  • 熄灭:系统掉电或进入深度休眠。

你想想看,现场工程师一看LED的闪烁频率,就能大概知道系统在干什么。这比拿个串口线去连,方便多了。

// 状态指示函数
void LED_Indicate_Status(uint8_t status)
{
    switch(status)
    {
        case STATUS_NORMAL:
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // 常亮
            break;
        case STATUS_STANDBY:
            // 慢闪,在主循环中配合定时器调用
            HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
            break;
        case STATUS_BUSY:
            // 快闪
            HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
            break;
        case STATUS_SLEEP:
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); // 熄灭
            break;
        default:
            break;
    }
}

3.5 避坑指南与个人经验

最后,分享几个我这些年积累下来的经验,希望能帮你少走弯路:

  1. 时钟必须先使能。我见过太多新手,配置了半天GPIO,灯就是不亮。结果发现忘了开时钟。GPIO模块的时钟默认是关闭的,你不开它,它就不工作。
  2. 输出模式别选错。推挽输出(PP)和开漏输出(OD)不一样。推挽输出能主动拉高拉低,开漏输出只能拉低,拉高要靠外部上拉电阻。驱动LED用推挽就够了。
  3. 电流限制。单片机的GPIO输出电流有限,一般也就几毫安到20毫安。直接驱动LED没问题,但如果要驱动继电器或者电机,必须加驱动芯片或者三极管。
  4. 调试时多用示波器。我曾经遇到一个奇怪的问题:代码逻辑完全正确,但LED就是不按预期闪烁。后来用示波器一看,发现引脚波形是对的,但LED的限流电阻焊错了。嗯,硬件问题有时候比软件更隐蔽。

总结一下:GPIO是嵌入式系统最基础也最重要的外设。输出模式让系统“表达”,输入模式让系统“感知”。LED灯就是系统状态的“表情包”。掌握了GPIO,你就掌握了跟硬件对话的基本能力。

下一章,我们会用定时器来精确控制LED的闪烁频率,而不是用那个粗糙的延时函数。到时候你会发现,系统的“呼吸”可以更优雅。