Back to Sensors & Actuators Series

Part 7: System Integration

July 14, 2025 Wasil Zafar 45 min read

Combining sensors and actuators into complete embedded systems — software architecture, RTOS concepts, state machines, and communication protocol orchestration.

Table of Contents

  1. System Architecture
  2. RTOS Fundamentals
  3. State Machines
  4. Protocol Selection
  5. Conclusion & Next Steps

System Architecture

Super Loop Pattern

The simplest embedded architecture — a single infinite loop that polls sensors, runs control algorithms, and drives actuators sequentially. Suitable for simple systems with relaxed timing requirements.

// Super loop architecture for sensor-actuator system
#include "stm32f4xx.h"
#include "sensor_drivers.h"
#include "motor_control.h"
#include "pid.h"

int main(void) {
    SystemInit();
    sensor_init();     // Initialize I2C, ADC, etc.
    motor_init();      // Initialize PWM, GPIO
    pid_init(&pid_speed, 0.5f, 2.0f, 0.01f, 0.01f, -100, 100);

    uint32_t last_tick = 0;

    while (1) {
        uint32_t now = HAL_GetTick();

        // 100 Hz control loop (10ms period)
        if (now - last_tick >= 10) {
            last_tick = now;

            // 1. Read sensors
            float temperature = read_temperature();
            float motor_rpm   = read_encoder_speed();
            float distance    = read_ultrasonic();

            // 2. Process / control algorithms
            float speed_cmd = pid_compute(&pid_speed, target_rpm, motor_rpm);

            // 3. Safety checks
            if (temperature > 80.0f || distance < 10.0f) {
                speed_cmd = 0;  // Emergency stop
            }

            // 4. Drive actuators
            motor_set_speed(speed_cmd);
        }

        // Low-priority tasks (logging, display, etc.)
        update_display();
    }
}

Layered Architecture

Recommended Software Layers:
  • HAL (Hardware Abstraction): Direct register access, GPIO, timers, peripherals. Platform-specific
  • Driver Layer: Sensor/actuator drivers using HAL. E.g. bme280_read(), motor_set_speed()
  • Service Layer: Control algorithms, filtering, calibration. Platform-independent
  • Application Layer: System behavior, state machines, user interface, communication
Embedded Software Architecture Layers
graph TD
    subgraph Application["Application Layer"]
        A1["State Machines"]
        A2["UI / Display"]
        A3["Communication"]
    end
    subgraph Service["Service Layer"]
        S1["Control Algorithms"]
        S2["Calibration"]
        S3["Filtering"]
    end
    subgraph Driver["Driver Layer"]
        D1["bme280_read()"]
        D2["motor_set_speed()"]
        D3["encoder_get_pos()"]
    end
    subgraph HAL["HAL Layer"]
        H1["GPIO"]
        H2["Timers"]
        H3["I2C / SPI"]
        H4["ADC / DAC"]
    end
    Application --> Service
    Service --> Driver
    Driver --> HAL
    HAL --> HW["🔌 Hardware"]
    style HW fill:#3B9797,stroke:#3B9797,color:#fff
    style Application fill:#e8f4f4,stroke:#3B9797,color:#132440
    style Service fill:#f0f4f8,stroke:#16476A,color:#132440
    style Driver fill:#e8f4f4,stroke:#3B9797,color:#132440
    style HAL fill:#f0f4f8,stroke:#16476A,color:#132440
                            

RTOS Fundamentals

Tasks & Scheduling

RTOS Task States
stateDiagram-v2
    [*] --> Ready : Task Created
    Ready --> Running : Scheduler Dispatch
    Running --> Ready : Preempted / Yield
    Running --> Blocked : Wait for Event
Semaphore / Queue / Delay Blocked --> Ready : Event Received
Timeout Expired Running --> Suspended : vTaskSuspend() Suspended --> Ready : vTaskResume() Running --> [*] : vTaskDelete()

A Real-Time Operating System (RTOS) allows multiple tasks to run concurrently with deterministic timing guarantees. Each task has its own stack and priority, and the scheduler preempts lower-priority tasks when higher-priority ones are ready.

RTOS Scheduling Types

SchedulerBehaviorWhen to Use
Preemptive PriorityHighest ready priority always runs; interrupts lowerReal-time motor control, safety
Round-RobinEqual-priority tasks share CPU in time slicesMultiple similar-priority tasks
CooperativeTasks must explicitly yieldSimple systems, no preemption needed

FreeRTOS Example

// FreeRTOS multi-task sensor-actuator system
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"

QueueHandle_t sensorQueue;
SemaphoreHandle_t i2cMutex;

// High-priority: Motor control at 1 kHz
void vMotorControlTask(void *pvParams) {
    TickType_t xLastWake = xTaskGetTickCount();

    for (;;) {
        float rpm = encoder_read_speed();
        float output = pid_compute(&pid, target_rpm, rpm);
        motor_set_pwm(output);

        vTaskDelayUntil(&xLastWake, pdMS_TO_TICKS(1));
    }
}

// Medium-priority: Sensor reading at 100 Hz
void vSensorTask(void *pvParams) {
    TickType_t xLastWake = xTaskGetTickCount();
    SensorData_t data;

    for (;;) {
        // Protect shared I2C bus
        if (xSemaphoreTake(i2cMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
            data.temperature = bme280_read_temp();
            data.humidity    = bme280_read_humidity();
            data.distance    = vl53l0x_read_distance();
            xSemaphoreGive(i2cMutex);

            xQueueSend(sensorQueue, &data, 0);
        }
        vTaskDelayUntil(&xLastWake, pdMS_TO_TICKS(10));
    }
}

// Low-priority: Logging and display
void vDisplayTask(void *pvParams) {
    SensorData_t data;

    for (;;) {
        if (xQueueReceive(sensorQueue, &data, pdMS_TO_TICKS(100))) {
            printf("T=%.1fC H=%.1f%% D=%dmm\n",
                   data.temperature, data.humidity, data.distance);
        }
    }
}

int main(void) {
    SystemInit();
    hardware_init();

    sensorQueue = xQueueCreate(10, sizeof(SensorData_t));
    i2cMutex = xSemaphoreCreateMutex();

    xTaskCreate(vMotorControlTask, "Motor", 256, NULL, 3, NULL);
    xTaskCreate(vSensorTask,       "Sensor", 512, NULL, 2, NULL);
    xTaskCreate(vDisplayTask,      "Display", 512, NULL, 1, NULL);

    vTaskStartScheduler();
    for (;;);
}

Synchronization Primitives

RTOS Synchronization:
  • Mutex: Mutual exclusion for shared resources (I2C bus, SPI bus, UART). Has priority inheritance to prevent inversion
  • Binary Semaphore: Signal from ISR to task. E.g., ADC complete ISR wakes processing task
  • Counting Semaphore: Track available resources (DMA channels, buffer slots)
  • Queue: Thread-safe FIFO for passing data between tasks (sensor readings, commands)
  • Event Groups: Wait on multiple conditions (sensor A ready AND sensor B ready)

State Machines

FSM Design Pattern

Finite State Machines (FSMs) model system behavior as discrete states with transitions triggered by events. They make complex control logic explicit, testable, and maintainable.

// Table-driven state machine for robotic gripper
#include <stdint.h>

typedef enum {
    STATE_IDLE,
    STATE_OPENING,
    STATE_OPEN,
    STATE_CLOSING,
    STATE_GRIPPING,
    STATE_ERROR,
    STATE_COUNT
} GripperState;

typedef enum {
    EVT_OPEN_CMD,
    EVT_CLOSE_CMD,
    EVT_LIMIT_OPEN,
    EVT_LIMIT_CLOSED,
    EVT_FORCE_EXCEEDED,
    EVT_RESET,
    EVT_COUNT
} GripperEvent;

typedef struct {
    GripperState next_state;
    void (*action)(void);
} Transition;

// Action functions
void start_open(void)    { motor_drive(MOTOR_OPEN, 80); }
void start_close(void)   { motor_drive(MOTOR_CLOSE, 60); }
void stop_motor(void)    { motor_stop(); }
void grip_hold(void)     { motor_drive(MOTOR_CLOSE, 30); }  // Reduced holding force
void error_stop(void)    { motor_stop(); led_set(LED_RED, 1); }
void error_clear(void)   { led_set(LED_RED, 0); }

// Transition table [current_state][event]
static const Transition fsm[STATE_COUNT][EVT_COUNT] = {
    /* STATE_IDLE */
    [STATE_IDLE][EVT_OPEN_CMD]  = { STATE_OPENING, start_open },
    [STATE_IDLE][EVT_CLOSE_CMD] = { STATE_CLOSING, start_close },

    /* STATE_OPENING */
    [STATE_OPENING][EVT_LIMIT_OPEN] = { STATE_OPEN, stop_motor },

    /* STATE_OPEN */
    [STATE_OPEN][EVT_CLOSE_CMD] = { STATE_CLOSING, start_close },

    /* STATE_CLOSING */
    [STATE_CLOSING][EVT_LIMIT_CLOSED]   = { STATE_GRIPPING, grip_hold },
    [STATE_CLOSING][EVT_FORCE_EXCEEDED] = { STATE_ERROR, error_stop },

    /* STATE_GRIPPING */
    [STATE_GRIPPING][EVT_OPEN_CMD]       = { STATE_OPENING, start_open },
    [STATE_GRIPPING][EVT_FORCE_EXCEEDED] = { STATE_ERROR, error_stop },

    /* STATE_ERROR */
    [STATE_ERROR][EVT_RESET] = { STATE_IDLE, error_clear },
};

static GripperState current_state = STATE_IDLE;

void fsm_process_event(GripperEvent evt) {
    const Transition *t = &fsm[current_state][evt];
    if (t->action) {
        t->action();
        current_state = t->next_state;
    }
}
Gripper Finite State Machine
stateDiagram-v2
    [*] --> IDLE
    IDLE --> OPENING : CMD_OPEN
    OPENING --> OPEN : Limit Switch Hit
    OPENING --> ERROR : Timeout / Overcurrent
    OPEN --> CLOSING : CMD_CLOSE
    CLOSING --> GRIPPING : Object Detected
    CLOSING --> ERROR : Timeout / Overcurrent
    GRIPPING --> OPENING : CMD_OPEN
    GRIPPING --> ERROR : Force Exceeded
    ERROR --> IDLE : CMD_RESET
    OPEN --> IDLE : CMD_RESET
                            

Hierarchical State Machines

When to Use Hierarchical State Machines (HSM):
  • When multiple states share common transitions (e.g., all operational states handle EMERGENCY_STOP the same way)
  • When states have sub-states (e.g., RUNNING → {ACCELERATING, CRUISE, DECELERATING})
  • Reduces state explosion: parent state handles common events, child overrides specific ones
  • Frameworks: QP (Quantum Platform), UML Statecharts, custom with function pointers

Protocol Selection

Bus Topology Design

Protocol Selection Guide

ProtocolBest ForTypical Sensors/Actuators
I2CLow-speed, many devices, short distance (<1m)BME280, MPU6050, OLED displays
SPIHigh-speed, few devices, ADC/DACMCP3008 ADC, SD cards, TFT displays
UARTPoint-to-point, GPS, BT modulesGPS NEO-6M, HC-05 Bluetooth, ESP8266
CANNoisy environments, multi-node, automotiveDistributed motor controllers, sensors
1-WireSingle-wire, long cable runsDS18B20 temperature
RS-485Long distance (>1000m), industrialModbus sensors/actuators

Multi-Sensor I2C Bus Management

// Multi-sensor I2C bus with error recovery
#include "stm32f4xx.h"

#define I2C_TIMEOUT 1000

typedef struct {
    uint8_t addr;
    const char *name;
    uint8_t (*init)(void);
    uint8_t (*read)(float *data);
    uint8_t online;
} I2C_Sensor;

static I2C_Sensor sensors[] = {
    { 0x76, "BME280",   bme280_init,   bme280_read,   0 },
    { 0x68, "MPU6050",  mpu6050_init,  mpu6050_read,  0 },
    { 0x23, "BH1750",   bh1750_init,   bh1750_read,   0 },
    { 0x29, "VL53L0X",  vl53l0x_init,  vl53l0x_read,  0 },
};
#define SENSOR_COUNT (sizeof(sensors) / sizeof(sensors[0]))

void i2c_bus_scan(void) {
    for (uint8_t i = 0; i < SENSOR_COUNT; i++) {
        if (i2c_ping(sensors[i].addr)) {
            sensors[i].online = sensors[i].init();
            printf("[OK] %s at 0x%02X\n", sensors[i].name, sensors[i].addr);
        } else {
            sensors[i].online = 0;
            printf("[--] %s not found\n", sensors[i].name);
        }
    }
}

void i2c_bus_recover(void) {
    // Toggle SCL 9 times to release stuck SDA
    // Then send STOP condition
    GPIO_TypeDef *port = GPIOB;
    uint16_t scl = 6, sda = 7;

    // Switch to GPIO mode temporarily
    port->MODER &= ~(3U << (scl * 2));
    port->MODER |=  (1U << (scl * 2));  // Output

    for (int i = 0; i < 9; i++) {
        port->BSRR = (1U << (scl + 16));  // SCL LOW
        for (volatile int d = 0; d < 100; d++);
        port->BSRR = (1U << scl);          // SCL HIGH
        for (volatile int d = 0; d < 100; d++);
    }
    // Re-init I2C peripheral
    i2c_init();
}

Conclusion & Next Steps

Integrating sensors and actuators into a cohesive system requires thoughtful software architecture. Whether using a super loop for simple systems or FreeRTOS for complex multi-task applications, the principles of layered design, state-driven behavior, and proper bus management apply universally.

Key Takeaways:
  • Super loop works for simple systems; RTOS enables deterministic multi-tasking
  • Use mutexes to protect shared buses (I2C, SPI) in multi-task environments
  • State machines make complex control logic explicit and testable
  • Choose communication protocols based on speed, distance, noise, and device count
  • Always implement I2C bus recovery for production robustness

In Part 8, we explore Real-World Applications — automotive ADAS, healthcare wearables, industrial automation, consumer electronics, and aerospace systems.