当前位置: 首页 > news >正文

中断服务程序(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_stateuint8_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 打断,最终也能读取到完整的新数据。

总结

     核心就两点:

  1. 必须用 volatile 声明共享变量,避免编译器优化导致的 “值不可见” 问题;
  2. 保障“操作结果的原子性”。有校验,重试机制等虽不是原子操作,但最终目标结果是具有原子特性;

        以上是只是常见的一些方法,每种方法都有优缺点,实际开发时需要结合场景,选择合适的方法。遇到这种数据竞争场景要格外重视,很容易隐藏Bug,本人就曾在这方面载过跟头。

http://www.dtcms.com/a/536847.html

相关文章:

  • 西部数码网站流量怎么充简易网站开发时长
  • FFmpeg 基本数据结构 AVFrame分析
  • Kafka 相关内容总结
  • 霍邱网站设计10000ip网站怎么做
  • C++ 从零实现Json-Rpc 框架
  • 29. Makefile 创建和使用变量
  • Docker 安装和配置 Redis 完整指南
  • 高效对象池设计:提升Unity性能的关键
  • 网站建设需要了解哪些信息常州网站制作公司
  • 如何做正版小说网站工厂电商具体是做什么的
  • 磁盘和注册表清理工具
  • 【windows】证书引起的浏览器请求问题-https红色斜线-解决方法
  • mormot2创建一个httpserver
  • 科技类网站简介怎么做有哪些游戏可以做网站
  • 定制化TTS数据实践:解锁语音大模型的无限潜能
  • 微网站是什么嘉兴高端网站定制
  • 一分钟讲透:c++新特性string_view
  • sns社交网站 建设做网站图片尺寸
  • 营销网站结构网站免费优化平台
  • 免费视频模板网站制作微信网页
  • android实践:loadUrl执行JavaScript异常
  • FFmpeg 基本数据结构 AVCodecParser分析
  • celery知识点总结
  • langchain将用户问题转sql查询探索
  • compareAndSet怎么用
  • Skill Seeker——一站式自动化将文档网站、GitHub 仓库和 PDF 文件转换为可部署 AI 技能的深度解析
  • 浅谈 Agent 开发工具链演进历程
  • 帝国cms小说阅读网站模板果洛电子商务网站建设哪家快
  • 学校网站建设背景科技作品手工
  • SmartPLS下载安装教程(附安装包)SmartPLS 4.1保姆级图文教程