Cast volatile array to non volatile array

Viewed 1226

I have a global volatile unsigned char array volatile unsigned char buffer[10] to which data is written in an interupt. I have a function that takes an unsigned char * and stores that value to hardware (EEPROM) void storeArray(unsigned char *array), in this example the first three values. Is it safe to cast the volatile array to a non-volatile array like so?

store_array((unsigned char *) buffer);

I read the following, which I do not quite understand, but which concerns me:

6.7.3:5 If an attempt is made to refer to an object defined with a volatile-qualified type through use of an lvalue with non-volatile-qualified type, the behavior is undefined.

Does this affect my code?

Then I have this followup question: The buffer array only has a section of data I want to store (can't change that), for this example beginning with the third value. Is it legit to do the following?

store_array((unsigned char *) buffer + 3);

If it is, how is the cast affected, if 3 is added to the array? BR and thank you!

EDIT: @Cacahuete Frito linked a very similar question: Is `memcpy((void *)dest, src, n)` with a `volatile` array safe?

4 Answers

Yes, the standard quote you've posted covers precisely what you're trying to do. By doing the cast, you're pretending the objects in the array are unsigned char when they're actually volatile unsigned char, so inside the function, you're referring to volatile object through an lvalue without a volatile qualifier. Undefined Behaviour.

If you cannot change the function storeArray, you will have to copy the data from the volatile array to a non-volatile one before passing it to the function.

Regarding the second question: the pointer arithmetic is fine, it will simply convert buffer to an unsigned char* and then add 3 to the resulting pointer, pointing to buffer[3] (but with the wrong qualification).

You have found the correct section of the standard, this code leads to undefined behavior.

A function writing something "to hardware" should probably have a volatile-qualifier parameter, depending on what "hardware" is. If it is a memory-mapped register, a DMA buffer or non-volatile memory, then the parameter should definitely have been volatile unsigned char* (or optionally, volatile uint8_t* which also is to be regarded as a character type).


Details: C allows us to iterate through any chunk of data using a character pointer, C17 6.3.2.3/7:

When a pointer to an object is converted to a pointer to a character type, the result points to the lowest addressed byte of the object. Successive increments of the result, up to the size of the object, yield pointers to the remaining bytes of the object.

The part you quote about accessing a "lvalue" refers to accesing data through a different pointer type than what's actually stored in that location. Plainly: no matter how much you cast various pointers pointing at it, the actual data retains its original type.

Accessing the data through the wrong pointer type is normally not even allowed, but again character access is a special exception to the "strict aliasing rule", C17 6.5/7:

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:
...
- a character type.

So you can access any kind of data through a character pointer, but if that pointer is not volatile-qualified, you invoke undefined behavior as per the part you quoted, C17 6.7.3/5.

In practice, using a non-volatile pointer type could cause the compiler to optimize the access in unexpected ways. So this isn't just theoretical "language-lawyering", you could in practice get very strange code generated with optimizations enabled. Lots of very hard to find bugs in embedded systems originate from such a missing volatile.


Regarding your follow-up question, the cast and the buffer + 3 changes nothing: you are still dealing with a character pointer without volatile qualifier - same type. The actual data remains of type volatile unsigned char, so you can't access it from the function through a unsigned char*.

  1. If the array is changes in interrupt you need to provide a mechanism to acces and modify it atomic way. If you don't any RW or RMW operation may be unsuccessful and the data inconsistent.

  2. You access volatile data make the f=unction parameters volatile as well. storeArray(volatile unsigned char *) and no cast will be needed. The cast only removes the warning. Even you pass non-volatile data to it, it will work as well.

As you found, you're relying on "undefined behavior". However, depending among other things on the separation of compilation units (and things like "whole-program-optimization" (WPO)) it will probably work. In most cases, the compiler (at least gcc) is not "smart enough" to optimize array accesses across functions in different compilation units. That said, the clean, safe and portable way would be to copy the array, making the dependency of the non-volatile array's values on the volatile ones visible to the compiler.

Related