news 2026/6/23 20:00:40

STM32 SysTick延时函数中断调用死锁分析与可重入性改进

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 SysTick延时函数中断调用死锁分析与可重入性改进

1. 项目概述与问题引入

做嵌入式开发,尤其是玩STM32这类MCU的朋友,估计都自己动手写过延时函数。用库函数里的HAL_Delay或者标准库的delay_ms当然方便,但有时候为了追求极致的效率、可控性,或者就是想搞明白底层到底是怎么跑的,自己撸一个基于SysTick的系统滴答定时器延时函数,就成了必经之路。我前段时间在折腾一个红外遥控解码的项目,用的就是自己写的延时函数,一开始在main函数里跑得好好的,解码成功率百分百,心里还挺美。结果一把这个延时函数放到中断服务程序里调用,整个系统时不时就卡死,或者解码出来的时间码完全对不上,遥控器直接失灵。

这问题就很有意思了,它暴露了一个在编写底层驱动时非常经典的陷阱:可重入性与资源冲突。我最初写的那个延时函数,思路很直接:配置SysTick,启动,然后原地死等标志位。代码看起来简洁高效,但就像一颗埋在中断里的定时炸弹。经过一番调试和代码分析,我发现问题核心有两个:第一,在中断里调用会导致延时严重不准;第二,更致命的是,可能会直接导致程序“死锁”,永远卡在延时循环里出不来。这篇文章,我就来详细拆解这个问题的来龙去脉,分享我最终修复的、支持在中断中安全调用的SysTick延时函数实现,并深入探讨其中的嵌入式系统编程思想。无论你是正在学习STM32的新手,还是想优化底层代码的老鸟,相信这些“踩坑”经验和原理分析都能给你带来启发。

2. 问题根源:深入剖析旧版延时函数的致命缺陷

要解决问题,首先得把问题看清楚。我最初写的延时函数,是很多教程里都能见到的“经典”写法,核心就是操作SysTick的三个关键寄存器:LOAD(重装载值)、VAL(当前值)和CTRL(控制及状态寄存器)。我们先来看看问题代码,并理解SysTick的基本工作模式。

2.1 SysTick定时器工作原理与旧版代码回顾

SysTick是一个24位的递减计数器,挂在Cortex-M内核上,所以所有基于该内核的芯片(如STM32全系列)都有它。它的工作流程很简单:

  1. 我们向LOAD寄存器写入一个初始值(比如N)。
  2. 启动计数器后,VAL寄存器会从LOAD的值开始,每个时钟周期减1。
  3. VAL减到0时,会在下一个时钟周期自动重载LOAD的值,同时将控制状态寄存器CTRL的第16位(COUNTFLAG)置1,表示一次计时完成。
  4. 如果开启了SysTick中断,此时还会触发中断。

我最初的延时函数delay_us是这样的(delay_ms逻辑类似):

void delay_us(u32 Nus) { SysTick->LOAD = Nus * fac_us; // 时间加载 SysTick->CTRL |= 0x01; // 开始倒数 while(!(SysTick->CTRL & (1<<16))); // 等待时间到达 SysTick->CTRL = 0X00000000; // 关闭计数器 SysTick->VAL = 0X00000000; // 清空计数器 }

这段代码在单一的执行流(比如只在main循环中)里运行,是没问题的。它完成一次指定微秒数的延时后,会关闭计数器,清理现场。但一旦引入中断,情况就复杂了。

2.2 场景复现:中断如何“搞砸”你的延时

让我们模拟一个最典型的死锁场景:

  1. 主程序调用delay_ms(1000),开始一个1秒的延时。此时,LOAD被设置为1000ms对应的计数值,CTRLENABLE位被置1,计数器开始递减。
  2. 在计数器递减到一半(比如VAL还剩 500)时,一个外部中断(比如红外接收引脚的电平变化中断)发生了。
  3. CPU暂停主程序的延时,跳转到中断服务程序中执行。
  4. 中断服务程序为了进行精确的时序判断(在红外解码中很常见),也调用了delay_us(500)
  5. 中断中的delay_us函数开始执行:
    • SysTick->LOAD = 500 * fac_us;//致命操作1:它直接修改了LOAD寄存器!
    • SysTick->CTRL |= 0x01;// 启动计数器(其实已经在运行)。
    • 然后进入while循环,等待COUNTFLAG置位。

这里就出现了第一个问题:LOAD被意外修改。SysTick的重载发生在VAL递减到0的瞬间。当中断函数修改LOAD时,主程序设置的1000ms的LOAD值被覆盖了。但更关键的是,当前的VAL值(假设是500)并不会立即被重置为新的LOAD值(500)。计数器会继续从500往下减,直到0。这意味着,中断中期望的500us延时,实际等待的时间是(当前VAL值) / 时钟频率,这是一个随机值,导致延时完全失准。

2.3 死锁陷阱:为什么程序会永远卡住?

中断中的delay_us(500)函数,在(不准确地)等待一段时间后,VAL终于减到0,COUNTFLAG被硬件置1。while循环检测到标志位,退出等待。接着,它执行了那两条“清理”语句:

SysTick->CTRL = 0X00000000; // 关闭计数器 SysTick->VAL = 0X00000000; // 清空计数器

这是致命操作2:它直接关闭了SysTick计数器(清除了CTRLENABLE位)。

当中断服务程序执行完毕,CPU返回到主程序被中断的地方,也就是主程序的delay_ms(1000)函数里,此时它还在执行那条while(!(SysTick->CTRL & (1<<16)))语句,眼巴巴地等着它的COUNTFLAG被置位。

然而,SysTick计数器已经被中断里的代码关闭了!VAL寄存器被清0,计数器停止工作。VAL永远不会再从任何值递减到0,因此硬件永远不会再将COUNTFLAG标志位置1。于是,主程序的while循环成了一个真正的“死循环”,程序彻底卡死在这里。这就是中断中调用非可重入函数导致的典型资源破坏和死锁。

注意:即使有些实现没有关闭计数器,只是清理VAL,问题依然存在。因为主程序等待的是它自己设置的LOAD值对应的周期完成标志,而这个周期已经被中断中的代码篡改和打断了,标志位可能永远无法按预期置位。

3. 解决方案设计:打造中断安全的SysTick延时函数

分析了旧代码的缺陷,我们就能有的放矢地设计一个新方案。目标很明确:实现一个可以在中断服务程序中安全调用的延时函数,同时尽量避免对主程序延时造成过大误差。核心思想是:不破坏、不独占、主动避让

3.1 核心设计思路与策略选择

面对SysTick这个被多方(主程序、中断)争抢的共享资源,我们有几种思路:

  1. 禁止在中断中延时:最简单粗暴,通过编程规范约束。但这限制了设计灵活性,在很多需要精确定时的中断处理中不现实。
  2. 使用独立的定时器:为中断服务程序分配一个单独的硬件定时器(如TIM2)。这是最干净、最可靠的方案,但消耗了额外的硬件资源。
  3. 实现可重入的SysTick延时:这是我们本文探讨的重点。即改造我们的延时函数,使其能够安全地在中断嵌套环境中运行。

要实现可重入,关键点在于:

  • 原子操作与状态保护:在修改LOADVAL等关键寄存器时,需要考虑是否会被打断。虽然SysTick的寄存器操作本身是原子的(一次总线写操作),但我们的“读-修改-写”逻辑序列需要保护。
  • 避免破坏性操作:不能简单地在函数末尾关闭计数器,因为其他执行流可能还在依赖它。
  • 优雅地处理冲突:当检测到SysTick正在被其他任务使用时,我们的延时函数应该如何处理?是等待、报错,还是做一次不精确的延时后退出?

我的策略是:采用“非破坏性检查”和“单次触发”模式。函数只负责启动一次延时并等待完成,完成后复位计数器状态,但不再关闭使能位(除非它自己就是拥有者)。同时,通过检查CTRL寄存器的使能位(ENABLE)来判断SysTick是否已被占用,如果被占用,则放弃本次精确延时,避免陷入死等。

3.2 关键改进点详解

基于以上思路,我对代码进行了以下几处关键改进:

1. 清空VAL寄存器:在设置LOAD值之后,立即将VAL清0。这是一个非常重要的操作。根据ARM Cortex-M手册,向VAL寄存器写入任何值,都会将其清零。这确保了当我们设置新的LOAD后,计数器是从这个全新的LOAD值开始递减,而不是从某个不可预知的残留值开始。这解决了中断中修改LOAD后延时时间随机的问题。

SysTick->LOAD = (u32)nms * fac_ms; // 时间加载 SysTick->VAL = 0x00; // 清空计数器,确保从LOAD开始计数 SysTick->CTRL = 0x01; // 开始倒数

2. 循环等待条件的强化:旧代码只等待COUNTFLAG置位。新代码的等待循环加入了额外的安全检查:

do { temp = SysTick->CTRL; } while((temp & 0x01) && !(temp & (1<<16))); // 等待时间到达

这个while条件包含了两个部分:

  • !(temp & (1<<16)):等待COUNTFLAG标志位变为1(时间到)。
  • (temp & 0x01)检查SysTick计数器是否仍然处于使能状态

这个额外的检查是避免死锁的灵魂。想象一下,如果本函数在中断中执行,而主程序突然关闭了SysTick(虽然不合理,但需防错),或者更可能的是,发生了更高优先级的中断并修改了SysTick状态。一旦CTRL的使能位(ENABLE)被清除,计数器停止,COUNTFLAG可能永远不会被置位。如果没有这个检查,函数就会死在这个循环里。加上这个检查后,一旦发现计数器被禁用,循环条件不满足,函数会立即退出。

3. 谨慎的“清理”操作:函数退出前,我们依然会执行:

SysTick->CTRL = 0x00; // 关闭计数器 SysTick->VAL = 0X00; // 清空计数器

这里需要理解其含义:这个“关闭”操作,关闭的是“本次”延时所启动的计数器。由于我们在函数开头总是先清空VAL再使能,所以函数退出时的“关闭”操作,在逻辑上是成对出现的。如果SysTick在进入本函数前就已经被使能(即被其他执行流占用),那么本函数的“关闭”操作是否会带来问题?

会的。这就是这个方案的核心代价。假设主程序在延时,中断发生,中断函数也调用本延时函数。中断函数返回前关闭了计数器,这会导致主程序的延时被破坏。这就是我原文中提到的“跳出中断后当前的延时不再有效”。主程序的延时将因为计数器被关闭而提前结束或行为异常。

3.3 方案评估:优缺点与适用场景

经过上述改进,我们得到了一个“中断可调用”但“非完全可重入”的延时函数。

优点:

  • 解决了死锁问题:通过检查计数器使能状态,避免了程序永远卡在等待循环中。
  • 提高了中断中的时序准确性:通过清空VAL,确保了在中断中设置的延时周期是从预设的LOAD值开始计算,消除了时间随机性。
  • 代码健壮性增强:能够应对SysTick被意外修改的情况。

缺点与代价:

  • 破坏了原有延时任务的完整性:这是最大的缺点。中断中的延时函数退出时会关闭计数器,这会粗暴地打断任何正在进行中的、由其他代码(如主程序)发起的SysTick延时,导致其失效。
  • 延时误差:对于被中断打断的那个延时(如上例中主程序的1000ms延时),其实际延时时间将变得不准确。它等于“从开始到被中断”的时间 + “中断函数执行时间” + “中断返回后可能残留的不可预知时间”。这个误差在时序要求严苛的场合是不可接受的。

适用场景:

  • 中断服务程序中的延时是短暂、非关键的。
  • 主程序中的延时对绝对精度要求不高,可以容忍被中断偶尔打断并产生一些误差。
  • 系统设计上严格避免了长延时进入中断,或者中断优先级经过精心安排,确保高优先级中断不会使用此延时函数。
  • 作为学习、调试或对可靠性要求不高的原型项目。

实操心得:在复杂的嵌入式系统中,依赖一个全局的、非可重入的延时资源是危险的。这个改进版函数是一个不错的教学案例和临时解决方案,但它揭示了嵌入式开发中的一个重要原则:对于关键的系统资源(如定时器),要么严格序列化访问(如关中断),要么为不同的执行上下文提供独立的资源。在产品级代码中,我会更倾向于使用独立的硬件定时器来处理中断中的精确延时需求。

4. 新版延时函数代码实现与逐行解析

接下来,我们完整地审视改进后的代码,并逐行分析其工作原理和注意事项。这里我提供了带详细注释的版本。

#ifndef __DELAY_H #define __DELAY_H // 使用SysTick的普通计数模式对延迟进行管理 // 包括delay_us, delay_ms // 正点原子@SCUT // 2008/12/14 // V1.2 // 修正了中断中调用出现死循环的错误 // 防止延时不准确,采用do while结构! static u8 fac_us = 0; // us延时倍乘数 static u16 fac_ms = 0; // ms延时倍乘数 // 初始化延迟函数 // SYSCLK: 系统时钟频率,单位MHz void delay_init(u8 SYSCLK) { // 选择SysTick时钟源为HCLK/8 (即系统时钟的1/8) // bit2 = 1: 选择外部时钟(HCLK/8); bit2 = 0: 选择内核时钟(HCLK) // 这里清bit2,选择外部时钟。对于72MHz系统,SysTick时钟为9MHz。 SysTick->CTRL &= 0xfffffffb; // 计算1us对应的计数值。因为SysTick时钟是SYSCLK/8 MHz。 // 例如:SYSCLK=72, 则SysTick时钟=9MHz, 周期约111.1ns。 // 要延时1us,需要计数次数 = 1us / (1/9MHz) = 9次。 fac_us = SYSCLK / 8; // 计算1ms对应的计数值。1ms = 1000us。 fac_ms = (u16)fac_us * 1000; } // 延时n毫秒 // 注意nms的范围 // 对于24位计数器,LOAD最大值是0xFFFFFF。 // 所以 nms <= 0xffffff * 8 / SYSCLK // 对72M条件下,nms <= (0xFFFFFF * 8) / 72 ≈ 1864 ms void delay_ms(u16 nms) { u32 temp; // 步骤1:设置重装载值。这是本次延时需要计数的总周期数。 SysTick->LOAD = (u32)nms * fac_ms; // 步骤2:清空当前计数器。这是关键一步!确保计数器从LOAD值开始递减,而不是从某个中间值开始。 SysTick->VAL = 0x00; // 步骤3:启动SysTick计数器。使能位(bit0)置1,不开启中断(bit1=0)。 SysTick->CTRL = 0x01; // 步骤4:等待延时完成。 // 使用do...while结构,确保至少进入一次循环,读取一次状态寄存器。 do { // 读取控制及状态寄存器。注意:读取CTRL会清除COUNTFLAG(bit16)标志位! temp = SysTick->CTRL; // 循环条件: // 1. (temp & 0x01): 检查计数器是否仍在运行。如果被其他代码关闭,则退出。 // 2. !(temp & (1<<16)): 检查COUNTFLAG是否为0(时间未到)。时间到则退出。 // 两者同时满足时继续等待。 } while((temp & 0x01) && !(temp & (1<<16))); // 步骤5:延时结束,关闭计数器并清空。 // 注意:这里会无条件关闭计数器,无论它之前是否由本函数开启。 // 这会导致其他正在使用SysTick的延时被打断! SysTick->CTRL = 0x00; SysTick->VAL = 0X00; } // 延时N微秒 // 逻辑与delay_ms完全一致,仅倍乘数不同。 void delay_us(u32 Nus) { u32 temp; SysTick->LOAD = Nus * fac_us; SysTick->VAL = 0x00; SysTick->CTRL = 0x01; do { temp = SysTick->CTRL; } while((temp & 0x01) && !(temp & (1<<16))); SysTick->CTRL = 0x00; SysTick->VAL = 0X00; } #endif

4.1 关键代码行深度解析

1.SysTick->VAL = 0x00;的位置与重要性:这行代码必须紧跟在设置LOAD之后,使能计数器之前。顺序不能错。其作用是将当前计数器的值归零。在SysTick中,写入VAL寄存器会使其清零。这保证了当我们执行SysTick->CTRL = 0x01;启动计数器时,VAL会立即重载为LOAD寄存器的值,然后开始递减。如果没有这一步,如果VAL中残留有上次计数的值(非0),那么从启动到COUNTFLAG置位的时间将是(残留VAL值) / 时钟频率,而不是我们预期的(LOAD值) / 时钟频率,导致第一次延时严重错误。

2.do...while循环与COUNTFLAG的“读-清零”特性:使用do...while确保了循环体至少执行一次,能正确读取到CTRL寄存器的初始状态。while条件里的(temp & 0x01)是安全退出机制。而COUNTFLAG(bit 16) 有一个硬件特性:当程序读取CTRL寄存器时,该标志位会被自动清零。这意味着:

  • 我们不能在循环条件中直接使用SysTick->CTRL & (1<<16),因为每次判断都会读取并清除它,可能导致条件判断异常。
  • 正确的做法是:在循环体内一次性将CTRL的值读到一个临时变量temp中,然后用这个temp变量来判断两个条件。这样,COUNTFLAG的状态在本次循环周期内是稳定的。

3. 函数末尾的清理操作SysTick->CTRL = 0x00;这行代码是双刃剑。它确保了函数退出后,SysTick处于一个确定的、未使能的状态,避免了它作为后台定时器继续运行(可能影响低功耗模式)。但同时,正如前文所述,它粗暴地中止了任何基于SysTick的计时过程。这是本方案无法在嵌套调用中保持所有延时精度的根本原因。

5. 更优实践与高级话题探讨

虽然上述改进版函数解决了一些问题,但它依然不是一个工业级的、鲁棒性高的解决方案。在实际项目中,我们可以有更好的选择。

5.1 方案升级:使用独立的硬件定时器

对于中断服务程序中必须的精确延时,最可靠的方法是分配一个独立的通用定时器(如TIM2, TIM3等)。步骤简述如下:

  1. 初始化定时器:配置为单次触发模式(One-pulse mode),或者普通向上/向下计数模式,并开启更新中断(如果需要)。
  2. 编写延时函数
    void delay_us_it(uint16_t us) { __HAL_TIM_SET_AUTORELOAD(&htim2, us - 1); // 设置重载值 __HAL_TIM_SET_COUNTER(&htim2, 0); // 清空计数器 __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE); // 清除更新标志 HAL_TIM_Base_Start(&htim2); // 启动定时器 while(__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) == RESET); // 等待更新标志 HAL_TIM_Base_Stop(&htim2); // 停止定时器 }
  3. 优势:与SysTick完全隔离,中断和主程序的延时互不影响。精度高,可靠性强。
  4. 代价:占用一个额外的硬件定时器资源。

5.2 无阻塞延时与状态机设计

在很多嵌入式应用中,尤其是事件驱动的系统里,使用while循环死等的“阻塞式延时”本身就是一种不良设计,它会浪费CPU周期,影响系统响应性。更好的模式是无阻塞延时基于状态机的延时

使用SysTick实现无阻塞延时(滴答时钟法):

  1. 在SysTick中断服务程序里,对一个全局变量(如uwTick)进行递增。
    // 在SysTick中断中 void SysTick_Handler(void) { uwTick++; }
  2. 提供获取当前滴答数的函数和延时判断函数。
    uint32_t HAL_GetTick(void) { return uwTick; } // 非阻塞延时检查 uint32_t startTick = HAL_GetTick(); while((HAL_GetTick() - startTick) < 1000) { // 在这里可以执行其他任务,而不是死等 // do_other_work(); } // 1000个tick时间到
  3. 在中断服务程序里,如果需要延时,可以记录一个“唤醒时间点”,然后直接退出。在主循环或后台任务中检查是否到达该时间点,再执行后续操作。这要求中断处理逻辑被拆分成“触发”和“后续处理”两部分。

状态机设计:将需要延时的任务建模为一个状态机。例如,红外解码:

  • 状态IDLE:等待下降沿。
  • 状态GAP_START:收到下降沿,记录时间戳start_time = HAL_GetTick(),进入状态WAIT_GAP
  • 状态WAIT_GAP:在main循环或低优先级任务中检查if(HAL_GetTick() - start_time > LEADING_CODE_GAP)。如果时间到,则判断为引导码,进入解码数据位状态;如果未到,则继续等待,CPU可以处理其他任务。

这种方式彻底消除了在中断中进行任何形式延时的需求,是构建高效、响应迅速嵌入式系统的推荐方法。

5.3 关于时钟源选择的思考

在我的delay_init函数中,我选择了HCLK/8作为SysTick的时钟源。SysTick->CTRL &= 0xfffffffb;这行代码清除了第2位,选择了外部时钟源。

  • 为什么是/8?主要是为了降低计数频率,使得24位的计数器能够表示更长的时间范围。对于72MHz系统,72MHz / 8 = 9MHz,计数器每个周期约111ns。1ms需要9000个计数,仍在24位计数器范围内(最大16,777,215)。如果想延时更长时间,这个分频是必要的。
  • 精度权衡:分频降低了时钟频率,也降低了延时分辨率。此时1个计数周期约111ns,那么我们的delay_us函数的最小精度也就是1us左右(9个计数周期)。对于需要纳秒级精度的场合,这个分频可能不合适,可能需要选择HCLK作为时钟源(不分频),但代价是最大延时时间会缩短。
  • 初始化时机delay_init必须在所有延时函数调用之前执行,通常在主函数刚开始、初始化所有外设时调用。并且要注意,如果后续有代码修改了SysTick的配置(比如RTOS启动时会重新初始化SysTick),可能会覆盖你的设置。

6. 常见问题排查与调试技巧

在实际使用自己编写的延时函数时,你可能会遇到各种奇怪的问题。这里我总结了一个排查清单和调试方法。

6.1 问题速查表

现象可能原因排查步骤与解决方案
延时时间严重不准,长了好几倍1.fac_usfac_ms计算错误。
2. SysTick时钟源选择错误(如应为HCLK/8但未分频)。
3. 系统时钟SYSCLK配置与传入delay_init的参数不符。
1. 检查delay_init传入的SYSCLK值是否与实际系统时钟一致。
2. 检查SysTick->CTRL的bit2,确认时钟源选择。
3. 使用示波器或逻辑分析仪测量一个GPIO翻转的周期来反推实际延时。
程序偶尔卡死,特别是在中断产生后1. 中断服务程序中调用了旧版的不安全延时函数,导致死锁。
2. 多个中断嵌套调用延时函数,资源冲突。
1. 检查所有中断服务程序,确保其调用的延时函数是“中断安全”版本。
2. 考虑使用独立定时器或在中断中避免长延时,改用状态机。
第一次延时准确,后续延时变短未在每次设置LOAD后清空VAL寄存器。确保在SysTick->LOAD = ...之后,SysTick->CTRL = 0x01之前,有SysTick->VAL = 0x00;语句。
在RTOS(如FreeRTOS)中,自定义延时函数失效或冲突RTOS内核会接管SysTick作为系统心跳时钟。绝对不要在RTOS中使用自定义的SysTick阻塞延时。应使用RTOS提供的vTaskDelay()osDelay()等API。如果非要使用,必须确保在RTOS启动前初始化,并了解内核如何操作SysTick,避免冲突(通常不推荐)。
进入低功耗模式后,延时函数失效SysTick在低功耗模式下可能被停止或时钟源改变。在进入低功耗前,应停止所有依赖SysTick的延时操作。唤醒后,可能需要重新初始化SysTick或使用其他定时器。

6.2 实用调试技巧

  1. GPIO“示波器”法:这是最直观的方法。在延时函数开始和结束时,翻转一个空闲的GPIO引脚。

    void delay_us(u32 Nus) { GPIO_SetBits(GPIOA, GPIO_Pin_0); // 开始,拉高 // ... 原有延时代码 ... GPIO_ResetBits(GPIOA, GPIO_Pin_0); // 结束,拉低 }

    用示波器或逻辑分析仪测量这个引脚高电平的持续时间,就是实际的延时时间。可以非常方便地校准fac_usfac_ms

  2. 软件仿真:在Keil MDK或IAR等IDE中,使用软件仿真模式,在延时函数前后设置断点,查看系统时钟SysTick->VAL的变化,或者使用性能分析器(Performance Analyzer)查看函数执行时间。

  3. 检查中断优先级:如果怀疑是中断嵌套导致的问题,检查相关中断的优先级。确保使用了SysTick延时的中断,其优先级设置合理,避免不必要的嵌套。对于CM3/CM4内核,可以检查NVIC_SetPriority的调用。

  4. 静态分析:仔细阅读代码,特别是中断服务程序,画出可能的执行流。问自己:如果主程序在延时,中断发生,中断里也延时,会发生什么?资源(LOAD, VAL, CTRL)是如何被修改的?这个思考过程往往能提前发现很多逻辑漏洞。

踩坑实录:我曾经在一个项目里,因为一个低优先级中断里调用了delay_ms(10),而一个高优先级中断频繁发生,导致低优先级中断的延时被不断打断,累积误差巨大,使得一个周期性任务完全乱套。最后通过将低优先级中断中的阻塞延时改为基于系统滴答的无阻塞检查,才解决了问题。这个教训告诉我,在中断中,时间就是一切,能不用阻塞延时就不用,能用短延时就不用长延时

7. 总结与最终建议

通过从一个有缺陷的SysTick延时函数出发,我们深入探讨了中断、可重入性、资源冲突这些嵌入式核心概念。修复后的函数通过清空VAL和增强循环检查,解决了死锁问题和中断中的计时准确性问题,但它以“破坏其他延时”为代价,换取了自己的安全。

对于学习和理解底层机制,这个过程非常有价值。它让你真正看懂了SysTick是如何工作的,以及并发编程中的陷阱。然而,对于实际项目开发,我给出以下最终建议:

  1. 区分场景:在简单的、单任务或对时序要求不高的学习项目中,可以使用这个改进版的延时函数。它比标准库函数更直观,有助于理解原理。
  2. 中断慎用:尽量避免在中断服务程序中进行任何形式的阻塞延时。如果必须要有时间间隔,优先考虑使用独立的硬件定时器,或者采用“记录时间戳+退出检查”的无阻塞模式。
  3. 拥抱RTOS和系统滴答:在稍复杂的应用中,考虑使用实时操作系统(RTOS)。RTOS提供的延时API是经过精心设计、可重入且支持任务调度的。即使不用RTOS,也可以模仿其思路,实现一个基于SysTick中断的全局时钟节拍,然后提供HAL_GetTick()和超时判断函数,这是最优雅和通用的做法。
  4. 精度与范围的权衡:在设计延时函数时,始终要计算计数器的溢出边界。24位的SysTick,在72MHz系统时钟下(不分频),最大延时约0.23秒;分频8后,最大延时约1.86秒。如果需要更长的延时,需要在函数内部进行循环计数。

编写底层驱动函数就像在钢丝上跳舞,每一步都需要考虑并发和异常情况。希望这次对SysTick延时函数的“刨根问底”,不仅能帮你写出更健壮的代码,更能让你建立起嵌入式系统开发的资源管理和并发思维。下次当你再写一个看似简单的函数时,不妨多问一句:“如果中断来了,它会怎么样?”

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/23 20:00:23

如何快速构建个人A股数据仓库:5分钟搞定专业量化分析

如何快速构建个人A股数据仓库&#xff1a;5分钟搞定专业量化分析 【免费下载链接】AShareData 自动化Tushare数据获取和MySQL储存 项目地址: https://gitcode.com/gh_mirrors/as/AShareData 还在为A股数据获取头疼吗&#xff1f;&#x1f914; 想象一下&#xff0c;你只…

作者头像 李华
网站建设 2026/6/8 0:12:26

㉔ AI副业规模化:从个人到工作室

㉔ AI副业规模化&#xff1a;从个人到工作室个人做AI副业&#xff0c;天花板是月入3-5万——想再往上&#xff0c;必须规模化。前言&#xff1a;为什么个人有天花板&#xff1f; 数据说话&#xff1a;- 个人做AI副业&#xff1a;每天最多工作12小时- 按每小时收入100元算&#…

作者头像 李华
网站建设 2026/6/5 16:51:59

AntiDupl.NET:你的数字相册智能管家,告别重复图片的烦恼

AntiDupl.NET&#xff1a;你的数字相册智能管家&#xff0c;告别重复图片的烦恼 【免费下载链接】AntiDupl A program to search similar and defect pictures on the disk 项目地址: https://gitcode.com/gh_mirrors/an/AntiDupl 你是否曾花费数小时整理电脑中的照片&a…

作者头像 李华
网站建设 2026/6/5 16:50:53

Equalizer APO:免费系统级音频均衡器让你的电脑音质飞升

Equalizer APO&#xff1a;免费系统级音频均衡器让你的电脑音质飞升 【免费下载链接】equalizerapo Equalizer APO mirror 项目地址: https://gitcode.com/gh_mirrors/eq/equalizerapo 还在忍受电脑音质平平无奇吗&#xff1f;想要用普通耳机听到专业级的音效体验吗&…

作者头像 李华
网站建设 2026/6/8 5:07:16

Android模糊效果实现指南:BlurView库的3种核心应用场景解析

Android模糊效果实现指南&#xff1a;BlurView库的3种核心应用场景解析 【免费下载链接】BlurView Android blur view 项目地址: https://gitcode.com/gh_mirrors/blu/BlurView 在Android应用开发中&#xff0c;实现iOS风格的毛玻璃模糊效果一直是设计师和开发者的共同追…

作者头像 李华