Firmware Development7 min read

How to Structure Maintainable Firmware Projects

Lafiz Maruf Rahman
Lafiz Maruf Rahman
March 18, 2025
How to Structure Maintainable Firmware Projects

I have seen firmware projects where everything — sensor drivers, communication protocols, application logic, hardware initialization — was crammed into a single 3,000-line main.c file. Adding a new feature meant scrolling through thousands of lines trying to figure out where to put it. Fixing a bug meant hoping you found all the places where the same logic was duplicated.

Good structure is not about following rules. It is about making your future self's life easier.

The layered approach

The most effective way to organize firmware is in layers. Each layer only talks to the layer directly below it.

At the bottom is the hardware — the actual microcontroller and its peripherals.

Above that is the Hardware Abstraction Layer (HAL). This wraps the hardware-specific code. Instead of writing GPIOA->BSRR = (1 << 5) everywhere, you write GPIO_Set(LED_PORT, LED_PIN). The HAL hides the hardware details.

Above the HAL are drivers — code that controls specific components like sensors, displays, and communication modules.

Above drivers are services — higher-level functionality like sensor management, data logging, and communication protocols.

At the top is the application — the business logic that makes your device do what it is supposed to do.

A practical folder structure

text
project/
├── app/              # Application logic, state machines
├── drivers/          # Component drivers (sensor, display, etc.)
├── services/         # Higher-level services
├── hal/              # Hardware abstraction layer
├── lib/              # Third-party libraries (FreeRTOS, etc.)
├── tests/            # Unit tests
└── docs/             # Documentation

The hardware abstraction layer in practice

Here is why the HAL matters. Suppose you write this in your application:

c
// Without HAL — hardware-specific code in application
GPIOA->BSRR = (1 << 5); // Set PA5 high

Now you need to port to a different microcontroller. You have to find every place you wrote GPIOA->BSRR and change it.

With a HAL:

c
// hal/gpio.h
void GPIO_Set(GPIO_Port port, uint8_t pin);
void GPIO_Clear(GPIO_Port port, uint8_t pin);
bool GPIO_Read(GPIO_Port port, uint8_t pin);

// Application code
GPIO_Set(LED_PORT, LED_PIN); // works on any hardware

When you port to new hardware, you only change the HAL implementation. The application code stays the same.

State machines for complex behavior

When your device has complex behavior — different modes, sequences of operations, error handling — a state machine makes it manageable:

c
typedef enum {
    STATE_IDLE,
    STATE_WARMING_UP,
    STATE_MEASURING,
    STATE_TRANSMITTING,
    STATE_ERROR
} DeviceState;

static DeviceState currentState = STATE_IDLE;

void RunStateMachine(void) {
    switch (currentState) {
        case STATE_IDLE:
            if (IsTimeToMeasure()) {
                PowerOnSensor();
                currentState = STATE_WARMING_UP;
            }
            break;
            
        case STATE_WARMING_UP:
            if (SensorReady()) {
                currentState = STATE_MEASURING;
            }
            break;
            
        // ... other states
    }
}

State machines make behavior explicit and easy to test. You can test each state transition independently.

One config file for all settings

Put all your configuration constants in one place:

c
// config.h
#define SAMPLE_INTERVAL_MS    10000   // 10 seconds
#define SENSOR_I2C_ADDRESS    0x48
#define UART_BAUD_RATE        115200
#define MAX_RETRY_COUNT       3

When a client asks you to change the sample rate, you change one number in one file. Not hunt through the codebase.

Good structure is an investment. It takes a bit more time upfront. But the first time you need to add a feature or fix a bug six months later, you will be glad you did it.

FirmwareSoftware ArchitectureCBest Practices

Ready to build something great?

Let's talk about your project. We will give you honest advice, a clear plan, and a fair price. No pressure, no sales pitch.

Free consultation
No commitment required
Response within 24 hours