ARM单片机滴答定时器理解与应用(二)(详细解析)(完)

发布于:2025-07-14 ⋅ 阅读:(13) ⋅ 点赞:(0)


二、滴答定时器应用

2.1、宏的顺序

首先关于宏的顺序需要说明一下,宏的顺序不影响展开的结果。

#define SYSTICK_CYCLE_TIME SYSTICK_CYCLE_125US 

#define SYSTICK_LOAD (SystemCoreClock /(unsigned long int)(SYSTICK_CYCLE_1000MS/SYSTICK_CYCLE_TIME))

//各systick频率下的tick时长
#define SYSTICK_CYCLE_1000MS 				((unsigned long int) 1000000)
#define SYSTICK_CYCLE_100MS 				((unsigned long int) 100000)
#define SYSTICK_CYCLE_10MS 					((unsigned long int) 10000)
#define SYSTICK_CYCLE_5MS 					((unsigned long int) 5000)
#define SYSTICK_CYCLE_1MS 					((unsigned long int) 1000)
#define SYSTICK_CYCLE_500US 				((unsigned long int) 500)
#define SYSTICK_CYCLE_250US 				((unsigned long int) 250)
#define SYSTICK_CYCLE_125US 				((unsigned long int) 125)
#define SYSTICK_CYCLE_50US 					((unsigned long int) 50)
#define SYSTICK_CYCLE_10US 					((unsigned long int) 10)
#define SYSTICK_CYCLE_1US 					((unsigned long int) 1)

按照正常的理解我的应该这样书写

#define SYSTICK_CYCLE_1000MS 				((unsigned long int) 1000000)
#define SYSTICK_CYCLE_100MS 				((unsigned long int) 100000)
#define SYSTICK_CYCLE_10MS 					((unsigned long int) 10000)
#define SYSTICK_CYCLE_5MS 					((unsigned long int) 5000)
#define SYSTICK_CYCLE_1MS 					((unsigned long int) 1000)
#define SYSTICK_CYCLE_500US 				((unsigned long int) 500)
#define SYSTICK_CYCLE_250US 				((unsigned long int) 250)
#define SYSTICK_CYCLE_125US 				((unsigned long int) 125)
#define SYSTICK_CYCLE_50US 					((unsigned long int) 50)
#define SYSTICK_CYCLE_10US 					((unsigned long int) 10)
#define SYSTICK_CYCLE_1US 					((unsigned long int) 1)

#define SYSTICK_CYCLE_TIME SYSTICK_CYCLE_125US 

#define SYSTICK_LOAD (SystemCoreClock /(unsigned long int)(SYSTICK_CYCLE_1000MS/SYSTICK_CYCLE_TIME))

  • 多轮扫描机制
    预处理器会对源代码进行多次扫描,直到所有宏被完全展开:
    • 第一轮扫描​:识别并记录所有 #define 定义的宏名(如 SYSTICK_CYCLE_TIMESYSTICK_LOAD 等)。
    • 后续扫描​:从代码顶部开始逐行替换宏名。若替换后的文本中包含其他宏名(如 SYSTICK_LOAD 中的 SYSTICK_CYCLE_1000MS),预处理器会重新扫描该部分,继续展开嵌套的宏,直到无宏可展开。
  • 宏的“声明顺序”不影响展开
    宏定义的位置(如 SYSTICK_CYCLE_1000MSSYSTICK_LOAD 之后定义)​不影响替换结果,因为预处理器在首次扫描时已记录所有宏名。例如:

宏展开的本质是 ​预处理器通过多轮扫描实现的递归文本替换,​不受宏定义顺序影响。表达式中的数值计算(如 1000000/125)由编译器在后续阶段完成。代码编译成功正是因为:

  1. 预处理器正确递归展开所有嵌套宏;
  2. 展开后的表达式语法合法(如类型转换和运算符优先级正确);
  3. 编译器优化了常量表达式(1000000/125 → 8000);

2.2、滴答定时器中断与变量

首先是时间变量和中断函数

typedef struct SYSTICK_BUF
{
    volatile unsigned long int DelayTickMS; // 定时计数变量 用于MS延时函数
    volatile unsigned long int DelayTickUS; // 定时计数变量 用于MS延时函数
    volatile unsigned long int DelayTick;   // 定时计数变量 用于延时函数
    volatile unsigned long int TimeTick;    // 系统滴答计时
} SYSTICK_BUF;

// 滴答定时器中断函数
void SysTick_Handler(void)
{
    WDT_CLR; // 清看门狗  溢出时间4369ms

    // 延时函数计数
    if(SYSTICK.DelayTick > 0){
			SYSTICK.DelayTick--;
    }

    if (++SYSTICK.TimeTick >= SYS_TIMETICK_MAX)
    {
        SYSTICK.TimeTick = 0;
    }
}

我们前面已经配置了重装载值,那么滴答定时器已经起到了作用,那么现在要做的

一方面是通过SYSTICK.DelayTick--;实现延时。

一方面是通过
if (++SYSTICK.TimeTick >= SYS_TIMETICK_MAX) { SYSTICK.TimeTick = 0; }
为系统提供连续的时间刻度,避免计数器溢出导致时间计算错误。
SYS_TIMETICK_MAX 的取值 需根据系统最长定时需求设定,避免未到周期就归零导致逻辑错误。

SYSTICK.TimeTick:自增的值,用来供外部判断,此时是什么时间,是1ms到了、还是10ma到了。因为这个值是通过函数返回给外部接口:

unsigned long int Sys_GetTimeTick(void)
{
    return SYSTICK.TimeTick;
}
- `for (int i=0; i<10; ++i)`:推荐前置,避免临时变量开销。
- `while (a++ < 5)`:判断用旧值,循环体内 `a` 已自增(输出 1~5
void Sys_BlockDelayMS(unsigned long int n)
{
    SYSTICK.DelayTick = n * (SYSTICK_CYCLE_1MS / SYSTICK_CYCLE_TIME);
    while (SYSTICK.DelayTick)
        ;
}

前面我们已经确定最小的时基是125us,

1ms需要4个250us组成,也就是我们需要给这个DelayTick给赋值的个数,如果需要2ms,那就是2×4=8,那我们需要2000ms,那就是用2000×4=8000,本质上还是基于最小时基,然后确定我们想要的时间最小单位里面包含多少个125us,因为只有计算了一个,我们才能用n相乘才能确定最终的延时时间需要赋值的变量DelayTick。

在定时器编写的时候,一定需要有一种思想,这种思路就是我这两节描述的,就是只有有了这种思想,才能更好的封装,灵活的引入各种延时时间。

2.3、应用滴答定时器

如前面所述,我们需要找到我们需要定时的最基本单位(也就是确定单位定时时间(这个时间一般是ms或者us)需要几次个125us。)说白了就是看1ms和1us需要多少个定时中断,但是不要误会,虽然最低的中断时间是125us,但是我们还是需要计算1us的这是因为我们要统一方便计算,因为我们可能需要的是1000us,那这样只需要用1000*SYSTICK_PER_US就很方便的就得到了这个数据。

#define SYSTICK_PER_MS ((float)SYSTICK_CYCLE_1MS / (float)SYSTICK_CYCLE_TIME)
#define SYSTICK_PER_US ((float)SYSTICK_CYCLE_1US / (float)SYSTICK_CYCLE_TIME)

这是对外暴露的中断函数:

bool Sys_IsTimeOutMS(unsigned long int startTick, unsigned short int ms)
{
    uint32_t intervaltick = ((float)ms) * SYSTICK_PER_MS;

    if(intervaltick == 0){
			intervaltick = 1; // 至少1个tick		
		}
    return Sys_IsTimeOutTick(startTick, intervaltick);
}

这是外面调用的函数:

void Board_Main(void)
{
    static unsigned long cycle200ms = 0, cycle10ms = 0, cycle20ms = 0;
    if (Sys_IsTimeOutMS(cycle200ms, 200))
    {
        LCD_Cycle();
        R115EC_Cycle();
        cycle200ms = Sys_GetTimeTick();
    }
    if (Sys_IsTimeOutMS(cycle10ms, 10))
    {
        LED_Cycle();
        cycle10ms = Sys_GetTimeTick();
    }
    if (Sys_IsTimeOutMS(cycle20ms, 20))
    {
        Touch_Cycle();
        cycle20ms = Sys_GetTimeTick();
    }
    UART_Cycle();
}

这里使用了关键字staticstatic unsigned long cycle200ms = 0 的初始化仅在程序首次执行到该声明时发生,后续所有函数调用均跳过初始化,直接使用上次修改后的。

  • 当执行 cycle200ms = Sys_GetTimeTick() 时,会将当前时间戳赋值给 cycle200ms
  • 下一次函数调用时,cycle200ms 仍保持为上次赋值的结果(而非重新初始化为0),直到再次被修改。

第一次执行逻辑:

  • static unsigned long cycle200ms = 0:初始化为 0
  • Sys_IsTimeOutMS(cycle200ms, 200):判断是否超时(初始 0 与当前时间差≥200ms)。
  • 若超时,执行 LCD_Cycle() 等操作,并更新:
cycle200ms = Sys_GetTimeTick();  // 假设返回时间戳 200

第二次执行逻辑:

  • 跳过初始化(static 变量已存在)。
  • cycle200ms 仍为 200(保留上次值)。
  • 判断 Sys_IsTimeOutMS(200, 200)
    • 若当前时间戳 ≥ 400(即距离上次更新已过200ms),则执行操作并更新 cycle200ms = 400
    • 否则跳过操作。

后续执行
每次调用均继承上次更新的值,持续判断时间间隔并选择性更新。

作用域限制
虽然 cycle200ms 的值在多次调用中保留,但其作用域仍限于该函数内部,外部无法直接访问
初始值仅一次有效
如代码中 =0 仅在首次生效,后续更新完全依赖 Sys_GetTimeTick() 的赋值,与初始值无关

需要注意的是:startTick

  • startTick:是时间起点(时间戳),用于计算时间间隔的起始参考点。
  • 超时逻辑​:通过 当前时间 - startTick ≥ ms 转换的 Tick 数 判断是否超时。
  • 典型应用​:配合 static 变量记录上次执行时间点,实现周期性任务调度(如每 200ms 执行一次)

结合以上理解接着分析对外暴露的函数参数:

bool Sys_IsTimeOutTick(unsigned long int startTick, unsigned long int intervalTick)
{

    if (SYSTICK.TimeTick >= startTick)
    { // 在一次g_SysTimeTick重置周期内的比较
        return (SYSTICK.TimeTick - startTick) > intervalTick ? true : false;
    }
    else
    { // g_SysTimeTick发生了一次重置
        return (SYS_TIMETICK_MAX - (startTick - SYSTICK.TimeTick)) > intervalTick ? true : false;
    }
}

首先传进来的startTick是一个静态变量,也就是每次都会使用上一次的值,第一次执行的时候startTick是0,需要记住的是SYSTICK.TimeTick 是一直会递增的,但是startTick的更新是有频率的,分析以下代码:

    if (Sys_IsTimeOutMS(cycle200ms, 200))
    {
        LCD_Cycle();
        R115EC_Cycle();
        cycle200ms = Sys_GetTimeTick();
    }

只有满足if语句cycle200ms才会获取函数 unsigned long int Sys_GetTimeTick(void) 的返回值 return SYSTICK.TimeTick;也就是滴答定时器不停的在计时的心跳频率。

以下分析都是从第一次中断开始分析:

假设我们延迟的200ms,那么我们就需要1600次计数,

bool Sys_IsTimeOutTick(unsigned long int startTick, unsigned long int intervalTick)
{

    if (SYSTICK.TimeTick >= startTick)
    { // 在一次g_SysTimeTick重置周期内的比较
        return (SYSTICK.TimeTick - startTick) > intervalTick ? true : false;
    }
    else
    { // g_SysTimeTick发生了一次重置
        return (SYS_TIMETICK_MAX - (startTick - SYSTICK.TimeTick)) > intervalTick ? true : false;
    }
}

bool Sys_IsTimeOutMS(unsigned long int startTick, unsigned short int ms)
{
    uint32_t intervaltick = ((float)ms) * SYSTICK_PER_MS;

    if(intervaltick == 0){
			intervaltick = 1; // 至少1个tick		
		}
    return Sys_IsTimeOutTick(startTick, intervaltick);
}

从上述代码可以分析,
第一次startTick传进来的值是0,由于SYSTICK.TimeTick一直都会自增,每次循环都会判断一次 (SYSTICK.TimeTick - startTick) > intervalTick,其中变量intervalTick表示的就是200ms需要的心跳次数1600。因此SYSTICK.TimeTick第一次到1600的时候该函数就会返回True,从而使得函数Sys_IsTimeOutMS返回True,这样就可以满足函数Board_Main里面的第一个条件判断Sys_IsTimeOutMS(cycle200ms, 200)。但是这个时候要注意,

    if (Sys_IsTimeOutMS(cycle200ms, 200))
    {
        LCD_Cycle();
        R115EC_Cycle();
        cycle200ms = Sys_GetTimeTick();
    }

如果函数 LCD_Cycle(); R115EC_Cycle(); 内容较多,执行时间较长,因为滴答定时器一直在心跳那么cycle200ms = Sys_GetTimeTick();获取的值就不是1600,而是其他的数值。

这里其实有一个问题,就是假如我这一次进来的时间是1599,不能满足条件,但是由于其他函数的作用,可能下一次的时间就是1700,因为进来一次,就是一个主循环的时间,这样就会产生一些误差,是否会有影响?

影响肯定会影响,但是我们要做的就是保证不会出现累计误差,首先我们要明白本次进入的时间可能是13ms,那么那么我们能做到的就是将下一次的进入我给他弄成7ms,这么看从整体20ms来看,我们保证了20ms进去了两次,这已经是在裸机层面能解决的很不错的办法了,如果还是不够,还是需要很准时,那么只能通过RTOS来解决,这个就是我另外专栏的内容了。(后续会开专题对此问题进行分析并校正)

2.4、边界效应

    if(intervaltick == 0){
			intervaltick = 1; // 至少1个tick		
		}

目的是确保时间间隔的最小有效单位为1个SysTick计时单元(1 tick)​,其必要性源于硬件特性、计算逻辑和系统稳定性需求。

SysTick的最小计时单位是1个tick
SysTick作为24位递减计数器,其最小计数步长为1个时钟周期(1 tick)。

物理上无法实现小于1 tick的时间间隔,强制设为0会导致逻辑矛盾。

  • 时间差计算依赖公式:当前时间 - 起始时间 ≥ intervaltick
  • intervaltick=0,则 ​任何非负时间差(包括0)均满足条件,导致函数始终返回超时(true),违背设计意图。

在阻塞式延时函数中(如 vDelayUs(nus)),若 intervaltick=0

while (!Sys_IsTimeOut(start, 0)); // 死循环!条件恒成立

代码将陷入无限等待,占用CPU资源。

  • 物理层面​:SysTick的硬件能力决定了 ​1 tick是最小可测量时间单位
  • 逻辑层面​:0值会使超时判断失效,引发死循环、误触发或崩溃
  • 系统层面​:保障任务调度、延时控制的确定性,避免未定义行为。

2.5、简单理解封装思想

此外还需要学习的思想的是:
不管任何定时器还是计数器所产生的中断,如果想用封装的思想 必须要转换到硬件计数上面,落脚点要明确。同样在写其他封装层的时候,就需要这种思想,学会转化,就跟以前高中做题的时候一样,要转化。
并且需要看边界效应,特别是在封装的时候,因为封装会导致忽略一些边界,因为封装是从整体考虑,但是这样就会遗漏一些内容。


文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。

【版权声明】
本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:
署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。
相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。

感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言。


网站公告

今日签到

点亮在社区的每一天
去签到