Table of Contents
KEY TAKEAWAYS
- DIP: depend on abstractions (function pointers, interface structs), not on concrete implementations
- Pass dependencies in (via init functions or constructor parameters) rather than hardcoding them
- This makes your code testable (inject fakes), portable (swap hardware drivers), and flexible
- In C, the abstraction is typically a function pointer or a struct of function pointers defined in a header file
What Is the Dependency Inversion Principle?
The Dependency Inversion Principle (DIP) states: high-level modules should not depend on low-level modules. Both should depend on abstractions.In plain terms: your business logic should not directly calluart_send() or flash_write(). Instead, it should call through a function pointer or an interface struct that can be swapped out — for testing, for a different platform, or for a different driver.Example 1: Business Logic Directly Calling UART
Bad: High-level module depends on low-level module
/* alarm.c - directly calls uart functions */
#include "uart.h" /* Hard dependency on UART */
void alarm_check(int temperature) {
if (temperature > 80) {
uart_send_string("ALARM: Temperature too high!rn");
uart_send_int(temperature);
}
}Problems:- Cannot test
alarm_checkon a PC without a real UART - Cannot switch to Bluetooth, SPI display, or cloud logging without editing alarm.c
- alarm.c is tightly coupled to uart.c
Good: Inject the dependency through a function pointer
/* output.h - the abstraction */
typedef void (*output_fn)(const char *message);
/* alarm.c - depends on the abstraction, not on UART */
#include "output.h"
static output_fn notify = NULL;
void alarm_init(output_fn output_handler) {
notify = output_handler;
}
void alarm_check(int temperature) {
if (temperature > 80 && notify != NULL) {
char msg[64];
sprintf(msg, "ALARM: Temperature %d C - too high!", temperature);
notify(msg);
}
}
/* main.c - wires up the dependency */
#include "uart.h"
#include "alarm.h"
void uart_output(const char *msg) {
uart_send_string(msg);
uart_send_string("rn");
}
int main(void) {
alarm_init(uart_output); /* Inject UART */
/* ... */
}
/* test_alarm.c - inject a fake for testing */
static char last_message[64];
void fake_output(const char *msg) {
strcpy(last_message, msg);
}
void test_alarm_triggers(void) {
alarm_init(fake_output);
alarm_check(85);
assert(strstr(last_message, "ALARM") != NULL); /* Pass! */
}Now alarm.c has zero knowledge of UART. You can inject any output method: UART, Bluetooth, a test stub, or even a file logger.Example 2: Logging Through an Abstract Interface
Bad: Logger hardcoded to write to serial
/* logger.c */
#include "uart.h"
void log_info(const char *msg) {
uart_send_string("[INFO] ");
uart_send_string(msg);
uart_send_string("rn");
}
void log_error(const char *msg) {
uart_send_string("[ERROR] ");
uart_send_string(msg);
uart_send_string("rn");
}Good: Logger depends on an abstract output interface
/* logger.h - defines the logger interface */
typedef struct {
void (*write)(const char *message);
} log_output_t;
void logger_init(const log_output_t *output);
void log_info(const char *msg);
void log_error(const char *msg);
/* logger.c - uses the abstraction */
static const log_output_t *out = NULL;
void logger_init(const log_output_t *output) {
out = output;
}
void log_info(const char *msg) {
if (out && out->write) {
char buf[128];
snprintf(buf, sizeof(buf), "[INFO] %s", msg);
out->write(buf);
}
}
void log_error(const char *msg) {
if (out && out->write) {
char buf[128];
snprintf(buf, sizeof(buf), "[ERROR] %s", msg);
out->write(buf);
}
}
/* uart_output.c - one concrete implementation */
void uart_write(const char *message) {
uart_send_string(message);
uart_send_string("rn");
}
log_output_t uart_logger = { .write = uart_write };
/* sd_card_output.c - another implementation */
void sd_write(const char *message) {
sd_card_append_line(message);
}
log_output_t sd_logger = { .write = sd_write };
/* main.c - choose at startup */
logger_init(&uart_logger); /* Or &sd_logger */
log_info("System started");
log_error("Sensor timeout");The Dependency Direction
BAD (direct dependency):
alarm.c ──depends on──▶ uart.c
GOOD (inverted dependency):
alarm.c ──depends on──▶ output.h (abstraction)
uart.c ──implements──▶ output.h (abstraction)Both the high-level module (alarm) and the low-level module (UART) depend on the abstraction (output.h). Neither depends on the other directly.Key Takeaways
- DIP: depend on abstractions (function pointers, interface structs), not on concrete implementations
- Pass dependencies in (via init functions or constructor parameters) rather than hardcoding them
- This makes your code testable (inject fakes), portable (swap hardware drivers), and flexible
- In C, the abstraction is typically a function pointer or a struct of function pointers defined in a header file
Example 3: Sensor Abstraction for Testing
One of the biggest benefits of DIP is testability. When your code depends directly on hardware, testing requires real hardware. With an abstraction layer, you can swap in a fake.Bad — Direct Hardware Dependency
#include "i2c_driver.h"
#include "bmp280_registers.h"
float read_temperature(void) {
uint8_t raw[3];
i2c_read(BMP280_ADDR, BMP280_TEMP_REG, raw, 3);
int32_t adc_T = (raw[0] << 12) | (raw[1] <> 4);
// BMP280 compensation formula
float temp = compensate_temperature(adc_T);
return temp;
}
void check_overtemp(void) {
float t = read_temperature(); // Can't test without real BMP280
if (t > 85.0f) {
activate_cooling();
}
}You cannot unit test check_overtemp on your development machine — it calls real I2C hardware.Good — Inject the Sensor Interface
// sensor.h — the abstraction
typedef struct {
float (*read_temp)(void *ctx);
void *ctx;
} TempSensor;
// business logic depends only on the abstraction
void check_overtemp(const TempSensor *sensor) {
float t = sensor->read_temp(sensor->ctx);
if (t > 85.0f) {
activate_cooling();
}
}
// --- Production: real BMP280 ---
float bmp280_read(void *ctx) {
// real I2C read + compensation
return compensate_temperature(i2c_read_temp(ctx));
}
TempSensor hw_sensor = { .read_temp = bmp280_read, .ctx = &i2c_handle };
// --- Test: fake sensor ---
float fake_read(void *ctx) {
return *(float *)ctx; // return whatever value we set
}
void test_overtemp(void) {
float fake_temp = 90.0f;
TempSensor fake = { .read_temp = fake_read, .ctx = &fake_temp };
check_overtemp(&fake); // Now testable without hardware!
}The business logic never touches I2C directly. In production, you pass the real sensor. In tests, you pass a fake that returns any temperature you want. This is the power of DIP — your high-level logic is completely decoupled from low-level hardware details.DIP vs Dependency Injection
These terms are related but different:- Dependency Inversion Principle (DIP) is the design rule: high-level modules should not depend on low-level modules. Both should depend on abstractions.
- Dependency Injection (DI) is a technique to implement DIP: you pass (inject) dependencies from outside rather than creating them internally.
📖 Related: Hardware Abstraction Layer (HAL) Design in C — With Examples

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.







