嵌入式STM32 GPIO输出与LED控制
STM32 GPIO输出与LED控制知识总结
一、GPIO寄存器概述
1.1 什么是GPIO寄存器
GPIO(General Purpose Input Output)模块包含多个寄存器,通过配置这些寄存器,可以控制GPIO引脚的工作模式、电平状态等。类比来说,就像给洗衣机设置参数,设定好后设备就会按照预设方式工作。
1.2 核心寄存器清单(STM32F1)
| 寄存器 | 偏移地址 | 复位值 | 功能描述 |
|---|---|---|---|
| GPIOx_CRL(端口配置低) | 0x00 | 0x44444444 | 配置引脚0~7的模式和速度 |
| GPIOx_CRH(端口配置高) | 0x04 | 0x44444444 | 配置引脚8~15的模式和速度 |
| GPIOx_IDR(输入数据) | 0x08 | 0x0000XXXX | 读取引脚电平状态 |
| GPIOx_ODR(输出数据) | 0x0C | 0x00000000 | 控制引脚输出高低电平 |
| GPIOx_BSRR(位设置/清除) | 0x10 | 0x00000000 | 原子操作设置/清除特定引脚 |
| GPIOx_BRR(位清除) | 0x14 | 0x00000000 | 清除特定引脚 |
| GPIOx_LCKR(配置锁) | 0x18 | 0x00000000 | 锁定引脚配置 |
1.3 统一编址与寄存器映射
STM32采用统一编址方式,即寄存器和内存共用同一地址空间。访问GPIO寄存器本质就是通过物理地址直接读写内存。
// GPIO寄存器结构体定义(HAL库)
typedef struct {
__IO uint32_t CRL; // 端口配置低寄存器
__IO uint32_t CRH; // 端口配置高寄存器
__IO uint32_t IDR; // 输入数据寄存器
__IO uint32_t ODR; // 输出数据寄存器
__IO uint32_t BSRR; // 位设置/清除寄存器
__IO uint32_t BRR; // 位清除寄存器
__IO uint32_t LCKR; // 配置锁寄存器
} GPIO_TypeDef;
// 基地址定义示例
#define PERIPH_BASE 0x40000000UL
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000UL)
#define GPIOF_BASE (APB2PERIPH_BASE + 0x00001C00UL)
#define GPIOF ((GPIO_TypeDef *)GPIOF_BASE)
二、GPIO输出模式
2.1 推挽输出 vs 开漏输出
| 模式 | 输出0 | 输出1 | 应用场景 |
|---|---|---|---|
| 推挽输出 | NMOS导通,PMOS截止(接GND) | PMOS导通,NMOS截止(接VDD) | 普通数字信号输出 |
| 开漏输出 | NMOS导通(接GND) | 高阻态,需外部上拉电阻 | I2C总线、多设备共享线路 |
2.2 为什么需要配置输出速度?
虽然GPIO输出只有0V和3.3V两种状态,但电平变化不是瞬间完成的,而是有上升/下降时间的斜坡。
- 速度过快可能导致信号变成"尖峰"形状,接收端无法正确识别
- 设置速度等级是为了在正确输出信息的前提下,适应不同应用场景
- STM32F1可选速度:2MHz、10MHz、50MHz
三、STM32CubeMX配置要点
3.1 调试接口配置( 非常重要)
在CubeMX中必须将调试接口配置为 Serial Wire(SWD)。
如果使用默认的No Debug,第一次烧录可以成功,但后续烧录会失败! 原因是程序运行后会禁用调试接口。
DAP vs SWD 类比:
- DAPLink(调试器硬件)= 考试时坐在座位上的同学本人
- SWD(通信协议)= 传递纸条的通道 + 约定好的暗号
3.2 GPIO引脚配置
CubeMX中GPIO输出配置项:
| 配置项 | 说明 |
|---|---|
| GPIO output level | 引脚初始电平(输出寄存器的初始值) |
| GPIO mode | 推挽输出(Push Pull)或开漏输出(Open Drain) |
| GPIO Pull-up/Pull-down | 是否启用内部上拉/下拉电阻 |
| Maximum output speed | 输出速度等级(LOW/MEDIUM/HIGH) |
| User Label | 用户自定义标签,便于识别引脚功能 |
3.3 时钟配置(了解)
- 开发板使用 8MHz HSE晶振
- 通过PLL可将系统时钟配置到 72MHz(STM32F1最大频率)
- 需在CubeMX中配置时钟树并选通外部晶振
四、项目目录结构
STM32CubeMX生成的工程目录:
Project/
├── Application/
│ ├── MDK-ARM/
│ │ └── startup_stm32f103xe.s # 启动文件(汇编)
│ └── User/Core/
│ ├── main.c # 主函数入口
│ ├── stm32f1xx_it.c # 中断处理
│ └── stm32f1xx_hal_msp.c # 硬件层初始化
├── Drivers/
│ ├── STM32F1xx_HAL_Driver/ # HAL库源码
│ └── CMSIS/ # CMSIS核心文件
└── .mxproject # CubeMX项目配置
注意事项:
- 编写代码必须放在
/* USER CODE BEGIN ... */和/* USER CODE END ... */之间- 否则重新生成代码时,外部代码会被覆盖
- CubeMX会根据配置的外设自动拷贝对应的HAL库文件,未使用的不会参与编译
五、代码实现
5.1 跑马灯核心代码
/* USER CODE BEGIN PV */
uint16_t leds[] = {GPIO_PIN_8, GPIO_PIN_9, GPIO_PIN_10, GPIO_PIN_11};
#define LED_NUM (sizeof(leds) / sizeof(leds[0]))
/* USER CODE END PV */
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
// 先熄灭所有LED(开漏模式,SET为熄灭)
for(int i = 0; i < LED_NUM; i++) {
HAL_GPIO_WritePin(GPIOF, leds[i], GPIO_PIN_SET);
}
/* USER CODE END 2 */
while (1) {
/* USER CODE BEGIN 3 */
// 从左向右跑
for(int i = 0; i < LED_NUM; i++) {
HAL_GPIO_WritePin(GPIOF, leds[i], GPIO_PIN_RESET); // 点亮
HAL_Delay(200); // 等待200ms
HAL_GPIO_WritePin(GPIOF, leds[i], GPIO_PIN_SET); // 熄灭
}
// 从右向左跑
for(int i = LED_NUM - 1; i >= 0; i--) {
HAL_GPIO_WritePin(GPIOF, leds[i], GPIO_PIN_RESET);
HAL_Delay(200);
HAL_GPIO_WritePin(GPIOF, leds[i], GPIO_PIN_SET);
}
/* USER CODE END 3 */
}
}
5.2 引脚编号的本质
引脚宏定义本质是只有一个比特位为1的掩码:
#define GPIO_PIN_8 ((uint16_t)0x0100) // 第8位为1
#define GPIO_PIN_9 ((uint16_t)0x0200) // 第9位为1
#define GPIO_PIN_10 ((uint16_t)0x0400) // 第10位为1
#define GPIO_PIN_11 ((uint16_t)0x0800) // 第11位为1
多个引脚可以用 按位或(|) 组合传递:
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8 | GPIO_PIN_9, GPIO_PIN_RESET);
六、HAL库核心源码分析
6.1 GPIO_Init函数核心流程
HAL_GPIO_Init() 的核心工作就是设置CRL/CRH控制寄存器:
// 伪代码简化流程
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)
{
// 1. 根据引脚号确定使用CRL(0~7)还是CRH(8~15)
configregister = (iocurrent < GPIO_PIN_8) ? &GPIOx->CRL : &GPIOx->CRH;
// 2. 计算配置位的偏移
registeroffset = (iocurrent < GPIO_PIN_8) ? (position << 2) : ((position - 8) << 2);
// 3. 根据Mode构建配置值(CNF + MODE)
switch(GPIO_Init->Mode) {
case GPIO_MODE_OUTPUT_PP:
config = Speed + GPIO_CR_CNF_GP_OUTPUT_PP; // 推挽
break;
case GPIO_MODE_OUTPUT_OD:
config = Speed + GPIO_CR_CNF_GP_OUTPUT_OD; // 开漏
break;
// ...
}
// 4. 写入寄存器
MODIFY_REG(*configregister, (掩码) << registeroffset, config << registeroffset);
}
6.2 为什么不用ODR,而用BSRR?
| 操作方式 | 代码 | 安全性 |
|---|---|---|
| 直接修改ODR | `GPIOx->ODR | = GPIO_PIN_8` |
| 使用BSRR | GPIOx->BSRR = GPIO_PIN_8 |
原子操作,只影响指定位 |
💡 核心原因:BSRR允许原子操作(单条指令完成),而ODR需要"读-改-写"三步,在中断场景下可能导致竞态条件。
6.3 写引脚函数实现
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
{
if (PinState != GPIO_PIN_RESET) {
GPIOx->BSRR = GPIO_Pin; // 置1(SET)
} else {
GPIOx->BRR = GPIO_Pin; // 清0(RESET)
// 或 GPIOx->BSRR = GPIO_Pin << 16;
}
}
BSRR寄存器位映射:
- 低16位(0~15):置1
- 高16位(16~31):清0
七、VSCode + Keil协同开发
7.1 为什么需要VSCode?
| 功能 | Keil | VSCode + 插件 |
|---|---|---|
| 代码编辑 | 较弱 | 强大(语法高亮、补全、跳转) |
| 编译 | 原生 | 需配置Keil Assistant |
| 烧录 | 原生 | 需配置Keil Assistant |
| 调试 | 原生 | 仍需Keil |
Visual Studio Code ≠ Visual Studio,两者是完全不同的软件。
7.2 推荐VSCode插件
| 插件 | 功能 |
|---|---|
| Chinese (Simplified) | 中文界面汉化 |
| C/C++ (微软官方) | 语法高亮、代码补全、跳转、重命名 |
| Keil Assistant | 调用Keil MDK进行编译 & 烧录 |
7.3 Keil Assistant配置
- 安装Keil Assistant插件
- 配置Keil MDK路径:
UV4.exe所在目录 - 首次编译可能需要安装 .NET Framework 3.5
八、关键概念补充
8.1 RCC(复位和时钟控制)
- RCC = Reset and Clock Control
- 复位:让设备从头开始执行程序(相当于"重启")
- 时钟:一组周期性的高低电平信号
🎵 类比:STM32就像交响乐团,时钟就是指挥。指挥按节奏挥舞指挥棒,各个声部整齐划一地演奏,整个乐队才能协调工作。
8.2 原理图网络标签
- 网络标签是原理图上的符号,不是真实元件
- 相同名称的网络标签在电气上是连通的
- 方便将分模块绘制的原理图表达物理连接关系
8.3 常见电子元件
| 元件 | 特性 |
|---|---|
| 电阻 | 限制电流,分压 |
| 发光二极管(LED) | 单向导通,电流通过时发光;长脚为正极(阳极) |
九、常见问题
Q1:为什么第一次烧录成功,后续失败?
A:CubeMX中调试接口默认设为"No Debug",程序运行后会禁用SWD接口。必须在CubeMX中配置为"Serial Wire"。
Q2:重新生成代码后,自己写的代码不见了?
A:必须将代码写在 /* USER CODE BEGIN */ 和 /* USER CODE END */ 之间,否则会被覆盖。
Q3:开漏模式下为什么需要外部上拉电阻?
A:开漏输出高电平时为高阻态,无法提供驱动电流,必须通过外部上拉电阻拉到高电平。
更多推荐

所有评论(0)