GPIO — 流水灯
固件库: GD32F30x_Firmware_Library V3.0.3 芯片: GD32F307 (GD32307C-EVAL-V1.1 开发板) 源文件:
GD32F30x_Firmware_Library_V3.0.3/Examples/GPIO/Running_led/main.c
功能简介
这是嵌入式世界的"Hello World"。
程序让开发板上的 4 颗 LED(LED2、LED3、LED4、LED5)依次点亮、依次熄灭,形成一个循环往复的"跑马灯"效果:LED2 亮 → 1 秒后 LED3 亮同时 LED2 灭 → 1 秒后 LED4 亮同时 LED3 灭 → …… 一直循环。
别看它简单,这个例子把每一个 GD32 项目都必须做的三件事都演示完了:
- 配置系统节拍定时器(SysTick),这样才有能力"延时 1 秒";
- 配置 GPIO 为推挽输出,这样引脚才有能力驱动 LED;
- 在主循环里改变引脚电平,控制外部器件。
只要你彻底看懂这一篇,后面所有外设的学习都会顺很多,因为套路完全一样。
硬件准备
| 器材 | 说明 |
|---|---|
| GD32307C-EVAL-V1.1 开发板 | 或任意 GD32F30x 系列最小系统板 |
| GD-Link / J-Link / ST-Link | 用来下载程序 |
| USB 线 | 供电 |
开发板上 LED 的实际接线(这份信息来自官方 readme,非常重要):
| LED 名称 | 引脚 |
|---|---|
| LED2 | PC0 |
| LED3 | PC2 |
| LED4 | PE0 |
| LED5 | PE1 |
不需要任何额外接线 —— LED 已经焊在板子上了,插上 USB 下载就能看效果。
如果你用的是自己画的板子或者别的最小系统板,LED 大概率不在这些脚上。别慌,往下看"常见问题",里面有改成任意引脚的完整写法。
完整代码
|
|
代码总共只有 20 多行有效语句,但每一行都有讲究。下面拆开细讲。
代码分段讲解
第 1 段:三个头文件分别管什么
|
|
新手常问:“为什么要包含这么多头文件,能不能只留一个?”
| 头文件 | 提供了什么 | 能不能删 |
|---|---|---|
gd32f30x.h |
芯片所有寄存器地址、所有外设库函数声明(gpio_init、rcu_periph_clock_enable…) |
绝对不能删,这是根 |
gd32f307c_eval.h |
板级封装:LED2 这个枚举值、gd_eval_led_init/on/off 函数 |
可以不用,但你得自己写 GPIO 代码 |
systick.h |
systick_config() 和 delay_1ms() |
不用延时的话可以删 |
关键理解:gd32f307c_eval.c/h 不是芯片固件库的一部分,而是官方为这块特定开发板写的适配层。它把"LED2 在 PC0 上"这种板子专有的信息封装起来。所以如果你换了板子,只要改这个文件里的引脚定义,main.c 一行都不用动 —— 这就是分层设计的好处。
第 2 段:systick_config() 到底做了什么
|
|
SysTick 是 Cortex-M 内核自带的一个 24 位递减计数器。systick_config() 的典型实现是这样的:
|
|
配好之后,SysTick 每毫秒触发一次中断,中断服务函数里给一个全局计数变量减 1,delay_1ms(n) 就是"把计数变量设成 n,然后死等它变成 0"。
为什么不用
for(i=0;i<1000000;i++);这种空循环延时? 因为空循环的实际耗时取决于编译器优化等级、主频、Flash 等待周期,换个编译选项延时就变了,完全不可控。用 SysTick 是硬件计时,准确、可移植。这是专业写法和业余写法的分水岭。
第 3 段:gd_eval_led_init() 里面藏着什么
|
|
这一句看起来简单,但它是本例最值得深挖的地方。这个函数在 gd32f307c_eval.c 里的实现,展开后逻辑等价于:
|
|
如果不用 BSP,自己手写点亮 PC0 上的 LED,完整代码是这样:
|
|
这三行就是"GPIO 输出三部曲",请务必背下来。GD32 上任何一个外设的配置,都逃不出"开时钟 → 配参数 → 用起来"这个模式。
第 4 段:为什么要"开一个、关一个"
|
|
注意作者的写法:点亮当前灯的同时,熄灭上一颗灯。
- 如果只写
on不写off,效果就变成"灯一颗一颗累加点亮",最后 4 颗全亮 —— 那是"跑马灯累积效果",不是流水灯。 - 第一轮循环刚进来时 LED5 本来就是灭的,再
off一次也无害,所以代码不需要特殊处理首轮。这种"多余但无害"的写法让循环体保持了完美的对称,可读性更好。
gd_eval_led_on() 内部长这样:
|
|
这里用的是 GD32 的 BOP / BC 位操作寄存器,它的好处是:一次写入只影响你指定的那一位,不需要"读出整个端口 → 改一位 → 写回"。这个操作是原子的,中断打断也不会出错。所以在实际项目中,推荐用 gpio_bit_set/reset(内部就是 BOP/BC),而不是直接操作 GPIO_OCTL 整个输出寄存器。
第 5 段:while(1) 不能省
|
|
嵌入式的 main() 函数永远不能返回。如果 while(1) 写漏了,main 执行完会跳到启动文件里的一个死循环(或者更糟,跳到不可预知的地址导致 HardFault)。所有裸机程序的骨架都是:
|
|
实验现象
- 用 Keil / GCC 编译工程,通过 GD-Link 下载到板子。
- 上电或按下复位键。
- 观察板上 4 颗 LED:
|
|
肉眼看到的效果就是一个光点在 4 颗 LED 之间来回"跑",每秒走一格,循环不停。
如果想让它跑得更快,把 delay_1ms(1000) 改成 delay_1ms(200),效果会明显活泼很多,也更接近常见的流水灯灯带。
常见问题
Q1:下载成功了,但一颗灯都不亮
按这个顺序排查,90% 的问题在前两条:
- 引脚对不上。你的板子 LED 可能不在 PC0/PC2/PE0/PE1。用万用表量一下,或者查板子原理图,然后改
gd32f307c_eval.h里的引脚宏。 - LED 是低电平点亮的。很多板子的接法是
3.3V → 电阻 → LED → MCU 引脚,这时候引脚输出低电平才亮,输出高电平反而灭。解决办法是把gd_eval_led_on和off的行为互换,或者直接自己写:
|
|
- 忘了开 GPIO 时钟。如果你没用 BSP 而是自己写的,检查有没有
rcu_periph_clock_enable(RCU_GPIOx)。 - 芯片没跑起来。用调试器单步,看是否卡在
HardFault_Handler或SystemInit里。
Q2:灯亮了,但闪得飞快 / 完全不闪
这是 delay_1ms() 不准的典型症状,根因通常是 SysTick 配置和实际主频不匹配。
检查 system_gd32f30x.c 里的时钟配置宏,确认你选的主频(比如 __SYSTEM_CLOCK_120M_PLL_HXTAL)和板子上实际的晶振频率(GD32307C-EVAL 是 8MHz 或 25MHz)匹配。如果外部晶振起振失败,芯片会自动降频跑内部 8MHz RC,延时就会差十几倍。
在 main() 开头加一句打印验证:
|
|
Q3:PE0、PE1 在小封装芯片上找不到
GD32F303CBT6 这类 48 脚 LQFP 封装没有 GPIOE。这时候把 LED4/LED5 改到别的空闲引脚(比如 PB0、PB1),或者干脆只做 2 颗灯的流水灯。
Q4:能不能用 for 循环把重复代码简化掉?
当然可以,这也是更工程化的写法:
|
|
代码从 20 行缩到 10 行,而且想加到 8 颗灯只需改数组和常量。官方例程为了让新手一眼看懂流程才写成展开形式,实际项目里推荐用数组 + 循环。
Q5:delay_1ms() 期间 CPU 在干什么?
在死等,什么正事都没干(这叫"阻塞式延时")。跑流水灯没问题,但如果你还要同时读按键、发串口,CPU 全被延时占住就麻烦了。
进阶做法是用 SysTick 中断 + 状态机做"非阻塞延时":
|
|
这个思路是从"点灯玩具"迈向"实际产品"的关键一步,建议学完 SysTick 中断示例后回来自己改一遍。
小结
| 知识点 | 要记住的核心 |
|---|---|
| GPIO 输出三部曲 | rcu_periph_clock_enable() → gpio_init() → gpio_bit_set/reset() |
| 时钟必须先开 | GD32 外设默认时钟关闭,不开时钟配置全部无效 |
| 推挽输出 | 点灯、驱动开关量用 GPIO_MODE_OUT_PP |
| 位操作寄存器 | gpio_bit_set/reset 是原子操作,比读改写整个端口安全 |
| SysTick 延时 | systick_config() + delay_1ms(),比空循环准确且可移植 |
| BSP 分层 | 板子相关信息(哪个脚接哪颗灯)应该封装在 xxx_eval.c 里,不要写死在 main |
while(1) |
裸机 main 永不返回 |
核心心法:GD32 的每一个外设,配置流程都是"开时钟 → 配 GPIO → 配外设参数 → 使能外设 → 读写数据"。流水灯只用到了前三步,但这个骨架会贯穿你后面学的 USART、ADC、SPI、TIMER 全部内容。
拓展应用
学会 GPIO 输出,下面这些项目你现在就能做:
入门级(改改本例代码就行)
- 呼吸灯(软件版):不用 PWM,纯靠"亮 1ms 灭 9ms"快速切换 + 逐渐改变比例,实现渐亮渐暗。理解这个再去学 PWM 会豁然开期。
- 摩尔斯电码发报机:把字符串编码成长短闪烁,用 LED 发 “SOS”。练习数组遍历 + 延时控制。
- 交通灯模型:3 颗 LED 当红黄绿,按真实时序(绿 30s → 黄 3s → 红 30s)循环,加上数字倒计时就是完整的课程设计。
- 蜂鸣器报警器:把 LED 换成有源蜂鸣器(引脚经三极管驱动),改变间隔就是不同的报警音效。
进阶级(需要再学一点别的外设)
- 8 位 LED 灯带跑马灯:8 颗 LED 接一整组 GPIO,用
gpio_port_write()一次写 8 位,通过移位运算1 << i实现流水,代码优雅很多。 - 74HC595 级联驱动:只用 3 根 GPIO(DATA / CLOCK / LATCH)驱动 8、16、32 颗 LED。这是学习"用软件模拟时序协议"的最佳入门项目,也是理解 SPI 的前置知识。
- 数码管动态扫描显示:4 位共阴数码管,GPIO 快速轮流点亮每一位(每位亮 2ms),利用视觉暂留让 4 位看起来同时亮。这是 GPIO 输出 + 精确定时的经典综合应用。
- 步进电机四相八拍驱动:4 根 GPIO 按特定相序通电(A→AB→B→BC→C→CD→D→DA),电机就转起来了。改变相序间隔就是调速,改变顺序就是反转。
- 继电器控制 + 定时开关:GPIO 经光耦驱动继电器,配合 RTC 做"每天早上 7 点自动开灯"的智能插座。
实战产品级
- LED 点阵屏 / WS2812 灯带:WS2812 是单总线协议,靠精确的高低电平宽度(0.35μs / 0.9μs)编码数据。用 GPIO + 精确延时(或 PWM+DMA)就能驱动任意长度的全彩灯带,做氛围灯、电竞灯效。
- 工业开关量采集卡:多路 GPIO 输入读传感器状态 + 多路 GPIO 输出驱动执行机构,加上 Modbus 串口通信,这就是一个最小的 PLC 模块。
下一步该学什么:看完流水灯,建议按 按键轮询(GPIO 输入) → USART Printf(学会打印调试) → TIMER PWM(真正的呼吸灯) 的顺序继续。特别是 Printf,它让你能"看见"程序内部在想什么,是效率提升最大的一个技能。