利用 ARM Cortex-M DWT 计数器实现微秒级精确延时,且不阻塞中断
引言
在嵌入式系统开发中,精确的微秒级延时(例如驱动传感器、通信协议时序)往往依赖硬件定时器。但传统方案(如 SysTick 查询或 for 循环)会占用 CPU 并阻塞中断,导致系统实时性下降。ARM Cortex-M 内核内置的 DWT(Data Watchpoint and Trace)单元提供了一个免费的 32 位周期计数器 CYCCNT,可精确测量 CPU 周期,且配置简单。本文将展示如何基于 DWT 实现非阻塞的微秒延时,让中断及时响应。
DWT 原理简介
DWT 是 Cortex-M3/M4/M7(以及部分 M0+)内核中的调试组件,主要用于性能分析和跟踪。其中 CYCCNT 是一个 32 位向上计数器,每个 CPU 时钟周期递增一次,溢出后自动回绕(wrap-around)。通过读取该计数器,我们可以获得精确的周期数,进而换算成时间。
关键寄存器:
-
DWT_CTRL(地址 0xE0001000):控制 CYCCNT 使能。 -
DWT_CYCCNT(地址 0xE0001004):当前计数值。 -
CoreDebug_DEMCR(地址 0xE000EDFC):需要使能 TRCENA 位(bit 24)以允许访问 DWT。
非阻塞延时设计思路
传统延时(如 delay_us)会循环等待计数器达到目标值,期间 CPU 被占用,中断无法及时处理。非阻塞延时的核心思想:记录起始时间,然后立即返回,让 CPU 继续执行其他任务;在需要等待的代码处,通过检查时间差来决定是否继续。
具体实现方式:
- 提供
dwt_delay_us_start()记录起始周期。 - 提供
dwt_delay_us_elapsed()检查是否已过指定微秒数。 - 在业务代码中,利用状态机或轮询方式调用这些函数,避免阻塞。
配置步骤
1. 使能 DWT 和 CYCCNT
在系统初始化时,执行以下代码(以 STM32 为例,但适用于所有 Cortex-M3/M4):
#include "core_cm4.h" // 或 core_cm3.h
void DWT_Init(void) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能 DWT 访问
DWT->CYCCNT = 0; // 清零计数器
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 使能 CYCCNT
}
注意:如果系统时钟频率为 SystemCoreClock,则每个周期时间为 1/SystemCoreClock 秒。
2. 获取当前周期数
static inline uint32_t DWT_GetCycles(void) {
return DWT->CYCCNT;
}
3. 计算微秒延时所需的周期数
#define CYCLES_PER_US (SystemCoreClock / 1000000)
4. 非阻塞延时 API
// 记录起始时间点
static inline uint32_t DWT_DelayUs_Start(void) {
return DWT_GetCycles();
}
// 检查是否已过指定微秒数
static inline uint32_t DWT_DelayUs_Elapsed(uint32_t start, uint32_t us) {
uint32_t cycles = (DWT_GetCycles() - start); // 处理回绕
return cycles >= (us * CYCLES_PER_US);
}
注意:DWT_GetCycles() - start 利用无符号整数减法自动处理回绕,只要延时不超过 2^32 周期(在 72MHz 下约 59.6 秒),结果就是正确的。
完整代码示例
以下是一个完整的非阻塞延时模块,包含初始化、延时启动和检查函数,以及一个使用示例(模拟非阻塞等待)。
// dwt_delay.h
#ifndef DWT_DELAY_H
#define DWT_DELAY_H
#include "stm32f4xx.h" // 根据芯片调整
void DWT_Init(void);
uint32_t DWT_DelayUs_Start(void);
uint32_t DWT_DelayUs_Elapsed(uint32_t start, uint32_t us);
#endif
// dwt_delay.c
#include "dwt_delay.h"
#define CYCLES_PER_US (SystemCoreClock / 1000000)
void DWT_Init(void) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
uint32_t DWT_DelayUs_Start(void) {
return DWT->CYCCNT;
}
uint32_t DWT_DelayUs_Elapsed(uint32_t start, uint32_t us) {
uint32_t cycles = (DWT->CYCCNT - start);
return cycles >= (us * CYCLES_PER_US);
}
使用示例:
// main.c
#include "dwt_delay.h"
void Task_NonBlocking(void) {
static uint32_t start = 0;
static uint8_t state = 0;
switch (state) {
case 0:
start = DWT_DelayUs_Start();
state = 1;
break;
case 1:
if (DWT_DelayUs_Elapsed(start, 100)) { // 等待 100us
// 执行需要精确延时的操作
GPIO_ToggleBits(GPIOA, GPIO_Pin_0);
state = 0;
}
break;
}
}
int main(void) {
SystemInit();
DWT_Init();
while (1) {
Task_NonBlocking();
// 其他任务可以继续执行,中断也能及时响应
}
}
注意事项
-
时钟频率:
SystemCoreClock必须在运行时正确设置,否则延时不准。建议在系统初始化后调用SystemCoreClockUpdate()。 - 回绕处理:利用无符号减法自动处理回绕,但延时时间不能超过 2^32 周期(例如 72MHz 下约 59.6 秒)。对于微秒级延时完全够用。
- 中断优先级:虽然非阻塞延时不会阻塞中断,但如果在中断服务函数中调用这些函数,需注意中断嵌套可能导致时间计算偏差。
- DWT 可用性:部分低端 Cortex-M0 可能没有 DWT(如 M0 非 M0+),需查阅芯片手册。若不可用,可退化为 SysTick 或定时器。
-
编译器优化:确保
DWT_GetCycles()等函数内联,避免函数调用开销影响精度。可使用__attribute__((always_inline))或static inline。 - 多任务环境:在 RTOS 中,任务切换可能导致时间计算不连续,建议在任务内使用,或使用 RTOS 提供的延时函数。
总结
利用 DWT 的 CYCCNT 计数器,我们可以轻松实现微秒级精确延时,并通过非阻塞设计避免中断延迟。这种方法代码简洁、无需占用额外定时器,非常适合对实时性要求高的嵌入式系统。掌握这一技巧,能显著提升你的嵌入式开发水平。