"Global variables" (variables with external linkage) should be avoided in C programs.
There are a few rare exceptions to this rule, such as when implementing a hardware register map or perhaps when dealing with memory mapping into data flash or some other form of non-standard memory. But in the 99.99% of all use-cases you can think of, global variables is not the answer. In beginner-level programs, global variables should never be used for sure.
Because they come with the following severe and serious problems:
Tight coupling between unrelated parts of the code. You are creating dependencies between different parts of the code which do not make sense. In proper program design, every module should do it's designated task and not worry about anything else. Dependencies between different modules should be handled through functions, not with direct access to variables.
For example, your LCD driver may have a dependency as it uses your SPI driver. But if your SPI driver in turn requires the LCD driver, something has gone terribly wrong in the program design. How some LCD happens to work is no business of a SPI driver.
"Spaghetti programming". It is very hard to read code where various state variables are updated literally everywhere, in multiple files. It will be difficult to get a sense over when a variable is updated from where, or whom is responsible for the variable.
Namespace clutter. The exposed variable will be visible to all manner of unrelated parts of the code, meaning that there could be naming collisions or even accidental access to the variable. Or some module accesses the variable on purpose, while the internal workings of your code module is really none of their business. Good programming practice is therefore to reduce the scope of all variables as much as possible.
const qualified "global" variables are generally a less serious design mistake than read/write ones, since they don't have the issues of spaghetti programming. So make everything that should be read-only const, which you probably want to do anyway to get it allocated in flash and not RAM.
Work-arounds:
To avoid all of the above issues, a proper program design technique called private encapsulation is used. This involves restricting the access of variables to the module where they are declared.
Normal embedded system C design uses static variables to solve this. They can still be declared at file scope and remain accessible to the module where they reside, but not to the outside world. If the outside world need to know the data they contain, that is done through so-called "setter/getter" functions. Example:
spi.c
#include "spi.h"
static uint32_t baudrate;
void spi_init (uint32_t baud)
{
baudrate = baud;
/* register setup code */
}
void spi_set_baudrate (uint32_t baud)
{
baudrate = baud;
/* calculate register values and update them */
}
uint32_t spi_get_baudrate (void)
{
return baudrate;
}
spi.h
#ifndef SPI_H
#define SPI_H
/* document this function here */
void spi_init (uint32_t baud);
/* document this function here */
void spi_set_baudrate (uint32_t baud);
/* document this function here */
uint32_t spi_get_baudrate (void);
#endif
This is how the vast majority of code in normal "bare metal" microcontroller programs is designed.
Similarly, when using interrupts, variables communicating with the ISR should be kept local and static as described here: Avoiding global variables when using interrupts in embedded systems
There are a few problems even with static file scope variables too. They are not re-entrant, so even when only accessing them through setters/getters, that might not be ideal for code using interrupts or for multi-process RTOS applications.
They also block the code from having multiple instances. For example, the SPI driver above, as written, will only work for one SPI hardware peripheral. But what if you have several identical ones in the same MCU? Using arrays of the same kind of variables internally might be one solution.
But in general, for more intricate designs when you work with lots of data, or when you need to declare multiple instances of something and still maintain private encapsulation, you can use the design pattern known as "opaque type" - How to do private encapsulation in C?
This comes with a little bit of overhead, so one has to be sensible when to use it. For example I often use opaque type when implementing CAN bus drivers/HALs, since these tend to be rather complex and with various message buffer structures that may need to be allocated on the caller-side. On the other hand, the caller shouldn't need to know how the message buffers are laid out in memory in order to use the CAN driver.