一、代码实现总结

1. 目标

用 GPIO 输出功能实现:

  • 单灯点亮
  • 单灯闪烁
  • 跑马灯

2. 硬件连接

LED 接在 GPIOF 的 PF8~PF11,采用低电平点亮方式:

  • 输出低电平:NMOS 导通,形成回路,LED 亮。
  • 输出高电平:NMOS 断开,LED 灭。

所以代码里:

  • GPIO_PIN_RESET → 点亮
  • GPIO_PIN_SET → 熄灭

3. 跑马灯核心代码

选择开漏输出模式实现跑马灯:

/* 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 */

初始化后,先手动熄灭所有 LED:

for (int i = 0; i < LED_NUM; i++)
{
    HAL_GPIO_WritePin(GPIOF, leds[i], GPIO_PIN_SET); // 开漏输出高,NMOS断开,灯灭
}

主循环中正向、反向跑马灯:

while (1)
{
    // 正向
    for (int i = 0; i < LED_NUM; i++)
    {
        HAL_GPIO_WritePin(GPIOF, leds[i], GPIO_PIN_RESET); // 点亮
        HAL_Delay(200);
        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);
    }
}

4. 注意事项

  • 用户代码必须写在 USER CODE BEGIN ... 和 USER CODE END ... 之间,否则 STM32CubeMX 重新生成代码时会被覆盖。
  • 开漏、推挽都可以驱动 LED,本案例选择开漏;开漏高电平靠外部上拉,低电平驱动能力强。
  • 输出速度配置为 GPIO_SPEED_FREQ_LOW 即可,跑马灯不需要高速。

二、源码分析总结

1. GPIO 端口和寄存器的本质

GPIOA、GPIOB、GPIOF 等本质是宏,最终展开为指向 GPIO_TypeDef 结构体的指针:

#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)

GPIO_TypeDef 结构体成员对应 GPIO 的各个寄存器:

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;

基地址由 PERIPH_BASE + APB2PERIPH_BASE + 端口偏移 计算得到。通过结构体指针访问成员,就是直接读写对应物理地址的寄存器。

2. CubeMX 配置生成的初始化代码

MX_GPIO_Init() 中:

__HAL_RCC_GPIOF_CLK_ENABLE();

HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11, GPIO_PIN_RESET);

GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;   // 开漏输出
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);

3. 引脚掩码的本质

GPIO_PIN_x 是位掩码,例如:

#define GPIO_PIN_5 ((uint16_t)0x0020)  // 第5位为1
#define GPIO_PIN_8 ((uint16_t)0x0100)  // 第8位为1

第几位为 1,就代表第几号引脚。可以用按位或一次配置多个引脚:

GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11

4. HAL_GPIO_Init 源码逻辑

HAL_GPIO_Init() 的核心是配置 CRL / CRH 寄存器:

  1. 遍历 Pin 的每一位,找到要配置的引脚。
  2. 根据 Mode 计算 config:
    • 推挽输出:config = Speed + GPIO_CR_CNF_GP_OUTPUT_PP
    • 开漏输出:config = Speed + GPIO_CR_CNF_GP_OUTPUT_OD
    • 输入模式:再根据上拉/下拉配置。
  3. 根据引脚号选择寄存器:
    • 引脚 0~7 → CRL
    • 引脚 8~15 → CRH
  4. 计算偏移:
    registeroffset = (position < 8) ? (position << 2) : ((position - 8) << 2);
    
  5. 用 MODIFY_REG 把 config 写入对应 MODEy 和 CNFy 位。

所以,GPIO 初始化本质上就是设置 CRL/CRH 控制寄存器。

5. HAL_GPIO_WritePin 源码逻辑

if (PinState != GPIO_PIN_RESET)
{
    GPIOx->BSRR = GPIO_Pin;                  // 置位,输出高
}
else
{
    GPIOx->BSRR = (uint32_t)GPIO_Pin << 16;  // 复位,输出低
}

它操作的是 BSRR(Bit Set/Reset Register):

  • 低 16 位写 1:对应引脚输出高电平。
  • 高 16 位写 1:对应引脚输出低电平。
  • 写 0:忽略,不影响其他位。

6. 为什么不直接改 ODR?

核心原因:

  • 直接写 ODR 需要读-改-写,不是原子操作,可能被中断打断,影响其他引脚。
  • BSRR 支持原子位操作,写 1 生效、写 0 忽略,可以单独置位或复位目标引脚,不影响其他引脚。
  • 高低 16 位分别对应“置位”和“复位”,使用方便。

三、整体流程串起来

  1. STM32CubeMX 图形化配置 GPIOF 的 PF8~PF11 为开漏输出、低速、无上下拉。
  2. 生成代码后,MX_GPIO_Init() 调用 HAL_GPIO_Init() 配置 CRL/CRH。
  3. main() 中通过 HAL_GPIO_WritePin() 写 BSRR,实现引脚电平变化。
  4. 跑马灯就是循环调用 HAL_GPIO_WritePin(),配合 HAL_Delay(200) 实现轮流点亮。

一句话:CubeMX 配置最终变成对 CRL/CRH 的初始化,HAL_GPIO_WritePin 最终变成对 BSRR 的原子写操作。

Logo

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

更多推荐