0

I'm supposed to write multiple fields in a register. The vendor "API" is a struct mapped to a memory address. The struct looks something like this:

typedef struct
{
    unsigned Reg_A : 28;
    unsigned Reg_B : 1;
    unsigned Reg_C : 3;
}RegisterFooFields;

typedef union
{
    unsigned U;
    RegisterFooFields B;
}RegisterFoo;

I'm supposed to write all of A, B, and C atomically, as one constrains the other. Does C guarantee that to happen if I assign to RegisterFoo.B?

RegsiterFooFields fields = {.A = 123, .B = 0, .C = 0b10};
RegisterFooAddress->B = fields; /* Is this an atomic write (mov) operation? */

My environment is C99, 32-bit.


EDIT:

RegisterFooAddress is declared volatile.


EDIT 2:

What I'm afraid for is the compiler generating a series of assignments for each field, masked appropriately, resulting in a transient register state where (e.g.) Reg_A has been updated but Reg_B and Reg_C have not, leading to the hardware possibly reading the transient state.

Reading the generated assembly is not feasible in case of aggressive optimization, possibly inlining the code.

Andreas
  • 5,086
  • 3
  • 16
  • 36
  • 1
    No it is not atomic operation. like increment (++) asignement is not atomic. Atomics as part of the C language are an optional feature that is available since `C11`. – Adam Jan 29 '21 at 08:47
  • And even if it's implemented atomically, you can't be sure of the visibility of any updates, nor the ordering of that visibility when compared to other updates you're making. And then, if you don't use proper synchronization, an optimizing compiler might reorder things on you anyway. Don't try to cheat when doing multithreaded programming - if you do and you start getting [Heisenbugs](https://en.wikipedia.org/wiki/Heisenbug) you will ***never*** solve them. – Andrew Henle Jan 29 '21 at 09:01
  • C does not guarantee that, but your platform C compiler may. Note that type punning in C using a union (even though very common in low-level code) is undefined behavior in C99. See "casting through a union(1)" in https://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html – Paul Hankin Jan 29 '21 at 09:01
  • @AndrewHenle I agree with what you say, but I think the use of the word "atomic" in the question is confusing and the question is not about concurrency or synchronization but that the _hardware_ requires an "atomic" write of the word. I think that means Andreas wants to be able to guarantee that `*x = 42u` turns into a single machine instruction when `x` is an `unsigned*` that points to the special hardware address. – Paul Hankin Jan 29 '21 at 09:08
  • 1
    `that type punning in C using a union is undefined behavior` is it? Doesn't this footnote: https://port70.net/~nsz/c/c11/n1570.html#note95 specifically like "allow" it? uff- https://stackoverflow.com/questions/25664848/unions-and-type-punning :p – KamilCuk Jan 29 '21 at 09:11
  • I believe industry standard is just to assume the code that's in the question just works, even if the C standard is clear that it's not guaranteed. A purist would write some platform-specific assembly to poke the value in the address and use that. – Paul Hankin Jan 29 '21 at 09:12
  • @PaulHankin spot on. – Andreas Jan 29 '21 at 09:14
  • 2
    The C standard is clear on the *union access*, you can access the different members of the union now; the standard is *less clear* on if you use a union pointer to access an object with a non-union effective type – Antti Haapala -- Слава Україні Jan 29 '21 at 09:15
  • what target hardare? – 0___________ Jan 29 '21 at 10:19
  • Since the code is pretty platform specific, you could look at the generated machine code to see if it is atomic or not. But from a C point of view, it seems inherently safer to assign to `RegisterFooAddress->U` instead of `RegisterFooAddress->B` in case the compiler generates `memcpy` type code for structure assignment. – Ian Abbott Jan 29 '21 at 10:32
  • @PaulHankin *I agree with what you say, but I think the use of the word "atomic" in the question is confusing and the question is not about concurrency or synchronization but that the hardware requires an "atomic" write of the word* The ***reason*** questions like this get asked is because something other than the thread doing the update is hoping to get an atomic result. *I believe industry standard is just to assume the code that's in the question just work* Unless you're on a platform that guarantees that such code is atomic, that's an awfully ***low*** standard to code to. – Andrew Henle Jan 29 '21 at 11:15
  • (cont) And even then, atomicity isn't enough. – Andrew Henle Jan 29 '21 at 11:21
  • @PaulHankin: Type-punning through a union is not undefined in C 1999. See KamilCuk’s comment above. – Eric Postpischil Jan 29 '21 at 12:08
  • 1
    @EricPostpischil yes, I stand corrected. I should take my own frequently-given-out advise and check the actual C standard before spouting opinions :) – Paul Hankin Jan 29 '21 at 12:21

2 Answers2

2

Does C guarantee that to happen if I assign to RegisterFoo.B?

No, it does not. There is no such guarantee.

But it may be possible that on your platform your compiler compiles it to one single instruction, making it "atomic". Check your compiler documentation, check the generated assembly.

To be sure, I would write a code with volatile access. I usually expect volatile accesses to be "atomic" in the sense that they are compiled to a single mov instruction if the architecture supports it, like:

void RegisterFoo_assign_RegisterFooFields(RegisterFoo *reg, RegisterFooFields fields) {
      static_assert(sizeof(RegisterFooFields) == 4, "");
      static_assert(sizeof(RegisterFoo) == 4, "");
      static_assert(sizeof(unsigned) == 4, "");
      static_assert(_Alignof(unsigned) == _Alignof(RegisterFoo), "");
      unsigned var;
      memcpy(&var, &fields, 4);
      *(volatile unsigned*)reg = var;
      // or really just:
      *(volatile unsigned*)&reg->U = var;
}

The generated assembly from gcc looks nice on godbolt.

KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • Edited question, reg is declared volatile by the API. – Andreas Jan 29 '21 at 09:53
  • 1
    `volatile` does not guarentee any atomicity or coherency of operation – 0___________ Jan 29 '21 at 10:44
  • But even if it is atomic, that's not enough. Visibility and ordering matter. And none of the code in this answer addresses that - because it can't be addressed in a platform-independent, portable manner unless you drag in threading from later C standards. And even then I'm pretty sure those are only optional. Handwaving such concerns away is not a good way to get reliable code. – Andrew Henle Jan 29 '21 at 11:19
  • @AndrewHenle indeed. This answer is a "naive" attempt to find a simple answer to the complex question. And shows common myths and misunderstandings. – 0___________ Jan 29 '21 at 11:35
  • @AndrewHenle It is hardware reading the register, not another thread. Sorry for not making myself clearer. – Andreas Jan 29 '21 at 11:43
  • @Andreas as in my answer: you need to protect access with barriers and write and read using needed data widths. Many hardware distinct accesses - for example periptherals with FIFO. If the data size is 8 bits and you write using the 32bit access the hardware will add 4 bytes to the fifo - not the one as expected. Very common beginners problem when using STM32 SPI – 0___________ Jan 29 '21 at 11:51
  • @AndrewHenle: Visibility does not matter because the question does not ask about multiple threads (or other separate executions of something) observing the write, just about sending the desired command to hardware. And it is not clear what ordering is needing; no other action is shown in the question that would need to be ordered relative to the write to the hardware register. – Eric Postpischil Jan 29 '21 at 12:58
  • @EricPostpischil *Visibility does not matter because the question does not ask about multiple threads (or other separate executions of something) observing the write* Really? Write-only memory or write-only hardware then? If there's not something else observing the write, there's no purpose in doing it. – Andrew Henle Jan 29 '21 at 13:28
  • @AndrewHenle: Re “Write-only hardware then?”: What do you think the pixels on your monitor are? – Eric Postpischil Jan 29 '21 at 13:30
  • 1
    @EricPostpischil How about you reorder the writes to all those pixels in a timeframe visible to the human eye then? – Andrew Henle Jan 29 '21 at 13:31
0

If we consider hardware registers mapped into the address space and hardware specification does not guarantee the strong ordering of the accesses there is no mechanisms in the C language to guarantee coherency even if the processor uses instruction which is atomic.

So the atomicity does not guarantee that the return value of this function will be the same as its parameter

volatile uint32_t *reg = (void *)0xE0000454; // some address of the hardware register

uint32_t writeAndRead(uint32_t val)
{
    *reg = val;
    return *reg;
}

In this case, some barriers have to be used. Sometimes it is enough to use Core instructions, sometimes more complex solutions are necessary.

static inline __attribute__((always_inline)) void __DSB(void)
{
  asm volatile ("dsb 0xF":::"memory");
}

uint32_t writeAndRead1(uint32_t val)
{
    *reg = val;
    __DSB();
    return *reg;
}
0___________
  • 60,014
  • 4
  • 34
  • 74
  • The OP is about whether assigning the OP struct is an atomic write or whether fields may be written in a non-atomic fashion. I still have a way to go in writing clear question it seems. – Andreas Jan 29 '21 at 12:09
  • Thought it meant "Original Post". And yes, we do. – Andreas Jan 29 '21 at 12:33
  • Are you saying `*reg = val; return *reg;` is not guaranteed to return the parameter value because `*reg` is volatile and therefore could be changed? Because, otherwise, you seem to be saying `return *reg` might not see the value written by the assignment because the modification is not guaranteed to be observable without the barrier. That is not true within a single thread; a single thread’s view of memory is coherent. Otherwise a data barrier would be required after essentially every memory modification in every program. – Eric Postpischil Jan 29 '21 at 12:54
  • Regarding the use of a `dsb` instruction, did OP indicate somewhere they are using a particular architecture? Also note that `__DSB` is in C’s reserved name space. Toolchains might use this name, but user programs should avoid it. – Eric Postpischil Jan 29 '21 at 12:56
  • @EricPostpischil complaint to ARM (but I do not think that they will care). I sse the same name as in the CMSIS as CMSIS is in very common use. https://github.com/ARM-software/CMSIS_5/blob/develop/CMSIS/Core/Include/cmsis_gcc.h – 0___________ Jan 29 '21 at 13:05
  • ***`Regarding the use of a dsb instruction`*** @EricPostpischil it is an example – 0___________ Jan 29 '21 at 13:08
  • An example of what? What could make `*reg = val; return *reg;` fail to return `val`? – Eric Postpischil Jan 29 '21 at 13:11
  • ARM is not at fault here: As I wrote, toolchains may use the name. ARM is taking the responsibility of coordinating the names in its tools with the names used by the compiler and libraries they support or otherwise indicate for use with their products. In effect, they become part of the C implementation, and the names are reserved for the C implementation. ARM’s use of a reserved name is fine and proper. Using it in user code is not. You should not be suggesting that users should write code that uses these names. And there is no reason to: No benefit is gained, and changing the name is easy. – Eric Postpischil Jan 29 '21 at 13:16
  • @EricPostpischil `As I wrote, toolchains may use the name` you wrote but you do not write the standard. Standard does not say that authors of the toolchain may use it. `No benefit is gained` - disagree. I showed the function from the CMSIS. Normally CMSIS will be used and someone will know that such a function exists. I see it beneficial. IMO it simple a nitpick to DV the answer – 0___________ Jan 29 '21 at 16:01
  • Re “Standard does not say that authors of the toolchain may use it”: Yes, it does, because the toolchain is a set of things (compiler, linker, libraries, and such) that form or contribute to the C implementation. The C standard says that if a C program declares or defines an identifier in a reserved context, the behavior is undefined. It does not say this for things that are part of the implementation rather than part of a program. Toolchains deliberately use `__` prefixes to keep identifiers out of the way of user programs. – Eric Postpischil Jan 29 '21 at 16:26
  • And your answer was voted down primarily because it falsely states the `writeAndRead` function may return a value other than that of the parameter, unless it is intended to make some point about modifications from some other source that are not explained. It is misinforming people. – Eric Postpischil Jan 29 '21 at 16:27
  • @EricPostpischil are you serious? When buss access in not strongly ordered then if write was not propagated through the bus, the read of the same location may not reflect that previous write, And it is a **very common** issue. – 0___________ Jan 29 '21 at 16:33