Panel For Example Panel For Example Panel For Example

Nine Common Software Architectures for Microcontrollers

Author : Adrian November 19, 2025

 Common Software Architectures for Microcontrollers

Many engineers, even after years of work, may not be familiar with software architecture. I became aware of it in my sixth year as an R&D engineer. Before that I worked on simple projects with a single main function; complex architectures felt like unnecessary overhead. Later I encountered more complex projects and found my previous skills were insufficient. Through project requirements I studied many solid code architectures, such as those from former colleagues, some module vendors' SDKs, and mature systems on the market. It may sound exaggerated, but one good project can accelerate growth more than several small projects. What software architectures does an engineer encounter on the path from novice to senior?

Below is an overview of nine common architectures, each with a simple example and progressively increasing difficulty.

1. Linear Architecture

This is the simplest programming method, the kind often used when learning to program. The following is a linear architecture example written in C:

#include   // Include register definitions for 8051 microcontrollers
// Delay function to generate a certain delay
void delay(unsigned int count) {
    unsigned int i;
    while(count--) {
        for(i = 0; i < 120; i++) {}  // Empty loop to produce delay
    }
}
void main() {
    // Initialize P1 port as output for controlling LEDs
    P1 = 0xFF;  // Set P1 port to high level to turn off all LEDs
    while(1) {  // Infinite loop
        P1 = 0x00;  // Set P1 port to low level to turn on all LEDs
        delay(500000);  // Call delay function to wait
        P1 = 0xFF;  // Set P1 port to high level to turn off all LEDs
        delay(500000);  // Call delay function again to wait the same time
    }
}

2. Modular Architecture

Modular architecture decomposes a program into independent modules, each performing a specific task. This helps code reuse, maintenance, and testing. The following C example simulates a simple traffic light control system.

#include   // Include register definitions for 8051 microcontrollers
// Define traffic light states
typedef enum {
    RED_LIGHT,
    YELLOW_LIGHT,
    GREEN_LIGHT
} TrafficLightState;
// Function declarations
void initializeTrafficLight(void);
void setTrafficLight(TrafficLightState state);
void delay(unsigned int milliseconds);
// Main function for traffic light control
void main(void) {
    initializeTrafficLight();  // Initialize traffic light
    while(1) {
        setTrafficLight(RED_LIGHT);
        delay(5000);  // Red for 5 seconds
        setTrafficLight(YELLOW_LIGHT);
        delay(2000);  // Yellow for 2 seconds
        setTrafficLight(GREEN_LIGHT);
        delay(5000);  // Green for 5 seconds
    }
}
// Initialize the traffic light
void initializeTrafficLight(void) {
    // Initialization code can be added here, such as setting port direction and default state
    // Assume P1 port is connected to the traffic light and initial state is off (high level)
    P1 = 0xFF;
}
// Set the traffic light state
void setTrafficLight(TrafficLightState state) {
    switch(state) {
        case RED_LIGHT:
            // Set red on, others off
            P1 = 0b11100000;  // Assuming active low: set P1.0 low, others high
            break;
        case YELLOW_LIGHT:
            // Set yellow on, others off
            P1 = 0b11011000;  // Set P1.1 low, others high
            break;
        case GREEN_LIGHT:
            // Set green on, others off
            P1 = 0b11000111;  // Set P1.2 low, others high
            break;
        default:
            // Default: turn off all lights
            P1 = 0xFF;
            break;
    }
}
// Delay function, parameter is milliseconds
void delay(unsigned int milliseconds) {
    unsigned int delayCount = 0;
    while(milliseconds--) {
        for(delayCount = 0; delayCount < 120; delayCount++) {
            // Empty loop to generate delay
        }
    }
}

3. Layered Architecture

Layered architecture decomposes a system into multiple layers, each responsible for different functionality.

#include   // Include register definitions for 8051 microcontrollers
// Define different operation levels
typedef enum {
    LEVEL_USER,
    LEVEL_ADMIN,
    LEVEL_SUPERUSER
} OperationLevel;
// Function declarations
void systemInit(void);
void performOperation(OperationLevel level);
void displayMessage(char* message);
// Main loop after system initialization
void main(void) {
    systemInit();  // System initialization
    // Simulate operations at different levels
    performOperation(LEVEL_USER);
    performOperation(LEVEL_ADMIN);
    performOperation(LEVEL_SUPERUSER);
    while(1) {
        // Main loop can be idle or handle other low-priority tasks
    }
}
// System initialization function
void systemInit(void) {
    // Initialize system resources, such as setting ports and interrupts
    // Specific initialization code omitted
}
// Execute operations for different levels
void performOperation(OperationLevel level) {
    switch(level) {
        case LEVEL_USER:
            // User operation implementation
            break;
        case LEVEL_ADMIN:
            // Admin operation implementation
            break;
        case LEVEL_SUPERUSER:
            // Superuser operation implementation
            break;
    }
}
// Display message function
void displayMessage(char* message) {
    // Actual display code is omitted because microcontrollers often lack direct screen output
    // Messages can be shown via LED blinking, serial output, or other methods
    // For example, LEDs on P1 could represent characters with blink patterns
    // Implementation depends on hardware design
}

4. Event-Driven Architecture

Event-driven architecture is a programming paradigm where program flow is triggered by events such as user input, sensor changes, or timer expirations. In microcontroller development it is typically used to respond to external hardware interrupts or software events. The following C example simulates LED control based on button input.

#include   // Include register definitions for 8051 microcontrollers
// Define key and LED ports
#define KEY_PORT P3  // Assume key connected to P3 port
#define LED_PORT P2  // Assume LED connected to P2 port
// Function declarations
void delay(unsigned int milliseconds);
bit checkKeyPress(void);  // Returns whether the key is pressed (1 pressed, 0 not pressed)
// Timer initialization function
timer0Init(void){
    TMOD = 0x01;  // Set timer mode register, use mode 1 (16-bit timer)
    TH0 = 0xFC;   // Set timer initial value to generate timer interrupt
    TL0 = 0x18;
    ET0 = 1;      // Enable timer 0 interrupt
    EA = 1;       // Enable global interrupt
    TR0 = 1;      // Start timer
}
// Timer interrupt service routine
timer0_ISR() interrupt 1{
    // Timer overflow will automatically reload the initial value if configured
    // Place code to execute after timer overflow here
}
// Key interrupt service routine
bit keyPress_ISR(void) interrupt 2 using 1{
    if(KEY_PORT != 0xFF) // Check if any key is pressed
    {
        LED_PORT = ~LED_PORT;  // Toggle LED state if a key is pressed
        delay(20);  // Debounce delay
        while(KEY_PORT != 0xFF);  // Wait for key release
        return 1;  // Return key pressed
    }
    return 0;  // Return 0 if no key pressed
}
// Delay function, parameter is milliseconds
void delay(unsigned int milliseconds) {
    unsigned int i, j;
    for(i = 0; i < milliseconds; i++)
        for(j = 0; j < 1200; j++);  // Empty loop to generate delay
}
// Main function
void main(void){
    timer0Init();  // Initialize timer
    LED_PORT = 0xFF;  // Initial LED off (assuming low level lights LED)
    while(1)
    {
        if(checkKeyPress())
        {  // Check for key press event
            // Additional handling can be added here when a key press is detected
        }
    }
}
// Check whether a key is pressed
bit checkKeyPress(void){
    bit keyState = 0;
    // Simulate key interrupt trigger; real application requires hardware interrupt
    if(1) // Assume key interrupt triggered
    {
      keyState = keyPress_ISR();  // Call key interrupt service routine
    }
    return keyState;  // Return key state
}

5. State Machine Architecture

State machines are commonly used in microcontroller development to handle complex logic and event sequences, such as user interface management and protocol parsing. The following C example implements a finite state machine (FSM) that simulates a simple vending machine state transition.

#include   // Include register definitions for 8051 microcontrollers
// Define vending machine states
typedef enum {
    IDLE,
    COIN_INSERTED,
    PRODUCT_SELECTED,
    DISPENSE,
    CHANGE_RETURNED
} VendingMachineState;
// Define events
typedef enum {
    COIN_EVENT,
    PRODUCT_EVENT,
    DISPENSE_EVENT,
    REFUND_EVENT
} VendingMachineEvent;
// Function declarations
void processEvent(VendingMachineEvent event);
void dispenseProduct(void);
void returnChange(void);
// Current state
VendingMachineState currentState = IDLE;
// Main function
void main(void){
    // Initialization code (if any)
    // ...
    while(1)
    {
        // Assume events are triggered externally; here we use a simulated event
        VendingMachineEvent currentEvent = COIN_EVENT; // Simulate coin insertion event
        processEvent(currentEvent);  // Handle the current event
    }
}
// Handle events
void processEvent(VendingMachineEvent event){
    switch(currentState)
    {
        case IDLE:
            if(event == COIN_EVENT)
            {
                // If idle and a coin is detected, move to coin inserted state
                currentState = COIN_INSERTED;
            }
            break;
        case COIN_INSERTED:
            if(event == PRODUCT_EVENT)
            {
                // If coin inserted and product selected, move to product selected state
                currentState = PRODUCT_SELECTED;
            }
            break;
        case PRODUCT_SELECTED:
            if(event == DISPENSE_EVENT)
            {
                dispenseProduct();  // Dispense product
                currentState = DISPENSE;
            }
            break;
        case DISPENSE:
            if(event == REFUND_EVENT)
            {
                returnChange();  // Return change
                currentState = CHANGE_RETURNED;
            }
            break;
        case CHANGE_RETURNED:
            // Wait for next cycle, return to IDLE
            currentState = IDLE;
            break;
        default:
            // If state is invalid, reset to IDLE
            currentState = IDLE;
            break;
    }
}
// Dispense product
dispenseProduct(void){
    // Add dispensing logic here, for example activating a motor to push the product
    // Assume P1 port is connected to the dispensing motor
    P1 = 0x00;  // Activate motor
    // ... dispensing logic
    P1 = 0xFF;  // Deactivate motor
}
// Return change
void returnChange(void){
    // Add change-return logic here, for example activating a mechanism to release coins
    // Assume P2 port is connected to the change mechanism
    P2 = 0x00;  // Activate mechanism
    // ... change-return logic
    P2 = 0xFF;  // Deactivate mechanism
}

6. Object-Oriented Style in C

Although C does not directly support object-oriented programming, concepts such as encapsulation and abstraction can be simulated using structs and function pointers. The following simplified example shows how to model an LED "class" in C for an 8051 microcontroller.

#include
// Define an LED "class"
typedef struct {
    unsigned char state;  // LED state
    unsigned char pin;    // Pin connected to LED
    void (*turnOn)(struct LED*);  // Method to turn on LED
    void (*turnOff)(struct LED*); // Method to turn off LED
} LED;
// Constructor for LED class
void LED_Init(LED* led, unsigned char pin) {
    led->state = 0;  // Default off
    led->pin = pin;   // Set connected pin
}
// Method to turn on LED
void LED_TurnOn(LED* led) {
    // Turn on LED based on pin
    if(led->pin < 8) {
        P0 |= (1 << led->pin);  // Assume P0.0 to P0.7 connect 8 LEDs
    } else {
        P1 &= ~(1 << (led->pin - 8));  // Assume P1.0 to P1.7 connect another 8 LEDs
    }
    led->state = 1;  // Update state to on
}
// Method to turn off LED
void LED_TurnOff(LED* led) {
    // Turn off LED based on pin
    if(led->pin < 8) {
        P0 &= ~(1 << led->pin);  // Turn off LED on P0
    } else {
        P1 |= (1 << (led->pin - 8));  // Turn off LED on P1
    }
    led->state = 0;  // Update state to off
}
// Main function
void main(void) {
    LED myLed;  // Create an LED object
    LED_Init(&myLed, 3);  // Initialize LED object on P0.3
    // Bind methods to the LED object
    myLed.turnOn = LED_TurnOn;
    myLed.turnOff = LED_TurnOff;
    // Use object-style calls to control LED
    while(1) {
        myLed.turnOn(&myLed);  // Turn on LED
        // Delay
        myLed.turnOff(&myLed); // Turn off LED
        // Delay
    }
}

7. Task-Based Architecture

This architecture decomposes the program into independent tasks, each performing a specific job. Without an RTOS, a simple polling scheduler can simulate a task-based approach. The example below implements a task-based architecture on an 8051 microcontroller.

#include
// Assume P1.0 is LED output
sbit LED = P1^0;
// Global variable to track system ticks
unsigned int systemTick = 0;
// Task function declarations
void taskLEDBlink(void);
void taskKeyScan(void);
// Timer0 ISR to generate system ticks
timer0_ISR() interrupt 1 using 1{
    // Timer overflow handling; update system tick counter
    systemTick++;  // Update system tick counter
}
// Task scheduler called from main loop to poll tasks
taskScheduler(void){
    // Check system tick to decide whether to run tasks
    // For example, run LED blink every 1000 ticks
    if (systemTick % 1000 == 0)
    {
       taskLEDBlink();
    }
    // Run key scan more frequently
    if (systemTick % 10 == 0)
    {
       taskKeyScan();
    }
}
// LED blink task
taskLEDBlink(void){
    static bit ledState = 0;  // Track current LED state
    ledState = !ledState;  // Toggle LED state
    LED = ledState;         // Update LED hardware
}
// Key scan task (implementation omitted)
taskKeyScan(void){
    // Key scanning logic
}
// Main function
void main(void){
    // Initialize LED state
    LED = 0;
    // Timer0 initialization
    TMOD &= 0xF0;  // Configure timer mode register, use mode 1 (16-bit timer/counter)
    TH0 = 0x4C;     // Set timer initial value; period depends on system clock
    TL0 = 0x00;
    ET0 = 1;        // Enable timer0 interrupt
    EA = 1;         // Enable interrupts
    TR0 = 1;        // Start timer0
    while(1)
    {
        taskScheduler();  // Call the task scheduler
    }
}

8. Agent (Proxy) Architecture

In the agent architecture, each agent is an independent entity encapsulating decision logic and data, and interacting with other agents. In practice, this requires multiple independent tasks or modules that communicate via message queues, events, or direct calls. This improves scalability and portability. The following is a simplified model with LED and key agents.

#include   // Include register definitions for 8051 microcontrollers
// Assume P3.5 is key input and P1.0 is LED output
sbit KEY = P3^5;
sbit LED = P1^0;
typedef struct{
    unsigned char pin;    // Pin associated with the agent
    void (*action)(void); // Agent behavior function
} Agent;
// Declarations for agent actions
void keyAction(void);
void ledAction(void);
// Agent array storing agents and their actions
Agent agents[] ={
    {5, keyAction},  // Key agent associated with P3.5
    {0, ledAction}   // LED agent associated with P1.0
};
// Key agent behavior
void keyAction(void){
    if(KEY == 0) // Check if key is pressed
    {
        LED = !LED;   // Toggle LED if key pressed
        while(KEY == 0);  // Wait for key release
    }
}
// LED agent behavior
void ledAction(void){
    static unsigned int toggleCounter = 0;
    toggleCounter++;
    if(toggleCounter == 500)  // Toggle LED every 500 clock cycles
    {
        LED = !LED;               // Toggle LED
        toggleCounter = 0;        // Reset counter
    }
}
// Main function
void main(void){
    unsigned char agentIndex;
    // Main loop
    while(1)
    {
        for(agentIndex = 0; agentIndex < sizeof(agents) / sizeof(agents[0]); agentIndex++)
        {
            // Call each agent's behavior function
            (*agents[agentIndex].action)(); // Note function pointer invocation
        }
    }
}

9. Component-Based Architecture

Component-based architecture decomposes software into independent, reusable components. Split the program into modules responsible for specific tasks, such as LED control, key handling, or sensor reading. Each component can be developed and tested independently and then composed into a complete system. The example below models LED control and key input as two components, with communication via direct function calls for simplicity.

#include   // Include register definitions for 8051 microcontrollers
// Component structure definition
typedef struct{
    void (*init)(void);      // Component initialization function
    void (*task)(void);      // Component task function
} Component;
// Assume P3.5 is key input and P1.0 is LED output
sbit KEY = P3^5;
sbit LED = P1^0;
// LED component
void LED_Init(void){
    LED = 0;  // Initialize LED state to off
}
void LED_Task(void){
    static unsigned int toggleCounter = 0;
    toggleCounter++;
    if (toggleCounter >= 1000) // Toggle LED every 1000 clock cycles
    {
        LED = !LED;                // Toggle LED state
        toggleCounter = 0;         // Reset counter
    }
}
// Key component
void KEY_Init(void){
    // Key initialization code
}
void KEY_Task(void){
    if (KEY == 0) // Check if key is pressed
    {
       LED = !LED;  // Toggle LED if key pressed
       while(KEY == 0);  // Wait for key release
    }
}
// Component array storing initialization and task functions
Component components[] ={
    {LED_Init, LED_Task},
    {KEY_Init, KEY_Task}
};
// System initialization: call all component init functions
void System_Init(void){
    unsigned char componentIndex;
    for (componentIndex = 0; componentIndex < sizeof(components) / sizeof(components[0]); componentIndex++)
    {
        components[componentIndex].init();
    }
}
// Main loop: call all component task functions
void main(void){
    System_Init();  // System initialization
    while(1)
    {
        unsigned char componentIndex;
        for (componentIndex = 0; componentIndex < sizeof(components) / sizeof(components[0]); componentIndex++)
        {
            components[componentIndex].task();  // Call component task
        }
    }
}