中断服务程序(ISR)与主循环共享变量时,访问冲突(数据竞争)如何解决
在单片机开发中,中断服务程序(ISR)与主循环共享变量时,由于中断可能在主循环操作变量的任意时刻触发,极易导致数据不一致(比如主循环读取变量时被中断打断,中断修改后主循环继续读取,得到错误值)。解决该问题的核心是保证共享变量的 “操作原子性”(即操作过程不被中断打断),常见方法如下:
1. 基础:用 volatile 关键字声明共享变量
共享变量 必须用 volatile 修饰,告诉编译器 “该变量可能被意外修改(如中断),禁止优化到寄存器中,每次访问必须从内存读取”。
示例:
volatile uint32_t shared_cnt; // 中断和主循环共享的计数器 若不加volatile,编译器可能会将变量缓存到寄存器,导致主循环无法感知中断对变量的修改。
2. 核心:保证操作的 原子性
根据共享变量的类型(单字节 / 多字节)和操作复杂度,选择以下方法:
方法 1:访问时临时禁用中断
在主循环读写共享变量前,先禁用中断;操作完成后,再重新使能中断。确保主循环对变量的操作不被中断打断。
注意:禁用中断的时间要尽可能短,避免影响中断响应的实时性(尤其是对高频 / 高优先级中断)。
示例(以 STM32 为例,用 __disable_irq() 和 __enable_irq()):
// 中断服务程序:修改共享变量
void TIM_IRQHandler(void)
{if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET){shared_cnt++; // 中断中修改共享变量TIM_ClearITPendingBit(TIM2, TIM_IT_Update);}
}// 主循环:读取共享变量
int main(void)
{uint32_t local_cnt; // 用局部变量暂存共享变量的值while (1){__disable_irq(); // 禁用所有中断local_cnt = shared_cnt; // 读取共享变量(操作被保护)__enable_irq(); // 重新使能中断// 主循环使用 local_cnt 处理,无需再访问共享变量printf("Count: %lu\n", local_cnt);}
}方法 2:使用原子操作(适合单字节 / 硬件支持的场景)
若共享变量是单字节(如 uint8_t),多数单片机对其读写是 “原子操作”(一条指令完成,不会被中断打断),无需额外处理。
若变量是多字节(如 uint16_t/uint32_t),可利用单片机的硬件原子操作指令(如 ARM 的 LDREX/STREX,或 AVR 的 ATOMIC_BLOCK),确保多字节操作不被中断拆分。
示例1(ARM Cortex-M 系列的原子操作宏):
LDREX:读取变量时标记 “排他访问”,若期间有其他操作(如 ISR 修改),标记会失效;STREX:仅当 “排他标记有效” 时才写入变量,否则写入失败,需重试。
volatile uint32_t shared_cnt; // 多字节共享变量// ISR中修改(用原子操作)
void ISR(void)
{uint32_t temp;do{temp = LDREXW(&shared_cnt); // 排他加载temp++; // 修改}while (STREXW(temp, &shared_cnt) != 0); // 排他存储,失败则重试
}// 主循环中修改(同样用原子操作)
int main(void)
{uint32_t temp;while(1){do{temp = LDREXW(&shared_cnt); // 排他加载temp *= 2; // 修改}while (STREXW(temp, &shared_cnt) != 0); // 失败重试}
}示例2(AVR 的原子操作宏):
volatile uint16_t shared_val; // 16位共享变量// 主循环读取(原子操作)
uint16_t read_shared(void)
{uint16_t val;ATOMIC_BLOCK(ATOMIC_RESTORESTATE) // 原子块:内部自动禁用/使能中断{val = shared_val;}return val;
}方法 3:用标志位分离数据与控制(减少共享变量暴露)
中断不直接修改主循环的核心数据,而是设置一个 “标志位”(单字节,原子操作);主循环通过检查标志位决定是否处理数据,处理时再安全读取。
优点:减少共享变量的直接操作,降低冲突概率。
示例:
volatile uint8_t data_ready = 0; // 标志位:0-未就绪,1-数据可用
volatile uint32_t shared_data; // 共享数据// 中断:接收数据后置标志
void UART_IRQHandler(void)
{if (UART_GetITStatus(UART1, UART_IT_RXNE) != RESET){shared_data = UART_ReceiveData(UART1); // 读取数据data_ready = 1; // 置标志UART_ClearITPendingBit(UART1, UART_IT_RXNE);}
}// 主循环:检查标志并处理
int main(void)
{uint32_t local_data;while (1){if (data_ready){ // 标志位是单字节,读取是原子操作__disable_irq();local_data = shared_data; // 安全读取数据data_ready = 0; // 清标志(需在中断禁用时操作,避免重入)__enable_irq();// 处理 local_dataprocess_data(local_data);}}
}方法 4:数据备份与校验(适合不关中断,低实时性场景)
主循环多次读取共享变量,若连续两次读取结果一致,则认为数据有效(未被中断修改);否则重试。
优点:不用关闭中断
缺点:可能存在重试开销,两次读取和比较效率低,不适合实时性要求高的场景。
示例:
volatile uint16_t shared_val;uint16_t safe_read(void)
{uint16_t val1, val2;do{val1 = shared_val;val2 = shared_val;}while (val1 != val2); // 两次读取一致则返回return val1;
}方法 5:标志位 + do-while重试机制(适合不关中断场景)
主循环通过检测标志位判断读取过程是否被 ISR 打断,若被打断则重新读取,从而避免使用错误数据。
示例1:
volatile uint8_t data_state = 0; // 0: 空闲;1: ISR中已更新
volatile uint32_t shared_data; // 多字节共享数据// ISR:更新数据前先锁状态,完成后解锁
void ISR(void)
{shared_data = new_value; // 更新数据data_state = 1; // 标记为“已更新”(单字节,原子操作)
}// 主循环:读取前复位状态,当状态为“已更新”时,重新读取
int main(void)
{uint32_t local_data;while (1){do{data_state = 0; // 复位状态(单字节,原子操作)local_data = shared_data; // 读取数据(即使被ISR打断,因ISR此时只能在状态0时操作)}while(data_state) // 当状态为“已更新”时,重新读取process(local_data);}
}优点:
- 无需关闭中断:避免了关闭中断对实时性的影响,适合对中断响应敏感的场景(如高频传感器数据采集)。
- 利用单字节标志的原子性:
data_state是uint8_t,其读写是原子操作(一条指令完成),ISR 和主循环对标志位的修改 / 读取不会互相干扰(不会出现 “标志位读一半被修改” 的情况) - 单字节标记提高效率:相比方法4,多字节进行比较校验,节省代码,提高了效率。
缺点:
- 无效操: shared_data没有更新也会被反复读取,无效操作导致主循环处理效率下降。
示例2:
volatile uint8_t data_state = 0; // 0: 空闲;1: ISR中已更新
volatile uint32_t shared_data; // 多字节共享数据// ISR:更新数据前先锁状态,完成后解锁
void ISR(void)
{shared_data = new_value; // 更新数据data_state = 1; // 标记为“已更新”(单字节,原子操作)
}// 主循环:读取前复位状态,当状态为“已更新”时,重新读取
int main(void)
{while (1){if(data_state) // 当状态为“已更新”时,才读取{uint32_t local_data;do{data_state = 0; // 复位状态(单字节,原子操作)local_data = shared_data; // 读取数据(即使被ISR打断,因ISR此时只能在状态0时操作)}while(data_state) // 当状态为“已更新”时,重新读取process(local_data);}}
}优点:
- 减少无效操作:只有当 ISR 确实更新了数据(
data_state=1)时,主循环才进行读取和重试,避免了无新数据时的空转,效率更高。 - 保持无中断关闭特性:依然依赖单字节标志的原子性,无需禁用中断,不影响 ISR 的实时响应。
- 确保最终数据正确性:通过
do-while重试机制,即使多字节读取被 ISR 打断,最终也能读取到完整的新数据。
总结
核心就两点:
- 必须用
volatile声明共享变量,避免编译器优化导致的 “值不可见” 问题; - 保障“操作结果的原子性”。有校验,重试机制等虽不是原子操作,但最终目标结果是具有原子特性;
以上是只是常见的一些方法,每种方法都有优缺点,实际开发时需要结合场景,选择合适的方法。遇到这种数据竞争场景要格外重视,很容易隐藏Bug,本人就曾在这方面载过跟头。
