GPIO_Toggle — 点亮第一颗 LED
芯片: PY32F403(Cortex-M4,最高 72MHz)
固件库: PY32F403_Firmware V1.4.5
源文件: PY32F403_Firmware_V1.4.5/Projects/PY32F403-STK/Example/GPIO/GPIO_Toggle/Src/main.c
难度: ★☆☆☆☆ 入门必做
1. 功能简介
这是嵌入式世界的 “Hello World”。
程序做的事情非常简单:把开发板上绿色 LED 对应的那个引脚(PA1)配置成推挽输出,然后在 while(1) 里不停地做两件事——等 250 毫秒、把引脚电平翻个面。
翻转一次是 250ms,翻两次(亮一次+灭一次)才是一个完整周期,也就是 500ms,所以肉眼看到的闪烁频率是:
1
|
f = 1 / (250ms × 2) = 1 / 0.5s = 2 Hz
|
别小看这个程序。它能跑起来,说明你的工程配置、时钟、下载器、启动文件、LED 接线全部是对的。后面所有复杂外设都建立在这个基础上。
2. 硬件准备
| 需要的东西 |
说明 |
| PY32F403-STK 开发板 |
板载绿色 LED 已经接在 PA1 上 |
| 下载器 |
J-Link / DAP-Link,接 SWD(SWDIO / SWCLK / GND / 3V3) |
| USB 线 |
供电 |
接线:不用接任何线。LED 是板载的,直接下载就能看效果。
板上 LED 的宏定义写在 BSP 头文件 py32f403xx_Start_Kit.h 里:
1
2
3
|
#define LED3_PIN GPIO_PIN_1 /* PA1 */
#define LED3_GPIO_PORT GPIOA
#define LED3_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
|
如果你用的是自己画的板子,LED 接在别的脚上,只要改 main.c 顶部那三个宏就行,代码其它地方一个字都不用动——这就是 BSP 抽象的好处。
3. 完整代码(带中文注释)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
|
/* ==========================================================================
* PY32F403 GPIO_Toggle —— LED 以 2Hz 闪烁
* ========================================================================== */
#include "main.h"
/* ---- 把"哪个引脚是 LED"抽出来,换板子只改这三行 ---------------------- */
#define LED_GPIO_PIN LED3_PIN /* PA1 */
#define LED_GPIO_PORT LED3_GPIO_PORT /* GPIOA */
#define LED_GPIO_CLK_ENABLE() LED3_GPIO_CLK_ENABLE()/* AHB2 时钟 */
static void APP_SystemClockConfig(void);
static void APP_GpioConfig(void);
int main(void)
{
/* 第 1 步:HAL 库初始化。
* 内部做了两件关键事:
* ① 配置 Flash 预取/等待周期
* ② 初始化 SysTick 为 1ms 中断 —— HAL_Delay() 就靠它计时
* 不调用它,HAL_Delay() 会死等,程序卡住不动。 */
HAL_Init();
/* 第 2 步:配置系统时钟(本例用内部 HSI 8MHz) */
APP_SystemClockConfig();
/* 第 3 步:把 PA1 配成推挽输出 */
APP_GpioConfig();
while (1)
{
/* 延时 250ms(阻塞式,CPU 在这里空转数 SysTick 节拍) */
HAL_Delay(250);
/* 翻转 PA1 电平:高变低、低变高
* 底层就一句:GPIOA->ODR ^= GPIO_PIN_1; */
HAL_GPIO_TogglePin(LED_GPIO_PORT, LED_GPIO_PIN);
}
}
/**
* @brief GPIO 配置:PA1 → 推挽输出
*/
static void APP_GpioConfig(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0}; /* 一定要清零!残留值会配错模式 */
/* ★ 关键第一步:打开 GPIOA 的时钟。
* PY32F403 的 GPIO 挂在 AHB2 总线上。
* 时钟没开 → 寄存器写进去读出来还是 0 → LED 死活不亮,
* 这是新手最常踩的坑。 */
LED_GPIO_CLK_ENABLE();
GPIO_InitStruct.Pin = LED_GPIO_PIN; /* 要配置的引脚:PA1 */
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出:能拉高也能拉低 */
GPIO_InitStruct.Pull = GPIO_PULLUP; /* 内部上拉(输出模式下影响很小)*/
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; /* 输出翻转速率档位 */
/* 把结构体里的配置真正写进 GPIOA 的 MODER/OTYPER/OSPEEDR/PUPDR 寄存器 */
HAL_GPIO_Init(LED_GPIO_PORT, &GPIO_InitStruct);
}
/**
* @brief 系统时钟配置
* 本例最简单:只开 HSI(内部 8MHz RC),不用 PLL
* 结果:SYSCLK = HCLK = PCLK1 = 8MHz,PCLK2 = 4MHz
*/
static void APP_SystemClockConfig(void)
{
RCC_OscInitTypeDef OscInitstruct = {0};
RCC_ClkInitTypeDef ClkInitstruct = {0};
/* ---- ① 配置振荡器:告诉 HAL 每个时钟源开还是关 ---- */
OscInitstruct.OscillatorType = RCC_OSCILLATORTYPE_HSE | RCC_OSCILLATORTYPE_HSI
| RCC_OSCILLATORTYPE_LSE | RCC_OSCILLATORTYPE_LSI
| RCC_OSCILLATORTYPE_HSI48M;
OscInitstruct.HSEState = RCC_HSE_OFF; /* 外部晶振 关 */
OscInitstruct.HSI48MState = RCC_HSI48M_OFF; /* 内部 48M(USB)关 */
OscInitstruct.HSIState = RCC_HSI_ON; /* 内部 8M RC 开 ← 本例时钟源 */
OscInitstruct.LSEState = RCC_LSE_OFF; /* 外部 32.768k 关 */
OscInitstruct.LSIState = RCC_LSI_OFF; /* 内部低速 RC 关 */
OscInitstruct.PLL.PLLState = RCC_PLL_OFF; /* PLL 倍频器 关 */
if (HAL_RCC_OscConfig(&OscInitstruct) != HAL_OK)
{
APP_ErrorHandler();
}
/* ---- ② 配置时钟树:选谁当系统时钟 + 各总线怎么分频 ---- */
ClkInitstruct.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
ClkInitstruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI; /* 系统时钟 = HSI = 8MHz */
ClkInitstruct.AHBCLKDivider = RCC_SYSCLK_DIV1; /* HCLK = 8MHz */
ClkInitstruct.APB1CLKDivider = RCC_HCLK_DIV1; /* PCLK1 = 8MHz */
ClkInitstruct.APB2CLKDivider = RCC_HCLK_DIV2; /* PCLK2 = 4MHz */
/* 第二个参数是 Flash 等待周期。8MHz 很慢,0 等待就够 */
if (HAL_RCC_ClockConfig(&ClkInitstruct, FLASH_LATENCY_0) != HAL_OK)
{
APP_ErrorHandler();
}
}
/**
* @brief 出错时进死循环(方便打断点抓现场)
*/
void APP_ErrorHandler(void)
{
while (1)
{
}
}
|
4. 代码分段讲解
4.1 为什么 HAL_Init() 必须放最前面
HAL_Init() 里做了这些事:
- 配置 Flash 的预取缓冲和等待周期;
- 把 SysTick 配成 1ms 中断,并把优先级设好;
- 调用
HAL_MspInit()(在 py32f403_hal_msp.c 里,本例只开了 SYSCFG 和 PWR 时钟)。
第 2 点最关键:HAL_Delay() 的原理是读一个全局变量 uwTick,而 uwTick 是在 SysTick 中断里 ++ 的。如果你没调 HAL_Init(),SysTick 不跑,uwTick 永远是 0,HAL_Delay() 就变成死循环。
调试技巧:程序卡在 HAL_Delay() 出不来,99% 是 SysTick 没跑或者中断被全局关掉了(__disable_irq() 之后忘了开)。
4.2 GPIO_InitTypeDef GPIO_InitStruct = {0}; 为什么要清零
这是个局部变量,放在栈上。栈上的内容是上次函数调用留下的垃圾数据。如果不清零:
1
2
3
4
|
GPIO_InitTypeDef s; // s.Alternate 可能是随机值 0x0B
s.Pin = GPIO_PIN_1;
s.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOA, &s); // Alternate 字段虽然输出模式不用,但某些外设会读它 → 玄学 bug
|
养成习惯:所有 HAL 的 Init 结构体,声明时一律 = {0}。
4.3 推挽输出 vs 开漏输出
这是初学最容易糊涂的地方,用一张表说清:
|
推挽 OUTPUT_PP |
开漏 OUTPUT_OD |
| 输出高电平 |
内部 PMOS 导通,主动拉到 VDD |
PMOS 不存在,高阻态(靠外部上拉电阻拉高) |
| 输出低电平 |
内部 NMOS 导通,拉到 GND |
NMOS 导通,拉到 GND |
| 驱动能力 |
强,高低都能驱动 |
只能"往下拉" |
| 典型用途 |
LED、蜂鸣器、SPI、普通控制信号 |
I2C 总线、多个器件"线与"、5V 电平转换 |
点灯就用推挽,不用犹豫。
4.4 LED 是"灌电流"还是"拉电流"
板上 LED 有两种接法,直接影响你写高还是写低才亮:
1
2
3
4
5
|
接法 A(拉电流 / 高电平点亮):
PA1 ──►|── 电阻 ── GND 写 1 亮,写 0 灭
接法 B(灌电流 / 低电平点亮):
3V3 ──►|── 电阻 ── PA1 写 0 亮,写 1 灭
|
本例用 TogglePin 翻转,两种接法都能看到闪烁,所以不用管。但如果你要写"开机就亮",就得先确认板子是哪种接法。
4.5 时钟树:8MHz 是怎么来的
1
2
3
4
|
┌──── AHB /1 ────► HCLK = 8MHz ── CPU / GPIO / DMA / SysTick
HSI (内部 8MHz RC) ──►SYSCLK 8MHz ─┤
├──── APB1 /1 ───► PCLK1 = 8MHz ── TIM2/3/4、USART2、I2C1、SPI2
└──── APB2 /2 ───► PCLK2 = 4MHz ── TIM1、USART1、SPI1、ADC、GPIO 配置
|
本例没用 PLL,跑的是最保守的 8MHz。PY32F403 最高能跑 72MHz(需要开 HSE + PLL 倍频,并把 FLASH_LATENCY 调到合适的值),但点灯完全没必要。
注意:HAL_Delay() 的精度只跟 SysTick 有关,SysTick 又是从 HCLK 分频来的。所以改了时钟配置,延时依然准确,因为 HAL_RCC_ClockConfig() 内部会重新算 SysTick 的重装值。
4.6 HAL_GPIO_TogglePin() 底层做了什么
1
2
3
4
5
6
7
|
/* HAL 库里的实现(简化) */
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
{
uint32_t odr = GPIOx->ODR;
/* 高 16 位写 1 = 复位对应位,低 16 位写 1 = 置位对应位 */
GPIOx->BSRR = ((odr & GPIO_Pin) << 16u) | (~odr & GPIO_Pin);
}
|
用的是 BSRR 原子置位/复位寄存器,而不是 ODR ^= x 这种"读-改-写"。好处是:即使翻转过程中来了中断,中断里也操作同一个端口的别的引脚,也不会互相覆盖。这是嵌入式里很重要的原子操作思想。
5. 实验现象
- 编译、下载(MDK / IAR / GCC-Makefile 三套工程都在示例目录里);
- 复位或直接运行;
- 开发板上的绿色 LED 稳定闪烁,亮 250ms、灭 250ms,肉眼看是每秒闪 2 次。
用示波器或逻辑分析仪夹在 PA1 上,能看到一个漂亮的方波:
1
2
3
|
┌─────┐ ┌─────┐ ┌─────┐
PA1 │250ms│250ms│ │ │ │ 周期 500ms,占空比 50%
────┘ └─────┘ └─────┘ └────
|
6. 常见问题
Q1:下载成功了,但 LED 一点反应都没有?
按这个顺序排查:
- 时钟有没有开?
LED_GPIO_CLK_ENABLE() 这行在不在 HAL_GPIO_Init() 之前?
- 引脚对不对?拿万用表直流电压档量 PA1,应该在 0V / 3.3V 之间来回跳。如果一直不变,是引脚配置问题;如果在跳但灯不亮,是硬件/接法问题。
- LED 有没有焊反?LED 是有极性的二极管,反了绝对不亮。
- 限流电阻是不是虚焊/开路?
- 是不是根本没跑到 main?在
main() 第一行打断点,看能不能停住。
Q2:LED 常亮或常灭,不闪?
大概率卡在 HAL_Delay() 里出不来:
- 检查
HAL_Init() 是否被调用;
- 检查
SysTick_Handler() 里有没有 HAL_IncTick();(在 py32f403_it.c);
- 检查有没有在别处调
__disable_irq() 关了全局中断。
Q3:闪烁频率跟算的不一样,比如慢了一倍?
HAL_Delay(n) 的实际延时是 n ~ n+1 ms(因为不知道当前 tick 走到哪了)。250ms 里这 1ms 误差可以忽略。如果差了一倍以上,检查:
- 时钟配置是否和你以为的一致,可以
printf("%lu\r\n", HAL_RCC_GetHCLKFreq()); 打出来看;
HSI_VALUE 宏是否被改过(PY32F403 默认 8000000)。
Q4:我想让 LED 亮的时间和灭的时间不一样怎么办?
别用 Toggle,改成显式控制:
1
2
3
4
5
6
7
|
while (1)
{
HAL_GPIO_WritePin(LED_GPIO_PORT, LED_GPIO_PIN, GPIO_PIN_SET);
HAL_Delay(100); /* 亮 100ms */
HAL_GPIO_WritePin(LED_GPIO_PORT, LED_GPIO_PIN, GPIO_PIN_RESET);
HAL_Delay(900); /* 灭 900ms */
}
|
Q5:HAL_Delay() 阻塞了 CPU,能不能不阻塞?
可以,用"时间戳比较法",这是实际项目里的标准做法:
1
2
3
4
5
6
7
8
9
10
|
static uint32_t last = 0;
while (1)
{
if (HAL_GetTick() - last >= 250) /* 无符号减法,自动处理 49 天后的溢出 */
{
last = HAL_GetTick();
HAL_GPIO_TogglePin(LED_GPIO_PORT, LED_GPIO_PIN);
}
/* 这里可以干别的活,不会被延时挡住 */
}
|
7. 小结
| 要点 |
内容 |
| GPIO 三步套路 |
开时钟 → 填结构体 → HAL_GPIO_Init(),顺序不能乱 |
| 点灯用什么模式 |
GPIO_MODE_OUTPUT_PP 推挽输出 |
| 结构体必须清零 |
GPIO_InitTypeDef s = {0}; |
HAL_Init() 的作用 |
配 Flash + 启动 SysTick(HAL_Delay 的命根子) |
| 本例时钟 |
HSI 8MHz,HCLK=8M / PCLK1=8M / PCLK2=4M |
| 翻转的原子性 |
TogglePin 用 BSRR,中断安全 |
| 常见失败原因 |
忘开 GPIO 时钟(第一名)、LED 装反、没调 HAL_Init() |
8. 拓展应用
掌握了 GPIO 输出,下面这些项目立刻就能做:
入门级
- 流水灯:接 8 个 LED 到 PB0~PB7,用
GPIOB->ODR = 1 << i 循环左移。
- 蜂鸣器报警:无源蜂鸣器接 GPIO,用不同频率的方波发出不同音调(进一步就该用 PWM 了 → 看 TIM1_PWM)。
- 继电器控制:GPIO 经三极管/MOS 驱动继电器,做智能插座。注意加续流二极管。
进阶级
- 按键 + LED 联动:把 PA0 配成
GPIO_MODE_INPUT + 上拉,读按键状态控制 LED。加上 20ms 消抖、长按/短按/双击识别。
- 软件模拟单总线:DS18B20 温度传感器、DHT11 湿度传感器都是单线协议,用一个 GPIO 在输入/输出模式之间来回切换就能驱动。这是练时序的最佳题目。
- 软件 I2C / SPI:当硬件外设引脚被占用时,用两三个普通 GPIO 手动打时序,能挂任何 I2C/SPI 器件。
- 驱动 74HC595:3 根 GPIO 扩展出 8 路输出,级联可以扩到几十路,做大型点阵屏。
工程级
- 代码执行时间测量:进函数拉高 GPIO、出函数拉低,用逻辑分析仪量脉宽,比
printf 打时间戳精确得多,而且几乎零开销。这是排查实时性问题的利器。
- 看门狗喂狗指示:主循环每跑一圈翻转一次 GPIO,接示波器就能一眼看出程序有没有跑飞、周期是否稳定。
- 上电自检指示:用不同的闪烁次数/节奏表示不同错误码(类似硬盘故障灯),产品没有屏幕时非常实用。
下一步建议
先做 EXTI 外部中断(按键中断点灯),再学 USART 把 printf 打通——有了串口输出,后面调试所有外设都会轻松十倍。