STM32开发常用延时方式详解:原理、场景与代码示例
在STM32开发中,延时是常见的操作之一,实现方法多样。下面将详细介绍几种常用的延时方式、其工作原理、适用场景,并结合示例代码进行分析。
在STM32开发中,延时是常见的操作之一,实现方法多样。下面将详细介绍几种常用的延时方式、其工作原理、适用场景,并结合示例代码进行分析。
目录
PS:1s= 1000ms,1ms=1000μs,1us=1000ns
1. 空循环延时
(1)原理:通过执行一系列无实际功能的指令来消耗CPU时间,从而达到延时的效果。这种方式简单直接,但精确度不高。
void delay(uint32_t count) {
for(uint32_t i = 0; i < count; i++);
}
(2)适用场景
裸机系统中的简单应用:例如控制LED闪烁,对于精度要求不高的场合非常实用。
调试阶段:在快速测试和观察现象时很有帮助。
(3)缺点:受CPU频率和编译器优化的影响较大,导致延时不够稳定和准确。
2. HAL库延时函数 HAL_Delay()(ms级别)
(1)原理:基于SysTick定时器实现,提供毫秒级别的阻塞式延时。调用此函数时,程序会进入等待状态直到指定时间到达。
/**
* @brief 此函数是 HAL 库提供的毫秒级延时函数,使用弱定义(__weak),允许用户在需要时重写该函数。
* @param Delay 要延时的毫秒数
* @retval 无
*/
__weak void HAL_Delay(uint32_t Delay)
{
// 记录延时开始时的系统滴答计数
uint32_t tickstart = HAL_GetTick();
// 保存要延时的毫秒数
uint32_t wait = Delay;
/* Add a freq to guarantee minimum wait */
// 如果要延时的时间小于 HAL_MAX_DELAY,为了保证最小的延时时间,在延时时间上加上 uwTickFreq
// uwTickFreq 通常是系统滴答定时器的频率(单位:毫秒)
if (wait < HAL_MAX_DELAY)
{
wait += (uint32_t)(uwTickFreq);
}
// 进入循环,持续检查从延时开始到现在经过的时间是否达到指定的延时时间
// HAL_GetTick() 函数用于获取当前的系统滴答计数
while ((HAL_GetTick() - tickstart) < wait)
{
// 循环体为空,此循环会一直占用 CPU 资源,直到延时时间到达
}
}
(2)适用场景
对实时性要求不高的裸机项目:适合需要简单的毫秒级延时的应用场景。
非实时操作系统:由于其阻塞性质,不适合对响应速度有严格要求的环境。
(3)缺点:HAL_Delay()不要在中断服务程序中使用,因其是阻塞式函数,延时期间CPU等待,会错过其他中断,还破坏中断嵌套机制,影响系统实时性与稳定性,可用定时器或软件计时替代。
3. SysTick定时器延时(us级别)
(1)原理:利用ARM Cortex-M内核自带的24位递减计数器SysTick(系统节拍),它属于 NVIC的一部分,且可以产生 SysTick 异常(异常类型#15)。通过读取并判断计数值来实现精确延时,从0xFFFFFF向下计数到0。
/**********************************************************************
* 函数名称: udelay
* 功能描述: us 级别的延时函数(复制 rt - thread 的代码)
* 输入参数: us - 延时多少 us
* 输出参数: 无
* 返 回 值: 无
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
***********************************************************************/
void udelay(int us)
{
// 记录 SysTick 定时器当前的计数值,作为计时起点
uint32_t told = SysTick->VAL;
uint32_t tnow;
// 获取 SysTick 定时器的重装载值,用于后续计算
uint32_t load = SysTick->LOAD;
/* LOAD+1 个时钟对应 1ms
* n us 对应 n*(load+1)/1000 个时钟
*/
// 根据输入的 us 数,计算需要的时钟滴答数
uint32_t ticks = us*(load+1)/1000;
// 用于累计已经过去的时钟滴答数
uint32_t cnt = 0;
// 进入循环,开始计时
while (1)
{
// 获取 SysTick 定时器当前的计数值
tnow = SysTick->VAL;
if (told >= tnow)
// 如果计数值没有溢出,直接计算两次计数值的差值并累加到 cnt 中
cnt += told - tnow;
else
// 如果计数值溢出了,需要加上重装载值来正确计算经过的滴答数
cnt += told + load + 1 - tnow;
// 更新计时起点为当前计数值
told = tnow;
if (cnt >= ticks)
// 当累计的滴答数达到需要的滴答数时,退出循环,延时结束
break;
}
}
(2)适用场景
需要精确短时延时的裸机系统:如I2C、SPI通信中的时序控制。
RTOS环境中作为心跳时钟:为操作系统提供基本的时间基准。
4. 通用定时器延时(us级别)
(1)原理:通过配置STM32的通用定时器(如TIM2-TIM5),通过读取定时器 4 的计数值,结合定时器的自动重装载值,计算出需要的时钟滴答数,然后在一个循环中不断累加已经过去的时钟滴答数,直到达到需要的延时时间,从而实现微秒级的延时。
/**
* @brief 实现微秒级延时的函数
* @param us: 需要延时的微秒数
* @retval 无
* @note 此函数利用定时器4 (TIM4) 的计数值来实现精确的微秒级延时。
* 前提是TIM4已经正确配置,并且其计数周期和时钟频率已知。
*/
void udelay(int us)
{
// 获取定时器4当前的计数值,作为延时开始的初始计数值
uint32_t told = __HAL_TIM_GET_COUNTER(&htim4);
// 用于存储定时器4当前的计数值
uint32_t tnow;
// 获取定时器4的自动重装载值,该值决定了定时器计数的上限
uint32_t load = __HAL_TIM_GET_AUTORELOAD(&htim4);
/* LOAD+1个时钟对应1ms
* n us对应 n*(load+1)/1000个时钟
*/
// 根据传入的微秒数us,计算需要的时钟滴答数
// 这里的计算基于LOAD+1个时钟对应1ms的关系
uint32_t ticks = us*(load+1)/1000;
// 用于累计已经过去的时钟滴答数
uint32_t cnt = 0;
// 进入循环,开始进行延时计时
while (1)
{
// 获取定时器4当前的计数值
tnow = __HAL_TIM_GET_COUNTER(&htim4);
if (tnow >= told)
// 如果当前计数值大于等于初始计数值,说明定时器没有发生溢出
// 直接计算两次计数值的差值,并累加到已经过去的时钟滴答数cnt中
cnt += tnow - told;
else
// 如果当前计数值小于初始计数值,说明定时器发生了溢出
// 需要考虑溢出情况,计算经过的时钟滴答数并累加到cnt中
cnt += load + 1 - told + tnow;
// 更新初始计数值为当前计数值,为下一次循环做准备
told = tnow;
if (cnt >= ticks)
// 当累计的时钟滴答数达到或超过需要的时钟滴答数时
// 说明延时时间已到,退出循环
break;
}
}
(2)适用场景
对延时精度有较高要求的裸机应用:如电机控制或PWM信号生成。
RTOS任务中的精确延时需求:确保不影响其他任务的同时完成精准延时。
5. RTOS系统自带延时函数(ms级别)
(1)原理:以FreeRTOS为例,vTaskDelay函数允许任务暂停指定的时间,期间释放CPU给其他任务,待时间到达后恢复任务。
/*********************** Generic Wait Functions *******************************/
/**
* @brief Wait for Timeout (Time Delay)
* @param millisec time delay value
* @retval status code that indicates the execution status of the function.
*/
osStatus osDelay (uint32_t millisec)
{
#if INCLUDE_vTaskDelay
TickType_t ticks = millisec / portTICK_PERIOD_MS;
vTaskDelay(ticks ? ticks : 1); /* Minimum delay = 1 tick */
return osOK;
#else
(void) millisec;
return osErrorResource;
#endif
}
(2)适用场景
多任务环境下的同步与调度:适用于周期性任务执行或任务间协调。
总结
每种延时方法都有其特点和适用范围。在选择时,应综合考虑应用的需求、系统的架构以及性能要求。
更多推荐
所有评论(0)