How does the compiler work when I use volatile in my following code? Does the compiler look at both inside and outside the ISR if I don't use it volatile?
int x;
void main ()
{
x = 0;
while (1)
{
if ( x == 1)
{
x++;
}
}
}
ISR ()
{
if(flag == 0)
{
flag = 0;
x = 1;
}
}
I'm not sure what you mean by looking inside and outside the ISR.
The "volatile" qualifier means that the compiler will not perform certain kinds of optimizations when accessing the variable that it normally would. It is a directive to the compiler that the value of the variable can be changed by external factors and so it shouldn't make any assumptions about what that variable contains.
Quick example:
int x;
int y;
...
x = 3;
y = x;
...
For the assignment to y an optimizing C compiler might simply assign 3 to y. However, if x is marked as volatile it should generate code which actually loads the value stored at x even though the previous statement assigned it to 3. And the compiler should do this regardless of where the access to x is located -- whether it occurs in an ISR or not.
Even if you have declared a variable to be volatile you still may need to disable interrupts before accessing it if the value cannot be read atomically. See this page on the use of volatile in the Arduino environment for more details:
https://arduinogetstarted.com/reference/arduino-volatile
C standard atomics are a big, stinking trap, you really need to be careful with them.
This is AVR code which decreases a volatile char variable by one:
00000000 <atomic>:
0: 80 91 00 00 lds r24, 0x0000
4: 8f 5f subi r24, 0x01 ; 1
6: 80 93 00 00 sts 0x0000, r24
a: 08 95 ret
Now volatile int:
00000000 <atomic>:
0: 80 91 00 00 lds r24, 0x0000
4: 90 91 00 00 lds r25, 0x0000
8: 01 97 sbiw r24, 0x01 ; 1
a: 90 93 00 00 sts 0x0000, r25
e: 80 93 00 00 sts 0x0000, r24
12: 08 95 ret
These are not quite correct because they could be interrupted in the middle of the operation, but that can be resolved with cli/sei.
For some simpler code than the above, particularly if dealing with char rather than int, disabling interrupts may not even be necessary.
And now atomic_int:
00000000 <atomic>:
0: 0f 93 push r16
2: 1f 93 push r17
4: cf 93 push r28
6: df 93 push r29
8: 00 d0 rcall .+0 ; 0xa <atomic+0xa>
a: cd b7 in r28, 0x3d ; 61
c: de b7 in r29, 0x3e ; 62
e: 65 e0 ldi r22, 0x05 ; 5
10: 70 e0 ldi r23, 0x00 ; 0
12: 80 e0 ldi r24, 0x00 ; 0
14: 90 e0 ldi r25, 0x00 ; 0
16: 00 d0 rcall .+0 ; 0x18 <atomic+0x18>
18: 9a 83 std Y+2, r25 ; 0x02
1a: 89 83 std Y+1, r24 ; 0x01
1c: 00 c0 rjmp .+0 ; 0x1e <atomic+0x1e>
1e: 89 81 ldd r24, Y+1 ; 0x01
20: 9a 81 ldd r25, Y+2 ; 0x02
22: 48 2f mov r20, r24
24: 59 2f mov r21, r25
26: 41 50 subi r20, 0x01 ; 1
28: 51 09 sbc r21, r1
2a: 05 e0 ldi r16, 0x05 ; 5
2c: 10 e0 ldi r17, 0x00 ; 0
2e: 25 e0 ldi r18, 0x05 ; 5
30: 30 e0 ldi r19, 0x00 ; 0
32: 6c 2f mov r22, r28
34: 7d 2f mov r23, r29
36: 6f 5f subi r22, 0xFF ; 255
38: 7f 4f sbci r23, 0xFF ; 255
3a: 80 e0 ldi r24, 0x00 ; 0
3c: 90 e0 ldi r25, 0x00 ; 0
3e: 00 d0 rcall .+0 ; 0x40 <atomic+0x40>
40: 88 23 and r24, r24
42: 01 f0 breq .+0 ; 0x44 <atomic+0x44>
44: 0f 90 pop r0
46: 0f 90 pop r0
48: df 91 pop r29
4a: cf 91 pop r28
4c: 1f 91 pop r17
4e: 0f 91 pop r16
50: 08 95 ret
:scared:
You need to test for the ATOMIC_xxx_LOCK_FREE macro and stay the hell away from types for which it's not defined.
Without volatile, the compiler will "know" that x is 0 in main and may remove the "unnecessary" test for 1 (and x++) inside the loop.
Okay It means without volatile, the compiler doesn't look at the value of variable x inside ISR
With volatile, the compiler looks at the value of variable x in main and inside ISR
volatile int x;
void main ()
{
x = 0;
while (1)
{
if ( x == 1)
{
x++;
}
}
}
ISR ()
{
if(flag == 0)
{
flag = 0;
x = 1;
}
}
Okay It means without volatile, the compiler doesn't look at the value of variable x inside ISR
No. It means the compiler assumes the normal meaning of the code, as interpreted from the perspective of that code. In the following fragment, is x ever modified, if it’s not 1?
while (1) {
if (1 == x) {
++x;
}
}
Clearly not, there is no single operation that would modify anything. The code is exactly equivalent to:if (1 == x) {
x = 2;
}
while (1) {
}
That’s what the compiler sees without x being marked as volatile.
A side note: since there is an infinite loop in this code, the actual meaning of the code is undefined and the compiler may do anything it wishes: including never even generating any loop or simply “make demons fly out of your nose.”
With volatile, the compiler looks at the value of variable x in main and inside ISR
No. It means that the compiler knows the snippets of code shown above are not equivalent, because x may be “magically” modified despite it seems it can not. So it must assume that every read from x may produce a different value, even if it never sees any stores into x in this fragment.
Nope as said already. It's actually much simpler than this, and sort of the opposite. With volatile, the compiler doesn't try to analyze the usage of the corresponding variable, and will generate an access to it in any case.
That is not exactly true. It’s a bit nitpicky, but in the case of volatile such details are important and missing them is what brings pain.
With volatile the compiler assumes that any read from the variable may produce a different value, despite no modification to that variable can be seen. But volatile does not guarantee by itself generation of an access to the actual storage.
The difference is slight, but not understanding it has grave consequences. The presence of volatile tells the compiler that some natural assumptions about the meaning of code can’t be made, which leads to different interpretation of the described logic and in consequence missing some optimizations. But “generate an access” is a much stronger requirement. On simple platforms like small microcontrollers merely skipping an optimization does accidentally generate an access, but that is never true on modern, more complex architectures. Which include even any modern consumer CPU. With multilayer caches, complex CPU-level optimization, and multiple cores or processors it is required to use the right synchronization methods to ensure memory consistency. That includes explicit memory barriers, instructions offering such guarantees as side effects etc. volatile never introduces those. And it is usually not even needed if they are used. For example on any platform supported by pthreads using synchronization primitives alone induces the desired behavior. So is using atomics API of C.
And that’s only about reading a variable! I’m not even touching things like the order of execution. :D
Also note that volatile is not the only situation that prevents the compiler from making assumptions about reads. char pointers behaviour in the context of strict aliasing is another such example.
Remember that C will almost always favor efficiency over "correctness", and that some additions (such as the volatile qualifier) are there to manually instruct the compiler when the intent of the programmer can't be clearly conveyed without them. Just the way it works. If you want a language that favors correctness over efficiency in all cases, pick another one. :)
In this case correctness is not sacrificed at all. The behaviour without volatile is perfectly correct. The addition of volatile is not making that code more correct: it changes its semantics. Do not confuse “correct” with “what a programmer had on their mind”. While C is known to sacrifice safety and clarity for some types of performance gains, this is not the case here.