Where should the "volatile" go? Is it enough to put it in the initial declaration of addr i.e.
volatile uint32_t addr=0x08000000;
I can't understand how a compiler could optimise out "addr" however, given that the address being read is "obviously" continually changing (incremented by 512 each time).
Yeah. Your question shows that understanding "volatile" is much trickier than it looks for many people.
Possibly you're confusing the "address" and the read operation itself. Possibly same with the pointer.
For the code in your screenshot, there's still one potential pitfall. But it's not in the piece of code you're showing us. It's with the AT45dbxx_WritePage() function you're calling, and it's possibly yet another related problem that we haven't touched quite yet.
Assuming this function was defined within the same compilation unit (which typically means, either within the same source file, or in a source file itself included in this source file), the compiler could decide, depending on how the function is written, that it may have NO effect whatsoever when called with a pointer which points to no known object, and thus decide to optimize the call out entirely. The possible side-effect would be that 'addr' itself would never get incremented, unless you use the 'addr' value after the 'for' loop. If you don't, the optimizer may not even actually generate any code for addr. It would for page though, because page is used in an 'if' condition which itself has effects. Assuming the KDE_LED_xx() functions themselves are not optimized out. Are you starting to like it?
An example of function, used in place of AT45dbxx_WritePage(), that could trigger this behavior, is memset() or memcpy(). If you called memset() with a pointer to something converted from an int (some 'address' to something that's not known by the compiler), the call to memset() could be optimized out. Reason is that the compiler may assume that this call would have no effect.
One way to circumvent this is to write your own memset(), or memcpy() function. With a pointer to volatile as parameter. The std ones don't have the volatile qualifier in their parameters. This is something that can bite your ass here.
So for instance, for an "always has an effect" version of memset(), regardless of what you call it with, you would need to write your own.
The original std one has the following prototype: "void *memset(void *str, int c, size_t n)".
Yours should have this one: "void *my_memset(volatile void *str, int c, size_t n)".
A common trap is believing that merely casting the passed pointer to memset() with a volatile qualifier will do the trick, such as: "memset((volatile void *) p, ..., ...)". It won't, for the reason explained above about your pointer casts. This is actually an example I remember was discussed in another thread on this forum.