在这里插入图片描述

在这里插入图片描述

🎬 云泽Q个人主页

🔥 专栏传送入口: 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列》《笔试算法》《AI赋能》《STM32

⛺️遇见安然遇见你,不负代码不负卿~

前言

大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~

一、LED 闪烁功能的硬件原理与电路分析

1.1 LED4 的硬件连接与电平逻辑

接下来我们从源码层面回归到实际的需求层面,我们要实现的下一个功能是“闪灯”,即不仅仅是点亮 LED,还要让它产生周期性的闪烁效果。

首先,我们需要明确控制对象。在本次实验中,我们操作的 LED 是 LED4。根据原理图可知,LED4 是一个红色的发光二极管,它连接在 MCU 的 PF8 引脚上(网络标签为 SYS_LED1)。

这里有一个非常关键的电路特征:LED4 采用了外接高电平的设计。也就是说,LED 的阳极通过一个 680Ω 的限流电阻(R47)直接连接到了 3.3V 电源(VDD_3.3V)上,而 LED 的阴极则连接到了 MCU 的 PF8 引脚。这种接法决定了我们的控制逻辑是“低电平点亮,高电平熄灭”。
在这里插入图片描述

1.2 开漏输出模式与 MOS 管工作原理

为了配合这种“低电平点亮”的硬件电路,我们在配置 GPIO 时选择了**开漏输出(Output Open Drain)**模式。为什么选开漏而不是推挽?这里涉及到芯片内部 MOS 管的物理结构,我们来详细拆解一下。

在 STM32 的 GPIO 内部结构中,输出级通常包含两个 MOS 管:一个是 P-MOS 管(上拉),一个是 N-MOS 管(下拉)。

  • 推挽输出:P-MOS 和 N-MOS 都会参与工作。输出高电平时 P-MOS 导通,输出低电平时 N-MOS 导通。
  • 开漏输出:我们在 CubeMX 中配置为开漏模式时,实际上是在内部禁用了 P-MOS 管,只保留了 N-MOS 管在工作。

基于只使用 N-MOS 管的特性,我们可以这样理解 PF8 引脚的电平变化过程:

  1. 写 0(低电平):当我们向输出数据寄存器写 0 时,内部的 N-MOS 管导通。此时,PF8 引脚相当于直接连通到了内部的 GND。由于外部 LED 阳极接的是 3.3V,阴极(PF8)被拉低到 GND,形成了完整的电流回路(3.3V → R47 → LED4 → PF8 → GND),因此 LED4 点亮
  2. 写 1(高电平):当我们向输出数据寄存器写 1 时,由于 P-MOS 被禁用,N-MOS 管截止(闭合)。此时,PF8 引脚处于高阻态(High-Z),相当于内部断开了,MCU 内部与外部电路没有电气连接。既然回路断开,没有电流流过,LED4 自然熄灭
    在这里插入图片描述

虽然理论上使用推挽输出也能通过输出高低电平来控制 LED 亮灭,但在本案例的硬件设计中,开漏输出是更匹配“外部上拉/高电平供电”逻辑的配置方式。

二、STM32CubeMX 配置与工程生成

2.1 GPIO 参数详细配置

明确了硬件原理后,我们打开 STM32CubeMX 进行具体的引脚配置。针对 PF8 引脚,我们需要关注以下几个关键参数的设置,这些设置直接对应了刚才讲解的硬件逻辑:

  • Pin Name: PF8
  • GPIO output level: 设置为 High
    • 注意:这里初始电平设为 High 是为了让系统在复位启动的瞬间,LED 处于熄灭状态(高阻态或高电平),避免上电瞬间出现不可控的闪烁。
  • GPIO mode: 选择 Output Open Drain(开漏输出)。这是核心配置,确保只启用 N-MOS 管。
  • GPIO Pull-up/Pull-down: 选择 No pull-up and no pull-down。因为外部电路已经有明确的电源连接,且开漏模式下内部上拉通常不参与主要驱动逻辑(视具体设计而定,这里后面有文章会细讲)。
  • Maximum output speed: 设置为 Low。对于控制 LED 这种低频信号,低速模式足以满足需求,同时有助于降低电磁干扰(EMI)。
    在这里插入图片描述

2.2 代码生成与环境同步

配置完成后,点击 GENERATE CODE 生成工程代码。此时需要注意开发环境的同步问题。如果你之前在 VSCode 或其他编辑器中手动修改过文件,重新生成代码可能会触发文件保护或覆盖冲突。

在实际操作中,我们需要将生成的代码同步到 VSCode 环境中。如果在 VSCode 中看到文件有修改标记或提示,需要正确处理合并或覆盖,确保 main.c 中的初始化代码是最新的。特别是 MX_GPIO_Init() 函数,它必须包含我们刚才配置的 PF8 开漏输出初始化逻辑。

三、LED 闪烁的代码实现方案

实现闪灯的本质,就是让 LED 在“亮”和“灭”两个状态之间循环切换,并在每个状态保持一段时间,让人眼能够观察到变化。我们提供了两种主要的代码实现思路。

3.1 基础方案:显式置位与复位

最直观的方法是分别调用点亮和熄灭的函数,中间穿插延时。

while(1) 主循环中,逻辑如下:

  1. 点亮 LED:调用 HAL_GPIO_WritePin,将 PF8 置为 GPIO_PIN_RESET(即低电平 0)。根据前面的原理,N-MOS 导通,灯亮。
  2. 延时保持:调用 HAL_Delay(200),让灯亮持续 200 毫秒。
  3. 熄灭 LED:调用 HAL_GPIO_WritePin,将 PF8 置为 GPIO_PIN_SET(即高电平 1)。此时 N-MOS 截止,进入高阻态,灯灭。
  4. 延时保持:再次调用 HAL_Delay(200),让灭的状态持续 200 毫秒。

示例代码如下:

/* USER CODE BEGIN WHILE */
while (1)
{
    // 1. 点亮 LED4 (写 0,N-MOS 导通)
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, GPIO_PIN_RESET); 
    HAL_Delay(200); // 延时 200ms

    // 2. 熄灭 LED4 (写 1,N-MOS 截止,高阻态)
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, GPIO_PIN_SET);   
    HAL_Delay(200); // 延时 200ms
}
/* USER CODE END WHILE */

在这个阶段,我们将延时时间从最初的 1000ms 调整为了 200ms。原本的 1 秒闪烁频率较慢,改为 200ms 后,闪烁节奏明显变快,视觉效果更佳。

3.2 进阶方案:使用 TogglePin 翻转函数

除了显式地写 0 和写 1,HAL 库还提供了一个更简洁的函数——HAL_GPIO_TogglePin

这个函数的作用是反转当前引脚的电平状态:

  • 如果当前引脚是低电平(灯亮),调用后变为高电平(灯灭)。
  • 如果当前引脚是高电平(灯灭),调用后变为低电平(灯亮)。

使用这个函数,我们不需要关心当前灯到底是亮还是灭,只需要不断地“翻转”它即可。配合延时函数,同样能实现完美的闪烁效果。

优化后的代码如下:

/* USER CODE BEGIN WHILE */
while (1)
{
    // 翻转 PF8 引脚电平
    HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_8); 
    
    // 延时 1000ms (1秒翻转一次)
    // 你可以根据需要改为 200ms
    HAL_Delay(1000); 
}
/* USER CODE END WHILE */

这种写法代码量更少,逻辑也更通用。无论你初始状态是什么,它都能保证产生方波一样的闪烁信号。

3.3 跑马灯逻辑实现

接下来实现一个经典的“跑马灯”效果。这里搞会之后灯随便怎么亮都可以自己玩了

3.3.1 跑马灯需求分析与硬件映射

我们要实现的跑马灯效果,并非简单的单向流动,而是一种“钟摆式”的往复运动。具体来说,就是 LED 灯按照 1→2→3→4 的顺序依次点亮并熄灭,到达末端后,再按照 4→3→2→1 的顺序反向回扫,如此循环往复。

要实现这个效果,首先必须搞清楚硬件上的引脚对应关系。

在我的开发板上,物理存在的四个 LED 灯分别标记为 LED4、LED5、LED7、LED8。查看原理图时,这四个灯对应的网络标签被设计者命名为 SYS_LED1SYS_LED4。这还没完,当我们顺着网络标签找到 STM32F103ZET6 芯片时,会发现它们实际连接的 MCU 内部引脚分别是 PF8、PF9、PF10、PF11
在这里插入图片描述

⚠️ 避坑指南
千万不要看到板子上写着 “LED4” 就以为它对应芯片的 Pin 4,也不要看到原理图上的 SYS_LED1 就以为是 PA1 或 PB1。必须严格遵循原理图的电气连接

  • LED4 (红) → \rightarrow SYS_LED1 → \rightarrow PF8
  • LED5 (绿) → \rightarrow SYS_LED2 → \rightarrow PF9
  • LED7 (黄) → \rightarrow SYS_LED3 → \rightarrow PF10
  • LED8 (蓝) → \rightarrow SYS_LED4 → \rightarrow PF11

明确了这一点,我们在软件配置时才能有的放矢。虽然控制四个灯和控制一个灯在本质上没有区别,都是操作 ODR(输出数据寄存器)的特定位,但为了代码的优雅和可维护性,我们需要一种更聪明的配置方式。

3.3.2 CubeMX 配置与底层初始化逻辑

在 STM32CubeMX 中,我们将 PF8 至 PF11 统一配置为 GPIO_Output 模式。这里有几个关键参数需要特别注意,它们直接决定了 LED 的驱动逻辑:
在这里插入图片描述

  1. GPIO Output Level (初始电平):设置为 High
    结合我们之前讲过的开漏输出电路特性,当 MCU 输出高电平时,MOS 管截止,LED 两端无压差,灯是的。这样设置可以保证系统上电复位的瞬间,LED 不会发生误闪烁。
  2. GPIO Mode (输出模式):设置为 Output Open Drain (开漏输出)
    当然,推挽输出(Push Pull)也是完全可以的。但在本案例中,为了配合外部上拉或特定的驱动电路逻辑,我们选择开漏模式。
  3. GPIO Pull-up/Pull-down:设置为 No pull-up and no pull-down
  4. Maximum Output Speed:设置为 Low
    控制 LED 闪烁属于低频操作,不需要高速响应,低速模式有助于降低电磁干扰(EMI)和功耗。

生成代码后,打开 main.c,你会看到自动生成的 MX_GPIO_Init() 函数。请注意这段代码中的细节处理:

static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* USER CODE BEGIN MX_GPIO_Init_1 */

  /* USER CODE END MX_GPIO_Init_1 */

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOF_CLK_ENABLE(); // 开启 F 端口时钟
  __HAL_RCC_GPIOA_CLK_ENABLE(); // 开启 A 端口时钟(虽未用到,但CubeMX可能默认开启)

  /*Configure GPIO pin Output Level */
  // 【关键点】初始化时直接通过 BSRR 寄存器将 PF8-PF11 置高(熄灭)
  HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11, GPIO_PIN_SET);

  /*Configure GPIO pins : PF8 PF9 PF10 PF11 */
  GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);

  /* USER CODE BEGIN MX_GPIO_Init_2 */

  /* USER CODE END MX_GPIO_Init_2 */
}

在这里插入图片描述

大家注意看 HAL_GPIO_WritePin 这一行。它在 GPIO 初始化结构体配置之前,就先把这四个引脚的电平拉高了。这是为了防止在 GPIO 模式配置的短暂窗口期内,引脚处于默认的低电平状态导致 LED 瞬间亮一下。这种“先置位,后配置”的手法,是嵌入式开发中保证系统稳定性的常用技巧。

3.3.3 核心代码实现:数组映射与双向循环

为了让跑马灯的逻辑清晰且易于扩展,我们不建议直接硬编码 PF8PF9 这样的宏定义。相反,我们应该利用 C 语言的数组特性来抽象硬件。

1. 定义 LED 引脚数组

main.c 的私有变量区域(/* USER CODE BEGIN PV */),我们定义如下变量:

/* USER CODE BEGIN PV */
// 定义一个数组,按顺序存储四个 LED 的引脚号
uint16_t gleds[] = {GPIO_PIN_8, GPIO_PIN_9, GPIO_PIN_10, GPIO_PIN_11};

// 利用 sizeof 动态计算 LED 的数量,避免手动数数出错
const uint16_t gleds_num = sizeof(gleds) / sizeof(gleds[0]);
/* USER CODE END PV */

在这里插入图片描述

这样做的好处是显而易见的:如果未来硬件改版,LED 换到了 PE 端口,或者顺序变了,你只需要修改这个数组的内容,后面的循环逻辑完全不用动。gleds_num 的计算方法也是非常标准的 C 语言技巧,它能自动适应数组大小的变化。

2. 主循环中的跑马灯逻辑

main 函数的 while(1) 循环中,我们分两步来实现“钟摆”效果:

第一步:正向扫描(从左向右)

// 从左向右跑马灯
for(int i = 0; i < gleds_num; i++)
{
    HAL_GPIO_WritePin(GPIOF, gleds[i], GPIO_PIN_RESET); // 点亮当前 LED (低电平有效)
    HAL_Delay(200);                                     // 延时 200ms
    HAL_GPIO_WritePin(GPIOF, gleds[i], GPIO_PIN_SET);   // 熄灭当前 LED (高电平)
}

这里利用了数组下标 i0 递增到 3。每次循环,先写低电平(RESET)点亮灯,人眼观察到亮后,延时 200ms,再写高电平(SET)熄灭灯,为下一次循环做准备。

第二步:反向扫描(从右向左)

// 从右向左跑马灯
for(int i = gleds_num - 1; i >= 0; i--)
{
    HAL_GPIO_WritePin(GPIOF, gleds[i], GPIO_PIN_RESET); // 点亮当前 LED
    HAL_Delay(200);                                     // 延时 200ms
    HAL_GPIO_WritePin(GPIOF, gleds[i], GPIO_PIN_SET);   // 熄灭当前 LED
}

反向扫描的逻辑与正向完全一致,唯一的区别在于循环变量 i 的初始值是 gleds_num - 1(即 3),并且条件是 i >= 0,步长是 i--。这样就实现了从最后一个灯回溯到第一个灯的效果。
在这里插入图片描述

此外,在进入 while(1) 死循环之前,我们还加了一段初始化代码:先遍历一次数组将所有灯熄灭(双重保险),然后执行 HAL_Delay(2000)。这 2 秒的延时是为了让系统上电后有一个明显的“准备期”,避免一上电就开始狂闪,体验更好。

3.3.4 进阶技巧:HAL 库函数的查阅与使用

在编写上述代码时,我们用到了 HAL_GPIO_WritePin。其实 HAL 库还提供了一个非常便捷的函数叫 HAL_GPIO_TogglePin(翻转引脚电平)。

如果你不想显式地写“点亮”和“熄灭”两步操作,而是想让灯的状态直接取反(亮变灭,灭变亮),就可以使用这个函数。它的源码实现非常精妙,利用了 BSRR 寄存器的特性进行原子操作:

void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
{
  uint32_t odr;
  
  /* 检查参数合法性 */
  assert_param(IS_GPIO_PIN(GPIO_Pin));

  /* 获取当前 ODR 寄存器的值 */
  odr = GPIOx->ODR;

  /* 
   * 核心逻辑:
   * 如果当前位是低(0),则向 BSRR 的低 16 位写 1 (置位/Set);
   * 如果当前位是高(1),则向 BSRR 的高 16 位写 1 (复位/Reset)。
   * 这是一个原子操作,不会被中断打断。
   */
  GPIOx->BSRR = ((odr & GPIO_Pin) << GPIO_NUMBER) | (~odr & GPIO_Pin);
}

虽然在这个跑马灯案例中,我们需要明确的“亮-灭”时序,用 WritePin 更直观,但在很多 Blink(闪烁)场景下,TogglePin 能少写一半代码。

如何高效查阅 HAL 库手册?

很多人在找函数原型或参数说明时会感到迷茫。这里教大家三种高效的方法:

  1. AI 辅助:直接让 AI 生成 HAL 库的常用接口,让它解释参数和用法,这是最快的方式。

  2. IDE 跳转:在代码中右键点击函数名(如 HAL_GPIO_WritePin),选择“Go to Definition”,直接看头文件 stm32f1xx_hal_gpio.h 中的声明和注释。
    在这里插入图片描述

  3. 官方 CHM 手册(推荐)

    • 找到你的固件包目录,通常在 STM32Cube\Repository\STM32Cube_FW_F1_V1.8.0\Drivers\STM32F1xx_HAL_Driver 下。

    • 打开 STM32F103xG_User_Manual.chm(根据你的芯片型号选择对应的 G/B/E 系列手册)。
      在这里插入图片描述
      在这里插入图片描述

    • 在索引栏搜索 HAL_GPIO_W,你就能立刻找到 WritePinTogglePin 等所有相关函数的详细文档,包括参数定义、返回值说明以及注意事项。

建议可以把这个 CHM 手册创建一个快捷方式放到桌面上,可以当字典来用。


结语

在这里插入图片描述

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐