RISC-V IDE MRS2 开发笔记一:volatile关键字的使用

发布于:2025-05-22 ⋅ 阅读:(21) ⋅ 点赞:(0)

RISC-V IDE MRS2 开发笔记一:volatile关键字的使用

一、volatile是什么

二、GCC 中 volatile 的行为
2.1禁止编译器优化
2.2 不等于内存屏障
2.3 GCC扩展行为

三、什么时候需要 volatile
3.1防止编译器优化掉“有效代码”
3.2 访问硬件寄存器
3.3 中断服务程序

四、哪些地方不该用 volatile

一、volatile是什么

在 C 语言中,volatile 是一个类型修饰符,英文的意思是 “易变的”。
它告诉编译器:“这个变量的值随时可能被程序外部因素更改,因此在每次访问时都必须重新从内存中读取。”这意味着编译器不能对使用 volatile 修饰的变量进行编译优化。
在沁恒提供的外设库以及STM32官方库都大量使用了__IO、__O 等,其实本质上都是 volatile:
在这里插入图片描述

二、GCC 中 volatile 的行为
官网描述:
https://gcc.gnu.org/onlinedocs/gccint/Flags.html
https://gcc.gnu.org/onlinedocs/gcc/Volatiles.html

2.1禁止编译器优化

在GCC编译器中,volatile关键字具有优化无关性,其语义行为不受编译优化级别(如-O0、-Os等)的影响。都会在编译阶段禁止一些编译优化,比如,读/写访问的合并、死代码消除、寄存器缓存等。并且在 GCC 的中间表示中,volatile 会被特殊标记,访问 volatile 变量的指令会被标记为 volatile load 或 volatile store,即不可删除、不可合并的访问操作。
GCC 会使用 MEM_VOLATILE_P 标记这些内存访问,告诉中间层和后端:

  • 此内存访问必须按顺序保留
  • 每次访问都必须生成实际的指令

在这里插入图片描述

举个例子:

在这里插入图片描述

在这里插入图片描述

使用 GCC 的 -fdump-rtl-expand 选项可以清晰地看到 volatile 如何影响 RTL 表达式生成。被标记为 volatile 的变量在 RTL 中会显示为 mem/v:SI,其中 /v 表示访问具有 volatile 属性,禁止优化。这与 GCC 对 volatile 的语义一致:始终保持对内存的实际访问,防止编译器优化掉。

2.2 不等于内存屏障

引自官网:https://gcc.gnu.org/onlinedocs/gcc/Volatiles.html
volatile 保证“不会优化掉访问”,但不保证访问顺序,特别是对其他非 volatile 内存的影响顺序。
在这里插入图片描述

不能保证 *ptr = 123; 会在 vobj = 1; 之前执行。因为 *ptr 是普通内存,编译器可以重排这些指令。

在这里插入图片描述

如果需要严格的内存顺序,则必须使用更强的内存屏障(如__asm__ volatile(“” ::: “memory”)) 或使用GCC内置函数(__sync_synchronize())。这样 GCC 会认为 “所有内存都可能被修改”,不会重排 *ptr 和 vobj 的访问顺序。

2.3 GCC扩展行为

还可以配合内联汇编的使用:asm volatile确保汇编指令不被优化删除,即使输出未使用。
在这里插入图片描述

同样可以用 GCC 的 attribute((optimize(“O0”))) 来保护某些函数不被优化。

三、什么时候需要 volatile

3.1防止编译器优化掉“有效代码”

以图二的wait、wait_vol函数为例,其对应的汇编代码如下:
在这里插入图片描述

在 wait 函数中,编译器生成的指令仅在进入函数时读取一次 normal_var 的值(通过 lw 指令加载到 a5 寄存器),然后循环中始终基于该寄存器的值进行判断:如果 normal_var 在循环过程中由外部修改,由于变量未声明为 volatile,编译器假设其值不会被改变,因此不会再次从内存读取。这可能导致程序永远卡在循环中,无法感知变量变化,从而形成死循环。
相比之下,wait_vol 函数中对应的 volatile_var 被声明为 volatile,编译器因此不敢假设其值稳定,每次循环都重新发出 lw 指令,也就是不断从内存读取 volatile_var 的值。这体现了 volatile 的真实作用:强制每次都从内存取值,不允许优化,确保了对变量变化的感知。

3.2 访问硬件寄存器

在嵌入式系统中,经常通过内存映射直接访问硬件寄存器,这些寄存器的值会由外部硬件动态改变,而不是由程序直接控制。如果不加 volatile,编译器可能会出于优化目的缓存寄存器值或省略重复读取,从而导致数据错误。

以MR2创建的CH32V303CBT6模板工程为例,在\CH32V303CBT65\Debug\debug.c文件中,用于printf重定向的_write函数
在这里插入图片描述
在这里插入图片描述

在 _write 中,代码如下所示:
while (*(DEBUG_DATA0_ADDRESS) != 0u) {
// 等待硬件准备好
}
这是一个等待机制,通过while循环持续读取 DEBUG_DATA0_ADDRESS 的值直到硬件写入窗口空闲。如果 DEBUG_DATA0_ADDRESS 未使用 volatile 修饰,编译器可能会将其只读取一次,导致永远无法退出等待循环,进而卡死系统。

3.3 中断服务程序

在嵌入式系统中,中断服务程序与主程序之间共享变量是一种常见的设计模式。为了实现任务的及时响应,某些标志变量或数据缓冲区往往需要在主循环和中断服务程序之间共享。然而,由于中断具有异步性,其对变量的修改可能随时发生,这就要求编译器在访问这类变量时不得进行优化,否则可能导致主程序使用过期的值,从而引发严重逻辑错误。
以沁恒的CH32V307EVT的串口中断为例CH32V307EVT\EVT\EXAM\USART\USART_Interrupt\User\main.c

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

变量 RxCnt1、Rxfinish1 在主程序和 USART2_IRQHandler 中断服务程序中都可能被访问或修改,因此必须用 volatile 修饰,以防止编译器进行缓存优化。如果这些变量未使用 volatile 修饰,主程序可能会将它们的值缓存在寄存器中。
代码如下所示:

while(Rxfinish1 == 0) {
// 等待接收完成
}
如果编译器缓存了 Rxfinish1 的值,那么即使中断函数设置它为 1,主循环仍然会看到旧值(0),从而进入死循环。使用 volatile 后,编译器会强制每次从内存中重新读取变量值,保证获取的是中断修改后的最新状态。

四、哪些地方不该用 volatile

虽然 volatile 是强有力的工具,但滥用也会带来问题:

  • 普通局部变量:局部变量的作用域仅限于函数内部,不会被外部事件(如中断、多线程)修改。使用 volatile 会强制每次访问都从内存读写,禁用寄存器缓存优化,导致性能下降。
  • 多线程同步仅依赖volatile:volatile仅保证可见性(每次访问内存),但不保证原子性或操作顺序。

网站公告

今日签到

点亮在社区的每一天
去签到