为何mult=3/4时while循环在条件为真时提前终止?
问题解析:循环提前终止与volatile变量的非原子读取
问题现象
当mult取值为3或4时,EXTI_Main函数中的while(TMR3_GetTick() < minimum);循环会提前终止,输出结果显示实际tick值小于设定的minimum(例如mult=3时tick=512 < 600,mult=4时tick=768 < 800)。
相关代码
EXTI_Main函数
void EXTI_Main(uint8_t mult)//range 1-4(inclusive) { uint16_t minimum = 200 * mult; while(TMR3_GetTick() < minimum); printf("mult:%hhu tick:%hu minimum:%hu\n", mult, TMR3_GetTick(), minimum); //RF_Main(); LCD_Read(); }
定时器tick读取代码(tmr3.c)
static volatile uint16_t msTick;//incremented using ISR uint16_t TMR3_GetTick(void) { return msTick; }
运行输出示例
mult:0 tick:0 minimum:0 mult:1 tick:414 minimum:200 mult:2 tick:453 minimum:400 mult:3 tick:600 minimum:600 mult:4 tick:800 minimum:800 mult:0 tick:0 minimum:0 mult:1 tick:414 minimum:200 mult:2 tick:453 minimum:400 mult:3 tick:600 minimum:600 mult:4 tick:768 minimum:800 mult:0 tick:0 minimum:0 mult:1 tick:414 minimum:200 mult:2 tick:453 minimum:400 mult:3 tick:512 minimum:600 mult:4 tick:768 minimum:800 mult:0 tick:0 minimum:0
问题根源
核心原因是16位volatile变量的非原子读取:
msTick是16位变量,且在中断服务程序(ISR)中被递增修改,因此被声明为volatile以防止编译器缓存其值。- 在8位架构的MCU上,读取16位变量需要分两步完成:先读取高字节,再读取低字节(或反之),两步操作之间存在时间窗口。
- 如果ISR在这两步读取之间触发并修改了
msTick,就会得到一个错误的“拼凑值”。例如:- 当
msTick从0x01FF(511)递增到0x0200(512)时,若先读取低字节0xFF,随后ISR完成递增操作(低字节变为0x00,高字节变为0x02),再读取高字节0x02,最终得到的错误值是0x02FF(767)。 - 这个错误值远大于
mult=3时的minimum=600,导致循环错误地认为已满足终止条件而提前退出。
- 当
- 循环退出后,
printf再次读取msTick时,得到的是正确的最新值(512),因此出现输出中tick < minimum的矛盾现象。
解决方案解析
用户采用的写法:
uint16_t tock; while((tock = TMR3_GetTick()) < minimum);
本质是通过每次循环只读取一次msTick并存储到局部变量tock,再基于tock判断循环条件。虽然没有从根本上解决非原子读取的问题,但大幅降低了错误值导致提前终止的概率:
- 若某次读取到错误的偏小值,循环会继续执行,直到读取到正确的、满足条件的值;
- 若读取到错误的偏大值,虽然仍会提前终止,但这种情况的发生概率极低,适用于对时间精度要求不高的场景。
更严谨的解决方案是保证msTick读取的原子性,通过关中断完成读取操作:
uint16_t TMR3_GetTick(void) { uint16_t tick; __disable_irq(); // 关闭全局中断,防止ISR打断读取操作 tick = msTick; __enable_irq(); // 恢复全局中断 return tick; }
这样能确保读取msTick的过程不被打断,彻底避免错误值的产生。
内容的提问来源于stack exchange,提问作者Wesley Kienhuis
相关产品推荐
相关产品推荐

