第2章:GPIO基础与LED控制
各位同学,欢迎来到第二章。
上一章我们搭好了开发环境,点亮了第一颗LED。但说实话,那只是“Hello World”级别的操作。这一章,我们要真正搞懂GPIO是怎么工作的。你想想看,一个按摩仪上那么多按键、指示灯、电机控制,归根结底都是GPIO在干活。
2.1 GPIO工作原理
GPIO,全称General Purpose Input Output,通用输入输出口。说白了,它就是芯片和外界打交道的“手脚”。
每个GPIO引脚,内部其实是一堆MOS管和寄存器。我刚开始学的时候,总觉得这东西很神秘。后来拆了几个芯片的datasheet才发现,原理其实很简单:
- 输出模式:芯片内部写寄存器,引脚就输出高电平或低电平
- 输入模式:读取引脚上的电平状态,判断是0还是1
- 复用功能:比如当作串口、I2C、PWM等专用接口
嗯,这里要注意:不同芯片的GPIO结构略有差异,但核心思想是一样的。我在项目中遇到过一个问题——某次用STM32驱动一个继电器,结果死活拉不低电平。查了半天,原来是GPIO没配置成推挽输出模式。所以,配置模式这一步,千万别偷懒。
核心概念:GPIO的三种基本模式——输入、输出、复用。输出又分推挽和开漏,输入又分浮空和上拉/下拉。
2.2 寄存器操作
操作GPIO,本质就是读写寄存器。每个芯片都有对应的寄存器映射表。以STM32为例,GPIO相关的寄存器主要有:
| 寄存器 | 功能 | 位宽 |
|---|---|---|
| GPIOx_MODER | 模式选择(输入、输出、复用、模拟) | 32位 |
| GPIOx_OTYPER | 输出类型(推挽、开漏) | 32位 |
| GPIOx_OSPEEDR | 输出速度 | 32位 |
| GPIOx_PUPDR | 上拉/下拉配置 | 32位 |
| GPIOx_IDR | 输入数据寄存器(只读) | 32位 |
| GPIOx_ODR | 输出数据寄存器 | 32位 |
| GPIOx_BSRR | 位设置/复位寄存器 | 32位 |
我个人习惯用BSRR寄存器来操作输出。为什么?因为它可以原子操作,不会被打断。比如你要同时拉高PA0和拉低PA1,用ODR需要两步,中间可能被中断干扰。用BSRR一步搞定。
来看一个实际代码:
// 配置PA5为推挽输出,速度50MHz
GPIOA->MODER &= ~(0x3 << (5 * 2)); // 先清零
GPIOA->MODER |= (0x1 << (5 * 2)); // 设置为输出模式
GPIOA->OTYPER &= ~(0x1 << 5); // 推挽输出
GPIOA->OSPEEDR |= (0x2 << (5 * 2)); // 50MHz速度
// 点亮LED
GPIOA->BSRR = (1 << 5); // 置位BS5,输出高电平
// 熄灭LED
GPIOA->BSRR = (1 << (5 + 16)); // 置位BR5,输出低电平
小技巧:操作寄存器时,尽量用“读-改-写”模式,不要直接赋值。否则会影响到其他引脚配置。我曾经见过一个同事直接写MODER = 0xFFFFFFFF,结果所有引脚都变成模拟模式了……
2.3 点亮板载LED
大多数开发板都有一颗板载LED,通常连接在某个GPIO上。以STM32F103C8T6的“蓝色 pill”为例,LED接在PC13上,低电平点亮。
点亮它的步骤很简单:
- 使能GPIOC的时钟
- 配置PC13为推挽输出
- 输出低电平
// 使能GPIOC时钟
RCC->APB2ENR |= (1 << 4); // APB2外设时钟使能寄存器,第4位对应GPIOC
// 配置PC13为推挽输出
GPIOC->CRH &= ~(0xF << ((13 - 8) * 4)); // 先清零
GPIOC->CRH |= (0x3 << ((13 - 8) * 4)); // 设置为推挽输出,50MHz
// 点亮LED(低电平有效)
GPIOC->BSRR = (1 << (13 + 16)); // BR13置位,输出低电平
嗯,这里要注意:不同开发板的LED连接方式不同。有的高电平点亮,有的低电平点亮。我建议你拿到板子后,先看原理图确认一下。我曾经因为没看原理图,直接按高电平点亮写代码,结果LED死活不亮,折腾了半小时才发现是反的。
2.4 实现呼吸灯效果
呼吸灯,说白了就是让LED的亮度从暗到亮、再从亮到暗,循环变化。怎么实现?用PWM(脉冲宽度调制)。
PWM的原理很简单:通过调节高电平占空比,控制LED的平均功率。占空比越大,LED越亮。
但如果我们不用硬件PWM,只用GPIO和延时,也能模拟出呼吸灯效果。方法就是:
- 快速切换GPIO的高低电平
- 每次切换时,改变高电平的持续时间
- 人眼有视觉暂留效应,看起来就是亮度变化
void breath_led(void) {
int i;
// 渐亮
for (i = 0; i < 100; i++) {
GPIOA->BSRR = (1 << 5); // 高电平
delay_us(i * 10); // 延时逐渐增加
GPIOA->BSRR = (1 << (5 + 16)); // 低电平
delay_us((100 - i) * 10); // 延时逐渐减少
}
// 渐暗
for (i = 100; i > 0; i--) {
GPIOA->BSRR = (1 << 5);
delay_us(i * 10);
GPIOA->BSRR = (1 << (5 + 16));
delay_us((100 - i) * 10);
}
}
注意:这种软件模拟PWM的方式,会占用CPU大量时间。在按摩仪这种实时性要求不高的场景下可以用。但如果你的系统有多个任务,建议用硬件PWM模块。我做过一个项目,用软件PWM控制4个LED,结果CPU占用率飙到80%,其他任务全卡死了。
2.5 按键输入检测与消抖
按键,是按摩仪上最常用的输入设备。但按键有个烦人的问题——抖动。
为什么会抖动?因为按键是机械结构,按下和释放的瞬间,触点会弹跳几次,产生多个电平变化。如果不处理,一次按键会被误判成多次。
消抖的方法有两种:
- 硬件消抖:加RC滤波电路,或者用施密特触发器
- 软件消抖:检测到电平变化后,延时10-20ms再读一次
我个人更推荐软件消抖,成本低、灵活。来看代码:
// 按键检测函数,返回1表示按下,0表示未按下
uint8_t key_scan(void) {
static uint8_t last_state = 1; // 上一次状态,默认高电平(未按下)
uint8_t current_state;
current_state = (GPIOA->IDR >> 0) & 0x01; // 读取PA0的电平
// 检测下降沿(高->低)
if (last_state == 1 && current_state == 0) {
delay_ms(20); // 延时消抖
current_state = (GPIOA->IDR >> 0) & 0x01; // 再读一次
if (current_state == 0) {
last_state = 0;
return 1; // 确认按下
}
}
// 检测上升沿(低->高)
if (last_state == 0 && current_state == 1) {
delay_ms(20);
current_state = (GPIOA->IDR >> 0) & 0x01;
if (current_state == 1) {
last_state = 1;
// 释放检测,这里不返回
}
}
return 0;
}
避坑指南:我曾经在一个量产项目中,按键消抖只做了10ms延时。结果有批按键质量不好,抖动时间长达30ms,导致频繁误触发。后来我把消抖时间改成了30ms,问题解决。所以,消抖时间要根据实际按键的规格来定,别死板地用20ms。
另外,按键检测还有一种更高效的方式——状态机。用状态机可以避免阻塞延时,适合在RTOS中使用。不过那是后面章节的内容了,这里先不展开。
小结
这一章我们讲了GPIO的工作原理、寄存器操作、LED控制、呼吸灯和按键消抖。这些都是嵌入式开发的基本功。你想想看,按摩仪上的模式切换按键、力度指示灯、电机启停,哪一样离得开GPIO?
下一章,我们会讲定时器。有了定时器,我们就能做更精确的时间控制,比如PWM输出、脉冲计数等。到时候,呼吸灯效果就可以用硬件PWM来实现,效果会好很多。
好了,这一章就到这里。动手试试吧,把板载LED点亮,再做个呼吸灯,最后接个按键试试消抖效果。遇到问题别怕,调试本身就是学习的过程。