2. STM32 GPIO基础与寄存器操作:从理论到点灯实战
各位同学,欢迎来到第二章。上一章我们搭好了开发环境,这一章咱们直接上手硬件操作——GPIO。说白了,GPIO就是单片机跟外界打交道的最基本通道。你想想看,一个单片机要控制LED、读取按键、驱动传感器,哪个离得开GPIO?
我个人习惯,学任何外设都先从寄存器入手。为什么?因为只有理解了寄存器,你才能真正明白芯片在干什么。HAL库固然方便,但出了问题你连debug都不知道从哪下手。好,咱们开始。
2.1 GPIO是什么?
GPIO全称General Purpose Input Output,通用输入输出口。每个GPIO引脚可以独立配置为输入或输出。STM32的GPIO比51单片机复杂得多,但功能也强大得多。
STM32F103系列每个GPIO端口有16个引脚(PA0-PA15、PB0-PB15等),每个引脚可以配置为多种模式。我刚开始用STM32时,看到那么多模式头都大了,后来发现其实就两大类:输入模式和输出模式。
| 模式类别 | 具体模式 | 典型用途 |
|---|---|---|
| 输入模式 | 浮空输入、上拉输入、下拉输入、模拟输入 | 读取按键、ADC采样 |
| 输出模式 | 推挽输出、开漏输出、复用推挽、复用开漏 | 驱动LED、控制外设 |
2.2 推挽输出与开漏输出的区别
这两个概念很多初学者搞混。我当年也踩过坑,有一次用开漏输出驱动LED,死活不亮,查了半天才发现是模式配错了。
推挽输出(Push-Pull):内部有两个MOS管,一个负责推(输出高电平),一个负责挽(输出低电平)。输出高电平时,上管导通,引脚直接接到VCC;输出低电平时,下管导通,引脚直接接到GND。说白了,推挽输出能主动输出高低电平,驱动能力强。
开漏输出(Open-Drain):只有下管,没有上管。输出低电平时,下管导通,引脚接地;输出高电平时,下管截止,引脚处于高阻态。所以开漏输出不能主动输出高电平,必须外接上拉电阻。
核心区别一句话总结:推挽输出能主动输出高低电平;开漏输出只能主动输出低电平,高电平靠外部上拉。
| 对比项 | 推挽输出 | 开漏输出 |
|---|---|---|
| 输出高电平 | 直接输出VCC | 需外接上拉电阻 |
| 输出低电平 | 直接输出GND | 直接输出GND |
| 驱动能力 | 强 | 弱(取决于上拉电阻) |
| 多引脚线与 | 不支持 | 支持 |
| 典型应用 | 驱动LED、普通IO | I2C总线、电平转换 |
我的经验:驱动LED用推挽输出就够了。开漏输出主要用于I2C这类需要线与功能的总线,或者不同电压域之间的电平转换。
2.3 GPIO寄存器详解
STM32每个GPIO端口有7个寄存器,但常用的就4个。咱们以点亮LED为例,只需要用到CRL/CRH(配置模式)和ODR(输出数据)。
| 寄存器 | 名称 | 作用 |
|---|---|---|
| CRL | 端口配置低寄存器 | 配置引脚0-7的模式 |
| CRH | 端口配置高寄存器 | 配置引脚8-15的模式 |
| IDR | 端口输入数据寄存器 | 读取引脚电平 |
| ODR | 端口输出数据寄存器 | 设置引脚输出电平 |
| BSRR | 端口位设置/清除寄存器 | 原子操作设置/清除位 |
| BRR | 端口位清除寄存器 | 原子操作清除位 |
| LCKR | 端口配置锁定寄存器 | 锁定引脚配置 |
每个引脚在CRL或CRH中占用4位,其中CNF[1:0]配置模式,MODE[1:0]配置速度。具体配置如下:
| MODE[1:0] | 模式 | 说明 |
|---|---|---|
| 00 | 输入模式 | 复位后的默认状态 |
| 01 | 输出模式,10MHz | 低速 |
| 10 | 输出模式,2MHz | 中速 |
| 11 | 输出模式,50MHz | 高速 |
| CNF[1:0](输出模式) | 配置 |
|---|---|
| 00 | 通用推挽输出 |
| 01 | 通用开漏输出 |
| 10 | 复用推挽输出 |
| 11 | 复用开漏输出 |
2.4 实战:使用寄存器点亮LED
好,理论讲完了,咱们直接上手。假设LED接在PB0引脚,低电平点亮。为什么用低电平?因为STM32的IO口驱动能力有限,高电平驱动LED电流不够,用低电平灌电流方式更可靠。这是我做第一个项目时学到的教训。
步骤很简单:
- 使能GPIOB时钟
- 配置PB0为推挽输出,50MHz
- 设置PB0输出低电平
先看时钟使能。STM32外设时钟默认是关闭的,需要手动开启。GPIOB挂在APB2总线上,对应的时钟使能位在RCC_APB2ENR寄存器的第3位。
// 使能GPIOB时钟
RCC->APB2ENR |= (1 << 3); // 设置第3位为1
然后配置PB0的模式。PB0属于低8位,用CRL寄存器配置。PB0对应CRL的bit[3:0],其中bit[1:0]是MODE,bit[3:2]是CNF。
// 配置PB0为推挽输出,50MHz
GPIOB->CRL &= ~(0xF << 0); // 先清零bit[3:0]
GPIOB->CRL |= (0x3 << 0); // MODE=11(50MHz), CNF=00(推挽输出)
最后设置ODR寄存器,让PB0输出低电平:
// PB0输出低电平,点亮LED
GPIOB->ODR &= ~(1 << 0); // 清零bit0
完整代码:
#include "stm32f10x.h"
int main(void)
{
// 1. 使能GPIOB时钟
RCC->APB2ENR |= (1 << 3);
// 2. 配置PB0为推挽输出,50MHz
GPIOB->CRL &= ~(0xF << 0);
GPIOB->CRL |= (0x3 << 0);
// 3. PB0输出低电平,点亮LED
GPIOB->ODR &= ~(1 << 0);
while(1)
{
// 程序停在这里,LED一直亮
}
}
注意:操作寄存器时,一定要先清零再赋值。不清零直接赋值会导致配置错误。我曾经因为这个问题,查了整整一个下午的bug。
2.5 使用BSRR寄存器优化
上面用ODR寄存器设置电平有个问题:如果多个线程或中断同时操作ODR,可能会产生读-改-写冲突。BSRR寄存器可以解决这个问题,它支持原子操作。
// 使用BSRR设置PB0输出低电平(原子操作)
GPIOB->BSRR = (1 << (0 + 16)); // BR0位,低16位是设置,高16位是清除
BSRR的低16位用于设置对应引脚为高电平,高16位用于设置对应引脚为低电平。写1有效,写0不影响。这样就不需要先读再写了。
我的习惯:在实际项目中,我一般用BSRR操作输出,用ODR读取当前状态。BSRR的原子特性在RTOS环境下特别有用。
2.6 本章知识体系
下面这张图总结了GPIO操作的核心流程,从时钟使能到寄存器配置,再到输出控制,每一步都离不开对寄存器的理解。
2.7 避坑指南
我曾经踩过的坑:
- 忘记使能时钟:配置了寄存器但没开时钟,引脚死活没反应。这是新手最常犯的错误,没有之一。
- 配置前不清零:直接赋值导致其他引脚配置被覆盖。记住:先清零,再赋值。
- 模式选错:用开漏输出驱动LED,结果LED微亮或不亮。驱动LED用推挽输出。
- 速度配置过高:50MHz模式功耗大、噪声大。驱动LED用10MHz就够了。