嵌入式_常见延时方式的差异与选择(for循环延时、定时器延时、汇编延时…)
这里整理几种常见的延时方式,并做简单测试供大家参考,如果有什么不对的地方,欢迎指正,共同探讨。
前言
测试基于GD32F103CBT6硬件平台,标准的72MHz系统时钟, 使用标准库GD32F10x_Firmware_Library_V1.0.0提示:(提示:此库坑多、慎用!)
测试方法为:
1:在Debug模式下延时开始于结束为止使用断点,测试两个断点之间的时间差。
2:通过延对GPIO周期性输出高低电平,使用示波器测试周期的准确性。
准备工作:
需设置Debug模式Keil对于片子的时钟配置参数,不然Debug模式下测试断点时间是不准确的。如图所示,我们使用的是72兆时钟,所以需要设置为实际的系统时钟参数为72.0。
以此保证,测试时间的准确性。
一、简单的for循环延时
for(uint6_t i = 0;i < 10000;i++)
{
}
delaytime(s) = 0.00096057 - 0.00012024 = 0.00084033s
大概也就840us左右,使用示波器看一下周期,大概也差不多就这样。
如果把10000改成100,这个时间延时会不会缩小100倍呢?这个实验我也试过了,结果如下:delaytime(s) = 0.00012907 - 0.00011732 = 0.00001165s 大概也就11us左右,还是有一点差异的,不会等比变化。
而且使用Cotex-M3内核与Cotex-M0内核做对比,个人猜想这个结果也是有差异的。(没找到M3内核的板子)
而且我使用Cotex-M4内核与Cotex-M0内核做对比,这个结果差异也很大。
贴一个简单的for循环C语言代码
for(uint6_t i = 0;i < 1000;i++)
{}
该for循环编译形成汇编代码如下,有兴趣的朋友可以去分析一波,我这里就不分析了,
代码如下(示例):
0x0800023C DBFA BLT 0x08000234
0x08000232 E001 B 0x08000238
0x08000234 1C41 ADDS r1,r0,#1
0x08000236 B288 UXTH r0,r1
0x08000238 F5B07F7A CMP r0,#0x3E8
0x0800023C DBFA BLT 0x08000234
二、常见的定时器延时
如果片子上有定时器资源,可以使用定时器延时,但是片子上资源有限要提前做好资源分配,废话不多说这里使用通用定时器进行延时操作,并对结果进行对比。
通用定时器:
/**********************************************************************
*1-函数名:Timer2_Init
*2-函数功能:初始化定时器
**********************************************************************/
void Timer2_Init(void)
{
/*由于GD32与ST32寄存器差异,此处配置的Timer1定时器*/
/*步长1us计时器*/
TIMER_BaseInitPara sTIM_TimeBaseStructure;
NVIC_InitPara NVIC_InitStructure;
//NVIC_PRIGroup_Enable(NVIC_PRIGROUP_0);
RCC_APB1PeriphClock_Enable(RCC_APB1PERIPH_TIMER2, ENABLE); //实际是复位Timer2(代码名字错位)
TIMER_DeInit(TIMER2);
sTIM_TimeBaseStructure.TIMER_Period = 0x0000FFFF; //计数器自动重装值
sTIM_TimeBaseStructure.TIMER_Prescaler = 71; //计数器时钟预分频值,计数器时钟等于 PSC 时钟除以 (PSC+1)
sTIM_TimeBaseStructure.TIMER_ClockDivision = TIMER_CDIV_DIV2; //设置时钟分割:fDTS=fTIMER_CK
sTIM_TimeBaseStructure.TIMER_CounterMode = TIMER_COUNTER_UP; //TIM向上计数模式
TIMER_BaseInit(TIMER2, &sTIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
TIMER_INTConfig(TIMER2,TIMER_INT_UPDATE,ENABLE); //中断使能
NVIC_InitStructure.NVIC_IRQ = TIMER2_IRQn; //TIM2中断
NVIC_InitStructure.NVIC_IRQPreemptPriority = 0; //Q抢占优先级优先级0级
NVIC_InitStructure.NVIC_IRQSubPriority = 8; //副优先级2级
NVIC_InitStructure.NVIC_IRQEnable = ENABLE; //IRQ通道被使能
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
TIMER_Enable(TIMER2, ENABLE); //使能定时器外设
}
/**********************************************************************
*1-函数名:Task_Init
*2-函数功能:获取定时器计数个数
**********************************************************************/
uint16_t Timer1_GetTimerCounter(void)
{
uint16_t counter = 0;
counter = (uint16_t)TIMER_GetCounter(TIMER2);
return counter;
}
/**********************************************************************
*1-函数名:Delay_us
*2-函数功能:Delay_us
**********************************************************************/
void Delay_us(uint16_t usCounter)
{
uint16_t TempCounter = 0;
uint16_t TotalCounter = 0;
uint16_t InitCounter = 0;
InitCounter = Timer1_GetTimerCounter();
TotalCounter = InitCounter + usCounter;
if(TotalCounter >= InitCounter)
{
do
{
TempCounter = Timer1_GetTimerCounter();
} while (TempCounter < TotalCounter);
}
else
{
do
{
TempCounter = Timer1_GetTimerCounter();
} while ((TempCounter > TotalCounter) && (TempCounter < InitCounter));
}
}
通用定时器:
delaytime(s) = 0.00032718 - 0.00012229 = 0.00020587s 大概也就205us左右,使用示波器看一下周期,还是比较准确的。
结果:
三、汇编延时
汇编延时,利用内嵌汇编进行循环进行延时,这里简单解读一下:
内嵌汇编就是在它让你可以在C程序中插入使用汇编语言编写的函数, 详细见代码注释
#if defined (__CC_ARM) /*!< ARM Compiler */
/*
delayus = (1/SystemCoreClock * 3 * ulCount)us
*/
__ASM volatile void SysCtlDelay(unsigned long ulCount)
{
subs r0, #1; //函数参数保存在R0寄存器,这里将参数进行自减操作并把结果依旧保存在R0中。
bne SysCtlDelay; //R0 != 0,则跳到SysCtlDelay,这里如果R0自减不等于0,则继续执行该函数
bx lr; //返回主程序(不可省略)
}
void SysCtlDelayus(unsigned long ulCount)
{
//CPU_INI_DISABLE(); //关闭中断
SysCtlDelay(ulCount * (SystemCoreClock/3000000));
//CPU_INI_ENABLE(); //打开中断
}
#endif /* __CC_ARM */
__ASM volatile void SysCtlDelay(unsigned long ulCount)函数:参数ulCount表示循环执行该函数的次数
该函数由三条汇编指令构成,当参数ulcount = 1时,只执行一次三条汇编指令,
执行一条汇编指令时间为:1/系统时钟(MHz),所以ulcount = 1时,延时为delayus = (1/SystemCoreClock(MHz)* 3)us
当参数ulcount = n时,执行n次三条汇编指令,
执行一条汇编指令时间为:1/系统时钟(MHz),所以ulcount = n时,延时为delayus = (1/SystemCoreClock (MHz)* 3 * n)us
void SysCtlDelayus(unsigned long ulCount)函数:参数表示延时多少us
计算方法:系统中SystemCoreClock为CPU时钟频率,单位为Hz,所以SystemCoreClock(MHz)= SystemCoreClock(Hz)/1000000
根据上述,
因为延时 (1/SystemCoreClock(MHz)* 3)us需要ucount = 1,
所以延时1us,需要ucount = 1/(1/SystemCoreClock(MHz)* 3),
根据计算方法所述,带入公式,延时1us,需要ucount = 1/(1/(SystemCoreClock(Hz)/1000000)* 3)
化简得:
延时1 us ucount = SystemCoreClock(Hz)/3000000)
延时x us ucount = x * SystemCoreClock(Hz)/3000000),得到函数void SysCtlDelayus(unsigned long ulCount),
delaytime(s) = 0.00112594 - 0.00012038 = 0.00100556s 大概也就1000us左右,用示波器测试,延时比较精准,在使用前需要明确板子时钟是以兆为时钟单位,可打开或关闭系统中断会更加精准
但是这里有个疑问?SysCtlDelay()函数中当ulCount大于1时候,跳回主函数指令 bx lr;该指令只执行了一次,另两条指令分别执行两次,如果按照3次计算 是不是不准确。而且内核如果是三级流水线操作,执行时间是不是不能这么算?
总结
三种延时处理方式各有优缺点:
for循环:简单明了,几乎适用于所有C平台;但是延时精度不高,所以适用于延时精度要求不高,不会多次重复调用的情况(重复调用会使时间误差叠加放大),例如ADC初始化完成之后有一个短延时后再使能,
Timer定时器:延时精度相当准确,但是会多占用一个定时器资源,所以实际开发时候要先对资源做好规划。
汇编延时:延时也非常准确,不会占用资源,但是移植性差。原理可以通用,但代码不通用,计算过程比较复杂,需要对时钟和周期有较深的理解才行。