Table of Contents
KEY TAKEAWAYS
- A driver interface standardizes how your application interacts with hardware
- Define the interface as a struct of function pointers — the C equivalent of an abstract class
- Each hardware device gets its own implementation file
- Use a factory function to create the right driver based on configuration
- The application code never needs to know which specific hardware is connected
- This pattern combines Strategy, Factory, DIP, OCP, SRP, and Encapsulation into one practical architecture
What Is a Driver Interface?
A driver interface is a standardized API that lets your application work with different hardware devices through the same set of functions. Instead of writing separate code for each sensor, display, or communication chip, you define a common interface and implement it for each device.This is where multiple design principles come together: the Strategy pattern (swappable implementations), Factory pattern (creating the right driver), Dependency Inversion (depending on abstractions), and Open/Closed Principle (extending without modifying).A Complete Example: Sensor Driver Interface
Let us build a sensor system that supports multiple temperature sensor types (thermistor, BMP280, DHT22) through one common interface.Step 1: Define the Interface
// sensor_driver.h — the abstract interface
#ifndef SENSOR_DRIVER_H
#define SENSOR_DRIVER_H
#include <stdbool.h>
typedef enum {
SENSOR_OK = 0,
SENSOR_ERR_INIT,
SENSOR_ERR_READ,
SENSOR_ERR_TIMEOUT,
SENSOR_ERR_RANGE,
} SensorStatus;
typedef struct SensorDriver SensorDriver;
struct SensorDriver {
const char *name; // Human-readable name
SensorStatus (*init)(SensorDriver *self); // Initialize hardware
SensorStatus (*read)(SensorDriver *self, float *value); // Read a value
SensorStatus (*self_test)(SensorDriver *self); // Run self-diagnostics
void (*destroy)(SensorDriver *self); // Cleanup resources
void *ctx; // Private driver data
};
#endifThis interface does not know or care about any specific sensor. It defines what a sensor driver must do, not how.Step 2: Implement for a Thermistor (ADC-Based)
// thermistor_driver.c
#include "sensor_driver.h"
#include "hal_adc.h"
#include <stdlib.h>
#include <math.h>
typedef struct {
int adc_channel;
float r_series; // Series resistor value
float b_coefficient; // Beta coefficient
} ThermistorCtx;
static SensorStatus thermistor_init(SensorDriver *self) {
ThermistorCtx *ctx = (ThermistorCtx *)self->ctx;
hal_adc_init(ctx->adc_channel);
return SENSOR_OK;
}
static SensorStatus thermistor_read(SensorDriver *self, float *value) {
ThermistorCtx *ctx = (ThermistorCtx *)self->ctx;
int raw = hal_adc_read(ctx->adc_channel);
if (raw = 4095) return SENSOR_ERR_RANGE;
float resistance = ctx->r_series * (4095.0f / raw - 1.0f);
float steinhart = log(resistance / 10000.0f) / ctx->b_coefficient;
steinhart += 1.0f / 298.15f;
*value = 1.0f / steinhart - 273.15f;
return SENSOR_OK;
}
static SensorStatus thermistor_self_test(SensorDriver *self) {
float val;
SensorStatus st = thermistor_read(self, &val);
if (st != SENSOR_OK) return st;
if (val 125.0f) return SENSOR_ERR_RANGE;
return SENSOR_OK;
}
static void thermistor_destroy(SensorDriver *self) {
free(self->ctx);
free(self);
}
SensorDriver *thermistor_create(int channel, float r_series, float beta) {
SensorDriver *drv = malloc(sizeof(SensorDriver));
ThermistorCtx *ctx = malloc(sizeof(ThermistorCtx));
ctx->adc_channel = channel;
ctx->r_series = r_series;
ctx->b_coefficient = beta;
drv->name = "Thermistor";
drv->init = thermistor_init;
drv->read = thermistor_read;
drv->self_test = thermistor_self_test;
drv->destroy = thermistor_destroy;
drv->ctx = ctx;
return drv;
}Step 3: Implement for BMP280 (I2C-Based)
// bmp280_driver.c
#include "sensor_driver.h"
#include "hal_i2c.h"
#include <stdlib.h>
typedef struct {
int i2c_bus;
uint8_t address;
int32_t calib_data[6]; // Calibration coefficients
} BMP280Ctx;
static SensorStatus bmp280_init(SensorDriver *self) {
BMP280Ctx *ctx = (BMP280Ctx *)self->ctx;
hal_i2c_init(ctx->i2c_bus);
uint8_t chip_id;
hal_i2c_read_reg(ctx->i2c_bus, ctx->address, 0xD0, &chip_id, 1);
if (chip_id != 0x58) return SENSOR_ERR_INIT;
// Read calibration data from registers 0x88-0x9F
read_calibration(ctx);
return SENSOR_OK;
}
static SensorStatus bmp280_read(SensorDriver *self, float *value) {
BMP280Ctx *ctx = (BMP280Ctx *)self->ctx;
uint8_t raw[3];
// Trigger measurement
uint8_t ctrl = 0x27; // Normal mode, oversampling x1
hal_i2c_write_reg(ctx->i2c_bus, ctx->address, 0xF4, &ctrl, 1);
// Wait and read
delay_ms(50);
hal_i2c_read_reg(ctx->i2c_bus, ctx->address, 0xFA, raw, 3);
int32_t adc_T = (raw[0] << 12) | (raw[1] <> 4);
*value = compensate_temperature(adc_T, ctx->calib_data);
return SENSOR_OK;
}
SensorDriver *bmp280_create(int i2c_bus, uint8_t addr) {
SensorDriver *drv = malloc(sizeof(SensorDriver));
BMP280Ctx *ctx = malloc(sizeof(BMP280Ctx));
ctx->i2c_bus = i2c_bus;
ctx->address = addr;
drv->name = "BMP280";
drv->init = bmp280_init;
drv->read = bmp280_read;
drv->self_test = bmp280_self_test;
drv->destroy = bmp280_destroy;
drv->ctx = ctx;
return drv;
}Step 4: Factory for Sensor Selection
// sensor_factory.c
#include "sensor_driver.h"
SensorDriver *sensor_create(const char *type) {
if (strcmp(type, "thermistor") == 0) {
return thermistor_create(0, 10000.0f, 3950.0f);
}
if (strcmp(type, "bmp280") == 0) {
return bmp280_create(0, 0x76);
}
if (strcmp(type, "dht22") == 0) {
return dht22_create(GPIO_PORT_A, 4);
}
return NULL;
}Step 5: Application Code — Hardware Agnostic
// main.c — does not know or care which sensor is connected
#include "sensor_driver.h"
void run_sensor_system(SensorDriver *sensor) {
if (sensor->init(sensor) != SENSOR_OK) {
printf("Failed to init %s\n", sensor->name);
return;
}
if (sensor->self_test(sensor) != SENSOR_OK) {
printf("Self-test failed for %s\n", sensor->name);
return;
}
while (1) {
float temperature;
SensorStatus st = sensor->read(sensor, &temperature);
if (st == SENSOR_OK) {
printf("%s: %.1f Cn", sensor->name, temperature);
} else {
printf("%s: read error %d\n", sensor->name, st);
}
delay_ms(1000);
}
}
int main(void) {
// Configuration determines which sensor is used
SensorDriver *sensor = sensor_create("bmp280");
run_sensor_system(sensor);
sensor->destroy(sensor);
return 0;
}The application code is completely independent of the sensor hardware. Swapping from a thermistor to a BMP280 means changing one string. The run loop, error handling, and display logic remain untouched.Design Principles at Work
| Principle | Where It Appears |
|---|---|
| Strategy Pattern | Different read() implementations behind the same interface |
| Factory Pattern | sensor_create() builds the right driver from a type string |
| Dependency Inversion | Application depends on SensorDriver interface, not BMP280 or thermistor |
| Open/Closed Principle | Adding a new sensor means adding one .c file — no changes to application code |
| Single Responsibility | Each driver file handles one sensor type; the factory handles creation; the app handles business logic |
| Encapsulation | The void *ctx hides driver-specific data from the application |
Testing the Driver Interface
// mock_sensor.c — for unit testing
SensorDriver *mock_sensor_create(float fixed_value) {
// Returns a driver that always produces fixed_value
// No hardware needed — perfect for testing application logic
}By injecting a mock sensor, you can test the entire application — error handling, display logic, threshold checks — on your development machine without any hardware.Key Takeaways
- A driver interface standardizes how your application interacts with hardware
- Define the interface as a struct of function pointers — the C equivalent of an abstract class
- Each hardware device gets its own implementation file
- Use a factory function to create the right driver based on configuration
- The application code never needs to know which specific hardware is connected
- This pattern combines Strategy, Factory, DIP, OCP, SRP, and Encapsulation into one practical architecture

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.





