3. 系统节拍与高精度时间基准

在实时控制系统中,“时间”不是辅助信息,而是与电流、位置、速度同等重要的控制量。无论是 10 kHz 的电流环、1 kHz 的速度环,还是通信超时判断、任务执行时间统计,都依赖一个稳定、可读、分辨率足够高的时间基准。STM32 提供了从毫秒级到纳秒级的多层时间资源:SysTick 负责系统节拍与任务调度,通用/高级定时器提供灵活时基与 PWM 同步,DWT 周期计数器则给出 CPU 周期级的时间戳。理解它们的定位与配合方式,是构建实时系统的第一步。

SysTick:系统节拍与调度心跳

SysTick 是 Cortex-M 内核自带的 24 位递减计数器,位于内核私有外设空间,不占用任何通用定时器资源。它的典型用法是产生固定周期的中断,作为 RTOS 或裸机调度器的时间片基准。配置时需明确三个参数:时钟源、重装载值、中断优先级。

时钟源可选内核时钟(HCLK)或 HCLK/8。以 STM32F407 主频 168 MHz 为例,若选择 HCLK,则计数频率为 168 MHz。要得到 1 ms 节拍,重装载值应为 168000−1=167999。代码如下:

// SysTick 配置为 1ms 节拍,HCLK = 168MHz
SysTick->LOAD = 168000 - 1;      // 24位重装载值
SysTick->VAL  = 0;               // 清空当前计数
SysTick->CTRL = (1 << 2) |      // 选择 HCLK 作为时钟源
                (1 << 1) |      // 使能中断
                (1 << 0);       // 启动计数

需要特别注意的是,SysTick 中断优先级由内核的 SHPR3 寄存器配置,而不是 NVIC_IPR。在 FreeRTOS 中,SysTick 优先级通常设为最低,以保证中断服务程序不被其他中断抢占过久。若节拍中断被高优先级中断长时间阻塞,任务延时和超时判断都会出现累积误差。

SysTick 的 24 位计数器在 168 MHz 下最长定时约 99.8 ms。若需要更长周期,应在中断中维护软件计数变量,而不是继续增大重装载值。

DWT 周期计数器:CPU 级时间戳

当需要测量微秒甚至纳秒级的时间间隔时,SysTick 的 1 ms 分辨率远远不够。Cortex-M3/M4/M7 内核中的 DWT(Data Watchpoint and Trace)模块包含一个 32 位周期计数器 CYCCNT,它以 CPU 主频递增,每个时钟周期加一。在 168 MHz 下,分辨率约为 5.95 ns,32 位计数约 25.6 s 溢出一次。

启用 DWT 需要先使能跟踪与调试模块,再开启 CYCCNT:

#define DWT_CYCCNT  (*(volatile uint32_t *)0xE0001004)
#define DWT_CTRL    (*(volatile uint32_t *)0xE0001000)
#define DEMCR       (*(volatile uint32_t *)0xE000EDFC)

void DWT_Init(void) {
    DEMCR |= (1 << 24);        // 使能 TRCENA
    DWT_CYCCNT = 0;            // 清零计数器
    DWT_CTRL |= (1 << 0);      // 使能 CYCCNT
}

static inline uint32_t DWT_GetCycles(void) {
    return DWT_CYCCNT;
}

基于 CYCCNT 可以封装微秒级延时和计时函数。由于计数器只增不减,计算差值时使用无符号减法即可自然处理溢出:

void Delay_us(uint32_t us) {
    uint32_t start = DWT_CYCCNT;
    uint32_t ticks = us * (SystemCoreClock / 1000000U);
    while ((DWT_CYCCNT - start) < ticks) { }
}

这种忙等延时精度高,但会占用 CPU。它适合用于外设时序等待、短脉冲控制等场景,不适合长时间延时。若在中断中使用,应确保延时时间远小于中断周期。

定时器时基:灵活的事件触发源

通用定时器 TIM2~TIM5 和高级定时器 TIM1/TIM8 提供比 SysTick 更灵活的时基能力。它们支持预分频器 PSC 和自动重装载寄存器 ARR,可产生更新事件、比较事件、PWM 输出和输入捕获。以 TIM2 产生 100 μs 更新中断为例,若 APB1 定时器时钟为 84 MHz:

// TIM2: 84MHz, PSC=83 -> 1MHz, ARR=99 -> 100us
TIM2->PSC = 83;
TIM2->ARR = 99;
TIM2->EGR = 1;                  // 产生更新事件,立即加载 PSC
TIM2->SR  = 0;                  // 清除更新标志
TIM2->DIER |= (1 << 0);         // 使能更新中断
TIM2->CR1  |= (1 << 0);         // 启动定时器

定时器时基的优势在于:可与 PWM 输出、ADC 触发、DMA 请求联动,形成硬件同步的控制链路。例如用 TIM1 的更新事件触发 ADC 采样,再用 ADC 转换完成中断执行电流环计算,可以显著降低软件调度抖动。

时间戳记录与实时性能测量

在调试实时性能时,最直接的方法是在关键代码段前后读取 DWT 周期计数,计算执行周期数并换算为时间。例如测量一次 PID 计算耗时:

uint32_t t0 = DWT_CYCCNT;
PID_Calculate(&pid, setpoint, feedback);
uint32_t cycles = DWT_CYCCNT - t0;
float us = (float)cycles / (SystemCoreClock / 1000000.0f);

若需要记录多个事件的时间戳,可以定义一个环形缓冲区,在中断或任务中写入 DWT 计数值和事件标识,事后通过串口或调试器导出分析。判断实时性能是否达标,主要看三个指标:最坏执行时间(WCET)、中断响应延迟、任务周期抖动。WCET 可通过多次测量取最大值获得;中断延迟可用 GPIO 翻转配合示波器观察;周期抖动则通过连续记录定时器中断的时间戳差值来评估。

DWT 计数器在调试器暂停时可能停止计数,且部分低功耗模式下会关闭。若系统进入 Sleep/Stop 模式,DWT 时间戳将不再连续,需要改用 RTC 或独立定时器作为长时间基准。
STM32 多层时间基准与实时测量链路 SysTick 24位递减 / 1ms节拍 任务调度心跳 DWT CYCCNT 32位周期计数 / 5.95ns 微秒延时 / 时间戳 TIM2~TIM5 PSC+ARR / 100μs PWM / ADC触发 TIM1 / TIM8 高级定时器 / 互补PWM 硬件同步触发 任务调度 延时 / 超时 / 时间片 性能测量 WCET / 中断延迟 / 抖动 控制环路 电流环 / 速度环 / 位置环 外设同步 ADC采样 / DMA搬运 时间戳记录 环形缓冲区 + 事件标识 DWT计数值 / 周期差 实时性能判据 WCET ≤ 周期预算 抖动 < 允许偏差 SysTick 提供调度节拍 · DWT 提供周期级时间戳 · 定时器提供硬件同步时基

选型与配合原则

三类时间资源并非互相替代,而是分层配合。SysTick 适合作为全局节拍,驱动任务调度和软件定时器;DWT 适合短时高精度测量和微秒延时,不产生中断,开销极低;通用定时器适合需要硬件联动、PWM 输出或输入捕获的场景。实际工程中常见的组合是:SysTick 产生 1 ms 节拍,TIM1 产生 20 kHz PWM 并触发 ADC,DWT 用于测量电流环执行时间和记录关键事件时间戳。

需要强调的是,时间基准的精度不仅取决于计数器本身,还受中断优先级、代码执行路径和总线竞争影响。即使 DWT 分辨率达到纳秒级,如果中断被长时间屏蔽,测量到的时间戳仍然会失真。因此在设计阶段就应规划好中断优先级分组,把时间关键的中断放在较高优先级,同时避免在中断中进行浮点运算、动态内存分配或阻塞式等待。

时间资源 分辨率 典型用途 是否产生中断
SysTick 1 ms(可调) 任务调度、软件定时器 是
DWT CYCCNT 约 5.95 ns @168 MHz 微秒延时、WCET 测量、时间戳 否
TIM2~TIM5 约 11.9 ns @84 MHz 周期中断、PWM、输入捕获 可配置
TIM1/TIM8 约 11.9 ns @168 MHz 互补 PWM、ADC 触发、硬件同步 可配置

最后给出一个可复用的时间戳记录结构示例,用于在调试阶段采集事件序列:

typedef struct {
    uint32_t timestamp;   // DWT 周期计数
    uint8_t  event_id;    // 事件编号
} TimeStamp_t;

#define TS_BUF_SIZE 256
static TimeStamp_t ts_buf[TS_BUF_SIZE];
static volatile uint16_t ts_head = 0;

void TS_Record(uint8_t id) {
    ts_buf[ts_head].timestamp = DWT_CYCCNT;
    ts_buf[ts_head].event_id  = id;
    ts_head = (ts_head + 1) % TS_BUF_SIZE;
}

将该结构配合串口导出,即可还原中断与任务的真实执行顺序,定位抖动来源和优先级反转问题。时间基准的建立不是一次性配置,而是需要在系统运行中持续验证和校准的过程。