☰
嵌入式基础第三课——标准外设库 / HAL 库控制 GPIO 流水灯
2026/9/30 5:26:53 网站建设 项目流程

嵌入式基础第三课——标准外设库 / HAL 库控制 GPIO 流水灯

本课是上一课《寄存器方式控制 GPIO 流水灯》之后的两个进阶实验。上一课已经用直接操作寄存器的方式点亮了三盏 LED、搞清了 GPIO 的底层原理;本课在此基础上,分别改用 ST 官方的**标准外设库(StdPeriph)**和HAL 库重新实现同一个流水灯,并加入两个新内容:软件延时标定(Keil 逻辑分析仪)与按键中断(暂停/恢复)。

三盏灯的接线、闪烁顺序(绿灯 PB0 → 蓝灯 PC13 → 红灯 PA0,同一时刻只有一盏灯亮)与上一课完全相同,这里不再赘述电路。

一、先搞清楚:三种写法是什么关系

写 STM32 程序一共有三层「由低到高」的写法,这三课正好各对应一层:

写法怎么配 GPIO特点
寄存器方式(第二课)直接往RCC_APB2ENR、GPIOx_CRL/CRH这些地址写数最短最快,但每个地址每个位都要查手册
标准外设库(本课实验二)调GPIO_Init()、RCC_APB2PeriphClockCmd()这类函数封装了一层,可读性好,但要自己搭工程
HAL 库(本课实验三)CubeMX 图形界面点几下,代码自动生成最省事,代码量最大、层次最多

一句话总结:越往上越方便,越往下越透明。本课做的就是从中间两层各实现一遍。

二、实验二:标准外设库方式(软件延时 + 逻辑分析仪)

1. 实验目的

  1. 掌握用 STM32F10x 标准外设库(StdPeriph)配置 GPIO 的方法,理解它和寄存器方式的异同;
  2. 掌握软件空循环延时的原理,学会估算它的周期;
  3. 学会用Keil 软件仿真的逻辑分析仪观察 GPIO 引脚波形,测量并分析实际时序。

2. 硬件连接

与上一课完全一样:

LED引脚接法点亮方式
🟢 绿灯PB0PB0 → 330Ω → LED → GND高电平点亮
🔵 蓝灯PC133.3V → 1kΩ → LED → PC13(与板载蓝灯同亮)低电平点亮
🔴 红灯PA0PA0 → 330Ω → LED → GND高电平点亮

3. 标准外设库工程怎么建

标准外设库工程要手动搭,容易漏的几点:

  1. 新建工程选器件STM32F103C8,加启动文件startup_stm32f10x_md.s(md= 中容量,C8T6 用这个)、system_stm32f10x.c,以及外设驱动stm32f10x_gpio.c、stm32f10x_rcc.c、misc.c;

  2. 魔术棒 → C/C++ →Define填两个宏,漏了必报错:

    • STM32F10X_MD(告诉库是中容量芯片)
    • USE_STDPERIPH_DRIVER(打开标准外设库头文件,不填的话stm32f10x_conf.h里那些#include全被注释掉,GPIO 函数全是「未定义」);
  3. Include Paths把库的inc、CMSIS、Device\Include目录加进去;

  4. 先用Use Simulator做逻辑分析仪仿真,要烧录时再切成 ST-Link。

4. 标准外设库 C 代码

#include"stm32f10x.h"/* 标准外设库总头文件 *//* 软件延时粗调系数:72MHz、-O0 下空循环每轮约 4 个周期, 1ms = 72000 周期,所以 1ms ≈ 72000/4 = 18000 次(估算值,需实测微调) */#defineDELAY_LOOP_PER_MS18000U/* ===== 供 Keil 逻辑分析仪观察的"软信号" ===== * Keil 软件仿真器不模拟 STM32 的 GPIO 外设,PORTB.0 / GPIOB_ODR 这类 * 内存映射寄存器在仿真器里既不是变量、也不是内核虚拟寄存器,逻辑分析仪 * 认不出(报 Unknown Signal)。所以用三个普通全局变量代替引脚电平, * 逻辑分析仪里直接看这三个变量。统一按 1=亮、0=灭 记录。 */volatileunsignedintg_led_green=0;/* 绿灯 PB0 */volatileunsignedintg_led_blue=0;/* 蓝灯 PC13 */volatileunsignedintg_led_red=0;/* 红灯 PA0 *//* 软件空循环延时:不依赖 SysTick,周期不精确 */voidDelay_ms(uint32_tnms){uint32_ti,j;for(i=0;i<nms;i++)for(j=0;j<DELAY_LOOP_PER_MS;j++);}voidLED_GPIO_Config(void){GPIO_InitTypeDef GPIO_InitStructure;/* 使能 GPIOA/B/C 时钟(标准库函数,底层还是写 RCC_APB2ENR) */RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC,ENABLE);/* PB0(绿灯):50MHz 通用推挽输出 */GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOB,&GPIO_InitStructure);/* PA0(红灯):参数相同,复用结构体 */GPIO_Init(GPIOA,&GPIO_InitStructure);/* PC13(蓝灯):备份域引脚,速度只能 2MHz */GPIO_InitStructure.GPIO_Pin=GPIO_Pin_13;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_2MHz;GPIO_Init(GPIOC,&GPIO_InitStructure);/* 上电全灭:PA0/PB0 输出 0(灭),PC13 输出 1(低电平点亮,置 1=灭) */GPIO_ResetBits(GPIOB,GPIO_Pin_0);GPIO_ResetBits(GPIOA,GPIO_Pin_0);GPIO_SetBits(GPIOC,GPIO_Pin_13);}intmain(void){LED_GPIO_Config();while(1){/* 第1拍:绿灯亮 */GPIO_SetBits(GPIOB,GPIO_Pin_0);GPIO_ResetBits(GPIOA,GPIO_Pin_0);GPIO_SetBits(GPIOC,GPIO_Pin_13);g_led_green=1;g_led_blue=0;g_led_red=0;Delay_ms(1000);/* 第2拍:蓝灯亮 */GPIO_ResetBits(GPIOC,GPIO_Pin_13);GPIO_ResetBits(GPIOB,GPIO_Pin_0);GPIO_ResetBits(GPIOA,GPIO_Pin_0);g_led_green=0;g_led_blue=1;g_led_red=0;Delay_ms(1000);/* 第3拍:红灯亮 */GPIO_SetBits(GPIOA,GPIO_Pin_0);GPIO_ResetBits(GPIOB,GPIO_Pin_0);GPIO_SetBits(GPIOC,GPIO_Pin_13);g_led_green=0;g_led_blue=0;g_led_red=1;Delay_ms(1000);}}

5.【重点】用 Keil 逻辑分析仪测时序

软件仿真不需要硬件,用 Keil 自带的逻辑分析仪就能看 GPIO 波形、测周期。

操作步骤:

  1. 编译通过后,魔术棒 → Debug,左上角选Use Simulator;
  2. 点Debug(放大镜 / Ctrl+F5)进入仿真;
  3. 菜单View → Analysis Windows → Logic Analyzer;
  4. 点窗口左上角Setup,添加三个信号:g_led_green、g_led_blue、g_led_red,每个的Display Type 选 Bit;
  5. 点Run(F5)跑几秒再Stop,用光标卡两个跳变沿读时间差。

⚠️ 这里有两个大坑,本课花了很久才解决:

坑 1:直接添加PORTB.0/GPIOB_ODR会报 Unknown Signal。原因是 Keil 软件仿真器不模拟 STM32 的 GPIO 外设,这些引脚是内存映射寄存器,在仿真器里既不是变量、也不是它认识的内核虚拟寄存器。解决办法就是上面代码里定义的三个全局变量g_led_green / g_led_blue / g_led_red——它们是真实符号,逻辑分析仪 100% 能看。

坑 2:一运行就报access violation at 0x40021000(RCC 寄存器)。原因是仿真器默认没把外设地址段映射进内存。在Command 窗口输入MAP 0x40000000, 0x400FFFFF READ WRITE即可(推荐写成debug.ini,在魔术棒 Debug → Initialization File 指定,每次进仿真自动执行)。

6. 软件延时的实际周期是多少

软件空循环延时本质上不精确,取决于「空循环每轮执行几个时钟周期」,而这又由主频和编译器优化等级决定。粗略估算:

  • 主频 72MHz,1ms = 72000 个时钟周期;
  • for(j=0;j<N;j++);空循环在 -O0 下每轮约 4 个周期;
  • 所以 1ms 需要 N ≈ 72000/4 =18000,Delay_ms(1000)约 1 秒。

用逻辑分析仪实测,把结果填入下表(数值以实测为准):

测量项理论值实测值结论
单拍(LED 点亮)时长1000 ms约 1000 ms(略有偏差)接近 1s,但不完全相等
完整流水灯循环周期3000 ms约 3 × 单拍与三拍逻辑一致
时序是否错开任何时刻只有一路亮三路方波互不重叠时序正确

结论:只要三路波形错开、任何时刻只有一路处于「点亮电平」,时序就正确;单拍实测值和 1000ms 的偏差就是软件延时的误差,偏大就调小DELAY_LOOP_PER_MS、偏小就调大。这正是软件延时的特点:只能靠实测标定,不像 SysTick 天生精确。

7. 实验现象

烧到真板后,绿灯 1 秒 → 蓝灯 1 秒 → 红灯 1 秒循环,任何时刻只有一盏亮。

三、实验三:HAL 库 + 按键中断(暂停/恢复)

1. 实验目的与思路

  1. 掌握用HAL 库 + STM32CubeMX快速配置工程的方法;
  2. 理解**外部中断(EXTI)**原理,掌握按键中断的配置;
  3. 实现用按键中断控制流水灯的暂停与恢复。

思路:

  • 三盏灯与实验二相同,轮流闪 1 秒(HAL 里用HAL_Delay(1000),内部是 SysTick,周期天生精确);
  • 加一个按键接PB1(另一端接 GND,按下为低电平),用芯片的外部中断EXTI1检测;
  • 用一个全局标志g_paused:初始 0(运行),每按一下按键在中断回调里把它取反——按一下暂停、再按一下恢复;
  • 主循环里用「可暂停延时」代替HAL_Delay,每毫秒检查一次g_paused,按下就立刻冻住当前灯。

2. 硬件连接

器件引脚接法
🟢 绿灯PB0PB0 → 330Ω → LED → GND
🔵 蓝灯PC133.3V → 1kΩ → LED → PC13(与板载蓝灯同亮)
🔴 红灯PA0PA0 → 330Ω → LED → GND
🔘 按键PB1PB1 → 按键 → GND,按下为低电平

3. STM32CubeMX 是什么、怎么生成工程

STM32CubeMX是 ST 官方的免费图形化配置工具:以前要手写的 GPIO/时钟/中断初始化代码,现在在界面上点一点,它自动生成完整的 HAL 工程。从官网(st.com)下载,需要注册 myST 账号,还要先装 Java。

在 CubeMX 里的操作:

  1. 新建工程,器件选STM32F103C8;
  2. 引脚图上把PB0、PA0、PC13设为GPIO_Output,把PB1设为GPIO_EXTI1;
  3. System Core → GPIO里把 PB1 的 Mode 设为External Interrupt Mode with Falling edge trigger detection(下降沿)、Pull 设为Pull-up;
  4. System Core → NVIC勾选EXTI line1 interrupt;
  5. System Core → SYS的 Debug 选Serial Wire(不设的话 SWD 下载口可能被关掉,之后烧不进去);
  6. 时钟:因为板子外部晶振起振不了(见实验二踩坑),不启用 HSE,直接用内部 HSI 时钟;
  7. Project Manager填工程名、路径(别带中文和空格),Toolchain 选MDK-ARM,点GENERATE CODE。


💡 CubeMX 生成完会弹窗问是否打开,点Open Project就用 Keil 打开了。工程里文件一大堆,我们只需要改main.c(和中断相关的stm32f1xx_it.c是自动生成的,不用动)。

4. HAL 库代码

生成的main.c里,只需在USER CODE区域补几段(放在这些区域里,重新生成时不会被覆盖):

① 全局标志(USER CODE BEGIN PV):

volatileuint8_tg_paused=0;/* 0=运行 1=暂停 */volatileuint32_tg_last_press=0;/* 上次按键时间,用于消抖 */

② 可暂停延时(USER CODE BEGIN 0):

/* 带暂停功能的延时:等待 nms 毫秒,期间若按下暂停则原地等待直到恢复 */staticvoidPauseable_Delay(uint32_tnms){uint32_tstart=HAL_GetTick();while(HAL_GetTick()-start<nms){while(g_paused)/* 暂停:冻结在当前灯,直到按键恢复 */{}}}

③ PB0 初始化(MX_GPIO_Init里,注意必须是输出):

/*Configure GPIO pin : PB0 */GPIO_InitStruct.Pin=GPIO_PIN_0;GPIO_InitStruct.Mode=GPIO_MODE_OUTPUT_PP;/* 千万别是 GPIO_MODE_IT_RISING */GPIO_InitStruct.Pull=GPIO_NOPULL;GPIO_InitStruct.Speed=GPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(GPIOB,&GPIO_InitStruct);

④ 主循环(USER CODE BEGIN 3):

while(1){/* 第1拍:绿灯亮 */HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_SET);HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_SET);Pauseable_Delay(1000);/* 第2拍:蓝灯亮 */HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_RESET);Pauseable_Delay(1000);/* 第3拍:红灯亮 */HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_SET);HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_SET);Pauseable_Delay(1000);}

⑤ 按键中断回调(USER CODE BEGIN 4,带 50ms 消抖):

voidHAL_GPIO_EXTI_Callback(uint16_tGPIO_Pin){uint32_tnow;if(GPIO_Pin==GPIO_PIN_1)/* 按键 PB1 */{now=HAL_GetTick();if(now-g_last_press>50)/* 50ms 内只认一次,滤掉抖动 */{g_last_press=now;g_paused=!g_paused;/* 按一下:暂停/恢复切换 */}}}

💡EXTI1_IRQHandler是 CubeMX 自动生成在stm32f1xx_it.c里的,它会调用HAL_GPIO_EXTI_IRQHandler(),最终转到上面的回调函数,无需手写。

5. 实验现象

  • 上电后流水灯正常跑:绿灯 → 蓝灯 → 红灯,每 1 秒切换;
  • 按一下按键:流水灯立即停在当前那盏灯上(暂停);
  • 再按一下:从停的位置继续往后闪(恢复)。

四、这两次实验踩过的坑

现象原因解决
逻辑分析仪加信号报 Unknown SignalKeil 仿真器不模拟 GPIO 外设,PORTB.0是内存映射寄存器用普通全局变量当「软信号」观察
仿真报 access violation at 0x40021000仿真器默认没映射外设地址段MAP 0x40000000, 0x400FFFFF READ WRITE
烧录报 Connection refused due to device mismatch板子芯片是国产仿片 CS32F103C8T6(ID 0x2BA01477)Debug→Settings→Pack 取消 Enable 校验
真板延时比 1 秒长 9 倍外部 8MHz 晶振没起振,退回内部 8MHz HSIDELAY_LOOP_PER_MS改小,或改用 HSI 时钟
下载后要按复位才运行没勾 Reset and RunDebug→Settings→Flash Download 勾选 Reset and Run
编译报 Failed to check out a license(compiler5)ARM Compiler 5 的 license 失效换成 ARM Compiler 6(V6)
烧录后只有蓝灯亮流水灯代码没加进主循环,停在初始状态把代码加进USER CODE 3
PB0 不亮CubeMX 里 PB0 误配成中断输入改成 GPIO_Output /GPIO_MODE_OUTPUT_PP
按键不灵敏、只能停红灯① 按键抖动;② 暂停判断只在整轮循环开头检查一次① 50ms 软件消抖;② 用可暂停延时替代HAL_Delay

五、实验心得

这两次实验把同一个流水灯用标准外设库和 HAL 库各实现了一遍,加上第二课的寄存器方式,三种写法都走了一遍,收获很大。

最大的体会还是**「越往上越方便,越往下越透明」**。寄存器方式每个地址每个位都要自己查手册,写起来最累,但也让我最清楚芯片到底在做什么;标准外设库把寄存器包成GPIO_Init这类函数,读起来舒服多了;到了 HAL 库,CubeMX 点几下就生成一整个工程,省事是省事,但出问题时那一层层函数点进去看,反而更晕。所以这门课安排三个实验从底层往上走,是有道理的。

第二个深刻的点是软件延时是真的不可靠。它的周期取决于主频和编译器,只能靠逻辑分析仪实测标定;而 HAL 的HAL_Delay基于 SysTick,天生精确。这一课我还亲身撞上了一个更隐蔽的坑——主频不对延时差 9 倍:板子外部晶振没起振,芯片就静默退回 8MHz 内部时钟,灯一下慢了 9 倍。这让我真正意识到硬件和软件是分不开的,不看板子实际状态、不测时钟,光改代码是没用的。

第三个点是中断和消抖。按键中断让程序不用一直轮询,响应快了很多,但机械按键会抖动,不消抖就「时灵时不灵」。而且我还发现暂停判断的位置很关键:放在整轮循环开头,按下后要等一轮走完才生效,结果「只能停在红灯」;改成在延时里每毫秒检查一次,才真正做到「停在当前灯」。这个 bug 是我自己观察现象一步步定位出来的,印象特别深。

最后还有一个「意外收获」:这一路踩的坑里,有好几个其实不是代码的错——仿片报 device mismatch、ARM Compiler 5 的 license 失效,都会挡住编译下载。这让我学会了一个排查习惯:出问题时先分清是代码错、还是工具/硬件错,能少走很多弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询