凡亿教育-佳佳
凡事用心,一起进步
打开APP
公司名片
凡亿专栏 | 盘点4种STM32 实现延时的方法
盘点4种STM32 实现延时的方法

不管你是刚入门,对着 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 < us; 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 < ms; 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 < HAL_MAX_DELAY)    {        wait  = (uint32_t)(uwTickFreq); // uwTickFreq 通常是 1,代表1ms    }    while ((HAL_GetTick() - tickstart) < wait)    {        // 在这里等待        // 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的就是采用这种定时架构,我们实际产品一直在用,简直不要太爽。    

ca225342459a317c4519c9a42f524a.jpg

          

 

下面给大家大概讲解下。

          

 

核心思想:

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 表示成功,<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 < SOFT_TIMER_MAX_TIMERS;   i) {        g_soft_timers[i].is_used = 0;        g_soft_timers[i].state = TIMER_STATE_STOPPED;        g_soft_timers[i].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 < SOFT_TIMER_MAX_TIMERS;   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 < SOFT_TIMER_MAX_TIMERS;   id) {        // 检查定时器是否在使用且处于运行状态        if (g_soft_timers[id].is_used

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表凡亿课堂立场。文章及其配图仅供工程师学习之用,如有内容图片侵权或者其他问题,请联系本站作侵删。
相关阅读
进入分区查看更多精彩内容>
精彩评论

暂无评论