uiint 发表于 2025-5-12 19:45

单片机延时函数

单片机中有很多延时的实现方式,这里参考了鱼鹰谈单片机的,安福莱的原子的等网上信息,做一个整理。更加细节可以参考鱼鹰的文章,很详细。

1、汇编延时,nop指令,这个51当中就有了,332位单片机未验证也不想找了。一般不用,属于死等方式。

2、软件延时,这个方式就是for循环,属于死等方式,这个方式延时不太准确,nop不用。

3、systick定时器的方式,这个是原子或野火中常用到的,时间延时是基本上准确的,但是也属于死等方式。

当然,systick有中断的方式的,那么基本上是1ms的定时中断,我们可以在裸机的HAL库中重新写systick定时中断回调函数,而且hal_delay也是使用的这个systick的。其实可以用dwt来重写,因为hal库是若定义的。
索性把systick弄弄明白:
/**
* @brief This function provides minimum delay (in milliseconds) based
* on variable incremented.
* @NOTE In the default implementation , SysTick timer is the source of time base.
* It is used to generate interrupts at regular time intervals where uwTick
* is incremented.
* @note This function is declared as __weak to be overwritten in case of other
* implementations in user file.
* @param Delay specifies the delay time length, in milliseconds.
* @retval None
*/
__weak void HAL_Delay(uint32_t Delay)
{
uint32_t tickstart = HAL_GetTick(); //首先得到基准的时刻
uint32_t wait = Delay; //获取延时值

/* Add a freq to guarantee minimum wait */
if (wait < HAL_MAX_DELAY) //如果小于0xFFFFFFFFU 32位变量
{
wait += (uint32_t)(uwTickFreq); //递增1,根据uwTickFreq定义来,默认是1ms,也就是1 HAL_TickFreqTypeDef uwTickFreq = HAL_TICK_FREQ_DEFAULT; /* 1KHz */
}

while ((HAL_GetTick() - tickstart) < wait) //(当前的值)-(过去的基准值)< 延时时间 ,就死等,其实就是一种死等的方式了,
{
}
}
uwTick这个变量是在SysTick ISR中每1ms中递增的,

/**
* @brief This function is called to increment a global variable "uwTick"
* used as application time base.
* @note In the default implementation, this variable is incremented each 1ms
* in SysTick ISR.
* @note This function is declared as __weak to be overwritten in case of other
* implementations in user file.
* @retval None
*/
__weak void HAL_IncTick(void)
{
uwTick += uwTickFreq;
}

Src\stm32f1xx_it.c函数中的定时中如下:
/**
* @brief This function handles System tick timer.
*/
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */

/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
HAL_SYSTICK_IRQHandler();
/* USER CODE BEGIN SysTick_IRQn 1 */

/* USER CODE END SysTick_IRQn 1 */
}

/**
* @brief Provides a tick value in millisecond.
* @note This function is declared as __weak to be overwritten in case of other
* implementations in user file.
* @retval tick value
*/
__weak uint32_t HAL_GetTick(void)
{
return uwTick;
}
小结起来就是,cubemx会把systick开启1ms的定时中断,而且uwTick这个变量会在定时中断中每1ms递增一个。HAL_Delay的注释如上所示。
因此,在裸机工程中,systick可以使用死等的方式延时,也可以定时中断的方式延时。具体问题具体分析

4、dwt数据观察点与跟踪
这个和systick一样是cm3内核自带的,cm4也有,因此可以直接使能拿来使用。
初始化后就可以使用,而且是可重入的。这个在鱼鹰的文章有详细的分析,但是还是属于死等的延时方式。安福来有移植好的文件,杰杰的公众号也有写过一篇《精确延时ns的文章》,这个方式的延时精度相当高了。这个计数器是主时钟每震荡一次,就增加一次,stm32F103的72Mhz,那么精度1/72000000,当然实际上不可能这么精确,毕竟代码执行需要时间,但是1us这个级别肯定是足够了。

DWT中有剩余的计数器,它们典型地用于应用程序代码的“性能速写”(profiling)。可以编程它们,让它们在计数器溢出时发出事件(以跟踪数据包的形式)。最典型地,就是使用CYCCNT寄存器来测量执行某个任务所花的周期数,这也可以用作时间基准相关的目的(操作系统中统计 CPU 使用率可以用到它)。————《权威指南》原话

具体的可以参考安福莱的论坛:http://www.armbbs.cn/forum.php?mod=viewthread&tid=89128&highlight=dwtDWT
《实现一个精确微秒延迟的参考例程 》
而且,keil调试的时候,也是使用dwt来计算运行时间的。可以参考鱼鹰谈单片机的文章。

5、stm32的基本定时器作为延时,这个在51单片机中就已经是在熟悉不过了,其中基本上tim6、7两个都只有定时功能,于是可以拿来做是定时中断,其实systick中断也和这个类似,只是systick我们基本上是1ms的,不怎们修改,基本定时器通常可以修改定时周期。分频器+周期的设置,就可以达到我们想要的定时时长。

通常的方式:定时到了,设置一个标志位,在主循环中,判断标志位是否置位,有就执行,并清零标志位。这种方式基本上没多大问题,但是仔细分析下,假如定时的频率是20ms,但是main函数中的查询频率比较慢,30ms一次。理想情况下,到了60ms,主函数查询了2次,定时器中断触发3次,刚刚好差不多重叠了,但是注意,这是裸机,顺序执行,不会发生可冲入的问题。那么主函数中,恰恰到了这个子函数了,立马定时中断也触发了,那么回到主函数中,就执行了,也就是定时中断触发了3次,但是主函数值执行了2次。这种情况比较极端了。需要重新设计主函数,重新设定定时时长。因此这种情况,我们要好好规划程序的框架。main函数执行一次的时间要短于定时到时长。因此在main函数中,尽量不要有死等的延时,尽量使用定时器来规划。这样单片机效率高,同时可维护。

6、使用定时器实现单次延时

7、删除标志位来实现定时,这两种方式,可以参考鱼鹰的文章,这里不细说了。这里仅做抛砖引玉。

8、合作时调度器,这个其实就是软件定时器的方式来实现,网上有很多模板,其实这种方式是可维护性比较高,但是缺点比较多,限制条件比较多,具体参考安福莱的ucos教程,这里摘要如下
限制一、只有一个中断的原则。
限制二、任务重叠的问题。
限制三、使用合作式调度器的应用程序有一个重要的要求:任务的运行时间 < 时标间隔,这个要求非常重要,而且实现起来额不容易,特别是程序中含有一些无法确定时间的函数,

其实对于裸机程度,没有其他的隐蔽的东西,自己好好分析还是可以理清程序运行的细节及时序关系。遵守的原则:1、尽量不要在主程序中使用死等的延时,二、每个子程序(也可以叫任务吧)的查询频率要大于主程序运行的时间。比如:ad采样,100ms采样一次,那么,主程序一定要在100ms以内执行完毕。

想说的这么多了,裸机程序=定时器+状态机。死等的延时可以是us级别的,时序性较高的地方,大的延时就使用定时器。



uytyu 发表于 2025-5-21 09:28

void delay_ms(uint16_t ms) {
    uint32_t count = ms * 1200; // 12MHz下,1ms约需1200次循环
    while(count--);
}

pixhw 发表于 2025-5-21 09:53

在单片机编程中,延时函数是一个常用的工具

yeates333 发表于 2025-5-21 10:51

用 _nop_() 或少量循环。

rosemoore 发表于 2025-5-21 12:05

RT-Thread支持多种延时方式,包括基于线程的阻塞延时和硬件定时器的精准延时。

robincotton 发表于 2025-5-21 12:31

利用定时器中断或系统时钟            

bestwell 发表于 2025-5-21 13:00

如果需要在不同的单片机或平台上使用延时函数,应考虑其兼容性和可移植性。

sesefadou 发表于 2025-5-21 13:25

需要根据单片机的时钟周期和指令周期,精确计算出实现特定延时所需的循环次数。

pixhw 发表于 2025-5-21 13:47

延时函数的实际延时时间可能与理论值存在偏差。

fengm 发表于 2025-5-21 14:09

避免使用循环延时,改用硬件定时器。

louliana 发表于 2025-5-21 14:30

结合硬件定时器或RTC实现唤醒式延时。

olivem55arlowe 发表于 2025-5-21 15:38

如果需要实现长时间的延时,可以考虑使用定时器中断或外部硬件定时器,而不是简单的空循环延时。

sdlls 发表于 2025-5-21 16:31

使用定时器中断实现延时            

qiufengsd 发表于 2025-5-21 17:23

不同的单片机有不同的时钟频率和指令周期时间

mattlincoln 发表于 2025-5-21 17:52

为了提高延时的准确性,应尽量减少在延时函数执行期间发生中断的可能性,或者在计算延时时考虑这些因素。

tifmill 发表于 2025-5-21 18:39

在多任务或中断频繁触发的环境下,延时函数需要具备良好的可重入性,避免因多次调用导致的冲突或错误。

i1mcu 发表于 2025-5-21 19:11

在裸机环境中,延时函数通常通过循环计数器实现

jackcat 发表于 2025-5-21 19:40

如果时钟频率发生变化,需要相应调整延时函数中的计数值,以保证延时时间的准确性。

modesty3jonah 发表于 2025-5-21 19:59

裸机延时无法实现任务调度,适合单任务或简单系统。

claretttt 发表于 2025-5-21 20:20

延时函数会导致任务进入阻塞状态,系统会调度其他就绪任务。
页: [1] 2 3
查看完整版本: 单片机延时函数