The "Holy Bible" for embedded engineers
Essential C programming concepts for embedded software development
C is the primary programming language for embedded systems due to its:
C is a general-purpose programming language developed in the 1970s by Dennis Ritchie at Bell Labs. It was designed to be a simple, efficient language that provides low-level access to memory while maintaining portability across different computer architectures.
Strengths:
Limitations:
Language Comparison for Embedded Systems:
┌─────────────────┬─────────────┬─────────────┬─────────────────┐
│ Language │ Performance │ Safety │ Learning Curve │
├─────────────────┼─────────────┼─────────────┼─────────────────┤
│ C │ High │ Low │ Medium │
│ C++ │ High │ Medium │ High │
│ Rust │ High │ High │ High │
│ Python │ Low │ High │ Low │
│ Assembly │ Highest │ Low │ High │
└─────────────────┴─────────────┴─────────────┴─────────────────┘
C became the dominant language for embedded systems due to several historical factors:
Performance Benefits:
Resource Efficiency:
Hardware Integration:
System Control:
Use C When:
Consider Alternatives When:
C is primarily a procedural programming language, which means:
C defines an abstract machine; the actual memory layout is implementation-defined. Embedded targets typically place code in Flash/ROM and data in RAM, and the linker script controls section placement.
Typical Embedded Memory Layout (varies by target/toolchain):
┌─────────────────────────────────────────────────────────────┐
│ Stack (Local Variables) │
│ ↓ Often grows downward │
├─────────────────────────────────────────────────────────────┤
│ Heap (Dynamic Memory) │
│ ↑ Often grows upward │
├─────────────────────────────────────────────────────────────┤
│ .bss (Zero-initialized Data) │
├─────────────────────────────────────────────────────────────┤
│ .data (Initialized Data) │
├─────────────────────────────────────────────────────────────┤
│ .text/.rodata (Code/Const) │
└─────────────────────────────────────────────────────────────┘
C on embedded targets compiles into multiple object files (translation units) that the linker places into memory regions defined by a linker script. Understanding this flow helps you read the map file and control where data/code lands.
-Wl,-Map=out.map and open the map file. Locate a static const table vs a non-const global.static to non-static and observe its visibility (external vs internal linkage).static at file scope gives internal linkage; keep symbols local by default.C programs go through several stages before execution:
Compilation Process:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Source │ → │ Preprocess │ → │ Compile │ → │ Link │
│ Code │ │ (Macros) │ │ (Assembly) │ │ (Executable)│
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
C uses a static type system with weak typing:
Scope Rules:
{} blocks (including function bodies)goto)Lifetime Rules:
Rather than memorizing types, think in terms of storage duration and lifetime: who owns the object, where is it placed in memory, and when is it initialized/destroyed. On MCUs, these choices affect RAM usage, startup cost, determinism, and safety.
const), reducing RAM.int g1; // Zero-initialized (\`.bss\`)
static int g2 = 42; // Pre-initialized (\`.data\`)
void f(void) {
int a; // Uninitialized (stack, indeterminate)
static int b; // Zero-initialized, retains value across calls
static const int lut[] = {1,2,3}; // Often placed in Flash/ROM
(void)a; (void)b; (void)lut;
}
g1, g2, a local variable, and b. Inspect the linker map to see section placement (`.text`, `.data`, `.bss`, stack).lut large and observe Flash vs RAM usage in the map file when const is present vs removed.static inside a function behaves like a global with function scope.const data may reside in non‑volatile memory; don’t cast away const to write to it.Platform note: On Cortex‑M, large zero‑initialized objects increase `.bss` and extend startup clear time; large initialized objects increase `.data` copy time from Flash to RAM.
Variables are named storage locations in memory that can hold data. In C, variables must be declared before use, specifying their type and optionally initializing them with a value.
Declaration vs. Definition:
Variable Attributes:
Integer Types:
Floating Point Types:
Character Types:
// Signed integers
int8_t small_int; // 8-bit signed (-128 to 127)
int16_t medium_int; // 16-bit signed (-32768 to 32767)
int32_t large_int; // 32-bit signed (-2^31 to 2^31-1)
int64_t huge_int; // 64-bit signed
// Unsigned integers
uint8_t small_uint; // 8-bit unsigned (0 to 255)
uint16_t medium_uint; // 16-bit unsigned (0 to 65535)
uint32_t large_uint; // 32-bit unsigned (0 to 2^32-1)
uint64_t huge_uint; // 64-bit unsigned
// Traditional C types (avoid in embedded)
int platform_dependent; // Size varies by platform
long also_variable; // Size varies by platform
float single_precision; // Typically 32-bit IEEE 754
double double_precision; // Implementation-defined (32 or 64-bit)
char character; // Usually 8-bit
uint8_t byte_data; // Explicit 8-bit unsigned
// ✅ Good: Explicit initialization
uint32_t counter = 0;
uint8_t status = 0xFF;
float temperature = 25.5f;
// ❌ Bad: Uninitialized variables
uint32_t counter; // Contains garbage data
// Compile-time constants
#define MAX_BUFFER_SIZE 1024
#define PI 3.14159f
// Runtime constants
const uint32_t TIMEOUT_MS = 5000;
const float VOLTAGE_REFERENCE = 3.3f;
// Enum constants
typedef enum {
LED_OFF = 0,
LED_ON = 1,
LED_BLINK = 2
} led_state_t;
In embedded, function design drives predictability and testability. Prefer small, single-purpose functions with explicit inputs/outputs. Avoid hidden dependencies (globals) except for well-defined hardware interfaces behind an abstraction.
// Before: mixes IO, computation, and policy
void control_loop(void) {
int raw = adc_read();
float temp = convert_to_celsius(raw);
if (temp > 30.0f) fan_on(); else fan_off();
}
// After: separate IO from policy
float read_temperature_c(void) { return convert_to_celsius(adc_read()); }
bool fan_required(float temp_c) { return temp_c > 30.0f; }
void apply_fan(bool on) { if (on) fan_on(); else fan_off(); }
fan_required off-target (no hardware) to validate thresholds and hysteresis.static inline for very small helpers on hot paths.Functions are reusable blocks of code that perform specific tasks. They are the primary mechanism for code organization and reuse in C programming.
Function Components:
Function Types:
Single Responsibility:
Parameter Design:
Error Handling:
// Function declaration (prototype)
return_type function_name(parameter_list);
// Function definition
return_type function_name(parameter_list) {
// Function body
// Local variables
// Statements
return value; // Optional
}
// Simple function with no parameters
void initialize_system(void) {
// System initialization code
configure_clocks();
setup_peripherals();
enable_interrupts();
}
// Function with parameters and return value
uint32_t calculate_average(uint32_t* values, size_t count) {
if (count == 0) return 0;
uint32_t sum = 0;
for (size_t i = 0; i < count; i++) {
sum += values[i];
}
return sum / count;
}
// Function with multiple return points
bool validate_sensor_data(uint16_t value, uint16_t min, uint16_t max) {
if (value < min) return false;
if (value > max) return false;
return true;
}
Deeply nested branches increase cyclomatic complexity and code size on MCUs. Early returns with guard clauses keep critical paths obvious and reduce stack pressure in error paths.
// Nested
bool handle_packet(const pkt_t* p) {
if (p) {
if (valid_crc(p)) {
if (!seq_replay(p)) { process(p); return true; }
}
}
return false;
}
// Guarded
bool handle_packet(const pkt_t* p) {
if (!p) return false;
if (!valid_crc(p)) return false;
if (seq_replay(p)) return false;
process(p);
return true;
}
Control structures determine the flow of program execution. They allow programs to make decisions, repeat operations, and organize code execution.
Decision Making:
Looping:
Flow Control:
// Simple if statement
if (temperature > 30.0f) {
turn_on_fan();
}
// if-else statement
if (battery_level > 20) {
normal_operation();
} else {
low_power_mode();
}
// Nested if-else
if (sensor_status == SENSOR_OK) {
if (temperature > threshold) {
activate_cooling();
} else {
deactivate_cooling();
}
} else {
handle_sensor_error();
}
// Switch statement for multiple conditions
switch (button_pressed) {
case BUTTON_UP:
increase_volume();
break;
case BUTTON_DOWN:
decrease_volume();
break;
case BUTTON_SELECT:
select_option();
break;
default:
// Ignore unknown buttons
break;
}
// Traditional for loop
for (int i = 0; i < 10; i++) {
process_data(i);
}
// Embedded-style for loop
for (uint32_t i = 0; i < BUFFER_SIZE; i++) {
buffer[i] = 0; // Initialize buffer
}
// Infinite loop (common in embedded systems)
for (;;) {
process_events();
update_display();
delay_ms(100);
}
// Condition-checked loop
while (data_available()) {
process_data();
}
// Infinite loop with break
while (1) {
if (shutdown_requested()) {
break;
}
main_loop();
}
// Execute at least once
do {
read_sensor();
} while (sensor_error());
Memory management in C involves allocating, using, and freeing memory resources. Unlike higher-level languages, C requires manual memory management, giving programmers direct control but also responsibility for memory safety.
Memory Types:
Memory Lifecycle:
Memory Safety:
Stack Memory:
Heap Memory:
void stack_example(void) {
// Stack-allocated variables
uint32_t local_var = 42;
uint8_t buffer[256];
struct sensor_data data;
// Memory automatically freed when function returns
}
void heap_example(void) {
// Allocate memory
uint8_t* buffer = malloc(1024);
if (buffer == NULL) {
// Handle allocation failure
return;
}
// Use memory
memset(buffer, 0, 1024);
// Free memory
free(buffer);
buffer = NULL; // Prevent use-after-free
}
/*
* Stack vs Heap: When to use each
*
* STACK: small, fixed-size, short-lived data
* HEAP: large, variable-size, or data that outlives the function
*/
// ✅ Stack: small fixed buffer for local processing
void process_sensor(void) {
uint8_t raw[8]; // 8 bytes on stack - fast, automatic
read_sensor(raw, 8);
uint16_t value = (raw[0] << 8) | raw[1];
send_value(value);
} // raw automatically freed here
// ✅ Heap: large buffer or data returned to caller
uint8_t* allocate_frame_buffer(size_t width, size_t height) {
size_t size = width * height * 3; // RGB
uint8_t* fb = malloc(size);
if (fb) memset(fb, 0, size);
return fb; // caller must free
}
Pitfall 1: Returning Stack Address (Dangling Pointer)
// ❌ BUG: returning address of stack memory
uint8_t* bad_get_buffer(void) {
uint8_t tmp[64];
fill_buffer(tmp);
return tmp; // UNDEFINED BEHAVIOR - tmp is gone after return
}
// ✅ FIX: use caller-provided buffer or heap
void good_get_buffer(uint8_t* out, size_t len) {
fill_buffer(out); // caller owns the memory
}
uint8_t* good_get_buffer_heap(size_t len) {
uint8_t* buf = malloc(len);
if (buf) fill_buffer(buf);
return buf; // caller must free
}
Pitfall 2: Memory Leak in Error Path
// ❌ BUG: memory leak if second allocation fails
int bad_init(void) {
ctx->buf1 = malloc(1024);
if (!ctx->buf1) return -1;
ctx->buf2 = malloc(2048);
if (!ctx->buf2) return -1; // LEAK: buf1 not freed!
return 0;
}
// ✅ FIX: clean up on error
int good_init(void) {
ctx->buf1 = malloc(1024);
if (!ctx->buf1) return -1;
ctx->buf2 = malloc(2048);
if (!ctx->buf2) {
free(ctx->buf1); // clean up first allocation
ctx->buf1 = NULL;
return -1;
}
return 0;
}
Pitfall 3: Use-After-Free
// ❌ BUG: accessing freed memory
void bad_cleanup(msg_t* msg) {
free(msg->payload);
log("Freed %zu bytes", msg->payload_len); // OK so far
// ... later in code ...
if (msg->payload[0] == 0xAA) { } // UAF! payload is freed
}
// ✅ FIX: NULL after free, check before use
void good_cleanup(msg_t* msg) {
free(msg->payload);
msg->payload = NULL; // prevent accidental reuse
msg->payload_len = 0;
}
Pitfall 4: Double Free
// ❌ BUG: freeing the same memory twice
void bad_reset(void) {
free(global_buf);
// ... other code ...
free(global_buf); // DOUBLE FREE - undefined behavior
}
// ✅ FIX: NULL after free
void good_reset(void) {
free(global_buf);
global_buf = NULL;
// ... other code ...
free(global_buf); // safe: free(NULL) is a no-op
}
Pitfall 5: Stack Overflow (Large Local Arrays)
// ❌ RISKY: large array on stack (may overflow small embedded stack)
void bad_process_image(void) {
uint8_t frame[320 * 240]; // 76KB on stack!
capture_frame(frame);
}
// ✅ SAFER: use static or heap for large buffers
static uint8_t frame_buffer[320 * 240]; // in .bss, not stack
void good_process_image(void) {
capture_frame(frame_buffer);
}
Pointers are variables that store memory addresses. They provide indirect access to data and are fundamental to C programming, especially in embedded systems where direct memory manipulation is common.
Address and Value:
Pointer Types:
Pointer Arithmetic:
// Pointer declaration and initialization
uint32_t value = 42;
uint32_t* ptr = &value; // Address-of operator
// Dereferencing
uint32_t retrieved = *ptr; // Dereference operator
// Pointer arithmetic
uint32_t array[5] = {1, 2, 3, 4, 5};
uint32_t* array_ptr = array;
// Access elements
uint32_t first = *array_ptr; // array[0]
uint32_t second = *(array_ptr + 1); // array[1]
uint32_t third = array_ptr[2]; // array[2]
Memory-Mapped Register Access
// Direct hardware register manipulation via pointers
#define GPIO_BASE 0x40020000u
#define GPIO_MODER (*(volatile uint32_t*)(GPIO_BASE + 0x00))
#define GPIO_ODR (*(volatile uint32_t*)(GPIO_BASE + 0x14))
#define GPIO_IDR (*(volatile uint32_t*)(GPIO_BASE + 0x10))
void gpio_set_output(uint8_t pin) {
GPIO_MODER &= ~(3u << (pin * 2)); // clear mode bits
GPIO_MODER |= (1u << (pin * 2)); // set output mode
}
void gpio_write(uint8_t pin, uint8_t val) {
if (val) GPIO_ODR |= (1u << pin);
else GPIO_ODR &= ~(1u << pin);
}
Pointer Arithmetic: Type Matters
/*
* Key insight: ptr + 1 advances by sizeof(*ptr) bytes
*
* uint8_t* + 1 = +1 byte
* uint16_t* + 1 = +2 bytes
* uint32_t* + 1 = +4 bytes
*/
void demonstrate_pointer_arithmetic(void) {
uint8_t buf[16];
uint8_t* p8 = buf;
uint16_t* p16 = (uint16_t*)buf;
uint32_t* p32 = (uint32_t*)buf;
// All start at same address
// p8 = 0x2000
// p16 = 0x2000
// p32 = 0x2000
p8++; // p8 = 0x2001 (+1 byte)
p16++; // p16 = 0x2002 (+2 bytes)
p32++; // p32 = 0x2004 (+4 bytes)
}
Practical: Parsing a Protocol Packet
/*
* Parse: [SYNC:1][LEN:2][CMD:1][PAYLOAD:n][CRC:2]
* This is how embedded protocols like UART frames are parsed
*/
typedef struct {
uint8_t cmd;
uint16_t len;
uint8_t* payload;
uint16_t crc;
} packet_t;
bool parse_packet(uint8_t* raw, size_t raw_len, packet_t* pkt) {
uint8_t* p = raw;
uint8_t* end = raw + raw_len;
// Check minimum size
if (raw_len < 6) return false;
// Parse SYNC
if (*p++ != 0xAA) return false;
// Parse LEN (little-endian 16-bit)
pkt->len = p[0] | (p[1] << 8);
p += 2;
// Bounds check before accessing payload
if (p + pkt->len + 3 > end) return false;
// Parse CMD
pkt->cmd = *p++;
// Payload pointer (no copy, just reference)
pkt->payload = p;
p += pkt->len;
// Parse CRC
pkt->crc = p[0] | (p[1] << 8);
return true;
}
Pointer Comparison and Bounds Checking
/*
* Safe buffer iteration with boundary checks
* Common pattern for circular buffers and DMA
*/
void safe_buffer_copy(uint8_t* dst, const uint8_t* src,
size_t len, size_t dst_size) {
const uint8_t* src_end = src + len;
uint8_t* dst_end = dst + dst_size;
while (src < src_end && dst < dst_end) {
*dst++ = *src++;
}
}
// Ring buffer read with wrap-around
size_t ring_read(ring_t* r, uint8_t* out, size_t max) {
size_t count = 0;
uint8_t* end = r->buf + r->size; // one past last valid
while (count < max && r->head != r->tail) {
*out++ = *r->tail++;
if (r->tail >= end) {
r->tail = r->buf; // wrap to start
}
count++;
}
return count;
}
Multi-Byte Access Patterns (Endianness-Aware)
/*
* Portable multi-byte read/write for protocol buffers
* Avoids alignment issues and works regardless of CPU endianness
*/
// Read 16-bit little-endian from byte buffer
static inline uint16_t read_le16(const uint8_t* p) {
return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
}
// Read 32-bit big-endian from byte buffer (network order)
static inline uint32_t read_be32(const uint8_t* p) {
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
((uint32_t)p[2] << 8) | (uint32_t)p[3];
}
// Write 16-bit little-endian to byte buffer
static inline void write_le16(uint8_t* p, uint16_t v) {
p[0] = (uint8_t)(v & 0xFF);
p[1] = (uint8_t)(v >> 8);
}
// Usage: build a packet
void build_response(uint8_t* buf, uint16_t seq, uint32_t value) {
buf[0] = 0xAA; // sync
write_le16(buf + 1, seq); // sequence number
buf[3] = 0x02; // command
write_le16(buf + 4, (uint16_t)value); // payload
}
Void Pointers and Type Casting
/*
* void* is the "generic" pointer - can point to any type
* Must cast before dereferencing
* Common in callbacks, memory allocators, and HAL APIs
*/
// Generic compare callback (like qsort)
typedef int (*compare_fn)(const void*, const void*);
int compare_uint16(const void* a, const void* b) {
uint16_t va = *(const uint16_t*)a;
uint16_t vb = *(const uint16_t*)b;
return (va > vb) - (va < vb);
}
// Generic memory pool
void* pool_alloc(pool_t* pool, size_t size) {
if (pool->free + size > pool->end) return NULL;
void* ptr = pool->free;
pool->free += size;
return ptr;
}
// Usage
sensor_t* s = (sensor_t*)pool_alloc(&pool, sizeof(sensor_t));
// Function pointer type
typedef void (*callback_t)(uint32_t);
// Function that takes a callback
void process_data(uint32_t data, callback_t callback) {
// Process data
if (callback != NULL) {
callback(data);
}
}
// Callback function
void data_handler(uint32_t data) {
printf("Received: %u\n", data);
}
// Usage
process_data(42, data_handler);
An array name in an expression decays to a pointer to its first element. The array itself has a fixed size and lives where it was defined (stack, `.bss`, `.data`). A pointer is just an address that can point anywhere and can be reseated.
sizeof and parameter passing.static const look like ordinary arrays but are read‑only; use volatile only for memory‑mapped registers or data changed by hardware/ISRs.static uint8_t table[16];
size_t size_in_caller = sizeof table; // 16
void use_array(uint8_t *p) {
size_t size_in_callee = sizeof p; // size of pointer, not array
(void)size_in_callee;
}
void demo(void) {
use_array(table); // array decays to uint8_t*
}
sizeof table in the defining scope and inside a callee parameter.uint8_t a[16] and observe it’s still a pointer in the callee.static const uint16_t lut[] = { ... } and verify via the map file whether it resides in Flash/ROM.sizeof(param) inside a function where param is declared as type param[] yields the pointer size.(ptr, length) pairs, or wrap in a struct to preserve size information.Cross‑links: See
Type_Qualifiers.mdforconst/volatileon memory‑mapped regions, andStructure_Alignment.mdfor layout implications.
Arrays are collections of elements of the same type stored in contiguous memory locations. They provide efficient access to multiple related data items.
Array Characteristics:
Array Operations:
Array Limitations:
String Representation:
String Operations:
// Array declaration and initialization
uint32_t numbers[5] = {1, 2, 3, 4, 5};
// Array traversal
for (size_t i = 0; i < 5; i++) {
printf("Element %zu: %u\n", i, numbers[i]);
}
// Array as function parameter
void process_array(uint32_t* array, size_t size) {
for (size_t i = 0; i < size; i++) {
array[i] *= 2; // Double each element
}
}
// String declaration
char message[] = "Hello, World!";
// String length calculation
size_t length = 0;
while (message[length] != '\0') {
length++;
}
// String copying
char destination[20];
size_t i = 0;
while (message[i] != '\0') {
destination[i] = message[i];
i++;
}
destination[i] = '\0'; // Null-terminate
Structures are user-defined data types that group related data items of different types into a single unit. They provide a way to organize complex data in embedded systems.
Structure Components:
Structure Usage:
Structure Design:
Union Characteristics:
Union Applications:
// Basic structure
typedef struct {
uint32_t id;
float temperature;
uint8_t status;
} sensor_data_t;
// Structure with bit fields
typedef struct {
uint8_t red : 3; // 3 bits for red
uint8_t green : 3; // 3 bits for green
uint8_t blue : 2; // 2 bits for blue
} rgb_color_t;
// Structure with function pointer
typedef struct {
uint32_t (*read)(void);
void (*write)(uint32_t value);
uint32_t address;
} hardware_register_t;
// Union for type conversion
typedef union {
uint32_t as_uint32;
uint8_t as_bytes[4];
float as_float;
} data_converter_t;
// Union for protocol messages
typedef union {
struct {
uint8_t type;
uint8_t length;
uint8_t data[32];
} message;
uint8_t raw[34];
} protocol_message_t;
Note: Type-punning through unions is implementation-defined in C. For strict portability, use
memcpyto move between object representations.
Guideline: Keep macros minimal and local. Prefer
static inlinefunctions for type safety, debuggability, and better compiler analysis unless you truly need token pasting/stringification or compile‑time branching.
Preprocessor directives are instructions to the C preprocessor that are processed before compilation. They provide text substitution, conditional compilation, and file inclusion capabilities.
Text Substitution:
Conditional Compilation:
File Management:
// Simple macro
#define MAX_BUFFER_SIZE 1024
#define PI 3.14159f
// Function-like macro
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define ABS(x) ((x) < 0 ? -(x) : (x))
// Multi-line macro
#define INIT_SENSOR(sensor, id, threshold) \
do { \
sensor.id = id; \
sensor.threshold = threshold; \
sensor.status = SENSOR_INACTIVE; \
} while(0)
// Platform-specific code
#ifdef ARM_CORTEX_M4
#define CPU_FREQUENCY 168000000
#elif defined(ARM_CORTEX_M3)
#define CPU_FREQUENCY 72000000
#else
#define CPU_FREQUENCY 16000000
#endif
// Debug code
#ifdef DEBUG
#define DEBUG_PRINT(msg) printf("DEBUG: %s\n", msg)
#else
#define DEBUG_PRINT(msg) ((void)0)
#endif
#include <stdint.h>
#include <stdbool.h>
// Constants
#define MAX_SENSORS 8
#define TEMPERATURE_THRESHOLD 30.0f
// Data structures
typedef struct {
uint32_t id;
float temperature;
bool active;
} sensor_t;
typedef struct {
sensor_t sensors[MAX_SENSORS];
uint8_t sensor_count;
bool system_active;
} system_state_t;
// Function prototypes
void initialize_system(system_state_t* state);
void read_sensors(system_state_t* state);
void process_data(system_state_t* state);
void update_outputs(system_state_t* state);
// Main function
int main(void) {
system_state_t system;
// Initialize system
initialize_system(&system);
// Main loop
while (system.system_active) {
read_sensors(&system);
process_data(&system);
update_outputs(&system);
}
return 0;
}
// Function implementations
void initialize_system(system_state_t* state) {
state->sensor_count = 0;
state->system_active = true;
// Initialize sensors
for (uint8_t i = 0; i < MAX_SENSORS; i++) {
state->sensors[i].id = i;
state->sensors[i].temperature = 0.0f;
state->sensors[i].active = false;
}
}
void read_sensors(system_state_t* state) {
for (uint8_t i = 0; i < state->sensor_count; i++) {
if (state->sensors[i].active) {
// Simulate sensor reading
state->sensors[i].temperature = 25.0f + (i * 2.0f);
}
}
}
void process_data(system_state_t* state) {
for (uint8_t i = 0; i < state->sensor_count; i++) {
if (state->sensors[i].active &&
state->sensors[i].temperature > TEMPERATURE_THRESHOLD) {
// Handle high temperature
activate_cooling();
}
}
}
void update_outputs(system_state_t* state) {
// Update system outputs based on processed data
update_display();
send_status_report();
}
Problem: Using variables before they’re initialized Solution: Always initialize variables
// ❌ Bad: Uninitialized variable
uint32_t counter;
printf("Counter: %u\n", counter); // Undefined behavior
// ✅ Good: Initialized variable
uint32_t counter = 0;
printf("Counter: %u\n", counter);
Problem: Writing beyond array boundaries Solution: Always check array bounds
// ❌ Bad: Buffer overflow
uint8_t buffer[10];
for (int i = 0; i < 20; i++) {
buffer[i] = 0; // Buffer overflow!
}
// ✅ Good: Bounds checking
uint8_t buffer[10];
for (int i = 0; i < 10; i++) {
buffer[i] = 0;
}
Problem: Not freeing allocated memory Solution: Always free allocated memory
// ❌ Bad: Memory leak
void bad_function(void) {
uint8_t* buffer = malloc(1024);
// Use buffer...
// Forgot to free!
}
// ✅ Good: Proper cleanup
void good_function(void) {
uint8_t* buffer = malloc(1024);
if (buffer != NULL) {
// Use buffer...
free(buffer);
}
}
Problem: Using pointers after memory is freed Solution: Set pointers to NULL after freeing
// ❌ Bad: Dangling pointer
uint8_t* ptr = malloc(100);
free(ptr);
*ptr = 42; // Use-after-free!
// ✅ Good: Null pointer after free
uint8_t* ptr = malloc(100);
free(ptr);
ptr = NULL; // Prevent use-after-free
Next Steps: Explore Memory Management to understand memory allocation strategies, or dive into Pointers and Memory Addresses for low-level memory manipulation.