So, recently in a job interview for a job as an embedded C developer the interviewer asked me:
What is the point of declaring
const volatile int *p;
?
The answer that I gave was that this was the case of a read-only status register. It's volatile because it can change unexpectedly and its const because the program should not be allowed to modify it.
After the interview, I was thinking about this and remembered that for the cases of status registers you may need declare the address of the pointer also "const", like:
const volatile int * const p;
Which is the correct answer?
CodePudding user response:
Your answer is correct. As far as I'm concerned you aced that one particular question, but your knowledge about the rest would reveal more.
And, you don't need the extra const
after the *
, but it does add extra meaning. It makes the pointer variable itself constant. Use it if you really don't want that pointer variable to ever point to some other const volatile int
memory block. And, to make it point to any memory block at all, you'd need to initialize it to a desired value at definition time.
Examples:
const volatile uint32_t * const ptr6 = (const volatile uint32_t *)0x01234567UL;
ptr6 = something_else; // not allowed! ptr6 itself is const!
// vs
const volatile uint32_t * ptr5 = (const volatile uint32_t *)0x01234567UL;
ptr5 = something_else; // this is fine
But, let's talk about the various cases you could do, and what each means.
First off, note that registers are usually defined in macros, like this, for example (see more on this in my STM32 microcontroller answer here):
// Note that the size of the addresses below is equal to the size
// of a ptr, or `sizeof(void*)`, for instance, for your architecture.
// 1. read-writable (rw) register
#define RW_REGISTER (*(volatile uint32_t *)(0x01234567UL))
// 2. read-only (ro) register
#define RO_REGISTER (*(const volatile uint32_t *)(0x01234567UL))
// Example usage
RW_REGISTER |= (1 << 7); // set the 8th bit
RW_REGISTER &= ~(1 << 7); // clear the 8th bit
bool bit_value1 = (RW_REGISTER >> 7) & 0x1; // read the 8th bit
bool bit_value2 = (RO_REGISTER >> 7) & 0x1; // read the 8th bit
The UL
ensures the address is interpreted as unsigned long
so that no precision is lost. This is required if your architecture's uint32_t
value is unsigned long
.
The (volatile uint32_t *)
casts an arbitrary number to a pointer to a uint32_t
block of memory which is volatile, meaning it could change at any time and the compiler can make no assumptions about its state during compiler optimizations, and therefore should read it every time.
The *
to the left of that cast dereferences that pointer to read out the uint32_t
from it, so you can read or write to it like you can a normal variable, as I show above.
The const
in (const volatile uint32_t *)
says that the uint32_t
block of memory being pointed to is constant, as you said, and cannot be changed. It is "read-only".
Now, let's look at these ptr variables. After I started with the question you were asked, I'd have gone to the register defines above, then gone to these pointers. Let's talk about them and what they mean. There are several more variations than shown here, using const
and volatile
, but this is sufficient to get the points well-understood I think:
uint32_t * ptr1;
const uint32_t * ptr2;
uint32_t * const ptr3;
const uint32_t * const ptr4;
const volatile uint32_t * ptr5;
const volatile uint32_t * const ptr6;
const volatile uint32_t * const volatile ptr7;
ptr1
points to a block of memory containing auint32_t
. You can change the bytes in that memory (it is notconst
). The compiler can make assumptions about that memory in its optimizations (it is notvolatile
), so the compiler can assume that if something just wrote0xff
to a byte there, then a few steps later, it is still0xff
and didn't magically change by itself during that time.ptr2
points to a constant block of memory containing auint32_t
. You can NOT change the contents of that memory, as it is read-only. The compiler can assume the contents always are the same, and optimize as such.ptr3
is a constant pointer to NON-constant memory. You can change the contents of the memory. You just can't change whatptr3
itself points to is all, as the pointer itself, not what it points to, is constant.ptr4
is a constant pointer that cannot be changed, to a constant block of memory that also cannot be changed.ptr5
is a non-constant pointer to a constant block of memory about which the compiler can NOT make optimizations or assumptions. In other words, the compiler must read that memory every time it needs its value, since it isvolatile
.volatile
means: "this memory can change at any time for any reason by some process, thread, or hardware mechanism the compiler isn't even aware of." So, if the compiler needs its value, it has the processor re-read it each and every time. But, since that block of memory is constant, the compiler will only allow you to read it, not change it, even though external hardware or whatever can change it. Like you said, this is a read-only status register, perhaps.ptr6
also makes theptr6
variable itself constant, so you can't reassign that variable to some otherconst volatile uint32_t *
memory space. Withptr5
, you could! This is used if you want, just like when you'd set a normal variable to be constant.ptr7
also makes the ptr itself volatile, meaning that the compiler has to read theptr7
variable itself each time to ensure the address it points to hasn't changed, since it is alsovolatile
and could change at any moment without warning. This is unusual, and implies that theptr7
variable itself is a value that could change at any moment by some other context, such as an interrupt service routine (ISR) or another thread which will periodically update the address stored inside theptr7
variable without the knowledge of the thread or context in which your definition above is running. This would be very unusual to do, but not something that would "never" be used.
Read more here: https://embeddedgurus.com/barr-code/2012/01/combining-cs-volatile-and-const-keywords/