简介:ST官方发布的STM32CubeF1固件包V1.8.0,专为STM32F1系列MCU设计,覆盖驱动、板级支持与示例工程的完整开发资源。资源共133个文件,以74个HAL/LL驱动头文件和34个C源码文件为主体,另含HTML说明页、CHM帮助文档、PDF入门指南等,压缩包仅2.8MB。包内整合HAL驱动库与CMSIS核心,提供STM3210C_EVAL、Nucleo-F103RB等评估板BSP代码与示例工程;目录按Drivers、Utilities、Documentation、Third_Party划分,可直接导入Keil MDK、IAR、GCC等主流IDE编译运行。已有445人学习下载,适合STM32初学者快速上手,也适合项目开发中稳定复用ST标准化驱动层。
1. 拿到手就能用的STM32F1固件库V1.8.0:HAL、BSP与示例一次配齐
做STM32F1开发,大部分时间不是耗在写逻辑上,而是在折腾“工程能不能编译”。一套完整固件库里缺一个CMSIS文件,都会让新手摸不着头脑。V1.8.0这个版本把HAL驱动、BSP板级支持、示例工程和文档打包在一起,配合STM32Cube生态,基本做到解压即用,不用东拼西凑。HAL驱动统一外设接口,BSP则管理板级差异,适合从标准外设库转过来的开发者,也适合做课程设计底板的工程模板。
之前帮某开发者调一块F103板子,他手里混着SPL和HAL两代库文件,灯都点不亮。换到V1.8.0完整包后,直接用官方模板,几分钟就有了可复现的起点。它解决的核心问题不是“教你怎么写代码”,而是让你在写业务逻辑之前,先有一个配置正确、编译通过、烧录能跑的工程底座。无论你是手工建工程还是用STM32CubeMX生成,这套包都值得放在本地库里长期使用。
2. 固件库内部结构:HAL驱动、BSP与文档的职责划分
拿到压缩包先别急着复制到工程目录,先把它当一本书翻开看。固件库的目录结构其实就代表了芯片厂商对代码的划分方式:CMSIS管内核,HAL管外设,BSP管板子,Projects管示例。把这四个象限先认清楚,后面所有操作都不会慌。
2.1 解开压缩包后的目录,先认清四块核心
通用结构类似下面这样,具体版本的文件名略有差异:
. ├── Drivers │ ├── CMSIS │ ├── STM32F1xx_HAL_Driver │ └── BSP ├── Middlewares ├── Projects ├── Utilities └── Readme.html这张目录图里有几个区域是经常要和命令行、IDE窗口打交道的。Drivers/CMSIS是内核抽象层,包含Cortex-M3相关的头文件、系统初始化文件以及启动文件,这部分不要随意改,但Include Path里必须加。Drivers/STM32F1xx_HAL_Driver是整套HAL驱动的源码,一切带HAL_前缀的函数都在这,常见外设如gpio.c、uart.c、tim.c、dma.c都在这里。Drivers/BSP是板级支持包,专门封装LED、按键、串口这类板载外设,让你像调库函数一样调板子。Projects下面是各种现成的示例工程,这是新手最容易忽略的区域,其实它比任何教程都有价值。
用表格来划分职责会更直观:
| 目录 | 作用 | 你该关心什么 |
|---|---|---|
| Drivers/CMSIS | 内核头文件、系统启动代码 | 启动文件后缀是hd还是ld,要与芯片容量匹配 |
| Drivers/STM32F1xx_HAL_Driver | 外设驱动源码 | 剪裁开关在stm32f1xx_hal_conf.h里 |
| Drivers/BSP | 板级硬件封装 | 需要定义板卡宏,否则编译会报错 |
| Projects | 模板和示例工程 | 找最小模板比自己建工程快得多 |
需要注意,Projects下的示例工程并不是简单的“Hello World”,而是包含了完整IDE工程配置。复制出来直接用,能避开Include Path、芯片型号、启动文件这类最容易被新手搞砸的问题。我一般会把某个和手头芯片最接近的工程整体复制一份,改改宏定义就开工,而不是从空工程开始一根根加文件。
2.2 为什么是这个版本:HAL库对现代工具链的适配
先明确一点,STM32F1系列早期的官方库是标准外设库(SPL),函数风格是USART_SendData、GPIO_SetBits这一类。而HAL库的接口风格是HAL_UART_Transmit、HAL_GPIO_WritePin,整体设计思路完全不一样。SPL更贴近寄存器操作,直接、高效,但每次配置外设都要手动写好几十行寄存器代码,而且对DMA、中断回调、RTOS支持很弱。HAL库则是在寄存器外面套了一层抽象,把初始化流程收敛到结构体里,再把中断事件的回调暴露给用户,代码可读性高很多。
V1.8.0这个版本在F1系列里属于比较稳定的一个迭代。从它开始,HAL库接口和STM32CubeMX生成的代码做到了高度兼容。也就是说,你可以先用CubeMX图形化配置引脚和时钟,生成一份工程框架,再手动往里面塞业务代码,两边不会“打架”。这是SPL年代做不到的。
| 维度 | SPL | HAL |
|---|---|---|
| API风格 | 直接寄存器操作 | 外设句柄+结构体配置 |
| 中断处理 | 用户自己维护中断标志 | 回调函数机制 |
| 中间件支持 | 较弱 | 较好地支持RTOS、FATFS、USB等 |
| CubeMX生成 | 不支持 | 支持,且生成代码可读 |
| 调试友好度 | 依赖寄存器窗口 | 提供句柄和状态字段 |
所以选V1.8.0并不是因为它“新”,而是因为它正好卡在“功能完整”和“兼容性好”的交汇点上。再往后的版本对F1的改进更多是修bug,但核心接口没有大变。用V1.8.0,你在搜索引擎里能找到最多的资料和踩坑记录,这对独立学习很重要。
2.3 BSP层到底做了什么:从板级差异中解放应用层
BSP板级支持包的作用,是把“这块板子上LED接在哪个引脚”这样的细节从应用代码里剥离出去。比如某评估板上有个LED,硬件上连的是GPIOA的Pin 5,高电平点亮。直接用HAL写的话,你每次都要记得引脚号,还得开GPIO时钟、设置模式。而BSP层会封装成类似BSP_LED_Init和BSP_LED_On这样的函数,应用层只需要关心“这是第几号LED”。
看一段典型调用:
#include "bsp.h" void led_demo(void) { BSP_LED_Init(LED1); BSP_LED_On(LED1); }这段代码里,BSP_LED_Init内部做了什么?它会打开GPIO时钟、把相应引脚配置为推挽输出、设置初始电平。BSP_LED_On则直接往GPIO数据寄存器写值。好处是,如果你的硬件平台换了,只要换一套BSP实现,上面这段led_demo完全不用动。对做产品原型的人来说,这就是板级差异隔离出来的底气。
但要注意,BSP不是万能黑匣子。你用自己的板子时,必须修改BSP驱动里的引脚宏和时钟宏。打开BSP头文件,能看到大量“#define LED1_PIN GPIO_PIN_5”这样的定义,把每个引脚宏改成你板子实际的连接即可。另外一个坑是编译器的预定义宏,BSP头文件通常会根据一个板卡宏选择加载哪一套引脚映射。如果你没定义这个宏,编译会直接失败,提示“Unknown board”之类。
3. 搭一个能跑能调的工程:从官方示例到最小HAL工程
这一章解决“手里有库,但不知道怎么落到IDE里”的问题。两种路径都会讲到:一条是用现成的示例工程快速跑起来,适合验证环境;另一条是手工建一个最小HAL工程,适合理解库的组成和依赖关系。两条路不建议只走一条,至少都要过一遍。
3.1 用现成示例三分钟跑通点灯
最快上手的方式,是从压缩包的Projects目录里找一个和手头芯片型号最接近的工程。比如手头是STM32F103ZE芯片,就找工程名里带“F103ZE”或“F103ZET6”的文件夹。复制到工作区后,用搭建好的IDE打开工程文件。
操作步骤如下:
- 在Projects目录里,按板卡文件夹进入,找一个名为“GPIO_EXTI”或“LED_Blink”的示例工程。
- 复制整个文件夹到你的工作区,不要直接在压缩包里打开。
- 用IDE打开后缀为.uvprojx的工程文件。
- 打开工程选项,确认Device型号和你的芯片一致。
- 编译,若报错,优先检查“Include Paths”是否包含了Drivers下的三个路径。
- 连接调试器,直接下载运行。
这里面最容易翻车的,是工程里的芯片型号和实际不符。比如工程原本写的是F103VET6,而你手里是F103RET6,两个芯片Flash容量不同,启动文件也不同,强行烧录轻则程序跑飞,重则调试器连接不上。正确的做法是先查芯片丝印,再在IDE里把Device型号改对。有些IDE会在改型号时提醒你重新选择启动文件,这个是好事,别点掉。
另外,如果下载器是ST-Link,注意在Debug选项卡里把Flash Download的Programming Algorithm也改成匹配的型号。很多时候明明编译过了,一按下载按钮就报“RDDI-DAP Error”,一大半原因是这里选错了芯片算法。
3.2 手写最小HAL工程:没有IDE模板也能建
用官方示例跑通只能算热身,手工建工程才是真正理解库结构的机会。以MDK为例,新建工程后不要选任何“Device Support Library”,而是手动添加固件库源码。一个能点灯的最小工程至少需要以下文件清单:
| 文件类型 | 必须包含 | 说明 |
|---|---|---|
| 启动文件 | startup_stm32f103x.s | 在CMSIS/Device/Support/Startup里 |
| 系统时钟 | system_stm32f1xx.c | 提供SystemInit和SystemCoreClock |
| 内核头文件 | core_cm3.h、stm32f1xx.h | CMSIS核心定义 |
| HAL驱动 | stm32f1xx_hal.c | HAL_Init和时基相关 |
| 外设驱动 | stm32f1xx_hal_gpio.c | 至少要有GPIO驱动 |
| 配置文件 | stm32f1xx_hal_conf.h | 模板修改而来 |
然后写一个最简main.c,用HAL库点亮一颗灯:
#include "stm32f1xx_hal.h" int main(void) { HAL_Init(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_5; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); while (1) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); HAL_Delay(500); } }这段代码只有一个目标:证明你的库、启动文件、时钟和GPIO配置全部是通的。HAL_Init做了三件事:初始化FLASH接口、配置SysTick作为时间基准、使能全局中断。GPIO初始化结构体里,Mode用的是GPIO_MODE_OUTPUT_PP,代表推挽输出,这是驱动LED最常用的模式;Speed设置为LOW,对几mA级别的LED够用了。最后进入主循环,用HAL_GPIO_TogglePin翻转灯的状态,HAL_Delay实现毫秒延时。
注意,这段代码没有配置PLL,芯片默认使用内部HSI时钟,主频8MHz。点灯没问题,但串口波特率之类的就别指望准了。接下来就是要把系统时钟配到72MHz。
3.3 时钟树设置:翻车最多的地方之一
STM32F1的时钟树说难不难,说简单但也有一堆细节。最常见的外部晶振方案是8MHz晶振,经PLL倍频9倍得到72MHz系统时钟。配置代码常写成这样:
void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; 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; HAL_RCC_OscConfig(&RCC_OscInitStruct); RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2); }这段代码把HSE当PLL输入源,8MHz经过9倍频后变成72MHz。AHB不分频,所以HCLK是72MHz。APB1最高只能到36MHz,所以必须二分频;APB2可以保持72MHz不变。FLASH等待周期设置为2个周期,是因为72MHz下Flash访问至少要两个等待周期。参数照抄通常没问题,但有两个地方要特别留意。
第一个是HSEPredivValue。部分F1芯片(如F105/F107)的PLL输入源前有一个预分频器,如果你不小心配成了DIV2,实际倍频出来就是36MHz,还查不到原因。第二个是HSE_VALUE宏,这个宏在stm32f1xx.h中定义,默认是8000000(8MHz)。如果你板子上焊的是12MHz晶振,光把HSE_ON改成RCC_HSE_ON还不够,还得把HSE_VALUE改成12000000,否则HAL库在计算波特率、延时的时候会按错误时钟算,结果就是串口乱码、延时不准。这类跑偏问题几乎都指向时钟树。
4. HAL驱动实战:GPIO、定时器与串口三件套
工程能编译、灯能闪之后,就可以开始写真正的外设逻辑了。GPIO、定时器、串口是BSP和大多数应用绕不开的三件套。这一章用三段可运行代码,把HAL库的常用姿势讲透。每个代码块后都会解释参数意图和常见变体。
4.1 GPIO:轮询按键与LED翻转
按键和LED是GPIO最典型的输入输出场景。先给出初始化代码:
void gpio_init(void) { __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; // 按键接在PC13,对地触发,所以用上拉输入 GPIO_InitStruct.Pin = GPIO_PIN_13; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); // LED接在PA5,推挽输出 GPIO_InitStruct.Pin = GPIO_PIN_5; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); }主循环里读取按键状态并翻转LED:
while (1) { if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET) { HAL_Delay(30); // 简单消抖 if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); while (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET) { // 等待按键松开 } } } }Pull参数选择GPIO_PULLUP,是因为按键通常把引脚拉低到地,内部上拉可以保证空闲时引脚为高电平。如果板子设计是按键接电源、引脚内部下拉,那Pull参数应改成GPIO_PULLDOWN。Mode这里要注意区分GPIO_MODE_OUTPUT_PP(推挽输出)和GPIO_MODE_OUTPUT_OD(开漏输出)。驱动LED通常用推挽,驱动I2C这种需要“线与”的场合才用开漏。GPIO_Speed可以考虑提升到HIGH,如果LED驱动电流大或引脚翻转频率高。低频点灯用LOW没毛病,但复用成串口、定时器时,建议设置成HIGH。
4.2 定时器:1ms中断与PWM呼吸灯
HAL库用定时器的逻辑是先初始化句柄,配置时基,再配置通道。下面以TIM3为例,实现1ms定时中断:
TIM_HandleTypeDef htim3; void timer_init(void) { __HAL_RCC_TIM3_CLK_ENABLE(); htim3.Instance = TIM3; htim3.Init.Prescaler = 72 - 1; htim3.Init.Period = 1000 - 1; htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(&htim3); HAL_TIM_Base_Start_IT(&htim3, TIM_CHANNEL_ALL); }时钟树配好之后TIM3挂载在APB1上,频率72MHz。预分频Prescaler为71,也就是72分频,得到1MHz计数频率;Period设为999,表示计数到999后溢出,每次溢出间隔1ms。这是72MHz主频下最常见的1ms时基组合。中断回调函数写在别处:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM3) { // 每1ms进来一次 } }如果要做呼吸灯,要配置PWM模式。PWM不是用Base初始化,而是用OC初始化:
void pwm_init(void) { __HAL_RCC_TIM3_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_6; // TIM3_CH1 GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); htim3.Instance = TIM3; htim3.Init.Prescaler = 72 - 1; htim3.Init.Period = 2000 - 1; htim3.Init.CounterMode = TIM_COUNTERMODE_UP; HAL_TIM_PWM_Init(&htim3); }PWM输出频率和分辨率由预分频和周期共同决定。例如预分频72-1,计数时钟就是1MHz,ARR为1999时PWM频率是500Hz,占空比分辨率2000级。设置占空比用:
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 1000);在循环里缓慢改变比较值就能看到呼吸效果。这里要注意GPIO_MODE_AF_PP是复用推挽模式,不是普通输出。如果你忘了改模式,引脚上永远看不到波形。
4.3 串口:中断接收一帧数据
串口是HAL库回调机制最典型的外设。初始化如下:
UART_HandleTypeDef huart1; void uart_init(void) { __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; HAL_UART_Init(&huart1); HAL_UART_Receive_IT(&huart1, &rx_data, 1); }发送数据时直接调用:
HAL_UART_Transmit(&huart1, (uint8_t *)"hello\r\n", 7, 1000);接收一个字节采用中断方式,必须在初始化最后调用HAL_UART_Receive_IT,使能接收中断。注意这个函数的第三个参数是“要接收的字节数”,如果传1就是每收到一个字节触发一次回调。回调函数实现:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { rx_buffer[rx_index++] = rx_data; HAL_UART_Receive_IT(&huart1, &rx_data, 1); // 继续接收 } }很多新手第一次写中断接收,会发现只能收到第一个字符,原因就在于HAL_UART_Receive_IT是一次性的,接收完成后中断被关闭。必须在回调里重新调用,才能继续接收下一帧。这里我把rx_data定义成局部或全局变量,看你的数据用途,如果是全局变量,注意加volatile修饰,避免编译器优化时读不到最新值。
5. 避坑/常见问题:用HAL库翻车的那几次
HAL库用顺手之后,一些坑会反复出现。这里的每一条都是从实际调试现场扒出来的,不是理论推演。每一条都按“现象→原因→解决”的顺序写,读者可以当查错手册用。
5.1 编译与链接期:魔鬼藏在配置里
现象:编译时报“No such file or directory”,而且指向的是stm32f1xx_hal_gpio.h这类头文件。或编译时大量“macro redefined”警告,缝缝补补加了一堆路径还是报错。
原因:工程里Include Path没加全,比如只加了Drivers/STM32F1xx_HAL_Driver/Inc,却没加CMSIS的Include目录。另一类是工程里同时引用了老SPL库和HAL库,两个库里存在同名宏或同名函数,预处理阶段就会出现宏重定义,进入链接阶段则可能报一堆“redefined”。
解决:以V1.8.0压缩包里的官方示例工程为准,把Include Path加上以下四个路径:CMSIS核心头文件目录、CMSIS设备目录、HAL驱动Inc目录、HAL驱动Inc/Externals目录。同时检查工程里有没有添加过任何以“stm32f10x_”开头的标准外设库文件,如果有,全部移除,只保留HAL库那一套。经验是:不要在现有老工程上打补丁,重新从官方模板复制一份更省时。
现象:编译通过,但下载后程序一上电就进入HardFault_Handler。
原因:启动文件与芯片容量不匹配。STM32F1启动文件分为md(中等容量)、hd(高容量)、xl(超大容量)等。如果你用F103ZET6(512KB Flash)却用了startup_stm32f10x_md.s,向量表偏移不对,中断全部错位。
解决:去工程里确认启动文件后缀。F103C8T6(64KB)用md生,F103ZET6和F103RET6用hd生,F103ZGT6这类1MB Flash用xl生。改完启动文件后,编译,再烧录,大概率HardFault消失。
现象:BSP相关源文件一编译就报“#error Unknown board”,或者有几十行错误指向某个未知的宏。
原因:BSP层通常通过一个板卡选择宏来决定编译哪一块硬件映射。比如“U813”这种开发板宏没在IDE里定义,BSP头文件就不知道该如何配置引脚,于是主动报错。
解决:在工程选项的C/C++选项卡里,找到Preprocessor Symbols,在Defines栏加上你的板卡标识。具体宏名去Drivers/BSP目录下翻头文件,看它#error前面的条件是#if defined哪个宏。照着补上就能编译过。
5.2 运行期:行为异常的玄学与修复
现象:调用HAL_Delay(500),实际延时快得离谱或者慢得几乎停在那里。串口波特率也不对,示波器看到的波形频率偏了大几十倍。
原因:九成是因为SystemCoreClock变量和真实时钟不一致。如果工程里实际配置了PLL到72MHz,但SystemCoreClock没有更新,或者HSE_VALUE写的8MHz但板上晶振是12MHz,那么HAL库计算出来的时间基准就全错了。HAL_Delay内部是用SysTick计数器减去到达值实现的,时钟基准错了,延时自然不准。
解决:在SystemClock_Config函数末尾,手动打印或调试查看SystemCoreClock是否为72000000。如果不是,检查stm32f1xx.h里的HSE_VALUE宏,再检查RCC配置中PLLMUL。还有一个省事做法:从官方示例工程复制system_stm32f1xx.c,里面默认覆盖了HSE到PLL的重映射逻辑,很多时钟问题会直接消失。
现象:串口中断接收,第一帧数据完整,后面只能收到一个字节或完全无响应。
原因:HAL_UART_Receive_IT的设计是单次模式。当接收中断触发并进入回调时,接收通道已经关闭,必须由用户在回调里再次调用HAL_UART_Receive_IT来重新打开。如果回调里没写,就只会收到第一帧。
解决:在HAL_UART_RxCpltCallback里,每次处理完数据后,立刻重新调用一次HAL_UART_Receive_IT(&huart1, &rx_data, 1)。注意调用前确认全局接收变量没有被业务代码写坏。另外,如果开了多个串口,回调里要根据huart->Instance区分数据来源,避免串口1的数据被存进串口2的缓冲区。
6. 进阶:把固件库变成你的基础设施
如果你已经能用V1.8.0这套包独立建工程、调外设,那可以再往前走一步:把它和STM32CubeMX、模块裁剪、错误处理结合起来,让固件库成为你长期复用的基础设施,而不是每次开发都从零折腾的碎石堆。
6.1 用CubeMX生成工程,再手动合并HAL代码
STM32CubeMX能图形化配置引脚、时钟、外设初始参数,然后生成HAL工程基础。强烈建议生成的CubeMX工程和V1.8.0包保持一致的库版本。做法是:在CubeMX里选择“从本地固件包安装”,指定到V1.8.0的压缩包解压目录,这样生成的代码路径里引用的库函数和本包完全对齐。
生成后不需要再手动写SystemClock_Config和GPIO_Init,CubeMX已经帮你写好了。你的业务代码放在main.c中标记“USER CODE BEGIN”和“USER CODE END”之间,这样即使你改了引脚定义再重新生成,业务代码也不会被覆盖。这个技巧我用了很多年,生成代码和手写代码互不干扰。
6.2 裁剪HAL库:只编译用到的模块
HAL库全量编译会拖慢构建时间,并且烧写到芯片里也占Flash空间。V1.8.0的stm32f1xx_hal_conf.h里有一排模块使能宏,想裁哪个就注释哪个:
#define HAL_MODULE_ENABLED #define HAL_GPIO_MODULE_ENABLED #define HAL_UART_MODULE_ENABLED #define HAL_TIM_MODULE_ENABLED // #define HAL_SPI_MODULE_ENABLED // #define HAL_I2C_MODULE_ENABLED // #define HAL_DMA_MODULE_ENABLED注释掉后,对应的stm32f1xx_hal_spi.c等文件就不会被编译进工程,链接时体积能小不少。注意别把用到的模块注释掉,否则编译会报“未定义引用”。我一般把常用模块全开着,到项目中期不再改外设分配后,再集中裁剪一次,把编译时间从那几十秒压下来。
这套V1.8.0固件库我前前后后用了三年多。后来我养成了一个习惯:每个新板子到手,不管需求多急,都强制先跑一遍官方模板工程,确认HAL版本、启动文件和芯片型号匹配,再开始写业务代码。这个习惯救过我很多次,尤其是深夜调板子到心态崩溃的时候,一个能稳定复现的工程就是最好的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取