Home > Net >  What is the point of declaring "const volatile int *p"?
What is the point of declaring "const volatile int *p"?

Time:07-20

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;
  1. ptr1 points to a block of memory containing a uint32_t. You can change the bytes in that memory (it is not const). The compiler can make assumptions about that memory in its optimizations (it is not volatile), so the compiler can assume that if something just wrote 0xff to a byte there, then a few steps later, it is still 0xff and didn't magically change by itself during that time.
  2. ptr2 points to a constant block of memory containing a uint32_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.
  3. ptr3 is a constant pointer to NON-constant memory. You can change the contents of the memory. You just can't change what ptr3 itself points to is all, as the pointer itself, not what it points to, is constant.
  4. ptr4 is a constant pointer that cannot be changed, to a constant block of memory that also cannot be changed.
  5. 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 is volatile. 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.
  6. ptr6 also makes the ptr6 variable itself constant, so you can't reassign that variable to some other const volatile uint32_t * memory space. With ptr5, you could! This is used if you want, just like when you'd set a normal variable to be constant.
  7. ptr7 also makes the ptr itself volatile, meaning that the compiler has to read the ptr7 variable itself each time to ensure the address it points to hasn't changed, since it is also volatile and could change at any moment without warning. This is unusual, and implies that the ptr7 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 the ptr7 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/

  • Related