# 利用 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): ```c #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. 获取当前周期数 ```c static inline uint32_t DWT_GetCycles(void) { return DWT->CYCCNT; } ``` ### 3. 计算微秒延时所需的周期数 ```c #define CYCLES_PER_US (SystemCoreClock / 1000000) ``` ### 4. 非阻塞延时 API ```c // 记录起始时间点 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 秒),结果就是正确的。 ## 完整代码示例 以下是一个完整的非阻塞延时模块,包含初始化、延时启动和检查函数,以及一个使用示例(模拟非阻塞等待)。 ```c // 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 ``` ```c // 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); } ``` 使用示例: ```c // 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 计数器,我们可以轻松实现微秒级精确延时,并通过非阻塞设计避免中断延迟。这种方法代码简洁、无需占用额外定时器,非常适合对实时性要求高的嵌入式系统。掌握这一技巧,能显著提升你的嵌入式开发水平。