news 2026/9/28 4:46:23

STM32标准外设库、HAL库实现流水灯

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32标准外设库、HAL库实现流水灯

本次实验继续使用 STM32F103C8T6 最小系统板和三个外接 LED。在任务二中,我使用 STM32 标准外设库实现流水灯;在任务三中,我改用 HAL 库,并加入按键中断控制流水灯暂停和恢复。

实验目的

  1. 熟悉 STM32 标准外设库和 HAL 库的基本工程结构。
  2. 学会使用库函数配置 GPIO 输出,并控制多个 LED 依次点亮。
  3. 理解软件延时的误差,能够估算流水灯的完整循环周期。
  4. 学会配置 GPIO 外部中断,并利用按键控制程序运行状态。
  5. 理解阻塞延时和非阻塞计时的区别,进一步熟悉HAL_GetTick()的使用。

一、实验器材与环境

  • STM32F103C8T6 最小系统板;
  • 红、绿、蓝 LED 各一个;
  • 限流电阻、按键、面包板和杜邦线;
  • ST-Link 下载器;
  • Keil µVision 5;
  • STM32F10x 标准外设库和 STM32F1 HAL 库。

三个 LED 的接线相同:GPIO 引脚经过限流电阻连接 LED 正极,LED 负极接 GND,因此输出高电平时点亮。

LED引脚点亮顺序
蓝灯PA2第一个
红灯PA0第二个
绿灯PA1第三个

二、实验任务二:标准外设库流水灯

2.1 实验要求

使用 STM32F10x 标准外设库控制 PA0、PA1、PA2 三个引脚,使蓝、红、绿三个 LED 每隔约 1 秒依次点亮。

与上一次直接操作寄存器的方式相比,本任务不再手动设置 RCC 和 GPIO 寄存器的具体位,而是通过 ST 提供的标准外设库函数完成时钟使能、GPIO 初始化和输出控制。库函数的底层仍然会访问寄存器,但代码更容易阅读和维护。

2.2 建立工程并添加标准库

我先准备 STM32F10x 标准外设库,工程使用的固件库版本为STM32F10x_StdPeriph_Lib_V3.5.0。

标准外设库的src文件夹中保存.c源文件,将这些文件复制到工程的Library文件夹。

标准外设库的inc文件夹中保存.h头文件,也将这些文件复制到工程的Library文件夹。


工程主要建立以下四个文件夹:

文件夹主要内容
Start启动文件、CMSIS 内核和芯片文件
LibrarySTM32F10x 标准外设库文件
System延时模块Delay.c/.h
Usermain.c、中断和配置文件

在 Keil 中选择STM32F103C8,外部晶振设置为 8 MHz;在 C/C++ 设置中加入宏USE_STDPERIPH_DRIVER,并添加Start、Library、System和User的头文件搜索路径。

工程配置的主要步骤如下:

  1. 新建 Keil 工程并选择STM32F103C8芯片。
  2. 添加中容量启动文件startup_stm32f10x_md.s。
  3. 建立Start、Library、System、User四个文件组。
  4. 将标准外设库源文件、头文件以及用户代码加入对应文件组。
  5. 在预处理宏中加入USE_STDPERIPH_DRIVER。
  6. 将./Start、./Library、./System、./User加入 Include Paths。
  7. 在 Output 选项中勾选 Create HEX File,最后重新编译工程。

2.3 程序设计

程序先打开 GPIOA 时钟,再将 PA0、PA1、PA2 配置为 50 MHz 通用推挽输出。每次先关闭三个 LED,再打开当前 LED,保证同一时间只亮一盏灯。

主要初始化和显示程序如下:

#include"stm32f10x.h"#include"Delay.h"#defineLED_RED_PINGPIO_Pin_0/* PA0 */#defineLED_GREEN_PINGPIO_Pin_1/* PA1 */#defineLED_BLUE_PINGPIO_Pin_2/* PA2 */#defineLED_ALL_PINS(LED_RED_PIN|LED_GREEN_PIN|LED_BLUE_PIN)staticvoidLED_Init(void){GPIO_InitTypeDef GPIO_InitStructure;/* 打开 GPIOA 的外设时钟 */RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);/* 初始状态下三个 LED 全部熄灭 */GPIO_ResetBits(GPIOA,LED_ALL_PINS);/* PA0、PA1、PA2 配置为通用推挽输出 */GPIO_InitStructure.GPIO_Pin=LED_ALL_PINS;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;GPIO_Init(GPIOA,&GPIO_InitStructure);}staticvoidLED_Show(uint16_tpin){GPIO_ResetBits(GPIOA,LED_ALL_PINS);/* 先熄灭全部灯 */GPIO_SetBits(GPIOA,pin);/* 点亮指定灯 */}

主循环如下:

intmain(void){LED_Init();while(1){LED_Show(LED_BLUE_PIN);Delay_ms(1000);LED_Show(LED_RED_PIN);Delay_ms(1000);LED_Show(LED_GREEN_PIN);Delay_ms(1000);}}

程序中主要使用了以下标准库函数:

  • RCC_APB2PeriphClockCmd():打开 GPIOA 时钟;
  • GPIO_Init():配置 GPIO 工作方式;
  • GPIO_ResetBits():输出低电平,熄灭 LED;
  • GPIO_SetBits():输出高电平,点亮 LED。

任务二的执行流程如下:

程序开始

使能 GPIOA 时钟

配置 PA0、PA1、PA2 为推挽输出

蓝灯 PA2 亮约 1 秒

红灯 PA0 亮约 1 秒

绿灯 PA1 亮约 1 秒

GPIO_Speed_50MHz表示 GPIO 输出电路允许的速度等级,并不表示 LED 以 50 MHz 的速度闪烁。LED 的切换速度由程序中的Delay_ms(1000)决定。

2.4 流水灯周期分析

三个状态各延时约 1 秒,因此完整流水周期为:

T ≈ 1 s × 3 = 3 s

所以每只 LED 点亮约 1 秒、熄灭约 2 秒;同一只 LED 从本次点亮到下次点亮的重复周期约为 3 秒,占空比约为 33.3%。程序运行和晶振会产生少量误差,因此实际周期可能略大于 3 秒。

在没有示波器的情况下,可以进入 Keil 调试模式,使用 Logic Analyzer 添加 PA0、PA1、PA2 对应信号,观察三个 GPIO 的高低电平变化。正常情况下,每个信号的高电平约持续 1 秒,相邻信号依次出现高电平,同一个引脚相邻两次高电平的间隔约为 3 秒。若使用软件仿真,需要确认仿真时钟设置与工程实际时钟相符,否则观察到的时间可能与真实开发板存在差异。
结果如图

2.5 实验结果

任务二工程已经通过 Keil 编译,结果为:

0 Error(s), 0 Warning(s)

程序运行后,LED 按照“蓝灯 PA2→红灯 PA0→绿灯 PA1”的顺序循环点亮。

实验二视频

三、实验任务三:HAL 库与按键中断

3.1 实验要求

任务三改用 HAL 库控制流水灯,并增加一个按键:第一次按下按键时暂停流水灯,当前 LED 保持点亮;再次按下按键时恢复运行,并接着暂停前剩余的时间继续计时。

本任务没有在中断函数中直接完成全部控制,而是采用“中断提交请求、主循环处理请求”的方法。这样可以缩短中断执行时间,使程序结构更清晰,也能避免在中断函数中使用阻塞延时。

3.2 HAL 工程结构

HAL 工程包含HAL、Start、System和User等文件夹。HAL文件夹中又分为Inc和Src,分别保存 HAL 头文件和源文件。

Keil 中建立Start、System、HAL和User文件组,将相应文件加入工程。调试时可以使用软件仿真,也可以选择 ST-Link 下载到开发板。

3.3 硬件连接与中断设置

LED 接线与任务二相同。按键一端连接PB11,另一端连接 GND。PB11 配置为内部上拉输入,平时为高电平;按下按键后变为低电平,产生下降沿中断。

PB11 对应 EXTI 线 11,使用EXTI15_10_IRQn中断入口。

#defineKEY_PINGPIO_PIN_11#defineKEY_PORTGPIOBstaticvoidKEY_EXTI_Init(void){GPIO_InitTypeDef gpio={0};__HAL_RCC_GPIOB_CLK_ENABLE();__HAL_RCC_AFIO_CLK_ENABLE();gpio.Pin=KEY_PIN;gpio.Mode=GPIO_MODE_IT_FALLING;/* 下降沿触发 */gpio.Pull=GPIO_PULLUP;/* 内部上拉 */HAL_GPIO_Init(KEY_PORT,&gpio);HAL_NVIC_SetPriority(EXTI15_10_IRQn,1U,0U);HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);}

按键平时由内部上拉保持高电平,按下后 PB11 被接到 GND,电平由高变低。这个下降沿会触发 EXTI11,中断控制器再进入EXTI15_10_IRQHandler()。

3.4 HAL 流水灯程序

LED 使用HAL_GPIO_WritePin()控制。程序利用HAL_GetTick()判断是否经过 1000 ms,不使用阻塞式延时,因此主循环可以及时处理按键请求。

#defineLED_ON_TIME_MS1000Ustaticconstuint16_ts_led_pins[3]={GPIO_PIN_2,/* 蓝灯 PA2 */GPIO_PIN_0,/* 红灯 PA0 */GPIO_PIN_1/* 绿灯 PA1 */};staticvoidLED_Show(uint8_tindex){uint8_ti;for(i=0U;i<3U;i++){HAL_GPIO_WritePin(GPIOA,s_led_pins[i],(i==index)?GPIO_PIN_SET:GPIO_PIN_RESET);}}

任务三的整体流程如下:

否

是

否

是

是

否

HAL 初始化和系统时钟配置

初始化 LED 与 PB11 外部中断

蓝灯开始点亮

收到按键请求?

当前灯已亮满 1 秒?

切换到下一盏灯

当前是否运行?

保存剩余时间并暂停

恢复运行并继续剩余时间

程序使用以下几个状态变量:

变量作用
s_running表示流水灯处于运行还是暂停状态
s_led_index记录当前点亮的是蓝灯、红灯还是绿灯
s_led_start_tick保存当前 LED 开始点亮的时间
s_led_remaining暂停时保存当前 LED 剩余的点亮时间
s_key_request中断回调向主循环提交的按键请求

主循环在运行状态下每隔 1 秒切换 LED;收到按键请求时,在运行和暂停两种状态之间切换:

while(1){if(s_key_request!=0U){s_key_request=0U;if(s_running!=0U){uint32_tnow=HAL_GetTick();uint32_telapsed=now-s_led_start_tick;/* 暂停并保存当前灯剩余的点亮时间 */s_led_remaining=(elapsed>=LED_ON_TIME_MS)?LED_ON_TIME_MS:(LED_ON_TIME_MS-elapsed);s_running=0U;}else{uint32_tnow=HAL_GetTick();/* 恢复后继续暂停前剩余的时间 */s_led_start_tick=now-(LED_ON_TIME_MS-s_led_remaining);s_running=1U;LED_Show(s_led_index);}}if(s_running!=0U){uint32_tnow=HAL_GetTick();if((now-s_led_start_tick)>=LED_ON_TIME_MS){s_led_index=(uint8_t)((s_led_index+1U)%3U);s_led_start_tick=now;LED_Show(s_led_index);}}}

3.5 按键消抖和中断回调

机械按键按下时可能连续产生多个跳变,因此程序使用HAL_GetTick()实现约 50 ms 的非阻塞消抖。中断回调中只设置请求标志,暂停和恢复的具体处理放在主循环中完成。

#defineKEY_DEBOUNCE_MS50UvoidHAL_GPIO_EXTI_Callback(uint16_tGPIO_Pin){uint32_tnow;if(GPIO_Pin!=KEY_PIN){return;}now=HAL_GetTick();if((now-s_last_key_tick)<KEY_DEBOUNCE_MS){return;}s_last_key_tick=now;s_key_request=1U;}

中断服务函数调用 HAL 的 EXTI 处理函数,再进入上面的回调:

voidEXTI15_10_IRQHandler(void){HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_11);}

一次完整的按键控制过程可以表示为:

操作流水灯状态LED 状态
上电运行正常切换蓝→红→绿循环
第一次按键暂停当前 LED 保持点亮
暂停期间不推进序号画面保持不变
第二次按键恢复当前 LED 继续剩余时间
剩余时间结束正常运行切换到下一盏 LED

例如某盏 LED 已点亮约 400 ms 时按下按键,程序会保存约 600 ms 的剩余时间。再次按键恢复后,这盏灯继续保持约 600 ms,然后才切换到下一盏灯,而不是重新点亮完整的 1 秒。

3.6 实验结果

任务三工程已经通过 Keil 编译,结果为:

Program Size: Code=4108 RO-data=316 RW-data=36 ZI-data=1636 0 Error(s), 0 Warning(s)

正常运行时三盏灯按照蓝、红、绿顺序循环。第一次按下按键后,流水灯暂停并保持当前 LED;再次按下后继续运行。

实验三视频

四、实验中遇到的问题与处理方法

4.1 标准库文件较多

STM32F10x 标准外设库包含许多外设的.c和.h文件。本实验实际主要使用 GPIO 和 RCC,但为了保持完整工程模板,仍然保留了其他标准库文件。编译时必须正确设置头文件搜索路径和USE_STDPERIPH_DRIVER宏,否则容易出现找不到头文件或函数声明的问题。

4.2 LED 不亮或顺序不正确

出现这种情况时,应检查 LED 正负极、限流电阻、公共 GND 和 PA0~PA2 接线。由于本实验采用高电平点亮,程序需要先复位全部输出,再置位当前 LED 引脚。

4.3 按一次却切换多次

机械按键存在抖动,一次按下可能产生多个下降沿。任务三使用 50 ms 消抖时间,短时间内重复产生的中断会被忽略,从而使一次按键只完成一次暂停或恢复。

4.4 中断中不使用长延时

如果在中断函数中执行长时间延时,主程序和其他中断会被阻塞。因此本实验的中断回调只设置s_key_request,实际状态切换放到主循环中完成。

五、三种实现方式比较

开发方式主要写法优点特点
寄存器方式直接设置寄存器地址和位能清楚理解硬件底层原理代码较繁琐,需要仔细查手册
标准外设库调用GPIO_Init()等函数可读性较好,旧版 STM32F1 示例较多需要添加标准库文件和宏定义
HAL 库调用HAL_GPIO_Init()等函数接口统一,便于配置中断和复杂外设工程文件和代码量相对较大

任务二让我熟悉了 STM32 标准外设库的文件结构和 GPIO 控制函数;任务三进一步学习了 HAL 库、外部中断、按键消抖和非阻塞计时。通过这两个任务,我实现了流水灯的基本运行,以及使用按键控制暂停和恢复。

通过对比可以看出,不同开发方式最终都会对 GPIO 和中断相关寄存器产生操作,只是代码封装层次不同。寄存器方式适合理解底层,标准外设库适合熟悉 STM32F1 的传统项目,HAL 库则更方便组织较完整的应用程序。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 4:46:15

DeepSeek LeetCode 122. 买卖股票的最佳时机 II Rust实现

LeetCode 122. 买卖股票的最佳时机 II&#xff08;Rust 实现&#xff09; 思路 由于可以无限次交易&#xff0c;且任意时刻最多持有一股&#xff0c;所以只要后一天价格比前一天高&#xff0c;就可以在前一天买入、后一天卖出&#xff0c;赚取差价。将所有相邻两天的正收益累加…

作者头像 李华
网站建设 2026/9/28 4:46:10

第八章 组网技术

第八章 组网技术 8-1 交换机 交换机分类 ■ 1.根据交换方式分存储转发式交换&#xff08;Store and Forward&#xff09;&#xff1a;完整接收数据帧&#xff0c;缓存、验证、碎片过滤&#xff0c;然后转发。优点&#xff1a;可以提供差错校验和非对称交换。缺点&#xff1a;延…

作者头像 李华
网站建设 2026/9/28 4:46:08

玻尔兹曼机:深度学习的「前世」,从统计力学到 RBM

从统计力学到受限玻尔兹曼机&#xff0c;理解深度学习的历史脉络开头&#xff1a;统计力学和机器学习有什么关系&#xff1f; 上一篇我们学习了信息论&#xff0c;用熵度量不确定性。 今天我们学习玻尔兹曼机——深度学习的「前世」。 统计力学&#xff1a; 研究大量粒子的宏观…

作者头像 李华
网站建设 2026/9/28 4:45:52

JVM 应用度量框架 Micrometer 实战详解

1. 为什么需要 Micrometer在微服务和云原生时代&#xff0c;应用的运行状态已经不能只靠「服务是否在线」来判断。接口有没有变慢、线程池是否被打满、缓存命中率是否下降、GC 是否频繁、下游依赖是否抖动&#xff0c;这些问题都需要通过可量化的指标来持续观测。传统的日志只能…

作者头像 李华