盘点4种STM32实现延时的方法

原创 无际单片机编程 2025-04-10 07:51

关注公众号,回复“入门资料”获取单片机入门到高级开挂教程

 开发板带你入门,我们带你飞

文 | 无际(微信: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 < 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的就是采用这种定时架构,我们实际产品一直在用,简直不要太爽。    

          

 

下面给大家大概讲解下。

          

 

核心思想:

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 == 0return 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 && 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\n");}void Button_Debounce_Callback(void) {    // 按键消抖完成,在这里处理按键事件    // printf("Button debounced and processed.\r\n");}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 延时/定时器服务,如 vTaskDelayosTimer 等。这是最高效、最符合多任务协作的方式。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



下面是更多无际原创个人成长经历、行业经验、技术干货

1.电子工程师是怎样的成长之路?10年5000字总结

2.如何快速看懂别人的代码和思维

3.单片机开发项目全局变量太多怎么管理?

4.C语言开发单片机为什么大多数都采用全局变量的形式

5.单片机怎么实现模块化编程?实用程度让人发指!

6.c语言回调函数的使用及实际作用详解

7.手把手教你c语言队列实现代码,通俗易懂超详细!

8.c语言指针用法详解,通俗易懂超详细!


    

 

无际单片机编程 单片机编程、全栈孵化。
评论 (0)
  • 一、行业背景与市场需求高血压作为全球发病率最高的慢性病之一,其早期监测与管理已成为公共卫生领域的重要课题。世界卫生组织数据显示,全球超13亿人受高血压困扰,且患者群体呈现年轻化趋势。传统血压计因功能单一、数据孤立等缺陷,难以满足现代健康管理的需求。在此背景下,集语音播报、蓝牙传输、电量检测于一体的智能血压计应运而生,通过技术创新实现“测量-分析-管理”全流程智能化,成为慢性病管理的核心终端设备。二、技术架构与核心功能智能血压计以电子血压测量技术为基础,融合物联网、AI算法及语音交互技术,构建起多
    广州唯创电子 2025-04-23 09:06 123浏览
  • 前言本文主要演示基于TL3576-MiniEVM评估板HDMI OUT、DP 1.4和MIPI的多屏同显、异显方案,适用开发环境如下。Windows开发环境:Windows 7 64bit、Windows 10 64bitLinux开发环境:VMware16.2.5、Ubuntu22.04.5 64bitU-Boot:U-Boot-2017.09Kernel:Linux-6.1.115LinuxSDK:LinuxSDK-[版本号](基于rk3576_linux6.1_release_v
    Tronlong 2025-04-23 13:59 67浏览
  •   无人机结构仿真与部件拆解分析系统平台解析   北京华盛恒辉无人机结构仿真与部件拆解分析系统无人机技术快速发展的当下,结构仿真与部件拆解分析系统平台成为无人机研发测试的核心工具,在优化设计、提升性能、降低成本等方面发挥关键作用。以下从功能、架构、应用、优势及趋势展开解析。   应用案例   目前,已有多个无人机结构仿真与部件拆解分析系统在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润无人机结构仿真与部件拆解分析系统。这些成功案例为无人机结构仿真与部件拆解分析系统的推广和应用提
    华盛恒辉l58ll334744 2025-04-23 15:00 106浏览
  • 文/Leon编辑/cc孙聪颖‍在特朗普政府发起的关税战中,全球芯片产业受到巨大冲击,美国芯片企业首当其冲。据报道称,英伟达本周二公布的8-K文件显示,美国政府通知该公司向中国(包括中国香港及澳门)销售尖端芯片(H20)时,需要获得美国政府的许可。文件发布后,英伟达预计会在第一季度中额外增加55亿美元的相关费用计提。随后,英伟达股价单日下跌6.9%,市值一夜蒸发约1890亿美元(约合人民币1.37万亿元)。至截稿时,至截稿时,其股价未见止跌,较前日下跌4.51%。北京时间4月17日,英伟达创始人、
    华尔街科技眼 2025-04-22 20:14 87浏览
  •   电磁频谱数据综合管理平台系统解析   一、系统定义与目标   北京华盛恒辉电磁频谱数据综合管理平台融合无线传感器、软件定义电台等前沿技术,是实现无线电频谱资源全流程管理的复杂系统。其核心目标包括:优化频谱资源配置,满足多元通信需求;运用动态管理与频谱共享技术,提升资源利用效率;强化频谱安全监管,杜绝非法占用与干扰;为电子战提供频谱监测分析支持,辅助作战决策。   应用案例   目前,已有多个电磁频谱数据综合管理平台在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润电磁频谱数
    华盛恒辉l58ll334744 2025-04-23 16:27 88浏览
  •   陆地边防事件紧急处置系统平台解析   北京华盛恒辉陆地边防事件紧急处置系统平台是整合监测、预警、指挥等功能的智能化综合系统,致力于增强边防安全管控能力,快速响应各类突发事件。以下从系统架构、核心功能、技术支撑、应用场景及发展趋势展开全面解读。   应用案例   目前,已有多个陆地边防事件紧急处置系统在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润陆地边防事件紧急处置系统。这些成功案例为陆地边防事件紧急处置系统的推广和应用提供了有力支持。   一、系统架构   感知层:部
    华盛恒辉l58ll334744 2025-04-23 11:22 85浏览
  • 故障现象一辆2016款奔驰C200L车,搭载274 920发动机,累计行驶里程约为13万km。该车组合仪表上的防侧滑故障灯、转向助力故障灯、安全气囊故障灯等偶尔异常点亮,且此时将挡位置于R挡,中控显示屏提示“后视摄像头不可用”,无法显示倒车影像。 故障诊断用故障检测仪检测,发现多个控制单元中均存储有通信类故障代码(图1),其中故障代码“U015587 与仪表盘的通信存在故障。信息缺失”出现的频次较高。 图1 存储的故障代码1而组合仪表中存储有故障代码“U006488 与用户界
    虹科Pico汽车示波器 2025-04-23 11:22 55浏览
  •   复杂电磁环境模拟系统平台解析   一、系统概述   北京华盛恒辉复杂电磁环境模拟系统平台是用于还原真实战场或特定场景电磁环境的综合性技术平台。该平台借助软硬件协同运作,能够产生多源、多频段、多体制的电磁信号,并融合空间、时间、频谱等参数,构建高逼真度的电磁环境,为电子对抗、通信、雷达等系统的研发、测试、训练及评估工作提供重要支持。   应用案例   目前,已有多个复杂电磁环境模拟系统在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润复杂电磁环境模拟系统。这些成功案例为复杂电
    华盛恒辉l58ll334744 2025-04-23 10:29 119浏览
  •   后勤实验仿真系统平台深度解析   北京华盛恒辉后勤实验仿真系统平台依托计算机仿真技术,是对后勤保障全流程进行模拟、分析与优化的综合性工具。通过搭建虚拟场景,模拟资源调配、物资运输等环节,为后勤决策提供数据支撑,广泛应用于军事、应急管理等领域。   应用案例   目前,已有多个后勤实验仿真系统平台在实际应用中取得了显著成效。例如,北京华盛恒辉和北京五木恒润后勤实验仿真系统平台。这些成功案例为后勤实验仿真系统平台的推广和应用提供了有力支持。   一、核心功能   (一)后勤资源模拟
    华盛恒辉l58ll334744 2025-04-23 15:39 75浏览
  • 在科技飞速发展的当下,机器人领域的每一次突破都能成为大众瞩目的焦点。这不,全球首届人形机器人半程马拉松比赛刚落下帷幕,赛场上的 “小插曲” 就掀起了一阵网络热潮。4月19日,北京亦庄的赛道上热闹非凡,全球首届人形机器人半程马拉松在这里激情开跑。20支机器人队伍带着各自的“参赛选手”,踏上了这21.0975公里的挑战之路。这场比赛可不简单,它将机器人放置于真实且复杂的动态路况与环境中,对机器人在运动控制、环境感知和能源管理等方面的核心技术能力进行了全方位的检验。不仅要应对长距离带来的续航挑战,还要
    用户1742991715177 2025-04-22 20:42 83浏览
  • 一、技术背景与市场机遇在智能家居高速发展的今天,用户对家电设备的安全性、智能化及能效表现提出更高要求。传统取暖器因缺乏智能感知功能,存在能源浪费、安全隐患等痛点。WTL580-C01微波雷达感应模块的诞生,为取暖设备智能化升级提供了创新解决方案。该模块凭借微波雷达技术优势,在精准测距、环境适应、能耗控制等方面实现突破,成为智能取暖器领域的核心技术组件。二、核心技术原理本模块采用多普勒效应微波雷达技术,通过24GHz高频微波信号的发射-接收机制,实现毫米级动作识别和精准测距。当人体进入4-5米有效
    广州唯创电子 2025-04-23 08:41 110浏览
我要评论
0
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦