硬件原理
这块板子是 keysking大佬开发的集成开发板,板上载了三个LED,分别连接在以下引脚:
| LED | 引脚 | 控制信号 | 限流电阻 |
|---|---|---|---|
| 🔴 红色 | PB0 | GPIO 高电平亮 | 板载已集成 |
| 🟢 绿色 | PA7 | GPIO 高电平亮 | 板载已集成 |
| 🔵 蓝色 | PA6 | GPIO 高电平亮 | 板载已集成 |
电路原理图是这样的
电路原理很简单:GPIO 引脚输出高电平时,LED 通过限流电阻接地,灯就亮了。
如果你用的是其他开发板,只需要设置或者查询一下LED对应的引脚号,原理是一样的。
CubeMX 引脚配置
新建项目
打开 CubeMX,新建项目,芯片选择 STM32F103C8T6,步骤和上一篇完全一样,不再重复。
配置三个GPIO引脚
这次要用到三个引脚,分别控制三个颜色的LED:
| LED | 引脚 | 功能 |
|---|---|---|
| 🔴 红色 | PB0 | GPIO_Output |
| 🟢 绿色 | PA7 | GPIO_Output |
| 🔵 蓝色 | PA6 | GPIO_Output |
在芯片图上依次配置 PB0 → GPIO_Output、PA7 → GPIO_Output、PA6 → GPIO_Output。
配置完成后,右键各个引脚,给每个引脚起一个User Label(用户标签),这样生成的代码里就不用记 PB0、PA6 这种编号了,直接写 LED_RED、LED_BLUE 就行,可读性高很多。
以 PA6 为例:在芯片界面中找到 PA6,在 ENTER User Label 一栏输入 LED_BLUE。同样,PA7 标为 LED_GREEN,PB0 标为 LED_RED。
其他参数保持默认即可:
- Output mode:Push-Pull(推挽输出)
- Pull:No pull(无上下拉)
- Speed:Low(低速即可,LED不需要高速翻转)
- Initial level:Low(初始低电平,LED默认熄灭)
配置调试接口
这一步非常重要——如果不配,烧录一次后 SWD 就被禁用了,再也烧不进新程序。
在 System Core → SYS 中,将 Debug 设为 Serial Wire。
这一步在上一篇博客里也强调过,如果你的项目是从之前的 CubeMX 项目改过来的,记得检查这一步有没有漏掉。
生成代码
切换到 Project Manager 标签页:
| 设置项 | 值 |
|---|---|
| Project Name | LED_Blink |
| Toolchain / IDE | CMake |
| Toolchain Location | 保持默认 |
点击 Generate Code,生成后用 VS Code 打开项目。
VS Code 中编写核心代码
认识 main.h 中的引脚宏定义
CubeMX 生成的 main.h 里,User Label 会自动转换成宏定义:
1 | // 红色 LED — PB0 |
有了这些宏,后面的代码就不用记引脚号了,直接写 LED_RED_Pin 就行。
状态机思路
我们要实现的效果是:三个LED按照不同的组合依次亮灭,通过红绿蓝三色的搭配混合出绚烂的灯光变化。
这里设计成一个6状态的循环,每个状态持续300ms:
| 状态 | 红色(PB0) | 绿色(PA7) | 蓝色(PA6) | 实际颜色 |
|---|---|---|---|---|
| 0 | 🔴亮 | 🟢灭 | 🔵灭 | 🔴 红色 |
| 1 | 🔴亮 | 🟢亮 | 🔵灭 | 🟡 黄色(红+绿) |
| 2 | 🔴亮 | 🟢亮 | 🔵亮 | ⚪ 白色(红+绿+蓝) |
| 3 | 🔴灭 | 🟢亮 | 🔵亮 | 🟢 青色(绿+蓝) |
| 4 | 🔴灭 | 🟢灭 | 🔵亮 | 🔵 蓝色 |
| 5 | 🔴亮 | 🟢灭 | 🔵亮 | 🟣 紫色(红+蓝) |
每300ms切换到下一个状态,完成一轮(6个状态)后回到状态0,一直循环。
main.c 核心代码
打开 main.c,在 USER CODE BEGIN 2 中定义一个状态变量:
1 | /* USER CODE BEGIN 2 */ |
然后在 while(1) 中编写状态切换逻辑:
1 | /* USER CODE BEGIN WHILE */ |
代码讲解
用到的HAL函数:
| 函数 | 作用 |
|---|---|
HAL_GPIO_WritePin(GPIO端口, 引脚号, 电平) |
设置指定引脚输出高或低电平 |
HAL_Delay(毫秒) |
延时,单位毫秒 |
重点看 HAL_GPIO_WritePin,它的三个参数分别是:
| 参数 | 含义 | 取值 |
|---|---|---|
| GPIO端口 | 引脚属于哪个GPIO组(GPIOA、GPIOB…) | GPIOA、GPIOB 等 |
| 引脚号 | 具体第几个引脚 | GPIO_PIN_0、GPIO_PIN_6、GPIO_PIN_7 等 |
| 电平 | 输出高电平还是低电平 | GPIO_PIN_SET(高电平/亮)或 GPIO_PIN_RESET(低电平/灭) |
所以 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET) 的意思就是:让 GPIOB 组的第 0 号引脚输出高电平——对应到电路上,就是红色 LED 亮了。
但问题来了——每次写代码都要记”红色LED在PB0、蓝色在PA6……”,引脚一多根本记不住。这时就体现出 User Label 的作用了。
回头看在 CubeMX 里配置的那几个标签,生成代码后会变成宏定义:
1 |
这样一来,上面的代码就可以写成:
1 | HAL_GPIO_WritePin(LED_RED_GPIO_Port, LED_RED_Pin, GPIO_PIN_SET); |
不用记引脚号,代码自己就能说明白自己在干什么。对比一下两种写法:
1 | // 不用 User Label — 每次要回想"PB0是什么灯来着?" |
在项目只有一两个LED时差距还不明显,但当你同时操作多个引脚时,User Label 能让代码可读性提升一个档次,再也不用对着引脚表一个个对了。
逻辑分析:
每个LED的亮灭由 state 的值决定。state 每300ms加1,到5后归0,形成循环。
这里每个LED的亮灭条件是用区间判断写的,比如绿色LED在 state >= 1 && state <= 3 时亮。你也可以改成查表法——用一个数组存6种状态,每次读数组取值——逻辑会更清晰,但原理是一样的。
解读 MX_GPIO_Init —— 三个引脚的配置
MX_GPIO_Init() 中配置了三个 GPIO 输出引脚,我们看看代码:
1 | static void MX_GPIO_Init(void) |
和第一篇的区别
相比第一篇博客的单引脚配置,这次有三个关键变化:
| 变化 | 说明 |
|---|---|
| 两个时钟使能 | __HAL_RCC_GPIOA_CLK_ENABLE() + __HAL_RCC_GPIOB_CLK_ENABLE(),因为三个引脚分布在 GPIOA(PA6、PA7)和 GPIOB(PB0)两个端口 |
| User Label 替代硬编码 | 前一篇直接写 GPIOA、GPIO_PIN_7,这一篇写成 LED_RED_GPIO_Port、LED_RED_Pin——CubeMX 把 User Label 翻译成了宏,代码可读性大大提升 |
| 三个引脚重复配置 | 每次配置前重新给 GPIO_InitStruct 赋值,CubeMX 就是按照这个模板批量配置引脚的 |
参数回顾
单个引脚的配置参数和第一篇完全一样,这里汇总一下:
| 参数 | 值 | 解释 |
|---|---|---|
Mode |
GPIO_MODE_OUTPUT_PP |
推挽输出——可以输出高/低电平,适合驱动LED |
Pull |
GPIO_NOPULL |
输出引脚不需要上下拉 |
Speed |
GPIO_SPEED_FREQ_LOW |
LED 不需要高速翻转,低速省电、抗干扰 |
为什么不一次性配置所有引脚?
你可能会想,既然三个引脚参数都一样,为什么 CubeMX 不把它们放在一个结构体里一次性配置?
答案是 HAL_GPIO_Init 的 Pin 成员支持按位或组合,所以其实可以一次配多个:
1 | GPIO_InitStruct.Pin = LED_RED_Pin | LED_GREEN_Pin | LED_BLUE_Pin; |
不过引脚分布在不同的端口(PA6、PA7 在 GPIOA,PB0 在 GPIOB),所以至少要用两次 HAL_GPIO_Init。CubeMX 选择分开配置,逻辑更清晰,也更容易针对某个引脚单独修改参数。
编译 & 烧录
编译和烧录的步骤和上一篇完全一样,不再重复写一遍了:
- VS Code 底部工具栏点击齿轮图标 → 生成
- 连接 ST-Link,点击运行和调试 → 选择 STM32Cube:STM32 Launch STLink GDB Server
- 点击继续按钮,程序开始运行
如果遇到编译或烧录的问题,翻回去看上一篇的排坑部分。
效果展示
烧录成功后,三色 LED 会以 300ms 的间隔循环变化:
1 | 红色亮 → 红+绿亮 → 全亮 → 绿+蓝亮 → 蓝色亮 → 红+蓝亮 → 回到红色亮 ... |
红色、绿色、蓝色以不同的组合交替出现,看起来就像在调色盘上不断调出新的颜色,比单灯闪烁丰富太多了。
进阶思路
这个状态机虽然简单,但稍加扩展就能玩出更多花样:
- 改变速度:把
state++放到定时器中断里,用按键调节速度 - 呼吸效果:把 GPIO 换成 PWM,LED 就能渐亮渐灭
- 更多状态:把6种组合扩展到 2³ = 8 种(全亮/全灭也加进来),或者用随机数控制
- 查表法:用一个二维数组存所有状态,
while(1)里取第state行的值写入引脚,代码更简洁
总结
这次的项目相比上一篇的单灯闪烁,主要多了两个新东西:
- 多引脚配置 —— CubeMX 里配多个 GPIO,用 User Label 区分
- 状态机编程 —— 用一个变量控制多个LED的亮灭组合,比单独控制每个灯要简洁得多



