【STM32】通过 DWT 实现毫秒级延时
目录
- 零、前言
- 一、DWT
- 1、DEMCR
- 2、DWT_CTRL
- 3、DWT_CYCCNT
- 二、实现代码
- 三、测试
零、前言
在 FreeRTOS 中,SysTick
被用于作为调度器的一部分进行任务调度,那么如果我需要使用软件模拟通信,例如软件 I2C,需要使用 delay,就无法使用 SysTick 实现的 delay。
因此,这里提供一种基于 DWT 实现的 delay。
一、DWT
在实现我们的代码之前,如果你没有了解过 DWT,那就先来看一下:
这里只介绍待会儿会用到的延时相关的内容
在 Cortex-M 内核内核中里面有一个外设叫 DWT
(Data Watchpoint and Trace),是用于系统调试及跟踪。
它有一个 32 位的寄存器叫 CYCCNT
,它是一个向上的计数器,记录的是内核时钟运行的个数,内核时钟跳动一次,该计数器就加 1。
它的精度非常高,取决于内核的频率是多少,如果是 F103 系列,内核时钟是 72M,那精度就是 1 / 72 M = 14 n s 1/72M = 14ns 1/72M=14ns,而程序的运行时间都是微秒级别的,所以 14ns 的精度是远远够的。最长能记录的时间为: 60 s = 2 32 / 72000000 60s = 2^{32}/72000000 60s=232/72000000 (假设内核频率为72M,内核跳一次的时间大概为 1 / 72 M = 14 n s 1/72M=14ns 1/72M=14ns),而如果是 H7 这种 400M 主频的芯片,那它的计时精度高达 2.5ns( 1 / 400000000 = 2.5 1/400000000 = 2.5 1/400000000=2.5)。
当 CYCCNT
溢出之后,会清 0 重新开始向上计数。
要实现延时的功能,总共涉及到三个寄存器:DEMCR
、DWT_CTRL
、DWT_CYCCNT
,分别用于开启 DWT 功能、开启 CYCCNT 及获得系统时钟计数值。下面就来看一下这几个寄存器吧。
1、DEMCR
参照权威指南:
配置的时候,将 TRCENA
设置为 1 就行了。
2、DWT_CTRL
将 CYCCNTENA
使能位置 1 即可。
3、DWT_CYCCNT
使用 DWT_CYCCNT
寄存器之前,先清 0。
综上所述,要使用 DWT 的 CYCCNT
配置步骤如下:
- 使能 DWT 外设,这个由内核调试寄存器 DEMCR 的位 24
TRCENA
控制,写 1 使能 - 使能
CYCCNT
寄存器之前,先清 0。 - 使能
CYCCNT
寄存器,这个由 DWT 控制寄存器的CYCCNTENA
位控制,也就是 DWT 控制寄存器的位 0 控制,写 1 使能
二、实现代码
#define DWT_CYCCNT *(volatile unsigned int *)0xE0001004
#define DWT_CR *(volatile unsigned int *)0xE0001000
#define DEM_CR *(volatile unsigned int *)0xE000EDFC
#define DEM_CR_TRCENA (1 << 24)
#define DWT_CR_CYCCNTENA (1 << 0)
/******************************************************************************
* @brief 初始化 DWT
* @return none
******************************************************************************/
void bsp_dwt_init(void)
{
DEM_CR |= (unsigned int)DEM_CR_TRCENA; /* Enable Cortex-M4's DWT CYCCNT reg. */
DWT_CYCCNT = (unsigned int)0u;
DWT_CR |= (unsigned int)DWT_CR_CYCCNTENA;
}
/******************************************************************************
* @brief
* @param[in] _delay_time : 延时时间
* @return none
******************************************************************************/
void bsp_dwt_delay(uint32_t _delay_time)
{
uint32_t cnt, delay_cnt;
uint32_t start;
cnt = 0;
delay_cnt = _delay_time; /* 需要的节拍数 */
start = DWT_CYCCNT; /* 刚进入时的计数器值 */
while(cnt < delay_cnt)
{
cnt = DWT_CYCCNT - start; /* 求减过程中,如果发生第一次32位计数器重新计数,依然可以正确计算 */
}
}
/******************************************************************************
* @brief 这里的延时采用CPU的内部计数实现,32位计数器
* OSSchedLock(&err);
* bsp_DelayUS(5);
* OSSchedUnlock(&err); 根据实际情况看看是否需要加调度锁或选择关中断
* @param[in] _delay_time : 延迟长度,单位1 us
* @return none
* @note 1. 主频168MHz的情况下,32位计数器计满是2^32/168000000 = 25.565秒
* 建议使用本函数做延迟的话,延迟在1秒以下。
* 2. 实际通过逻辑分析仪测试,微妙延迟函数比实际设置实际多运行0.25us左右的时间。
* 3. 测试硬件:STM32F407VET6
******************************************************************************/
void bsp_delay_us(uint32_t _delay_time)
{
uint32_t cnt, delay_cnt;
uint32_t start;
start = DWT_CYCCNT; /* 刚进入时的计数器值 */
cnt = 0;
delay_cnt = _delay_time * (SystemCoreClock / 1000000); /* 需要的节拍数 */
while(cnt < delay_cnt)
{
cnt = DWT_CYCCNT - start; /* 求减过程中,如果发生第一次32位计数器重新计数,依然可以正确计算 */
}
}
/******************************************************************************
* @brief 为了让底层驱动在带RTOS和裸机情况下有更好的兼容性
* 专门制作一个阻塞式的延迟函数,在底层驱动中ms毫秒延迟主要用于初始化,并不会影响实时性。
* @param[in] _delay_time : 延迟长度,单位1 ms
* @return none
******************************************************************************/
void bsp_delay_ms(uint32_t _delay_time)
{
bsp_delay_us(1000 * _delay_time);
}
三、测试
下面通过一个简单的 demo 测试一下 us 级的延时函数,通过翻转 PC0 的电平状态,再通过逻辑分析仪查看延时效果:
int main(void)
{
/******
* 系统及外设初始化函数
*****/
bsp_dwt_init();
/* 使用PC0测试时间 */
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE);
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; /* 输出类型为推挽 */
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; /* 内部上拉电阻使能 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; /* 复用模式 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
}
while (1)
{
GPIOC->BSRR = (uint32_t)GPIO_Pin_0;
bsp_delay_us(1);
GPIOC->BSRR = (uint32_t)GPIO_Pin_0 << 16;
bsp_delay_us(1);
}
return 0;
}
bsp_delay_us(10)
:
由于本人使用的逻辑分析仪精度较低,只能看个大概数据,大致要比实际设置的时间多运行 0.25 us
bsp_delay_us(1)
:
注意事项
在烧录运行程序的时候,由于下载器的问题,早期用的 D 版 JLINK,不能正常复位 DWT。所以 DWT 时钟计数器容易出现不运行的情况,而调试状态或者重新上电都不存在问题,使用的时候要注意。