1. 为什么你需要一个“干净”的HAL库工程
如果你刚开始接触STM32,或者刚从标准库(Standard Peripheral Library)转向HAL库,你可能会被STM32CubeMX生成的庞大工程吓到。它确实方便,一键生成,但里面文件繁多,结构复杂,对于新手来说,就像走进了一个装满精密仪器的房间,你知道它能干活,但不知道从哪下手,也不敢乱动,生怕碰坏了什么。
我自己刚开始用HAL库时,也依赖CubeMX。但很快我就发现,当我想深入理解一个外设的初始化流程,或者想优化代码结构时,面对那个自动生成的工程,总有种“隔靴搔痒”的感觉。文件之间的依赖关系不清晰,很多配置散落在各个角落。更重要的是,如果你想在不同的项目间复用自己精心编写的驱动模块,从CubeMX工程里剥离出来非常麻烦。
所以,我决定回归“原始”,从零开始,手动搭建一个最精简、最清晰的HAL库工程。这个过程就像自己组装一台电脑,从选择每一个零件(文件)开始,到最终点亮(编译通过)。这么做的好处太多了:首先,你对工程的每一个文件、每一条编译选项都了如指掌,出了问题能快速定位;其次,工程结构完全由你掌控,可以设计出最适合自己或团队开发的目录规范;最后,这个工程模板一旦建立,就是属于你自己的“瑞士军刀”,后续任何新项目都可以基于它快速开始,效率倍增。
这个指南,就是带你一起完成这次“组装”。我们不依赖任何自动化工具,就用最“笨”但最有效的方法,在Keil MDK这个经典的环境里,构建一个专属于你的、高效且易于维护的STM32 HAL库开发地基。不用担心复杂,我会一步步拆解,把每个环节都讲透。
2. 战前准备:理清思路与备齐“弹药”
动手之前,我们得先想清楚这个工程最终要长什么样,以及需要准备哪些材料。盲目开始只会导致中途不断返工。
2.1 规划你的工程目录结构
一个清晰、合理的目录结构是高效开发的基石。它能让你的代码模块化,便于管理和团队协作。我经过多个项目的迭代,总结出了一个非常实用的结构,你可以直接参考:
My_STM32_Project/
├── Core/
│ ├── Inc/ # 核心头文件:启动文件、系统文件、中断文件等
│ └── Src/ # 核心源文件
├── Drivers/
│ ├── HAL_Driver/ # 存放从ST官方包拷贝的HAL库源文件(.c/.h)
│ └── BSP/ # 板级支持包,你的LED、按键、屏幕等硬件驱动
├── Middlewares/ # 中间件,如FatFS、FreeRTOS、USB库等(后续扩展用)
├── User/
│ ├── Inc/ # 用户应用头文件
│ └── Src/ # 用户应用源文件,main.c就在这里
├── Project/
│ └── MDK-ARM/ # Keil工程文件(.uvprojx)及其输出文件(.axf, .hex)
├── Docs/ # 项目文档
└── Utilities/ # 工具脚本,如批处理清理脚本
这个结构的关键在于分离。Core和Drivers/HAL_Driver里的东西,一旦确定基本不会动,它们是ST官方提供的“地基”。Drivers/BSP和User里的才是你发挥创意的地方。Project目录只放工程相关文件,编译生成的中间文件也在这里,这样你用一个简单的Clean.bat脚本就能一键清理,保持仓库干净。
2.2 下载必备的软件与资料包
工欲善其事,必先利其器。我们需要三样东西:
- Keil MDK-ARM开发环境:这个不用说,你得先安装好。建议使用V5版本,兼容性和稳定性都经过时间检验。
- STM32F1的Device Family Pack (DFP):这是Keil用来认识你芯片的“户口本”。没有它,Keil连STM32F103是谁都不知道。
- STM32CubeF1固件包:这是HAL库的“源代码仓库”,我们需要的所有HAL驱动文件、启动文件、系统文件都从这里取。
获取DFP芯片支持包:
最简单的方法是在Keil软件内直接安装。打开Keil,点击菜单栏的 Pack Installer 图标(那个小盒子)。在打开的窗口中,左侧找到 All Devices -> STMicroelectronics -> STM32F1 Series -> STM32F103 -> STM32F103ZE。选中后,右侧 Packs 选项卡里应该能看到 Keil::STM32F1xx_DFP,点击旁边的 Install 按钮,等待下载安装完成即可。这种方式省去了手动查找的麻烦。
获取STM32CubeF1固件包:
我们需要访问ST的官网。找到 Tools & Software -> Embedded Software -> STM32Cube MCU & MPU Packages 页面。在里面找到 STM32CubeF1 这个包,点击进入其详情页。你会看到一个 Get Latest 或类似版本的下载按钮,通常是一个ZIP压缩包,大小在100MB左右。下载它,这就是我们全部的“原料”。
3. 动手搭建:创建工程与组织文件
现在,让我们在电脑上实际创建这个工程框架,并把必要的文件“搬运”到位。
3.1 创建工程根目录与子文件夹
首先,在你喜欢的位置(比如D:\Projects)创建一个新的文件夹,命名为你的项目名,例如 STM32_HAL_Template。然后,按照我们之前规划的结构,在里面创建好所有子文件夹。你可以直接在资源管理器里手动新建,但我更推荐写一个简单的批处理脚本(create_dirs.bat)来一键生成,方便以后复用。
@echo off
mkdir Core\Inc
mkdir Core\Src
mkdir Drivers\HAL_Driver
mkdir Drivers\BSP
mkdir User\Inc
mkdir User\Src
mkdir Project\MDK-ARM
mkdir Docs
mkdir Utilities
echo 目录结构创建完成!
pause
双击运行这个脚本,骨架就立起来了。
3.2 从固件包中抽取核心文件
接下来是最关键的一步:从下载好的 STM32Cube_FW_F1_Vx.x.x 压缩包中,找出我们工程必需的文件。解压固件包,你会看到一个结构清晰的目录。
第一步:拷贝启动文件。
这是芯片上电后运行的第一段代码,用汇编写的。路径通常在:Drivers\CMSIS\Device\ST\STM32F1xx\Source\Templates\arm。对于STM32F103ZE(大容量),我们需要 startup_stm32f103xe.s 这个文件。把它拷贝到我们工程的 Core\Src 目录下。注意,.s是汇编文件。
第二步:拷贝系统与设备相关文件。
- 头文件:从
Drivers\CMSIS\Device\ST\STM32F1xx\Include拷贝以下文件到Core\Inc:stm32f1xx.h:所有F1系列芯片的通用头文件,包含外设寄存器定义。stm32f103xe.h:针对F103xE系列的具体定义。system_stm32f1xx.h:系统时钟配置的头文件。
- 源文件:从
Drivers\CMSIS\Device\ST\STM32F1xx\Source\Templates拷贝system_stm32f1xx.c到Core\Src。这个文件里包含了SystemInit()函数,负责初始化系统时钟。
第三步:拷贝CMSIS核心文件。
CMSIS是ARM制定的 Cortex-M 处理器通用接口标准。我们需要其核心头文件。从 Drivers\CMSIS\Core\Include 拷贝 core_cm3.h(因为F103是Cortex-M3内核)到 Core\Inc。
第四步:拷贝中断与HAL配置文件。 这些是HAL库运行的“脚手架”。
- 从固件包示例模板里找,例如
Projects\STM3210E_EVAL\Templates。 - 将
Inc目录下的stm32f1xx_it.h(中断头文件)和stm32f1xx_hal_conf.h(HAL库裁剪配置文件)拷贝到Core\Inc。 - 将
Src目录下的stm32f1xx_it.c(中断源文件)拷贝到Core\Src。
第五步:拷贝HAL库驱动文件。
这是HAL库的主体。将整个 Drivers\STM32F1xx_HAL_Driver 文件夹(里面包含 Inc 和 Src)拷贝到我们工程的 Drivers\HAL_Driver 目录下。这样,我们就拥有了所有外设的HAL驱动源码。
至此,文件搬运工作完成。你的 Core 和 Drivers\HAL_Driver 目录应该已经充实起来。其他目录(如User, BSP)暂时空着,等着我们填充自己的代码。
4. 在Keil中构建工程骨架
文件准备好了,现在打开Keil,把它们组织成一个真正的项目。
4.1 新建工程与选择芯片
打开Keil MDK,点击 Project -> New uVision Project...。在弹出的对话框中,导航到你工程的 Project\MDK-ARM 目录,为工程取个名,比如 MyHALTemplate,点击保存。
紧接着会弹出设备选择窗口。因为我们已经安装了DFP包,所以可以在搜索框输入 STM32F103ZE,然后在列表中选择 STMicroelectronics 下的 STM32F103ZE。右侧会显示该芯片的详细描述,确认无误后点击 OK。
随后会弹出一个“Manage Run-Time Environment”窗口,这是Keil RTE环境,用于添加软件组件。对于我们的纯手动工程,直接点击 Cancel 取消即可。 我们要完全自己管理文件。
4.2 创建工程分组与添加文件
一个清晰的工程分组(Group)能让项目管理更直观。在Keil左侧的 Project 窗口中,右键点击 Target 1,选择 Manage Project Items...。
在弹出的对话框中,我们可以创建分组:
-
在
Project Targets页签,可以将Target 1改名为更有意义的名字,如MyHALTemplate。 -
切换到
Groups/ Files页签。这里默认有一个Source Group 1。我们删除它,然后点击上方的New (Insert)按钮,创建与我们目录对应的分组:CoreHAL_DriverBSPUser(Middlewares等可以后续需要时再加)
-
向分组添加文件:
- 选中
Core分组,点击下方的Add Files,导航到Core\Src,选择startup_stm32f103xe.s,system_stm32f1xx.c,stm32f1xx_it.c。注意:.h头文件不要在这里添加! - 选中
HAL_Driver分组,点击Add Files,导航到Drivers\HAL_Driver\Src。这里文件很多,我们不需要全部添加。对于初始模板,可以先添加最常用和必需的,例如:stm32f1xx_hal.cstm32f1xx_hal_cortex.cstm32f1xx_hal_rcc.cstm32f1xx_hal_gpio.c其他外设驱动(如hal_uart.c,hal_spi.c)等用到时再加,避免工程臃肿。特别注意:不要添加文件名中带_template.c的文件,这些是模板,不是最终源码。
- 选中
User分组,添加User\Src目录下的文件(目前还没有,我们可以先创建一个空的main.c加进去)。
- 选中
一个关键设置: 添加完 .s 汇编文件后,需要右键点击它,选择 Options for File...,在 Properties 选项卡中,将 File Type 设置为 ARM Assembler,这样Keil才知道用汇编器来编译它。同样,.c 文件应保持为 C Source file。
4.3 配置关键的工程选项
工程选项是灵魂所在,配置错了就会编译失败。右键点击工程目标(MyHALTemplate),选择 Options for Target...。
-
Target 选项卡:
- 确认芯片型号正确。
Xtal (MHz)可以填你板载外部晶振的频率,比如8.0。这个主要用于软件模拟。Use MicroLIB:强烈建议勾选。这是一个针对嵌入式系统优化的精简C库,可以显著减少代码体积。
-
Output 选项卡:
- 点击
Select Folder for Objects...,将输出路径设置为.\Project\MDK-ARM\Objects。这样所有的.o中间文件都会集中在这里。 - 勾选
Create HEX File,方便后续烧录。
- 点击
-
Listing 选项卡:
- 点击
Select Folder for Listings...,将列表文件输出路径设置为.\Project\MDK-ARM\Listings。
- 点击
-
C/C++ (AC6) 选项卡:(这是最重要的部分)
Define:在这里输入全局宏定义。必须添加USE_HAL_DRIVER和STM32F103xE。USE_HAL_DRIVER:告诉编译器我们要使用HAL库。STM32F103xE:指定我们使用的是大容量型号。这个宏必须和启动文件startup_stm32f103xe.s中的xe后缀匹配。
Include Paths:点击后面的...按钮,添加头文件搜索路径。必须添加以下路径:../Core/Inc../Drivers/HAL_Driver/Inc../User/Inc../Drivers/BSP(为后续准备) 添加时使用相对路径(..表示上一级目录),这样工程移动到别的电脑上也能正常编译。
Language / Code Generation:ARM Compiler选择Use default compiler version 5。AC6编译器更严格,一些HAL库代码可能需要调整,用AC5兼容性更好。
-
Debug 选项卡:
- 根据你的调试器选择,比如
ST-Link Debugger,然后点击旁边的Settings,检查Debug和Flash Download配置是否正确,确保能正常下载和调试。
- 根据你的调试器选择,比如
5. 编写用户代码与解决编译问题
骨架和配置都好了,现在让我们注入“灵魂”——编写用户代码,并解决可能出现的编译错误。
5.1 编写最简单的main.c进行验证
在 User\Src 目录下创建 main.c 文件,并添加到Keil的 User 分组中。我们先写一个最简单的程序,点亮一个LED(假设连接在PB1引脚)。
/* main.c */
#include "main.h" // 我们将创建这个头文件,集中包含常用头文件
#include "stm32f1xx_hal.h" // 通过main.h间接包含
// 简单的毫秒延时函数(软件循环,不精确,仅用于测试)
void Delay_ms(uint32_t ms)
{
for(uint32_t i = 0; i < ms; i++)
{
for(uint32_t j = 0; j < 7200; j++) // 这个循环次数需要根据主频调整
{
__NOP(); // 空操作
}
}
}
// 错误处理函数,HAL库出错时会调用
void Error_Handler(void)
{
while(1) { } // 死循环,实际项目中可以点亮错误灯或重启
}
// 系统时钟配置函数
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
// 配置HSE(外部高速时钟)和PLL(锁相环)
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // 8MHz * 9 = 72MHz
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
// 配置系统时钟(SYSCLK)、AHB、APB1、APB2时钟
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 系统时钟源为PLL
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB时钟 = SYSCLK = 72MHz
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; // APB1时钟 = 36MHz
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2时钟 = 72MHz
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) // Flash等待周期为2
{
Error_Handler();
}
}
int main(void)
{
// 初始化HAL库,会重置所有外设,初始化SysTick定时器
HAL_Init();
// 配置系统时钟到72MHz
SystemClock_Config();
// 初始化GPIOB的时钟
__HAL_RCC_GPIOB_CLK_ENABLE();
// 配置PB1为推挽输出模式
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// 主循环,闪烁LED
while (1)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_1); // 翻转PB1电平
Delay_ms(500); // 延时约500ms
}
}
同时,在 User\Inc 创建 main.h,用于包含常用头文件和声明全局函数。
/* main.h */
#ifndef __MAIN_H
#define __MAIN_H
#ifdef __cplusplus
extern "C" {
#endif
#include "stm32f1xx_hal.h" // 包含HAL库
#include "stm32f1xx_it.h" // 包含中断处理函数声明
// 这里可以放你的全局函数声明或变量声明
void Error_Handler(void);
#ifdef __cplusplus
}
#endif
#endif /* __MAIN_H */
5.2 处理常见的编译与链接错误
第一次编译(点击 Rebuild 按钮)很大概率不会一帆风顺。别慌,我们来逐一破解。
错误1:undefined symbol SystemInit
这个函数在 system_stm32f1xx.c 中定义,但它是弱定义。我们需要确保它被调用。检查 startup_stm32f103xe.s 汇编文件,在复位中断服务程序 Reset_Handler 里,是否有一句 bl SystemInit。通常有。如果没有,可能是文件不对。另外,确保 system_stm32f1xx.c 已添加到工程并正确编译。
错误2:大量HAL库函数未定义
这通常是因为头文件包含路径没设对,或者 USE_HAL_DRIVER 宏没有定义。请返回 4.3节,仔细检查 C/C++ 选项卡下的 Include Paths 和 Define 设置,确保路径正确且宏已添加。
错误3:.s 文件编译报错
确认已按照 4.2节 末尾所述,将 .s 文件的 File Type 设置为 ARM Assembler。
错误4:链接错误,关于 _printf 或 _scanf
这是因为我们使用了标准库函数(如 printf),但链接器找不到实现。我们勾选了 Use MicroLIB,所以需要确保在代码中包含了 stdio.h,并且重定向了 fputc 函数(如果要用串口打印)。对于初始模板,如果不使用 printf,可以暂时不处理。如果使用,需要实现 int fputc(int ch, FILE *f) 函数,将其指向你的串口发送函数。
错误5:HAL_Init 中关于 HAL_NVIC_SetPriorityGrouping 的警告或错误
检查 stm32f1xx_hal_conf.h 文件。找到 #define TICK_INT_PRIORITY 这一行,确保其值是一个有效的优先级(如 0x0F)。STM32F1的中断优先级分组,HAL库默认使用 NVIC_PRIORITYGROUP_4,即4位抢占优先级,0位子优先级。这个配置在 HAL_Init 里设置,通常没问题。
解决完这些错误,再次点击 Rebuild,你应该能看到编译进度条快速滚动,最后在底部的 Build Output 窗口出现 "0 Error(s), 0 Warning(s)" 的字样。恭喜你,一个从零搭建的、纯净的STM32 HAL库工程模板诞生了!
6. 工程优化与进阶技巧
得到一个能编译的工程只是开始,让它变得“好用”和“专业”才是目标。
6.1 创建实用的批处理脚本
在工程根目录下创建两个批处理脚本,会极大提升效率。
Clean.bat:一键清理编译生成文件。
@echo off
echo 正在清理Keil生成文件...
if exist .\Project\MDK-ARM\Objects ( rmdir /s /q .\Project\MDK-ARM\Objects )
if exist .\Project\MDK-ARM\Listings ( rmdir /s /q .\Project\MDK-ARM\Listings )
del /q .\Project\MDK-ARM\*.axf .\Project\MDK-ARM\*.hex .\Project\MDK-ARM\*.bin .\Project\MDK-ARM\*.map 2>nul
echo 清理完成!
pause
在提交代码到Git或打包项目前运行一下,可以清除所有中间文件,保持仓库清洁。
OpenProject.bat:快速打开工程。
@echo off
start .\Project\MDK-ARM\MyHALTemplate.uvprojx
把工程文件夹扔在桌面,双击这个脚本就能直接打开Keil工程,比一层层点进去方便多了。
6.2 配置HAL库与裁剪
stm32f1xx_hal_conf.h 这个文件是HAL库的“配置中心”。你可以通过注释或取消注释里面的 #define 来启用或禁用特定的外设驱动。例如,如果你的项目只用GPIO和UART,那么可以把 HAL_ADC_MODULE_ENABLED、HAL_SPI_MODULE_ENABLED 等都注释掉。这样做有两个好处:一是编译速度更快,二是最终生成的代码体积更小。记得,用哪个外设,就使能哪个模块。
6.3 建立自己的板级支持包
在 Drivers/BSP 目录下,开始建立你自己的硬件抽象层。例如,创建 BSP_LED.c 和 BSP_LED.h,将初始化GPIO、点亮、熄灭LED的操作封装成 BSP_LED_Init(), BSP_LED_On(), BSP_LED_Off() 这样的函数。同样,为按键、蜂鸣器、特定的传感器都创建独立的BSP模块。
这样做的好处是硬件无关性。你的应用层代码(在 User/Src 里)只调用 BSP_LED_On(LED1),而不关心LED1具体连在哪个GPIO引脚上。如果哪天你换了一块板子,只需要修改 BSP_LED.c 里的引脚定义,应用层代码一行都不用改。这是写出可移植、易维护代码的关键。
6.4 版本管理与文档
立即为这个工程模板初始化一个Git仓库(在根目录执行 git init)。将 Core, Drivers/HAL_Driver, User/Inc, User/Src/main.c 等你自己创建和修改的核心文件提交进去。而 Project/MDK-ARM 目录下的 .uvprojx 等工程文件,因为包含本地绝对路径信息,建议添加到 .gitignore 文件中,或者团队成员约定好使用相对路径配置后也可纳入管理。
在 Docs 目录下,简单写一个 README.md,说明这个工程的结构、编译方法、以及主要功能。好记性不如烂笔头,也为日后可能的团队协作打下基础。
走到这一步,你已经不仅仅是一个HAL库的使用者,更是一个拥有自己强大“开发武器库”的创造者。这个亲手搭建的工程模板,会成为你未来所有STM32项目的坚实起点。每次开启新项目,只需复制这个模板,修改芯片型号和BSP驱动,就能迅速进入业务逻辑开发,把时间花在真正创造价值的地方。
5869



被折叠的 条评论
为什么被折叠?



