STM32流水灯寄存器、标准库与HAL库三种实现方式对比
一、实验目的
- 掌握直接操作寄存器的方法,理解GPIO端口的寄存器地址、位域含义与配置参数。
- 掌握STM32标准外设库的工程搭建方法与函数式编程思路。
- 掌握HAL库配合STM32CubeMX的快速开发方式,理解外部中断与回调机制。
- 学会使用Keil软件仿真中的逻辑分析仪观察GPIO输出波形,测量高低电平转换周期。
- 通过同一功能在三种开发方式下的实现,理解“寄存器→标准库→HAL库”的封装层次差异。
二、实验设备与软件
| 类别 | 名称 / 型号 | 说明 |
|---|---|---|
| 主控 | STM32F103C8T6 | ARM Cortex-M3内核,LQFP48封装 |
| 主频 | 最高72MHz | 外部8MHz晶振经PLL 9倍频 |
| Flash | 64KB | 起始地址0x0800 0000 |
| SRAM | 20KB | 起始地址0x2000 0000 |
| 开发环境 | Keil MDK5 | 编译器V6.16 |
| 固件库 | STM32F10x StdPeriph Lib V3.5.0 | 标准外设库 |
| 配置工具 | STM32CubeMX | 图形化代码生成器 |
| 下载器 | USB TO TTL | 串口烧录,BOOT0=1 |
三、实验任务一:寄存器方式流水灯
3.1 程序设计思路
直接操作寄存器是最接近硬件本质的编程方式。开发者需手动计算并访问APB2ENR(使能GPIO时钟)、CRL/CRH(配置端口模式与速度)、ODR(输出数据寄存器)等关键寄存器地址。
本次实验使用PA4、PA5、PA6三个外接LED和PC13板载LED,采用低电平点亮方式。
3.2 GPIO寄存器地址
| 寄存器名称 | 物理地址 | 功能说明 |
|---|---|---|
| RCC_APB2ENR | 0x40021018 | APB2外设时钟使能寄存器 |
| GPIOA_CRL | 0x40010800 | 端口A配置低寄存器(PA0~PA7) |
| GPIOA_ODR | 0x4001080C | 端口A输出数据寄存器 |
| GPIOC_CRH | 0x40011004 | 端口C配置高寄存器(PC8~PC15) |
| GPIOC_ODR | 0x4001100C | 端口C输出数据寄存器 |
3.3 代码实现
// 纯寄存器操作,不需要包含标准库头文件voidSystemInit(void){}// 1. 定义寄存器地址#defineRCC_APB2ENR*(unsignedvolatileint*)0x40021018#defineGPIOA_CRL*(unsignedvolatileint*)0x40010800#defineGPIOA_ODR*(unsignedvolatileint*)0x4001080C#defineGPIOC_CRH*(unsignedvolatileint*)0x40011004#defineGPIOC_ODR*(unsignedvolatileint*)0x4001100C// 2. 软件延时函数(volatile防止编译器优化)voidDelay(volatileunsignedintcount){while(count--);}intmain(void){// 3. 开启GPIOA和GPIOC的时钟RCC_APB2ENR|=(1<<2)|(1<<4);// 4. 配置PA4、PA5、PA6为推挽输出,2MHzGPIOA_CRL&=~(0xF<<16);GPIOA_CRL|=(0x2<<16);// PA4GPIOA_CRL&=~(0xF<<20);GPIOA_CRL|=(0x2<<20);// PA5GPIOA_CRL&=~(0xF<<24);GPIOA_CRL|=(0x2<<24);// PA6// 5. 配置PC13为推挽输出,2MHzGPIOC_CRH&=~(0xF<<20);GPIOC_CRH|=(0x2<<20);// PC13// 6. 主循环:流水灯逻辑(低电平点亮)while(1){// 状态一:PA4亮GPIOA_ODR&=~(1<<4);GPIOA_ODR|=(1<<5);GPIOA_ODR|=(1<<6);GPIOC_ODR|=(1<<13);Delay(500000);// 状态二:PA5亮GPIOA_ODR|=(1<<4);GPIOA_ODR&=~(1<<5);GPIOA_ODR|=(1<<6);GPIOC_ODR|=(1<<13);Delay(500000);// 状态三:PA6亮GPIOA_ODR|=(1<<4);GPIOA_ODR|=(1<<5);GPIOA_ODR&=~(1<<6);GPIOC_ODR|=(1<<13);Delay(500000);// 状态四:PC13亮GPIOA_ODR|=(1<<4);GPIOA_ODR|=(1<<5);GPIOA_ODR|=(1<<6);GPIOC_ODR&=~(1<<13);Delay(500000);}}3.4 实验现象
烧录后,四个LED依次循环点亮:PA4 → PA5 → PA6 → PC13 → PA4……
四、实验任务二:标准外设库方式流水灯
4.1 标准库工程搭建过程
与直接操作寄存器相比,标准外设库将底层寄存器操作封装为函数调用,代码更易读、易维护。搭建过程如下:
第一步:建立工程文件夹结构
新建工程文件夹,在其中创建三个子文件夹:
Startup:存放启动文件startup_stm32f10x_md.sLibrary:存放标准库的inc和src文件夹User:存放main.c、stm32f10x_it.c、system_stm32f10x.c等
第二步:在Keil中新建工程并分组
打开Keil,Project → New uVision Project,选择芯片STM32F103C8。通过Project → Manage → Project Items建立Startup、Library、User三个组,分别添加对应文件。
第三步:配置魔术棒
在C/C++选项卡中:
Define框填入:USE_STDPERIPH_DRIVER, STM32F10X_MDInclude Paths添加:.\Library\inc、.\User、.\Library
在Output选项卡中勾选Create HEX File。
4.2 代码实现
#include"stm32f10x.h"// 粗略的软件延时函数voidDelay(__IOuint32_tnCount){for(;nCount!=0;nCount--);}intmain(void){// 1. 开启GPIOA和GPIOC时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOC,ENABLE);GPIO_InitTypeDef GPIO_InitStructure;// 2. 配置PA4、PA5、PA6推挽输出GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_6;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_2MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);// 3. 配置PC13推挽输出GPIO_InitStructure.GPIO_Pin=GPIO_Pin_13;GPIO_Init(GPIOC,&GPIO_InitStructure);while(1){// 状态一:PA4亮(低电平点亮)GPIO_ResetBits(GPIOA,GPIO_Pin_4);GPIO_SetBits(GPIOA,GPIO_Pin_5|GPIO_Pin_6);GPIO_SetBits(GPIOC,GPIO_Pin_13);Delay(800000);// 状态二:PA5亮GPIO_SetBits(GPIOA,GPIO_Pin_4);GPIO_ResetBits(GPIOA,GPIO_Pin_5);GPIO_SetBits(GPIOA,GPIO_Pin_6);GPIO_SetBits(GPIOC,GPIO_Pin_13);Delay(800000);// 状态三:PA6亮GPIO_SetBits(GPIOA,GPIO_Pin_4|GPIO_Pin_5);GPIO_ResetBits(GPIOA,GPIO_Pin_6);GPIO_SetBits(GPIOC,GPIO_Pin_13);Delay(800000);// 状态四:PC13亮GPIO_SetBits(GPIOA,GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_6);GPIO_ResetBits(GPIOC,GPIO_Pin_13);Delay(800000);}}4.3 Keil软件仿真与逻辑分析仪波形分析
4.3.1 配置步骤
- 魔术棒
Target选项卡中,Xtal (MHz) 设置为8.0。 - 魔术棒
Debug选项卡中,选择Use Simulator。 Dialog DLL填写DARMSTM.DLL,Parameter填写-pSTM32F103C8。- 按
Ctrl+F5进入仿真模式。 - 菜单栏
View → Analysis Windows → Logic Analyzer。 - 点击
Setup,添加信号:PORTA & 0x00000010、PORTA & 0x00000020、PORTA & 0x00000040、PORTC & 0x00002000。 - 将
Display Type改为Bit,按F5运行。
4.3.2 波形分析
从波形图可以观察到:
- 红、绿、蓝、黑四条线分别对应PA4、PA5、PA6、PC13的输出电平。
- 四条线依次错开变低,完美对应流水灯的循环逻辑。
- 根据底部时间轴,一个完整循环的周期约为2~3秒。
五、实验任务三:HAL库 + 按键中断控制流水灯
5.1 STM32CubeMX配置流程
HAL库进一步抽象为HAL_GPIO_Init()与HAL_GPIO_WritePin()等跨系列统一接口,支持CubeMX图形化配置,极大提升开发效率。
配置步骤:
- 打开STM32CubeMX,新建工程,搜索选择
STM32F103C8T6。 - 配置引脚:PA4、PA5、PA6、PC13设为
GPIO_Output;PA0设为GPIO_EXTI0。 - 在
System Core → GPIO中,将PA0模式设为External Interrupt Mode with Falling edge trigger detection。 - 在
System Core → NVIC中勾选EXTI line0 interrupt的Enabled。 - 在
RCC中将HSE设为Crystal/Ceramic Resonator。 - 在
Clock Configuration中将HCLK设为72MHz。 - 在
Project Manager中设置工程名,Toolchain选择MDK-ARM(V5),生成代码。
5.2 核心代码逻辑
全局变量定义(main.c中):
volatileuint8_tis_paused=0;// 0: 运行, 1: 暂停主循环逻辑:
while(1){if(!is_paused){HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOA,GPIO_PIN_5,GPIO_PIN_SET);HAL_GPIO_WritePin(GPIOA,GPIO_PIN_6,GPIO_PIN_SET);HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_SET);HAL_Delay(500);// 后续三个状态类似...}}中断回调函数(main.c底部):
voidHAL_GPIO_EXTI_Callback(uint16_tGPIO_Pin){if(GPIO_Pin==GPIO_PIN_0){HAL_Delay(20);// 消抖if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==GPIO_PIN_RESET){is_paused=!is_paused;// 翻转暂停状态}}}六、三种实现方式的对比分析
| 对比维度 | 寄存器方式 | 标准外设库 | HAL库 |
|---|---|---|---|
| 执行效率 | 最高,直接控制硬件 | 较高,经过轻量封装 | 较低,抽象层较厚 |
| 代码体积 | 最小 | 较小 | 较大 |
| 可读性/可维护性 | 差,需熟记寄存器映射 | 较好,函数命名直观 | 好,跨系列统一接口 |
| 可移植性 | 差,芯片系列间不兼容 | 中等,F1系列内可复用 | 好,F1/F4/F7等系列兼容 |
| 开发效率 | 低,需查手册计算地址和位域 | 中等,需手动搭建工程 | 高,CubeMX一键生成 |
| 学习曲线 | 陡峭,适合深入理解硬件 | 平缓,适合入门学习 | 平缓,适合快速原型开发 |
| 适用场景 | 超低延迟、资源受限场景 | 教学演示、工业应用 | 快速原型、多平台迁移 |
三种方式的本质区别在于封装层次的不同:寄存器是直接操作硬件的最底层;标准库在寄存器之上做了一层函数封装;HAL库则进一步抽象,屏蔽了芯片间的差异,提供了跨系列的统一接口。
七、心得体会
这次实验让我第一次真切地感受到了STM32开发的“三个层次”——寄存器、标准库、HAL库,它们就像是同一件事的三种做法,各有各的门道。
最开始写寄存器代码的时候,说实话是有点痛苦的。那一长串十六进制地址,比如0x40010800、0x40021018,看着就让人头大。配置PA4的时候,得对着参考手册把CRL寄存器的第16到19位翻来覆去地算,还要想清楚MODE和CNF分别是什么值。
到了标准库这一关,挑战从“写代码”变成了“搭环境”。我记得光是找齐那些该添加的.c文件就花了不少时间,第一次编译时Keil直接甩出十几个“file not found”,整个人都懵了。后来才知道,问题出在Include Paths少配了路径,还有一个core_cm3.c因为编译器版本不兼容需要移除。这些坑踩下来,让我真正理解了标准库工程的目录结构和编译逻辑——原来一个看似简单的工程背后,有这么多文件在协同工作。
最让我惊喜的其实是Keil的逻辑分析仪。以前调试只能靠打印和猜,有了波形之后,代码到底在干什么一下子就变得清清楚楚。当四条彩色的方波在屏幕上有节奏地错开,那种把抽象的代码变成看得见的信号的感觉,真的挺神奇的。通过测量波形,我也发现软件延时的实际周期和理论值有偏差,这种“理论归理论,实测归实测”的差距,如果不靠工具去观察,是很难意识到的。
至于HAL库和CubeMX,它给我的感觉更像是一个“加速器”。点几下鼠标,一个完整的工程就生成了,比起手动搭标准库省了太多力气。但我也意识到,正因为HAL库把很多东西都藏起来了,如果不懂底层的寄存器逻辑,出了问题会很茫然。所以这次实验最大的收获,不是学会了三种写法,而是理解了它们之间的关系:寄存器是根基,库是工具,选哪个取决于你想解决什么问题。
总的来说,从寄存器到HAL库,我经历了一个从“看不懂”到“能跑通”再到“能对比”的过程。这个过程虽然磕磕绊绊,但每解决一个问题,对STM32的理解就深一层。
本项目代码已托管至Gitee:https://gitee.com/wujje/stm32-led-test
八、参考链接
- 基于寄存器与基于固件库的stm32 LED流水灯
- STM32中文参考手册_V10.pdf
- STM32标准外设库官方下载地址
- Keil5仿真——示波器显示.pdf
- 利用KEIL的软件仿真的逻辑分析仪功能观察GPIO的波形-2.pdf