二、滴答定时器应用
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_TIME
、SYSTICK_LOAD
等)。 - 后续扫描:从代码顶部开始逐行替换宏名。若替换后的文本中包含其他宏名(如
SYSTICK_LOAD
中的SYSTICK_CYCLE_1000MS
),预处理器会重新扫描该部分,继续展开嵌套的宏,直到无宏可展开。
- 第一轮扫描:识别并记录所有
- 宏的“声明顺序”不影响展开
宏定义的位置(如SYSTICK_CYCLE_1000MS
在SYSTICK_LOAD
之后定义)不影响替换结果,因为预处理器在首次扫描时已记录所有宏名。例如:
宏展开的本质是 预处理器通过多轮扫描实现的递归文本替换,不受宏定义顺序影响。表达式中的数值计算(如 1000000/125
)由编译器在后续阶段完成。代码编译成功正是因为:
- 预处理器正确递归展开所有嵌套宏;
- 展开后的表达式语法合法(如类型转换和运算符优先级正确);
- 编译器优化了常量表达式(
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();
}
这里使用了关键字static
:static 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 协议进行发布。
感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言。