STM32开发项目:微秒级的精准延时 - 使用system tick/DWT寄存器/NOP命令

日期作者版本说明
2020.10.29TaoV0.0完成源码的撰写
2020.11.09TaoV0.1完成主体内容的撰写
2020.11.20TaoV0.2增加了F103与F407平台代码差异性描述
2021.09.01TaoV1.0修复了使用DWT内核的延时函数可能导致RTOS中调用延时的线程卡死的bug

背景

延时函数在STM32单片机开发的项目中有广泛的应用,微秒级延时在一些时间要求严格的场景下(例如软件模拟I2C通讯)是必不可少的。由于FreeRTOS默认采用了system tick作为时间片分配的时基定时器,可能与利用system tick设计的延时函数出现冲突。再加上FreeRTOS提供的延时函数void vTaskDelay( const TickType_t xTicksToDelay )最小的延时时间等于FreeRTOS的tick时间(一般设置为1ms),因此需要重新设计一套不基于system tick的微秒级延时函数。利用CM3/4内核中的数据观察点与跟踪(DWT)寄存器,可以在不占用硬件外设定时器的情况下实现微秒级的精准延时。

源码

通过选择#define USE_SYS_TICK, #define USE_DWT, #define USE_NOP_DELAY其中一个宏定义来决定delay函数的实现方式。需要特别注意的是,由于函数调用是需要时间的,因此使用延时函数时,一般会额外多出一些时间(1微秒左右)。

  • #define USE_SYS_TICK是利用system tick设计的延时函数,在没有使用实时操作系统的时候,它是一个比较适合实现微秒级延时的通用方法。
  • #define USE_DWT是利用CM3/4内核DWT寄存器设计的延时函数,它具有不占用system tick与外设定时器,延时精度高的特点。
  • #define USE_NOP_DELAY是利用了空指令设计的延时函数,它主要实现微秒以下的超短时间延时。

头文件

#ifndef __DELAY_H__
#define __DELAY_H__ 			   

#include "stm32f10x_conf.h"
#include "stm32f10x.h"

//#define USE_SYS_TICK
#define USE_DWT
//#define USE_NOP_DELAY

void delay_init(void);
void delay_ms(uint16_t ms);
void delay_us(u32 us);

/**
 * 定义ns级延时,72M主频下,每一条空指令大约14ns
 */
//#define DELAY_6ns()					__NOP()
//#define DELAY_12ns()				DELAY_6ns(); __NOP()
//#define DELAY_18ns()				DELAY_12ns(); __NOP()
//#define DELAY_24ns()				DELAY_18ns(); __NOP()
//#define DELAY_30ns()				DELAY_24ns(); __NOP()
//#define DELAY_60ns()				DELAY_30ns(); DELAY_30ns()
//#define DELAY_90ns()				DELAY_60ns(); DELAY_30ns()


#endif

源文件

#include "delay.h"

#ifdef USE_SYS_TICK

static uint8_t  fac_us = 0;                          //us延时倍乘数
static uint16_t fac_ms = 0;                          //ms延时倍乘数,在ucos下,代表每个节拍的ms数

//初始化延迟函数
//当使用OS的时候,此函数会初始化OS的时钟节拍
//SYSTICK的时钟固定为HCLK时钟的1/8
//SYSCLK:系统时钟
void delay_init()
{
	SysTick_CLKSourceConfig ( SysTick_CLKSource_HCLK_Div8 ); //选择外部时钟  HCLK/8
	fac_us = SystemCoreClock / 8000000;         //为系统时钟的1/8
	fac_ms = ( uint16_t ) fac_us * 1000;             //非OS下,代表每个ms需要的systick时钟数
}

//延时nus
//nus为要延时的us数.
void delay_us ( u32 us )
{
	u32 temp;
	SysTick->LOAD = us * fac_us;               //时间加载
	SysTick->VAL = 0x00;                        //清空计数器
	SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk ;  //开始倒数

	do
	{
		temp = SysTick->CTRL;
	}
	while ( ( temp & 0x01 ) && ! ( temp & ( 1 << 16 ) ) ); //等待时间到达

	SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;  //关闭计数器
	SysTick->VAL = 0X00;                         //清空计数器
}
//延时nms
//注意nms的范围
//SysTick->LOAD为24位寄存器,所以,最大延时为:
//nms<=0xffffff*8*1000/SYSCLK
//SYSCLK单位为Hz,nms单位为ms
//对72M条件下,nms<=1864
void delay_ms ( uint16_t ms )
{
	u32 temp;
	SysTick->LOAD = ( u32 ) ms * fac_ms;       //时间加载(SysTick->LOAD为24bit)
	SysTick->VAL = 0x00;                        //清空计数器
	SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk ;  //开始倒数

	do
	{
		temp = SysTick->CTRL;
	}
	while ( ( temp & 0x01 ) && ! ( temp & ( 1 << 16 ) ) ); //等待时间到达

	SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;  //关闭计数器
	SysTick->VAL = 0X00;                        //清空计数器
}

#elif defined USE_DWT

// 0xE000EDFC DEMCR RW Debug Exception and Monitor Control Register.
#define DEMCR           ( *(__IO uint32_t *)0xE000EDFC )
#define TRCENA          ( 0x01 << 24) // DEMCR的DWT使能位

#define  DBGMCU_CR   *(__IO uint32_t *)0xE0042004			//MCU调试模块控制寄存器,详细内容参考《stm32中文参考手册》调试支持(DBG)章节,747页

// 0xE0001000 DWT_CTRL RW The Debug Watchpoint and Trace (DWT) unit
#define DWT_CTRL        ( *(__IO uint32_t *)0xE0001000 )
#define DWT_CTRL_CYCCNTENA       ( 0x01 << 0 ) // DWT的SYCCNT使能位

// 0xE0001004 DWT_CYCCNT RW Cycle Count register,
#define DWT_CYCCNT      ( *(__IO uint32_t *)0xE0001004) // 显示或设置处理器的周期计数值

//#define DWT_DELAY_mS(mSec)    DWT_DELAY_uS(mSec*1000)

void delay_init()
{
	//使能DWT外设
	DEMCR |= (uint32_t)TRCENA;

	//DWT CYCCNT寄存器计数清0
	DWT_CYCCNT = (uint32_t)0u;

	//使能Cortex-M3 DWT CYCCNT寄存器
	DWT_CTRL |= (uint32_t)DWT_CTRL_CYCCNTENA;
}


// 微秒延时
void delay_us(uint16_t uSec)
{
	if(uSec > 10000) uSec = 10000;

    uint32_t ticks_start, ticks_end, ticks_delay;

    ticks_start = DWT_CYCCNT;
    ticks_delay = ( uSec * ( SystemCoreClock / (1000000) ) ); // 将微秒数换算成滴答数
    ticks_end = ticks_start + ticks_delay;

    // ticks_end没有溢出
    if ( ticks_end >= ticks_start )
    {
        // DWT_CYCCNT在上述计算的这段时间中没有溢出
        if(DWT_CYCCNT > ticks_start)
        {
            while( DWT_CYCCNT < ticks_end );
        }
        // DWT_CYCCNT溢出
        else
        {
            // 已经超时,直接退出
            return;
        }
    }
    else // ticks_end溢出
    {
        // DWT_CYCCNT在上述计算的这段时间中没有溢出
        if(DWT_CYCCNT > ticks_start)
        {
            // 等待DWT_CYCCNT的值溢出
            while( DWT_CYCCNT > ticks_end );
        }
        // 等待溢出后的DWT_CYCCNT到达ticks_end
        while( DWT_CYCCNT < ticks_end );
    }
}

void delay_ms(uint16_t ms)
{
		for(uint16_t i = 0; i < ms; i++)
    {
        // delay 1 ms
        delay_us(1000);
    }
}

#elif defined USE_NOP_DELAY

void delay_init(void)
{

}

void delay_ms(uint16_t ms)
{

}

void delay_us(u32 us)
{

}

#else

#endif

特别说明

使用DWT内核的延时函数中,涉及了两次数据溢出处理,第一次数据溢出处理是指计算结束时间(ticks_end = ticks_start + ticks_delay)发生溢出,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i1RK6NwT-1630479807917)(STM32F103%20F407%E7%9A%84%E5%BE%AE%E7%A7%92%E7%BA%A7%E7%B2%BE%E5%87%86%E5%BB%B6%E6%97%B6%20cfc00e9322314c539f467277c5b8c985/Untitled.png)]

这里有个限制条件就是延时长度(ticks_delay)不能超过uint32_t的最大值,但考虑到这是一个极大的数值,几乎不可能发生,因此可以简化处理限制微秒延时函数的输入参数小于10k,以提高延时精度。对于这个溢出判断就比较明确了:

if ( ticks_end >= ticks_start )
{
	// 计算未溢出
}
else 
{
	// 计算溢出
}

第二个溢出相对比较复杂,处理不当会导致严重问题,尤其是使用了RTOS的系统中有很可能遇到。其核心原因是从标记初始化时间戳ticks_start = DWT_CYCCNT到准备比较时间戳例如while( DWT_CYCCNT < ticks_end );的这段时间的耗时是不确定的。即使没有使用RTOS,如下代码(使用绿色底色标注)在执行时也可能被硬中断暂停,从而导致DWT_CYCCNT时间戳可能已经越过了ticks_end ,或者也发生了溢出重置。

ticks_start = DWT_CYCCNT;
ticks_delay = ( uSec * ( SystemCoreClock / (1000000) ) ); // 将微秒数换算成滴答数
ticks_end = ticks_start + ticks_delay;

if ( ticks_end >= ticks_start )
{
	// DWT_CYCCNT在上述计算的这段时间中没有溢出
	if(DWT_CYCCNT > ticks_start)
	{

	}
	else
	{
	
	}
}
else 
{
	// DWT_CYCCNT在上述计算的这段时间中没有溢出
	if(DWT_CYCCNT > ticks_start)
	{

	}
	else
	{
	
	}
}

因此需要根据延时等待操作时DWT_CYCCNT所处位置再进行一次判断:
在这里插入图片描述

  • ticks_end未溢出时(ticks_end1):
    • DWT_CYCCNT未溢出(DWT_CYCCNT2 & DWT_CYCCNT3)时:while( DWT_CYCCNT < ticks_end );
    • DWT_CYCCNT溢出(DWT_CYCCNT4)时:直接返回。
  • ticks_end溢出时(ticks_end2):
    • DWT_CYCCNT未溢出(DWT_CYCCNT2)时:while( DWT_CYCCNT > ticks_end ); while( DWT_CYCCNT < ticks_end );
    • DWT_CYCCNT溢出(DWT_CYCCNT4 & DWT_CYCCNT5)时:while( DWT_CYCCNT < ticks_end );在这里插入图片描述

使用指南

  • 在程序开始的地方初始化延时函数:delay_init();
  • 在需要延时的地方调用延时函数:delay_us(10);

需要特别说明的是,在FreeRTOS的任务中调用本文介绍的例如void delay_us(uint32_t us)等延时函数,任务会阻塞等待延时结束。如果是调用FreeRTOS提供的延时函数void vTaskDelay( const TickType_t xTicksToDelay )系统会执行低优先级任务直到延时结束

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全能骑士涛锅锅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值