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 避坑指南与个人经验
最后,分享几个我这些年积累下来的经验,希望能帮你少走弯路:
- 时钟必须先使能。我见过太多新手,配置了半天GPIO,灯就是不亮。结果发现忘了开时钟。GPIO模块的时钟默认是关闭的,你不开它,它就不工作。
- 输出模式别选错。推挽输出(PP)和开漏输出(OD)不一样。推挽输出能主动拉高拉低,开漏输出只能拉低,拉高要靠外部上拉电阻。驱动LED用推挽就够了。
- 电流限制。单片机的GPIO输出电流有限,一般也就几毫安到20毫安。直接驱动LED没问题,但如果要驱动继电器或者电机,必须加驱动芯片或者三极管。
- 调试时多用示波器。我曾经遇到一个奇怪的问题:代码逻辑完全正确,但LED就是不按预期闪烁。后来用示波器一看,发现引脚波形是对的,但LED的限流电阻焊错了。嗯,硬件问题有时候比软件更隐蔽。
总结一下:GPIO是嵌入式系统最基础也最重要的外设。输出模式让系统“表达”,输入模式让系统“感知”。LED灯就是系统状态的“表情包”。掌握了GPIO,你就掌握了跟硬件对话的基本能力。
下一章,我们会用定时器来精确控制LED的闪烁频率,而不是用那个粗糙的延时函数。到时候你会发现,系统的“呼吸”可以更优雅。