This article details the “interrupt service routine variable not updating” issue in embedded ARM development, which stems from compiler optimization and missing volatile modifiers. It uses examples to explain errors caused by compiler “redundant load elimination”, illustrates how the volatile keyword prohibits register caching and maintains instruction order, and compares optimization differences at the ARM assembly level. It also summarizes the mandatory scenarios of volatile in interrupt-shared variables, multi-threads, and hardware registers, and provides suggestions for compilation optimization levels, helping developers solve the core problem of ARM main loops failing to read ISR variables.
1. Background: A “Puzzling” Phenomenon in ARM Program Debugging
Recently, when debugging an ARM architecture embedded program, a typical issue was encountered: the main loop could never read the updated value of a variable modified by the Interrupt Service Routine (ISR). After repeatedly checking the code logic and interrupt configuration, it was found that the root cause was not a code logic error, but a combination of compiler optimization level and missing volatile keyword—a high-frequency and critical classic problem in embedded system programming.
In simple terms, if a variable modified in the ISR is not decorated with volatile
, the compiler may generate incorrect machine code due to “over-optimization”, preventing the main program from obtaining the latest value of the variable. Below is a detailed explanation from core principles, problem breakdown, solution to application scenarios.

2. Core Issue: Optimization Traps Caused by Compiler “Perspective Limitations”
When the compiler translates C code into ARM machine code, it uses optimization techniques such as “redundant load elimination” and “instruction reordering” to reduce code size and improve execution efficiency. However, the compiler cannot perceive modifications to variables by asynchronous operations (e.g., interrupts) and can only analyze the code logic of the current execution flow (e.g., the main
function), leading to optimization “misjudgments”.
1. A Dangerous Example Without volatile
int flag = 0; // Flag for communication between ISR and main function
// Interrupt Service Routine: Set flag to 1 when interrupt is triggered
void IRS_Handler(void) {flag = 1;}
// Main loop: Execute operations when flag is detected as 1
int main(void) {while (1) {if (flag) { // Expected to read the updated flag value from ISR
do_something();
flag = 0; // Clear the flag
}
}
}
2. The Compiler’s “Incorrect Optimization Logic”
When analyzing the main
function, the compiler makes the following misjudgments, ultimately causing the program to enter an “infinite loop”:
- First load the variable into a register: After the
main
function enters thewhile
loop, the compiler reads the value offlag
from memory into a CPU register (e.g., the R0 register of ARM); - Judge no modification to the variable: The compiler does not detect any code in the
main
function that modifiesflag
(it cannot perceive the existence of the ISR) and assumes that the value offlag
is “permanently unchanged”; - Eliminate “redundant reads”: The compiler determines that “repeatedly reading
flag
from memory is a waste of performance”, so in subsequent loops, it directly uses the copy offlag
in the register instead of reloading it from memory; - Generate incorrect machine code: The optimized ARM assembly code is as follows.
flag
is read only once. Even if the ISR modifiesflag
in memory to 1, the main loop cannot detect this change:
main:
ldr r1, =flag ; Load the memory address of flag into r1
ldrb r0, [r1] ; Read flag from memory to r0 only once (value is 0)
loop:
cmp r0, #0 ; Compare the value in register r0 (always 0)
beq loop ; If 0, jump back to loop for infinite loop
... ; do_something() can never be executed
3. The volatile
Keyword: The Key to Solving Compiler Optimization Issues
The core function of volatile
is to send a signal to the compiler that “the variable is volatile”, clearly indicating that the value of the variable may be modified by agents outside the current execution flow (such as interrupts, hardware registers, and multi-threads), and prohibiting the compiler from performing hypothetical optimizations on it.
1. Two Core Functions of volatile
- Prohibit register caching: Force the compiler to reload the latest value from memory every time the variable is used, instead of using the old copy in the register;
- Maintain instruction order: Prevent the compiler from reordering instructions related to the variable (complementary to the memory barrier function), ensuring that the execution order of the code is consistent with the source code logic.
2. Corrected Example with volatile
volatile int flag = 0; // Key: Add volatile modification
void IRS_Handler(void) {flag = 1; // Modify flag in memory when interrupt is triggered}
int main(void) {while (1) {if (flag) { // Read the latest flag value from memory in each loop
do_something();
flag = 0; // Clear the flag (also write to memory)
}
}
}
3. Correct Machine Code After Optimization
At this point, the compiler will generate ARM assembly that “reads flag
from memory in each loop”, ensuring that the main loop can perceive modifications to the variable by the ISR in real time:
main:
ldr r1, =flag ; Load the memory address of flag into r1
loop:
ldrb r0, [r1] ; Read flag from memory to r0 in each loop
cmp r0, #0 ; Compare with the latest value
beq loop ; If 0, continue looping; if not, execute subsequent operations
... ; Execute do_something() normally
4. Summary: Mandatory Scenarios for volatile
in Embedded Programming
In ARM and other embedded architecture programming, variables must be modified with volatile
in the following scenarios; otherwise, program abnormalities may occur due to compiler optimization:
Application Scenario | Detailed Description | Example |
---|---|---|
Shared variables between interrupts and main program | Variables modified by ISR and read by the main program (or vice versa) need to ensure the main program can obtain the latest value | Interrupt flags, data reception buffer flags |
Shared variables between multi-threads/RTOS tasks | Variables shared between different tasks (Note: volatile only ensures visibility; mutex locks are required to ensure atomicity) |
Status variables for inter-task communication |
Memory-mapped hardware registers | Registers whose addresses are mapped to hardware peripherals (values are dynamically modified by hardware, not controlled by software) | GPIO data registers, UART receive registers |
5. Important Reminder: Limitations of volatile
and Suggestions for Compiler Optimization
- Only solves “visibility”, not “atomicity”:
volatile
can ensure that the latest value of a variable is read, but it cannot guarantee the atomicity of variable operations. For example, when an 8-bit MCU operates a 32-bitvolatile
variable, multiple instructions are required. If interrupted during the process, data errors may occur. In this case, protection methods such as “disabling interrupts” and “using atomic operation functions” are needed. - Suggestions for compiler optimization levels:
If there are no strict requirements on program size, it is recommended to set the compiler optimization level to the lowest (e.g., GCC’s-O0
) in both Debug and Release versions to avoid hidden issues caused by optimization; if optimization needs to be enabled (e.g.,-O1
,-O2
), ensure that critical variables are modified withvolatile
.
FAQ:
1、Why Can’t the ARM Main Loop Read Variables Modified by the Interrupt Service Routine?
This is because the compiler performs ‘redundant load elimination’ optimization: after reading a variable into a CPU register for the first time, if no modification to the variable is detected in the current execution flow (e.g., the main function), it will directly use the register copy instead of reloading from memory. The Interrupt Service Routine (ISR) is an asynchronous operation, and the compiler cannot perceive its modification to the variable, causing the main loop to always read the old value. The variable needs to be modified with the volatile keyword to solve this problem.
2、What Is the Use of the Volatile Keyword in Embedded ARM Programming?
The volatile keyword mainly has two functions: 1. It prohibits the compiler from caching variables in registers and forces reloading the latest value from memory every time the variable is used; 2. It prevents the compiler from reordering instructions related to the variable, ensuring the execution order is consistent with the source code. It can solve the ‘invisibility’ problem of variables in scenarios such as interrupts, hardware registers, and multi-threads.
3、What Is the Appropriate ARM Compiler Optimization Level? Do We Need to Disable Optimization?
If there are no requirements on program size, it is recommended to set the optimization level to the lowest (e.g., GCC -O0) in both Debug and Release versions to avoid hidden issues caused by optimization; if optimization needs to be enabled (e.g., -O1/-O2), ensure that critical variables such as interrupt-shared variables and hardware registers are modified with the volatile keyword to prevent incorrect compiler optimization.
4、Can Volatile Ensure the Atomicity of ARM Variable Operations?
No. Volatile only solves the ‘visibility’ of variables (ensuring the latest value is read) but does not guarantee ‘atomicity’. For example, when an 8-bit MCU operates a 32-bit volatile variable, multiple instructions are required, and data errors may occur if interrupted during the process. Complex shared data needs to be protected by disabling interrupts or using atomic operation functions (e.g., LDREX/STREX of ARM).