一、为什么需要非阻塞微秒延时?

在嵌入式开发中,HAL_Delay() 等阻塞延时会导致CPU空转,浪费算力且影响实时响应。非阻塞延时通过记录时间戳,在主循环中轮询判断,实现多任务并行。微秒级精度对传感器时序、通信协议(如单总线、I2C)至关重要。

Cortex-M0+内核精简,通常不包含DWT(Data Watchpoint and Trace)周期计数器,但许多厂商(如STM32G0、STM32L0)在调试组件中保留了DWT。通过使能调试跟踪,可访问DWT->CYCCNT,获得CPU周期计数,实现高精度计时。

二、DWT周期计数器原理

DWT是ARM CoreSight调试组件的一部分,其CYCCNT寄存器在每个处理器时钟周期递增。访问需满足:

  • 使能调试跟踪(CoreDebug->DEMCR的TRCENA位)
  • 使能DWT计数器(DWT->CTRL的CYCCNTENA位)
  • 复位并启动CYCCNT

在Cortex-M0+上,DWT寄存器基地址为0xE0001000,需确保芯片支持。部分低端型号可能裁剪DWT,使用前请查阅参考手册。

三、配置步骤

  1. 使能调试跟踪:CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
  2. 复位周期计数器:DWT->CYCCNT = 0;
  3. 使能CYCCNT:DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
  4. 读取当前周期数:uint32_t cyc = DWT->CYCCNT;

注意:CYCCNT为32位,在72MHz下约59.6秒溢出,需处理回绕。

四、微秒级非阻塞延时实现

假设系统时钟为 SystemCoreClock,则每微秒周期数为 SystemCoreClock / 1000000。定义延时结构体记录起始周期和延时周期数。

#include "stm32g0xx.h" // 根据实际芯片调整

typedef struct {
    uint32_t start_cyc;
    uint32_t delay_cyc;
} dwt_delay_t;

void dwt_init(void) {
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}

void dwt_delay_start(dwt_delay_t *d, uint32_t us) {
    d->start_cyc = DWT->CYCCNT;
    d->delay_cyc = us * (SystemCoreClock / 1000000U);
}

// 返回1表示延时未到,0表示已到
int dwt_delay_check(dwt_delay_t *d) {
    uint32_t elapsed = DWT->CYCCNT - d->start_cyc; // 自动处理回绕
    return (elapsed < d->delay_cyc) ? 1 : 0;
}

使用示例:

dwt_delay_t delay1;
dwt_init();
dwt_delay_start(&delay1, 100); // 延时100us
while (dwt_delay_check(&delay1)) {
    // 执行其他任务
}
// 延时结束

五、时钟偏差校准

DWT基于CPU时钟,若系统时钟源(如HSI)存在偏差,会导致延时误差。可利用SysTick(通常由HCLK驱动)或外部晶振进行校准。

校准思路:使用高精度参考(如GPS秒脉冲或已知频率信号)测量固定时间内的DWT周期数,计算实际每微秒周期数。

简化方法:用SysTick产生1ms中断,在中断中读取DWT差值,多次平均得到实际周期/毫秒,进而计算校准系数。

volatile uint32_t dwt_per_ms = 0;
volatile uint32_t last_cyc = 0;

void SysTick_Handler(void) {
    uint32_t now = DWT->CYCCNT;
    dwt_per_ms = now - last_cyc;
    last_cyc = now;
}

void calibrate_dwt(void) {
    // 配置SysTick为1ms中断
    SysTick_Config(SystemCoreClock / 1000U);
    // 等待多次测量取平均
    uint32_t sum = 0;
    for (int i = 0; i < 10; i++) {
        while (dwt_per_ms == 0);
        sum += dwt_per_ms;
        dwt_per_ms = 0;
    }
    uint32_t avg = sum / 10;
    // 实际每微秒周期数 = avg / 1000
    // 可更新全局变量用于延时计算
}

校准后,将 SystemCoreClock / 1000000U 替换为 avg / 1000 即可。

六、注意事项

  • 芯片支持性:并非所有Cortex-M0+都实现DWT,使用前确认参考手册。
  • 调试器影响:连接调试器时DWT正常工作,脱机运行需确保TRCENA已使能。
  • 溢出处理:使用无符号减法自动处理32位回绕,但延时不能超过溢出周期。
  • 中断干扰:DWT计数不受中断影响,但长时间延时中若发生频繁中断,可能影响精度(因中断服务程序占用CPU周期,但CYCCNT仍递增,实际延时不变)。
  • 低功耗模式:睡眠时CYCCNT可能停止,需根据应用调整。
  • 校准时机:建议在系统稳定后校准,避免时钟切换影响。

七、总结

利用DWT周期计数器,可在Cortex-M0+上实现高精度非阻塞微秒延时,结合SysTick校准可消除时钟偏差。该方法资源占用少、精度高,适用于时序敏感的嵌入式场景。代码已封装为可复用模块,读者可根据实际芯片调整寄存器定义。