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

ZYNQ平台中断服务函数中的变量不加volatile修饰导致的奇怪问题解决

文章目录

      • 1. 核心问题:编译器的“视角”局限性
        • 一个没有 `volatile` 的危险例子:
      • 2. `volatile` 关键字的作用
        • 修正后的例子:
      • 总结:何时使用 `volatile`?

ZYNQ7020平台,调用AXI-Quad SPI IP,中断方式传输数据。


uint8_t doing = 0;int main()
{spi_init();gic_init();while(1){doing = 1;XSpi_Transfer(...);while(doing);delay_ms(500);xil_printf("send done\r\n");}
}void handler(void *CallBackRef, u32 StatusEvent, u32 ByteCount)
{doing = 0;
}

就是对SPI外设初始化,并使能中断,在中断服务函数中将doing变量置为0,在主函数中发送完成后,如果doing=0,则表明发送完成,可以进行下一次发送。

实际现象是,在SDK中使用debug版本的elf文件在线调试,发现程序可以正常运行。
但是如果将编译版本改为release,并将release.elf固化到QSPI中,发现程序会卡死在while(doing)处,表明doing的值并没有置为0。

而且如果使用release.elf在线调试,现象和固化后一致,也是doing无法置0。

猛然想起,中断复位函数中改变的变量应该加volatile修饰,所以将doing定义为如下:

volatile uint8_t doing = 0;

再次测试,发现无论是debug版本还是release版本elf文件,进行在线调试或者固化,运行现象都正常。

通过查看SDK默认的编译器优化等级配置,发现debug版本编译时无优化,而release版本编译时优化等级为-O2。

看来这里也是一个问题,所以把release版本优化等级也改为了无优化,取消volatile修饰,再次进行在线调试和固化运行,结果也是一切正常,符合设计。

所以,总结两点:

  1. 如果对elf程序文件大小不敏感,release版本编译可以不进行优化。
  2. 凡是在中断服务函数中修改的变量,都必须加上volatile修饰,以避免意想不到的结果。

为了彻底搞明白这个问题,我把这个问题丢给了DeepSeek,以下是它的回答:

问:为什么中断中改变的变量要加volatile修饰

说得非常好!这是一个嵌入式系统编程中至关重要且经典的问题。

简单直接的回答是:为了防止编译器进行“过度优化”,从而生成错误的代码,导致程序无法读取到变量在中断中的最新值。

下面我们进行详细的分解说明:

1. 核心问题:编译器的“视角”局限性

编译器在将你的C代码翻译成机器码时,会进行各种优化来让代码跑得更快、体积更小。其中一种常见的优化叫做 “冗余加载消除”

编译器在分析你的main函数(或任何非中断函数)时,它不知道无法感知这个变量会被一个异步发生的中断服务程序(ISR)修改。在它看来,这个变量的值只能被当前的执行流改变。

一个没有 volatile 的危险例子:
int flag = 0; // 用于在ISR和main之间通信的标志位// 中断服务程序
void IRS_Handler(void) {flag = 1; // 中断发生,设置标志位
}// 主循环
int main(void) {while (1) {if (flag) { // 检查标志位do_something(); // 如果标志位为真,执行某些操作flag = 0; // 清除标志位}}
}

编译器可能会这样“思考”(优化):

  1. main函数的while循环中,它第一次读取flag的值到CPU寄存器(比如R0)。
  2. 编译器发现,在循环体内,没有任何代码会修改flag(它看不到IRS_Handler!)。
  3. 因此,它“聪明地”认为:flag的值永远不会变,每次判断if(flag)都用同一个值(最初读到的0),重复从内存读取是浪费性能。
  4. 优化后的机器码可能看起来像这样
    main:ldr  r1, =flag    ; 将flag的地址加载到寄存器r1ldrb r0, [r1]     ; 【第一次】将flag的值从内存加载到寄存器r0
    loop:cmp  r0, #0       ; 检查寄存器r0的值(即flag)是否为0beq  loop         ; 如果为0,跳回loop继续循环(死循环!)...               ; 永远执行不到这里
    
    看到了吗?flag的值只从内存中读取了一次,之后就一直在使用寄存器r0中的副本。即使中断发生,IRS_Handler将内存中的flag改为了1,但main函数使用的r0寄存器里的值依然是0,它永远也检测不到这个变化,程序就“死”在了这个循环里。

2. volatile 关键字的作用

volatile 关键字就是用来告诉编译器:“这个变量是易变的,它的值可能会被当前代码之外的代理改变(比如中断、硬件寄存器、另一个线程),你不能对它做任何假设性的优化。

它的具体含义是:

  • 禁止编译器将变量缓存在寄存器中,每次使用都必须老老实实地从内存中重新读取
  • 防止编译器调整指令顺序(与另一个关键字memory barrier相关),确保对volatile变量的操作顺序在生成的汇编代码中得到严格保持。
修正后的例子:
volatile int flag = 0; // 加上 volatile 修饰!void IRS_Handler(void) {flag = 1;
}int main(void) {while (1) {if (flag) {      // 每次都会从内存地址读取flag的最新值do_something();flag = 0;}}
}

现在,编译器生成的代码会是这样:

main:ldr  r1, =flag    ; 将flag的地址加载到寄存器r1
loop:ldrb r0, [r1]     ; 【每次循环】都从内存加载flag的值到r0cmp  r0, #0       ; 检查新值beq  loop         ; 如果为0,继续循环...               ; 如果不为0,执行后续操作

这样,无论中断何时发生,main函数都能在下次循环时读取到最新的、正确的flag值。

总结:何时使用 volatile

在嵌入式编程中,你必须在以下情况下使用 volatile

  1. 在中断服务程序(ISR)和主程序(或非ISR任务)之间共享的变量。(正如上面的例子)
  2. 在多线程(或RTOS任务)之间共享的变量(虽然对于多线程,通常还需要更强的同步机制如互斥锁,但volatile是基础要求)。
  3. 映射到内存的硬件寄存器。例如:
    #define GPIO_DATA (*(volatile unsigned int *)0x40000000)
    
    这个指针指向一个硬件地址,其值会由硬件外设(如引脚电平)改变,编译器绝不能优化对它的访问。

重要提醒:
volatile 只解决了“可见性”问题(即确保读到最新值),但它并不保证操作的“原子性”。例如,对一个volatile uint32_t的写入在32位机器上是原子的,但在8位机器上可能需要多条指令。如果同时被中断打断,可能会写入错误的数据。对于复杂的共享数据,通常需要暂时关闭中断来进行保护。

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

相关文章:

  • 2026年UX/UI五大趋势:AI、AR与包容性设计将重新定义用户体验
  • 网站做跳转自己建网站卖鞋
  • 百度网站服务器外贸网站优化
  • 应广单片机烧录跳线J7专用PCB使用说明
  • Java 前后端加密与编码技术:从概念到实战场景全解析
  • 拒绝笨重,一款轻量、极致简洁的开源接口管理工具 - PostIn
  • 建设银行信用卡网站是哪个茶叶seo网站推广与优化方案
  • vant van-uploader上传file文件;回显时使用imageId拼接路径
  • Java常用中间件整理讲解——Redis,RabbitMQ
  • JavaEE初阶7.0
  • 从“天书”到源码:HarmonyOS NEXT 崩溃堆栈解析实战指南
  • 个人网站收款google play 应用商店
  • _撸猫websocket服务器端,手机远程服务端
  • 【论文精读】FDGaussian:基于几何感知扩散模型的单图快速高斯溅射 3D 重建
  • 功防世界-Web-bug
  • 做网站能拿多少钱平面设计 网站推荐
  • REST介绍,实质,六大约束,优缺点(数据冗余问题,身份验证困难(解决方式 -- JWT+集中式认证服务,使用代理))
  • Snapchat Data Scientist 面试经验分享|从 OA 到 Final Round 全流程复盘
  • 消息队列集群——RabbitMQ
  • 初识C语言14.动态内存管理
  • ks2e做网站高端品牌设计
  • 华为od-22届考研-C++面经
  • Win10 系统构建仿真 NVIDIA Jetson Orin Nano 环境部署 YOLOv8 模型
  • 英文网站开发付费下插件wordpress
  • 【面板数据】汽车之家及懂车帝汽车配置信息数据集(1999-2025.4)
  • Slotted Aloha
  • 「赤兔」Chitu 框架深度解读(六):剖析 Attention 机制后端实
  • 嵌入式开发中为啥常用do{}while(0)进行宏定义
  • 第六部分:VTK进阶(第172章 vtk-m加速器管线)
  • 矽塔 SA8207 36V输入耐压 高精度可调过流保护与集成智能故障管理 过压过流保护芯片