Chapter 01
C for embedded
The parts of C that matter most on hardware: exact-width integers, bitwise operations, pointers to registers, volatile and where your variables live in memory.
Fixed-width integers
Also called stdint.h, uint8_t, uint32_t, int16_t
- What
- Types from stdint.h such as uint8_t, int16_t and uint32_t whose size is the same on every compiler and chip, unlike int, whose size depends on the platform.
- Why
- Registers, sensor frames and protocol packets have exact sizes. Using the matching type makes overflow, sign and memory use predictable.
#include <stdint.h>
uint8_t status = 0x80; // 0 to 255
int16_t raw_temp = -125; // sensor value, signed
uint32_t uptime_ms = 0; // wraps after about 49.7 daysWatch out
Mixing signed and unsigned values in one comparison, so -1 silently becomes a huge positive number.
Bitwise set, clear, toggle and test
Also called bit manipulation, bit masks, bitwise tricks, shift
- What
- Using OR to set a bit, AND with an inverted mask to clear it, XOR to toggle it and AND to test it, with shifts to build the mask.
- Why
- Every peripheral is controlled by bits inside registers. Changing one bit without touching its neighbours is the daily work of firmware.
#define BIT(n) (1U << (n))
reg |= BIT(5); // set bit 5
reg &= ~BIT(5); // clear bit 5
reg ^= BIT(5); // toggle bit 5
if (reg & BIT(5)) { } // test bit 5
x = (reg >> 4) & 0x0F; // read the 4-bit field at bit 4Watch out
Writing reg = BIT(5) when you meant reg |= BIT(5), which wipes every other setting in the register.
Memory-mapped registers
Also called peripheral registers, register map, MMIO
- What
- Peripheral control and status registers appear at fixed addresses, so C reads and writes them through pointers to those addresses.
- Why
- Libraries such as the Arduino core, the ESP-IDF drivers and the STM32 HAL are built on exactly this. Knowing it lets you read their source and the reference manual.
// A register at an address from the reference manual
#define GPIO_OUT (*(volatile uint32_t *)0x40020014U)
GPIO_OUT |= (1U << 5); // drive pin 5 highWatch out
Copying a register address from a different chip family, where the same peripheral lives somewhere else.
volatile
- What
- A qualifier telling the compiler a variable can change outside the code it can see, so every read and write must really happen.
- Why
- Hardware registers and variables shared with an interrupt change behind the compiler's back. Without volatile, optimised builds cache them and loops never end.
volatile bool button_flag = false;
void button_isr(void) { // runs on the interrupt
button_flag = true;
}
// main loop: while (!button_flag) { } keeps re-readingWatch out
Believing volatile makes a variable thread-safe: it stops caching, but a read-modify-write can still be interrupted halfway.
Pointers on hardware
Also called pointer, pointer arithmetic, dereference
- What
- A pointer holds an address. On a microcontroller that address may be RAM, flash or a peripheral, and pointer arithmetic moves in units of the pointed-to type.
- Why
- Buffers, DMA, driver structs and callbacks all pass pointers. Reading
*(p + 2)fluently is required to read any driver code.
Watch out
Returning a pointer to a local variable, whose stack memory is reused as soon as the function returns.
Flash, RAM, stack and heap
Also called .data, .bss, stack overflow, linker script, memory map
- What
- Code and constants live in flash. Initialised globals (.data) and zeroed globals (.bss) live in RAM, the stack holds local variables and calls, and the heap serves malloc.
- Why
- Microcontrollers have kilobytes of RAM, not gigabytes. Knowing where each byte goes explains most random crashes and resets.
- Example
- An Arduino Uno has 2 KB of RAM. A 1,500-byte local array inside a function can overflow the stack and corrupt globals without any error message.
Watch out
Calling malloc and free repeatedly in a long-running device, which fragments the heap until an allocation fails days later.
const and static
Also called static variable, const data, PROGMEM
- What
- const marks data that never changes, so the toolchain can keep it in flash. static inside a function keeps a value between calls; at file level it hides the name from other files.
- Why
- Lookup tables and strings in flash save scarce RAM, and file-level static keeps drivers from clashing on names.
Watch out
On AVR boards such as the Uno, assuming const alone keeps a string in flash: it needs PROGMEM or the F() macro.
Go deeper