尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
STM32F1/F4延时函数选型指南:从SysTick到DWT的避坑实践
简介这份资源面向使用STM32F1与STM32F4系列微控制器进行嵌入式开发的工程师与学习者提供一套可直接复用的延时函数实现方案用于解决裸机开发中毫秒级与微秒级精确延时、定时控制等常见需求。压缩包内共2个文件包含1个c源文件与1个h头文件分别承载延时函数的具体实现与函数原型、宏定义等接口声明整体体积约848B轻量易集成添加到工程即可调用。资源围绕循环计数与SysTick、通用定时器等硬件定时器两种思路展开兼顾短延时与长延时场景并涉及延时上限与精度校准等关键问题。目前已有3011人学习下载适合希望快速获得稳定延时库、减少底层重复实现的开发者参考使用。1. 从一次串口丢包说起STM32F1/F4 延时函数到底该怎么选前阵子帮一个做工业网关的朋友排查问题现象很典型STM32F1 上跑着 Modbus 轮询主机发来的帧偶尔整帧丢失示波器抓 485 使能脚发现收发切换的间隔忽长忽短。他一开始怀疑是串口中断优先级折腾两天没结果最后定位到延时函数——他在中断里调了HAL_Delay而这个函数底层靠 SysTick 中断累加计数中断里等中断等于自己把自己锁死。这个坑在 F1 和 F4 上表现还不一样F4 主频高、HAL 库版本新有些默认配置下反而更容易翻车。STM32F1 和 STM32F4 的延时函数看着是入门第一课实际是区分“能跑”和“跑得稳”的分水岭。F1 常见 72MHz 主频、Cortex-M3 内核F4 常见 168MHz、Cortex-M4 带 FPU两者在 SysTick 时钟源、HAL 库时基配置、DWT 周期计数器可用性上都有差异。这篇笔记不堆理论按我实际拆项目的顺序讲先分清几类延时方案的适用边界再落到 F1/F4 各自的配置代码然后把踩过的坑一条条摆出来最后给一个能直接抄的微秒级延时封装。手上正好有这两类板子、或者正在被延时精度和中断安全折磨的人可以顺着往下看。2. 三类延时方案忙等、SysTick 与 DWT 的选型逻辑在动手写代码前得先把“延时”这件事拆开。STM32 上能用的延时手段不止一种选错了不是精度问题是功能直接失效。我一般按三个维度判断延时长度、是否在中断里调用、要不要微秒级精度。2.1 软件忙等最短平快但别指望它准最原始的做法就是空循环靠循环次数凑时间。它的优点是没有任何外设依赖复位后立刻能用连时钟都没配好时也能顶一下。缺点是精度完全靠编译器和主频-O0和-O2编出来的循环次数能差好几倍主频一变就得重算。// 最粗糙的忙等仅用于对精度无要求的场景 void delay_loop(volatile uint32_t count) { while (count--) { __NOP(); // 防止编译器把空循环优化掉 } }这里的volatile和__NOP()是关键。如果count不加volatile开优化后编译器可能直接把整个循环删掉延时归零。但即便加了这个函数的实际延时也随主频线性变化F1 的 72MHz 和 F4 的 168MHz 下同一份代码差一倍多。所以忙等只适合初始化早期、或者对时间完全没要求的场合正式逻辑里我不推荐用它做通信时序。2.2 SysTick 计数延时HAL 库的默认答案也是重灾区HAL_Delay是大多数人第一个接触的延时函数底层用 Cortex-M 内核自带的 SysTick 定时器每 1ms 产生一次中断中断里把uwTick加一HAL_Delay就轮询这个变量直到差值够。它的好处是跨 F1/F4 通用HAL 库自动配好不用自己算重装载值。问题也出在这。第一它最小分辨率是 1ms要微秒级延时它给不了。第二它依赖 SysTick 中断在中断服务函数里调用它如果当前中断优先级高于或等于 SysTick 中断优先级uwTick永远不会更新直接死等。第三F1 和 F4 的 HAL 库对 SysTick 时钟源处理不同F1 的 HAL 默认把 SysTick 时钟设成 HCLK/8F4 默认设成 HCLK这会影响HAL_InitTick里重装载值的计算改主频时如果没同步更新HAL_Delay(1000)可能实际延时不是 1 秒。// HAL_Delay 的典型实现逻辑简化 void HAL_Delay(uint32_t Delay) { uint32_t tickstart HAL_GetTick(); uint32_t wait Delay; if (wait HAL_MAX_DELAY) { wait (uint32_t)(uwTickFreq); // 补偿 1 个 tick } while ((HAL_GetTick() - tickstart) wait) { // 空转等待依赖 SysTick 中断更新 uwTick } }看这段代码就明白为什么中断里不能用HAL_GetTick()读的就是uwTick而uwTick只在 SysTick 中断里自增。你在一个优先级更高的中断里等它变它永远不变。这是 F1/F4 通病跟芯片型号无关只跟中断优先级配置有关。2.3 DWT 周期计数器微秒级延时的正解如果要微秒级精度又不想占用定时器外设Cortex-M3/M4 内核里的 DWTData Watchpoint and Trace是个好选择。它有个CYCCNT寄存器每个内核时钟周期加一读它就能拿到周期数除以主频就是时间。F1 是 M3、F4 是 M4两者都带 DWT所以这套方案能跨型号复用。用 DWT 前要手动使能先开DEMCR的TRCENA位再清零DWT_CYCCNT然后置DWT_CTRL的CYCCNTENA位。使能后CYCCNT就自由运行最大能计到 2^32 个周期F4 在 168MHz 下约 25 秒溢出一次做短延时完全够用。// DWT 初始化F1/F4 通用 void DWT_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; // 使能跟踪 DWT-CYCCNT 0; // 清零周期计数 DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; // 启动 CYCCNT } // 微秒级延时基于 DWT 周期计数 void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000U); // 换算成周期数 while ((DWT-CYCCNT - start) ticks) { // 忙等但精度到周期级 } }SystemCoreClock是 CMSIS 里的全局变量F1 默认 72M、F4 默认 168MHAL 库在时钟配置后会更新它所以这段代码换芯片不用改。注意ticks用无符号减法比较能自然处理CYCCNT溢出的情况这是很多人手写时容易忽略的边界。三类方案摆在一起选型就清楚了初始化早期用忙等顶一下毫秒级且不在中断里用HAL_Delay微秒级或中断内用 DWT。下面按 F1 和 F4 分别说配置细节。3. F1 与 F4 的时基配置差异SysTick 时钟源和 HAL 版本同样是 HAL 库F1 和 F4 在时基这块的默认行为不一样直接照搬代码很容易出现“F1 上好好的F4 上延时翻倍”这种事。这一章把差异点摊开顺便给出改法。3.1 SysTick 时钟源F1 默认 8 分频F4 默认不分频Cortex-M 的 SysTick 可以选择内核时钟HCLK或 HCLK/8 作为时钟源由SysTick-CTRL的CLKSOURCE位决定。F1 的 HAL 库在HAL_InitTick里默认用 HCLK/8F4 默认用 HCLK。这意味着同样调HAL_InitTick(TICK_INT_PRIORITY)F1 的 SysTick 输入是 9MHz72M/8F4 是 168MHz。重装载值的计算公式是reload (SystemCoreClock / (1000U / uwTickFreq)) - 1但 F1 的库内部会先做一次分频判断。如果你在 F1 上手动把CLKSOURCE改成 HCLK却没同步改重装载值HAL_Delay就会快 8 倍。反过来在 F4 上改成 HCLK/8 而不改重装载就慢 8 倍。这个坑我在两个平台上都踩过示波器一测就现形。// 查看当前 SysTick 配置调试时很有用 void check_systick_config(void) { uint32_t clk_source SysTick-CTRL SysTick_CTRL_CLKSOURCE_Msk; uint32_t reload SysTick-LOAD; // clk_source 非 0 表示用 HCLK为 0 表示用 HCLK/8 // reload 是当前重装载值可反推实际 tick 周期 printf(CLKSOURCE%lu, LOAD%lu\r\n, clk_source, reload); }调试时把这两个值打出来和SystemCoreClock一对就能算出实际 tick 周期比盲猜快得多。3.2 HAL 库版本对uwTickFreq的处理较新的 HAL 库引入了uwTickFreq变量表示 tick 频率默认HAL_TICK_FREQ_1KHZ也就是 1ms 一个 tick。HAL_Delay里那句wait (uint32_t)(uwTickFreq)就是补偿这个。但 F1 和 F4 的库版本更新节奏不同有些 F1 的旧工程里uwTickFreq根本没定义直接编译报错。遇到这种情况要么升级 HAL 库到带uwTickFreq的版本要么在HAL_Delay里手动去掉那行补偿。我一般倾向升级库因为uwTickFreq还影响HAL_GetTick的语义混用容易出玄学问题。升级后记得把stm32f1xx_hal_conf.h里的HAL_TICK_FREQ_DEFAULT确认一遍F1 和 F4 的宏名一样但默认值可能不同。3.3 主频变更后必须重配时基F1 从 72M 超频到 128M、或者 F4 从 168M 降到 84M 省电这类操作后如果没重新调HAL_InitTickHAL_Delay的毫秒就不准了。正确做法是在SystemClock_Config改完 PLL 后重新初始化时基。// 主频变更后重新配置时基F1/F4 通用 void reconfig_tick_after_clock_change(void) { HAL_InitTick(TICK_INT_PRIORITY); // 重新计算重装载值 // 如果之前使能过 DWT这里也要重新校准 DWT_Init(); }TICK_INT_PRIORITY是 SysTick 中断优先级一般设成最低数值最大避免它抢占业务中断。F1 和 F4 的中断优先级分组可能不同改优先级前先确认NVIC_PriorityGroupConfig的设置否则算出来的抢占优先级和子优先级对不上。把时基这块理清楚HAL_Delay的毫秒级延时基本就稳了。接下来是微秒级和中断安全的实战封装。4. 微秒延时与中断安全DWT 封装和临界区处理毫秒级用HAL_Delay没问题但通信时序、传感器单总线协议、WS2812 这类场景要的是微秒甚至百纳秒级而且经常在中断里调用。这一章给一套能直接用的封装并说清中断里怎么保证安全。4.1 一套跨 F1/F4 的 delay_us / delay_ms 封装把 DWT 初始化和微秒延时打包再补一个基于 DWT 的毫秒延时这样中断里也能用不依赖 SysTick。// delay.h #ifndef __DELAY_H #define __DELAY_H #include stm32f1xx.h // F4 工程改成 stm32f4xx.h void DWT_Init(void); void delay_us(uint32_t us); void delay_ms(uint32_t ms); #endif// delay.c #include delay.h void DWT_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000U); while ((DWT-CYCCNT - start) ticks) { __NOP(); } } void delay_ms(uint32_t ms) { while (ms--) { delay_us(1000); } }delay_ms用循环调delay_us实现好处是全程不碰中断中断里也能安全调用。代价是长延时时 CPU 全程忙等功耗高。如果对功耗敏感毫秒级还是用HAL_Delay配合低功耗模式更合适。delay_us里的__NOP()不是必须但加上能让忙等循环的节奏更稳定减少流水线预取带来的抖动。4.2 中断里调用延时的优先级陷阱即便用了 DWT 方案中断里调延时仍有一个隐患如果延时期间来了更高优先级中断当前延时会拉长。这是所有忙等延时的通病无法完全避免只能控制。我的做法是在时序敏感的中断里把延时压到最短并且把该中断的优先级设成高于可能打断它的其他中断。F1 和 F4 的 NVIC 优先级位数不同F1 是 4 位F4 也是 4 位但分组方式要统一。用HAL_NVIC_SetPriority时抢占优先级数值越小越高。如果延时中断的抢占优先级是 5而另一个中断是 3那后者能打断前者延时就被拉长。把延时中断设成 0 或 1基本能保证不被业务中断打断。// 把时序敏感中断设成高抢占优先级 HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0); // 抢占 0子优先级 0 HAL_NVIC_EnableIRQ(TIM2_IRQn);注意 SysTick 中断优先级一般设成 15最低这样它不会打断任何业务中断但反过来业务中断里也不能调HAL_Delay因为 SysTick 被压住了。这就是为什么中断里要用 DWT 方案。4.3 用定时器做非阻塞延时如果既不想忙等又要在主循环里做定时任务可以用一个硬件定时器做时基配合状态机。比如用 TIM3 每 100us 中断一次维护一个全局 tick主循环里判断差值。这种方式 CPU 占用低适合多任务轮询。volatile uint32_t tim3_tick 0; void TIM3_IRQHandler(void) { if (TIM3-SR TIM_SR_UIF) { TIM3-SR ~TIM_SR_UIF; tim3_tick; } } // 非阻塞判断是否到达指定延时 uint8_t delay_elapsed(uint32_t start, uint32_t timeout_ticks) { return ((tim3_tick - start) timeout_ticks); }tim3_tick要加volatile因为它在中断里改、主循环里读。delay_elapsed用无符号减法能处理 tick 溢出。这套方案在 F1 和 F4 上都能用区别只是定时器时钟频率不同配置时注意 APB1 预分频。F1 的 TIM3 挂在 APB172M 下 APB1 是 36M定时器时钟可能倍频到 72MF4 的 APB1 是 42M定时器时钟 84M。算预分频值时别搞错。封装和中断安全讲完下面把实际踩过的坑集中列一下这些都是血泪经验能帮你省几天调试时间。5. 延时函数避坑排查五个真实翻车现场这一章按“现象 → 原因 → 解决”的格式写每条都是我在 F1 或 F4 项目里实际遇到的。有些坑看着低级但在赶进度时特别容易犯。5.1 现象中断里调 HAL_Delay 后程序卡死原因HAL_Delay依赖 SysTick 中断更新uwTick而当前中断优先级高于或等于 SysTick 优先级时SysTick 中断被屏蔽uwTick不再变化while循环永远出不来。解决中断里改用 DWT 的delay_us或者把 SysTick 优先级设成最高但这样会影响其他中断响应。更稳妥的做法是中断里只置标志位延时逻辑放到主循环。5.2 现象F4 上 HAL_Delay(1000) 实际延时约 125ms原因SysTick 时钟源被设成 HCLK/8但重装载值按 HCLK 算导致 tick 周期变成 8ms 而不是 1msHAL_Delay(1000)只等了 125 个 tick。解决检查SysTick-CTRL的CLKSOURCE位确认和HAL_InitTick里的分频设置一致。F4 默认用 HCLK如果手动改成 HCLK/8必须同步改重装载值。5.3 现象DWT 延时在 F1 上正常F4 上偏短原因SystemCoreClock变量没更新。F4 工程如果没调SystemCoreClockUpdate()或者时钟配置后没刷新这个变量delay_us里的换算就用了旧主频算出的周期数偏小。解决在SystemClock_Config末尾调SystemCoreClockUpdate()确保SystemCoreClock等于实际 HCLK。调试时打印这个值确认。5.4 现象delay_us(1) 实际延时远大于 1us原因函数调用开销和循环判断本身耗时。在 168MHz 下一次函数调用加几次判断可能就几十个周期delay_us(1)的ticks只有 168实际执行时间被开销吃掉大半。解决微秒级延时不要低于 5us更短的延时直接用__NOP()拼。如果非要精确到 1us 以内用硬件定时器单次触发别用软件忙等。5.5 现象开了编译器优化后延时变短或消失原因忙等循环里的变量没加volatile或者循环体被判定为无副作用编译器直接优化掉。解决延时函数里的计数变量加volatile循环体里加__NOP()或__DSB()。但注意volatile会阻止优化可能让延时变长需要重新校准。这五条覆盖了大部分延时相关的翻车场景。排查时我的习惯是先用示波器或逻辑分析仪测实际波形别靠感觉猜一测就知道是快是慢。6. 进阶技巧用 DWT 做代码段耗时测量与校准延时函数写到后面真正有价值的不是“能延时”而是“知道实际延时了多少”。DWT 的CYCCNT除了做延时还能反过来测代码执行时间这对优化时序、验证延时精度特别有用。我现在的习惯是任何涉及时序的模块先用 DWT 测一遍实际耗时再决定延时参数。6.1 用 CYCCNT 测量任意代码段耗时思路很简单进代码段前读一次CYCCNT出代码段再读一次差值除以主频就是秒数。注意CYCCNT是 32 位两次读取之间不能溢出F4 在 168MHz 下约 25 秒溢出测短代码段完全够。// 测量代码段耗时返回微秒数 uint32_t measure_us(void (*func)(void)) { uint32_t start DWT-CYCCNT; func(); // 执行被测代码 uint32_t end DWT-CYCCNT; return (end - start) / (SystemCoreClock / 1000000U); } // 使用示例 void test_delay(void) { delay_us(100); } // 调用uint32_t t measure_us(test_delay); // t 应该接近 100偏差大说明换算或主频有问题这个函数把被测代码包成函数指针传入避免编译器把测量代码和被测量代码混在一起优化。实测时t会比 100 略大因为函数调用和测量本身有开销一般多出几微秒属于正常。6.2 校准 delay_us 的偏差不同优化等级、不同主频下delay_us的实际输出和设定值会有固定偏差。我一般写一个校准表把常用延时值的实测结果记下来需要精确时查表补偿。设定值 (us)F1 72MHz 实测 (us)F4 168MHz 实测 (us)偏差趋势57.26.1偏大1012.511.3偏大5052.151.0偏大100102.3101.2偏大500502.5501.4偏大从表里能看出偏差主要是固定开销延时越长相对偏差越小。如果项目对短延时精度要求高可以在delay_us里减去一个校准常数比如 F1 上减 2us、F4 上减 1us。但校准常数随优化等级变换编译选项要重新测。6.3 把测量逻辑做成条件编译正式发布时不想带测量代码用宏开关控制。#ifdef DELAY_DEBUG #define DELAY_MEASURE_BEGIN() uint32_t _t0 DWT-CYCCNT #define DELAY_MEASURE_END() printf(cost %lu us\r\n, \ (DWT-CYCCNT - _t0) / (SystemCoreClock / 1000000U)) #else #define DELAY_MEASURE_BEGIN() #define DELAY_MEASURE_END() #endif调试时定义DELAY_DEBUG发布时去掉代码零开销。这套宏我一般放在公共头文件里哪个模块要测就插一对。从那以后我每次在新板子上跑延时相关代码都强制先用 DWT 测一遍实际波形和耗时确认无误再往下写业务逻辑。这个习惯帮我省掉了至少三次返工。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

Halcon焊球检测实战:ball.hdev例程拆解与产线优化指南

Halcon焊球检测实战:ball.hdev例程拆解与产线优化指南

1. 从一张焊球照片说起:这个例程到底在解决什么问题球焊,也就是业内常说的BGA焊球检测,是电子制造里非常典型的一类视觉任务。芯片封装完成后,底部会有一排排细小的焊球,这些焊球负责把芯片和电路板电气连接起来。焊球…

📅 2026/10/19 13:53:58
AI社交圈怎么搭?多智能体协作系统设计实战指南

AI社交圈怎么搭?多智能体协作系统设计实战指南

1. 从围观到入局:AI社交圈到底是什么说实话,我第一次看到“AI社交圈:围观机器人的奇妙互动”这个标题的时候,第一反应是:这不就是给一堆AI聊天机器人建了个群,然后让它们自己聊?但真深入去了解一…

📅 2026/10/19 13:53:58
Python+Django+小程序:停车场预约推荐系统全栈实战

Python+Django+小程序:停车场预约推荐系统全栈实战

真正把"Python Django 小程序"这套组合用在停车场预约项目上,会遇到很多文档里没写清楚的实际问题。我最近做完了一个智能停车场预约推荐系统,项目代号就叫这个标题里的"12ob0wt2",整体跑下来的体会是:这类…

📅 2026/10/19 13:48:57
MORE NEWS

更多资讯

📰

AI写作如何支撑公众号矩阵:一个人批量运营多号的完整方法

做公众号矩阵这件事,我是从去年才开始认真折腾的。起因很直接——手头几个号分别对应不同的内容方向,靠我一个人晚上下班后一篇篇手工码字,根本写不过来。后来我把AI写作真正纳入生产流程,才摸索出一套一个人也能批量运营多个账号…

📰

代码评审工程化:用open-code-review构建高效自动化评审体系

代码评审这事儿,做了这么多年,我见过太多团队把 Review 做成“走过场”。打开 Pull Request 页面,扫一眼标题,鼠标滚轮滑到最底部,点一下 Approve,完事。你以为你在做质量保障,实际上你只是在给…

📰

Open Code Review:基于Git Diff与本地LLM Agent的可审计代码评审范式

1. “open-code-review”不是个工具名,而是一类新型代码评审范式的代号最近在几个技术社区里频繁看到“open-code-review”这个词,它不像 git clone 或 npm install 那样指向某个具体命令,也不像 SonarQube、CodeClimate 那样对应一个安装包。…

📰

VNC、SPICE、RDP远程桌面协议选型与配置实战指南

远程桌面协议这个词,很多刚接触运维或者跨设备办公的朋友第一次听到会觉得很抽象。其实说白了就一件事:把另一台机器的画面搬到你这台机器上,同时把你的键盘鼠标操作送回去。听起来简单,但真到选型的时候,VNC、SPICE、…

📰

蓝牙协议仿真结果分析:指标判读、常见异常与排查思路

做通信协议仿真的人应该都有同感:跑完仿真只是万里长征第一步,真正费心思的是仿真结束之后那一大堆统计数据怎么读、怎么算、怎么从曲线里看出门道。这篇是蓝牙协议仿真系列的第12篇,专讲蓝牙协议仿真结果分析这个环节。内容围绕仿真跑完之后…

📰

Node版本不兼容怎么办?nvm-windows切换实战指南

上个月我打开一个维护了大半年的前端项目,习惯性在终端敲下 npm run dev ,结果迎面涌出一片黄色警告:某个依赖声明只支持 Node 20 以下版本,而当前环境已经是 22.x。页面倒是勉强起来了,但样式全乱,按钮间…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬