滴答时钟延时
SysTick 工作原理
SysTick 是 Cortex-M 内核的一个递减计数器,时钟源可配置为处理器时钟或外部时钟。初始化时需设置 LOAD 寄存器作为重装载值,VAL 寄存器存储当前计数值,从 LOAD 开始递减至 0 后自动重载。
周期计算方式为 SysTick->LOAD - SysTick->VAL
,表示当前已走过的时钟周期数。时钟配置通常使用处理器时钟(SysTick_CTRL_CLKSOURCE_Msk
),并启用中断(SysTick_CTRL_TICKINT_Msk
)和计数器(SysTick_CTRL_ENABLE_Msk
)。
解读一些不清晰的地方
我们注意到SysTick是一个递减计数器,
初始化时我们设置了LOAD,
它是一个重装载的值
VAL是当前计数器的值,
它会从LOAD开始向下递减
到达0之后,会被重新赋值为LOAD的值
故 SysTick->LOAD - SysTick->VAL是当前走过的周期数
关键宏定义
#define TICKS_PER_MS (SystemCoreClock / 1000) // 1ms 对应的时钟周期数
#define TICKS_PER_US (SystemCoreClock / 1000000) // 1us 对应的时钟周期数
初始化函数
void cpu_tick_init(void) {SysTick->LOAD = TICKS_PER_MS; // 设置重装载值为 1ms 的周期数SysTick->VAL = 0; // 清空当前计数值SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | // 使用处理器时钟SysTick_CTRL_TICKINT_Msk | // 允许中断SysTick_CTRL_ENABLE_Msk; // 启动计数器
}
时间获取函数
uint64_t cpu_now(void) {uint64_t now, last_count;do {last_count = cpu_tick_count; // 记录中断累积的计数值now = cpu_tick_count + (SysTick->LOAD - SysTick->VAL); // 计算当前总周期数} while (last_count != cpu_tick_count); // 避免读取时中断触发导致数据不一致return now;
}uint64_t cpu_get_us(void) {return cpu_now() / TICKS_PER_US; // 转换为微秒
}uint64_t cpu_get_ms(void) {return cpu_now() / TICKS_PER_MS; // 转换为毫秒
}
延时函数
void cpu_delay_us(uint32_t us) {uint64_t now = cpu_now();while (cpu_now() - now < (uint64_t)us * TICKS_PER_US); // 等待指定微秒数
}void cpu_delay_ms(uint32_t ms) {uint64_t now = cpu_now();while (cpu_now() - now < (uint64_t)ms * TICKS_PER_MS); // 等待指定毫秒数
}
中断处理函数
void SysTick_Handler(void) {cpu_tick_count += TICKS_PER_MS; // 每次中断增加 1ms 的周期数
}
注意事项
- 原子操作:
cpu_now()
使用循环检查确保数据一致性,避免中断导致读取错误。 - 时钟频率:
SystemCoreClock
需正确配置为当前处理器时钟频率。 - 中断优先级:SysTick 中断优先级需合理设置,避免影响其他关键任务。
- 溢出处理:
cpu_tick_count
为 64 位变量,可支持长时间运行,但需注意延时函数的参数范围。
原子操作解读
初始状态设定
text
系统时钟: 168MHz
TICKS_PER_MS = 168,000
LOAD = 168,000
当前时刻:
cpu_tick_count = 1,000,000 (表示已运行约5.95秒)
SysTick->VAL = 500 (当前ms还剩500个周期)
场景:在读取过程中发生中断
⚠️ 没有 do-while 的情况
c
uint64_t cpu_now_bad(void)
{
// 直接计算,没有保护
return cpu_tick_count + SysTick->LOAD - SysTick->VAL;
}
执行过程:
text
时间点1: 读取 cpu_tick_count = 1,000,000
时间点2: 读取 VAL = 500
时间点3: **发生中断!** cpu_tick_count 变成 1,168,000
时间点4: 计算 now = 1,000,000 + 168,000 - 500 = 1,167,500
返回: 1,167,500 ❌
结果分析:
- 正确时间应该是:1,168,000 + (168,000 - 500) = 1,168,000 + 167,500 = 1,335,500
- 实际返回:1,167,500
- 误差:1,335,500 - 1,167,500 = 168,000 个周期 = 1ms的误差!
✅ 有 do-while 的情况
c
uint64_t cpu_now_good(void)
{
uint64_t now, last_count;
do {
last_count = cpu_tick_count; // 第1次: 1,000,000
now = cpu_tick_count + SysTick->LOAD - SysTick->VAL; // 计算: 1,167,500
} while (last_count != cpu_tick_count); // 检查: 1,000,000 ≠ 1,168,000 → 重新循环
// 第2轮循环:
do {
last_count = cpu_tick_count; // 第2次: 1,168,000
now = cpu_tick_count + SysTick->LOAD - SysTick->VAL; // 计算: 1,335,500
} while (last_count != cpu_tick_count); // 检查: 1,168,000 = 1,168,000 → 退出
return now; // 返回: 1,335,500 ✓
}
加锁的意义在于,防止在计算时,突然发生了中断,而cpu_tick_count却来不及更新而导致的误差
源代码:
uint64_t cpu_now(void)
{
uint64_t now, last_count;
do {
last_count = cpu_tick_count;
now = cpu_tick_count + SysTick->LOAD - SysTick->VAL;
} while (last_count != cpu_tick_count);
return now;
}