C Refresher

Memory & Keywords

These concepts separate desktop C programmers from embedded engineers. Getting these wrong will cause silent, catastrophic bugs in your firmware.


const and volatile

These two qualifiers change how the compiler treats a variable.

const — "Don't Change This"

const tells the compiler: this value must not be modified. If you try to modify a const variable, the compiler gives an error.

c
1const int max_retries = 3;
2// max_retries = 5; // ❌ compiler error — can't modify const
3
4// Common use: const pointers — pointer can't change what it points to
5const char *device_name = "ESP32-S3"; // can't modify the string
6// device_name[0] = 'X'; // ❌ compiler error

volatile — "Don't Optimize This Away"

volatile tells the compiler: this variable can change at any time, even when no code in this function modifies it. The compiler must read the variable from memory every time — it cannot cache the value in a register.

This is critical in embedded programming for:

  1. Memory-mapped hardware registers — the hardware changes the register value behind the CPU's back.
  2. Variables shared between an ISR (Interrupt) and main code.
c
1// ❌ WITHOUT volatile — the compiler optimizes this to an infinite loop
2bool flag = false;
3
4void isr_handler(void) {
5 flag = true; // set by interrupt — happens asynchronously
6}
7
8while (!flag) {
9 // Compiler sees: flag is never modified inside this loop
10 // It optimizes it to: while(true) — your program hangs forever!
11}
12
13// ✅ WITH volatile — the compiler reads flag from memory every iteration
14volatile bool flag = false;
15
16void isr_handler(void) {
17 flag = true; // interrupt sets it
18}
19
20while (!flag) {
21 // Compiler MUST re-read flag each time — loop exits when ISR fires
22}

Forgetting volatile is a silent bug

The code compiles without errors or warnings. It just doesn't work correctly — the loop never exits, or the register value is stale.


Static Local Variables

You may have seen static on functions to limit their scope to a single file. But static on a local variable inside a function does something completely different: the variable keeps its value between function calls.

c
1#include <stdio.h>
2
3// Without static — count resets to 0 every call
4void counter_bad(void) {
5 int count = 0; // created fresh on the stack each call
6 count++;
7 printf("Bad Count: %d\n", count); // always prints 1
8}
9
10// With static — count persists across calls
11void counter_good(void) {
12 static int count = 0; // initialized ONCE, lives for the entire program
13 count++; // value carries over from last call
14 printf("Good Count: %d\n", count); // 1, 2, 3, 4, ...
15}
16
17int main() {
18 counter_bad();
19 counter_bad();
20
21 counter_good();
22 counter_good();
23 return 0;
24}

Why This Matters in ESP-IDF

Static locals are heavily used for one-time initialization flags and persistent state in FreeRTOS tasks:

c
1void init_wifi(void) {
2 static bool initialized = false; // persists across calls
3 if (initialized) {
4 printf("WiFi already initialized\n");
5 return; // skip re-initialization
6 }
7 // ... do expensive init ...
8 initialized = true;
9}

Memory: Stack vs Heap

c
1// Stack — automatic, fast, limited size (typically 4-8KB per FreeRTOS task)
2void stack_example(void) {
3 int local_var = 42; // on the stack
4 sensor_data_t data = {0}; // on the stack
5 // freed automatically when function returns
6}
7
8// Heap — manual allocation, larger pool, must free
9void heap_example(void) {
10 sensor_data_t *data = malloc(sizeof(sensor_data_t));
11 if (data == NULL) {
12 printf("Out of memory!\n");
13 return;
14 }
15 data->temperature = 25.5;
16
17 // ... use data ...
18
19 free(data); // MUST free — no garbage collector in C!
20 data = NULL; // prevent use-after-free
21}

Desktop vs Microcontroller Memory Constraints

The difference between Stack and Heap is the biggest trap for programmers moving from Desktop apps to Embedded systems.

1. The Stack

  • Desktop: The OS gives your program a massive stack (typically 8 Megabytes). You can declare massive arrays like int buffer[10000]; inside a function and it will work perfectly.
  • Microcontroller: Every FreeRTOS task has its own tiny stack, usually allocated when the task is created (typically 4 to 8 Kilobytes). If you declare int buffer[10000]; inside an ESP-IDF task, it will instantly cause a Stack Overflow, crash the ESP32, and trigger a reboot.

2. The Heap

  • Desktop: The OS manages Gigabytes of RAM. If you forget to free() memory, the OS will automatically clean up the mess when your program exits.
  • Microcontroller: The ESP32-S3 has about 320KB of internal SRAM. A single memory leak inside an infinite while(1) loop will eat up all the RAM in seconds. Furthermore, the program never exits, so the OS cannot clean it up. The device will just crash.

Memory leaks

In C, every malloc must have a matching free. Forgetting to free memory causes leaks that will eventually crash the ESP32. (ESP-IDF provides heap_caps_malloc for allocating in specific memory regions like external SPIRAM).

Previous
Structs & Enums