3. GPIO基础原理
各位同学,欢迎来到第三章。前两章我们把开发环境搭好了,也跑通了第一个程序。这一章,咱们要真正开始跟硬件打交道了——GPIO,也就是通用输入输出口。说白了,它就是芯片跟外界沟通的“手脚”。
我刚开始学嵌入式那会儿,觉得GPIO不就是输出高电平、低电平嘛,有啥好学的?结果第一次做项目,选错了输出模式,电机死活转不起来,查了两天才发现是推挽和开漏没搞明白。嗯,从那以后,我再也不敢小看GPIO了。
3.1 GPIO的四种核心模式
STM32的GPIO,每个引脚都可以独立配置成多种模式。但咱们做电机驱动和串口通信,最常用的就四种:推挽输出、开漏输出、上拉输入、下拉输入。我习惯把它们分成“输出模式”和“输入模式”两大类来理解。
| 模式 | 类型 | 典型用途 | 内部结构特点 |
|---|---|---|---|
| 推挽输出 | 输出 | 驱动LED、普通数字信号输出 | PMOS+NMOS,可输出强高/强低 |
| 开漏输出 | 输出 | I²C总线、电平转换、多路复用 | 只有NMOS,高电平需外部上拉 |
| 上拉输入 | 输入 | 读取按键、检测开关状态 | 内部接上拉电阻到VDD |
| 下拉输入 | 输入 | 读取按键、防浮空 | 内部接下拉电阻到VSS |
3.1.1 推挽输出——最常用的输出模式
推挽输出,你可以想象成两个开关:一个接电源(PMOS),一个接地(NMOS)。输出高电平时,上面的开关闭合,下面的断开;输出低电平时反过来。这样引脚就能主动输出强高或强低电平,驱动能力很强。
3.1.2 开漏输出——需要“外援”的模式
开漏输出就只有一个NMOS管,它只能主动拉低电平。想输出高电平?对不起,它做不到,得靠外部接一个上拉电阻把电平拉上去。你想想看,这有什么好处?
- 电平转换:5V的器件和3.3V的MCU通信,开漏输出配上拉到5V,完美解决。
- 线与逻辑:多个开漏输出可以接在一起,任何一个拉低,总线就是低电平。I²C就是这么干的。
3.1.3 上拉/下拉输入——让引脚不再“飘忽不定”
输入模式下,引脚如果什么都不接,电平是不确定的——这叫“浮空”。你读到的值可能是0,也可能是1,完全看心情。上拉输入就是在内部接一个电阻到VDD,让引脚默认是高电平;下拉输入则默认是低电平。
我习惯的做法:按键按下为低电平,就用上拉输入;按下为高电平,就用下拉输入。这样外部电路最简单,省两个电阻。
3.2 STM32 GPIO寄存器配置
STM32的GPIO,说到底就是操作几个寄存器。每个GPIO端口(如GPIOA、GPIOB)都有一组寄存器,咱们重点掌握这四个:
| 寄存器 | 全称 | 作用 | 位数 |
|---|---|---|---|
| MODER | 端口模式寄存器 | 配置输入/输出/复用/模拟 | 32位,每2位控制1个引脚 |
| OTYPER | 输出类型寄存器 | 配置推挽或开漏 | 32位,每1位控制1个引脚 |
| PUPDR | 上拉/下拉寄存器 | 配置上拉/下拉/无 | 32位,每2位控制1个引脚 |
| ODR | 输出数据寄存器 | 写输出电平 | 32位,每1位控制1个引脚 |
直接操作寄存器,其实比用HAL库更直观。你想想看,HAL_GPIO_WritePin()底层不也是改ODR寄存器嘛。我个人的习惯是:学习阶段一定要亲手写寄存器,这样才能真正理解硬件是怎么工作的。
3.2.1 寄存器配置实战:点亮LED
假设LED接在PA5上,低电平点亮。咱们一步步来配置:
- 开启GPIOA时钟:RCC->AHB1ENR |= (1 << 0);
- 设置PA5为输出模式:GPIOA->MODER |= (1 << 10); // MODER[11:10] = 01
- 设置输出类型为推挽:GPIOA->OTYPER &= ~(1 << 5); // 默认就是推挽,但显式清零更规范
- 设置无上拉/下拉:GPIOA->PUPDR &= ~(3 << 10); // PUPDR[11:10] = 00
- 输出低电平点亮LED:GPIOA->ODR &= ~(1 << 5);
// 完整代码示例:寄存器方式点亮PA5上的LED
#include "stm32f4xx.h"
void LED_Init(void)
{
// 1. 开启GPIOA时钟
RCC->AHB1ENR |= (1 << 0);
// 2. 配置PA5为输出模式 (MODER[11:10] = 01)
GPIOA->MODER |= (1 << 10);
GPIOA->MODER &= ~(1 << 11);
// 3. 推挽输出 (OTYPER[5] = 0)
GPIOA->OTYPER &= ~(1 << 5);
// 4. 无上拉/下拉 (PUPDR[11:10] = 00)
GPIOA->PUPDR &= ~(3 << 10);
// 5. 初始输出高电平,LED熄灭
GPIOA->ODR |= (1 << 5);
}
void LED_On(void)
{
GPIOA->ODR &= ~(1 << 5); // 低电平点亮
}
void LED_Off(void)
{
GPIOA->ODR |= (1 << 5); // 高电平熄灭
}
void LED_Toggle(void)
{
GPIOA->ODR ^= (1 << 5); // 异或翻转
}
3.3 实现LED闪烁——从理论到实践
有了上面的基础,LED闪烁就很简单了。无非就是:点亮→延时→熄灭→延时→循环。延时可以用简单的循环等待,也可以用定时器(后面章节会讲)。
// LED闪烁主程序
#include "stm32f4xx.h"
// 简单延时函数
void Delay(uint32_t count)
{
while(count--)
{
// 空循环,消耗时间
__NOP();
}
}
int main(void)
{
LED_Init(); // 初始化LED引脚
while(1)
{
LED_On(); // 点亮
Delay(500000); // 延时约500ms(实际需根据主频调整)
LED_Off(); // 熄灭
Delay(500000); // 延时约500ms
}
}
你可能会问:这个延时准不准?说实话,不准。循环延时的精度取决于主频、编译器优化等级,甚至温度。但做LED闪烁演示足够了。真正做项目,我会用定时器或者SysTick来精确延时。
3.4 本章知识体系
下面这张图,把GPIO的核心知识点串起来了。我建议你把它记在脑子里,以后配置任何引脚,都按这个流程走:
3.5 避坑总结
最后,我把这些年踩过的坑总结一下,你写代码时多留个心眼:
- 时钟没开:配置GPIO前一定要先开时钟。我见过太多人上来就配寄存器,结果怎么改都没反应——时钟没开,寄存器写不进去。
- 模式选错:输出引脚选了输入模式,或者输入引脚选了输出模式。读数据手册时多看一眼默认模式。
- 位操作失误:用等号赋值覆盖了其他引脚。养成“读-改-写”的习惯,或者用位带操作(后面会讲)。
- 上拉电阻忘接:开漏输出不接上拉,高电平浮空。输入引脚不配上拉/下拉,电平不稳定。