本次实验继续使用 STM32F103C8T6 最小系统板和三个外接 LED。在任务二中,我使用 STM32 标准外设库实现流水灯;在任务三中,我改用 HAL 库,并加入按键中断控制流水灯暂停和恢复。
实验目的
- 熟悉 STM32 标准外设库和 HAL 库的基本工程结构。
- 学会使用库函数配置 GPIO 输出,并控制多个 LED 依次点亮。
- 理解软件延时的误差,能够估算流水灯的完整循环周期。
- 学会配置 GPIO 外部中断,并利用按键控制程序运行状态。
- 理解阻塞延时和非阻塞计时的区别,进一步熟悉
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 内核和芯片文件 |
Library | STM32F10x 标准外设库文件 |
System | 延时模块Delay.c/.h |
User | main.c、中断和配置文件 |
在 Keil 中选择STM32F103C8,外部晶振设置为 8 MHz;在 C/C++ 设置中加入宏USE_STDPERIPH_DRIVER,并添加Start、Library、System和User的头文件搜索路径。
工程配置的主要步骤如下:
- 新建 Keil 工程并选择
STM32F103C8芯片。 - 添加中容量启动文件
startup_stm32f10x_md.s。 - 建立
Start、Library、System、User四个文件组。 - 将标准外设库源文件、头文件以及用户代码加入对应文件组。
- 在预处理宏中加入
USE_STDPERIPH_DRIVER。 - 将
./Start、./Library、./System、./User加入 Include Paths。 - 在 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。
任务二的执行流程如下:
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);}}任务三的整体流程如下:
程序使用以下几个状态变量:
| 变量 | 作用 |
|---|---|
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 库则更方便组织较完整的应用程序。