PY32F403 GPIO — GPIO_Toggle 点灯(2Hz 闪烁)

PUYA PY32F403 HAL 库 V1.4.5 GPIO_Toggle 示例逐行讲解:推挽输出、HAL_Delay、时钟树配置

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() 里做了这些事:

  1. 配置 Flash 的预取缓冲和等待周期;
  2. 把 SysTick 配成 1ms 中断,并把优先级设好;
  3. 调用 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. 实验现象

  1. 编译、下载(MDK / IAR / GCC-Makefile 三套工程都在示例目录里);
  2. 复位或直接运行;
  3. 开发板上的绿色 LED 稳定闪烁,亮 250ms、灭 250ms,肉眼看是每秒闪 2 次。

用示波器或逻辑分析仪夹在 PA1 上,能看到一个漂亮的方波:

1
2
3
       ┌─────┐     ┌─────┐     ┌─────┐
PA1    │250ms│250ms│     │     │     │      周期 500ms,占空比 50%
   ────┘     └─────┘     └─────┘     └────

6. 常见问题

Q1:下载成功了,但 LED 一点反应都没有?

按这个顺序排查:

  1. 时钟有没有开LED_GPIO_CLK_ENABLE() 这行在不在 HAL_GPIO_Init() 之前
  2. 引脚对不对?拿万用表直流电压档量 PA1,应该在 0V / 3.3V 之间来回跳。如果一直不变,是引脚配置问题;如果在跳但灯不亮,是硬件/接法问题。
  3. LED 有没有焊反?LED 是有极性的二极管,反了绝对不亮。
  4. 限流电阻是不是虚焊/开路
  5. 是不是根本没跑到 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 外部中断(按键中断点灯),再学 USARTprintf 打通——有了串口输出,后面调试所有外设都会轻松十倍。

世界是你们
使用 Hugo 构建
主题 StackJimmy 设计