目录
一、什么是DWT
在Cortex-M3及以上的内核中有一个外设叫DWT(Data Watchpoint and Trace数据监视点和跟踪单元),是用于系统调试及跟踪。DWT有一个32位的寄存器叫CYCCNT, 它是一个向上的计数器,记录的是内核时钟运行的次数,内核时钟跳动一次,该计数器就加1,精度非常高, 例如GD32F303ZET6内核时钟最大可达120MHz,那精度就是1/120M = 8.3ns。
解释:120MHz表示内核时钟每秒跳动120*10^6次,那么跳动一次所用的时间就是1S/120MHz = 1*10^9ns / 120*10^6 = 8.3ns,知道了跳动次数就相当于知道了时间,这就是DWT延时的思想,而且单片机程序运行时间通常都是微秒级别的,所以DWT实现延时的精度非常高。
如果内核时钟为120MHz,则直接使用DWT的CYCCNT寄存器(32位)延时的最大时间就是:
2^32 * (1S/120Mhz) = 36S。最大可以计2^32次内核时钟跳动次数,每次跳动耗时1/120MHz。
二、为什么要使用DWT实现延时
2.1 延时方法
我了解到的延时方法有软件延时和systick滴答定时器延时
①软件延时
eg.static void Delay(uint32_t count)
{
while (count--);
}
②systick滴答定时器延时
这是目前大多数人采用的延时方案。具体实现可搜索文章查看。
2.2上述两种方案的缺点
①软件延时的缺点
延时精度差,受系统主频影响,调校困难。
eg.软件延时本质就是指令的执行时间。假如系统主频120MHz,则t = 1S/120Mhz.假如系统主频72MHz,则t = 1S/72Mhz.在不同的时钟下,一条代码的执行时间是不同的。
②systick滴答定时器的缺点
在非阻塞性程序当中,是不会出现大于几十ms的死等延时。通常延时时间就是us级别,如果使用定时个人感觉有点浪费。
三、如何实现DWT延时
要实现DWT延时的功能,总共涉及三个内核寄存器:DEMCR, DWT的CTRL寄存器,DWT的CYCCNT寄存器。我会逐一说明为什么涉及这三个寄存器。百度网盘链接如下:https://pan.baidu.com/s/13xTim4urhfEHttxkBFLInQ?pwd=GD32
链接: https://pan.baidu.com/s/13xTim4urhfEHttxkBFLInQ?pwd=GD32 提取码: GD32
1,ARM Cortex-M3与Cortex-M4权威指南,之后简称权威手册。
2,Arm Cortex-M4 Processor Technical Reference Manual Revision r0p1,简称技术手册。
3,Arm®v7M Architecture Reference Manual,简称架构手册。
3.1 DEMCR寄存器
配置DEMCR寄存器用于开启DWT功能。在权威手册321页的表14.5中写了:
3.2 DWT_CTRL寄存器与DWT_CYCCNT寄存器
配置DWT_CTRL寄存器用于开启DWT_CYCCNT计数寄存器以及获取系统时钟计数值。在技术手册的第85页给出了DWT各种寄存器的基地址。
在架构手册的第741页有对时钟周期计数寄存器DWT_CYCCNT的详细说明。
我翻译一下红色方框中的内容:CYCCNT是一个32位,向上计数的寄存器。当DWT_CTRL寄存器的第0位CYCNTEENA为1并且DEMCR的第24位TRCENA为1时,CYCCNT寄存器被使能,每来一个内核时钟向上计数一次,溢出则被清零。
下面是架构手册中的一些截图:
四、 代码实现
下面给出基于Keil5,GD32F303系列单片机的代码实现。
#include "gd32f30x.h" // Device header
#include <stdint.h>
/**
***********************************************************
* @brief DWT延时驱动初始化
* @param
* @return
***********************************************************
*/
void DelayInit(void)
{
/*TRCENA位清零*/
CoreDebug->DEMCR &= ~CoreDebug_DEMCR_TRCENA_Msk;
/*TRCENA位置一*/
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
/*失能CYCCNT*/
DWT->CTRL &= ~DWT_CTRL_CYCCNTENA_Msk;
/*使能CYCCNT*/
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
/*计数器清零*/
DWT->CYCCNT = 0;
}
/**
***********************************************************
* @brief 微秒级延时函数
* @param nUs,延时时间(us)
* @return
***********************************************************
*/
void DelayNus(uint32_t nUs)
{
/*保存初始时钟跳动次数*/
uint32_t tickStart = DWT->CYCCNT;
/* 转换为nUs对应的时钟跳动次数*/
uint32_t nusToTick;
nusToTick = nUs * ( rcu_clock_freq_get(CK_AHB)/1000000 );
/*while延时*/
while ( (DWT->CYCCNT - tickStart) < nusToTick);
}
/**
***********************************************************
* @brief 毫秒级延时函数
* @param nMs,延时时间(ms)
* @return
***********************************************************
*/
void DelayNms(uint32_t nMs)
{
for (uint32_t i = 0; i < nMs; i++)
{
DelayNus(1000);
}
}
有的朋友可能会好奇DelayNus函数中的这条语句:while ( (DWT->CYCCNT - tickStart) < nusToTick);
会觉得这条语句存在这样一个bug:我们先假设CYCCNT是一个8位的寄存器,并且nUs的类型改为uint_8,这样方便说明。因为2^8 - 1 = 255,数字很直观。
那么问题是什么呢?假如tickStart保存了254次跳动次数,nusToTick由时间转换得到的跳动次数是247,DWT_CYCCNT每跳一次都会加1,上限255。
255 - 254 = 1 < 247,while(1)执行;
255++溢出为0, 0 - 254 = ?
实际上不会出错的,朋友们可以自己算一下,无符号uint8_t类型,
0000 0000
-- 1111 1110
= 0000 0010 == 2
五、验证延时函数
用示波器可以验证延时是否准确。具体操作大家可以百度。我这就不多写了。我主要在这说一个我犯得很傻的错误。
int main(void)
{
DrvInit();
AppInit();
while(1)
{
LedOn(LED0);
DelayNms(1000);
LedOff(LED0);
// DelayNms(1000);
}
}
我最开始是在主循环中用这个代码去看闪烁现象的。结果出现的情况是LED灯一直亮它不灭,我把代码改了一下。
int main(void)
{
DrvInit();
AppInit();
while(1)
{
LedOff(LED0);
DelayNms(1000);
LedOn(LED0);
// DelayNms(1000);
}
}
结果LED灯一直灭它不亮。 我以为是DWT配错了,它卡在delay里面的while循环出不来了。我在最后一个语句那打断点,结果函数可以执行到断点出,我就纳闷了。我又去看LED的函数是否右侧,单独测试发现每问题,可以正常亮灭。
后来我终于发现,是逻辑错误。以上图代码叙述:led灭---Delay1000---亮-灭---1000---亮-灭-----。。。。。
灭执行,延时1S,很快立刻亮再立刻灭, 这个立刻亮立刻灭太快了,灭又会持续1S,所以根本看不到亮的现象。最后加一个Delay就好了。
int main(void)
{
DrvInit();
AppInit();
while(1)
{
LedOff(LED0);
DelayNms(1000);
LedOn(LED0);
DelayNms(1000);
}
}
以后有空更新STM32的DWT延时函数。
标签:DWT,32,单片机,CYCCNT,内核,延时,寄存器,时钟 From: https://blog.csdn.net/m0_73101636/article/details/143028118