-1

I am a junior embedded software, and I spend most of my time writing C code for dspic33 microcontrollers.

My embedded code is timer interrupts based, with a couple of global variables. Let's consider the simplest code possible:

typedef struct data_t
{
    uint16_t a;
    uint16_t b;
} data;

data my_data = {0, 0};
int16_t main(void)
{
    my_data.a = 10;

    while(1)
    {
        // can access and modify my_data here
    }
}

and from another file:

extern my_data data;
void timer_interrupt()
{
    if (my_data.a == 10)
    {
        // stuff here
    }
}

This code works fine, but if I add scope control on my_data by replacing the variable declaration by:

static data my_data = {10, 10};

my_data is not accessible outside the main file anymore. So cool, I can create a global pointer and continue to access my_data member anywhere:

static data my_data = {10, 10};
data * const my_data_ptr = &my_data;

// from another file
extern data * const my_data;
void timer_interrupt()
{
    if (my_data_ptr->a == 10)
    {
        // do stuff here
    }
}

I prevent eventual reassignment by making the pointer constant, and I am sure my_data_ptr will point to the my_data's address for the entire program lifetime. So far, I meet all the requirements - my personal good practice requirements - I limit the scope of my global variables and preserve the variables' integrity.

Now let's imagine two functions, with requirements I define that function one needs read-only my_data and function two needs a read and write on my_data, something like:

// read-only access
void function_one()
{
    const data * const local_my_data_copy = my_data_ptr;
    
    if (local_my_data_copy->a == 10)
    {
        // do stuff here
    }
}

// read and write access
void function_two()
{
    data * local_my_data_copy = my_data_ptr;
    
    if (local_my_data_copy->a == 0)
    {
        // do stuff here
        local_my_data_copy->a = 20;
    }
}

So finally, my question is it relevant to create a local copy of my_data_ptr in every function that needs to access/modify the pointed data? From my perspective, it is relevant for variables access control and can prevent mistakes during code writing. This implementation requests more stack and data space usage because of the extra variables declaration needed, and microcontrollers are memory limited. Hard for me to determine the CPU cost of this implementation. Please let me know your point of view on this piece of code.

Clifford
  • 88,407
  • 13
  • 85
  • 165
panic
  • 2,004
  • 2
  • 17
  • 33
  • I think the compiler will optimize the variable away – Ackdari Jul 16 '21 at 12:31
  • To have confidence in the veracity of this question, we need to see _real_ code that you _know_ demonstrates the issue. When your code has clear errors such as `my_data * const my_data_ptr = &my_data;` (where you use `my_data` as if it were a type), how can we be clear what you are asking? – Clifford Jul 16 '21 at 13:13
  • And even if the compiler doesn't optimize your "local" variable away, how do you know it's more "local" than the global anyway? – Andrew Henle Jul 16 '21 at 13:27
  • 1
    From my perspective, in the first scenario, you've replaced a global structure with a global pointer to the structure, which still leaves you with one global variable — you've not done more than add to the storage requirements. Using the global structure (instead of the global pointer), you can still create the read-only and read-write pointers to the structure if that's what you want. It marginally clarifies what you're doing, but 'marginal' is probably the operative term. There comes a point at which the programmer is responsible for the code they write. – Jonathan Leffler Jul 16 '21 at 13:27
  • 1
    What do you want to achieve by using a global pointer to a static structure variable instead of a global structure variable? Do you want to permit read-only access outside the file that contains the variable? Then you would have to use `const data * const my_data_ptr = &my_data;` – Bodo Jul 16 '21 at 13:29
  • @Clifford Well, I have edited my code. I feel like you didn't really understand the sense of my question, I might have been unclear in my post. But if you prefer my question is `how do I control access to my shared variables` in an interrupt-driven system. – panic Jul 16 '21 at 13:30
  • 1
    @panic Not at all; at that point I had not completed reading the question - my point being that if the code is invalid it has not been tested and may not demonstrate your issue. You start off saying "_code works fine_", then continue with code modifications that have clearly never even been compiled. I understand the question and am currently composing an answer. I don't post answers in comments, and it was only a comment on the quality of the question, not an answer. You did the right thing in fixing it. – Clifford Jul 16 '21 at 13:33
  • "I limit the scope of my global variables and preserve the variables' integrity." --> except now `my_data_ptr` lost the information that `my_data` is an array of 2. – chux - Reinstate Monica Jul 16 '21 at 13:39
  • Actually your function_one() and function_two() are misleading. In both, these are NOT local copies of the data, they are still pointing to the actual my_data. What you have is a copy of the pointer. The data can still change at any point in time of the functions running. They might not even be consistent within the same function at different points accessing the elements. – kesselhaus Jul 17 '21 at 03:38

2 Answers2

3

Access to global data[] deserves special consideration given it is to read/written by an interrupt routine and non-interrupt code.

Instead of protecting via const and auxiliary pointers, consider only access functions - no variable access for the non-interrupt code.

uint16_t data_get_a(void);
uint16_t data_get_b(void);
void data_get_ab(uint16_t *a, uint16_t *b);
void data_set_ab(uint16_t a, uint16_t b);

In non-interrupt code, there is reduced need for speediness, but great need for access integrity such as setting members .a and .b with no possibility of an interrupt in between.

These helper functions can provide temporary interrupt disabling or other mechanics to insure atomic access. That may be very important. See atomic. Perhaps as:

foo() {
  save timer interrupt state
  disable timer interrupt
  access data
  restore timer interrupt state
} 
chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • You're right this is probably a better approach – panic Jul 16 '21 at 13:57
  • @panic Fixing a bug where non-interrupt code and interrupt code are accessing `data` near the same time can be very difficult to detect, replicate and repair. I use the function approach to make absolutely positive the sequence of one access is not partially done before the other occurs. – chux - Reinstate Monica Jul 16 '21 at 14:04
  • can you explain to me why you use four functions? – panic Jul 16 '21 at 15:46
  • @panic It is illustrative. Often reading/writing data needs to be done as an atomic group, so `data_get_ab()/data_set_ab()` like minutes:seconds. Sometimes reading of a lone member is OK - sometimes not. The key point is that with a function interface, you have control, validation, and object hiding. Could add abstract functions like `time_t get_time()` to avoid `data` details. – chux - Reinstate Monica Jul 16 '21 at 15:53
  • @panic An example is `FILE *`. Certainly you have used that often, but all through a functional interface - never writing code that cares about the internal members. – chux - Reinstate Monica Jul 16 '21 at 15:56
2

Your function_two() circumvents the const "promise" of my_data. That alone is bad practice, but since it is allowed the safer solution is the avoidance of global data. Your use of a pointer-to-const to enforce read-only semantics is only valid if you truly only ever want it to be read-only - and clearly you don't. Creating a global pointer to a static only creates a useless level of indirection and little else - you have not avoided a global or mad the code significantly safer. It is safer only in the sense that the coder has to explicitly circumvent it in the way you have, so avoids accidental modification. There is so much more control you can impose as the author of this code to avoid future maintainers (which may be you) shooting themselves in the foot.

The solution in this case is that any access to the shared data should be through accessor functions that have visibility of the data. In this case that might mean implementing function_one() and function_two() in the same file as the static my_data, but that may not be desirable for good cohesion and loose coupling. The generic solution then is to write access functions that control and enforce your desired semantics.

Example (flawed - read-on):

static volatile data my_data = {10, 10};
const data* getMyData(){ return &my_data ; }
void setMyData( const data* d ){ my_data = *d ; }

The advantages of getter/setter functions over global access (indirect or otherwise), include:

  • You can enforce safe access to non-atomic data objects - like the one in this question (by various means - that is perhaps another question).
  • In debugging, a breakpoint in the get/set will trap all accesses without the need to place a breakpoint on every access, or use data watch breakpoints.
  • You can enforce common validation, range-limiting or asserts in a setter to ensure only valid data is written.
  • You can perform "on-access" transformations such as converting ADC units to millivolts on read, or vice-versa for writes. Thus decoupling the data presentation from its internal representation, Doing such conversions in the get/set avoids possibly expensive processing in the interrupt context.

You can of course have access functions for individual members of the structure, allowing for example to enforce read-only for some members by not defining a setter, or even write-only semantics by defining only a setter - a semantic that cannot be enforced on a global using const. Overall it is far more flexible and much safer then using a global and trying to enforce semantics that const cannot support.

Really read the earlier linked article A pox on globals by Jack Ganssle. It will be enlightening and stand you in good stead as an embedded systems developer for the future.


Some other points:

You really should declare data shared between contexts volatile (see https://www.embedded.com/introduction-to-the-volatile-keyword/). But be aware that volatile alone is not a complete solution to safe shared memory between contexts.

You have to be aware for example that if the data access from the main thread (or any other thread context) is not atomic, it may be interrupted and the data modified part way through the process of reading it. For 32 bit integers on a 16 bit device that may mean for example you end up with the high-word from the old value and the low word from the new value - of vice versa. Even for data types that are atomic, operations on then need not be. For example i += j implies a read-modify-write operation that might be interruptible. For data structures it may mean that one member is inconsistent with another.

I raise the point here, because your code clearly exhibits these problems, but equally it is clearly not real code and you may already be aware of these issues. It is also not what your question is about and you might post a new question if you need solutions, but for what it is worth I provide one possible solution for the getter earlier:

data* getMyData( data* d )
{
    // read my_data until a consistent copy has been obtained. 
    do
    {
        *d = mydata ;

    } while( *d != my_data ) ;

    return d ;
}

A final point, it is largely irrelevant if you take the advice above, but regarding:

[...] and I am sure my_data_ptr will point to the my_data's address for the entire program lifetime.

There is no need to trust that to chance or maintenance error - you can enforce that assertion:

      const data* const my_data_ptr = &my_data;
 //   ^^^^^
Clifford
  • 88,407
  • 13
  • 85
  • 165
  • @chux-ReinstateMonica : Thanks. You are right I was testing my last point first, and did not revert to the original code. Even greater reason for not doing that since clearly the pointer-to-const provides no protection if you can circumvent it by assigning to a pointer-to-variable. – Clifford Jul 16 '21 at 17:18
  • The updated `getMyData()`, without a `volatile` may optimize `while( *d != my_data )` into `while(0)`. Yet looks like we both agree on `getMyData()` idiom as a function allows for crafted access - whatever that may entail. – chux - Reinstate Monica Jul 16 '21 at 17:46
  • Thanks, both of you for taking the time to answer my question and for the useful resources shared! – panic Jul 16 '21 at 18:04