当前位置: 首页 > news >正文

STM32 SysTick定时器原理与精准延时实现详解

1. 从“瞎等”到“精控”:为什么SysTick是STM32精准延时的基石

搞嵌入式开发的朋友,尤其是从51单片机转过来的,估计都干过用for循环或者while循环来做延时的事儿。我刚开始玩STM32点灯的时候也是这么干的,简单嘛,写个for(i=0; i<50000; i++),灯就一闪一闪的,感觉还挺有成就感。但很快问题就来了:这延时到底准不准?换个主频,或者编译器优化等级一调,时间全变了。更别提在需要精确定时的场合,比如驱动WS2812灯珠、读取DHT11温湿度传感器,或者做简单的PID控制,这种“瞎等”式的延时根本没法用,误差大到离谱。

后来在项目里被逼得没办法,开始研究系统定时器,也就是SysTick。用上之后才发现,这才是单片机精准延时的“正规军”。它不是一个外设定时器,而是Cortex-M内核自带的一个24位递减计数器,专为实时操作系统(RTOS)提供“心跳”而设计,但我们拿来做精准延时,简直是杀鸡用牛刀——稳得一批。它的精度直接和系统时钟挂钩,不受编译器优化和总线负载的轻微影响,延时函数本身几乎不消耗额外CPU时间(除了中断响应),这才是嵌入式开发该有的样子。

所以,今天这篇笔记,我就把自己从for循环延时踩坑,到熟练使用SysTick实现微秒、毫秒级精准延时的整个过程,包括原理、配置、代码实现、以及一堆实际调试中遇到的坑和技巧,系统地梳理出来。无论你是刚开始接触STM32,还是已经用过但对其原理一知半解,相信这篇近万字的“踩坑实录”都能让你彻底搞懂SysTick,并能在自己的项目里游刃有余地应用。

2. SysTick定时器核心原理与设计思路拆解

2.1 SysTick的“身份”与优势:为什么是它?

在深入代码之前,我们必须先搞清楚SysTick到底是什么,以及为什么它比软件循环延时强那么多。这决定了我们整个方案的设计思路。

SysTick,全称System Tick Timer,是ARM Cortex-M处理器内核集成的一个简易定时器。注意,是“内核集成”,不是“外设”。这意味着只要你是基于Cortex-M内核的芯片(比如STM32全系列、GD32、NXP的LPC系列等),就一定会有这个定时器,它的寄存器地址和操作方法在各家芯片公司之间是统一的,由ARM公司定义。这带来了第一个巨大优势:代码可移植性极强。你今天为STM32F103写的SysTick延时函数,稍作修改(主要是时钟源配置)就能用在STM32F4、F7甚至其他品牌的Cortex-M芯片上。

它的核心是一个24位的递减计数器(LOAD重装载值寄存器)。你给它设定一个初始值(比如9000),它就会在每个时钟脉冲到来时减1,当减到0时,会触发两个动作:第一,产生一个SysTick异常(中断);第二,计数器自动从重装载值(LOAD)开始重新递减,如此周而复始。这个“减到0”的周期时间,就是我们的基准定时时间。

对比软件延时,优势一目了然:

  1. 高精度与确定性:延时时间只取决于你设定的重装载值和系统时钟频率,是硬件行为,与CPU执行其他指令的流水线、缓存状态无关,几乎不受干扰。
  2. 低CPU占用:在等待延时结束期间,CPU可以进入低功耗模式(如WFI等待中断),或者去执行其他任务(在RTOS中),而不是傻傻地空转。这对于电池供电设备至关重要。
  3. 易于实现多任务调度:这正是RTOS的基础。SysTick定期产生中断,为操作系统提供时间片轮转的节拍。

2.2 时钟源选择:HCLK还是HCLK/8?

这是配置SysTick的第一个关键决策点,也直接影响了我们计算重装载值的公式。SysTick的时钟源可以有两个选择(具体看芯片参考手册):

  • 内核时钟(HCLK):对于STM32F1,就是SYSCLK(系统时钟);对于F4/F7/H7,就是HCLK。这是最快的时钟源。
  • 内核时钟的8分频(HCLK/8):速度慢8倍。

如何选择?这需要权衡。

  • 选择HCLK(高速):优点是定时精度高,能实现更短的定时周期(更高频率的中断)。如果你的延时需要非常精细,比如要产生1us的中断来做精准时间戳,或者RTOS需要更小时基(如100us),那么必须选HCLK。缺点是计数器递减得快,同样的延时时间所需的重装载值更小。对于24位计数器(最大值16,777,215)来说,在高速时钟下,它能定时的最大周期会变短。例如72MHz下,最大定时周期约为16.777215ms。
  • 选择HCLK/8(低速):优点是计数器递减得慢,同样的重装载值能定出更长的时间。在72MHz系统时钟下,HCLK/8就是9MHz,此时24位计数器能定时的最大周期约为1.86秒,非常适合实现像Delay(1000)这样的毫秒级、秒级延时函数。而且,在低速时钟下,功耗理论上会略低一丁点(几乎可忽略)。缺点是定时精度降低了8倍。

实操心得:对于大多数不需要极高定时精度的应用,比如普通的按键消抖、LED闪烁、传感器轮询间隔,我强烈推荐使用HCLK/8作为时钟源。原因很简单:它让我们能用较小的重装载值获得较长的定时周期,代码更直观(比如1ms对应9000),而且最大定时范围足够覆盖大多数延时需求。这也是ST官方库函数SysTick_Config(uint32_t ticks)默认采用的配置(如果你查看源码,会发现它默认设置时钟源为HCLK/8)。除非你的项目有明确的、低于微秒级的精确定时需求,否则跟着官方走,选HCLK/8准没错。

2.3 重装载值计算:让时间“量化”

这是整个SysTick延时的核心算法。我们的目标是将“时间”这个物理量,转化为SysTick计数器能理解的“滴答数”(ticks)。

计算公式非常简单:重装载值 = 所需时间 * 时钟频率

但要注意单位统一。我们通常以“秒”为时间基准,以“Hz”为频率单位。

  1. 假设我们需要的定时周期是T秒(例如1ms = 0.001秒)。
  2. SysTick的时钟频率是FHz(例如选择HCLK/8,系统时钟72MHz,则F=9MHz = 9,000,000 Hz)。
  3. 那么,在这段时间T内,SysTick计数器需要跳动的次数(即重装载值RELOAD)为:RELOAD = T * F

举例计算(最常用场景)

  • 目标:实现1ms(0.001秒)的定时中断。
  • 系统时钟(SYSCLK):72MHz。
  • 选择时钟源:HCLK/8 = 72MHz / 8 = 9MHz = 9,000,000 Hz。
  • 计算:RELOAD = 0.001秒 * 9,000,000 Hz = 9,000

所以,将重装载值设置为9000,SysTick就会每1ms产生一次中断。这个9000就是原文中SysTick_SetReload(9000)的由来。

注意事项RELOAD寄存器是24位的,最大值是2^24 - 1 = 16,777,215。计算时一定要确保结果不超过这个值,否则会导致定时错误。例如在9MHz下,最大定时周期为 16,777,215 / 9,000,000 ≈ 1.864秒。如果需要更长的延时,就需要在软件层面进行计数(比如用变量累加中断次数)。

3. SysTick精准延时实现与代码深度解析

理解了原理,我们来看如何用代码实现。我会提供两种风格的代码:一种是类似原文的寄存器直接操作,便于理解底层;另一种是使用ST官方标准外设库(Standard Peripheral Library)或HAL库,更规范、更便携。

3.1 基础寄存器版实现(深入底层)

这种方式直接操作SysTick的四个核心寄存器,适合想彻底弄明白原理的开发者。

首先,定义SysTick寄存器结构体(通常芯片头文件已定义,这里展示其原理):

typedef struct { __IO uint32_t CTRL; // 控制和状态寄存器 __IO uint32_t LOAD; // 重装载值寄存器 __IO uint32_t VAL; // 当前值寄存器 __I uint32_t CALIB; // 校准值寄存器(一般不用) } SysTick_Type;

关键位:

  • CTRL寄存器:
    • 位2CLKSOURCE:时钟源选择。0=HCLK/8,1=HCLK。
    • 位1TICKINT:中断使能。1=计数到0时产生SysTick异常。
    • 位0ENABLE:计数器使能。1=启动计数器。
  • LOAD寄存器:我们写入的重装载值(24位有效)。
  • VAL寄存器:读取当前计数值,写任何值会清空它(置为0)。

初始化函数SysTick_Init

/** * @brief 初始化SysTick定时器,配置为1ms中断周期(基于HCLK/8) * @param 无 * @retval 无 */ void SysTick_Init(void) { /* 1. 关闭SysTick计数器与中断(良好的初始化习惯) */ SysTick->CTRL = 0; // 直接清零CTRL寄存器,关闭一切 /* 2. 配置时钟源为 HCLK/8 (72MHz / 8 = 9MHz) */ // 不设置CTRL.CLKSOURCE位,默认为0,即HCLK/8。 // 如果想用HCLK,则需设置:SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk; /* 3. 设置重装载值,实现1ms中断 */ // 系统时钟72MHz,HCLK/8后为9MHz。 // 1ms = 0.001s, 重装载值 = 0.001 * 9,000,000 = 9000 SysTick->LOAD = 9000 - 1; // 注意:计数器从LOAD值递减到0,共LOAD+1个周期,所以通常设为目标值-1。 // 但根据ARM手册,写入LOAD的值就是重装载值,计数器减到0后,下次从LOAD开始。 // 经实测和库函数验证,设置9000即可得到准确的1ms。为保险起见,可与库函数保持一致。 /* 4. 清空当前计数器值 */ SysTick->VAL = 0; // 写任何值到VAL都会清空计数器 /* 5. 配置中断优先级(可选,但建议设置) */ // SysTick中断属于系统异常,优先级可设置。通常设置为最低或较低优先级,避免影响其他紧急中断。 NVIC_SetPriority(SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1); // 设置优先级为最低 /* 6. 使能SysTick中断和计数器 */ SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk; // 使能中断 SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; // 启动计数器 }

关键细节解析

  1. 清空与使能顺序:先关闭(CTRL=0),再配置(LOAD,VAL),最后使能(ENABLETICKINT)。这是一个防止配置过程中产生意外中断的良好实践。
  2. 重装载值-1问题:这是一个常见的困惑点。SysTick是递减到0触发中断。如果你设置LOAD=9000,计数器会从9000开始递减,经过9001个时钟周期(9000,8999,...,0)后触发中断。而我们想要的是9000个周期触发一次。因此,严格来说,应该设置LOAD=8999。但ST的库函数SysTick_Config(ticks)要求传入的ticks就是中断周期对应的时钟数,并且内部会执行LOAD = ticks - 1。为了保持概念清晰和与库兼容,我们在直接操作寄存器时,也可以直接写入9000,只要中断服务函数里的时间处理逻辑与之匹配即可(我们用的TimingDelay递减逻辑是匹配的)。为了绝对准确,我建议采用与库函数一致的理解:LOAD = 所需周期数 - 1。但原文示例和很多代码中直接写9000也能工作,是因为他们在别处(或潜意识里)做了补偿。这里我们明确一下:若时钟9MHz,欲得1ms中断,则ticks=9000,应设置LOAD = 9000 - 1
  3. 中断优先级:设置SysTick中断优先级是一个好习惯,尤其是在一个有多重中断的系统中。默认情况下它的优先级可能不是最低的。

延时函数Delay_ms与全局变量:这里我们实现一个毫秒级延时。需要一个全局变量来传递时间参数,并在中断中递减它。

volatile uint32_t g_systick_delay_ms = 0; // 必须加volatile,防止编译器优化 /** * @brief 毫秒级阻塞延时 * @param ms: 要延时的毫秒数 * @retval 无 */ void Delay_ms(uint32_t ms) { g_systick_delay_ms = ms; // 加载延时时间 // 等待全局变量被中断服务程序减到0 while(g_systick_delay_ms != 0) { // 这里可以插入WFI指令进入低功耗等待,但需确保SysTick中断能唤醒CPU // __WFI(); // 示例,实际使用需考虑全面 } }

中断服务程序SysTick_Handler这个函数的名字是固定的,由Cortex-M内核约定。在启动文件startup_stm32f10x_xx.s中已经将其声明为弱定义,我们需要在工程中重新实现它。

/** * @brief SysTick中断服务函数 * @param 无 * @retval 无 */ void SysTick_Handler(void) { // 每进入一次中断,意味着过去了1ms(根据我们的配置) if(g_systick_delay_ms > 0) { g_systick_delay_ms--; } // 这里还可以添加其他需要每毫秒执行的任务,例如软件定时器计时等 }

3.2 使用标准外设库(标准库)实现

ST的标准外设库提供了高度封装的函数,让配置变得非常简单。这也是我早期项目中最常用的方式。

初始化与延时实现:

#include "stm32f10x.h" // 根据你的芯片系列包含对应头文件 static __IO uint32_t TimingDelay; // 静态全局变量,仅在本文件使用 /** * @brief 使用库函数初始化SysTick,定时1ms * @param 无 * @retval 1: 初始化成功,0: 初始化失败(重装载值超出24位范围) */ uint32_t SysTick_Init_Lib(void) { /* SystemCoreClock 变量已在系统启动后由SystemInit()函数更新,代表HCLK频率 */ /* 使用库函数 SysTick_Config,参数为中断周期所需的时钟滴答数 */ /* 该函数会:配置时钟源为HCLK/8,设置重装载值(参数-1),清空计数器,设置中断优先级,使能中断和计数器 */ if (SysTick_Config(SystemCoreClock / 8000)) // 72000000 / 8000 = 9000 { // 初始化失败(通常因为重装载值>0xFFFFFF) return 0; } // 可选:调整SysTick中断优先级。库函数默认可能设了一个优先级,我们可以改。 NVIC_SetPriority(SysTick_IRQn, 0xF); // 设置优先级为最低(假设4位优先级位宽) return 1; } /** * @brief 毫秒延时函数(阻塞式) * @param nTime: 延时的毫秒数 * @retval 无 */ void Delay_ms(uint32_t nTime) { TimingDelay = nTime; while(TimingDelay != 0); } // SysTick中断服务函数(同上) void SysTick_Handler(void) { if (TimingDelay != 0x00) { TimingDelay--; } }

库函数优势分析SysTick_Config(uint32_t ticks)这个函数帮我们做了所有脏活累活。我们只需要关心一个参数:ticks,即两次中断之间的时钟周期数。它自动使用HCLK/8作为时钟源,并计算LOAD = ticks - 1。代码简洁,不易出错。SystemCoreClock是库定义的全局变量,表示系统核心时钟频率(HCLK),这样我们的代码即使更换晶振频率,也只需要修改SystemCoreClock的定义处(通常在system_stm32f10x.c中),而无需修改延时相关的代码,可移植性非常好。

3.3 进阶:微秒延时实现与注意事项

毫秒延时满足大部分需求,但有时我们需要更精细的控制,比如模拟I2C、SPI的时序,或者驱动某些对时序要求严格的器件,这时微秒(us)延时就很有必要了。

实现微秒延时的关键在于提高定时精度,也就是使用更快的时钟源。因此,我们需要选择HCLK(而不是HCLK/8)作为SysTick的时钟源。

微秒延时实现思路:

  1. 重新配置SysTick:将时钟源改为HCLK。注意,这会影响之前基于HCLK/8配置的毫秒延时。所以通常有两种策略:
    • 策略A:只保留一种延时。如果需要us延时,就全程用HCLK,ms延时通过循环调用us延时实现(例如Delay_ms(10)调用100次Delay_us(100))。但这样ms延时会占用大量CPU时间在循环和函数调用上。
    • 策略B(推荐)动态切换时钟源。平时SysTick以HCLK/8运行,提供1ms中断用于系统时基或ms延时。当需要us延时时,临时将SysTick切换到HCLK模式,实现一个高精度的短时间阻塞延时,用完后立即切回。这需要更精细的代码控制。
    • 策略C:使用另一个通用定时器(如TIM2)专门做高精度延时,与SysTick互不干扰。这是最干净的方法,但多占用一个定时器资源。

这里展示策略A的简化版本,即系统只使用高精度us延时,ms延时基于us延时构建。

volatile uint32_t g_fac_us = 0; // us延时的时钟周期数因子 volatile uint32_t g_fac_ms = 0; // ms延时的时钟周期数因子(基于us) /** * @brief 初始化SysTick,时钟源为HCLK,用于us级延时 * @param sysclk: 系统时钟频率,单位Hz (例如72,000,000) * @retval 无 */ void Delay_Init(uint32_t sysclk) { // 1. 关闭SysTick SysTick->CTRL = 0; // 2. 选择时钟源为 HCLK (SysTick_CTRL_CLKSOURCE_Msk) SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk; // 3. 计算1us对应的时钟周期数。sysclk是Hz,1us=1e-6秒。 g_fac_us = sysclk / 1000000; // 72MHz下,g_fac_us = 72 // 4. 计算1ms对应的时钟周期数(基于us延时循环) g_fac_ms = 1000 * g_fac_us; // 72MHz下,g_fac_ms = 72000 // 注意:此时不使能中断,因为我们用查询方式做阻塞延时。 } /** * @brief 微秒级阻塞延时(查询方式,非中断) * @param nus: 要延时的微秒数,范围受24位计数器限制 * 对于72MHz,最大值约为 2^24 / 72 ≈ 233,016us ≈ 233ms * @retval 无 */ void Delay_us(uint32_t nus) { uint32_t temp; // 设置重装载值,nus微秒对应的时钟周期数 SysTick->LOAD = nus * g_fac_us; SysTick->VAL = 0; // 清空计数器 SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; // 启动计数器(不使能中断) do { temp = SysTick->CTRL; // 读取控制寄存器 } while((temp & 0x01) // 检查ENABLE位是否还使能(防止被意外关闭) && !(temp & (1<<16))); // 检查COUNTFLAG位是否为0(计数器是否减到0) SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; // 关闭计数器 SysTick->VAL = 0; // 清空计数器(可选) } /** * @brief 毫秒级阻塞延时(基于Delay_us循环实现) * @param nms: 要延时的毫秒数 * @retval 无 * @note 此方法会长时间占用CPU,不适合在需要低功耗或复杂多任务的场合使用。 * 对于长延时,建议还是用中断方式。 */ void Delay_ms(uint32_t nms) { for(uint32_t i=0; i<nms; i++) { Delay_us(1000); // 延时1000us = 1ms } }

微秒延时关键点与坑

  1. 阻塞与CPU占用Delay_us函数采用查询方式,CPU会一直循环检查COUNTFLAG位,直到时间到。这期间CPU无法执行其他任务,功耗也高。所以微秒延时只适用于极短时间的等待。
  2. 延时范围:由于是24位计数器,一次能延时的最大时间是有限的。例如72MHz下,最大nus * g_fac_us不能超过16,777,215,所以单次Delay_us最大约233ms。Delay_ms函数通过循环调用Delay_us(1000)来突破这个限制,但代价是CPU被完全占用。
  3. 中断冲突:这个实现禁用了SysTick中断TICKINT位为0)。如果你系统中其他地方(如RTOS)还需要SysTick中断,那么就不能这样用,必须采用策略C(使用另一个定时器)。
  4. 精度误差:函数调用、循环判断本身会消耗几个时钟周期,因此Delay_us会有几个微秒的系统误差。对于非常精确的时序(如模拟8080并口),可能需要用示波器测量后做少量补偿。

4. 常见问题、调试技巧与进阶应用实录

在实际项目中,仅仅让SysTick跑起来还不够,还会遇到各种奇怪的问题。下面是我和很多开发者踩过的坑以及解决方案。

4.1 延时不准?可能是这些原因

  1. 时钟树配置错误:这是最根本的原因。SysTick的时钟来源于HCLKHCLK/8。如果你的SystemCoreClock(系统核心时钟)变量设置不对,或者你误以为主频是72MHz而实际是8MHz(使用内部RC振荡器且未配置PLL),那么所有计算都会出错。务必在初始化SysTick前,确认SystemCoreClock的值是正确的。可以通过在调试模式下查看这个变量,或者用定时器输出一个PWM波用示波器测量来反推系统频率。
  2. 中断优先级与中断嵌套:如果SysTick中断被更高优先级的中断频繁打断,那么它触发的时间间隔就会变长,导致基于中断递减的Delay_ms函数变慢。确保SysTick的中断优先级设置合理,通常设为最低。检查系统中是否有其他高优先级、执行时间长的中断。
  3. 编译器优化:全局延时变量TimingDelay必须用volatile关键字修饰!否则,编译器可能会认为while(TimingDelay != 0)这个循环条件永远不会变(因为它在主循环里没被修改),从而将其优化掉,导致死循环。volatile告诉编译器这个变量可能被意外改变(如中断服务程序),必须每次从内存读取。
  4. 重装载值计算错误:牢记公式重装载值 = (时间 * 时钟频率) - 1(如果遵循计数器从N减到0共N+1个周期的理解)。使用库函数时,直接传入SystemCoreClock / 1000得到的是1ms所需的HCLK周期数,但库函数内部用HCLK/8,所以实际是1ms的8倍时间。正确的库函数调用是SysTick_Config(SystemCoreClock / 8000)仔细核对你的时钟源选择和计算过程。

4.2 如何测量和校准SysTick延时?

“感觉延时不对”需要数据支撑。如何验证你的1ms延时真的是1ms?

  1. GPIO翻转法(最常用):在SysTick中断服务函数里,或者在你的Delay_ms(500)前后,翻转一个GPIO引脚的电平。用示波器或逻辑分析仪测量这个引脚方波的周期。如果配置为1ms中断,那么你会在中断里每1ms翻转一次,示波器应该看到周期2ms的方波。如果调用Delay_ms(500),你应该看到引脚高电平或低电平持续500ms。
    // 在SysTick_Handler中 void SysTick_Handler(void) { static uint8_t flag = 0; if(g_systick_delay_ms > 0) g_systick_delay_ms--; // 测量用:每进入一次中断,翻转一次IO GPIO_WriteBit(GPIOA, GPIO_Pin_0, (flag ^= 1) ? Bit_SET : Bit_RESET); }
  2. 使用高级定时器输入捕获:如果精度要求极高,可以用一个高精度定时器(如TIM2)的输入捕获功能,来测量上述GPIO脉冲的宽度,从而在代码内部分析误差。
  3. 软件仿真查看:在Keil或IAR的仿真模式下,可以设置断点,查看系统运行时间。虽然不如硬件测量准,但可以快速排查巨大误差。

4.3 SysTick在RTOS与裸机系统中的不同角色

这是理解SysTick价值的关键进阶点。

  • 在裸机系统中:SysTick通常被用作一个简单的“系统心跳”或“软件定时器”基准。就像我们上面做的,提供一个精准的毫秒级时基,所有需要定时的任务(如按键扫描、LED呼吸灯、传感器数据采集周期)都可以基于这个时基来判断时间是否到期。你可以维护一个全局的uint32_t systick_counter变量,在中断里自增,然后在主循环里判断if(systick_counter - last_time > interval)
  • 在RTOS(如FreeRTOS, uC/OS)中:SysTick是操作系统的“心脏”。它产生固定的时间片(Tick),用于:
    • 任务调度:当时间片用完,强制进行任务切换。
    • 内核延时:提供vTaskDelay()这类延时API。
    • 软件定时器:为RTOS的软件定时器功能提供驱动。在这种情况下,SysTick已经被操作系统接管,应用程序就不要再直接去修改它的配置或者使用它做阻塞延时了!你应该使用RTOS提供的延时函数。如果你在RTOS中还想用SysTick做其他用途,几乎肯定会破坏系统稳定性。

4.4 更优雅的设计:非阻塞延时与软件定时器

阻塞延时while(TimingDelay != 0)在简单的裸机程序中没问题,但它让CPU空转,效率低下。更高级的用法是非阻塞延时状态机

思路:不原地等待,而是记录一个“目标时间点”,然后主程序继续执行其他任务,每次循环时检查当前时间是否超过了目标时间点。

// 假设sys_tick是SysTick中断里每毫秒自增的全局变量 volatile uint32_t sys_tick = 0; void SysTick_Handler(void) { sys_tick++; } // 非阻塞延时函数 typedef struct { uint32_t start_tick; uint32_t delay_ms; uint8_t is_running; } soft_timer_t; void timer_start(soft_timer_t* timer, uint32_t delay_ms) { timer->start_tick = sys_tick; timer->delay_ms = delay_ms; timer->is_running = 1; } uint8_t timer_is_expired(soft_timer_t* timer) { if(!timer->is_running) return 0; // 处理计数器回绕(约49.7天回绕一次,对于ms级定时可忽略,但严谨起见应处理) if((sys_tick - timer->start_tick) >= timer->delay_ms) { timer->is_running = 0; return 1; } return 0; } // 在主循环中这样使用 soft_timer_t led_timer; timer_start(&led_timer, 500); // 启动一个500ms的定时器 while(1) { // 执行其他任务... if(timer_is_expired(&led_timer)) { LED_Toggle(); timer_start(&led_timer, 500); // 重新开始,实现闪烁 } // 可以同时管理很多个这样的软件定时器 }

这种方式,CPU利用率几乎是100%,可以同时处理多个定时任务,是裸机系统走向复杂应用的必备技能。而这一切,都建立在SysTick提供的稳定毫秒时基之上。

最后,关于那个volatile关键字,我再强调一次,在中断和主程序共享的变量上,忘记它会导致各种难以复现的诡异Bug。这是嵌入式程序员成长的必修课。SysTick虽小,但从它入手,你能把时钟系统、中断机制、阻塞与非阻塞编程、RTOS基础都串起来理解,绝对是STM32学习路上性价比极高的一个知识点。

http://www.jsqmd.com/news/955638/

相关文章:

  • 代理记账服务有哪些关键点?白云区资深财税咨询机构要点拆解 - 资讯综合站
  • 还在为电子课本下载烦恼吗?这个免费工具让你3分钟搞定全套教材!
  • 2026 天津包包回收综合实力:五大平台实测,收的顶领跑 - 奢侈品回收评测
  • MATLAB迎风格式求解ut+ux0方程:含阶跃初值、固定边界与数值-精确解对比可视化
  • 如何5分钟快速上手Tiny RDM:Redis可视化管理终极指南
  • 什么是一体化代理记账?天河区工商财税解决方案提供商详解 - 资讯综合站
  • 如何用League Toolkit打造你的终极游戏助手:5分钟快速上手指南
  • 别再只用split了!Java字符串拆分的3种实战方案与性能对比(含StringTokenizer)
  • ANSYS HFSS无源仿真实战:从传输线到过孔的信号完整性精准建模
  • SSH远程免密登录的两种方式
  • 汽车贴膜怎么选?南京日晟一文讲透玻璃膜、隐形车衣、改色膜 - GrowthUME
  • RS-485收发器电路设计:从差分信号原理到隔离与非隔离方案实战
  • 英雄联盟回放分析神器ReplayBook:从青铜到王者的进阶指南
  • 2026北京黄金回收避坑指南|报价透明可上门,实测靠谱 - 奢侈品回收测评
  • QZoneExport终极指南:三步永久保存你的QQ空间青春记忆
  • 渝中区高性价比手工牛油火锅推荐|景区周边无套路市井火锅指南 - 资讯纵览
  • 突破性低光照视觉数据集:系统性技术解析与实战应用指南
  • STM32 BOOT引脚设计不当导致系统死机:从电磁干扰到硬件可靠性
  • RFID档案管理柜生产公司推荐 - 聚澜智能
  • 5步免费获取国家中小学智慧教育平台电子课本PDF完整教程
  • 2026山东高考升学机构推荐:全周期服务实力排名与避坑指南 - 奔跑123
  • 如何轻松编辑Java字节码:Recaf的完整免费指南
  • 如何高效实现电子签名:vue-esign组件专业级解决方案
  • 手机外壳平面度翘曲度怎么光学检测?三维扫描方案详解 - 资讯纵览
  • 每天切换几十个微信手忙脚乱?同一界面聚合聊天,一站式搞定运营难题
  • STM32F103搭配ESP8266直连TLINK云,实现温湿度上传+继电器远程开关控制
  • 从方案到原厂:MEMS传感器工程师的六年技术成长与产业思考
  • 从调试实战解析冯·诺依曼与哈佛结构:嵌入式开发的内存访问本质
  • 增城区代理记账的标准是什么?精通政策的专业机构划定依据 - 资讯综合站
  • 2026黄金回收变现指南,禹竞名奢汇持证鉴定安全靠谱 - 奢侈品交易观察员