Embedded systems are everywhere—from your smartwatch to your car’s engine control unit. These systems are resource-constrained, requiring efficient and optimized code to perform tasks reliably. Programming for embedded systems often involves working with low-level hardware, real-time constraints, and limited memory. Here are some essential tricks and concepts to help you write better embedded system code in C.
1. Understand the Hardware
Before writing a single line of code, familiarize yourself with the hardware:
- Microcontroller Architecture: Know the CPU, memory, and peripherals.
- Datasheets: Study the microcontroller’s datasheet to understand registers, pin configurations, and timing requirements.
- Memory Map: Understand the memory layout (RAM, Flash, EEPROM).
Trick: Use volatile keyword for hardware registers to prevent compiler optimizations that could break your code.
define GPIO_PORT ((volatile unsigned int)0x40020000)2. Optimize Memory Usage
Embedded systems often have limited RAM and Flash memory. Use these tricks to save space:
- Use
constfor constants: Store constants in Flash instead of RAM.
const uint8_t LED_PATTERN[] = {0x01, 0x03, 0x07, 0x0F};- Avoid dynamic memory allocation: Use static or stack allocation instead of
malloc()to prevent fragmentation. - Pack data structures: Use
#pragma packor__attribute__((packed))to minimize padding.
struct __attribute__((packed)) SensorData {
uint8_t id;
uint32_t value;
};3. Bit Manipulation Tricks
Bitwise operations are essential for embedded systems to control hardware registers and save memory:
- Set a bit:
PORT |= (1 << PIN); // Set PIN high- Clear a bit:
PORT &= ~(1 << PIN); // Set PIN lowToggle a bit:
PORT ^= (1 << PIN); // Toggle PINCheck a bit:
if (PORT & (1 << PIN)) { /* PIN is high */ }Trick: Use bitfields for compact representation of flags or settings.
struct {
uint8_t flag1 : 1;
uint8_t flag2 : 1;
uint8_t reserved : 6;
} status;4. Use Inline Functions and Macros
Inline Functions
Inline functions are a way to suggest to the compiler to replace the function call with the actual code of the function. This reduces the overhead of function calls, which is especially useful in embedded systems where performance and memory are critical.
Example: Inline Function for Delay
static inline void delay(uint32_t ms) {
for (uint32_t i = 0; i < ms * 1000; i++) {
// Introduce a small delay by doing nothing
__asm__ volatile ("nop"); // No operation (assembly instruction)
}
}Why use inline?
- Reduces function call overhead.
- Improves performance for small, frequently called functions.
When to use inline?
- For small functions that are called frequently.
- When the function is simple and doesn’t involve complex logic.
Macros
Macros are preprocessor directives that replace text before compilation. They are powerful but should be used carefully to avoid unexpected behavior.
Example: Macro for Minimum Value
#define MIN(a, b) ((a) < (b) ? (a) : (b))Usage:
uint8_t x = 10, y = 20;
uint8_t min_value = MIN(x, y); // min_value will be 10Caution with Macros:
- Macros don’t perform type checking, which can lead to bugs.
- Always use parentheses around arguments to avoid precedence issues.
Example: Debugging Macro
#define DEBUG_LOG(message) \
do { \
printf("[DEBUG] %s:%d: %s\n", __FILE__, __LINE__, message); \
} while (0)Usage:
DEBUG_LOG("Starting main loop"); // Output: [DEBUG] main.c:25: Starting main loop5. Interrupt Handling
Interrupts are essential for real-time systems. They allow the microcontroller to respond immediately to external events without polling.
Best Practices for Interrupt Service Routines (ISRs):
- Keep ISRs Short: ISRs should execute quickly and return control to the main program.
- Use
volatilefor Shared Variables: This ensures the compiler doesn’t optimize away reads/writes to variables shared between the ISR and main code. - Disable Interrupts in Critical Sections: Prevent race conditions by disabling interrupts when accessing shared resources.
Example: Simple ISR
volatile uint8_t button_pressed = 0;
void EXTI0_IRQHandler(void) {
if (EXTI->PR & (1 << 0)) { // Check if interrupt is from EXTI0
button_pressed = 1; // Set flag
EXTI->PR |= (1 << 0); // Clear pending bit
}
}Main Code:
while (1) {
if (button_pressed) {
// Handle button press
button_pressed = 0; // Clear flag
}
}6. Optimize Loops and Conditions
Loop Unrolling
Loop unrolling reduces the overhead of loop control by executing multiple iterations in a single loop cycle.
Example: Loop Unrolling
// Normal loop
for (int i = 0; i < 4; i++) {
data[i] = 0;
}
// Unrolled loop
data[0] = 0;
data[1] = 0;
data[2] = 0;
data[3] = 0;When to use loop unrolling?
- When the loop has a small, fixed number of iterations.
- When performance is critical.
Avoid Floating-Point Operations
Floating-point operations are computationally expensive on most microcontrollers. Use fixed-point arithmetic instead.
Example: Fixed-Point Arithmetic
int16_t temperature_raw = 512; // Raw ADC value
int16_t temperature_celsius = (temperature_raw * 100) >> 10; // Convert to Celsius (fixed-point)7. Debugging and Logging
LED Debugging
Use LEDs to indicate the state of the program. This is a simple and effective way to debug hardware.
Example: LED Debugging
#define LED_ON() (GPIO_PORT |= (1 << LED_PIN))
#define LED_OFF() (GPIO_PORT &= ~(1 << LED_PIN))
void indicate_error() {
for (int i = 0; i < 3; i++) {
LED_ON();
delay(500);
LED_OFF();
delay(500);
}
}Serial Logging
Implement a lightweight logging mechanism using UART.
Example: UART Logging
void uart_send_char(char c) {
while (!(UART->SR & UART_SR_TXE)); // Wait for TX buffer to be empty
UART->DR = c; // Send character
}
void log(const char* message) {
while (*message) {
uart_send_char(*message++);
}
uart_send_char('\n'); // Newline
}Usage:
log("System initialized");8. Power Optimization
Sleep Modes
Use low-power modes to save energy when the system is idle.
Example: Entering Sleep Mode
void enter_sleep_mode() {
SCB->SCR |= SCB_SCR_SLEEPONEXIT_Msk; // Enable sleep-on-exit
__WFI(); // Wait for interrupt
}Clock Gating
Disable clocks for unused peripherals to save power.
Example: Disabling Peripheral Clocks
RCC->AHB1ENR &= ~(RCC_AHB1ENR_GPIOAEN); // Disable GPIOA clock9. Testing and Validation
Unit Testing
Test individual modules in isolation to ensure they work as expected.
Example: Unit Test for a Function
int add(int a, int b) {
return a + b;
}
void test_add() {
assert(add(2, 3) == 5);
assert(add(-1, 1) == 0);
}Hardware-in-the-Loop (HIL) Testing
Test your code on actual hardware to validate its behavior.
Static Analysis
Use tools like cppcheck or PC-lint to catch potential issues in your code.
Conclusion
By applying these detailed tricks and techniques, you can write efficient, reliable, and maintainable embedded system code. Always remember to test thoroughly and optimize for both performance and power consumption.