单片机中有很多延时的实现方式,这里参考了鱼鹰谈单片机的,安福莱的原子的等网上信息,做一个整理。更加细节可以参考鱼鹰的文章,很详细。
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;
}
Srcstm32f1xx_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级别的,时序性较高的地方,大的延时就使用定时器。