使用 SysTick 定时器(系统滴答定时器) 实现微秒级延时
SysTick 是 Cortex-M 内核自带的 24 位定时器,运行在 系统主频(如 84MHz / 168MHz) 上,非常适合用来实现延时函数:
1. 确定系统时钟频率(SYSCLK)
2. SysTick 基本公式
-
1 微秒(μs) = 1 / (SYSCLK_MHz) 秒
- 例如F407系统最大时钟频率为168MHz,以系统时钟为计数时钟频率,每计数168次为1微秒。
3. SysTick寄存器
因为标准库没有直接提供SysTick操作的函数,所以需要操作对应的寄存器。SysTick 定时器由以下 4 个内核寄存器 组成,定义在 Cortex-M 内核的 System Control Block (SCB) 中:
寄存器名称 |
寄存器全称 |
位宽 |
说明 |
---|---|---|---|
CTRL |
SysTick Control and Status Register |
32 bit |
控制与状态寄存器:使能、时钟源、中断使能、计数状态 |
LOAD |
SysTick Reload Value Register |
32 bit |
重装载值寄存器:设定定时周期 |
VAL |
SysTick Current Value Register |
32 bit |
当前计数值寄存器:读取当前倒计时值,可手动清零 |
CALIB |
SysTick Calibration Value Register |
32 bit |
校准值寄存器(一般用不到,由芯片厂商预配置) |
4. 延时函数实现方式1,轮询检查CTRL寄存器的标志位
void Systick_Delay_us(uint16_t us)
{
uint16_t i = 0;
SysTick->LOAD = SystemCoreClock / 8000000; // 8分频的情况下 1us需要计数的次数
SysTick->VAL = 0; // 清空计数值
SysTick->CTRL &= (~SysTick_CTRL_COUNTFLAG_Msk); // 清除标志位
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; // 使能
SysTick->CTRL &= (~SysTick_CTRL_TICKINT_Msk); // 不开启中断
SysTick->CTRL &= (~SysTick_CTRL_CLKSOURCE_Msk); // 8分频
for (i = 0; i < us; i++) // 每次等待1us
{
while ((SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) == 0); // 等待计数完成
SysTick->CTRL &= (~SysTick_CTRL_COUNTFLAG_Msk); // 清除标志位
}
SysTick->CTRL &= (~SysTick_CTRL_ENABLE_Msk); // 关闭计数器
}
实现原理:计算出1us需要计数的次数 然后×要等待的us数,开启定时器检查标志位,等到计数完成后结束循环,关闭计时器。
问题:如果Systick_Delay_us函数在运行中被中断,中断函数又调用了Systick_Delay_us函数,那么中断程序结束后,定时器将会被关掉,导致CTRL的标志位永远无法为1,延时函数直接会进入死循环。
解决方法:while循环可以多加入一个判断条件 :
SysTick->CTRL & SysTick_CTRL_ENABLE_Msk == 1
但是依然会丢掉上一次计数的过程,而且要加上中断程序处理的时间,导致计数值不准确。
所以如果中断程序中不使用Systick_Delay_us函数,这种方案是可行的。
5. 延时函数实现方式2,使用Systick的中断函数,创建一个全局变量,每us计数一次。
static uint32_t count = 0;
void SysTick_Init()
{
NVIC_InitTypeDef NVIC_InitStruct;
SysTick->LOAD = SystemCoreClock / 8000000; // 1us完成一次计数并产生中断
SysTick->VAL = 0;
SysTick->CTRL &= (~SysTick_CTRL_COUNTFLAG_Msk); // 清除标志位
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; // 使能
SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk; // 开启中断
SysTick->CTRL &= (~SysTick_CTRL_CLKSOURCE_Msk); // 设置8分频
/* NVIC设置Systick中断优先级为最高 */
NVIC_InitStruct.NVIC_IRQChannel = SysTick_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStruct);
}
void SysTick_Handler(void)
{
++count;
}
void Systick_Delay_us(u16 us)
{
u32 tmp = count;
if (0xFFFFFFFF - tmp <= us) // 数据会溢出 从0开始计数
{
count = 0;
tmp = 0;
}
while (count - tmp <= us); // 等待计数差大于要等待的时间
}
void Systick_Delay_ms(u16 ms)
{
u16 i = 0;
for(i = 0; i < ms; i++)
{
Systick_Delay_us(1000);
}
}