Many of us heard the word “variable” in math classes long before we knew much, if anything, about computer programming. A mathematical variable is a quantity whose value is not known or not limited to one number. This usage is similar, though not identical, to the concept of a C variable. Two important differences are: First, in math, we typically use a letter such as x or y to represent a variable, whereas in C we frequently use a descriptive word or phrase such as temperature, MaxValue, or Number_of_Samples. Second, there are situations in which we use a C variable to identify a quantity that is both known and not intended to ever be different from the original value.
Variables in Hardware
Variables are convenient and intuitive for programmers. For computational hardware, on the other hand, they have no real meaning. Microprocessors store data in registers and memory locations. This fundamental difference between the people who write firmware and the machines that execute firmware is overcome by high-level languages such as C, which handles various details associated with the translation between text-based variables and the physical reality of a processor.
Designers of embedded systems often work with 8-bit processors. In these devices, the fundamental size of data is always one byte. Memory is organized according to bytes, the size of the registers is one byte, and the CPU itself is designed to process 8-bit data. This is a rather awkward limitation because there are many situations in which the value of a variable will exceed the maximum value of an 8-bit number.
In the end, all of your carefully defined, cleverly named C variables end up as bits in memory (or registers).
The C language does not limit the size of a variable to 8 bits, even when you are working with an 8-bit processor. This means that one variable in your firmware can correspond to multiple registers or memory locations in the hardware. “Manually” managing multibyte variables (i.e., via assembly language) is not my idea of fun, but compilers don’t mind at all, and they do the job very well.
The first step in using a variable is defining that variable. The essential components of a variable definition are the type and the name.
There are many variable types; the complete list, as well as the details of the hardware implementation, will vary according to which compiler you’re using. Here are some examples:
- char: a one-byte signed value
- int: a two- or four-byte signed value
- long: a four-byte signed value
- float: a four-byte value that can have numbers after the decimal point—in other words, it’s not limited to integers
- bit: the value of the variable can be zero or one
This is a visual representation of how a series of bits is interpreted differently based on whether a variable is interpreted as signed (using two’s complement notation) or unsigned. See this article for more information.
The following code shows variable definitions that consist only of a basic type and a name (the more technical way to refer to the name is “identifier”):
int ADC_result; char ReceivedByte; float Reference_Voltage;
Initializing the Variable
In many cases, it is a good idea to give a variable an initial value. This facilitates debugging, and it’s essential if the variable will be used before it is set to a known value. You can initialize a variable in the definition or elsewhere in your code, but including the initial value in the definition is a good way to keep your code organized and develop a habit of consistently initializing when necessary.
Here are examples of variable definitions that include initialization:
int ADC_result = 0; char ReceivedByte = 0x00; float Reference_Voltage = 2.4;
Fine-Tuning Variable Definitions
There are various other words that can be included in a variable definition. These are used to more precisely specify the nature of the variable or to give the compiler instructions regarding how to implement the variable in hardware.
The following keywords might prove useful in your firmware projects:
- unsigned: As you might have guessed, this tells the compiler to interpret the variable as an unsigned value rather than a signed value. I define most of my variables as unsigned, because I rarely have need for negative numbers.
- const: The const type qualifier indicates to the compiler that the value of a variable must not change. As I mentioned at the beginning of the article, sometimes the value of a C “variable” is not variable. If you make a mistake in your code and attempt to modify the value of a const variable, the compiler will generate an error.
- volatile: Sophisticated compilers don’t just take your original code and translate it directly to assembly. They also attempt to make the code operate more efficiently, and this process is referred to as “optimization.” In general, optimization is a good thing. Every once in a while, though, it can ruin your day, because the compiler optimizes based only on the code and isn’t able to account for hardware events that interact with your code. When a variable has the volatile type qualifier, the compiler knows that it has to be careful with optimizations that are related to that variable.
An interrupt might cause a variable’s value to be modified in a way that the compiler doesn’t expect, and this can lead to problematic optimization.
- memory types, such as xdata, idata, and code: These words force the compiler to locate a variable in a specific portion of the microprocessor’s memory. The code memory type is particularly handy: RAM resources in a microcontroller are often much more limited than the nonvolatile program memory, and the code memory type allows you to utilize extra program memory for storing data that is used in your program but never modified.
Here are some examples:
unsigned char UART_byte; // The variable’s range of acceptable values is 0 to 255. const float PI = 3.14159; volatile unsigned char ADC_Register // The register can be modified by hardware, so we use the volatile qualifier to avoid optimizations that would cause the program to ignore hardware-generated events. unsigned char code CalibrationValue = 78;
Using Your Variables
There’s not a lot to say about how to use your variables after they’ve been defined. Actually, with regard to the variable itself, the definition is most of the work. After that, you simply incorporate the variable’s identifier into mathematical operations, loops, function calls, and so forth. A good compiler will not only handle the details of the hardware implementation but also look for ways to optimize the code with respect to execution speed or program size.
Perhaps the most common mistake related to variable usage is an overflow. This refers to a situation in which the value assigned to a variable is outside of the numerical range associated with the variable’s data type. You have to think about all the possible scenarios related to a given variable, and then choose the data type accordingly.
The basic variable functionality provided by the C language is intuitive and straightforward, but there are quite a few details that can help you to make an embedded application more reliable and efficient. If you have any questions related to C variables, feel free to mention them in the comments section below, and we’ll try to incorporate the relevant information into future articles.
C Language Series
- Introduction to the C Programming Language for Embedded Applications
- Understanding Variables in C Programming
- Understanding Arrays in C Programming
- Pointers in C Programming: What Is a Pointer and What Does It Do?
- What Are Functions in C Programming?
- How to Incorporate Functions into Embedded Firmware
- How to Use Pointers in C-Language Firmware
- Five Tips for Using Functions in C-Language Firmware