STM32关延时功能实现方式研究


接触过单片机的,肯定都用过延时函数,从while循环,到定时器延时,到systick延时,再到DWT延时等等。延时含义即从某一刻开始,到下一个某刻结束。
用途上来说,延时有 功能性非功能性之分。 功能性延时某些功能实现必须需要延时,如模拟IIC等;非功能性延时,是为了方便观察或记录现象而进行的延时,一般来说正式发布的时候可以去掉,如加入延时为了printf打印慢一点,非功能性延时通常不用很精确。
根据单片机延时时CPU的动作,分为阻塞延时和非阻塞延时。通俗理解就是 阻塞延时就是死等,CPU进入延时程序后,除了响应中断外啥也不干,就等延时时间到达。 非阻塞延时是指,进入延时后,CPU可以执 行除中断意外的其他功能
根据延时时是否是靠中断计时,分为中断延时(定时器溢出中断)和非中断延时。

主控:STM32F407ZGT6(HSE:8MHz)
库:STD标准库V1.8.0
工具:RIGOL DS1104Z PLUS数字示波器
各种延时函数测试标准:无论那种方式,mian函数中功能有RTC自动唤醒事件(1s唤醒),通过这些来模拟日常开发,每种延时方式开启后主要测试微秒us延时,毫秒ms延时因为有中断的存在会不准确,当然也有毫秒延时本身误差在。微妙延时会测试50微秒us级(<100us)3组,500微秒us级(<1000us)3组。毫秒延时会测试50毫秒ms级(<100ms)5组,500ms级(<1000us)3组。延时后翻转PB4脚电平,示波器测量电平翻转时间。

提示:如果不想看数据测试客户以直接阅读结论(黄色字体)。

一、阻塞延时函数

延时方式从阻塞方式到非阻塞方式,中断从无中断到有中断,准确性从粗略延时到精确延时。(本文持续更新,因此有些函数暂无)。
延时函数版本号说明

主版本号:1阻塞延时,2非阻塞延时
次版本号:0非中断,1中断,
修订号:

开源协议、版权和免责声明:

本文为原创,代码开源使用开源协议Apache-2.0,可以随意使用。请署名版权信息,格式:
Copyright (c) 20021-2021, Logan(wangzhaoyangly@Foxmail.com)。本文数据只做参考,
本人不对数据正确性和准确性做保证,由此产生问题本人概不负责。

1. 循环延时(V1.0.0)

循环延时即在一个循环中让CPU做一些没有意义的工作来完成延时的目的。由于NOP空指令在不同架构的MCU上执行时间不一定,因此不使用NOP进行延时。循环延时和NOP指令延时本质是一样的。不过这个含税延时时间需要摸索,下面这样设置,实际扩大5倍。

代码(while形式)

void delay_while_us(uint16_t time)
{
	uint16_t i = 0;
	while(time--)
	{
		i=168;//168MHz下
		while(i--);
	}
}

测试数据

在这里插入图片描述
5us延时,理论周期为10us,实际测试周期为50.8us,数据偏差+408%
在这里插入图片描述
50us延时,理论周期为100us,实际测试周期为500.8us,数据偏差+408%
这个数据已经说明问题了,微秒级延时很不准确。
上面已经说明了,这个延时函数只适合粗略延时,其他量级数据也没有测试必要。

特点总结

优点:实现简单
缺点:延时不准确,针对不同单片机需要调整
准确性:粗略定时
OS:理论可用,实际上由于OS多线程的原因,偏差更大。

2. SYSTICK非中断延时(V1.0.1)

正点原子的systick非中断延时,使用硬件定时器SYSTICK嘀嗒定时器,采用往LOAD寄存器里写值倒计时结束即延时结束,不占用额外定时器资源,不靠中断计时,但是还是阻塞方式延时,微秒级延时可以用于中断内,但是不建议在中断中使用毫秒级延时。可用于OS,但是需要改动,大多数OS的时钟节拍是靠SYSTICK的,采用时间摘取法,延时前后进行开中断和关中断,避免中断干扰。ticks 是延时 nus 需要等待的 SysTick 计数次数(也就是延时时间), told 用于记录最近一次的 SysTick->VAL 值,然后 tnow 则是当前的SysTick->VAL 值,通过他们的对比累加,实现 SysTick 计数次数的统计,统计值存放在 tcnt 里面,然后通过对比 tcnt 和 ticks,来判断延时是否到达,从而达到不修改 SysTick 实现 nus 的延时,从而可以和 OS 共用一个 SysTick(用于OS请参考正点原子代码)。

代码

/*注意systick的时钟来自AHB时钟(HCLK)8分频。一般配置系统时钟SYSCLK=AHB时钟HCLK。假设外部晶振为8MHz,然后倍频到168MHz,那么systick的时钟为21MHz,也就是systick的计数器VALMeizu减一,就代表时间过了1/21us=46.62ns(如果systick时钟源选择AHB时钟,则systick周期为1/168us=5.95ns)。所以fac_us=SystemCoreClock/8000000,意识是极端在系统时钟频率下1us需要多少个systick的周期,fac_ms同理,fac_ms=fac_us*1000。
nus为要延时的us数,nus的值<2^24/fac_us@fac_us=21 = 798915us
mus为延时的ms数,范围为0-798ms*/
#define fac_us SystemCoreClock/8000000 //us延时基数
#define fac_ms fac_us*1000
void delay_us(u32 nus)
{
 u32 temp;
 SysTick->LOAD = fac_us*nus;
 SysTick->VAL=0X00;//清空计数器
 SysTick->CTRL=0X01;//使能,减到零是无动作,采用外部时钟源
 do
 {
  temp=SysTick->CTRL;//读取当前倒计数值
 }while((temp&0x01)&&(!(temp&(1<<16))));//等待时间到达
     SysTick->CTRL=0x00; //关闭计数器
    SysTick->VAL =0X00; //清空计数器
}
void delay_ms(u16 nms) //168MHz下nms<=798ms
{
 u32 temp;
 SysTick->LOAD = fac_ms*nms;
 SysTick->VAL=0X00;//清空计数器
 SysTick->CTRL=0X01;//使能,减到零是无动作,采用外部时钟源
 do
 {
  temp=SysTick->CTRL;//读取当前倒计数值
 }while((temp&0x01)&&(!(temp&(1<<16))));//等待时间到达
    SysTick->CTRL=0x00; //关闭计数器
    SysTick->VAL =0X00; //清空计数器
}
//延时nms 
//nms:0~65535
void delay_ms(u16 nms)
{	 	 
	u8 repeat=nms/540;						//这里用540,是考虑到某些客户可能超频使用,
											//比如超频到248M的时候,delay_xms最大只能延时541ms左右了
	u16 remain=nms%540;
	while(repeat)
	{
		delay_xms(540);
		repeat--;
	}
	if(remain)delay_xms(remain);
} 

测试数据

50us级延时

在这里插入图片描述
延时5us,理论周期10us,实际周期10.8us,数据偏差8%
在这里插入图片描述
延时50us,理论周期100us,实际周期100.0us,数据偏差0%在这里插入图片描述

延时80us,理论周期160us,实际周期162.0us,数据偏差1.25%
结论:50us级延时偏差在0%-8%。

500us级延时

在这里插入图片描述
延时200us,理论周期400us,实际周期400.0us,数据偏差0%
在这里插入图片描述
延时500us,理论周期1000us,实际周期1000.0us,数据偏差0%
在这里插入图片描述
延时800us,理论周期1600us,实际周期1600.0us,数据偏差0%
结论:500us级延时偏差在0%是十分准确的,因为毫秒级延时以微秒延时为基础,因此毫秒级延时也是十分准确。

特点总结

优点:非中断,硬件计时,非常准确。
缺点:占用硬件资源,不可嵌套(主函数使用毫秒延时,中断中使用微秒延时,如果在微秒延时时中断产生,进行微秒延时再次修改SYSTICK寄存器的值,因此会导致主函数毫秒延时不准确)。
准确性:精确延时。
OS:理可用于OS,OS时需要开启SYSTICK中断。

3. DWT延时(V1.0.2)

DWT外设用于系统调试及跟踪,DWT 中有剩余的计数器,它们典型地用于程序代码的“性能速写”(profiling)。通过编程它们,就可以让它们在计数器溢出时发出事件(以跟踪数据包的形式)。最典型地,就是使用 CYCCNT寄存器来测量执行某个任务所花的周期数,这也可以用作时间基准相关的目的(操作系统中统计 CPU使用率可以用到它)。
在这里插入图片描述
在这里插入图片描述

适用范围:m3、m4、m7实测可用(m0不可用)。
精度:1/内核频率(s),1/168MHz=5.95ns。
参考链接:一种Cortex-M内核中的精确延时方法(ns级别)

代码

//.h
#include "stdio.h"
#include "stm32f4xx_conf.h"
#include "sys.h"
#include "core_cm4.h"

#define DWT_CR *(__IO uint32_t *)0xE0001000         //DWT控制寄存器
#define DWT_CYCCNT *(__IO uint32_t *)0xE0001004     //时钟周期寄存器
#define DEM_CR *(__IO uint32_t *)0xE000EDFC         //内核调试控制寄存器,使能DWT外设,DEMCR的位24控制,写1使能

#define DWT_CYCCNT_VAL (*(__IO uint32_t *)0xE0001004)
#define DWT_CTRL (*(__IO uint32_t *)0xE0001000)
#define DEM_CR_TRCENA (uint32_t)(1 << 24)           //使能DWT外设
#define DWT_CR_CYCCNTENA (uint32_t)(1 << 0)         //启动CYCCNT计数器

#define SYSCLK 168000000                            //系统时钟频率
void DWT_Init(uint32_t sys_clk_c);
void delay_dwt_us(uint32_t nus);
#define delay_dwt_ms(nms) delay_dwt_us(nms*1000)
//.c
static uint32_t sysclk = 0;
void DWT_Init(uint32_t sys_clk_c)
{
    //使能SWT外设
    DEM_CR |= DEM_CR_TRCENA;
    //CYCCNT寄存器清0
    DWT_CYCCNT = (uint32_t)0;
    //使能CYCCNT
    DWT_CR |= DWT_CR_CYCCNTENA;
    sysclk = sys_clk_c;
}

void delay_dwt_us(uint32_t nus)
{
    uint32_t ticks_start = 0, ticks_end = 0, tcnt = 0;
		DWT_CYCCNT = (uint32_t)0;
    ticks_start = DWT_CYCCNT_VAL;
    if (!sysclk)
    {
        DWT_Init(SYSCLK);
    }
    tcnt = (nus * (sysclk / 1000000));
    ticks_end = ticks_start + tcnt;
    if (ticks_end > ticks_start)
    {
        while (DWT_CYCCNT_VAL < ticks_end)
        {
        };
    }
    else
    {
        while (DWT_CYCCNT_VAL >= ticks_end)

        {
        };
        while (DWT_CYCCNT_VAL < ticks_end)
        {
        };
    }
}

测试数据

50us级延时

在这里插入图片描述
延时5us,理论周期10us,实际周期10.6us,数据偏差6%
在这里插入图片描述
延时50us,理论周期100us,实际周期101.0us,数据偏差1%在这里插入图片描述
延时80us,理论周期160us,实际周期160.0us,数据偏差0%
结论:50us级延时偏差在0%-6%。

500us级延时

在这里插入图片描述
延时200us,理论周期400us,实际周期400.0us,数据偏差0%

在这里插入图片描述
延时500us,理论周期1000us,实际周期1000.0us,数据偏差0%
在这里插入图片描述
延时800us,理论周期1600us,实际周期1600.0us,数据偏差0%
结论:500us级延时偏差在0%是十分准确的,因为毫秒级延时以微秒延时为基础,因此毫秒级延时也是十分准确。
注:50s级误差是基本一致的,无论是SYSTICK定时器还是DWT计时,误差都是一定的,在>100us时是没有误差的,推测<100us计时可能是示波器采样的因素。不过这个100us下不到10%的误差可以忽略不计,并且100us下,计时时间越接近于100us,误差越小,80us时误差就为0了,第一组测试数据5us延时有较大误差,我认为情有可原,是可以接收的。总的来说,硬件计时是十分准确的计时方式

特点总结

优点:非中断,硬件计时,非常准确,延时时间长(168MHZ下可达25s),可嵌套(最终延时时间大于等于需要延时时间)可用于任何中断
缺点:占用硬件资源(但是这个资源是不用白不用的资源)
准确性:精确延时。
OS:可用于OS,不占用OS心跳时钟。

二、非阻塞延时

本文主要讨论无OS下非阻塞延时函数实现,OS下有系统调度,线程可以挂起执行效率较高,如果有需要定时轮训的话还是需要用到非阻塞延时功能,思想是一样的。虽然这章是研究无OS下的非阻塞延时函数的,但在研究过程发现,在无OS下实现非阻塞延时,跟做个OS差不多,无OS下实现非阻塞延时照样会用到任务的概念,任务切换,任务状态等,跟OS下是一样的,但是OS下也会用到非阻塞延时。无OS下实现非阻塞延时实际上跟OS下的硬实时概念挺像的,不过应该称为硬延时,比如从系统运行初始化完成后开始,每50ms点亮一次LED灯,每100ms上传一次数据灯。本章就无OS下非阻塞延时就行研究讨论,后续可能会更新OS下的非阻塞延时。
非阻塞延时思想:使用定时器周期性中断产生定时时基,如10ms产生一次中断,然后在定时器中断函数中记录时间,while循环中根据时间执行相关任务,在任务A中判断是否到达执行任务时间,如果到达则执行,没有则调出判断任务B。实际上是将时间作为状态机状态来工作,何为有限状态机还请百度学习,不在本文讨论范围。

1. 无OS下非阻塞延时(待更新

2. OS下非阻塞延时(暂无更新

结语:由于水平有限,文中难免有错误之处,欢迎指正,也欢迎探讨交流。

联系方式:wangzhaoyangly@Foxmail.com
第一次编辑日期:2021年3月29日
最新更新日期:2021年3月29日