Don't want to repeat what others have said: but if you really want to dive into this problem and know what a compiler is doing.. post the code.. post the assembly listing. There is even a website for reviewing compiled code with many many GCC/CLang compilers, including crosscompilers:
https://godbolt.org/Here is an example to a simple toggle I/O program (which I assume is similar to what a blinky you've made -- but this is completely assumption based):
https://godbolt.org/z/KrzYE3ffx#include <cstdint>
volatile uint32_t* port = reinterpret_cast<uint32_t*>(0xCAFECAFE);
int toggle() {
while (1) {
*port = 1;
}
}On the Godbolt link you can play around with the compiler setting. The assembly output on O2 is:
toggle():
ldr r3, .L4
ldr r2, [r3]
movs r3, #1
.L2:
str r3, [r2]
str r3, [r2]
b .L2
.L4:
.word .LANCHOR0
port:
.word -889271554
Label L2 is the while loop. In the compilers infinite wisdom, it has unrolled the loop twice. You could try compiling for size (-Os in GCC), and see that it then only does 1 store to save on code size. I presume you then get a 50% 667kHz square wave again, as a 1 store (1 cycle)+1 branch(2 cycle), with 2 toggles, results in 6 cycles. 4M/6=667kHz.
I do agree with SiliconWizard though; it gets incrementally harder to predict how CPUs run code these days. Seemingly simple code can even lead to odd behaviour. This story is perhaps a bit off-topic on Simon's program, but something I encountered last week that was also heavily confusing with compiler settings.
I was debugging a timebase. A hardware timer is extended in software with a 64-bit integer value. The live timestamp is calculated with the hardware and software value added together. Example:
https://godbolt.org/z/76GGM8qT7The timer runs at high speed, which is the reason to use 64-bit ints. This code looks quite simple and works... The code waits till multiples of 0x100000 cycles have passed. If 'target' is a multiple of 0x100000, then obviously (N * 0x100000)&0x10000 will always be 0? Not quite.. this code will hang at some point on C-code line 25. If you look in the assembly listing at line 5 and 6, there is a 1 cycle opportunity for register `CNT` to be loaded as 0xFFFF on line 5, which would overflow and fire the IRQ
before line 6. Now the variable time is loaded, which was just incremented by 0x10000, and so 0xFFFF has been counted twice. 1 instruction doesn't sound like a big deal. What are the odds? Well.....
- I could add (one) extra line of debug or computation statement in foo() (e.g. toggle a LED), and the problem would disappear (or reappear).
- I could shift some lines of code in foo() around, and the problem would disappear or reappear.
- I could enable the CPU cache (was a Cortex-m7 chip), and the problem would disappear, mostly.
- I could flush the instruction buffer, and the problem would appear again, mostly.
- I could influence the problem with compiler settings, as well, with varying results.
It's like probing your circuit with your oscilloscope will stop it resonate. The actual fix was to deal with the race condition on variable time.
The morale of my story is to not expect the compiler to be some magic tool that fixes your code or will always make it run fast, or the way you want it to run. In my example, if it would have loaded `time` before CNT, then this problem would not occur. But since volatile is not "magic", and is allowed to be reordered (among other non-volatile values that are manipulated), you can get some annoying bugs. An more aggressive (optimizations) compiler can lead to weird-er behaviour. The worst of all is that from the C-code perspective everything looks like predictable C code (as in: trivial statements), but how it compiles and runs on the hardware varies a lot (especially on the m7 with caches, dual issue, etc.). So best to look at assembly output, and assume the worst
will at some point happen.