I'm rewriting an interrupt based I2C driver to avoid internal fixed buffer sizes. To achieve this, I'm passing a buffer pointer into the driver for it to use. This is simple for the transmit case as the data is fixed, but for the receive case the buffer needs to be volatile.
As I understand it this requires the original declaration of the buffer to be volatile, but if it is not declared as volatile GCC does not complain. This has led to some questions about passing a non-volatile variable pointer to a function expecting a volatile variable pointer
The basic structure of the receive code is as follows. (Greatly simplified)
driver.c
volatile uint8_t* buff_ptr;
volatile size_t buff_index;
size_t num_bytes;
volatile bool complete;
void receive_data(volatile uint8_t* buff, size_t length)
{
buff_ptr = buff;
buff_index = 0;
num_bytes = length;
complete = false;
// Code to start transfer
while (!complete) {}
}
ISR()
{
switch (state)
{
case DATA_RECEIVED:
{
buff_ptr[buff_index++] = DATA_REGISTER;
if (buff_index >= num_bytes)
{
complete = true;
// Code to end transfer
}
break;
}
// Other states
}
}
main.c
int main()
{
// Correctly typed usage
volatile uint8_t volatile_buff[10];
receive_data(volatile_buff, sizeof(volatile_buff));
printf("V: %s", volatile_buff);
// GCC doesn't complain that "normal_buff" is not volatile.
// This might cause problems
uint8_t normal_buff[10];
receive_data(normal_buff, sizeof(normal_buff));
printf("N: %s", normal_buff);
}
Questions
-Wall -Wextra -pedantic
) doesn't complain that a non-volatile variable is passed as a pointer to a function expecting a volatile variable pointer. Why is this?receive_data()
to accidentally pass it a non-volatile variable pointer. Is there a way to enforce that the variable must be volatile?Since the driver code always waits for the transfer to complete before returning, I'm planning to shift the state machine out of the interrupt and into the while loop. This should remove the need for volatile qualifiers entirely. However, I'm still interested in the above questions for academic purposes.
Based on the answers provided so far, (particularly Eric Postpischil and Lundin), I've done some testing with various options and looking at the (dis)assembly. Given the following new version of main()
:
int main(void)
{
uint8_t main_buff[2] = { 1, 2 };
PORTA = main_buff[1]; // PORTA is a hardware register
receive_data(main_buff, sizeof(main_buff));
PORTA = main_buff[1]; // PORTA is a hardware register
return 0;
}
If receive_data()
is in a different translation unit (source file) and link time optimisation is disabled, upon returning from receive_data()
, main_buff
is always read from memory like this regardless of if main_buff
is volatile
or not in main()
:
PORTA = main_buff[1];
ldd r24, Y+2 ; 0x02
out 0x02, r24 ; 2
However, if link time optimisation is enabled the compiler starts to assume that the function didn't change anything despite buff
and buff_ptr
both being volatile
inside receive_data()
. If main_buff
is not volatile
in main()
it assumes main_buff
still has its initial value and we get the following upon returning from receive_data()
:
PORTA = main_buff[1];
ldi r24, 0x02 ; 2
out 0x02, r24 ; 2
So it seems that if link time optimisation is disabled, having code in separate source files is sufficient to prevent unwanted optimisation and declaring main_buff
as volatile
in main()
is not required. However, if link time optimisation is enabled main_buff
must be volatile
in main()
to prevent potential optimisation.
What happens to a non-volatile variable which is passed as a pointer into a function expecting a volatile variable pointer?
That is fine and valid C. You can always add a type qualifier like volatile
or const
, but you can't get rid of one (or risk poorly-defined behavior). It is indeed treated as volatile
from there on, inside the function.
Does the compiler see that the function expects a volatile and assume that its contents might be changed unexpectedly inside the function, therefore avoiding optimisation around that function call even though the variable itself is not declared as volatile?
Normally, compilers cannot optimize function contents if that function is part of another translation unit. In this code main.c + all included headers form one translation unit and driver.c + all its included headers form another one.
So it probably won't be able to do any optimizations regardless. Inlining local functions with internal linkage would be another story. And then there's link-time optimization but that's mostly about removing functions from the binary which are never called anywhere.
Also note that in case of volatile type* name
, the pointer itself isn't volatile and may be stored in for example an index register. So that shouldn't affect calling convention. Unlike volatile type* volatile name
which may force the compiler to stack allocate the pointer and then it changes the necessary calling convention.
AVR GCC 8.3.0 (with -Wall -Wextra -pedantic) doesn't complain that a non-volatile variable is passed as a pointer to a function expecting a volatile variable pointer. Why is this?
Because that's valid standard C. Specifically, this is covered by the rules of the assignment operator (parameters are passed to functions "as per assignment"), see C23 6.5.17.1:
One of the following shall hold:
...
- the left operand has atomic, qualified, or unqualified pointer type, and (considering the type the left operand would have after lvalue conversion) both operands are pointers to qualified or unqualified versions of compatible types, and the type pointed to by the left operand has all the qualifiers of the type pointed to by the right operand;
What this means in plain English is that during assignment left = right;
, then left
must have at least all qualifiers (const
, volatile
) that right
got. But right
does not need to have all qualifiers that left
got.
Exactly the same rule applies when calling a function: void func (volatile* type left)
... func(right);
.
It would be very easy for a user of receive_data() to accidentally pass it a non-volatile variable pointer. Is there a way to enforce that the variable must be volatile?
That is not an issue. If you want strict type safety, then yeah it is possible in modern C, but involves writing some awkward wrapper macros, for example (C23):
void func (volatile int* x);
#define IS_VOLATILE(x) _Generic((x), volatile typeof_unqual(*x)*: 1, default: 0)
#define STATIC_ASSERT(expr, msg) (void)((struct{int dummy; static_assert(expr,msg);}){}.dummy) // dummy compound literal
#define func(x) (STATIC_ASSERT(IS_VOLATILE(x),"parameter was wrong type or not a pointer to volatile"), func(x))
int main (void)
{
int* x;
func(x);
volatile int* y;
func(y);
}
This should remove the need for volatile qualifiers entirely
Please note that the only time you need to use volatile
here is when a variable is shared between the ISR and the background program. And when that sharing happens in "run-time", ie it's not a problem to have the init function share a non-volatile variable with the ISR because the ISR won't even be active when the init function is called (unless the program design is wrong).
As for the function, it only needs volatile
qualified parameters in case you intend to pass pointers to hardware registers to it, but then it's volatile
for another reason.