💡 第三章:LED 模块详解(STM32G431RBT6 + HAL 库 · 小白友好版)
✅ 本章完全基于你的开发环境
硬件原理图:

配置CUBEMX环境:


🧩 一、你的开发板长什么样?LED 接在哪?
你用的 STM32G431RBT6 是一颗 64 引脚的芯片,常见于 Nucleo-G431RB 或自研最小系统板。
在很多 G431 开发板上:
- 8 个 LED 通常接在 GPIOC 的高 8 位 → PC8 ~ PC15
- 控制信号(锁存使能) 接在 GPIOD 的第 2 脚 → PD2
- 所有 LED 采用 共阳极接法(阳极接 3.3V,阴极接 GPIO)
🔌 共阳极 = 软件写 0 → 灯亮;写 1 → 灯灭
(这是理解你代码的关键!)
✅ 你已经在 STM32CubeMX 中把:
- PC8~PC15 和 PD2 都配置为 GPIO_Output(推挽输出)
- 系统时钟和 GPIO 时钟已自动开启
所以,你可以在代码里放心操作寄存器,不会出错!
📦 二、你的"LED状态箱":ucLed[8]
uint8_t ucLed[8]; // 全局变量,定义在 led_app.c 或 .h 中
这就像一个 8格的小抽屉,每格放一个数字:
| 抽屉编号 | ucLed[0] |
ucLed[1] |
... | ucLed[7] |
|---|---|---|---|---|
| 控制哪个灯? | LED0 | LED1 | ... | LED7 |
| 值 = 1 | 👉 "我想让这个灯 亮!" | |||
| 值 = 0 | 👉 "这个灯 灭。" |
💡 小白注意:你只管改这个数组,真正的"开关灯"工作,交给下面的函数去做!
⚙️ 三、核心函数 led_disp() ------ "把想法变成灯光"
这是你写的重点函数(原封不动,逐行解释)
void led_disp(uint8_t *ucLed)
{
uint8_t temp = 0x00; // 临时拼图板
static uint8_t temp_old = 0xff; // 记住上次拼的是什么图
// 把 ucLed[0]~ucLed[7] 拼成一个字节
// ucLed[0] 放最高位(bit7),ucLed[7] 放最低位(bit0)
for (int i = 0; i < 8; i++) {
temp |= (ucLed[i] << (7 - i));
}
// 只有当图案变了,才去更新硬件(省电 + 不闪屏!)
if (temp != temp_old) {
GPIOC->ODR = 0x00ff; // ① 先清空 PC8~PC15(全亮)
GPIOC->ODR = ~(temp << 8); // ② 把新图案取反后放到高8位
GPIOD->BSRR = 0x01 << 2; // ③ PD2 输出高电平("准备更新!")
GPIOD->BRR = 0x01 << 2; // ④ PD2 输出低电平("现在更新!")
temp_old = temp; // ⑤ 记住这次的图案
}
}
🌟 用"舞台换景"来理解这 4 步:
想象你是一个舞台导演:
- 舞台上 8 盏灯(LED)正在亮着
- 你想换成新图案,但不能一盏一盏关------观众会看到闪烁!
于是你这样做:
- 后台先准备好新灯控信号 (
temp就是新图案) - 拉下幕布(PD2 拉高 → 锁存器暂停输出)
- 快速切换所有灯的控制线 (写
GPIOC->ODR) - 拉开幕布 (PD2 拉低 → 锁存器输出新状态)→ 观众看到 瞬间切换!
✅ 这就是 PD2 的作用 :它是一个 同步使能信号 ,常用于驱动 74HC573 等锁存器。
❓ 四、小白高频问题解答
Q1:为什么要把 temp 左移 8 位?
因为:
temp是 8 位(0~255)- 你要控制的是 PC8 ~ PC15 (即 GPIOC 的 高 8 位)
- 所以必须
temp << 8,才能对齐到正确位置!
例如:
temp = 0b10000000(只想亮 LED0)temp << 8 = 0b10000000 00000000- 写入
GPIOC->ODR后,PC15 = 1,其他高8位=0
Q2:为什么要 取反(~)?
因为你的电路是 共阳极!
| 软件意图 | ucLed[i] |
temp 中的位 |
实际需要 GPIO 输出 |
|---|---|---|---|
| 灯亮 | 1 | 1 | 0(低电平) |
| 灯灭 | 0 | 0 | 1(高电平) |
所以必须 取反,才能让硬件行为和软件意图一致!
✅ 举例:
temp = 0b10000000→~(temp<<8) = 0b01111111 11111111- → PC15 = 0(亮),PC14~PC8 = 1(灭)✅
Q3:为什么先写 GPIOC->ODR = 0x00ff?
这是为了 保护低 8 位(PC0~PC7)!
GPIOC->ODR控制 全部 16 个引脚- 如果你直接写
~(temp<<8),结果是一个 32 位数(比如0xFFFFxxFF) - 可能会意外把 PC0~PC7 拉低(如果它们正在控制串口、按键等,就出大事了!)
所以:
0x00ff= 低8位=1,高8位=0 → 只清高8位,低8位保持原样- 再写
~(temp<<8)→ 高8位更新,低8位仍是0x00ff的低8位(即不变)✅
▶️ 五、怎么用?三步点亮你的想法!
第 1 步:确保 CubeMX 已配置(你已做完 ✅)
- PC8~PC15 → GPIO_Output, Push-Pull, Low Speed
- PD2 → GPIO_Output, Push-Pull, Low Speed
- 生成代码后,
MX_GPIO_Init()会自动开启时钟
第 2 步:在 main() 中初始化
// main.c
#include "led_app.h"
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init(); // ← CubeMX 生成,千万别删!
// 初始:所有灯灭
for (int i = 0; i < 8; i++) {
ucLed[i] = 0; // 共阳:0 = 灭
}
while (1)
{
// 示例:让 LED0 亮 500ms,灭 500ms
ucLed[0] = 1; // 亮
led_proc(); // 刷新
HAL_Delay(500);
ucLed[0] = 0; // 灭
led_proc();
HAL_Delay(500);
}
}
第 3 步(进阶):加入任务调度器
如果你有调度器,只需:
// 在任务数组中添加
{led_proc, 1, 0} // 每 1ms 检查一次(实际只在状态变化时刷新)
然后在主循环调用 scheduler_run() 即可!
🧠 本章口诀(背下来,不怕忘!)
💡 G431 板子要记牢 :
LED 接在 PC8 到 PC15!
🔌 共阳接法别搞反 :
软件写 0 灯才亮,写 1 就熄灭!
📦 ucLed 是指令箱 :
1=亮,0=灭,改它就生效!
⚡ 刷新四步稳又准 :
清高8 → 取反写 → PD2高 → PD2低!
🛡️ 先清 0x00ff 很重要 :
保住低8位,安全又可靠!
✅ 动手挑战(适合小白)
在 while(1) 里加这段代码:
static int dir = 1;
static int pos = 0;
ucLed[pos] = 1; // 当前位置亮
led_proc();
HAL_Delay(200);
ucLed[pos] = 0; // 熄灭
pos += dir;
if (pos == 7 || pos == 0) dir = -dir; // 到头就回头
你会看到一颗小灯 左右来回跑!🎉