ES: Embedded C Variables

Variables in Embedded C carry special significance because they directly map to memory locations in a resource-constrained system. Choosing the right type, qualifier, and storage class is not just good style — it affects firmware correctness, memory usage, and hardware behavior.

In desktop programming, variables are mostly implementation details. In embedded programming, every variable declaration is a decision about:

  • Which memory region holds the value
  • How the compiler is allowed to optimize accesses
  • Whether the value survives a reset
  • How much precious RAM or flash is consumed

Understanding variables in Embedded C means understanding how software maps to silicon.


Memory Regions in Embedded Systems

Before discussing variable types, it is essential to understand the memory landscape of a typical microcontroller.

md
+-----------------------------+  High Address
|         Peripherals         |  (Memory-mapped hardware registers)
+-----------------------------+
|           Stack             |  (Local variables, function call frames)
+-----------------------------+
|           Heap              |  (Dynamic allocation — rarely used in embedded)
+-----------------------------+
|    .bss (Zeroed data)       |  (Uninitialized global/static variables)
+-----------------------------+
|    .data (Initialized data) |  (Initialized global/static variables)
+-----------------------------+
|    .text (Code + const)     |  (Program instructions, string literals)
+-----------------------------+  Low Address (Flash start)

Each section maps to a physical memory region — typically Flash for read-only data and code, and RAM for data that changes at runtime.


Variable Types and Their Sizes

Embedded systems often target 8-bit, 16-bit, or 32-bit processors with specific register widths. Using the correct integer type avoids silent overflow, wasted memory, and portability bugs.

Standard C Integer Types

TypeTypical SizeRange
char1 byte-128 to 127 (signed)
unsigned char1 byte0 to 255
int2 or 4 bytesPlatform-dependent
unsigned int2 or 4 bytesPlatform-dependent
long4 bytes-2,147,483,648 to 2,147,483,647
unsigned long4 bytes0 to 4,294,967,295

On microcontrollers, the size of int depends on the architecture. On an 8-bit AVR, int is 2 bytes. On a 32-bit ARM Cortex-M, int is 4 bytes.

Fixed-Width Integer Types (Preferred in Embedded)

To avoid platform-dependent sizes, use the fixed-width types from <stdint.h>:

c
#include <stdint.h>

uint8_t  byte_value  = 0xFF;          // Always 8-bit unsigned
uint16_t word_value  = 0xFFFF;        // Always 16-bit unsigned
uint32_t dword_value = 0xDEADBEEF;   // Always 32-bit unsigned
int8_t   signed_byte = -100;          // Always 8-bit signed
int32_t  counter     = 0;             // Always 32-bit signed

Using fixed-width types ensures predictable behavior across different MCU architectures.


Variable Qualifiers

Qualifiers tell the compiler how a variable should be treated. In embedded C, two qualifiers are especially critical.

`volatile` — Do Not Optimize This

The volatile qualifier tells the compiler: this variable can change at any time outside of normal program flow. The compiler must read from or write to the actual memory location every time, rather than caching the value in a register.

Without volatile, the compiler may optimize away what it thinks are redundant reads:

c
// Without volatile — compiler may optimize the loop away
int flag = 0;
while (flag == 0) {
    // Wait for flag to be set by an interrupt
}

// With volatile — compiler reads flag every iteration
volatile int flag = 0;
while (flag == 0) {
    // Correctly waits for interrupt to change flag
}

volatile is mandatory for:

  • Hardware registers (values change based on hardware state)
  • Variables shared between ISR and main code
  • Variables accessed by multiple threads or tasks

c
// Hardware register — must be volatile
#define PORTB  (*(volatile uint8_t*)0x25)

// ISR-shared variable — must be volatile
volatile uint8_t uart_rx_ready = 0;

void UART_ISR(void) {
    uart_rx_ready = 1;  // Set by interrupt
}

void main_loop(void) {
    while (!uart_rx_ready) {
        // Wait — compiler will NOT optimize this away
    }
}

`const` — Read-Only Data

The const qualifier marks a variable as read-only. In embedded systems, const globals and string literals are typically stored in Flash (program memory) rather than RAM.

c
// Stored in Flash — does not consume RAM
const uint8_t lookup_table[256] = { 0x00, 0x01, 0x02, /* ... */ };
const char device_name[] = "PY32F003";

// Read-only local variable
const uint32_t system_clock = 48000000UL;

On Harvard architecture processors (like AVR), a separate qualifier PROGMEM is needed to explicitly place data in program memory.


Storage Classes

Local Variables (Stack)

Local variables declared inside a function live on the stack. They are created when the function is entered and destroyed when it returns.

c
void process_sensor(void) {
    uint16_t raw_value = 0;    // Lives on the stack
    float    voltage   = 0.0f; // Also on the stack

    raw_value = ADC_Read();
    voltage   = raw_value * (3.3f / 4095.0f);
}

Stack memory is limited. Deep recursion or large local arrays can cause stack overflow on microcontrollers with only a few kilobytes of RAM.

Global Variables (Data/BSS section)

Variables declared outside any function live for the entire lifetime of the program.

c
// Initialized global — placed in .data section (copied from Flash to RAM at boot)
uint32_t system_tick = 0;

// Uninitialized global — placed in .bss section (zeroed at boot)
uint8_t rx_buffer[64];

Static Variables

The static keyword changes two things depending on context:

  1. Static local variable — persists between function calls (stored in .data or .bss, not on the stack)
  2. Static global variable — limits scope to the current translation unit (file)

c
void count_events(void) {
    static uint32_t count = 0;  // Initialized once, persists between calls
    count++;
}


Combining Qualifiers

In embedded C, it is common to combine volatile and const:

c
// Hardware register — can change externally (volatile), but code should not write to it (const)
#define INPUT_REG  (*(volatile const uint8_t*)0x3F)

This combination is used for read-only hardware status registers that the hardware updates but the firmware should never modify.


Integer Overflow in Embedded C

Integer overflow is a silent source of firmware bugs. On microcontrollers with limited data widths, overflow wraps around without any warning.

c
uint8_t counter = 255;
counter++;         // counter is now 0, not 256

Best practices:

  • Use the smallest type that fits your range
  • Use explicit casts when mixing types
  • Use uint32_t for counters that might exceed 255 or 65535
  • Be especially careful in timing and sensor calculations

Practical Variable Checklist for Embedded C

DecisionGuideline
Type sizeUse stdint.h fixed-width types
Hardware registersAlways declare as volatile
ISR-shared variablesAlways declare as volatile
Lookup tables and constantsDeclare as const to save RAM
Persistent state across callsUse static local variables
Large buffersDeclare as global to avoid stack overflow
Dynamic allocationAvoid — use static allocation in embedded

Final Thoughts

Variables in Embedded C are not just storage slots. They are architecture decisions that affect:

  • How much RAM and Flash the firmware consumes
  • Whether hardware registers are read correctly
  • Whether interrupts and main code share data safely
  • Whether the binary image fits in the target device

Understanding variables in embedded C means understanding the memory map of the machine.