2. GPIO基础:从原理到实战点亮LED
各位同学,今天我们来啃一块硬骨头——GPIO。别紧张,其实它没那么神秘。GPIO的全称是General Purpose Input Output,通用输入输出口。说白了,就是单片机用来跟外界打交道的“手脚”。
我刚开始学STM32的时候,觉得GPIO不就是个引脚嘛,能有多复杂?结果第一次做项目就踩了坑——LED死活不亮,查了半天才发现是模式配错了。嗯,从那以后我就老老实实把GPIO原理啃了一遍。
2.1 GPIO工作原理
先看一张整体结构图,让你对GPIO内部有个直观印象。
这张图你看懂了吗?其实GPIO的工作就两件事:往外送电平(输出)和读外部电平(输入)。但这里有个关键——输出驱动电路有多种工作模式,选错了模式,你的电路就不干活。
2.2 推挽输出 vs 开漏输出
这是初学者最容易搞混的地方。我当年面试时就被问过这个问题,答得磕磕巴巴。现在我用最直白的方式给你讲清楚。
| 特性 | 推挽输出 | 开漏输出 |
|---|---|---|
| 输出高电平 | 由内部PMOS管直接驱动,输出VDD | 无法输出高电平,必须外接上拉电阻 |
| 输出低电平 | 由内部NMOS管驱动,输出GND | 由内部NMOS管驱动,输出GND |
| 驱动能力 | 强,可提供较大电流 | 弱,取决于上拉电阻 |
| 电平兼容 | 固定为芯片供电电压 | 可通过上拉电阻匹配不同电压 |
| 典型应用 | 驱动LED、普通数字信号 | I²C总线、多设备共享线 |
我的经验之谈:做普通项目,90%的情况用推挽输出就够了。开漏输出主要用在需要“线与”逻辑的场合,比如I²C总线。我曾经在一个项目中用推挽输出接了I²C,结果两个设备同时拉总线,直接把引脚烧了……从那以后,I²C我必用开漏+上拉。
2.3 GPIO寄存器配置
STM32的GPIO配置,说白了就是往几个寄存器里写值。常用的寄存器就这几个:
| 寄存器 | 全称 | 功能 | 位数 |
|---|---|---|---|
| CRL | 配置寄存器低 | 配置引脚0~7的模式和速度 | 32位 |
| CRH | 配置寄存器高 | 配置引脚8~15的模式和速度 | 32位 |
| ODR | 输出数据寄存器 | 写1输出高电平,写0输出低电平 | 16位 |
| IDR | 输入数据寄存器 | 读取引脚当前电平 | 16位 |
| BSRR | 位设置/复位寄存器 | 原子操作设置或清除ODR位 | 32位 |
每个引脚的模式由CRL或CRH中的4位控制。这4位分为两部分:MODE位(2位)和CNF位(2位)。
| MODE[1:0] | 模式 | CNF[1:0] | 配置 |
|---|---|---|---|
| 00 | 输入模式 | 00 | 模拟输入 |
| 00 | 输入模式 | 01 | 浮空输入 |
| 00 | 输入模式 | 10 | 上拉/下拉输入 |
| 01 | 输出模式(10MHz) | 00 | 推挽输出 |
| 01 | 输出模式(10MHz) | 01 | 开漏输出 |
| 10 | 输出模式(2MHz) | 10 | 复用推挽输出 |
| 11 | 输出模式(50MHz) | 11 | 复用开漏输出 |
小技巧:配置寄存器时,建议先用“读-改-写”的方式,避免影响其他引脚。比如要改PA0的模式,先读CRL,改对应位,再写回去。直接用赋值覆盖会把你之前配好的引脚全搞乱。
2.4 点亮LED灯实战
理论说完了,咱们直接上手。点亮一个LED,是嵌入式工程师的“Hello World”。
硬件连接很简单:LED正极通过一个限流电阻(一般220Ω~1kΩ)接到STM32的某个GPIO引脚,负极接地。我习惯用PA0来做演示。
下面给出两种实现方式:寄存器版和库函数版。寄存器版让你理解底层原理,库函数版让你快速开发。
2.4.1 寄存器版代码
// 点亮PA0上的LED(寄存器版)
// 假设LED正极接PA0,负极通过电阻接地
// 1. 开启GPIOA时钟(RCC_APB2ENR寄存器)
// GPIOA挂在APB2总线上,时钟使能位在第2位
*(volatile uint32_t *)0x40021018 |= (1 << 2);
// 2. 配置PA0为推挽输出,50MHz速度
// CRL寄存器地址:0x40010800
// PA0对应CRL的bit[3:0],MODE=11(50MHz),CNF=00(推挽输出)
*(volatile uint32_t *)0x40010800 &= ~(0x0F << 0); // 先清零
*(volatile uint32_t *)0x40010800 |= (0x03 << 0); // 设置MODE=11,CNF=00
// 3. 设置ODR寄存器,输出高电平点亮LED
// ODR寄存器地址:0x4001080C
// PA0对应bit0,写1输出高电平
*(volatile uint32_t *)0x4001080C |= (1 << 0);
// 延时函数(简单粗暴)
void delay(volatile uint32_t count) {
while(count--);
}
// 主循环:LED闪烁
while(1) {
*(volatile uint32_t *)0x4001080C |= (1 << 0); // 点亮
delay(500000);
*(volatile uint32_t *)0x4001080C &= ~(1 << 0); // 熄灭
delay(500000);
}
注意:寄存器地址因芯片型号不同会有差异。上面用的是STM32F103系列的标准地址。如果你用其他系列,一定要查数据手册确认地址。我曾经在F4系列上直接复制F1的代码,结果时钟没打开,查了半天才发现地址不一样。
2.4.2 库函数版代码
// 点亮PA0上的LED(标准库版)
#include "stm32f10x.h"
void GPIO_Config(void) {
GPIO_InitTypeDef GPIO_InitStructure;
// 1. 开启GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 2. 配置PA0为推挽输出,50MHz
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
int main(void) {
GPIO_Config();
while(1) {
GPIO_SetBits(GPIOA, GPIO_Pin_0); // 点亮
Delay(500000);
GPIO_ResetBits(GPIOA, GPIO_Pin_0); // 熄灭
Delay(500000);
}
}
void Delay(volatile uint32_t count) {
while(count--);
}
我的建议:初学者一定要先写寄存器版,哪怕只是照着抄一遍。为什么?因为只有亲手操作过寄存器,你才能真正理解“配置模式→设置电平”这个流程。库函数虽然方便,但它把底层细节封装掉了,出了问题你都不知道从哪查起。
2.5 常见问题与避坑指南
- LED不亮?先检查GPIO模式是不是配成了输入。我见过有人把模式写成浮空输入,然后对着电路板发呆半小时。
- LED微亮?可能是推挽输出配成了开漏输出,而且没加上拉电阻。开漏输出高电平是靠上拉电阻“拉”上去的,电流很小。
- 引脚烧了?检查限流电阻!STM32的GPIO最大输出电流约20mA,直接接LED(不加电阻)会烧引脚。我刚开始做实验时就烧过一个,冒烟的那种。
- 闪烁频率不对?延时函数的参数是循环次数,不是毫秒。想要精确延时,要用定时器或者SysTick。
好了,GPIO的基础知识就讲到这里。你先把LED点亮,这是你嵌入式生涯的第一步。点亮之后,你可以试试用按键控制LED——那就是下一阶段的事了。