Embedded C
Programming
Everything you’ve learned converges here —volatile, pointers, and bitwise operators come together to read sensors, drive GPIO pins, and respond to hardware interrupts.
volatile and const in embedded codeWhat is Embedded C?
Embedded C is not a separate language. It is Standard C —exactly what you’ve been learning since Chapter 1 —applied to programming hardware directly.
Same language, same syntax —applied to a different target
Standard C teaches variables, functions, pointers, and structures. Embedded C builds on every one of those same concepts, then adds direct control over registers, microcontrollers, sensors, interrupts, timers, and GPIO pins.
What is an Embedded System?
An embedded system is a computer designed to perform one specific task —repeatedly and reliably —rather than running general-purpose software like a laptop does.
Microwave Oven
Buttons → controller → display → heater. One job: heat food on command.
Car ABS
Brake sensor → microcontroller → anti-lock braking system, running continuously.
Smart Watch
Heart sensor → microcontroller → display, reading and rendering on a tight loop.
Traffic Light
Timer → controller → LED signals, switching states on a fixed, reliable schedule.
Embedded C vs Standard C
| Standard C | Embedded C |
|---|---|
| Runs on a PC | Runs on a microcontroller |
| Input from keyboard | Input from sensors |
| Output to a monitor | Output to an LCD / LEDs |
| Uses files | Uses hardware registers |
| Runs under an operating system | Often runs with no OS at all (bare-metal) |
CPU, Memory & Memory-Mapped I/O
The CPU talks to RAM, ROM, and peripherals entirely through addresses. Memory-mapped I/O means hardware registers are assigned addresses within the same address space the CPU already uses for ordinary memory —so the exact same load/store instructions that read a variable can also read or write a hardware register.
Each peripheral occupies a fixed address range alongside RAM and program memory. The CPU treats all of these identically.
*ptr, *ptr = value) you already know from Chapter 5 works identically whether you’re touching RAM or a hardware register.Hardware Registers
A register is a small memory location inside a peripheral, dedicated to a specific control or status purpose.
| Peripheral | Typical registers |
|---|---|
| GPIO | Direction Register, Output Register, Input Register |
| UART | Status Register, Control Register, Data Register |
| Timer | Counter, Prescaler, Control Register |
0x40021014, writing to that exact address physically energizes a pin on the chip. This is the fundamental difference from Standard C: a memory write here doesn’t just change a variable —it changes the real world.Reading & Writing Registers
Because hardware registers live at known addresses, you access them through exactly the pointer syntax you mastered in Chapter 5 —just pointed at a specific hardware address instead of a normal variable.
volatile unsigned int *BUTTON =
(volatile unsigned int *)0x40021010;
unsigned int value = *BUTTON; /* read the register's current value */volatile unsigned int *LED =
(volatile unsigned int *)0x40021014;
*LED = 1; /* write 1 — turns the LED ON */Practical Register Example: LED Control
#define GPIO_ODR (*(volatile unsigned int *)0x40021014)
GPIO_ODR = 1; /* turn ON */
GPIO_ODR = 0; /* turn OFF */
GPIO_ODR ^= 1; /* toggle */The Complete Memory Flow
Common Mistakes & Best Practices
Use symbolic names
#define GPIO_ODR (*(volatile unsigned int*)0x40021014) instead of bare hex everywhere.
Centralize in headers
Keep all register definitions for a peripheral in one .h file, reused across the project.
Always mark volatile
Every pointer to a hardware register needs volatile —no exceptions.
The volatile Keyword
Consider this innocent-looking loop:
while(flag == 0)
{
/* wait */
}If flag can be changed by hardware, an interrupt, another CPU core, or DMA —does the compiler know that? No. Without being told otherwise, the compiler may assume flag never changes during this loop and optimize the read away entirely, reading memory exactly once and then looping forever on a cached copy.
Interrupt Example
volatile int ready = 0;
/* Main program */
while(ready == 0)
{
/* wait for the interrupt to set ready */
}
/* Interrupt (runs independently) */
void someISR(void)
{
ready = 1;
}Because ready is volatile, the main program is guaranteed to eventually observe the change made by the interrupt —the compiler cannot optimize away the repeated check.
When to Use (and Not Use) volatile
Hardware registers
Their value can change from outside your program's control flow at any time.
Interrupt-shared variables
Set by an ISR, read by the main loop —or vice versa.
Memory shared with DMA
A peripheral writes to this memory independently of CPU instructions.
Ordinary local variables
volatile int age; is wrong if only the current program ever modifies age —it just disables useful optimizations.
volatile does not make code thread-safe, does not prevent race conditions, does not make operations atomic, and does not replace mutexes or other synchronization mechanisms. It only controls compiler optimization of memory accesses —nothing about timing or concurrency safety.const in Embedded Systems
const means read-only. Attempting to modify a const variable is a compile-time error —catching accidental writes before they ever reach hardware.
const float PI = 3.14159;
/* PI = 5; ← compile error: assignment of read-only variable */
const int *ptr1; /* value can't change through ptr1; pointer itself can */
int *const ptr2; /* pointer is fixed; the value it points to CAN change */
const int *const ptr3; /* neither the pointer nor the value can change */const volatile unsigned int STATUS; captures both facts at once: the program cannot write to it (const), but its value can still change unexpectedly from outside (volatile). This combination is extremely common for status registers.| Keyword | Meaning |
|---|---|
const | Program cannot write to it |
volatile | Value can still change from outside the program |
const volatile | Both at once — the classic status-register pattern |
Bit Manipulation
Embedded systems frequently control hardware one bit at a time. In an 8-bit register, each individual bit might enable a separate feature —so you need to change exactly one bit without disturbing any of the others.
| Operation | Expression | What it does |
|---|---|---|
Set bit n | x |= (1 << n) | Forces bit n to 1, leaves all other bits untouched |
Clear bit n | x &= ~(1 << n) | Forces bit n to 0, leaves all other bits untouched |
Toggle bit n | x ^= (1 << n) | Flips bit n: 0→1 or 1→0 |
Check bit n | x & (1 << n) | Non-zero if bit n is currently 1 |
value |= (1 << 3); /* set bit 3: 00000000 → 00001000 */
value &= ~(1 << 3); /* clear bit 3: 11111111 → 11110111 */
value ^= (1 << 3); /* toggle bit 3: flips whatever it currently is */
if(value & (1 << 3))
{
printf("Bit 3 is set\n");
}GPIO_ODR = (1 << 5); overwrites the entire register, clobbering every other bit’s value —potentially disabling pins you never intended to touch. Always use |=, &=, or ^= to modify only the bit(s) you actually mean to change.Bit Masks
A mask is a value used to select specific bits, leaving the rest untouched when combined with AND, OR, or XOR.
GPIO Programming Concepts
GPIO stands for General Purpose Input Output —the pins that let a microcontroller talk to the outside world.
| Register | Controls | Connected to |
|---|---|---|
| Direction Register | Input vs Output mode | Configures pin behavior |
| Output Register | Drives a pin HIGH or LOW | LED, motor, relay |
| Input Register | Reads a pin’s current state | Button, sensor |
#define GPIO_ODR (*(volatile unsigned int *)0x40021014) /* output reg */
#define GPIO_IDR (*(volatile unsigned int *)0x40021010) /* input reg */
/* LED on bit 5 of the output register */
GPIO_ODR |= (1 << 5); /* turn ON */
GPIO_ODR &= ~(1 << 5); /* turn OFF */
GPIO_ODR ^= (1 << 5); /* toggle */
/* Button on bit 2 of the input register */
if(GPIO_IDR & (1 << 2))
{
/* button pressed */
}|=/&=~/^=/& to set, clear, toggle, or check the specific bit that peripheral is wired to.Common Mistakes & Best Practices
Define bit positions with macros
#define LED_PIN 5 instead of a bare 5 scattered through the code.
Use symbolic register names
GPIO_ODR reads far clearer than a raw hex literal at every call site.
volatile on every hardware pointer
No exceptions for registers —always mark the pointer (or the macro it expands from) volatile.
Interview Questions
volatile, the compiler may assume the value never changes between accesses and optimize away what it thinks are redundant reads, serving a stale cached value instead of the register's actual current state. volatile forces every access to go to real memory.REG = value;) overwrites every bit at once, including bits controlling unrelated features you never meant to touch. Bit operators (|= to set, &= ~ to clear, ^= to toggle) modify only the specific bit(s) you intend, leaving every other bit's current state untouched.Frequently Asked Questions
volatile only controls whether the compiler is allowed to optimize away or reorder a memory access —it says nothing about atomicity or concurrency safety. Two interrupts (or an interrupt and the main loop) can still race on a volatile variable if the underlying read-modify-write sequence isn't otherwise protected. For true thread-safety on shared hardware resources, you need additional mechanisms like disabling interrupts during the critical section, or hardware-provided atomic operations.const documents and enforces the "never write" rule at compile time, while volatile ensures every read goes to real memory rather than a stale cached value. Together they precisely describe a read-only-but-externally-changing register.Polling vs Interrupts
Suppose a button is wired to a microcontroller. How does the CPU find out when it’s pressed? There are two fundamentally different strategies.
| Property | Polling | Interrupt |
|---|---|---|
| Detection mechanism | CPU continuously checks | Hardware notifies the CPU |
| CPU time | Wasted while waiting | Free for other work |
| Response speed | Slower, especially with many devices | Fast — immediate notification |
| Power consumption | Higher (CPU always active) | Lower (CPU can sleep between events) |
| Complexity | Simple, easy to debug | More complex |
Interrupt Service Routines (ISR)
An ISR is a function automatically executed by the CPU when a specific interrupt occurs —you never call it directly; the hardware does.
volatile int buttonPressed = 0;
void EXTI0_IRQHandler(void) /* exact name depends on the MCU */
{
buttonPressed = 1; /* set a flag — that's it */
}
/* Main loop processes the flag, NOT the ISR itself */
while(1)
{
if(buttonPressed)
{
buttonPressed = 0;
/* process the button press here, in main code */
}
}volatile, the compiler might cache the main loop’s read of buttonPressed and never notice the ISR changed it.Good ISR Practices
| ISRs should be | ISRs should avoid |
|---|---|
| Short | Long loops |
| Fast | Dynamic memory allocation |
| Predictable | Lengthy calculations |
| Blocking delays |
Interrupt Vector Table
Every interrupt source has an associated handler address. The CPU stores all of these addresses together in the Interrupt Vector Table —when an interrupt fires, the CPU looks up that source’s entry and jumps straight to the corresponding ISR.
A UART interrupt fires → the CPU looks up the table → jumps directly to UART_IRQHandler().
Embedded Coding Standards & MISRA C
Professional embedded software follows formal coding standards —published rule sets that catch dangerous patterns before they ship in safety-critical or hard-to-update hardware. Popular standards include MISRA C, CERT C, and company-specific internal guidelines.
Why Coding Standards?
Safer Code
Rules specifically target patterns known to cause real-world hardware bugs and safety incidents.
Easier Maintenance
A consistent style across an entire codebase means any engineer can read any file confidently.
Better Readability
Descriptive names and consistent structure make intent obvious at a glance.
Fewer Bugs
Many standards exist specifically because certain patterns have historically caused field failures.
Examples of MISRA Guidelines
- Minimize use of global variables.
- Avoid recursion (stack depth is hard to bound on constrained hardware).
- Avoid unchecked pointer arithmetic.
- Use explicit type conversions rather than relying on implicit ones.
- Always initialize variables.
- Always check function return values.
Embedded Best Practices & Project Structure
Always initialize variables
int count = 0; not int count; —eliminate the entire class of garbage-value bugs.
Check function results
if(fp == NULL) { /* handle error */ } after every call that can fail.
Keep functions small
readSensor(); processData(); displayResult(); instead of one sprawling function.
Document the "why," not the "what"
/* Configure UART for 115200 baud */ explains intent, not a restatement of the code.
Typical Embedded Project Structure
Project/
├── main.c
├── gpio.c gpio.h
├── uart.c uart.h
├── adc.c adc.h
├── timer.c timer.h
├── interrupt.c interrupt.h
└── startup.sEmbedded Firmware Development Flow
Common Mistakes & Memory Tricks
“Don’t ask repeatedly — wait to be told”
| Concept | Memory hook |
|---|---|
volatile | Always read memory — never trust the cache |
const | Read-only — the program can’t write here |
| | Set bit |
& ~ | Clear bit |
^ | Toggle bit |
& alone | Check bit |
| Bit mask | One bit at a time, without disturbing the rest |
Interview Questions
volatile flag. The CPU restores its saved state and resumes the interrupted code. On its next loop iteration, the main program checks the flag, clears it, and performs the actual LED toggle (e.g. GPIO_ODR ^= (1 << 5);) using bit manipulation on the output register.Frequently Asked Questions
Practice Programs
- Write a pointer to a register at address
0x40000000and print the value it currently holds. - Set bit 4 of a register, clear bit 2, toggle bit 7, and check whether bit 1 is set —all in one program.
- Explain in your own words why a pointer to a hardware register must be marked
volatile.
- Write macros for LED ON and LED OFF using bit masks, then use them to blink an LED in a loop.
- Write an example demonstrating
const volatileon a simulated status register, explaining what each keyword contributes. - Draw (or describe step by step) the complete path from a CPU instruction to a physical hardware register.
- An LED is connected to bit 6 of a GPIO output register at
0x40020014. Define the register, then write code to turn the LED ON, turn it OFF, toggle it, and check whether the bit is currently HIGH —explaining each statement. - Design firmware for a push button controlling an LED: use an interrupt for the button, toggle the LED on each press, keep the ISR minimal (just setting a flag), and perform the actual LED control in the main loop. Walk through the complete execution flow from press to toggle.
Chapter Summary
- Embedded C is Standard C applied to hardware —not a separate language, just the same syntax controlling registers instead of files and screens.
- Memory-mapped I/O places hardware registers in the same address space as RAM, so ordinary pointer dereference syntax reads and writes hardware directly.
- A hardware register write has a physical effect —turning on an LED, energizing a relay, configuring a peripheral.
volatileforces every access to go to real memory, essential for hardware registers, interrupt-shared variables, and DMA buffers —but it does not provide thread-safety or atomicity.constmarks data as read-only by the program;const volatiletogether describe registers that are read-only from software yet still change due to hardware.- Bit operators (
|=set,&= ~clear,^=toggle,&check) modify individual bits without disturbing the rest of a register —never overwrite a whole register with=when only one bit should change. - Bit masks isolate specific bits of interest, regardless of the surrounding bits' current state.
- GPIO direction, output, and input registers let a microcontroller drive actuators and read sensors using the exact same bit-manipulation patterns.
- Polling continuously checks for events at the cost of CPU time; interrupts let hardware notify the CPU only when needed, freeing the CPU otherwise.
- An ISR runs automatically on an interrupt; it should be short, fast, and predictable —defer real work to the main loop via a
volatileflag. - The Interrupt Vector Table maps each interrupt source to its handler's address, letting the CPU dispatch automatically.
- Coding standards (MISRA C, CERT C) and disciplined practices —meaningful names, initialized variables, checked return values, small functions —produce safer, more maintainable embedded software.
🎉 Congratulations — Course Complete!
You have now completed the entire C Programming for Embedded Systems handbook —from your very first printf("Hello World") to reading and writing hardware registers, handling interrupts, and following professional embedded coding standards.
From variables to volatile pointers — eight chapters, one complete foundation in C for embedded systems.
🚀 Recommended Next Learning Path
This handbook gave you the complete foundation of the C language as it applies to embedded hardware. Here’s where most engineers go next:
Data Structures in C
Linked lists, stacks, queues, trees —built directly on the pointers from Chapter 5.
A Real Microcontroller Family
STM32, AVR, or PIC —apply every register concept from Chapter 8 on actual hardware.
UART, SPI, I²C Communication
The standard protocols for talking to sensors, displays, and other chips.
Timers & PWM
Precise timing and motor/LED brightness control using hardware timer registers.
ADC & DAC
Converting between the analog and digital worlds —reading real sensors, driving real outputs.
Interrupt Programming (Deep Dive)
Going beyond Chapter 8’s basics into priority levels, nested interrupts, and latency tuning.
RTOS (FreeRTOS)
Task scheduling, queues, and semaphores for systems juggling many concurrent responsibilities.
Embedded Linux
When your project needs a full OS —device trees, kernel modules, and user-space drivers.
Device Drivers
Writing the software layer that lets an OS (or your own firmware) talk to a specific chip.
Bootloaders
The code that runs before your application —flashing, recovery, and firmware updates.
ARM Cortex-M Architecture
The most widely deployed embedded CPU architecture —understanding it deeply pays off everywhere.
Firmware Design Patterns
State machines, the observer pattern, and other proven structures for organizing larger firmware projects.