This article discusses the issue of global variables in embedded C programming.
In embedded systems, especially in OS-less microcontroller programs, a common mistake is the excessive use of global variables. This phenomenon is often seen among programmers transitioning from early assembly languages and beginners, who tend to use global variables almost like function parameters. They define numerous chaotic structures in .h files, declare a bunch of globally scoped variables that make one’s scalp tingle, and then assign values in one module while making decisions based on those values in another module. Whenever I see such code, I can’t help but frown and slam the table in frustration. Yes, I mean slam the table.
While I do not deny the importance of global variables, I believe they should be used with great caution; their misuse can lead to more severe structural system issues.
Why Fewer Global Variables Are Better
They can lead to unnecessary constants being used frequently, especially when these constants are not defined with macros, making the code extremely difficult to read.
They can cause unreasonable software layering; global variables act as a shortcut, blurring the boundaries between the “device layer” and the “application layer.” The low-level code may mistakenly focus on the upper-level applications. While this may seem efficient during the early stages of software system construction, leading to rapid debugging progress, it often results in a plethora of bugs and patches later on, making maintenance feel like a grueling ordeal.
Due to the unreasonable software layering, maintenance becomes a nightmare; even minor modifications often require digging through most modules, and the original code comments are frequently forgotten, leading to a system that resembles a “quagmire” for future maintainers, where comments only serve to add more confusion.
Excessive use of global variables often results in some variables lingering between interrupts and the main loop. If not handled properly, system bugs can appear randomly and unpredictably, indicating a system in critical condition. Without a skilled expert to intervene, it is doomed to a slow demise.
Needless to say, you end up with a distorted system that exists in a mysterious state of stability! You look at the machine, and it looks back at you, both silent and uneasy. You cannot tell when it will crash or when the next complaint will arise.
Consequences of Excessive Global Variable Usage
The “old-timer” feels proud because the system cannot function without him; he knows all the “danger zones.” When urgent bugs arise, only he can resolve them. You cannot fire him; instead, you have to give him a raise.
Newcomers face a harsh reality; anyone hired to maintain this system, apart from creating more bugs, usually leaves within a month, often spreading the word about the poor software quality of the company.
As the product undergoes subsequent upgrades, the original creator, who hasn’t interacted with the system for months, will find that he has forgotten many of the danger zones. Consequently, the maintenance cycle for product upgrades becomes increasingly lengthy, as fixing one feature leads to many new bugs, and addressing one bug triggers even more. During this time, even more global variables will be created. Eventually, he tells the boss, “We can’t go on like this; we’re running out of resources, RAM or flash space is too small, we need an upgrade.”
Customer complaints are incessant, after-sales support is on the verge of collapse, and salespeople are hesitant to recommend this product, leading to a shrinking market share and a deteriorating company image.
Solutions: Two Principles
Minimize the use of global variables whenever possible. I believe that aside from system states, control parameters, communication handling, and some efficiency-critical modules, most issues can be resolved through reasonable software layering and programming techniques.
If it is unavoidable to use them, hide them as deeply as possible.
-
If only a specific .c file uses it, declare it as static within that file, and include the structure definitions as well;
-
If only one function uses it, declare it as static within that function;
-
If it must be accessible for reading, return it through a function, making it read-only;
-
If it must be writable, then provide a function interface to pass parameters for assignment;
-
If extern access is absolutely necessary, I can strictly control which objects include my .h file, rather than allowing it to be publicly visible in includes.h, which would be embarrassing.
Thus, you can understand how deeply I feel about global variables. Unfortunately, I have had to rewrite all those cases where the “old-timers” handed over their maintenance to me. Can you understand? Don’t let people despise you behind your back.
Final Note
Global variables are unavoidable; almost every device’s low-level code needs them to record current states and control timing. However, they should not be used to pass parameters, as this is highly discouraged.
Try to limit the scope of variables to the module that uses them. If other modules need access, create read or write function interfaces to strictly control access. This principle is similar to the private attributes in C++. This is also beneficial for future debugging of the program. The reason C has a ++ version is largely to control its flexibility; the object-oriented concept has long existed in C and can also be implemented.
When the number of global variables in a module exceeds three, consider wrapping them in a structure. Reset them together to avoid losing track of them.
Declaring a static global variable or global array within a function does not occupy stack space. However, some compilers may place large global arrays in a different address space than regular variables. In Keil C51, since it is statically compiled, if the stack overflows, it will raise an alarm, so you can freely use it, just be mindful of the traffic rules.
In OS-less microcontroller systems, where only the stack is used without heap usage, those “startup.s” files that allocate heap space can be boldly removed: Detailed Explanation of STM32 Microcontroller Stack.
Program models? How to analyze and abstract them? From which perspective should the model be constructed? I would love to hear opinions from netizens. I have always analyzed systems from two perspectives: event-state machine transition diagrams and data flow diagrams. The former analyzes control flow and improves UI, while the latter reveals the origins and fates of system data. These theories are covered in university textbooks on “Software Engineering,” and I encourage everyone to refer to them. However, those theories ultimately originate from managing large system software, so they need to be tailored for smaller applications.