STM32 实验任务 2:标准外设库流水灯 + Keil 逻辑分析仪测波形
任务要求
在实验 1(寄存器方式流水灯)的基础上,改用STM32 标准外设库方式,使用 GPIOx 端口控制几个 LED 轮流闪烁,间隔 1 秒。要求:
写出工程项目创建、添加标准外设库文件(.c/.h)的详细过程;
用 Keil 软件仿真逻辑分析仪观察 GPIO 输出波形,分析时序是否正确、LED 闪烁周期实际为多少。
问 1:标准外设库工程怎么建?
1.1 准备工作:下载标准外设库
STM32F1 的标准外设库(STM32F10x_StdPeriph_Driver)从官方渠道下载后解压,里面Libraries目录有两大块:
CMSIS:芯片内核相关(core、设备头文件 stm32f10x.h、启动文件)STM32F10x_StdPeriph_Driver:外设驱动(gpio、rcc、flash、usart 等,每个外设一个 .c + .h)
注意:本项目只用到了 GPIO、RCC、FLASH 三个外设,所以只把这三个驱动文件复制进工程,其他外设不添加,工程更干净。
1.2 创建工程文件夹
我在D:\Project\STM32Project\下创建工程目录:
stm32-led-stdlib/ ├── main.c ← 用户主程序 ├── stm32f10x\_conf.h ← 外设配置头文件(声明启用哪些外设) └── StdPeriph/ ← 标准外设库驱动文件 ├── stm32f10x\_gpio.c / .h ├── stm32f10x\_rcc.c / .h └── stm32f10x\_flash.c / .h文件夹的作用:
StdPeriph/专门放标准外设库的驱动源码,和用户代码分开,结构清晰;stm32f10x_conf.h是标准库的 “总开关”,它 include 哪些外设头文件,工程就能用哪些外设。
1.3 新建 Keil 工程
Keil →
Project → New uVision Project→ 定位到stm32-led-stdlib文件夹,工程名main→ 保存;选择芯片STM32F103C8;
弹出 Manage Run-Time Environment 时,勾选:
CMSIS → CORE(提供内核头文件 core_cm3.h)Device → Startup(提供启动文件和系统时钟初始化文件)点 OK。
1.4 添加源文件
左侧Source Group 1上右键 →Add Existing Files to Group...,添加 4 个文件:
main.cStdPeriph\stm32f10x_gpio.cStdPeriph\stm32f10x_rcc.cStdPeriph\stm32f10x_flash.c
1.5 配置宏和头文件路径(重点!)
点魔术棒(Options for Target)→C/C++页:
| 配置项 | 值 | 说明 |
|---|---|---|
| Define | USE_STDPERIPH_DRIVER | 让 stm32f10x.h 自动包含 stm32f10x_conf.h |
| Include Paths | .\;.\StdPeriph | 告诉编译器去哪找头文件 |
| Language C | c99 | CMSIS 6.x 内核头文件要求 |
1.6 踩坑记录(编译报错排查)
坑 1:宏名写错 → 20 个错误
第一次编译报了一堆GPIO_InitTypeDef、GPIO_Pin_0未定义的错误。原因是:很多网络教程写的是USE_STDPERIPH_DEVICE,但我这台 Keil(STM32F1xx_DFP 2.4.0 包)里的 stm32f10x.h 实际要求的是:
\#ifdef USE\_STDPERIPH\_DRIVER   #include "stm32f10x\_conf.h" \#endif宏名必须改成USE_STDPERIPH_DRIVER,标准库的所有定义才能被包含进来。
坑 2:conf.h 引用不存在的头文件
宏改对后又报stm32f10x_adc.h file not found。原因:官方模板stm32f10x_conf.h默认 include 了全部外设头文件(adc、can、dac……),而我工程里只添加了 gpio/rcc/flash 三个驱动。解决:把 conf.h 精简,只保留用到的三个:
\#include "stm32f10x\_flash.h" \#include "stm32f10x\_gpio.h" \#include "stm32f10x\_rcc.h"编译通过,0 Error。
1.7 流水灯代码(标准外设库版)
\#include "stm32f10x.h" void delay\_ms(unsigned int ms) // 软件延时 {   volatile unsigned int i, j;   for (i = 0; i < ms; i++)   for (j = 0; j < 5000; j++) {} } int main(void) {   GPIO\_InitTypeDef GPIO\_InitStructure;   // 1. 使能 GPIOA/B/C 时钟   RCC\_APB2PeriphClockCmd(RCC\_APB2Periph\_GPIOA | RCC\_APB2Periph\_GPIOB |   RCC\_APB2Periph\_GPIOC, ENABLE);   // 2. PA0 红灯   GPIO\_InitStructure.GPIO\_Pin = GPIO\_Pin\_0;   GPIO\_InitStructure.GPIO\_Mode = GPIO\_Mode\_Out\_PP;   GPIO\_InitStructure.GPIO\_Speed = GPIO\_Speed\_50MHz;   GPIO\_Init(GPIOA, \&GPIO\_InitStructure);   // 3. PB0 绿灯(复用同一结构体)   GPIO\_InitStructure.GPIO\_Pin = GPIO\_Pin\_0;   GPIO\_Init(GPIOB, \&GPIO\_InitStructure);   // 4. PC14 蓝灯(PC13/14/15 低速引脚,2MHz)   GPIO\_InitStructure.GPIO\_Pin = GPIO\_Pin\_14;   GPIO\_InitStructure.GPIO\_Speed = GPIO\_Speed\_2MHz;   GPIO\_Init(GPIOC, \&GPIO\_InitStructure);   // 5. PC13 板载灯(低电平点亮)   GPIO\_InitStructure.GPIO\_Pin = GPIO\_Pin\_13;   GPIO\_Init(GPIOC, \&GPIO\_InitStructure);   // 6. 初始全灭   GPIO\_ResetBits(GPIOA, GPIO\_Pin\_0);   GPIO\_ResetBits(GPIOB, GPIO\_Pin\_0);   GPIO\_ResetBits(GPIOC, GPIO\_Pin\_14);   GPIO\_SetBits(GPIOC, GPIO\_Pin\_13);   while (1) {   GPIO\_SetBits(GPIOA, GPIO\_Pin\_0); delay\_ms(1000);   GPIO\_ResetBits(GPIOA, GPIO\_Pin\_0);   GPIO\_SetBits(GPIOB, GPIO\_Pin\_0); delay\_ms(1000);   GPIO\_ResetBits(GPIOB, GPIO\_Pin\_0);   GPIO\_SetBits(GPIOC, GPIO\_Pin\_14); delay\_ms(1000);   GPIO\_ResetBits(GPIOC, GPIO\_Pin\_14);   GPIO\_ResetBits(GPIOC, GPIO\_Pin\_13); delay\_ms(1000);   GPIO\_SetBits(GPIOC, GPIO\_Pin\_13);   } }相比实验 1 的寄存器操作,标准外设库把所有寄存器操作封装成了函数(GPIO_Init、GPIO_SetBits),可读性明显更好。
问 2:软件延时的 “1 秒” 到底准不准?
2.1 思路
LED 亮 / 灭靠delay_ms(1000)软件循环延时。它到底是精确 1 秒,还是存在误差?没有示波器的情况下,可以用Keil 软件仿真的逻辑分析仪直接观察 GPIO 波形、测量时间。
2.2 仿真配置步骤
Target 页设置晶振:Xtal (MHz) 设为
8.0(STM32F103C8T6 外部晶振 8MHz)。这一步很重要,仿真时间计算依赖它。我的 Keil 工程 Xtal 显示<undefined>改不动,直接修改工程文件main.uvprojx里的CLOCK(12000000)为CLOCK(8000000)解决;Debug 页:勾选
Use Simulator+Run to main();Dialog DLL 填DARMSTM.DLL,Parameter 填-pSTM32F103C8;点
Ctrl+F5进入仿真;菜单
View → Analysis Windows → Logic Analyzer打开逻辑分析仪;菜单
View → Symbols Window,找到GPIOA_ODR,拖入逻辑分析仪窗口;点逻辑分析仪里的
Setup:
Display Type 选Bit(按位显示)
And Mask 填
0x00000001(只看 bit0 = PA0)点 Close;
- 按
F5全速运行,观察波形。
2.3 波形分析
PA0 是红灯的控制脚,波形如下(绿色方波):
从波形可以清楚看到:
高电平段(红灯亮):0.879936s ≈0.88s
低电平段(红灯灭,其他三灯轮流亮):约 3 秒
完整周期(两个上升沿之间):3.519741s
验证自洽性:3.519741 ÷ 4 = 0.879935 ≈ 0.879936,正好等于单灯点亮时间—— 说明四盏灯确实是轮流各亮约 0.88s,程序逻辑和波形完全对应。
2.4 结论:软件延时准确吗?
以下是波形图图片数据记录
结论:不准确。实测数据如下:
| 项目 | 理论值 | 实测值 | 误差 |
|---|---|---|---|
| 单灯点亮时间 | 1.000 s | 0.879936 s | 约 12% |
| PA0 完整周期 | 4.000 s | 3.519741 s | 约 12% |
误差原因分析:
软件延时本质是 “数指令周期”:
delay_ms里 5000 这个参数是按经验估的循环次数,而每条循环指令实际占几个时钟周期、volatile 变量访问的内存开销,都和估算有偏差;编译优化等级(-O0)也影响循环开销;
所以软件延时的 “1 秒” 只能做到 “大约 1 秒”,误差在 10% 左右完全正常。
如果想精确到 1 秒,应该改用硬件定时器:SysTick 系统节拍定时器或 TIM 通用定时器(实验任务 3 的 HAL 库方案正是用 SysTick 实现精确延时,可以对比)。
总结
这个实验把 STM32 的开发方式从 “寄存器直接操作” 推进到了 “标准外设库函数调用”,工程从建文件夹、复制库文件、配置宏、到解决两个编译坑(宏名、conf.h),完整走了一遍标准外设库工程的搭建流程。更重要的是,通过 Keil 逻辑分析仪把 “软件延时到底准不准” 这个看不见的问题变成了看得见的波形和数据:实测 0.88s 与理论 1s 的 12% 误差,让我真正理解了为什么嵌入式开发里讲究 “能用定时器就别用软件延时”。