Is there a way to ensure that accesses are done by word?
Yes, don't use bit-fields but uint32_t. Which also removes loads of other problems caused by bit-fields, such an undefined bit order, byte order, alignment, padding, "storage unit boundaries", integer types actually supported, signed vs unsigned and so on. The root of all such problems is the pretty much non-existent standardization of bit-fields.
Instead use bit-wise operators and masking. You could just go my_var = THIS | THAT | ~(NOT_THAT); and that's likely just fine for most use cases. Keep it simple.
If we insist on a more advanced, fancy solution, we could cook up more detailed macros - this is purposely a quite verbose solution but I just want to show the options:
// positions of fields within the bit-field
#define MYVAR_FIELD1_POS 31
#define MYVAR_FIELD2_POS 28
#define MYVAR_FIELD3_POS 24
// sizes of fields
#define MYVAR_FIELD1_SIZE 1
#define MYVAR_FIELD2_SIZE 3
#define MYVAR_FIELD3_SIZE 4
// bit masks based on sizes
#define MYVAR_FIELD1_MASK ((1u << MYVAR_FIELD1_SIZE)-1)
#define MYVAR_FIELD2_MASK ((1u << MYVAR_FIELD2_SIZE)-1)
#define MYVAR_FIELD3_MASK ((1u << MYVAR_FIELD3_SIZE)-1)
// bit masks with a value shifted to the correct position
#define MYVAR_FIELD1(val) ( ((val) & MYVAR_FIELD1_MASK) << MYVAR_FIELD1_POS )
#define MYVAR_FIELD2(val) ( ((val) & MYVAR_FIELD2_MASK) << MYVAR_FIELD2_POS )
#define MYVAR_FIELD3(val) ( ((val) & MYVAR_FIELD3_MASK) << MYVAR_FIELD3_POS )
This is how many of the more properly written register maps for microcontrollers work. Now you can just write:
my_var = MYVAR_FIELD1(1) | MYVAR_FIELD2(5) | MYVAR_FIELD3(15);
and you'll get 0xDF000000 (1 | 5 in first nibble then 0xF in next nibble). Benchmarking the above on gcc x86 for example yields a single instruction:
mov DWORD PTR [rsp+12], -553648128
Where the magic number -553648128 = 0xDF000000.
Regarding single instruction, the above will boil down to an integer constant expression as long as we pass integer constants. If we pass variables, then it can't be calculated at compile time (bit-fields are no different here either). For such cases we should make a habit of using a temporary variable:
uint32_t tmp_var = MYVAR_FIELD1(x) | MYVAR_FIELD2(y) | MYVAR_FIELD3(z);
volatile my_var = tmp_var; // access the volatile qualified variable on a line of its own