Skip to content

Embedded Software Architecture — From MicroPython to AUTOSAR

How software is organized in embedded systems — from your MicroPython robot to professional automotive and Linux systems. Understanding these patterns helps you work with ANY embedded platform.


The Universal Pattern

Despite massive differences in complexity, ALL embedded architectures share this structure:

Application Logic    ← "What the system does"
        |
   Middleware/OS      ← "How tasks are scheduled"
        |
   HAL / Drivers      ← "How to talk to hardware"
        |
   Hardware Regs      ← "What the silicon provides"
        |
   Physical World     ← "What we're controlling/sensing"

When you move from MicroPython to C to RTOS to Linux, the layers change but the pattern doesn't. Your P-control formula is the same. Your state machine is the same. Your I2C protocol is the same. Only the API for accessing them changes.

RP2350 Peripheral Bus

All architectures on the RP2350 talk to the same hardware through this bus structure:

RP2350 Peripheral Bus Architecture

The CPU cores access peripherals (PWM, I2C, SPI, etc.) through a shared bus. Each peripheral operates autonomously once configured — the CPU sets up registers, then the hardware runs independently.


Architecture 1: Your Robot (MicroPython + picobot)

MicroPython Architecture Layers

Layer What Example
Application Your robot code robot.line_follow(speed=80, kp=30)
Library picobot package motors.py, sensors.py, imu.py
Runtime MicroPython interpreter machine.PWM, machine.I2C, GC
Hardware RP2350 peripherals PWM slices, I2C controllers, GPIO mux

Characteristics: - Fastest development cycle (REPL, no compilation) - ~50–100× slower than C for computation - Non-deterministic timing (garbage collector pauses) - Perfect for learning and prototyping


Architecture 2: Bare Metal C (RP2350 C SDK)

Layer What Example
Application Your C code main.c: control loop, state machine
SDK / HAL Pico SDK functions pwm_set_wrap(), i2c_write_blocking()
Startup CMSIS / vector table Clock config, stack setup, NVIC
Registers Memory-mapped I/O *(uint32_t*)(0x40050004) = 999

Same PWM at three levels:

// SDK function (portable, safe):
pwm_set_wrap(slice, 999);

// SDK inline (slightly faster):
pwm_hw->slice[slice].top = 999;

// Direct register (fastest, chip-specific):
*(volatile uint32_t *)(0x40050000 + 0x10 * slice + 0x04) = 999;

Architecture 3: RTOS (FreeRTOS / Zephyr)

RTOS Task Scheduling

Layer What Example
Tasks Application threads motor_task (1ms), sensor_task (10ms)
Kernel Scheduler + synchronization Preemptive scheduler, semaphores, queues
BSP Board support package Pin mappings, clock config
HAL Hardware abstraction Same as bare metal C SDK

Key difference from your robot:

MicroPython (cooperative) RTOS (preemptive)
If motor task takes too long Everything waits Scheduler interrupts it
Timing guarantee Best effort Provable worst-case
Task isolation Shared globals Separate stacks, queues
// FreeRTOS: three tasks at different priorities
void motor_task(void *p)   { while(1) { pid_step(); vTaskDelay(1); } }   // 1ms, HIGH
void sensor_task(void *p)  { while(1) { read_imu(); vTaskDelay(10); } }  // 10ms, MEDIUM
void display_task(void *p) { while(1) { oled_update(); vTaskDelay(100);} }// 100ms, LOW

Architecture 4: Embedded Linux

Layer What Example
Userspace Applications, scripts Python, C++, Qt GUI
System calls Kernel interface open(), read(), ioctl()
Kernel Scheduler, memory, VFS Linux kernel + device tree
Drivers Hardware abstraction i2c-bcm2835, spidev, pinctrl
Hardware SoC peripherals I2C, SPI, GPIO, DMA, MMU

Same I2C read — Linux vs MicroPython:

# MicroPython (direct hardware access):
data = i2c.readfrom_mem(0x69, 0x0C, 6)

# Linux (through kernel driver):
fd = open("/dev/i2c-1", "rb+")
# ... ioctl to set address, write register, read data

Same bytes on the wire. Different software layers.

Linux in Embedded Systems Course

In the Linux in Embedded Systems course, you'll work with Raspberry Pi running Linux — writing device drivers, using sysfs, and understanding the kernel architecture.


Architecture 5: Automotive (AUTOSAR)

Layer What Example
Application (SWC) Software Components Engine control, ABS, dashboard
RTE Runtime Environment Port-based communication
BSW Services OS, Communication, Memory OSEK OS, CAN stack, NvM
BSW ECU Abstraction ECU-specific drivers CAN driver, ADC driver
MCAL Microcontroller Abstraction Register-level drivers
Hardware ECU + sensors + actuators CAN controller, injectors

Characteristics: - Standardized across ALL car manufacturers - Formally verified, safety-certified (ISO 26262) - Configuration-driven (XML tools generate C code) - WCET analysis required for every ISR


Comparison Table

Architecture Comparison

MicroPython Bare Metal C RTOS Linux AUTOSAR
Layers 4 3 4 5+ 6+
Language Python C C/C++ C/Python C (generated)
Timing Best effort Deterministic Hard real-time Soft RT Certified
Memory GC managed Manual Pool/stack Virtual memory Static
Debugging REPL, print GDB, JTAG RTOS-aware GDB gdb, ftrace Trace tools
Use case Education, IoT Sensors, simple Drones, robots Camera, UI Cars, medical

Useful Data Structures

Circular Buffer (Ring Buffer)

The most common embedded data structure — UART RX, sensor logs, DMA buffers:

class RingBuffer:
    def __init__(self, size):
        self.buf = [0] * size
        self.size = size
        self.head = 0    # Write position
        self.tail = 0    # Read position
        self.count = 0

    def put(self, value):
        if self.count < self.size:
            self.buf[self.head] = value
            self.head = (self.head + 1) % self.size
            self.count += 1

    def get(self):
        if self.count > 0:
            value = self.buf[self.tail]
            self.tail = (self.tail + 1) % self.size
            self.count -= 1
            return value

Why circular? Fixed memory (safe in ISR), O(1) read/write, wraps automatically.

Lookup Table (LUT)

Pre-computed values for fast math — avoids expensive calculations in control loops:

import math
SIN_TABLE = [int(math.sin(math.radians(d)) * 1000) for d in range(360)]

# In control loop — instant lookup instead of 100µs sin() call:
sin_value = SIN_TABLE[angle_degrees]  # ~1 µs

Fixed-Point Arithmetic

Fast integer math when you need fractions but can't afford floats:

# Fixed-point with 8 fractional bits (multiply by 256):
PI_FP = 804          # 3.14159 * 256 ≈ 804
radius_fp = 10 << 8  # 10.0 in fixed-point = 2560

result_fp = (PI_FP * radius_fp) >> 8  # = 31.4 (as integer 31)

Integer math is 10–100× faster than float on small MCUs.


Useful Algorithms

PID Controller

Your P-control extended with Integral (removes steady-state error) and Derivative (dampens overshoot):

class PID:
    def __init__(self, kp, ki, kd, out_min, out_max):
        self.kp, self.ki, self.kd = kp, ki, kd
        self.out_min, self.out_max = out_min, out_max
        self.integral = 0
        self.prev_error = 0

    def compute(self, error, dt):
        self.integral += error * dt
        derivative = (error - self.prev_error) / dt if dt > 0 else 0
        self.prev_error = error

        output = self.kp * error + self.ki * self.integral + self.kd * derivative
        return max(self.out_min, min(self.out_max, output))

Debounce

Filters electrical noise on button/sensor inputs:

class Debounce:
    def __init__(self, pin, stable_ms=50):
        self.pin = pin
        self.stable_ms = stable_ms
        self.last_raw = pin.value()
        self.stable = self.last_raw
        self.last_change = time.ticks_ms()

    def read(self):
        raw = self.pin.value()
        now = time.ticks_ms()
        if raw != self.last_raw:
            self.last_change = now
            self.last_raw = raw
        if time.ticks_diff(now, self.last_change) > self.stable_ms:
            self.stable = raw
        return self.stable

Moving Average Filter

Smooths noisy sensor readings:

class MovingAverage:
    def __init__(self, size):
        self.buf = [0] * size
        self.idx = 0
        self.total = 0
        self.n = 0

    def update(self, value):
        self.total -= self.buf[self.idx]
        self.buf[self.idx] = value
        self.total += value
        self.idx = (self.idx + 1) % len(self.buf)
        self.n = min(self.n + 1, len(self.buf))
        return self.total / self.n

Peripheral Access at Each Level

GPIO: Turn on an LED

Level Code
MicroPython Pin(25, Pin.OUT).value(1)
C SDK gpio_put(25, 1)
Register *(uint32_t*)(0xd0000014) = (1<<25)
Linux sysfs echo 1 > /sys/class/gpio/gpio25/value
AUTOSAR Dio_WriteChannel(LED, STD_HIGH)

I2C: Read IMU data

Level Code
MicroPython i2c.readfrom_mem(0x69, 0x0C, 6)
C SDK i2c_write_blocking(); i2c_read_blocking()
Linux ioctl(fd, I2C_SLAVE, 0x69); read(fd, buf, 6)

Same bytes on the wire. Same I2C protocol. Different wrappers.


Further Reading