The volatile Keyword: Why Register Reads Cannot Be Optimized Away

Smart AI Code2026-10-015 min read

What you will learn in this article

  • Understand why register reads must use volatile.
  • Master the hardware behavior and correct reading method of SysTick->VAL.
  • Learn how to troubleshoot and mitigate 1 ms timing errors caused by compiler optimization.
  • Understand debugging methods to verify whether register reads are real.

1. Why compilers "delete" your read operations

When optimization is enabled (such as with -O2), the compiler analyzes the "visible results" of the code. If a read/write operation does not affect the final output, the compiler may remove it entirely. For example:

c uint32_t i; for (i = 0; i < 72000U; i++) { }

This loop does not change any external state, so the compiler considers it useless and deletes it, causing the delay to fail.

2. The role of volatile

volatile tells the compiler that reads and writes to this variable must be executed literally and cannot be cached in registers or optimized away. Its behavior is defined in the C standard as "allowing access to hardware registers or variables that can be safely modified by interrupts."

Source: An article on cnblogs.com points out that volatile uint32_t i; ensures that i++ actually reads and writes to memory every time, preventing the loop from being deleted as irrelevant code.

3. The hardware essence of SysTick->VAL

3.1 It is a hardware register

SysTick->VAL is located in the SysTick timer of ARM Cortex-M, at address 0xE000E018 (base address 0xE000E010 offset 0x08). This register is automatically decremented by the hardware counter at each core clock cycle, without any assignment from C code.

Sources: dzone.com and community.st.com provide this address definition; cnblogs.com describes the hardware decrement behavior.

3.2 volatile modifier in the structure

In the CMSIS standard peripheral library, __IO is defined as volatile. Therefore, all fields in the SysTick_Type structure (including VAL) have volatile semantics. This means every read of SysTick->VAL directly accesses the current hardware count value rather than reading a cached software variable. Other registers in the same group, such as CTRL, LOAD, and CALIB, are similarly modified.

Source: cnblogs.com explicitly states that __IO is equivalent to volatile.

4. How to correctly read SysTick->VAL

4.1 Basic usage

c uint32_t start = SysTick->VAL; // Read the current count value /* Perform the operation to be timed */ uint32_t end = SysTick->VAL; // Read again uint32_t elapsed = start - end; // Decrementing counter, the difference is the elapsed cycles (rollover must be considered)

Since SysTick->VAL is a 24-bit decrementing counter that auto-reloads from the value in the LOAD register down to zero, typically start is greater than or equal to end. If a rollover occurs (end > start), special handling is required.

4.2 Detecting rollover

When the counter rolls over from 0 to the reload value, end can be greater than start. An empirical correction method is to check if SysTick->VAL is greater than the threshold 0x0000BB1C. If it is, a rollover is assumed and one cycle is added. This method is not perfect and may sometimes count an extra tick.

Source: A user on community.st.com described this approach and its limitations.

5. Race conditions between interrupts and the main loop

When the SysTick interrupt (SysTick_Handler) modifies a global variable uwTick, and the main loop reads this variable simultaneously, a race condition can occur. In practice, this results in a 1 ms timing error with a probability of approximately 0.5%.

Source: A user on community.st.com reported that uwTick deviated by 1 count (equivalent to a 1 ms error) about 0.5% of the time.

5.1 Tried solutions

SolutionDescriptionEffect
Software dirty-bitSet a bit when entering the ISR; the main loop checks it after reading.Did not eliminate the error.
Voting samplingRead multiple times quickly and take the majority result.Still had edge cases where errors slipped through.
Full inliningInline functions to reduce call overhead.Significantly reduced lag reads but did not cure the root cause.

Source: A community.st.com post detailed these three attempts.

5.2 Why reading SysTick->VAL directly is more reliable

Since SysTick->VAL is a hardware register, it is not affected by the ISR modifying software variables. Combined with rollover detection, it can provide more accurate timestamps to some extent, though its imperfections must still be noted.

6. Verifying whether register reads are real

6.1 Using GPIO toggling and an oscilloscope

Toggle a GPIO pin before and after reading SysTick->VAL, then use an oscilloscope to observe the time interval between the two toggles. This visually determines whether the read operation has been optimized away. For example, a user on community.st.com used GPIO2_HIGH and GPIO2_LOW in combination with an oscilloscope to measure the execution time of the HAL_Precision_GetTick function.

Source: A community.st.com user actually used GPIO2_HIGH/LOW with an oscilloscope for measurement.

6.2 Checking the assembly code

After compiling under the optimization level, inspect the generated assembly to see if it contains an actual LDR instruction accessing 0xE000E018, rather than reading a cached value from a register or the stack.

7. Precautions and limitations

  • volatile only ensures that reads and writes are not optimized away; it does not provide atomicity or memory barriers. In multi-threaded or interrupt environments, disabling interrupts or using critical sections is still required.
  • The rollover detection threshold 0x0000BB1C for SysTick->VAL is an empirical value and may need adjustment under different clock frequencies.
  • Other peripheral registers (such as TIM2->CNT, GPIOB->IDR) also change autonomously by hardware and should likewise be declared as volatile.

Source: cnblogs.com listed examples of TIM2->CNT and GPIOB->IDR.

8. Next steps

  • Read the SysTick chapter in the ARM Cortex-M Reference Manual.
  • Practice the volatile modifier in your own project, and use an oscilloscope or GPIO toggling to verify the read behavior.
  • If higher precision timestamps are needed, consider using the capture/compare hardware of a general-purpose timer or a dedicated timestamp unit.