简介:面向STM32F1系列开发者的STM32Cube_FW_F1_V1.8.0固件包,定位为官方综合软件框架,适合从入门配置到性能优化的嵌入式项目开发。它将HAL硬件抽象层、LL底层驱动、中间件与示例工程组织在一起,帮助读者减少底层寄存器摸索,把精力放在应用逻辑与产品功能上。压缩包约95.73MB,上游未提供文件总数与类型明细,包内结构以实际解压为准;从描述可知,内容覆盖ADC、CAN、CRC、DMA、GPIO、I2C、SPI、TIM、USART等外设驱动,并包含USB OTG FS/HS、FatFS文件系统、FreeRTOS实时操作系统等中间件及配套应用示例。HAL侧重易用API,LL便于精细化控制,二者可配合用于不同性能要求的场景;STM32CubeMX图形化配置、HAL/LL参考文档和多IDE支持也一并纳入,方便在Keil MDK、IAR与GCC ARM环境中搭建工程。目前已有1096人学习,适合需要系统掌握STM32F1外设开发、中间件集成与工程模板复用的读者按模块查阅和验证。
1. 先不要急着双击那个工程文件
板子焊完,串口线插上,你从资料盘里翻出一个 STM32Cube_FW_F1_V1.8.0.rar,解压出来几百个文件夹:Projects 下面按板子排了几十个工程,Drivers 下面上千个 .c/.h,Middlewares 里还塞着 FatFS、FreeRTOS、USB 两套库。这时候最危险的动作就是随手双击一个 .uvprojx,编译过了皆大欢喜,编译不过连从哪查都不知道。这份包是 STM32F1 系列(F100/F101/F102/F103/F105/F107)的一整套固件集合,含 CMSIS 内核支持、STM32F1xx HAL 驱动源码、LL 驱动、中间件,以及按官方评估板组织的示例工程。它给的不是业务代码,是一套能编译、能对照参考手册跑的驱动地基。适合已经能点灯、准备把工程从寄存器或标准库迁到 HAL 的人,也适合刚拿到 F103 板子想找个能跑通起点的人;想直接抄业务逻辑的,这里没有。
2. 拆包后的骨架:四个一级目录各自管什么
一个包解压出来,第一件事不是看代码,是建立目录地图。地图不清楚,后面每找一个文件都要靠搜索,效率会掉一半。这一章把四个一级目录的职责边界划清楚,再从 一个具体的点灯工程反推出完整依赖链,最后用一个脚本把“启动文件选错”这类低级错误挡在编译之前。STM32Cube_FW_F1 的目录结构在 V1.8.0 这一版已经相当稳定,记住一次,后面换 F4、G0 的包也是同一套逻辑。
2.1 Drivers 不是 SDK,别把它当业务代码改
Drivers 下面分两块。CMSIS 是内核与芯片的基础定义:Include/里是core_cm3.h、cmsis_compiler.h、cmsis_gcc.h这些跟编译器打交道的头文件;Device/ST/STM32F1xx/Include/里是stm32f1xx.h(顶层头文件,靠预编译宏决定往下包含哪个具体型号)、stm32f103xb.h(寄存器结构体与基地址)、system_stm32f1xx.h;Device/ST/STM32F1xx/Source/Templates/下是system_stm32f1xx.c和三套编译器各自的启动文件目录(arm、gcc、iar)。
STM32F1xx_HAL_Driver 是外设驱动。Inc/里stm32f1xx_hal.h是总入口,stm32f1xx_hal_conf_template.h是配置模板,stm32f1xx_hal_def.h是公共宏与状态枚举;Src/里一个外设一个 .c,另外stm32f1xx_hal.c里放着HAL_Init、HAL_Delay、HAL_GetTick、HAL_IncTick这套时基机制。同一层还有一套stm32f1xx_ll_*.h,是寄存器级的轻量封装,跟 HAL 并存,可以混用。
关键认知:Drivers 目录是只读的。你可以把它当第三方库看待,升级时整目录替换,任何修改都会在下次换版本时丢掉。真正该改的是stm32f1xx_hal_conf.h和system_stm32f1xx.c里那几个宏,而且这两个文件要复制到你自己的工程目录里改,不要动 Drivers 下的原件。
| 目录 | 内容 | 建议进版本库 | 备注 |
|---|---|---|---|
| Drivers/CMSIS | 内核头、型号头、system 源文件、启动文件 | 是 | 整目录只读 |
| Drivers/STM32F1xx_HAL_Driver | HAL 与 LL 驱动 | 是 | 整目录只读 |
| Middlewares | FatFS、FreeRTOS、USB 设备/主机库、STemWin | 按需 | 不用就别拷,省几百 MB |
| Projects | 按板子分的 Examples / Applications / Templates | 否 | 当参考,不当工程 |
| Utilities | LCD、CPU、评估板级驱动、字体 | 按需 | 只有跑评估板例程才需要 |
2.2 从一个点灯工程反推完整依赖链
Projects/STM32F103RB-Nucleo/Examples/GPIO/GPIO_IOToggle/是很好的解剖样本。进去先看目录:Src/main.c、Src/stm32f1xx_it.c、Inc/main.h、Inc/stm32f1xx_hal_conf.h、Inc/stm32f1xx_it.h,再加MDK-ARM/、SW4STM32/、EWARM/三套工程文件。这个结构就是最小可编译单元的标准形态,后面自己搭工程照着摆即可。
再用命令行把工程里的 include 路径掏出来,比自己猜快得多:
# 从 Keil 工程里提取头文件搜索路径 grep -o "<IncludePath>[^<]*" \ Projects/STM32F103RB-Nucleo/Examples/GPIO/GPIO_IOToggle/MDK-ARM/*.uvprojx \ | sed 's/<IncludePath>//' | tr ';' '\n'输出会告诉你它依赖Drivers/CMSIS/Include、Drivers/CMSIS/Device/ST/STM32F1xx/Include、Drivers/STM32F1xx_HAL_Driver/Inc以及工程自己的Inc。如果是 SW4STM32 工程,同样的信息在.cproject的<option id="...includes...">节点里,用 grep 搜value=也能拿到。
拿到路径之后,依赖链就清楚了:启动文件startup_stm32f103xb.s建立向量表并跳到Reset_Handler,system_stm32f1xx.c里的SystemInit把时钟恢复到默认的 8MHz HSI,然后进main,HAL_Init()配置 Flash 预取、设置 SysTick 为 1ms、初始化 NVIC 优先级分组,接着SystemClock_Config()把主频拉到 72MHz,最后才是MX_GPIO_Init()这类外设初始化。这条链上一环断了,现象都不一样:向量表错了跑飞,时钟没配对串口全是乱码,HAL_Init没调则HAL_Delay永远不返回。
2.3 用脚本核对启动文件与芯片容量
F1 的启动文件是按具体型号命名的,startup_stm32f103xb.s管 64K 和 128K 的中容量型号,startup_stm32f103xe.s管 512K 大容量,startup_stm32f107xc.s管互联型。选错的直接后果是中断向量表长度和实际芯片对不上,外设中断一开就进Default_Handler死循环。写个十几行的脚本,把这件事变成一次校验:
import os import sys # 型号前缀 -> (启动文件, 预编译宏, Flash 容量 KB) DEVICE_MAP = { "STM32F103C8": ("startup_stm32f103xb.s", "STM32F103xB", 64), "STM32F103RB": ("startup_stm32f103xb.s", "STM32F103xB", 128), "STM32F103ZE": ("startup_stm32f103xe.s", "STM32F103xE", 512), "STM32F107VC": ("startup_stm32f107xc.s", "STM32F107xC", 256), "STM32F100RB": ("startup_stm32f100xb.s", "STM32F100xB", 128), } PKG = "STM32Cube_FW_F1_V1.8.0" GCC_DIR = os.path.join(PKG, "Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/gcc") def check(model): model = model.upper().replace("-", "") hit = next((v for k, v in DEVICE_MAP.items() if model.startswith(k)), None) if hit is None: print(f"[!] {model} 不在映射表里,请先补充容量归类") return 1 startup, macro, flash_kb = hit path = os.path.join(GCC_DIR, startup) if not os.path.exists(path): print(f"[x] 启动文件缺失: {path}") return 2 # 粗略校验:向量表条目数应与该型号中断数匹配,这里只做存在性 + 宏检查 print(f"[ok] 型号 : {model}") print(f"[ok] 启动文件 : {startup}") print(f"[ok] 预编译宏 : {macro}") print(f"[ok] Flash : {flash_kb} KB") print(f"[ok] 链接脚本需设置 FLASH = {flash_kb}K") return 0 if __name__ == "__main__": sys.exit(check(sys.argv[1] if len(sys.argv) > 1 else "STM32F103C8T6"))脚本里的映射表只覆盖了常见几款,实际项目按自己板子补一行即可。三个输出值要落到三个地方:启动文件名落到工程的汇编源列表,预编译宏落到 Makefile 或 Keil 的Define框,Flash 容量落到链接脚本的MEMORY段。这三处只要有一处对不上,症状分别是链接报错、头文件里寄存器定义错位、程序超过容量后直接跑飞。运行一次不到一秒,比烧进去再猜快得多。
3. 用 arm-none-eabi 拼一个最小 HAL 工程
很多人的 F1 工程是从 CubeMX 生成的,图形界面点完就有代码,但一旦生成的工程编译不过,或者要把代码搬到 Linux 服务器上跑 CI,就抓瞎了。这一章不用 CubeMX,纯手工从固件包里挑文件、写 Makefile、写链接脚本,把最小工程搭起来。搭过一次之后,CubeMX 生成的每一行配置你都能对上号,出问题也知道去哪找。
3.1 最小文件清单与目录布局
先把目录摆成下面这个样子,后面所有命令都基于这个布局:
mkdir -p f1_min/{Core/{Inc,Src},Drivers,ld} cp -r STM32Cube_FW_F1_V1.8.0/Drivers/CMSIS f1_min/Drivers/ cp -r STM32Cube_FW_F1_V1.8.0/Drivers/STM32F1xx_HAL_Driver f1_min/Drivers/ cp STM32Cube_FW_F1_V1.8.0/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/gcc/startup_stm32f103xb.s \ f1_min/Core/Src/ cp STM32Cube_FW_F1_V1.8.0/Drivers/CMSIS/Device/ST/STM32F1xx/Source/Templates/system_stm32f1xx.c \ f1_min/Core/Src/ cp STM32Cube_FW_F1_V1.8.0/Drivers/STM32F1xx_HAL_Driver/Inc/stm32f1xx_hal_conf_template.h \ f1_min/Core/Inc/stm32f1xx_hal_conf.h注意最后一条命令做了重命名:模板文件名带_template后缀,HAL 源码里 include 的是stm32f1xx_hal_conf.h,不改名编译器直接报找不到头文件。这是新手最常见的第一个坑。
接下来编辑Core/Inc/stm32f1xx_hal_conf.h,至少打开这几个模块宏,其余按需:
#define HAL_MODULE_ENABLED #define HAL_CORTEX_MODULE_ENABLED #define HAL_GPIO_MODULE_ENABLED #define HAL_RCC_MODULE_ENABLED #define HAL_FLASH_MODULE_ENABLED #define HAL_PWR_MODULE_ENABLED #define HAL_EXTI_MODULE_ENABLED #define HAL_UART_MODULE_ENABLED #define HAL_DMA_MODULE_ENABLED /* 板上晶振频率,必须与硬件一致,否则串口和延时全偏 */ #if !defined(HSE_VALUE) #define HSE_VALUE 8000000U #endif /* SysTick 中断优先级,默认最低,后面第 4 章会用到这个值 */ #define TICK_INT_PRIORITY 0x0FU #define USE_HAL_DRIVER每个宏对应的效果很直接:不开HAL_UART_MODULE_ENABLED,stm32f1xx_hal_uart.c里的代码会被#if整段裁掉,你调HAL_UART_Init就是链接期报 undefined reference;开了不用的模块则白白增加编译时间。HSE_VALUE这一行后面单独讲,它是整份配置里最容易出事的一个数。
3.2 Makefile 关键变量与链接脚本
Makefile 不需要写得很花,抓住四个变量就行:芯片架构、预编译宏、头文件路径、链接脚本。其余用 wildcard 把所有 .c 收进来,靠链接期裁剪控制体积。
TARGET := f1_min BUILD := build MCU := -mcpu=cortex-m3 -mthumb DEFS := -DUSE_HAL_DRIVER -DSTM32F103xB INCLUDES := -ICore/Inc \ -IDrivers/STM32F1xx_HAL_Driver/Inc \ -IDrivers/CMSIS/Device/ST/STM32F1xx/Include \ -IDrivers/CMSIS/Include CFLAGS := $(MCU) $(DEFS) $(INCLUDES) -O2 -g3 -Wall \ -ffunction-sections -fdata-sections LDFLAGS := $(MCU) -Tld/STM32F103C8Tx_FLASH.ld \ -Wl,--gc-sections -Wl,-Map=$(BUILD)/$(TARGET).map \ -Wl,--print-memory-usage C_SOURCES := $(wildcard Core/Src/*.c) \ $(wildcard Drivers/STM32F1xx_HAL_Driver/Src/*.c) ASM_SOURCES := Core/Src/startup_stm32f103xb.s OBJECTS := $(addprefix $(BUILD)/,$(notdir $(C_SOURCES:.c=.o))) \ $(addprefix $(BUILD)/,$(notdir $(ASM_SOURCES:.s=.o))) all: $(BUILD)/$(TARGET).elf $(BUILD)/%.o: Core/Src/%.c | $(BUILD) arm-none-eabi-gcc -c $(CFLAGS) $< -o $@ $(BUILD)/%.o: Drivers/STM32F1xx_HAL_Driver/Src/%.c | $(BUILD) arm-none-eabi-gcc -c $(CFLAGS) $< -o $@ $(BUILD)/%.o: Core/Src/%.s | $(BUILD) arm-none-eabi-gcc -c $(MCU) -x assembler-with-cpp $< -o $@ $(BUILD)/$(TARGET).elf: $(OBJECTS) arm-none-eabi-gcc $(OBJECTS) $(LDFLAGS) -o $@ arm-none-eabi-objcopy -O binary -S $@ $(BUILD)/$(TARGET).bin $(BUILD): mkdir -p $@ clean: rm -rf $(BUILD)逐项说清楚,换芯片时知道改哪里。-DSTM32F103xB决定stm32f1xx.h往下包含stm32f103xb.h,这个宏和启动文件必须成对换。-ffunction-sections -fdata-sections配合-Wl,--gc-sections,让链接器把没用到的 HAL 函数整个丢掉,全量编译 60 多个驱动源文件最后也能压到十几 KB。--print-memory-usage会在每次链接后打印 FLASH 和 RAM 占用,比事后看 map 文件直观。
链接脚本里最要紧的是两段:
MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K } _estack = ORIGIN(RAM) + LENGTH(RAM); SECTIONS { .isr_vector : { KEEP(*(.isr_vector)) } >FLASH .text : { *(.text*) *(.rodata*) } >FLASH .data : { *(.data*) } >RAM AT> FLASH .bss : { *(.bss*) *(COMMON) } >RAM }LENGTH = 64K和20K对应 F103C8T6 的实际容量,换成 F103ZE 就是 512K/64K。_estack放在 RAM 末尾,启动文件开头的ldr sp, =_estack靠它取初值。.data段的AT> FLASH表示初值存在 Flash、运行时拷到 RAM,少了这句全局变量初值全是随机的。
3.3 SystemClock_Config 与 HSE_VALUE 的对应关系
F1 的时钟树不复杂,但每一步都不能省。下面这段是 8MHz 外部晶振倍频到 72MHz 的标准写法:
void SystemClock_Config(void) { RCC_OscInitTypeDef osc = {0}; RCC_ClkInitTypeDef clk = {0}; osc.OscillatorType = RCC_OSCILLATORTYPE_HSE; osc.HSEState = RCC_HSE_ON; osc.HSEPredivValue = RCC_HSE_PREDIV_DIV1; osc.PLL.PLLState = RCC_PLL_ON; osc.PLL.PLLSource = RCC_PLLSOURCE_HSE; osc.PLL.PLLMUL = RCC_PLL_MUL9; /* 8MHz x 9 = 72MHz */ if (HAL_RCC_OscConfig(&osc) != HAL_OK) { Error_Handler(); } clk.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; clk.AHBCLKDivider = RCC_SYSCLK_DIV1; /* HCLK = 72MHz */ clk.APB1CLKDivider = RCC_HCLK_DIV2; /* PCLK1 = 36MHz */ clk.APB2CLKDivider = RCC_HCLK_DIV1; /* PCLK2 = 72MHz */ if (HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_2) != HAL_OK) { Error_Handler(); } }三个参数有硬约束。PLLMUL最大只能到 16,72MHz 是 F103 的上限,超了直接不启动。APB1CLKDivider不能设成 DIV1,因为 APB1 总线最高 36MHz,超频会让挂在上面的 USART2/3、I2C、SPI2 全部工作异常。第二个参数FLASH_LATENCY_2是 Flash 等待周期,24MHz 以下用 0、48MHz 以下用 1、72MHz 必须用 2,设小了取指出错,表现为随机的 HardFault。
然后是HSE_VALUE。这个宏在stm32f1xx_hal_conf.h里定义一次,system_stm32f1xx.c里算SystemCoreClock又用一次,HAL_RCC_GetHCLKFreq()返回的值、HAL_UART_Init里算 BRR 的值、HAL_Delay的毫秒基准,全部由它推出来。板上是 8MHz 而你写了 12000000,编译不会报错,烧进去串口助手收到的全是乱码,HAL_Delay(1000)实测只有 0.67 秒。这类问题靠示波器量 MCO 引脚最快,或者干脆在main里打印一次SystemCoreClock的值自查。
3.4 编译、烧写与三项验证
make -j8 arm-none-eabi-size build/f1_min.elfsize输出三列:text 是 Flash 占用,data 是带初值的 RAM 变量,bss 是清零的 RAM 变量。text 超过链接脚本里的 LENGTH,链接阶段就会报 region overflow,不用等烧进去才发现。
烧写用两种工具都行:
# 方式一:OpenOCD + ST-Link openocd -f interface/stlink.cfg -f target/stm32f1x.cfg \ -c "program build/f1_min.elf verify reset exit" # 方式二:STM32CubeProgrammer 命令行 STM32_Programmer_CLI -c port=SWD -w build/f1_min.bin 0x08000000 -v -rst烧完做三项验证,缺一项都可能在后面埋雷。第一,LED 是否按 500ms 翻转,验证 SysTick 和 GPIO 都正常;第二,串口 115200 是否打印出正确的字符串,验证时钟树和 USART 分频正确;第三,读回SystemCoreClock的值是否等于 72000000,验证HSE_VALUE没写错。三项都过,这个最小工程就可以当模板用了,后面加外设只是往上叠.c文件。
4. HAL 用顺的三个高频场景:GPIO、USART、中断
工程能跑只是起点,真正花时间的是把外设配对。F1 这一代芯片有几个跟 F4 之后不一样的设计,照抄 F4 的代码往往会卡在看起来毫不相干的地方。这一章挑三个最高频的场景,把结构体字段的实际含义、参数的取值边界、以及出错时先看哪里讲透。
4.1 GPIO 初始化结构体在 F1 上的实际含义
__HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef gpio = {0}; gpio.Pin = GPIO_PIN_5; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &gpio); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);四个字段里,Mode决定 CRL/CRH 寄存器里的 CNF 位,Speed决定 MODE 位。F1 的Speed只有三档,对应输出驱动器的翻转速率约 2MHz、10MHz、50MHz,它影响的是边沿陡峭程度和电磁干扰,不是“越快越好”。点灯、驱动继电器这类低速场景用 LOW 就够,只有 SPI、SDIO 这类高速信号才需要 HIGH。Pull在 F1 上只有上拉、下拉、浮空三种,没有 F4 那种“复用功能带上下拉”的组合选项。
F1 最需要注意的差异在复用功能上。F4 有独立的GPIOx_AFRL/AFRH寄存器,一个引脚可以映射到任意 AF 编号;F1 没有这个寄存器,外设复用是固定的默认引脚,想换引脚只能通过 AFIO 的重映射开关整体挪。所以下面这两行在 F1 工程里出现频率极高:
__HAL_RCC_AFIO_CLK_ENABLE(); /* 释放 PA15/PB3/PB4,把它们从 JTAG 功能还给普通 GPIO */ __HAL_AFIO_REMAP_SWJ_NOJTAG(); /* USART1 从 PA9/PA10 重映射到 PB6/PB7 */ __HAL_AFIO_REMAP_USART1_ENABLE();忘了开 AFIO 时钟,重映射宏写进去也不生效,现象是引脚死活没有输出。这个坑在 F1 上特别常见,因为 F4 用户根本没有“AFIO 时钟”这个概念。
4.2 USART 的参数落点与超时语义
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; huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } uint8_t msg[] = "boot ok\r\n"; HAL_UART_Transmit(&huart1, msg, sizeof(msg) - 1, 100);HAL_UART_Transmit最后一个参数是超时,单位毫秒,内部靠HAL_GetTick()轮询等待发送寄存器空。传HAL_MAX_DELAY表示死等,传 0 基本必然返回HAL_TIMEOUT,正常工程给 100 到 1000 之间的值就够。返回值一定要判,返回HAL_TIMEOUT通常意味着波特率算错或者引脚没配对,而不是“偶尔超时”。
真实项目里更常用中断接收。HAL_UART_Receive_IT的行为要记清楚:它只在你指定的缓冲区填满时才回调一次,回调里必须重新调用一次才能继续收下一个字节,否则串口就“哑”了。
uint8_t rx_byte; void start_rx(void) { HAL_UART_Receive_IT(&huart1, &rx_byte, 1); } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { ringbuf_push(rx_byte); /* 塞进环形缓冲,主循环慢慢消费 */ HAL_UART_Receive_IT(&huart1, &rx_byte, 1); /* 必须重启 */ } }还有一个容易被忽略的细节:USART1 挂在 APB2(72MHz),USART2/3 挂在 APB1(36MHz)。同样配 115200,挂在 36MHz 上的那路分频系数算出来是 19.53,存在约 0.16% 的误差,短帧没问题,长帧高速率时偶发误码。把OverSampling从 16 改成 8 能改善分频精度,代价是对时钟抖动更敏感。
4.3 中断优先级分组与 HAL_Delay 的致命组合
F1 的 NVIC 只实现了 4 位优先级,HAL_Init()里默认调HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4),也就是 4 位全给抢占优先级,取值范围 0 到 15,数字越小越优先。配置外部中断的典型写法:
HAL_NVIC_SetPriority(EXTI15_10_IRQn, 5, 0); HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);现在说那个血泪坑。TICK_INT_PRIORITY在stm32f1xx_hal_conf.h里的默认值是0x0F,也就是 SysTick 优先级最低。如果在优先级数值小于 15 的中断服务函数里调用HAL_Delay(),会发生什么:HAL_Delay靠HAL_GetTick()的计数递增来退出循环,而计数的递增依赖 SysTick 中断,SysTick 优先级比当前中断低,永远抢不到执行权,于是程序卡在HAL_Delay的 while 循环里出不来。调试器暂停时看到 PC 停在stm32f1xx_hal.c的延时循环里,很多人第一反应是芯片坏了。
解决办法有两条,按场景选。中断里本就不该做长延时,正确做法是置个标志位就退出,把耗时逻辑丢回主循环;确实需要在较高优先级上下文里等一小段,就把TICK_INT_PRIORITY改成0x00,让 SysTick 抢占一切,代价
本文还有配套的精品资源,点击获取