Irrigation Controller · ARM Bare-Metal · CI/CD

C-Firmware für eine automatisierte Bewässerungssteuerung — Cross-Compilation für ARM Cortex-M, Unit-Tests mit Unity, Simulation in QEMU und vollständige CI/CD-Pipeline ohne reale Hardware.

GitHub Repository

Kontext & Motivation

Klassische Embedded-Entwicklung testet oft manuell auf echter Hardware — langsam und schwer zu automatisieren. Ziel dieses Projekts war es, einen vollständigen DevOps-Workflow für Bare-Metal-Firmware aufzubauen: Cross-Compilation, automatisierte Tests und Hardware-Simulation, alles reproduzierbar in einer CI-Pipeline.

Domäne

Embedded C, Bare-Metal, ARM Cortex-M Mikrocontroller

Testing

Unity Framework — native + cross-compiled Validierung

Simulation

QEMU — Hardware-Emulation ohne physische Karte

CI/CD

GitHub Actions — Build, Test, Simulation bei jedem Push

1 — Architektur

Drei Ebenen, von der Logik bis zur simulierten Hardware:

Irrigation Controller Architektur Drei Schichten: Geschaeftslogik in C, Cross-Compilation Toolchain, QEMU Hardware-Simulation, validiert durch CI/CD Geschäftslogik (irrigation_controller.c) Hardware-unabhängig, reiner C-Code Schwellenwerte, Pumpensteuerung, Sicherheits-Timer Unity Tests main.c + startup.c Native Build (gcc/cl) Schnelle Validierung auf PC 7/7 Tests bestanden ARM Cross-Compile arm-none-eabi-gcc linker.ld definiert Speicherlayout firmware.elf QEMU Simulation lm3s6965evb Cortex-M Emulation Ersatz für reale Hardware Boot ohne HardFault verifiziert GitHub Actions CI Native Tests → ARM Build → QEMU Boot-Check, bei jedem Push 7/7 Unit Tests Unity Framework 2 Bugs gefunden diagnostiziert + behoben CI durchgehend grün GitHub Actions

2 — Logik

Einfache, sicherheitsorientierte Zustandslogik ohne externe Abhängigkeiten:

#define HUMIDITY_LOW_THRESHOLD   30
#define HUMIDITY_HIGH_THRESHOLD  70
#define MAX_PUMP_RUNTIME_SECONDS 600

void irrigation_tick(irrigation_state_t* state, uint32_t elapsed_seconds) {
    if (state->sensor_status == SENSOR_ERROR) {
        state->pump_active = false;   // Fail-safe bei Sensorfehler
        return;
    }
    if (state->pump_runtime_seconds >= MAX_PUMP_RUNTIME_SECONDS) {
        state->pump_active = false;   // Sicherheits-Timer
        return;
    }
    if (!state->pump_active && state->humidity_percent < HUMIDITY_LOW_THRESHOLD) {
        state->pump_active = true;
    }
    if (state->pump_active && state->humidity_percent > HUMIDITY_HIGH_THRESHOLD) {
        state->pump_active = false;
    }
}

Bare-Metal Startup

ARM Cortex-M benötigt eine vollständige Interrupt-Vektortabelle und einen Reset-Handler, der main() aufruft:

__attribute__((section(".isr_vector")))
void (* const isr_vector[16])(void) = {
    (void (*)(void))(&_estack),
    Reset_Handler,
    Default_Handler,
    /* ... 16 Einträge minimum für Cortex-M */
};

3 — Echte Bugs während der Entwicklung

Bug 1 — HardFault beim Boot in QEMU

⚠️ Symptom: qemu: fatal: Lockup: can't escalate 3 to HardFault, Program Counter blieb bei 0x00000000 stecken.

Diagnose: objdump -t firmware.elf zeigte korrekt platzierte Symbole, aber qemu -d in_asm Tracing enthüllte, dass die Ausführung nie die Adresse 0x08000000 erreichte.

Root Cause: Linker-Script nutzte STM32-typische FLASH-Adresse (0x08000000), aber die QEMU-Maschine lm3s6965evb erwartet Code ab 0x00000000.

MEMORY
{
    FLASH (rx)  : ORIGIN = 0x00000000, LENGTH = 256K   /* vorher: 0x08000000 */
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}
✅ Fix: FLASH-Adresse korrigiert + Interrupt-Vektortabelle auf 16 Einträge erweitert.

Bug 2 — Vertauschte Schwellenwerte

⚠️ Symptom: Ein Commit vertauschte versehentlich HUMIDITY_LOW_THRESHOLD und HUMIDITY_HIGH_THRESHOLD — die bestehenden Tests bemerkten es zunächst nicht, da sie nur Extremwerte prüften.

Lösung: Ein neuer Sanity-Check-Test wurde hinzugefügt, der explizit die korrekte Reihenfolge der Schwellenwerte validiert — dieser Test deckte den Bug zuverlässig auf.

void test_thresholds_are_correctly_ordered(void) {
    TEST_ASSERT_TRUE(HUMIDITY_LOW_THRESHOLD < HUMIDITY_HIGH_THRESHOLD);
}
✅ Fix: Werte zurück auf LOW=30, HIGH=70 korrigiert — alle 7 Tests bestanden.

4 — CI/CD Pipeline

jobs:
  native-tests:        # Schnelle Validierung der Logik
    - gcc compile + run Unity tests

  arm-cross-compile:    # Echte Zielarchitektur
    needs: native-tests
    - install gcc-arm-none-eabi + qemu-system-arm
    - arm-none-eabi-gcc cross-compile -> firmware.elf
    - qemu-system-arm boot check (kein HardFault)
    - upload firmware.elf als Artifact
7/7
Unit Tests bestanden
2
Reale Bugs behoben
2
CI Jobs (native + ARM)
0
Physische Hardware nötig

5 — Zusammenfassung

KomponenteTechnologieStatus
SpracheC (Bare-Metal)
ZielarchitekturARM Cortex-M
Cross-Compilerarm-none-eabi-gcc
Unit TestsUnity Framework
Hardware-SimulationQEMU (lm3s6965evb)
CI/CDGitHub Actions
Git WorkflowFeature/Fix Branches, Merges
Debuggingobjdump, QEMU Tracing

Skills

C / Bare-Metal ARM Cortex-M Cross-Compilation Unity Testing QEMU GitHub Actions Linker Scripts Embedded Debugging Git Workflow Zustandsmaschinen DevOps for Embedded

6 — Ausblick