Embedded C programming is where software meets the physical world. Unlike desktop applications running on powerful operating systems, embedded C runs on microcontrollers with limited resources, no OS, and direct hardware access. This comprehensive guide covers everything from memory-mapped I/O to real-time constraints, helping you master the art of embedded systems development.
What is Embedded C?
Embedded C is a set of language extensions and coding practices for programming microcontrollers and embedded systems. Key characteristics:
- Resource constraints: KB of RAM, tens of KB of flash
- No operating system: Bare-metal programming
- Direct hardware access: Memory-mapped peripherals
- Real-time requirements: Deterministic timing
- Low power: Energy efficiency is critical
Memory-Mapped I/O
#include <stdint.h>
// Memory-mapped register addresses (STM32 example)
#define RCC_BASE 0x40021000UL
#define GPIOA_BASE 0x40010800UL
// Register offsets
#define RCC_APB2ENR (*(volatile uint32_t*)(RCC_BASE + 0x18))
#define GPIOA_CRL (*(volatile uint32_t*)(GPIOA_BASE + 0x00))
#define GPIOA_ODR (*(volatile uint32_t*)(GPIOA_BASE + 0x0C))
// Bit definitions
#define RCC_IOPAEN (1 << 2)
#define GPIOA_CRL_MODE5 (1 << 20) // Mode for pin 5
#define GPIOA_CRL_CNF5 (1 << 22) // Configuration for pin 5
// Portable register access macros
#define REG(addr) (*(volatile uint32_t*)(addr))
#define SET_BIT(reg, bit) ((reg) |= (1 << (bit)))
#define CLR_BIT(reg, bit) ((reg) &= ~(1 << (bit)))
#define GET_BIT(reg, bit) (((reg) >> (bit)) & 1)
// Initialize LED on GPIO pin
void led_init(void) {
// Enable clock for GPIOA
RCC_APB2ENR |= RCC_IOPAEN;
// Configure PA5 as output (push-pull, 50MHz)
GPIOA_CRL &= ~(0xF << 20); // Clear configuration bits
GPIOA_CRL |= (0x3 << 20); // Set mode to output (50MHz)
}
void led_on(void) {
GPIOA_ODR |= (1 << 5); // Set PA5 high
}
void led_off(void) {
GPIOA_ODR &= ~(1 << 5); // Set PA5 low
}
void led_toggle(void) {
GPIOA_ODR ^= (1 << 5); // Toggle PA5
}
Bit Manipulation Techniques
#include <stdint.h>
#include <stdbool.h>
// Bit manipulation macros
#define BIT(n) (1UL << (n))
#define BIT_MASK(n) ((1UL << (n)) - 1)
#define BIT_MASK_RANGE(h, l) (((1UL << ((h)-(l)+1)) - 1) << (l))
// Read/write bit fields
#define REG_GET_FIELD(reg, shift, mask) \
(((reg) >> (shift)) & (mask))
#define REG_SET_FIELD(reg, shift, mask, value) \
((reg) = ((reg) & ~((mask) << (shift))) | (((value) & (mask)) << (shift)))
// Example: Timer configuration register
typedef struct {
volatile uint32_t CR1; // Control register 1
volatile uint32_t CR2; // Control register 2
volatile uint32_t SMCR; // Slave mode control
volatile uint32_t DIER; // DMA/interrupt enable
volatile uint32_t SR; // Status register
volatile uint32_t EGR; // Event generation
volatile uint32_t CCMR1; // Capture/compare mode 1
volatile uint32_t CCMR2; // Capture/compare mode 2
volatile uint32_t CCER; // Capture/compare enable
volatile uint32_t CNT; // Counter
volatile uint32_t PSC; // Prescaler
volatile uint32_t ARR; // Auto-reload register
} TIM_TypeDef;
#define TIM2_BASE 0x40000000UL
#define TIM2 ((TIM_TypeDef*)TIM2_BASE)
void timer_init(uint32_t prescaler, uint32_t period) {
// Enable timer clock (simplified)
RCC_APB1ENR |= BIT(0);
// Configure timer
TIM2->PSC = prescaler - 1; // Set prescaler
TIM2->ARR = period - 1; // Set auto-reload value
// Configure control register
REG_SET_FIELD(TIM2->CR1, 4, 0x7, 4); // Set prescaler division
TIM2->CR1 |= BIT(0); // Enable counter
}
// Bit-banding for atomic operations (ARM Cortex-M)
#define BITBAND(addr, bit) \
((volatile uint32_t*)(((uint32_t)(addr) & 0xF0000000) + \
0x2000000 + \
(((uint32_t)(addr) & 0xFFFFF) << 5) + \
((bit) << 2)))
// Atomic bit set/clear using bit-banding
void atomic_set_bit(volatile uint32_t *reg, uint8_t bit) {
*BITBAND(reg, bit) = 1;
}
void atomic_clear_bit(volatile uint32_t *reg, uint8_t bit) {
*BITBAND(reg, bit) = 0;
}
Interrupt Handling
#include <stdint.h>
// Interrupt vector table (simplified for ARM Cortex-M)
typedef void (*isr_t)(void);
__attribute__((section(".vectors"))) isr_t vector_table[] = {
(isr_t)0x20001000, // Initial stack pointer
(isr_t)reset_handler,
(isr_t)nmi_handler,
(isr_t)hard_fault_handler,
(isr_t)mem_manage_handler,
(isr_t)bus_fault_handler,
(isr_t)usage_fault_handler,
// ... more vectors
(isr_t)timer2_irq_handler, // TIM2 interrupt
(isr_t)usart1_irq_handler, // USART1 interrupt
};
// Interrupt priority control
#define NVIC_ISER0 (*(volatile uint32_t*)0xE000E100UL) // Interrupt set-enable
#define NVIC_ICER0 (*(volatile uint32_t*)0xE000E180UL) // Interrupt clear-enable
#define NVIC_IPR0 (*(volatile uint32_t*)0xE000E400UL) // Interrupt priority
void nvic_enable_irq(uint8_t irq_num) {
NVIC_ISER0 = (1 << irq_num);
}
void nvic_disable_irq(uint8_t irq_num) {
NVIC_ICER0 = (1 << irq_num);
}
void nvic_set_priority(uint8_t irq_num, uint8_t priority) {
uint32_t reg = NVIC_IPR0 + (irq_num / 4);
uint8_t shift = (irq_num % 4) * 8;
*(volatile uint32_t*)reg = (*(volatile uint32_t*)reg & ~(0xFF << shift)) |
(priority << shift);
}
// Interrupt handler example
volatile uint32_t systick_counter = 0;
void SysTick_Handler(void) {
systick_counter++;
// Handle periodic tasks
if ((systick_counter % 1000) == 0) {
led_toggle(); // Toggle LED every second (assuming 1ms tick)
}
}
// Critical section management
void disable_interrupts(void) {
__asm volatile("cpsid i"); // Disable interrupts
}
void enable_interrupts(void) {
__asm volatile("cpsie i"); // Enable interrupts
}
// Atomic operations with interrupt protection
uint32_t atomic_read_counter(void) {
uint32_t value;
disable_interrupts();
value = systick_counter;
enable_interrupts();
return value;
}
Memory Management for Embedded Systems
#include <stdint.h>
#include <stddef.h>
// Simple static memory pool
typedef struct {
uint8_t *pool;
size_t pool_size;
size_t allocated;
} MemoryPool;
MemoryPool system_pool;
void memory_pool_init(MemoryPool *pool, void *memory, size_t size) {
pool->pool = (uint8_t*)memory;
pool->pool_size = size;
pool->allocated = 0;
}
void* memory_pool_alloc(MemoryPool *pool, size_t size) {
if (pool->allocated + size > pool->pool_size) {
return NULL; // Out of memory
}
void *ptr = pool->pool + pool->allocated;
pool->allocated += size;
return ptr;
}
void memory_pool_reset(MemoryPool *pool) {
pool->allocated = 0;
}
// Stack-based allocator (for temporary allocations)
typedef struct {
uint8_t *stack;
size_t stack_size;
size_t top;
} StackAllocator;
void stack_allocator_init(StackAllocator *sa, void *memory, size_t size) {
sa->stack = (uint8_t*)memory;
sa->stack_size = size;
sa->top = 0;
}
void* stack_alloc(StackAllocator *sa, size_t size) {
if (sa->top + size > sa->stack_size) {
return NULL;
}
void *ptr = sa->stack + sa->top;
sa->top += size;
return ptr;
}
void stack_free(StackAllocator *sa, size_t size) {
if (size <= sa->top) {
sa->top -= size;
}
}
void stack_reset(StackAllocator *sa) {
sa->top = 0;
}
Peripheral Drivers
1. UART Driver
#include <stdint.h>
#include <stdbool.h>
// UART register structure
typedef struct {
volatile uint32_t SR; // Status register
volatile uint32_t DR; // Data register
volatile uint32_t BRR; // Baud rate register
volatile uint32_t CR1; // Control register 1
volatile uint32_t CR2; // Control register 2
volatile uint32_t CR3; // Control register 3
volatile uint32_t GTPR; // Guard time and prescaler
} USART_TypeDef;
#define USART1_BASE 0x40013800UL
#define USART1 ((USART_TypeDef*)USART1_BASE)
// UART configuration
typedef struct {
uint32_t baud_rate;
uint8_t data_bits;
uint8_t stop_bits;
uint8_t parity;
} UART_Config;
void uart_init(USART_TypeDef *uart, const UART_Config *config) {
// Enable clock (simplified)
RCC_APB2ENR |= BIT(14); // USART1 clock enable
// Configure baud rate
uint32_t apb_clock = 72000000; // 72 MHz APB clock
uint32_t usartdiv = apb_clock / config->baud_rate;
uart->BRR = usartdiv;
// Configure word length, stop bits, parity
uint32_t cr1 = 0;
uint32_t cr2 = 0;
switch (config->data_bits) {
case 8:
cr1 &= ~BIT(12); // 8-bit data
break;
case 9:
cr1 |= BIT(12); // 9-bit data
break;
}
switch (config->stop_bits) {
case 1:
cr2 &= ~(3 << 12); // 1 stop bit
break;
case 2:
cr2 |= (2 << 12); // 2 stop bits
break;
}
if (config->parity) {
cr1 |= BIT(10); // Enable parity
cr1 |= BIT(9); // Even parity (simplified)
}
uart->CR1 = cr1;
uart->CR2 = cr2;
// Enable transmitter and receiver
uart->CR1 |= BIT(2) | BIT(3); // TE and RE
uart->CR1 |= BIT(13); // UE
}
void uart_send_byte(USART_TypeDef *uart, uint8_t data) {
// Wait for transmit buffer empty
while (!(uart->SR & BIT(7))) {
// Busy wait
}
uart->DR = data;
}
uint8_t uart_receive_byte(USART_TypeDef *uart) {
// Wait for data ready
while (!(uart->SR & BIT(5))) {
// Busy wait
}
return uart->DR & 0xFF;
}
void uart_send_string(USART_TypeDef *uart, const char *str) {
while (*str) {
uart_send_byte(uart, (uint8_t)*str++);
}
}
// Interrupt-driven UART
volatile uint8_t uart_rx_buffer[256];
volatile uint16_t uart_rx_head = 0;
volatile uint16_t uart_rx_tail = 0;
void USART1_IRQHandler(void) {
if (USART1->SR & BIT(5)) { // RXNE (data ready)
uint8_t data = USART1->DR & 0xFF;
uint16_t next_head = (uart_rx_head + 1) % sizeof(uart_rx_buffer);
if (next_head != uart_rx_tail) {
uart_rx_buffer[uart_rx_head] = data;
uart_rx_head = next_head;
}
}
}
int uart_read_byte(uint8_t *data) {
if (uart_rx_head == uart_rx_tail) {
return -1; // Buffer empty
}
*data = uart_rx_buffer[uart_rx_tail];
uart_rx_tail = (uart_rx_tail + 1) % sizeof(uart_rx_buffer);
return 0; // Success
}
2. I2C Driver
// I2C register structure
typedef struct {
volatile uint32_t CR1; // Control register 1
volatile uint32_t CR2; // Control register 2
volatile uint32_t OAR1; // Own address 1
volatile uint32_t OAR2; // Own address 2
volatile uint32_t DR; // Data register
volatile uint32_t SR1; // Status register 1
volatile uint32_t SR2; // Status register 2
volatile uint32_t CCR; // Clock control
volatile uint32_t TRISE; // Rise time
} I2C_TypeDef;
#define I2C1_BASE 0x40005400UL
#define I2C1 ((I2C_TypeDef*)I2C1_BASE)
// I2C timing
void i2c_init(I2C_TypeDef *i2c, uint32_t speed_hz) {
// Enable clock (simplified)
RCC_APB1ENR |= BIT(21); // I2C1 clock enable
// Calculate CCR for standard mode (100kHz) or fast mode (400kHz)
uint32_t apb_clock = 36000000; // 36 MHz APB clock
uint32_t ccr_value;
if (speed_hz <= 100000) {
// Standard mode
i2c->CR1 &= ~BIT(15); // Disable fast mode
ccr_value = apb_clock / (2 * speed_hz);
} else {
// Fast mode
i2c->CR1 |= BIT(15); // Enable fast mode
ccr_value = apb_clock / (3 * speed_hz);
i2c->CCR |= BIT(14); // Duty cycle
}
i2c->CCR = ccr_value & 0xFFF;
i2c->TRISE = (apb_clock / 1000000) + 1;
i2c->CR1 |= BIT(0); // Enable I2C
}
void i2c_start(I2C_TypeDef *i2c) {
i2c->CR1 |= BIT(8); // Generate START
while (!(i2c->SR1 & BIT(0))) { // Wait for SB flag
// Busy wait
}
}
void i2c_stop(I2C_TypeDef *i2c) {
i2c->CR1 |= BIT(9); // Generate STOP
}
void i2c_send_byte(I2C_TypeDef *i2c, uint8_t data) {
i2c->DR = data;
while (!(i2c->SR1 & BIT(7))) { // Wait for TxE
// Busy wait
}
}
uint8_t i2c_receive_byte(I2C_TypeDef *i2c, bool ack) {
if (ack) {
i2c->CR1 |= BIT(10); // Set ACK
} else {
i2c->CR1 &= ~BIT(10); // Clear ACK
}
while (!(i2c->SR1 & BIT(6))) { // Wait for RxNE
// Busy wait
}
return i2c->DR;
}
bool i2c_write_register(I2C_TypeDef *i2c, uint8_t addr, uint8_t reg, uint8_t data) {
i2c_start(i2c);
// Send device address (write)
i2c_send_byte(i2c, (addr << 1) | 0);
if (!(i2c->SR1 & BIT(1))) { // Check ADDR
i2c_stop(i2c);
return false;
}
(void)i2c->SR2; // Clear ADDR flag
// Send register address
i2c_send_byte(i2c, reg);
if (!(i2c->SR1 & BIT(2))) { // Check BTF
i2c_stop(i2c);
return false;
}
// Send data
i2c_send_byte(i2c, data);
i2c_stop(i2c);
return true;
}
Real-Time Scheduling
#include <stdint.h>
#include <stdbool.h>
// Simple cooperative scheduler
typedef struct {
void (*task)(void);
uint32_t period_ms;
uint32_t last_run;
bool enabled;
} Task;
#define MAX_TASKS 8
Task tasks[MAX_TASKS];
uint8_t task_count = 0;
void scheduler_add_task(void (*task)(void), uint32_t period_ms) {
if (task_count < MAX_TASKS) {
tasks[task_count].task = task;
tasks[task_count].period_ms = period_ms;
tasks[task_count].last_run = 0;
tasks[task_count].enabled = true;
task_count++;
}
}
void scheduler_run(void) {
uint32_t current_time = systick_counter;
for (uint8_t i = 0; i < task_count; i++) {
if (tasks[i].enabled) {
uint32_t elapsed = current_time - tasks[i].last_run;
if (elapsed >= tasks[i].period_ms) {
tasks[i].task();
tasks[i].last_run = current_time;
}
}
}
}
// Simple round-robin scheduler
typedef struct {
void (*task)(void);
uint8_t priority;
} RRTask;
#define MAX_RR_TASKS 16
RRTask rr_tasks[MAX_RR_TASKS];
uint8_t rr_task_count = 0;
uint8_t current_task = 0;
void scheduler_add_rr_task(void (*task)(void), uint8_t priority) {
if (rr_task_count < MAX_RR_TASKS) {
rr_tasks[rr_task_count].task = task;
rr_tasks[rr_task_count].priority = priority;
rr_task_count++;
}
}
void scheduler_rr_run(void) {
for (uint8_t i = 0; i < rr_task_count; i++) {
uint8_t task_index = (current_task + i) % rr_task_count;
rr_tasks[task_index].task();
}
current_task = (current_task + 1) % rr_task_count;
}
// Task example
void blink_led_task(void) {
static uint32_t counter = 0;
if (++counter >= 500) {
led_toggle();
counter = 0;
}
}
void read_sensor_task(void) {
// Read sensor and process data
static uint16_t last_value = 0;
uint16_t value = read_adc(0);
if (value != last_value) {
process_sensor_data(value);
last_value = value;
}
}
Low-Power Modes
#include <stdint.h>
// ARM Cortex-M power modes
void sleep_mode(void) {
__asm volatile("wfi"); // Wait for interrupt
}
void stop_mode(void) {
// Configure for stop mode (simplified)
// Disable peripherals, configure wake-up sources
// Enter stop mode
__asm volatile("wfi");
// System resumes here after wake-up
}
void standby_mode(void) {
// Configure for standby mode
// Wake-up sources: RTC, external interrupts
// Clear wake-up flags
// Set standby mode
// Enter standby
}
// Deep sleep with wake-up timer
void deep_sleep_ms(uint32_t ms) {
// Configure wake-up timer
// Enter sleep mode
// On wake, resume execution
}
// Power management example
typedef enum {
POWER_MODE_RUN,
POWER_MODE_SLEEP,
POWER_MODE_STOP,
POWER_MODE_STANDBY
} PowerMode;
void set_power_mode(PowerMode mode) {
switch (mode) {
case POWER_MODE_RUN:
// Full speed, all peripherals enabled
// Set clock to maximum
break;
case POWER_MODE_SLEEP:
// CPU stopped, peripherals running
__asm volatile("wfi");
break;
case POWER_MODE_STOP:
// All clocks stopped except RTC
// Configurable wake-up sources
// Lower power consumption
break;
case POWER_MODE_STANDBY:
// Lowest power consumption
// Only RTC and reset logic active
// Wake-up requires reset
break;
}
}
Watchdog Timer
#include <stdint.h>
// Independent Watchdog (IWDG)
typedef struct {
volatile uint32_t KR; // Key register
volatile uint32_t PR; // Prescaler register
volatile uint32_t RLR; // Reload register
volatile uint32_t SR; // Status register
} IWDG_TypeDef;
#define IWDG_BASE 0x40003000UL
#define IWDG ((IWDG_TypeDef*)IWDG_BASE)
void iwdg_init(uint32_t timeout_ms) {
// Enable access to IWDG registers
IWDG->KR = 0x5555;
// Set prescaler (approximate)
IWDG->PR = 0; // 4:1 prescaler (~40kHz/4 = 10kHz)
// Set reload value
IWDG->RLR = (timeout_ms * 10); // 10kHz counter
// Wait for registers to update
while (IWDG->SR) {
// Busy wait
}
// Start watchdog
IWDG->KR = 0xCCCC;
}
void iwdg_refresh(void) {
IWDG->KR = 0xAAAA; // Reload counter
}
// Window Watchdog (WWDG)
typedef struct {
volatile uint32_t CR; // Control register
volatile uint32_t CFR; // Configuration register
volatile uint32_t SR; // Status register
} WWDG_TypeDef;
#define WWDG_BASE 0x40002C00UL
#define WWDG ((WWDG_TypeDef*)WWDG_BASE)
void wwdg_init(uint8_t window, uint8_t counter) {
// Enable WWDG clock
RCC_APB1ENR |= BIT(11);
// Configure prescaler and window
WWDG->CFR = (window & 0x7F) | (0 << 7); // Divider = 1
// Set counter and start
WWDG->CR = (counter & 0x7F) | BIT(7);
}
void wwdg_refresh(uint8_t value) {
WWDG->CR = (value & 0x7F) | BIT(7);
}
Bootloader Implementation
#include <stdint.h>
#include <string.h>
// Flash memory operations
typedef struct {
volatile uint32_t ACR; // Access control register
volatile uint32_t KEYR; // Key register
volatile uint32_t OPTKEYR;
volatile uint32_t SR; // Status register
volatile uint32_t CR; // Control register
volatile uint32_t AR; // Address register
volatile uint32_t RESERVED;
volatile uint32_t OBR; // Option byte register
volatile uint32_t WRPR; // Write protection register
} FLASH_TypeDef;
#define FLASH_BASE 0x40022000UL
#define FLASH ((FLASH_TypeDef*)FLASH_BASE)
// Flash unlock keys
#define FLASH_KEY1 0x45670123
#define FLASH_KEY2 0xCDEF89AB
void flash_unlock(void) {
if (FLASH->CR & BIT(7)) { // Check if locked
FLASH->KEYR = FLASH_KEY1;
FLASH->KEYR = FLASH_KEY2;
}
}
void flash_lock(void) {
FLASH->CR |= BIT(7);
}
void flash_erase_page(uint32_t page_address) {
while (FLASH->SR & BIT(0)) { // Wait for busy
// Busy wait
}
FLASH->CR |= BIT(1); // Page erase
FLASH->AR = page_address;
FLASH->CR |= BIT(6); // Start erase
while (FLASH->SR & BIT(0)) { // Wait for completion
// Busy wait
}
FLASH->CR &= ~BIT(1); // Clear page erase
}
void flash_write_word(uint32_t address, uint32_t data) {
while (FLASH->SR & BIT(0)) { // Wait for busy
// Busy wait
}
FLASH->CR |= BIT(0); // Programming enable
*(volatile uint32_t*)address = data;
while (FLASH->SR & BIT(0)) { // Wait for completion
// Busy wait
}
FLASH->CR &= ~BIT(0); // Disable programming
}
// Bootloader main loop
void bootloader_run(void) {
uint8_t buffer[256];
uint32_t address = 0x08008000; // Application start
// Initialize UART
UART_Config uart_cfg = {115200, 8, 1, 0};
uart_init(USART1, &uart_cfg);
uart_send_string(USART1, "Bootloader v1.0\r\n");
while (1) {
// Check for application start signal
uint8_t command = uart_receive_byte(USART1);
switch (command) {
case 'E': // Erase
uart_send_string(USART1, "Erasing...\r\n");
flash_unlock();
flash_erase_page(address);
flash_lock();
uart_send_string(USART1, "Done\r\n");
break;
case 'W': // Write
// Receive length
uint16_t len = uart_receive_byte(USART1);
len |= (uart_receive_byte(USART1) << 8);
// Receive data
for (uint16_t i = 0; i < len; i++) {
buffer[i] = uart_receive_byte(USART1);
}
// Write to flash
flash_unlock();
for (uint16_t i = 0; i < len; i += 4) {
uint32_t word = 0;
for (int j = 0; j < 4 && i + j < len; j++) {
word |= (uint32_t)buffer[i + j] << (j * 8);
}
flash_write_word(address + i, word);
}
flash_lock();
uart_send_string(USART1, "OK\r\n");
break;
case 'J': // Jump to application
uart_send_string(USART1, "Jumping...\r\n");
// Jump to application
void (*app_entry)(void) = (void(*)(void))address;
app_entry();
break;
}
}
}
Compiler-Specific Extensions for Embedded
// Interrupt handler attribute
__attribute__((interrupt("IRQ"))) void timer_irq_handler(void) {
// Interrupt handling code
// Compiler generates proper interrupt return sequence
}
// Section placement for linker scripts
__attribute__((section(".isr_vector"))) const uint32_t vectors[] = {
(uint32_t)&_estack,
(uint32_t)reset_handler,
// ...
};
// Packed structures for protocol parsing
__attribute__((packed)) struct CAN_Frame {
uint32_t id;
uint8_t dlc;
uint8_t data[8];
};
// Weak symbols for default handlers
__attribute__((weak)) void HardFault_Handler(void) {
while (1) {
// Hang
}
}
// Inline assembly
static inline uint32_t get_psp(void) {
uint32_t psp;
__asm volatile("mrs %0, psp" : "=r"(psp));
return psp;
}
Best Practices for Embedded C
- Use volatile for hardware registers: Prevents compiler optimization
- Minimize stack usage: Keep functions shallow, avoid deep recursion
- Use static allocation: Avoid malloc/free in most embedded systems
- Implement watchdog refresh: Critical for safety-critical systems
- Use hardware timers: For accurate timing, avoid software delays
- Design for power efficiency: Use low-power modes when idle
- Implement error handling: Log faults, handle gracefully
- Use assertions in debug: Catch issues early in development
- Document hardware dependencies: Clarify register usage
- Test with target hardware: Simulators can't catch all timing issues
Conclusion
Embedded C programming requires a deep understanding of hardware-software interaction, resource constraints, and real-time requirements. Key principles:
- Direct hardware control: Memory-mapped I/O, register manipulation
- Interrupt-driven design: Handle events asynchronously
- Resource efficiency: Minimize memory and CPU usage
- Deterministic timing: Meet real-time constraints
- Safety and reliability: Handle errors gracefully, use watchdog timers
By mastering these techniques, you can create robust, efficient embedded systems that interact reliably with the physical world, from simple sensor nodes to complex real-time controllers. The skills learned in embedded C form the foundation for firmware development, IoT devices, and safety-critical systems.