Question

Volatile access of struct member

If accessing a struct member marked as volatile from an interrupt, does the whole chain of access need to be marked as volatile ?

For example

struct bar
{
  volatile uint16_t a;
  volatile uint16_t b;
};

struct foo
{
  struct bar bar1;
  struct bar bar2;
};

struct foo foo_arr[10];

void interrupt_handler(void)
{
  struct bar *bar = &foo_arr[0].bar1;
  bar->a = 2;
  bar->b = 3;
}

Will it be enough for the members a and b to marked as volatile ?

 4  187  4
1 Jan 1970

Solution

 5

Put yourself in the shoes of a compiler.

First, let's decompose the &foo_arr[0].bar1 expression syntactically, in its order of precedence:

  • foo_arr is the name of your array
  • foo_arr[0] accesses an offset (0) and extracts a value
  • foo_arr[0].bar1 accesses a fixed offset within the array element and extracts a value
  • &foo_arr[0].bar1 returns the address of the last accessed value

At that point, you are having your local bar pointer. Nothing on that path is marked volatile and you don't need it there. The array itself is allocated statically and its location cannot suddenly change. It's a static layout of size 10 * sizeof(struct foo).

The expressions bar->a and bar->b are when you actually access stuff in that area. The compiler will generate code to dereference the pointer and then access a fixed offset from there. Since the a and b members are marked volatile, a memory access instruction should be generated with the expression.

2024-07-04
Blagovest Buyukliev

Solution

 3

Normally, it is often problematic to specify qualifiers on individual struct members rather than on the struct object. In this case, since every single member is volatile, you could as well have done volatile struct bar bar1, allowing non-volatile instances of the struct to exist as well.

However, in case a and b happens to be memory-mapped CPU registers, there exists no situation where they aren't volatile so by qualifying the individual members as such, you prevent any other use of them which is a good thing in that particular scenario. There would be no harm of making the struct volatile as well though.


As for the use of these variables, the only thing that matters is the type used for the "lvalue access" of the member object, which must be done through a correctly qualified type. For example * (int*)&bar->a would invoke undefined behavior - same thing if you used const in the declaration.

A compiler can't assume anything about this struct either, it must treat it as volatile all the way. It would however have been much clearer and self-documenting to the programmer if it was explicitly declared volatile at every turn. Not declaring something volatile when the intention is to treat it as volatile is simply bad programming style. But as far as the C language cares, that's not necessary.

You can easily test this: https://godbolt.org/z/rxqPe8abx Comment out volatile and the (x86_64 port) compiler optimizes the writes into a single 32 bit write, which wouldn't be conforming for 2 volatile variables. With volatile we do get 2 separate 16 bit writes, which is required by C's rules of program execution ("the abstract machine").

2024-07-04
Lundin