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
- 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
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
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
| Scheduler | Behavior | When to Use |
|---|---|---|
| Preemptive Priority | Highest ready priority always runs; interrupts lower | Real-time motor control, safety |
| Round-Robin | Equal-priority tasks share CPU in time slices | Multiple similar-priority tasks |
| Cooperative | Tasks must explicitly yield | Simple 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
- 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;
}
}
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 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
| Protocol | Best For | Typical Sensors/Actuators |
|---|---|---|
| I2C | Low-speed, many devices, short distance (<1m) | BME280, MPU6050, OLED displays |
| SPI | High-speed, few devices, ADC/DAC | MCP3008 ADC, SD cards, TFT displays |
| UART | Point-to-point, GPS, BT modules | GPS NEO-6M, HC-05 Bluetooth, ESP8266 |
| CAN | Noisy environments, multi-node, automotive | Distributed motor controllers, sensors |
| 1-Wire | Single-wire, long cable runs | DS18B20 temperature |
| RS-485 | Long distance (>1000m), industrial | Modbus 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.
- 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.