Discussing the Issues of Global Variables in Embedded C Programming

Having worked for several years, I have grown from a technical novice to someone who now considers themselves a junior expert. This journey has involved a lot of self-directed learning through various resources. Whether in forums, bookstores, or magazines, I often encounter either shallow posts from beginners or overly abstract conceptual articles. When I finally come across a practical article written by an expert, it often ends abruptly just as it starts to get interesting. This leaves me feeling frustrated. Nevertheless, today I will put on a brave face and share my thoughts on the issues of global variables in embedded C programming. In embedded systems, especially in os-less microcontroller programs, one of the most common mistakes is the rampant use of global variables. This phenomenon is particularly prevalent among programmers transitioning from assembly language and beginners, who often treat global variables as function parameters. They define numerous chaotic structures in header files, declare a plethora of globally scoped variables that make one’s head spin, and then assign values like 123 in one module while making decisions based on that value in another. Every time I see such code, I can’t help but frown and slam the table in frustration. Yes, I am frustrated. I do not deny the importance of global variables, but I believe they should be used with extreme caution; their misuse can lead to more severe structural system issues.Dear readers, let me elaborate 1. It can lead to unnecessary constants being used frequently, especially when these constants are not properly defined with macros, making the code extremely difficult to read. 2. It can result in unreasonable software layering. Global variables act as a shortcut, blurring the boundaries between the device layer and the application layer. Lower-level programs may mistakenly focus on upper-level applications. While this may enhance efficiency 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 the development process feel like a torturous slog. 3. Due to the unreasonable layering of software, maintenance becomes a nightmare. Even minor modifications or deletions of features often require extensive changes across multiple modules, and the original code comments are frequently forgotten. Consequently, the system handed over to future maintainers resembles a quagmire, with comments serving only to obscure the situation further. 4. The extensive use of global variables often leads to 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 senior expert to intervene, it is destined for chronic failure. There is no need to elaborate further; you have successfully created 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 predict when it will crash or when the next complaint will arise.What are the real-world consequences? 1. The “old guard” feels invincible because the system cannot function without them; they are the only ones who understand all the “danger zones.” When urgent bugs arise, only they can resolve them. You cannot fire them; instead, you have to give them a raise. 2. New hires face a quick demise; anyone recruited to maintain this system, aside from creating more bugs, typically leaves within a month, often spreading the word about the poor quality of the company’s software. 3. As the product undergoes subsequent upgrades, the original creator, who hasn’t interacted with the system for months, will find that they have forgotten many of the danger zones. Consequently, the maintenance cycle for product upgrades becomes increasingly lengthy, as fixing one feature leads to the emergence of numerous bugs, and addressing one bug triggers even more. During this time, more global variables will be introduced. Eventually, they will tell the boss, “It’s not working; we don’t have enough resources; RAM or flash space is too small; we need an upgrade.” 4. 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.As for countermeasures, there are only two principles 1. Avoid using global variables whenever possible. 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. 2. If it is unavoidable, hide them as deeply as possible. 1) If only a specific .c file uses it, declare it static within that file, and include the structure definitions as well; 2) If only one function uses it, declare it static within that function; 3) If it must be exposed for reading, return it through a function, making it read-only; 4) If it must be writable, then provide a function interface for passing parameters; 5) If it must be declared extern, I can strictly control which objects include my .h file, rather than allowing it to be publicly included in includes.h for everyone to see, which would be embarrassing. Thus, you can understand how deeply I feel about global variables. Unfortunately, I have had to rewrite all the projects that those “old guards” handed over to me for maintenance. Can you understand? Don’t let others despise you behind your back.Finally, a few additional comments 1. Global variables are unavoidable; almost every device’s lower level needs them to record the current state and control timing. However, they should not be used for parameter passing, which is highly discouraged. 2. 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 is similar to the private attribute in C++. This approach is beneficial for future debugging of the program. The reason C has a C++ version is largely to control its flexibility; the object-oriented concept has long existed in C and can also be implemented. 3. When the number of global variables in a module exceeds three (inclusive), encapsulate them in a structure. Grouping them together makes it easier to manage. 4. 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 use it freely, just be mindful of the traffic rules. 5. In os-less microcontroller systems, where only the stack is used and no heap is utilized, you can boldly eliminate heap space from the default “startup.s” that allocates space for the heap. 6. Program models? How to analyze and abstract them? From which perspective should the model be constructed? I would love to hear opinions from readers. 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, these theories ultimately originate from managing large system software, so they need to be tailored for smaller applications.

Leave a Comment