第三章 GPIO驱动开发:寄存器操作、位带操作、按键消抖与LED控制
各位同学,欢迎来到第三章。这一章我们聊聊GPIO——说白了,就是单片机跟外界打交道最基础的方式。LED亮不亮,按键按没按,全靠它。
我刚开始做嵌入式那会儿,觉得GPIO太简单了,不就是置1清0嘛。直到有一次,一个按键抖动让我排查了整整两天……嗯,从那以后我再也不敢小看GPIO了。
3.1 寄存器操作:直接跟硬件对话
GPIO的本质是什么?就是一堆寄存器。你往寄存器里写值,引脚电平就变了;你读寄存器,就知道引脚电平是高是低。
以STM32F103为例,GPIO有以下几个关键寄存器:
| 寄存器 | 功能 | 位宽 |
|---|---|---|
| GPIOx_CRL | 低8位引脚配置(模式、速度) | 32位 |
| GPIOx_CRH | 高8位引脚配置 | 32位 |
| GPIOx_IDR | 输入数据寄存器(只读) | 32位 |
| GPIOx_ODR | 输出数据寄存器 | 32位 |
| GPIOx_BSRR | 位设置/清除寄存器 | 32位 |
| GPIOx_BRR | 位清除寄存器 | 32位 |
直接操作寄存器,代码长这样:
// 开启GPIOB时钟
*(unsigned int*)0x40021018 |= (1 << 3);
// 配置PB0为推挽输出,50MHz
*(unsigned int*)0x40010C00 &= ~(0x0F << 0); // 先清0
*(unsigned int*)0x40010C00 |= (0x03 << 0); // 设为推挽输出
// PB0输出高电平
*(unsigned int*)0x40010C0C |= (1 << 0);
// PB0输出低电平
*(unsigned int*)0x40010C0C &= ~(1 << 0);
看着是不是有点晕?我当年第一次看到这种写法,心里想的是:这地址怎么来的?别急,这些地址在芯片手册里都能查到。不过说实话,实际项目中很少有人这么写——太容易出错了。
|= 0x03,结果因为之前的值没清干净,引脚配置全乱了。这种bug最难查。
3.2 位带操作:让GPIO像51单片机一样简单
用过51单片机的同学都知道,操作P1.0口直接写 P1_0 = 1 就行,多爽。到了ARM Cortex-M3,寄存器操作变得复杂了。好在有个好东西——位带操作。
位带操作是什么?说白了,就是把一个32位寄存器的每一位,映射到一个独立的32位地址上。你操作这个地址,就等于操作那一位。
STM32的位带区域有两个:
- SRAM区:0x20000000 ~ 0x200FFFFF(1MB)
- 外设区:0x40000000 ~ 0x400FFFFF(1MB)
映射公式很简单:
位带别名地址 = 位带基址 + (字节偏移 × 32) + (位号 × 4)
举个例子,GPIOB的ODR寄存器地址是0x40010C0C,我们要操作第0位:
// 计算位带别名地址
// 外区位带基址:0x42000000
// 字节偏移:0x40010C0C - 0x40000000 = 0x10C0C
// 位号:0
#define PB0_OUT (*(volatile unsigned int*)(0x42000000 + (0x10C0C * 32) + (0 * 4)))
// 使用
PB0_OUT = 1; // PB0输出高电平
PB0_OUT = 0; // PB0输出低电平
是不是清爽多了?我个人习惯把常用的位带地址都定义成宏,用起来跟51单片机一样顺手。
3.3 按键消抖:别让抖动坑了你
按键消抖,这绝对是新手最容易踩的坑。你想想看,一个机械按键按下去,触点会弹跳几次,时间大概5~20ms。如果不做消抖,一次按键可能被识别成好几次。
我曾经在一个项目中,按键控制电机启停。没做消抖,结果按一下,电机来回切换了三四次……那场面,别提多尴尬了。
消抖有两种常用方法:
3.3.1 硬件消抖
在按键两端并联一个0.1μF的电容。利用电容的充放电特性,把抖动滤掉。优点是简单,缺点是需要额外元件。
3.3.2 软件消抖
这是最常用的方法。核心思路:检测到按键状态变化后,延时10~20ms再读一次。如果状态一致,就确认按键按下。
// 按键消抖函数
uint8_t Key_Scan(void)
{
static uint8_t last_state = 1; // 上次状态,默认高电平(未按下)
uint8_t current_state;
current_state = KEY_IN; // 读取当前按键状态
// 状态变化了?
if(current_state != last_state)
{
// 延时消抖
Delay_ms(10);
// 再读一次
current_state = KEY_IN;
// 如果状态一致,说明真的变化了
if(current_state != last_state)
{
last_state = current_state;
// 如果是按下(低电平有效)
if(current_state == 0)
{
return 1; // 返回按键按下
}
}
}
return 0; // 无按键事件
}
3.4 LED控制:从点灯到流水灯
LED控制是GPIO输出的经典应用。一个LED加一个限流电阻,正极接VCC,负极接GPIO——这叫低电平点亮。反过来就是高电平点亮。
我建议新手先做这个电路:
// LED初始化
void LED_Init(void)
{
// 开启GPIOB时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
// PB0、PB1配置为推挽输出,50MHz
GPIOB->CRL &= ~(0xFF << 0); // 清空PB0、PB1配置
GPIOB->CRL |= (0x33 << 0); // 设为推挽输出
}
// 点亮LED
void LED_On(uint8_t pin)
{
// 使用位带操作
PB0_OUT = (pin == 0) ? 0 : PB0_OUT;
PB1_OUT = (pin == 1) ? 0 : PB1_OUT;
}
// 熄灭LED
void LED_Off(uint8_t pin)
{
PB0_OUT = (pin == 0) ? 1 : PB0_OUT;
PB1_OUT = (pin == 1) ? 1 : PB1_OUT;
}
// 流水灯效果
void LED_Flow(void)
{
uint8_t i;
for(i = 0; i < 8; i++)
{
GPIOB->ODR = ~(1 << i); // 低电平点亮
Delay_ms(200);
}
}
这里有个细节:用 ODR 直接赋值可以一次控制多个LED,但要注意不要影响其他引脚。如果只想改变某一位,用 BSRR 寄存器更安全。
(3.3V - 2V) / 10mA ≈ 130Ω,取个220Ω就挺合适。
3.5 实战:按键控制LED
把前面学的串起来,做一个按键控制LED的小项目:
int main(void)
{
// 初始化
LED_Init();
Key_Init();
while(1)
{
// 检测按键
if(Key_Scan() == 1)
{
// 切换LED状态
LED_Toggle(0); // 翻转PB0
}
}
}
// LED翻转函数
void LED_Toggle(uint8_t pin)
{
// 读取当前状态并取反
if(pin == 0)
{
PB0_OUT = !PB0_OUT;
}
}
这个程序看起来简单,但包含了GPIO驱动开发的核心:配置、输入、输出、消抖。你把这个吃透了,后面学什么I2C、SPI都会轻松很多。
好了,这一章的内容就到这里。GPIO驱动开发,说白了就是三板斧:配置寄存器、读输入、写输出。但要把这三板斧用好,消抖、位带操作这些小技巧一个都不能少。下一章我们聊聊定时器,那个更有意思。