Global Variables vs. Pointers in Embedded Design
I think I may see where you are coming from. (After reading your comments.) This one is more telling for me:


  ... do you see people architecting software and just avoiding globals at
  all cost in these tiny embedded systems? You can skin the cat either
  way, the globals seem faster to me.


(I also am imagining the 8051/8031/8052/8032 series cores, for now.)

Let's take a very simple method for creating operating system queues for a very simple operating system. We need a ready queue and a sleep queue, at a minimum. (We could add in semaphore queues, but let's not because I want to keep this to a dead minimum.) We also want to support a limited number of processes. (This is, after all, a small MCU without a lot of memory available.)



To start, let's define a few constants:

#define NPROC 10
#define TAILPRIORITY 10000  /* you will see the need later */


For the queues, let's go with a linked list approach and recognize the fact that we need insertion and deletion and would like to make these operations fairly easy to achieve. So we conclude that we want both next and prev pointers for each queue entry. We also want to support a priority field:

typedef struct proc_s proc_t;
typedef struct proc_s {
    int priority;
    proc_t *next;
    proc_t *prev;
} proc_t;


Now, we can just define this, statically (the scope could be kept at file level):

proc_t readyhead, readytail, sleephead, sleeptail, freehead, freetail, proc[NPROC];


Let's now look at what it takes to remove and insert a process into a queue (Note that the ready and sleep queue variables are full proc_t types, not just pointers to them. This simplifies the following code.) I'll also add a "getfirst" because we need that for some purposes.

/* Assumes that 'item' actually resides within a queue. */
proc_t * remove( proc_t * item ) {
    item-&gt;prev-&gt;next= item-&gt;next;
    item-&gt;next-&gt;prev= item-&gt;prev;
    return item;
}
/* Not to be used if 'item' is already in another queue. */
/* Priority insertion assumes that all queues end in a tail */
proc_t * insert( proc_t * queue, proc_t * item, int priority ) {
    proc_t *n, *p;
    for ( n= queue-&gt;next; n-&gt;priority &lt; priority; n= n-&gt;next ) ;
    item-&gt;next= n;
    item-&gt;prev= p= n-&gt;prev;
    item-&gt;priority= priority;
    p-&gt;next= item;
    n-&gt;prev= item;
    return item;
}
proc_t * getfirst( proc_t * queue ) {
    if ( queue-&gt;next-&gt;priority == TAILPRIORITY )
        return NULL;
    return remove( queue-&gt;next );
}


All of the above assumes, of course, that the queue head and tail pointers are properly initialized and that entries in proc[] are first all inserted into the free queue and removed sequentially. (It also assumes that the tail always has a "priority" that is the largest possible value and is bigger than any valid process may possess: TAILPRIORITY.)



What else could we do? Without breaking this up into pieces like above, here's another shot at it:

#define NPROC (10)
#define TAILPRIORITY (10000)
#define READYQUEUE (NPROC)
#define SLEEPQUEUE (NPROC+2)
int next[PROC+4];
int prev[PROC+4];
int prio[PROC+4];
int remove( int item ) {
    int n= next[item], p= prev[item];
    next[p]= n;
    prev[n]= p;
    return item;
}
int insert( int queue, int item, int priority ) {
    int n, p;
    for ( n= next[queue]; prio[n] &lt; priority; n= next[n] ) ;
    next[item]= n;
    prev[item]= p= prev[n];
    prio[item]= priority;
    next[p]= item;
    prev[n]= item;
    return item;
}
int getfirst( int queue ) {
    if ( next[queue] &gt; NPROC )
        return -1;
    return remove( next[queue] );
}


The C compiler now knows in advance about the address of the next[] and prev[] and prio[] arrays. getfirst() no longer needs to depend on a special priority value (though insert() still does require something like it, yet that too could now be changed.)



Does this matter from a code size and/or performance argument? Perhaps. It depends on the compiler. For grins, try out these two different approaches with the SDCC compiler and have a look at the generated assembly code for each. What do you think?

What about the case for readability? Which is more readable to you? (I didn't shoot for "particularly readable" or "particularly unreadable" but instead "consistent with each other.")

What about maintainable? What if you needed to extend the linked list node type? Would it be more maintainable to add another array (2nd source code example)? Or more maintainable to add another element to a struct (1st source code example)? Would it make that much difference, at all?

Suppose you were passing a linked list node around? Is it better to pass a pointer or an index? Note that passing a pointer allows a function to access any item within the structure, even if it isn't supposed to do so. But passing an index might permit placing the visibility of specific parts "elsewhere" so that if there was an attempt, the compiler could emit an error. But there are other considerations, of course. What arguments do you see, pro and con?

And so it goes.



Personally? I find style consistency perhaps the most important. I can get used to just about any coding style -- even ones I don't like much, at all. So long as the programming is consistent, it's mostly just a matter of getting used to it and then following along after that. However, if the programming switches from one mindset to another and then to another and there is little or no consistency to the code, I find it quite difficult to read and/or maintain well. So this is probably the most important thing for me. Set a style and then stay consistent with the style.

There are a few areas that are fraught with trouble. For example, using the heap in an embedded system instead of static memory. (Especially true, I suppose, for the 8051 family.) It is very, very easy for a compiler and linker to compute the total space required for static arrays and let you know if it can fit in the processor you are using at the time. But it is very difficult to find similar memory errors if the only way you get to find out is to run the program and make sure that you exercise all the needed various conditional code in order to force just the right combination of events to exceed the memory, when using heap.

There is nothing intrinsically wrong with heap. But in practice with embedded systems, it does need some good justification, I think. So I'd look for explicit criteria that justified its use, if used.

So I also look for crafted thinking in code. Why were certain choices made? Do they show good judgment? In cases where extraordinary evidence is required for some usage, has that evidence been provided and does it make sense? Etc.



In the case of the 8051 family, though, the instructions needed to directly reference specific assigned addresses is so much smaller (code space) and so much faster, that all of the good C compilers for it (a very much shrinking number of companies, by the way) will provide a mechanism for static call path analysis so that local function variables can be assigned (with some chance of success) fixed addresses. So in this particular case, I'd expect to find much greater use of statics, whether file-local or globally available in scope. It just makes too much sense on the 8051.

The transitions I've been a part of have gone from 4-8 bit ALU and memory widths to 32/64/128, with 32-bit quite common now. The definition of what kind of controller you may find in a small device has also changed and I'm not at all surprised to find Linux being used for little more than blinking a few LEDs on a 16/32 ARM7TDMI core. (Or even a Cortex-A53 superscale.) So what's appropriate at the C coding level will vary a bit.

I expect source code consistency and well-considered reasoning for the design choices that are made. That's the most important, to me. Beyond that, I'm a bit more flexible about my opinion.