使用 ​​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);
	}
}
这种方式要注意的是设置Systick的中断优先级为最高,否则可能其他中断中使用此延时函数时,Systick中断无法执行,计数值无法增加,会导致程序在while (count - tmp <= us);进入死循环。
另外要注意配置NVIC的中断管理组,让Systick的中断抢占优先级生效:NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐