裸机C语言实现毫秒级精准延时:不用系统滴答定时器也能稳定控时
裸机项目里SysTick常被RTOS占用,或某些小容量MCU根本不打算用内核滴答。毫秒级延时不一定非靠它:Cortex-M可用DWT周期计数器做纯硬件计时,通用MCU可用普通定时器轮询,51类可用Timer0/1做1ms时基。核心原则是“用硬件计数器读时间,不在循环里空转估时间”。
一、Cortex-M用DWT,不占SysTick
DWT是内核调试组件,CYCCNT是32位自由运行周期计数器,使能后每个CPU时钟加1,溢出回零 。M3/M4/M7大多可用,M0/M0+通常没有,需改定时器方案。
#define DEMCR (*(volatile uint32_t *)0xE000EDFC)
#define DWTCR (*(volatile uint32_t *)0xE0001000)
#define CYCCNT (*(volatile uint32_t *)0xE0001004)
#define TRCENA 0x01000000u
#define CNTENA 0x00000001u
static uint32_t cpu_hz = 72000000u;
void dwt_delay_init(uint32_t hz){
cpu_hz = hz;
DEMCR |= TRCENA;
DWTCR |= CNTENA;
CYCCNT = 0;
}
void dwt_delay_ms(uint32_t ms){
uint32_t total = (uint64_t)ms * cpu_hz / 1000u;
uint32_t start = CYCCNT;
while ((uint32_t)(CYCCNT - start) < total);
}
void dwt_delay_us(uint32_t us){
uint32_t total = (uint64_t)us * cpu_hz / 1000000u;
uint32_t start = CYCCNT;
while ((uint32_t)(CYCCNT - start) < total);
}
CYCCNT按处理器时钟运行,不受RTOS调度影响;但调试暂停时不计数,长时间延时要注意32位溢出。72MHz下1ms约72000个周期,1μs约72个周期,精度远高于软件for循环 。
二、普通定时器轮询,适合无DWT芯片
没有DWT时,用任一通用TIM,配1ms计数、不进中断,主循环读CNT判断:
// 假设TIM2时钟72MHz,分频7199得10kHz,自动重装999得1ms
void tim_delay_init(void){
RCC->APB1ENR |= (1u<<0); // 使能TIM2
TIM2->PSC = 7199;
TIM2->ARR = 999;
TIM2->CNT = 0;
TIM2->CR1 = (1u<<0); // 启动,向上计数
}
void tim_delay_ms(uint32_t ms){
for(uint32_t i=0;i<ms;i++){
TIM2->CNT = 0;
while((TIM2->SR & (1u<<0))==0); // 等更新标志
TIM2->SR &= ~(1u<<0);
}
}
轮询方式不依赖SysTick,也不被定时器中断频率绑死;若系统已有其他定时器,可只借CNT做时间差,避免反复重装。
三、51/小核用Timer做1ms时基
12MHz机器周期1μs,Timer0模式1定时1ms初值65536-1000=64536=0xFC18:
static volatile uint32_t g_ms;
void timer0_init(void){
TMOD &= 0xF0; TMOD |= 0x01; // Timer0模式1
TH0 = 0xFC; TL0 = 0x18;
ET0 = 1; EA = 1; TR0 = 1;
}
void timer0_isr(void) interrupt 1{
TH0 = 0xFC; TL0 = 0x18;
g_ms++;
}
void sw_delay_ms(uint32_t ms){
uint32_t t = g_ms;
while((g_ms - t) < ms);
}
中断只维护ms计数,业务用时间差判断,既不占SysTick,也避免软件空循环被编译器优化掉 。
四、软件循环只做粗延时
无定时器可用时再用循环,但必须按主频校准、变量加volatile,否则-O2/-Os可能把空循环删掉:
static volatile uint32_t loops_per_ms = 8000u; // 示波器校准
void soft_delay_ms(uint32_t ms){
volatile uint32_t n = ms * loops_per_ms;
while(n--);
}
上板用GPIO翻转+示波器测实际宽度,反调loops_per_ms;这种方案只适合LED闪烁、上电稳定等粗时序,不适合通信严格时序。
五、避坑要点
软件延时会被中断拉长,要求高精度别用空循环,改DWT或硬件定时器轮询 。
DWT在M0/M0+不一定存在,移植前查TRM;没有就走TIM或RTC。
32位周期计数器长时间延时会溢出,用无符号相减判断区间可自动回绕。
低功耗场景别用忙等延时,改RTC闹钟、低功耗定时器唤醒,睡醒再继续。
多实例外设不要把同一个TIM既做PWM又做延时基准,分开或只用CNT差分。
编译器优化开-Os/-O2时,空循环、NOP延时都要volatile或内联汇编,最好用硬件计数器替代。
按“Cortex-M优先DWT、通用MCU用TIM轮询、8/51用Timer中断计ms”的分层思路,不用SysTick也能把毫秒延时做成可测算、可移植、不受任务调度拖偏的稳定时基。





