STM32跑马灯实现和部分源码分析
·
一、代码实现总结
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 寄存器:
- 遍历
Pin的每一位,找到要配置的引脚。 - 根据
Mode计算config:- 推挽输出:
config = Speed + GPIO_CR_CNF_GP_OUTPUT_PP - 开漏输出:
config = Speed + GPIO_CR_CNF_GP_OUTPUT_OD - 输入模式:再根据上拉/下拉配置。
- 推挽输出:
- 根据引脚号选择寄存器:
- 引脚 0~7 →
CRL - 引脚 8~15 →
CRH
- 引脚 0~7 →
- 计算偏移:
registeroffset = (position < 8) ? (position << 2) : ((position - 8) << 2); - 用
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 位分别对应“置位”和“复位”,使用方便。
三、整体流程串起来
- STM32CubeMX 图形化配置 GPIOF 的 PF8~PF11 为开漏输出、低速、无上下拉。
- 生成代码后,
MX_GPIO_Init()调用HAL_GPIO_Init()配置CRL/CRH。 main()中通过HAL_GPIO_WritePin()写BSRR,实现引脚电平变化。- 跑马灯就是循环调用
HAL_GPIO_WritePin(),配合HAL_Delay(200)实现轮流点亮。
一句话:CubeMX 配置最终变成对 CRL/CRH 的初始化,HAL_GPIO_WritePin 最终变成对 BSRR 的原子写操作。
更多推荐


所有评论(0)