|

关注公众号,回复“入门资料”获取单片机入门到高级开挂教程
开发板带你入门,我们带你飞
文 | 无际(微信:2777492857)
全文约10950字,阅读大约需要 15 分钟
不管你是刚入门,对着 LED 闪烁心满意足的萌新,还是已经能熟练配置 DMA、玩转 USB 的老司机,有一个话题几乎是所有人都绕不开的——延时。
简单如控制 LED 亮灭间隔,复杂如遵循特定时序的传感器通信,延时无处不在。它就像代码里的“逗号”和“句号”,控制着程序的节奏。然而,看似简单的延时,实现起来却五花八门,里面的门道可不少。
你是不是也曾:
随手写个 for 循环空转,结果延时时长全凭“感觉”?
直接调用 HAL_Delay(),用得飞起,却不知其所以然,甚至有时还“坑”了自己?
听说过 SysTick、硬件定时器,甚至 RTOS 延时,但总觉得“不明觉厉”,不知道该用哪个?
或者,在某个需要精确延时的场合,被折腾得焦头烂额,恨不得把时钟周期掰开来数?
别慌!今天,我们就来一次性把 STM32 的延时方法扒个底朝天,从最简单粗暴的“傻等”,到最高效智能的“调度”,逐一分析它们的原理、优缺点和适用场景。
我们的目标是:不仅知道有哪些方法,更要理解为什么,最终能在不同的场景下,像个经验丰富的“老中医”一样,对症下药,开出最合适的“延时药方”。
准备好了吗?发车!
一、方法一:死循环大法
这是最直观,可能也是不少人(包括曾经的我)最早接触的延时方法。
原理简单粗暴:让 CPU 在原地不停地执行一些无意义的指令,直到达到指定的循环次数,以此消耗掉时间。
// 一个极其简陋的软件延时 (微秒级示意)void Delay_us_Software(volatile uint32_t us){ volatile uint32_t i, j; // 这个循环次数需要根据你的 CPU 时钟频率精确校准 // 这里只是个示意,实际数值需要测试确定 for (i = 0; i { for (j = 0; j 10; j++) // 内循环消耗一定时间 { __NOP(); // NOP 指令,空操作,消耗一个时钟周期 // 有时也直接用空循环体 ; } }}// 毫秒级延时 (同样需要校准)void Delay_ms_Software(volatile uint32_t ms){ volatile uint32_t i; for (i = 0; i { // 调用微秒延时,或者一个更大的循环 Delay_us_Software(1000); // 示意性调用 }}
剖析:
?优点:
?简单直观: 理解和实现门槛极低,不需要配置任何外设。
?不依赖硬件(除了CPU本身): 理论上,只要有 CPU 和时钟就能跑。
?缺点(敲黑板,重点来了!):
?精度极差,校准困难:
?CPU 时钟依赖: 延时时间直接取决于 CPU 的运行频率。SystemcoreClock 一变,延时全乱套。如果你用了 HSI、HSE、PLL,时钟配置稍微一改,之前的校准就得推倒重来。
?编译器优化: 现代编译器非常智能,它看到你写了个空循环,可能会觉得:“这小子在浪费时间!”然后大笔一挥,直接给你优化掉!或者优化得面目全非。使用 volatile 关键字可以部分缓解,但不能完全保证。__NOP() 指令相对稳定些,但大量使用也影响效率和可读性。
?指令执行时间不确定性: 不同指令、不同流水线状态下,执行时间可能有微小差异。
?中断搅局: 如果在你的“傻等”期间,发生了一个中断,CPU 跑去处理中断服务程序(ISR),回来后,你的延时时间就被无情地拉长了。中断越多、ISR 越长,误差越大。
?CPU 资源浪费: 这是最致命的缺点!在执行软件延时的时候,CPU 就像一个被按住暂停键的工人,除了原地踏步(执行空指令),啥也干不了!它 100% 被占用,无法响应其他任务、处理其他事件。对于需要同时处理多个任务(比如,一边延时,一边还要检测按键、接收串口数据)的应用来说,这是绝对不可接受的。想象一下,为了等 1 秒钟,整个系统“冻结”1 秒,用户体验能好吗?
?功耗: CPU 全速空转,功耗自然降不下来。在低功耗应用场景,这种方法简直是“电量刺客”。
?适用场景:
?极短、极特殊、非精确的延时(比如几个时钟周期的等待)。
?系统初始化早期,其他定时服务还没准备好时的临时措施。
?某些对时序要求非常严格(精确到指令周期级别),且能确保期间无中断、时钟稳定的特殊硬件操作(但这种情况非常罕见,且通常有更好的硬件方法)。
强烈建议:尽可能避免在正式项目中使用纯软件循环延时,尤其是毫秒级以上的延时。 它就像武侠小说里的“七伤拳”,伤敌(延时)一千,自损(CPU 资源)八百。
二、方法二:SysTick 定时器
几乎所有的 Cortex-M 内核(STM32 家族的核心)都内置了一个叫 SysTick 的定时器。
它是一个 24 位的递减计数器,设计初衷是为了给操作系统(OS)提供一个周期性的时钟节拍(Tick),但我们完全可以“征用”它来实现延时。
实现方式:
STM32 的 HAL 库(Hardware Abstraction Layer)已经为我们封装好了基于 SysTick 的延时函数:HAL_Delay()。
1.HAL 库初始化: 在 HAL_Init() 中,通常会配置 SysTick 定时器,使其每 1ms 产生一次中断(这是默认配置,也可修改)。
2.SysTick 中断服务函数 (SysTick_Handler): 在这个中断函数(通常在 stm32fxxx_it.c 文件中)里,会调用 HAL_IncTick()。这个函数的作用是让一个全局变量(通常是 uwTick)自增 1。这个 uwTick 就成了系统的时间基准(单位:毫秒)。
3.HAL_Delay(uint32_t Delay) 函数:
?记录下当前的 uwTick 值。
?进入一个 while 循环。
?在循环里不断检查当前的 uwTick 值与之前记录的值之差,是否小于要延时的毫秒数 Delay。
?如果不小于,就跳出循环,延时结束。
// HAL_Delay 的简化逻辑示意void HAL_Delay(uint32_t Delay){ uint32_t tickstart = HAL_GetTick(); // 获取当前 uwTick 值 uint32_t wait = Delay; // 防止 uwTick 溢出导致的问题 (虽然溢出概率低,但考虑周全) if (wait { wait += (uint32_t)(uwTickFreq); // uwTickFreq 通常是 1,代表1ms } while ((HAL_GetTick() - tickstart) { // 在这里等待 // CPU 在这里其实还是在循环检查,但不是空转整个延时时间 // 它会在每个 SysTick 中断之间“摸鱼” }}// 你需要确保 SysTick_Handler 被正确配置和调用// 在 stm32fxxx_it.c 中:void SysTick_Handler(void){ HAL_IncTick(); // 如果你用了 HAL 库的其他基于 SysTick 的超时机制,这里可能还有 HAL_SYSTICK_IRQHandler();}
剖析:
?优点:
?使用方便: HAL 库封装好了,直接调用 HAL_Delay() 即可,无需关心底层配置。
?精度相对较高(毫秒级): 基于硬件定时器,不受编译器优化影响,只要时钟稳定,毫秒级延时比较准确。
?标准化: SysTick 是 Cortex-M 标准,代码可移植性较好。
?CPU “部分”解放: 虽然 HAL_Delay 函数本身是阻塞的(调用它的代码会停在那里等待),但在等待期间,CPU 并不是 100% 空转。它会在两次 SysTick 中断之间执行 while 循环检查。如果 SysTick 周期是 1ms,那么 CPU 大约每 1ms 会忙一下(检查时间),其他时间理论上可以被中断抢占去干别的事。这比纯软件空转效率高得多。
?缺点:
?仍然是阻塞式延时: 调用 HAL_Delay() 的任务/代码流会被阻塞,无法执行后续代码,直到延时结束。如果你在主循环里调用一个长延时,系统响应性会变差。
?精度限制: 默认精度是 1ms。虽然可以配置 SysTick 产生更高频率的中断(比如 10us、100us),但这会增加中断开销,并且 HAL_Delay() 本身是按毫秒设计的。实现微秒级延时通常不直接用 HAL_Delay()。
?SysTick 资源占用: SysTick 定时器只有一个。如果你的项目使用了实时操作系统(RTOS),RTOS 通常会“霸占” SysTick 来作为系统的心跳时钟。这时,你再用 HAL_Delay() 或者手动配置 SysTick 可能会与 RTOS 冲突,导致不可预知的问题(比如系统节拍紊乱)。
?中断优先级问题:HAL_GetTick() 读取的 uwTick 是在 SysTick_Handler 中更新的。如果 HAL_Delay() 被一个更高优先级的中断打断,且该中断执行时间很长,uwTick 可能在这期间无法更新,导致实际延时时间变长。同时,SysTick_Handler 的优先级也需要合理配置。
?适用场景:
?简单的、非实时性要求高的延时: 例如,初始化外设后的短暂等待、按键消抖、控制慢速设备(如某些 LCD 显示)。
?裸机(无 RTOS)系统: 在不使用 RTOS 的情况下,HAL_Delay() 是一个非常方便可靠的毫秒级延时选择。
?调试: 临时加入短暂延时观察现象。
三、方法三:软件定时器架构
前面我们讨论了直接使用硬件定时器 (TIM) 来实现精确延时,无论是阻塞式轮询还是中断式非阻塞。
这对于单个、高精度的延时需求非常有效。但如果你的系统需要同时管理多个、周期性或一次性的定时任务呢?比如:
?LED 每 500ms 闪烁一次。
?每隔 1 秒读取一次传感器数据。
?按键按下后,需要延时 20ms 进行消抖处理。
?某个通信协议要求在发送后等待 100ms 再接收。
为每个任务都单独配置一个硬件 TIM 显然是不现实的,STM32 的 TIM 资源虽然不少,但也经不起这么挥霍。而且,如果都用中断方式,中断嵌套和管理也会变得复杂。
这时,一种更优雅、更通用的方法应运而生——软件定时器架构。
我们无际单片机项目3和6的就是采用这种定时架构,我们实际产品一直在用,简直不要太爽。
w4z5peom2z264041790817.png
下面给大家大概讲解下。
核心思想:
1.统一的时间基准 (Tick): 使用一个硬件定时器(SysTick 是绝佳选择,因为它通常被 HAL 库或 RTOS 用作系统节拍;或者也可以用一个通用 TIM)配置成周期性地产生中断,这个中断的周期就是我们整个软件定时器系统的最小时间单位,称为“系统节拍”或“Tick”(例如,1ms 或 10ms)。
2.软件定时器数据结构: 定义一个结构体来描述每一个逻辑上的“软件定时器”。这个结构体至少包含:
?定时器的状态(运行、停止、暂停等)。
?定时周期(需要多少个 Tick)。
?当前计数值(已经过去了多少个 Tick)。
?定时模式(一次性触发还是周期性触发)。
?到期后要执行的回调函数 (Callback Function)。
3.定时器管理数组: 创建一个该结构体的数组,用于存储所有需要管理的软件定时器实例。
4.Tick 更新机制: 在硬件定时器的中断服务程序 (ISR) 中,不做复杂的处理,只做一件最核心的事:通知主程序一个 Tick 已经到来。通常是设置一个全局标志位,或者使用更高级的机制如信号量(在 RTOS 中)。
5.主循环调度 (while(1)): 在 main 函数的 while(1) 循环中,不断地检查那个全局 Tick 标志位。
?如果标志位被置位,表示一个 Tick 时间过去了。
?主循环清除标志位。
?调用一个软件定时器处理函数。这个函数遍历定时器管理数组:
?对于每个处于“运行”状态的软件定时器,将其“当前计数值”加 1(或其他递减逻辑)。
?检查是否有定时器的计数值达到了其“定时周期”。
?如果达到周期:
?执行该定时器对应的**回调函数**。
?根据定时模式(一次性/周期性)更新定时器的状态(停止/重新开始计数)。
这种架构的精髓在于“合作式”调度:硬件定时器提供精准的“心跳”,而实际的定时器逻辑处理和回调函数执行则放在主循环中,由主循环主动检查和触发。
这避免了在 ISR 中执行过多代码,降低了中断处理时间,也使得回调函数的执行环境相对简单(就在主循环的上下文中)。
实现示例(基于 SysTick,HAL 库风格,主循环轮询标志位):
1. 定义软件定时器 (soft_timer.h)
#ifndef __SOFT_TIMER_H#define __SOFT_TIMER_H#include "stm32f1xx_hal.h" // 根据你的 STM32 型号选择头文件#include #include // For NULL// --- 配置项 ---#define SOFT_TIMER_MAX_TIMERS 10 // 最大支持的软件定时器数量#define SOFT_TIMER_TICK_MS 1 // 系统 Tick 的周期 (毫秒) - 需要与 SysTick 配置一致// --- 类型定义 ---// 定时器 ID (用枚举或索引)typedef uint8_t SoftTimerID_t;// 定时器状态typedef enum { TIMER_STATE_STOPPED = 0, TIMER_STATE_RUNNING = 1,} SoftTimerState_t;// 定时器模式typedef enum { TIMER_MODE_ONE_SHOT = 0, // 一次性 TIMER_MODE_PERIODIC = 1, // 周期性} SoftTimerMode_t;// 回调函数指针类型typedef void (*SoftTimerCallback_t)(void);// 软件定时器结构体typedef struct { SoftTimerState_t state; // 当前状态 (运行/停止) SoftTimerMode_t mode; // 模式 (一次性/周期性) uint32_t period_ticks; // 定时周期 (单位: Tick) uint32_t current_ticks; // 当前计数值 (单位: Tick) SoftTimerCallback_t callback; // 到期回调函数 uint8_t is_used; // 标记此定时器槽位是否被占用} SoftTimer_t;// --- 函数原型 ---/** * @brief 初始化软件定时器模块 (包括配置 SysTick) * @retval None */void SoftTimers_Init(void);/** * @brief 创建一个新的软件定时器 * @param mode 定时器模式 (一次性/周期性) * @param period_ms 定时周期 (单位: 毫秒) * @param callback 到期回调函数 * @retval SoftTimerID_t 定时器 ID (>=0 表示成功, * 注意:这里用 uint8_t 做 ID,可以用一个特殊值如 0xFF 表示失败 */SoftTimerID_t SoftTimer_Create(SoftTimerMode_t mode, uint32_t period_ms, SoftTimerCallback_t callback);/** * @brief 启动一个软件定时器 * @param id 要启动的定时器 ID * @retval HAL_StatusTypeDef HAL_OK 表示成功, HAL_ERROR 表示失败 (ID 无效或未创建) */HAL_StatusTypeDef SoftTimer_Start(SoftTimerID_t id);/** * @brief 停止一个软件定时器 * @param id 要停止的定时器 ID * @retval HAL_StatusTypeDef HAL_OK 表示成功, HAL_ERROR 表示失败 */HAL_StatusTypeDef SoftTimer_Stop(SoftTimerID_t id);/** * @brief 删除一个软件定时器 (释放槽位) * @param id 要删除的定时器 ID * @retval HAL_StatusTypeDef HAL_OK 表示成功, HAL_ERROR 表示失败 */HAL_StatusTypeDef SoftTimer_Delete(SoftTimerID_t id);/** * @brief 复位一个软件定时器的计数值 (不改变状态) * @param id 要复位的定时器 ID * @retval HAL_StatusTypeDef HAL_OK 表示成功, HAL_ERROR 表示失败 */HAL_StatusTypeDef SoftTimer_Reset(SoftTimerID_t id);/** * @brief 软件定时器 Tick 处理函数 (应在 main 循环中调用) * @retval None */void SoftTimers_TickHandler(void);/** * @brief 获取 Tick 标志位 (供 main 循环查询) * @retval uint8_t 1 表示 Tick 到来, 0 表示未到来 */uint8_t SoftTimers_GetTickFlag(void);/** * @brief 清除 Tick 标志位 (供 main 循环清除) * @retval None */void SoftTimers_ClearTickFlag(void);#endif // __SOFT_TIMER_H
2. 实现软件定时器 (soft_timer.c)
#include "soft_timer.h"// --- 全局变量 ---static SoftTimer_t g_soft_timers[SOFT_TIMER_MAX_TIMERS]; // 定时器实例数组static volatile uint8_t g_soft_timer_tick_flag = 0; // Tick 到来标志位// --- 内部函数 ---/** * @brief 根据毫秒计算所需的 Ticks */static uint32_t ms_to_ticks(uint32_t ms) { if (ms == 0) return 0; uint32_t ticks = ms / SOFT_TIMER_TICK_MS; // 至少为 1 个 tick,避免周期为 0 return (ticks == 0) ? 1 : ticks;}// --- 公共函数实现 ---void SoftTimers_Init(void) { // 1. 初始化定时器数组 for (int i = 0; i g_soft_timers.is_used = 0; g_soft_timers.state = TIMER_STATE_STOPPED; g_soft_timers.callback = NULL; } // 2. 配置 SysTick 定时器 // 确保 HAL_Init() 已经被调用 // 配置 SysTick 每 SOFT_TIMER_TICK_MS 毫秒中断一次 // HAL_SYSTICK_Config 的参数是 HCLK 频率下的计数值 // HCLK / (1000 / SOFT_TIMER_TICK_MS) // 例如 HCLK=72MHz, TICK=1ms -> 72000000 / 1000 = 72000 // 注意: HAL_Init() 默认可能配置为 1ms Tick, 如果与 SOFT_TIMER_TICK_MS 一致则无需重新配置 // 如果需要不同 Tick 频率,需要调用 HAL_SYSTICK_Config() // 例如,强制设置为 1ms Tick: if (SOFT_TIMER_TICK_MS == 1) { // 通常 HAL_Init() 做了这个,或者用默认的即可 // 若不确定或需要修改,取消注释并确保 HCLK 正确 // HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000); } else { // 配置自定义 Tick 周期 HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq() / (1000 / SOFT_TIMER_TICK_MS)); } // 3. 配置 SysTick 中断优先级 (如果需要调整) // HAL_NVIC_SetPriority(SysTick_IRQn, tick_interrupt_priority, 0); // 4. 使能 SysTick 中断 (HAL_SYSTICK_Config 内部通常会使能) // HAL_NVIC_EnableIRQ(SysTick_IRQn); g_soft_timer_tick_flag = 0; // 清除初始标志位}SoftTimerID_t SoftTimer_Create(SoftTimerMode_t mode, uint32_t period_ms, SoftTimerCallback_t callback) { if (callback == NULL || period_ms == 0) { return 0xFF; // 无效参数 } for (SoftTimerID_t id = 0; id if (!g_soft_timers[id].is_used) { g_soft_timers[id].state = TIMER_STATE_STOPPED; g_soft_timers[id].mode = mode; g_soft_timers[id].period_ticks = ms_to_ticks(period_ms); g_soft_timers[id].current_ticks = 0; g_soft_timers[id].callback = callback; g_soft_timers[id].is_used = 1; return id; // 返回创建成功的 ID } } return 0xFF; // 没有可用的定时器槽位}HAL_StatusTypeDef SoftTimer_Start(SoftTimerID_t id) { if (id >= SOFT_TIMER_MAX_TIMERS || !g_soft_timers[id].is_used) { return HAL_ERROR; } // 启动时重置计数值 g_soft_timers[id].current_ticks = 0; g_soft_timers[id].state = TIMER_STATE_RUNNING; return HAL_OK;}HAL_StatusTypeDef SoftTimer_Stop(SoftTimerID_t id) { if (id >= SOFT_TIMER_MAX_TIMERS || !g_soft_timers[id].is_used) { return HAL_ERROR; } g_soft_timers[id].state = TIMER_STATE_STOPPED; return HAL_OK;}HAL_StatusTypeDef SoftTimer_Delete(SoftTimerID_t id) { if (id >= SOFT_TIMER_MAX_TIMERS || !g_soft_timers[id].is_used) { return HAL_ERROR; } g_soft_timers[id].state = TIMER_STATE_STOPPED; g_soft_timers[id].is_used = 0; g_soft_timers[id].callback = NULL; // 清除回调 return HAL_OK;}HAL_StatusTypeDef SoftTimer_Reset(SoftTimerID_t id) { if (id >= SOFT_TIMER_MAX_TIMERS || !g_soft_timers[id].is_used) { return HAL_ERROR; } g_soft_timers[id].current_ticks = 0; // 注意:这里只重置计数值,不改变运行状态 return HAL_OK;}void SoftTimers_TickHandler(void) { for (SoftTimerID_t id = 0; id // 检查定时器是否在使用且处于运行状态 if (g_soft_timers[id].is_used && g_soft_timers[id].state == TIMER_STATE_RUNNING) { // 增加当前 Tick 计数值 g_soft_timers[id].current_ticks++; // 判断是否到达周期 if (g_soft_timers[id].current_ticks >= g_soft_timers[id].period_ticks) { // --- 到达周期 --- // 1. 执行回调函数 if (g_soft_timers[id].callback != NULL) { g_soft_timers[id].callback(); } // 2. 根据模式处理 if (g_soft_timers[id].mode == TIMER_MODE_PERIODIC) { // 周期模式:重置计数值,保持运行状态 g_soft_timers[id].current_ticks = 0; } else { // 一次性模式:停止定时器 g_soft_timers[id].state = TIMER_STATE_STOPPED; } } } }}uint8_t SoftTimers_GetTickFlag(void) { return g_soft_timer_tick_flag;}void SoftTimers_ClearTickFlag(void) { g_soft_timer_tick_flag = 0;}// --- SysTick 中断处理 ---// 这个函数需要在 stm32f1xx_it.c (或对应型号的 it.c) 文件中实现// 或者如果 HAL 库已经定义了 SysTick_Handler, 我们需要调用 HAL_IncTick()// 并在这里设置我们的标志位/*// 在 stm32f1xx_it.c 中:#include "soft_timer.h" // 引入头文件extern volatile uint8_t g_soft_timer_tick_flag; // 声明全局变量void SysTick_Handler(void){ // 如果你使用了 HAL 库并且需要 HAL_Delay 或其他基于 uwTick 的功能, // 必须调用 HAL_IncTick() HAL_IncTick(); // 设置我们的软件定时器 Tick 标志 g_soft_timer_tick_flag = 1; // 如果有其他基于 SysTick 的处理 (如 RTOS 的 Tick), 在这里调用}*/// 注意: 上面的 SysTick_Handler 需要在对应的 stm32fxxx_it.c 文件中实现或修改// 需要 #include "soft_timer.h" 并声明 extern volatile uint8_t g_soft_timer_tick_flag;// 或者,更简单的做法是在 soft_timer.c 中定义 g_soft_timer_tick_flag (去掉 static)// 然后在 soft_timer.h 中用 extern 声明它,这样 it.c 就能直接访问了。// (当前代码 g_soft_timer_tick_flag 是 static, 需要调整可见性)// 改为全局可见的 flag:// soft_timer.c:volatile uint8_t g_soft_timer_tick_flag = 0; // 去掉 static// soft_timer.h:extern volatile uint8_t g_soft_timer_tick_flag; // 声明
3. 在 main.c 中使用
#include "main.h"#include "soft_timer.h" // 引入软件定时器头文件#include "led.h" // 假设有 led 控制函数// --- 回调函数示例 ---void Led1_Toggle_Callback(void) { Led_Toggle(LED1); // 假设 Led_Toggle(LED_TypeDef led) 用于翻转 LED}void Sensor_Read_Callback(void) { // 在这里添加读取传感器的代码 // printf("Reading sensor...\r
");}void Button_Debounce_Callback(void) { // 按键消抖完成,在这里处理按键事件 // printf("Button debounced and processed.\r
");}int main(void) { HAL_Init(); // 初始化 HAL 库 (包含 SysTick 基础配置) SystemClock_Config(); // 配置系统时钟 MX_GPIO_Init(); // 初始化 GPIO (假设 CubeMX 生成) Led_Init(); // 初始化 LED // --- 初始化软件定时器模块 --- SoftTimers_Init(); // !! 重要: 配置 SysTick 并初始化定时器数组 // --- 创建并启动软件定时器 --- SoftTimerID_t timer_led1; SoftTimerID_t timer_sensor; // 创建一个周期性定时器,每 500ms 翻转 LED1 timer_led1 = SoftTimer_Create(TIMER_MODE_PERIODIC, 500, Led1_Toggle_Callback); if (timer_led1 != 0xFF) { SoftTimer_Start(timer_led1); } // 创建一个周期性定时器,每 1000ms 读取一次传感器 timer_sensor = SoftTimer_Create(TIMER_MODE_PERIODIC, 1000, Sensor_Read_Callback); if (timer_sensor != 0xFF) { SoftTimer_Start(timer_sensor); } // 可以在需要时创建一次性定时器,例如按键消抖 // SoftTimerID_t timer_debounce; // if (/* 检测到按键按下 */) { // timer_debounce = SoftTimer_Create(TIMER_MODE_ONE_SHOT, 20, Button_Debounce_Callback); // if (timer_debounce != 0xFF) { // SoftTimer_Start(timer_debounce); // } // } while (1) { // --- 主循环任务 --- // 1. 检查软件定时器 Tick 标志位 if (SoftTimers_GetTickFlag()) { // 检查是否有 Tick 到来 SoftTimers_ClearTickFlag(); // 清除标志位 SoftTimers_TickHandler(); // 处理所有软件定时器逻辑 } // 2. 执行其他非阻塞的应用代码 // 例如: 检查按键输入 (非阻塞方式) // Check_Buttons(); // 例如: 处理串口接收到的数据 // Process_Uart_Data(); // 例如: 更新显示 (如果很快的话) // Update_Display(); // !! 重要: main 循环中的所有任务都应该是短时执行的 !! // !! 不能有长时间阻塞的操作 (如 HAL_Delay 或长时间的计算) !! // !! 否则会影响软件定时器的精度和响应性 !! } // end while(1)} // end main// --- SysTick 中断处理 (需要在 stm32fxxx_it.c 中) ---/*// 在 stm32f1xx_it.c 或相应文件中:#include "soft_timer.h"// 如果 g_soft_timer_tick_flag 在 soft_timer.c 中是全局的:extern volatile uint8_t g_soft_timer_tick_flag;void SysTick_Handler(void){ HAL_IncTick(); // 保持 HAL 库的 Tick 计数 g_soft_timer_tick_flag = 1; // 设置软件定时器 Tick 标志}*/
剖析软件定时器架构(主循环轮询方式):
?优点:
?资源高效: 只需一个硬件定时器(如 SysTick)即可管理多个逻辑定时器。
?简单 ISR: 中断服务程序非常轻量,只设置一个标志位,执行时间极短,降低了对系统实时性的干扰。
?回调函数上下文简单: 所有回调函数都在主循环的上下文中执行,没有中断嵌套、共享资源保护(相对于 ISR)等复杂问题(但要注意回调函数本身不能阻塞主循环)。
?灵活性: 可以方便地创建、启动、停止、删除定时器,支持一次性和周期性模式。
?可移植性好: 更换底层硬件定时器(比如从 SysTick 换成 TIM)只需要修改 SoftTimers_Init() 和 ISR 部分,上层逻辑不变。
?缺点(敲黑板!):
?精度依赖主循环响应速度:SoftTimers_TickHandler() 的执行时机取决于 while(1) 循环检查 g_soft_timer_tick_flag 的频率。
如果主循环中有其他代码执行时间过长(阻塞),或者整个循环迭代一次的时间超过了一个 Tick 周期,那么 SoftTimers_TickHandler() 的调用就会延迟,导致所有软件定时器的精度下降,出现抖动 。
?合作式多任务,非抢占式: 定时器回调函数的执行是非抢占的。如果一个回调函数执行时间很长,它会阻塞主循环,进而阻塞其他软件定时器的处理以及主循环中的所有其他任务。必须确保所有回调函数都是短时、非阻塞的。
?不适用于硬实时场景: 对于需要纳秒或微秒级精确、低抖动响应的硬实时任务,这种软件定时器架构可能无法满足要求。这种场景还是需要硬件 TIM 的中断或 DMA 等机制。
?编程约束: 开发者必须时刻注意保持 main 循环的快速迭代,避免任何形式的阻塞。
?适用场景:
?裸机(无 RTOS)系统: 这是在裸机环境下实现多个定时任务管理的常用且有效的方法。
?非实时性或软实时系统: 对定时精度要求不高(允许几毫秒到几十毫秒的误差或抖动),例如界面刷新、状态轮询、慢速设备控制、常规通信超时等。
?替代多个 HAL_Delay():可以用一次性软件定时器来优雅地替代代码中散落的、阻塞式的 HAL_Delay(),实现非阻塞的延时等待。
?资源受限的 MCU: 当硬件定时器资源紧张时,可以用此方法扩展定时能力。
四、方法四:实时操作系统 (RTOS) 延时
如果你在项目里使用了 RTOS(如 FreeRTOS, RT-Thread, uC/OS 等),那么恭喜你,延时处理会变得既简单又高效。RTOS 的核心是任务调度,它提供了专门的延时函数。
实现方式:
不同的 RTOS API 可能略有不同,但原理相似。以 FreeRTOS 和 CMSIS-RTOS API 为例:
?FreeRTOS:vTaskDelay(TickType_t xTicksToDelay)
?参数 xTicksToDelay 是指要延时的系统节拍 (Tick)数量。如果系统 Tick 是 1ms,vTaskDelay(100) 就是延时 100ms。
?CMSIS-RTOS:osDelay(uint32_t millisec)
?参数 millisec 直接就是要延时的毫秒数。
工作原理:
当你调用 RTOS 的延时函数时,发生的事情与前面几种方法截然不同:
1.任务状态切换: 调用延时函数的那个任务 (Task),会被 RTOS 标记为阻塞态或睡眠态。
2.放弃 CPU: 该任务会主动放弃 CPU 的使用权。
3.调度器接管: RTOS 的调度器会立刻检查是否有其他处于就绪态 (Ready)的任务可以运行。
4.切换上下文: 如果有,调度器会进行上下文切换 ,让那个就绪任务获得 CPU 控制权并开始运行。
5.延时计时: RTOS 内核(通常利用 SysTick 或其他定时器产生的系统 Tick)会记录下被阻塞任务的唤醒时间。
6.时间到达,任务唤醒: 当延时时间到达后,RTOS 内核会在下一个系统 Tick 中断时,将被阻塞的任务重新标记为就绪态。
7.重新调度: 在合适的时机(比如当前运行的任务结束、被更高优先级任务抢占、或发生系统调用),调度器会根据优先级等规则,决定是否让刚刚睡醒的任务重新获得 CPU 执行。
剖析:
?优点:
?真正的非阻塞(对系统而言): 这是 RTOS 延时的核心优势!当一个任务调用延时函数“睡觉”时,CPU 并没有闲着,而是被 RTOS 调度去执行其他可以运行的任务了。整个系统的资源得到了充分利用,响应性极佳。
?使用简单: API 调用非常直观。
?与多任务环境完美契合: 是多任务协作的基础,让复杂的系统逻辑得以清晰实现。
?功耗优化: 当所有任务都处于阻塞态(比如都在等待事件或延时)时,RTOS 可以配合 MCU 的低功耗模式,让 CPU 进入睡眠状态,显著降低功耗。
?缺点:
?需要 RTOS 环境: 必须在项目中引入并配置好 RTOS,这本身就带来了额外的代码体积(Flash)和内存(RAM)开销,以及一定的学习成本。
?延时精度依赖于系统 Tick: 延时的最小单位和精度通常受限于 RTOS 的系统 Tick 周期(一般是 1ms 或 10ms)。虽然 vTaskDelay(1) 意图延时 1 个 Tick,但实际延时可能略大于 1 Tick 到接近 2 Ticks 之间,取决于调用时距离下一个 Tick 有多近。对于需要精确微秒级延时的场景,RTOS 延时可能不够用(除非 Tick 设得非常小,但这会增加系统开销)。
?理解 RTOS 概念: 需要理解任务、调度、优先级、上下文切换等 RTOS 基本概念。
?适用场景:
?使用了 RTOS 的项目: 几乎是标准做法,替代所有非必要的阻塞式延时。
?需要多任务并行处理的复杂系统。
?对系统响应性和资源利用率要求高的场合。
?低功耗应用。
五、终极对决:哪种延时方法是“最佳”?
我们来总结下几种方法:
1.“死循环”大法 (软件延时) - 简单粗暴,但内力消耗巨大(CPU 占用高),准头还差(精度低)。
2.SysTick / HAL_Delay() - 内核标配,方便易用,毫秒级尚可,但依然是“站桩”式(阻塞)功夫。
3.软件定时器架构 (SysTick/TIM + Main Loop) - 以小博大,用一个“心跳”(硬件 Tick)管理众多逻辑定时器,主循环不阻塞,但对整体主循环执行效率要求高。
4.硬件定时器 (TIM) 直接使用 - 高精度延时,可静可动(阻塞轮询/中断非阻塞),但每次只能管理一个精确延时事件,且配置稍繁。
5.RTOS 延时/定时服务 -操作系统亲自调度,高效协同,任务级非阻塞,资源利用最大化,但需要先引入 RTOS。
现在,面对琳琅满目的方法,我们要怎么选择?
答案还是那句老话,但更加明确:没有绝对的“最佳”,只有“最适合你当前战局”的招式!武功再好,用错了地方也是白搭。
六、选择指南
1.你是否使用 RTOS?
?是 (已用 RTOS): 恭喜!优先使用RTOS 延时/定时器服务,如 vTaskDelay, osTimer 等。这是最高效、最符合多任务协作的方式。CPU 在一个任务“休息”时,会被调度去干别的活。基本可以告别 HAL_Delay()和软件延时。只有在极少数需要 Tick 级别以下、硬实时精度的场景,才考虑直接动用硬件 TIM (方法 4)。软件定时器架构 (方法 3)在 RTOS 环境下通常不是首选,因为 RTOS 提供了更完善、抢占式的定时服务。
?否 (裸机系统): 继续往下看,选择更广阔!
2.你需要在不阻塞“主线任务”(main循环)的前提下,同时管理 多个定时事件吗?(比如,LED 每 500ms 闪,传感器每 1s 读一次,还要处理其他逻辑,其实这个是很多产品的需求)
?是:软件定时器架构 (方法 3) 是你的得力助手!它用一个硬件心跳(SysTick 或 TIM)就能驱动多个逻辑定时器,让你的 main 循环保持流畅。但切记: 你的 main 循环本身以及所有定时器的回调函数,都必须是非阻塞的、快速执行的,否则会拖累整个架构的精度和响应。
?否: 你可能只需要处理单个延时,或者可以容忍一定的阻塞。继续看…
3.你需要高精度延时吗?(微秒级,或者对抖动要求严格)
?是:硬件定时器 (TIM) 直接使用 是你的不二法门。无论是阻塞式轮询,还是中断式非阻塞,它都能提供硬件级的精准度。
?否 (毫秒级精度,一点抖动可以接受): 继续看…
4.你需要一个简单的 毫秒级延时,并且 可以接受在延时期间程序停在这里等待(阻塞)?
?是:HAL_Delay() (基于 SysTick, 方法 2)是最方便的选择。调用简单,对于简单的初始化等待、短时阻塞场景够用。
?否 (即使是毫秒级延时,也不希望阻塞):
?如果只是单个非阻塞延时:考虑用 硬件 TIM + 中断 。
?如果可能涉及多个非阻塞定时事件(现在或将来):软件定时器架构可能更具扩展性,用一次性模式的软件定时器来实现非阻塞延时。
5.你只是需要一个 极其短暂几个 CPU 周期)、非关键的延时,并且环境特殊(如极早期初始化,其他定时器未就绪),能容忍所有缺点?
?是 (三思而后行!): 软件延时 (方法 1),比如用 __NOP(),可以作为最后的、临时的选择。
?否: 尽量避免使用这种原始方法。
七、避坑指南
最后,送上几点使用延时函数时的注意事项:
1.警惕 HAL_Delay()在中断服务程序 (ISR) 中使用! HAL_Delay() 依赖于 SysTick_Handler 来增加 uwTick。
如果在一个优先级高于 SysTick 中断的 ISR 中调用 HAL_Delay(),或者在关闭中断的情况下调用,uwTick 无法更新,HAL_Delay() 会变成死循环!同理,在 ISR 中也应避免长时间的软件延时。ISR 应该快进快出。
2.注意 volatile关键字!在使用软件延时或某些依赖共享变量的延时逻辑(如手动实现的基于 TIM 轮询的延时)时,确保相关变量被声明为 volatile,防止编译器过度优化。
3.确认你的时钟配置! 所有基于硬件定时器(SysTick, TIM)的延时,其精度都直接依赖于系统时钟和相应总线时钟的正确配置。SystemCoreClockUpdate() 函数很重要,确保它在时钟更改后被调用,以便 HAL 库和其他依赖 SystemCoreClock 变量的地方能获取到正确的时钟频率。
4.理解 RTOS Tick 的影响: 使用 RTOS 延时时,要清楚其实际延时时间可能略大于指定的 Tick 数,并且最小延时单位是 1 个 Tick。如果需要非常精确的短延时,可能仍需借助 TIM。
5.延时不是银弹: 有些场景看似需要延时,但可能有更好的事件驱动或状态机设计来替代。例如,等待某个外部信号,与其轮询+延时,不如使用外部中断。过度依赖延时可能导致程序结构僵化,响应性差。
好了,关于 STM32 的延时方法,今天就盘到这里,这几个方法,应该足够你开发用了。
end
eqw3cegcw1064041790917.jpg
下面是更多无际原创的个人成长经历、行业经验、技术干货。
1.电子工程师是怎样的成长之路?10年5000字总结
2.如何快速看懂别人的代码和思维
3.单片机开发项目全局变量太多怎么管理?
4.C语言开发单片机为什么大多数都采用全局变量的形式?
5.单片机怎么实现模块化编程?实用程度让人发指!
6.c语言回调函数的使用及实际作用详解
7.手把手教你c语言队列实现代码,通俗易懂超详细!
8.c语言指针用法详解,通俗易懂超详细! |
|