电子产业一站式赋能平台

PCB联盟网

搜索
查看: 49|回复: 0
收起左侧

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

[复制链接]

310

主题

310

帖子

2764

积分

三级会员

Rank: 3Rank: 3

积分
2764
发表于 6 天前 | 显示全部楼层 |阅读模式
关注公众号,回复“入门资料”获取单片机入门到高级开挂教程
开发板带你入门,我们带你飞

文 | 无际(微信: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

    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

    eqw3cegcw1064041790917.jpg


    下面是更多无际原创的个人成长经历、行业经验、技术干货。
    1.电子工程师是怎样的成长之路?10年5000字总结
    2.如何快速看懂别人的代码和思维
    3.单片机开发项目全局变量太多怎么管理?
    4.C语言开发单片机为什么大多数都采用全局变量的形式
    5.单片机怎么实现模块化编程?实用程度让人发指!
    6.c语言回调函数的使用及实际作用详解

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

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

    使用道具 举报

    发表回复

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则


    联系客服 关注微信 下载APP 返回顶部 返回列表