Embedded C Programming
This guide covers C programming best practices for embedded systems, focusing on the Rock 5B+ platform.
Embedded C Fundamentals
Embedded C programming requires understanding of:
- Memory Management: Stack, heap, and static memory
- Hardware Interaction: Registers, interrupts, and peripherals
- Real-time Constraints: Timing, deadlines, and responsiveness
- Resource Limitations: CPU, memory, and power constraints
Memory Management
1. Memory Layout
┌─────────────────────────────────────┐
│ High Memory │
├─────────────────────────────────────┤
│ Stack (grows down) │
├─────────────────────────────────────┤
│ Heap (grows up) │
├─────────────────────────────────────┤
│ BSS (uninitialized) │
├─────────────────────────────────────┤
│ Data (initialized) │
├─────────────────────────────────────┤
│ Text (code) │
├─────────────────────────────────────┤
│ Low Memory │
└─────────────────────────────────────┘
2. Memory Allocation Strategies
// Static allocation (compile-time)
static int buffer[1000];
// Stack allocation (function scope)
void function() {
int local_buffer[100]; // Stack allocated
}
// Heap allocation (runtime)
int *dynamic_buffer = malloc(1000 * sizeof(int));
if (dynamic_buffer == NULL) {
// Handle allocation failure
}
free(dynamic_buffer);
Hardware Interaction
1. Register Access
// Define register addresses
#define GPIO_BASE 0xFE200000
#define GPIO_SET_OFFSET 0x1C
#define GPIO_CLR_OFFSET 0x28
// Define register pointers
volatile uint32_t *gpio_set = (volatile uint32_t *)(GPIO_BASE + GPIO_SET_OFFSET);
volatile uint32_t *gpio_clr = (volatile uint32_t *)(GPIO_BASE + GPIO_CLR_OFFSET);
// Set GPIO pin
void set_gpio_pin(int pin) {
*gpio_set = (1 << pin);
}
// Clear GPIO pin
void clear_gpio_pin(int pin) {
*gpio_clr = (1 << pin);
}
2. Bit Manipulation
// Set bit
#define SET_BIT(reg, bit) ((reg) |= (1 << (bit)))
// Clear bit
#define CLEAR_BIT(reg, bit) ((reg) &= ~(1 << (bit)))
// Toggle bit
#define TOGGLE_BIT(reg, bit) ((reg) ^= (1 << (bit)))
// Check bit
#define CHECK_BIT(reg, bit) (((reg) >> (bit)) & 1)
// Example usage
uint32_t control_register = 0;
SET_BIT(control_register, 5); // Set bit 5
CLEAR_BIT(control_register, 3); // Clear bit 3
if (CHECK_BIT(control_register, 5)) {
// Bit 5 is set
}
Interrupt Handling
1. Interrupt Service Routine
// Interrupt handler
volatile int interrupt_flag = 0;
void interrupt_handler(void) {
// Clear interrupt source
// Set flag for main loop processing
interrupt_flag = 1;
}
// Main loop
int main() {
while (1) {
if (interrupt_flag) {
// Process interrupt
interrupt_flag = 0;
}
// Other tasks
}
}
2. Critical Sections
// Disable interrupts
void disable_interrupts(void) {
__asm__ __volatile__ ("cpsid i" : : : "memory");
}
// Enable interrupts
void enable_interrupts(void) {
__asm__ __volatile__ ("cpsie i" : : : "memory");
}
// Critical section
void critical_section(void) {
disable_interrupts();
// Critical code here
enable_interrupts();
}
Real-time Programming
1. Timing Functions
#include <time.h>
#include <sys/time.h>
// High-resolution timing
struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);
// Your code here
clock_gettime(CLOCK_MONOTONIC, &end);
long elapsed = (end.tv_sec - start.tv_sec) * 1000000000L +
(end.tv_nsec - start.tv_nsec);
2. Task Scheduling
// Simple task scheduler
typedef struct {
void (*task)(void);
uint32_t period;
uint32_t last_run;
} task_t;
task_t tasks[] = {
{led_task, 100, 0},
{sensor_task, 50, 0},
{communication_task, 200, 0}
};
void scheduler(void) {
uint32_t current_time = get_time_ms();
for (int i = 0; i < 3; i++) {
if (current_time - tasks[i].last_run >= tasks[i].period) {
tasks[i].task();
tasks[i].last_run = current_time;
}
}
}
Performance Optimization
1. Loop Optimization
// Unoptimized loop
for (int i = 0; i < 1000; i++) {
array[i] = i * 2;
}
// Optimized loop (unroll by 4)
for (int i = 0; i < 1000; i += 4) {
array[i] = i * 2;
array[i+1] = (i+1) * 2;
array[i+2] = (i+2) * 2;
array[i+3] = (i+3) * 2;
}
2. Memory Access Optimization
// Cache-friendly access pattern
void process_array(int *array, int size) {
// Sequential access (good for cache)
for (int i = 0; i < size; i++) {
array[i] = process_element(array[i]);
}
}
// Avoid cache misses
void avoid_cache_misses(int **matrix, int rows, int cols) {
// Access by row (better cache locality)
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
matrix[i][j] = 0;
}
}
}
Error Handling
1. Return Codes
typedef enum {
SUCCESS = 0,
ERROR_INVALID_PARAM = -1,
ERROR_TIMEOUT = -2,
ERROR_MEMORY = -3
} error_code_t;
error_code_t read_sensor(int sensor_id, float *value) {
if (sensor_id < 0 || sensor_id >= MAX_SENSORS) {
return ERROR_INVALID_PARAM;
}
if (value == NULL) {
return ERROR_INVALID_PARAM;
}
// Read sensor
*value = sensor_read(sensor_id);
return SUCCESS;
}
2. Assertions
#include <assert.h>
void process_data(int *data, int size) {
assert(data != NULL);
assert(size > 0);
assert(size < MAX_SIZE);
// Process data
for (int i = 0; i < size; i++) {
data[i] = data[i] * 2;
}
}
Debugging Techniques
1. Print Debugging
#include <stdio.h>
#define DEBUG_ENABLED 1
#if DEBUG_ENABLED
#define DEBUG_PRINT(fmt, ...) printf("[DEBUG] " fmt "\n", ##__VA_ARGS__)
#else
#define DEBUG_PRINT(fmt, ...)
#endif
void debug_function(void) {
DEBUG_PRINT("Function called");
DEBUG_PRINT("Value: %d", some_value);
}
2. LED Debugging
// Use LEDs for debugging
void debug_led(int pattern) {
for (int i = 0; i < pattern; i++) {
set_led(1);
delay_ms(200);
set_led(0);
delay_ms(200);
}
}
// Usage
if (error_condition) {
debug_led(3); // 3 blinks for error
}
Best Practices
1. Code Style
// Use meaningful variable names
int sensor_temperature = 0;
int max_retry_count = 3;
// Use constants for magic numbers
#define MAX_BUFFER_SIZE 1024
#define TIMEOUT_MS 5000
// Use enums for related constants
typedef enum {
STATE_IDLE,
STATE_ACTIVE,
STATE_ERROR
} system_state_t;
2. Memory Safety
// Always check pointer validity
if (ptr != NULL) {
// Use pointer
}
// Initialize variables
int value = 0;
char buffer[MAX_SIZE] = {0};
// Use const where possible
const int MAX_ITEMS = 100;
3. Performance Tips
- Use appropriate data types
- Minimize function calls in loops
- Use lookup tables for calculations
- Optimize critical paths
- Profile your code
Common Pitfalls
1. Buffer Overflows
// Dangerous
char buffer[10];
strcpy(buffer, "This is too long"); // Buffer overflow!
// Safe
char buffer[10];
strncpy(buffer, "This is too long", sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
2. Integer Overflow
// Dangerous
uint8_t a = 200;
uint8_t b = 100;
uint8_t result = a + b; // Overflow!
// Safe
uint16_t result = (uint16_t)a + (uint16_t)b;
3. Uninitialized Variables
// Dangerous
int value; // Uninitialized
if (value > 0) { // Undefined behavior
// ...
}
// Safe
int value = 0; // Always initialize
if (value > 0) {
// ...
}