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 优先级通常设为最低,以保证中断服务程序不被其他中断抢占过久。若节拍中断被高优先级中断长时间阻塞,任务延时和超时判断都会出现累积误差。
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 翻转配合示波器观察;周期抖动则通过连续记录定时器中断的时间戳差值来评估。
选型与配合原则
三类时间资源并非互相替代,而是分层配合。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;
}
将该结构配合串口导出,即可还原中断与任务的真实执行顺序,定位抖动来源和优先级反转问题。时间基准的建立不是一次性配置,而是需要在系统运行中持续验证和校准的过程。