2. GPIO模块化编程:寄存器映射与位带操作、标准库与HAL库对比、模块化封装GPIO驱动、实战:按键控制LED
GPIO,说白了就是芯片的「手脚」。你要让LED亮,要读按键状态,都得通过它。我刚开始学STM32时,总觉得GPIO太简单,不就是置1清0嘛。直到有一次项目里因为GPIO配置不当,整块板子一上电就短路冒烟……嗯,从那以后我再也不敢小看它了。
这一章,我们不讲虚的。直接上手寄存器映射、位带操作,再对比标准库和HAL库的差异,最后封装一个能直接用的GPIO驱动模块。你跟着我走一遍,以后写GPIO代码就不会再「凭感觉」了。
2.1 寄存器映射:直接操作硬件的「门牌号」
每个GPIO端口都有一组寄存器,它们被映射到固定的内存地址。你往那个地址写数据,硬件就按你的意思动。我习惯把寄存器映射理解成「门牌号」——你知道门牌号,就能直接敲门。
STM32F103的GPIO寄存器基址如下:
| 外设 | 基地址 | 总线 |
|---|---|---|
| GPIOA | 0x4001 0800 | APB2 |
| GPIOB | 0x4001 0C00 | APB2 |
| GPIOC | 0x4001 1000 | APB2 |
每个端口有7个32位寄存器,常用的就这几个:
- GPIOx_CRL / CRH:配置模式(输入、输出、复用、模拟)和速度
- GPIOx_IDR:输入数据寄存器,读引脚电平
- GPIOx_ODR:输出数据寄存器,写引脚电平
- GPIOx_BSRR:位设置/清除寄存器,原子操作置位或清零
2.2 位带操作:把32位寄存器拆成「单个引脚」
为什么要位带操作?你想想看,如果我只想改变PA0的电平,用ODR的话得先读-改-写三步,中间还可能被中断插一脚。位带操作直接把每个位映射到一个独立的32位地址,你直接写那个地址就行。
位带计算公式:
// 位带别名地址 = 位带基址 + (字节偏移 * 32) + (位号 * 4)
// 以PA0为例:
#define PA0_BITBAND (0x42000000 + (0x4001080C - 0x40000000) * 32 + 0 * 4)
// 写1:*(volatile uint32_t *)PA0_BITBAND = 1;
// 写0:*(volatile uint32_t *)PA0_BITBAND = 0;
我一般会封装成宏,用起来更顺手:
// 位带操作宏封装
#define BITBAND(addr, bit) ((volatile uint32_t *)(0x42000000 + ((uint32_t)(addr) - 0x40000000) * 32 + (bit) * 4))
// 使用示例
#define PAout(n) (*BITBAND(&GPIOA->ODR, n))
#define PAin(n) (*BITBAND(&GPIOA->IDR, n))
// 点亮PA0
PAout(0) = 1;
2.3 标准库 vs HAL库:到底选哪个?
这个问题几乎每个初学者都会问。我个人的看法是:学底层用标准库,做产品用HAL库。但前提是,你得知道它们背后干了什么。
| 对比项 | 标准库 | HAL库 |
|---|---|---|
| 封装层次 | 薄封装,接近寄存器 | 厚封装,抽象层次高 |
| 代码量 | 小,执行快 | 大,有额外开销 |
| 可移植性 | 差,依赖具体型号 | 好,CubeMX一键换芯片 |
| 调试难度 | 容易跟踪到底层 | 层层封装,定位慢 |
| 适合场景 | 裸机、RTOS、对实时性要求高 | 快速原型、复杂外设、CubeMX生态 |
举个例子,同样是初始化PA0为推挽输出:
// 标准库方式
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// HAL库方式
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
你看,标准库更直接,HAL库多了个Pull参数。我个人做裸机项目时偏爱标准库,因为代码小、好调试。但如果你用CubeMX生成工程,HAL库确实省事。
2.4 模块化封装GPIO驱动
写代码不能每次都从头复制粘贴。我习惯把GPIO操作封装成一个独立的驱动模块,包含头文件和源文件。这样换项目时直接复制,改改引脚定义就行。
先看头文件 gpio_drv.h:
#ifndef __GPIO_DRV_H
#define __GPIO_DRV_H
#include "stm32f10x.h"
// GPIO方向枚举
typedef enum {
GPIO_DIR_IN = 0,
GPIO_DIR_OUT = 1
} gpio_dir_t;
// GPIO电平枚举
typedef enum {
GPIO_LOW = 0,
GPIO_HIGH = 1
} gpio_level_t;
// GPIO配置结构体
typedef struct {
GPIO_TypeDef *port; // GPIO端口,如GPIOA
uint16_t pin; // 引脚号,如GPIO_Pin_0
gpio_dir_t dir; // 方向
uint8_t pull; // 是否上拉/下拉(0:无, 1:上拉, 2:下拉)
} gpio_cfg_t;
// 接口函数
void gpio_init(gpio_cfg_t *cfg);
void gpio_write(gpio_cfg_t *cfg, gpio_level_t level);
gpio_level_t gpio_read(gpio_cfg_t *cfg);
void gpio_toggle(gpio_cfg_t *cfg);
#endif
再看源文件 gpio_drv.c 的核心实现:
#include "gpio_drv.h"
void gpio_init(gpio_cfg_t *cfg)
{
GPIO_InitTypeDef init = {0};
init.GPIO_Pin = cfg->pin;
if (cfg->dir == GPIO_DIR_OUT) {
init.GPIO_Mode = GPIO_Mode_Out_PP;
init.GPIO_Speed = GPIO_Speed_50MHz;
} else {
if (cfg->pull == 1)
init.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
else if (cfg->pull == 2)
init.GPIO_Mode = GPIO_Mode_IPD; // 下拉输入
else
init.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
}
GPIO_Init(cfg->port, &init);
}
void gpio_write(gpio_cfg_t *cfg, gpio_level_t level)
{
if (level == GPIO_HIGH)
cfg->port->BSRR = cfg->pin; // 原子置位
else
cfg->port->BRR = cfg->pin; // 原子清零
}
gpio_level_t gpio_read(gpio_cfg_t *cfg)
{
return (cfg->port->IDR & cfg->pin) ? GPIO_HIGH : GPIO_LOW;
}
void gpio_toggle(gpio_cfg_t *cfg)
{
cfg->port->ODR ^= cfg->pin;
}
2.5 实战:按键控制LED
理论说完了,咱们直接上手。这个实战的目标是:按下按键,LED亮;松开按键,LED灭。用我们刚封装的GPIO驱动模块来实现。
硬件连接:
- LED:PA0,高电平点亮
- 按键:PA1,按下为低电平(外部上拉)
主程序代码:
#include "gpio_drv.h"
// 定义LED和按键的配置
gpio_cfg_t led = {
.port = GPIOA,
.pin = GPIO_Pin_0,
.dir = GPIO_DIR_OUT,
.pull = 0
};
gpio_cfg_t key = {
.port = GPIOA,
.pin = GPIO_Pin_1,
.dir = GPIO_DIR_IN,
.pull = 1 // 上拉输入
};
int main(void)
{
// 初始化GPIO
gpio_init(&led);
gpio_init(&key);
// 初始状态:LED灭
gpio_write(&led, GPIO_LOW);
while (1)
{
// 读取按键状态
if (gpio_read(&key) == GPIO_LOW) {
// 按键按下,点亮LED
gpio_write(&led, GPIO_HIGH);
} else {
// 按键松开,熄灭LED
gpio_write(&led, GPIO_LOW);
}
}
}
这个实战虽然简单,但它展示了模块化编程的核心思想:把硬件操作封装成函数,上层代码只关心逻辑。你以后做更复杂的项目,比如多个LED、矩阵按键、甚至外设驱动,都可以沿用这个思路。