2. GPIO基础原理
寄存器编程,说白了就是直接跟芯片的硬件对话。很多初学者喜欢用HAL库、标准库,觉得省事。但我个人习惯,在项目初期一定会把寄存器级别的操作摸一遍。为什么?因为只有看过寄存器,你才能真正理解GPIO的每一毫秒在干什么。
这一章,我们就把GPIO的四个核心操作讲透:模式配置、输出数据、输入读取、复用功能。每个操作我都会结合寄存器地址和位域来讲,最后给出一段完整的实战代码。
2.1 GPIO模式寄存器配置
每个GPIO引脚都有多种工作模式。以STM32F4系列为例,每个引脚由两个寄存器控制:MODER(模式寄存器)和OTYPER(输出类型寄存器)。
MODER寄存器每2个bit控制一个引脚,共4种模式:
| MODER[1:0] | 模式 | 说明 |
|---|---|---|
| 00 | 输入模式 | 复位默认值,引脚为高阻输入 |
| 01 | 输出模式 | 推挽或开漏输出,由OTYPER决定 |
| 10 | 复用功能模式 | 引脚由外设控制(如UART、SPI) |
| 11 | 模拟模式 | 用于ADC/DAC,数字电路断开 |
举个例子,把PA5设置为输出模式:
// PA5对应的MODER位域是[11:10]
// 先清除这两位,再写入01
GPIOA->MODER &= ~(0x3 << 10); // 清除
GPIOA->MODER |= (0x1 << 10); // 设置为输出
嗯,这里要注意:一定要先清除再设置。我见过不少新手直接赋值,结果把其他引脚的模式也改了。调试的时候一脸懵,查了半天才发现是寄存器被整体覆盖了。
#define GPIO_MODER_MODE5_OUTPUT (0x1 << 10),这样代码可读性高,也不容易写错位。
2.2 输出数据寄存器操作
输出数据寄存器(ODR)和置位/复位寄存器(BSRR)是控制GPIO输出的两个关键寄存器。
- ODR:直接写整个端口的数据,但多线程或中断环境下容易产生"读-改-写"冲突。
- BSRR:原子操作,低16位写1置位,高16位写1复位,不会被打断。
我个人强烈建议:输出控制优先用BSRR。为什么?
有一次我在做电机驱动项目,PWM中断里用ODR翻转引脚。结果发现偶尔会出现引脚状态不对。查了三天,最后用示波器抓到是ODR的"读-改-写"被中断打断了。换成BSRR后,问题立刻消失。
// 用BSRR控制PA5
// 置位(输出高电平)
GPIOA->BSRR = (1 << 5);
// 复位(输出低电平)
GPIOA->BSRR = (1 << (5 + 16));
// 翻转(异或操作,仅用于演示,不推荐在中断中使用)
GPIOA->ODR ^= (1 << 5);
2.3 输入数据寄存器读取
输入数据寄存器(IDR)相对简单,每个bit对应一个引脚的当前电平状态。读取时直接读整个寄存器,然后提取对应位即可。
// 读取PA5的电平状态
uint8_t pin_state = (GPIOA->IDR >> 5) & 0x01;
if (pin_state) {
// 高电平
} else {
// 低电平
}
这里有个容易被忽略的点:输入模式下的上下拉配置。如果引脚浮空,IDR读到的值是不确定的。我遇到过一位同事,按键检测时没配置内部上拉,结果引脚悬空,读到的值随机跳变。程序逻辑完全乱掉。
所以,读取输入之前,一定要先确认PUPDR寄存器(上下拉配置寄存器)的设置:
| PUPDR[1:0] | 上下拉状态 |
|---|---|
| 00 | 无上下拉(浮空) |
| 01 | 上拉 |
| 10 | 下拉 |
| 11 | 保留 |
2.4 复用功能配置
当GPIO需要作为外设接口(如UART TX、SPI SCK)时,就要配置复用功能模式。这需要设置两个地方:
- MODER:设置为复用功能模式(10)
- AFR(复用功能寄存器):选择具体的外设功能
AFR寄存器分AFRL(低8位引脚)和AFRH(高8位引脚),每个引脚占用4个bit,对应0~15号复用功能。
// 配置PA9为USART1_TX(复用功能7)
// 1. 设置模式为复用功能
GPIOA->MODER &= ~(0x3 << 18); // PA9对应位域[19:18]
GPIOA->MODER |= (0x2 << 18); // 10 = 复用功能
// 2. 选择复用功能号
// PA9属于AFRH寄存器,偏移量 = (9 - 8) * 4 = 4
GPIOA->AFR[1] &= ~(0xF << 4); // 清除
GPIOA->AFR[1] |= (0x7 << 4); // AF7 = USART1_TX
你想想看,如果MODER没配成复用模式,或者AFR选错了功能号,外设根本不会工作。我调试过一块板子,SPI怎么都通不了,最后发现是AFR里写错了功能号,把SPI的SCK配成了I2C的SCL。这种错误,示波器都看不出来,只能靠对照数据手册逐位检查。
2.5 完整实战:按键控制LED
把上面的知识点串起来,写一个完整的按键控制LED程序。PA0接按键(输入、上拉),PA1接LED(输出、推挽)。
// 寄存器地址定义(以STM32F4为例)
#define GPIOA_BASE 0x40020000
#define GPIOA_MODER (*(volatile uint32_t *)(GPIOA_BASE + 0x00))
#define GPIOA_OTYPER (*(volatile uint32_t *)(GPIOA_BASE + 0x04))
#define GPIOA_PUPDR (*(volatile uint32_t *)(GPIOA_BASE + 0x0C))
#define GPIOA_IDR (*(volatile uint32_t *)(GPIOA_BASE + 0x10))
#define GPIOA_ODR (*(volatile uint32_t *)(GPIOA_BASE + 0x14))
#define GPIOA_BSRR (*(volatile uint32_t *)(GPIOA_BASE + 0x18))
void GPIO_Init(void) {
// 使能GPIOA时钟(RCC寄存器,此处省略)
// PA0:输入模式,上拉
GPIOA_MODER &= ~(0x3 << 0); // 00 = 输入
GPIOA_PUPDR &= ~(0x3 << 0);
GPIOA_PUPDR |= (0x1 << 0); // 01 = 上拉
// PA1:输出模式,推挽
GPIOA_MODER &= ~(0x3 << 2);
GPIOA_MODER |= (0x1 << 2); // 01 = 输出
GPIOA_OTYPER &= ~(0x1 << 1); // 0 = 推挽
}
int main(void) {
GPIO_Init();
while (1) {
// 读取按键状态(低电平有效)
if (!((GPIOA_IDR >> 0) & 0x01)) {
// 按键按下,点亮LED
GPIOA_BSRR = (1 << 1);
} else {
// 按键释放,熄灭LED
GPIOA_BSRR = (1 << (1 + 16));
}
}
}
这段代码虽然简单,但包含了模式配置、输入读取、输出控制三个核心操作。你可以把它作为模板,扩展到更复杂的场景。
2.6 知识体系总览
下面这张图把GPIO寄存器编程的四个操作和它们之间的关系画清楚了。建议你保存下来,调试时对照着看。
这张图把四个操作的关系理清了。你调试的时候,如果某个引脚不工作,就按这个流程排查:模式配对了没?输出用对寄存器了没?输入上下拉设了没?复用功能号选对了没?