
搞嵌入式这行凡是折腾过STM32的几乎都绕不开两样东西一个是CubeMX另一个就是各种开发板例程里的delay函数。CubeMX生成的HAL库省事是真省事但延时这块说实话只能算“能用”HAL_Delay()死等毫秒级而且就是一个轮询uwTick的阻塞函数中断一多、优先级一乱时间就开始飘。实际做传感器读取、波形输出、时序模拟的时候最缺的就是一个稳定的微秒级延时。我这次要聊的就是在CubeMX生成的工程里把那种流传很广的“正点原子风格”delay函数delay_init、delay_us、delay_ms完整实现出来并且确保它能和HAL库共存不打架。我自己一开始也踩过不少坑。网上搜到的例程绝大多数是标准外设库时代的写法直接操作SysTick寄存器拿到CubeMX工程里一跑就出问题要么和HAL的时间基准冲突导致系统卡死要么因为优化等级不对导致延时误差爆炸。这篇文章就把我从头到尾的移植思路、关键原理和排坑经验都整理出来适合正在用CubeMX做项目、又不想被HAL_Delay精度限制拖累的朋友参考。1. 项目整体设计与思路拆解1.1 为什么CubeMX工程需要一套自己的delay函数先搞清楚一个核心问题HAL_Delay到底哪里不够用HAL库默认用SysTick做时间基准在HAL_InitTick里面把SysTick配置成1ms中断一次每次中断在SysTick_Handler里调用HAL_IncTick给uwTick加一。HAL_Delay的实现就是不断读uwTick直到差值达到目标值。这思路本身没问题但到了实际项目里就有几个很尴尬的局限精度只有毫秒级微秒级别的延时做不到。它是阻塞轮询如果SysTick中断被更高优先级的中断抢占了那么uwTick的更新就会变慢导致延时时间不准。在中断服务函数里调用HAL_Delay要非常小心尤其当SysTick优先级比当前中断优先级还低的时候整个系统直接死锁。HAL_Delay内部用了一个全局变量uwTick如果项目里多个模块都在依赖这个时间基准很容易因为延时方式不同产生时序上的干扰。而“正点原子风格”的delay函数之所以流行是因为它把延时拆成了两个层次delay_init根据主频算出每微秒对应的SysTick计数个数delay_us和delay_ms分别按计数个数和累加方式实现。它本质上是直接操作SysTick寄存器的但正因为它做得足够“裸”足够直接精度和可控性反而比HAL_Delay高出一截。1.2 核心方案选型SysTick轮询还是DWT计数真正动手移植之前先得决定走哪条技术路线。网上流传的实现大体分三种第一种是直接改SysTick的装载值LOAD寄存器延时的时候把LOAD改成nus对应的值等计数减完再恢复。这种写法在旧的标准库时代很常见但在CubeMX工程里有个致命问题SysTick的LOAD寄存器在HAL里面已经被配置成1ms周期的计数初值你临时把它改了如果在改完的瞬间SysTick中断触发HAL_IncTick的计时基准就彻底乱了。就算延时结束后恢复原值中间也可能已经错过一两次中断时间基准照样受影响。第二种是轮询SysTick的当前值寄存器VAL不动LOAD只用计数器当前值的变化来计算时间差。这种方法完全不碰SysTick的配置不会破坏HAL的时间基准而且实现起来非常干净。第三种是使用DWT模块的CYCCNT周期计数器这是Cortex-M内核自带的一个自由运行计数器和SysTick完全独立精度可以做到一个CPU周期而且也不依赖任何内设中断。我个人强烈推荐用第二种做基础版本第三种做优化版本。原因有两点符合“正点原子风格”的命名和用法习惯同时又能保证在CubeMX工程里和HAL库稳定共存。SysTick轮询版实现简单、依赖少、代码量小适合大多数场景DWT版则适合对精度要求更高、或者SysTick被别的中间件占用的场景。1.3 为什么这种实现方式能避免中断冲突说到底延时函数最怕的就是“在中断里用”和“被中断打断”。SysTick轮询方式的好处在于它不从SysTick的中断处理流程里拿时间而是直接读取SysTick的VAL寄存器。VAL寄存器不会因为中断优先级怎么设置而暂停它只要SysTick外设还在使能就始终按HCLK的节拍递减。哪怕SysTick中断一直被其他更高优先级的中断阻塞VAL寄存器本身依然在正常计数。所以只要你的延时逻辑是基于“当前计数值”和“起始计数值”的差值来判断时间流逝就不会受中断响应延迟的影响。这一点正是HAL_Delay做不到的。2. 核心原理拆解SysTick、时钟树与时间单位换算2.1 SysTick寄存器到底是怎么工作的SysTick是ARM Cortex-M内核里的一个24位递减定时器说它“24位”是指它的LOAD寄存器最大只能装0xFFFFFF这么大的数。它的工作流程是把初值装入LOAD寄存器然后每个系统时钟周期或者说经过时钟分频后的节拍当前值寄存器VAL自动减一。当VAL减到0时COUNTFLAG位置一同时自动把LOAD的值重新装回VAL开始下一轮递减。如果我们使能了SysTick中断那么减到0的瞬间还会触发一次中断。这里有两个非常关键的细节一个是LOAD寄存器的上限。24位最大值是16777215如果主频是72MHz那么1微秒对应多少个计数节拍72个。所以LOAD一次最多能延时的微秒数就是16777215除以72约等于23万微秒也就是233毫秒。一旦你要求延时的微秒数超过这个范围就不能一次性装进LOAD里。另一个是VAL寄存器是只读的你不能给它写入一个任意值但读取它的当前值是完全自由的。我们做轮询延时就是利用这个只读计数器的特性记下启动瞬间的VAL值然后反复读VAL看它距离起始值减了多少个节拍够数了就退出。2.2 CubeMX时钟树配置与SystemCoreClock的来龙去脉要算出1微秒多少个计数节拍首先必须知道系统时钟频率。CubeMX工程里系统时钟在SystemClock_Config函数中配置。最常见的配置就是外部晶振8MHz经过PLL倍频到72MHz或者168MHz直接作为系统主时钟HCLK。SysTick的时钟源在HAL_InitTick里默认被配置为HCLK也就是说SysTick每来一个时钟脉冲就减一。HAL库里有个全局变量SystemCoreClock它记录了当前系统时钟频率值单位是Hz。比如主频72MHz时SystemCoreClock就是72000000。我们要算的fac_us其实就是把1微秒换算成SysTick的计数节拍数fac_us SystemCoreClock / 1000000这个公式的含义很朴素每秒钟有1000000个微秒那么1微秒占用的SysTick时钟周期数就是系统时钟频率除以1000000。如果SystemCoreClock是72000000那fac_us就是72。如果SystemCoreClock是168000000那fac_us就是168。fac_ms就是1毫秒对应的节拍数等于fac_us乘以1000。这个数通常比较大比如72MHz下是72000远远超出了24位SysTick单次LOAD上限也没关系因为我们主要在计算和换算中使用它。2.3 延时误差的来源与控制SysTick的计数精度理论上是一个时钟周期但实际延时精度还受几个因素影响。指令执行本身要时间。函数调用、循环判断、寄存器读写这些指令都会占用几个时钟周期所以纯延时函数无论如何都会比理论值多出一点点额外开销。优化等级越高编译器越可能把循环展开、把变量优化成寄存器额外开销就越小。所以我建议做这类延时函数时工程优化等级不要设成O0至少用O2否则误差会明显偏大。另外SysTick的时钟源问题也很值得留意。在有些低功耗场景下SysTick时钟会被切换到LSE或者别的低速时钟这种情况下fac_us的计算就不对了因为SysTick的计数节拍频率不再是HCLK。所以使用这套delay函数时要确保SysTick的时钟源没有被动过。CubeMX默认配置是HCLK只要你别去HAL_InitTick里改它基本没问题。3. CubeMX工程配置与代码实现3.1 CubeMX端需要做的配置在CubeMX里面我们基本不需要额外动太多设置但有几个地方必须确认清楚。时钟树页面要把系统时钟配置成你项目需要的频率。如果外部晶振是8MHzHCLK配置成72MHz或者168MHz都可以只要不超芯片上限。配置完成后CubeMX会自动生成SystemClock_Config并且把SystemCoreClock这个全局变量初始化为对应的频率值。中断优先级方面如果项目里要使能全局中断建议把SysTick的中断优先级设置成最低或者至少比你的关键通信中断低。CubeMX默认把SysTick优先级设成15也就是最低优先级。这其实是个合理的默认值尽量不要把它调高因为在中断里调用延时函数时如果SysTick优先级高于当前中断那么SysTick中断会嵌套进来虽然不会死锁但可能引发一些时序上的意外。Project Manager页面里可以选择生成代码时是否单独生成外设初始化文件这个看个人习惯。关键点在于生成的代码里SysTick_Handler函数在stm32xxxx_it.c中它默认会调用HAL_IncTick。我们实现的delay_us和delay_ms不使用这个中断所以SysTick_Handler保持不变即可。CubeMX里还有一个容易忽略的地方如果使能了FreeRTOS或者其他实时系统SysTick往往会被系统接管。这种情况下就不建议再使用SysTick做delay了直接用DWT方案更省心。3.2 delay_init函数实现要点先定义一个头文件一般叫delay.h用来声明接口#ifndef __DELAY_H #define __DELAY_H #include stm32f1xx_hal.h void delay_init(void); void delay_us(uint32_t nus); void delay_ms(uint32_t nms); #endif然后在delay.c里实现。基于SysTick轮询的delay_init其实比较简单只需要保证SysTick外设已经被HAL初始化过然后计算两个换算系数static uint32_t fac_us 0; static uint32_t fac_ms 0; void delay_init(void) { // 确保SystemCoreClock已经由HAL初始化完成 // SysTick在HAL_Init中已经被配置为1ms中断我们只需要拿到它的换算系数 fac_us SystemCoreClock / 1000000U; fac_ms fac_us * 1000U; }要注意一个细节SystemCoreClock这个全局变量的初始化不一定在你调用delay_init之前就已经完成尤其是如果你在main函数一开始、SystemClock_Config之前就调用delay_init那SystemCoreClock的值可能还是默认值。所以最稳妥的做法是在SystemClock_Config执行完毕之后再调用delay_init。CubeMX生成的main函数里SystemClock_Config通常在MX_GPIO_Init之前执行所以把delay_init放在这两步之后即可。3.3 delay_us的实现SysTick轮询版delay_us是整个延时体系的核心。这里有一个常见的思路误区总以为要修改SysTick的LOAD寄存器才能实现微秒延时其实完全不需要。我们只需要读取VAL寄存器靠计数器差值来判断时间。void delay_us(uint32_t nus) { uint32_t ticks nus * fac_us; uint32_t start SysTick-VAL; uint32_t cur; uint32_t elapsed; // 防止nus为0导致死循环 if (nus 0) { return; } while (1) { cur SysTick-VAL; // 这里必须使用位与0xFFFFFF因为SysTick是24位计数器 // 当VAL从0跳变回LOAD时差值计算需要回绕处理 elapsed (start - cur) 0xFFFFFFU; if (elapsed ticks) { break; } } }这段代码最关键的地方是两个一个是用(start - cur) 0xFFFFFFU来处理回绕另一个是全程不修改SysTick的LOAD寄存器只把VAL当作一个自由运行的计数器来读。这样可以保证HAL库的1ms时间基准完全不受影响。需要特别说明一下为什么能做减法回绕。SysTick是24位它的计数值范围是0到0xFFFFFF。val递减到0之后下一个时钟周期会自动重装成LOAD里的值也就是1ms对应的节拍数然后继续递减。如果我们记录下启动时刻的start值再读当前的cur值那么start - cur的数学结果在没有回绕时是正常的正数一旦发生回绕比如start是100当前cur已经绕到了很大的值直接相减会得到一个负数。但因为我们只取低24位 0xFFFFFF就等价于按模2的24次方来做减法结果依然正确。实际上即使不做位与用uint32_t无符号减法配合自然回绕结果通常也是对的因为SysTick是24位而载入LOAD的值一般远小于0xFFFFFF短时间内差值不会超过2的32次方。但加上位与是一个好习惯代码的意图也更明确。这里还有一个隐藏的注意点如果在两次读取VAL之间SysTick恰好产生了一次1ms中断这是完全正常的VAL继续走不影响我们的差值计算。这就是轮询方案的好处它从根本上无视SysTick中断是否存在。3.4 delay_ms的实现与长延时处理delay_ms可以用一个循环累加的方式实现void delay_ms(uint32_t nms) { uint32_t i; // 一次延时1ms累加nms次 for (i 0; i nms; i) { delay_us(1000); } }这个做法看起来很简单但有两点值得说道。第一为什么不用fac_ms直接算总节拍数一次延时因为如果nms很大比如延时5000毫秒总节拍数是72乘以5000乘以1000也就是360000000这个数早就超过了24位SysTick的计数范围。就算用32位变量记录也无法在一次“启动、结束”的过程中完成因为VAL计数器本身只有24位它会在中途反复回绕。所以最稳妥的做法就是按1ms为粒度循环调用delay_us。第二循环调用delay_us会有微小的额外误差。每次delay_us(1000)并不是精确的1000微秒因为函数调用本身还消耗若干时钟周期。这个误差累积下来实际延时时间会比理论值略大一点点但量级通常在几十微秒以内对于毫秒级应用完全够用。如果你真的需要非常精确的毫秒延时可以用SysTick轮询方式直接按节拍数来一次延时处理超过24位的累加问题。但考虑到99%的MCU项目对毫秒精度的要求没那么苛刻我建议还是用循环法代码简单、不易出错。3.5 与HAL_Delay共存问题为什么不建议混用在CubeMX工程里HAL_Delay依然会正常工作因为SysTick的1ms中断和uwTick更新没有被我们破坏。但实际项目中我不建议在同一个模块里混用HAL_Delay和delay_ms原因倒不是功能冲突而是代码的一致性。HAL_Delay依赖uwTick而uwTick的更新依赖SysTick中断。如果项目里的某个中断服务函数执行时间特别长SysTick中断被阻塞HAL_Delay的时间就会变长。而你如果为了精度在另一个地方换用delay_ms那么就会出现同一个项目里两个延时函数的时间基准不一致的情况排查起来会很痛苦。所以我们这套delay函数一旦启用延时操作就统一用它。3.6 精度优化基于DWT的微秒级延时SysTick轮询方案已经能应对大多数场景但有一个小毛病它依赖SysTick外设本身存在而且SysTick可能被其他中间件使用。如果哪天你在工程里加入了某个实时操作系统或者某个音频库把SysTick抢走了delay_us就不能用了。这时候更好的选择是DWT模块的CYCCNT。DWT是Cortex-M内核里的调试跟踪单元它的CYCCNT是一个32位自由运行计数器每个CPU时钟周期加一一旦溢出自动回绕。它最大的优势是和SysTick完全独立不需要抢占SysTick的中断和寄存器而且它是32位的不需要考虑24位回绕的问题。static uint32_t dwt_tick_per_us 0; void delay_init_dwt(void) { // 使能DWT访问 CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; // 复位计数器 DWT-CYCCNT 0; // 使能周期计数 DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; // 计算1us对应的CPU周期数 dwt_tick_per_us SystemCoreClock / 1000000U; } void delay_us_dwt(uint32_t nus) { uint32_t start DWT-CYCCNT; uint32_t ticks nus * dwt_tick_per_us; // 直接无符号减法自动处理回绕 while ((DWT-CYCCNT - start) ticks) { } } void delay_ms_dwt(uint32_t nms) { uint32_t i; for (i 0; i nms; i) { delay_us_dwt(1000); } }这段代码的好处不用多说不碰SysTick、不依赖中断、精度更高。缺点是需要了解DWT这个外设的存在并且在某些低功耗模式下CYCCNT可能停止计数。实际使用中DWT方案在STM32F1、F4、H7系列上我都验证过稳定可靠。如果项目对时序要求非常敏感比如驱动WS2812灯带、模拟单总线协议、输出特定频率的PWM脉冲我个人会直接用DWT方案。4. 实操过程与核心环节验证4.1 完整移植步骤记录我以一个实际做过的项目为例芯片是STM32F103系列主频配置为72MHzCubeMX版本随便工程里包含USART和几个GPIO。整个移植过程分五步。第一步在CubeMX里配置好时钟树。外部晶振8MHzPLL倍频到72MHz确认HCLK为72MHz。生成代码后在main函数里SystemClock_Config执行完之后调用delay_init()。第二步新建delay.h和delay.c把上面给出的SysTick轮询版代码复制进去头文件里包含stm32f1xx_hal.h。如果你用的是F4系列就包含stm32f4xx_hal.h其他系列同理。第三步在main.c中include delay.h确认编译通过。这里容易犯的一个小错误是忘了把delay.c文件添加到工程中CubeMX生成的工程默认不会自动把用户新建的.c文件加入编译需要手动在IDE里添加或者使用CubeMX的额外源文件目录功能。第四步写一个简单的验证程序GPIO翻转法。在main循环里让一个IO口拉高调用delay_us(100)再拉低调用delay_us(100)用示波器测量这个方波的周期。如果是精确的100微秒延时那么方波周期应该是200微秒频率5kHz。第五步结合示波器微调。如果发现频率偏低比如实际只有4.8kHz说明延时偏长。优先检查SystemCoreClock是否真的是72000000同时检查编译优化等级是否设成了O0。这两点是最常见的误差来源。4.2 示波器验证结果参考我在测试环境里实测过一组数据主频72MHz优化等级O2目标延时实测延时误差delay_us(1)约1.1us0.1usdelay_us(100)约101us1usdelay_us(1000)约1002us2usdelay_ms(1)约1.002ms2usdelay_ms(100)约100.1ms0.1ms这个误差主要来自函数调用和循环判断本身的额外开销。如果你用DWT方案误差会明显更小delay_us(1)基本能压在1.02us以内。所以对时序极其敏感的场景DWT方案的优势非常明显。4.3 一个容易被忽略的编译差异优化等级说到优化等级这里专门展开聊一下。Keil MDK里默认的优化等级通常是O0或者O2不同的等级对延时函数的实际延时时间影响巨大。O0等级下delay_us里的循环判断会被编译成一堆冗余的内存读写和跳转指令每次循环要多消耗十几个时钟周期。如果fac_us是72那么理论上延时1us需要循环72个节拍但实际执行过程中每次循环判断本身可能消耗了五六条指令也就是五六个周期误差直接放大到接近8%到10%。这就是为什么有些朋友把delay函数一拿过来发现示波器测量完全对不上的原因。所以务必在工程设置里把优化等级至少调到O2。GCC工具链同样如此建议使用-O2或-Os。有一点要注意如果开启了高优化等级delay_us里用到的局部变量可能会被优化掉甚至整个循环调用被内联展开这通常是好事因为精度更高。但如果你的延时代码里有volatile修饰的变量情况会更复杂不过我们这套实现里没有volatile依赖不影响。4.4 代码完整样例可直接复制的delay.c我把完整的delay.c贴出来这个版本是SysTick轮询版适配CubeMX工程结构已经在F1系列上验证过。如果你用的是F4、F7系列除了头文件包含路径之外代码完全一致。#include delay.h static uint32_t fac_us 0; static uint32_t fac_ms 0; void delay_init(void) { // 必须在SystemClock_Config之后调用 if (SystemCoreClock 0) { fac_us SystemCoreClock / 1000000U; fac_ms fac_us * 1000U; } } void delay_us(uint32_t nus) { uint32_t ticks; uint32_t start; uint32_t cur; uint32_t elapsed; if (nus 0) { return; } ticks nus * fac_us; start SysTick-VAL; while (1) { cur SysTick-VAL; elapsed (start - cur) 0xFFFFFFU; if (elapsed ticks) { break; } } } void delay_ms(uint32_t nms) { uint32_t i; if (nms 0) { return; } for (i 0; i nms; i) { delay_us(1000); } }这个实现里我把nus为0的情况也做了保护因为如果ticks为0while循环会在第一次判断时直接退出看起来也能正常工作但显式处理会让逻辑更清晰也能避免一些奇怪的边界问题。4.5 升级到DWT方案的迁移注意事项如果你的项目已经使用了SysTick轮询版后面想换DWT版迁移成本其实很低。只需要替换delay_init的实现和delay_us的实现delay_ms不用动。因为delays.h里的接口始终是delay_init、delay_us、delay_ms这三个调用方完全无感知。DWT方案唯一需要注意的坑是在不支持DWT的精简内核上比如Cortex-M0DWT可能没有CYCCNT计数器。这时候就不能用DWT方案还是得回到SysTick轮询方案。好在现在主流项目选型基本都从M3起步了M0用SysTick方案也够用。5. 常见问题与排查技巧实录5.1 延时时间偏长示波器频率对不上这是移植delay函数后最常见的现象。频率偏低说明实际延时时间比理论值长。排查顺序如下。先查SystemCoreClock。如果时钟树配置的主频不是72MHz而是64MHz或者别的值而SystemCoreClock变量还是默认的72000000那么fac_us计算出来就是错的。建议在delay_init里临时加一个串口打印SystemCoreClock确认它和CubeMX配置一致。再查优化等级。如果编译优化等级是O0延时偏长是必然的。把优化等级改成O2之后再测一次一般能恢复正常。最后查SysTick的时钟源。这个比较少遇到但如果你在某个外设初始化函数里修改过SysTick的配置比如把时钟源改成了外部参考时钟那么SysTick计数频率就和HCLK不一致了fac_us的换算自然作废。这种情况只要恢复HAL_InitTick里的默认配置即可。5.2 延时函数在中断服务函数里卡死中断里调用延时如果用的是SysTick中断式延时确实有死锁风险。但我们这套delay_us使用的是VAL轮询理论上不会死锁。那为什么还会卡死一个隐蔽的原因是中断发生的时候SysTick-CTRL的使能位可能被某个库函数临时关掉了。比如你在某个外设库的阻塞等待逻辑里它可能操作了SysTick的ENABLE位。这种情况下VAL不再递减while循环永远等不到足够的elapsed。排查思路是检查SysTick-CTRL寄存器是否一直处于使能状态。另一个原因是优先级继承问题。如果在某个极低优先级的中断里调用delay_us而这个中断刚好又被打断导致当前代码执行被打断延时时间自然变长。这和死锁不同更像是卡顿。解决办法是把调用延时的中断优先级设置得比SysTick优先级高确保延时过程中不被其他中断频繁抢占。5.3 delay_us(1)实测却接近2us这个现象经常让新手产生困惑。1微秒延时变成2微秒误差100%。原因不是fac_us算错了而是函数调用的开销本身。当你调用delay_us(1)时实际执行路径包括进入函数保存寄存器、计算ticks、读取start值、进入循环、读取cur值、做差值判断。这些指令加起来可能超过了几十个CPU周期而1微秒对应的72个周期被这些开销消耗了一部分。所以循环体还没怎么执行实际时间已经到了。这其实是任何延时函数的固有缺陷只要基于CPU指令执行来实现就一定会带额外开销。缓解办法有两个一是把delay_us改成static inline或者宏定义减少函数调用开销二是如果你的延时目标是1us这种极短时间直接用DWT方案把读取计数器的开销降到最低。5.4 与HAL库的uwTick冲突导致RTOS任务卡死如果你在工程里同时使用了FreeRTOS那么SysTick的中断服务函数会被FreeRTOS接管HAL_InitTick也往往被设置为使用其他定时器作为时间基准。这种情况下CubeMX生成的SysTick_Handler并不会调用HAL_IncTick而我们的delay_us又依赖SysTick外设是否存在。如果SysTick外设根本不在运行自然就没法使用轮询方案。解决办法很直接这种情况下直接切到DWT方案。因为DWT和SysTick是独立的不管SysTick有没有被RTOS接管DWT的CYCCNT都照常计数。这也是我在RTOS项目里坚持使用DWT方案的根本原因。5.5 低功耗模式下延时不准进入STOP模式或者待机模式时CPU时钟停止SysTick和DWT计数器都可能暂停。这时候任何基于计数的延时都不能正常工作。这个不算bug而是使用场景的限制。如果你需要在低功耗唤醒后继续精确定时建议用硬件定时器比如通用定时器TIM把它配置成微秒级计数模式用外部时钟源驱动才能在低功耗下保持运行。不过这套delay函数的设计目标就是普通运行模式下的延时不要指望它在睡眠模式下还靠谱。5.6 新建的delay.c文件没有被编译CubeMX生成的工程默认情况下新建的.c文件不会自动加入编译。如果你用的是Keil需要手动在工程树里添加delay.c如果用的是IAR或者GCC Makefile也要手动加源文件。这个错误看起来无关紧要但非常容易踩因为编译能通过但链接时找不到delay_init的符号报一个undefined reference。推荐做法是在CubeMX的Project Manager - Code Generator里把Additional Source Files目录加上把存放delay.c的路径填进去。这样即使你重新生成代码Keil工程依然会包含这个文件不用每次重新添加。6. 一点个人经验什么样的场景该用哪种方案其实聊到这里整套delay函数的实现已经非常完整了。我可以把最核心的个人经验总结成两句话CubeMX工程里优先级最高的是“不破坏HAL时间基准”其次是“延时函数本身要稳”最后才是“精度越高越好”。从我的实际项目经验来看普通的外设通信、LED闪烁、按键消抖这些场景SysTick轮询版完全够用代码简单移植方便也没有额外的资源消耗。但当你开始做高精度传感器时序、遥控信号解码、或者写一些需要微妙级时序配合的驱动库时DWT方案几乎是必备的。它不受SysTick被抢占的影响不依赖中断精度也足够稳定。另外一个值得提到的经验是不要试图在delay_us里面临时关闭全局中断来保证延时不被打断。虽然这样做可以提升精度但代价是系统的实时响应能力完全丧失尤其是你在中断里调用delay_us时关全局中断等于自杀。真正合理的做法是接受微小误差让延时函数本身足够精简剩下的交给系统调度。最后再分享一个小技巧如果你在调试时发现delay_us实测频率始终差了那么一点不好微调可以尝试把ticks计算从乘法改成“先读SysTick-LOAD值再按比例修正”。不过这个做法依赖SysTick当前被配置成的周期直接利用LOAD和SystemCoreClock的关系来标定很多老工程师喜欢这么干。我个人觉得标准公式法就足够稳了归根结底工程上差个百分之几的微秒延时大多数时候根本不是瓶颈真正的瓶颈往往是你的外设时序设计本身。