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配置

  1. 安装Keil Assistant插件
  2. 配置Keil MDK路径:UV4.exe 所在目录
  3. 首次编译可能需要安装 .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:开漏输出高电平时为高阻态,无法提供驱动电流,必须通过外部上拉电阻拉到高电平。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐