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:
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
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
}
Bug 2 — Vertauschte Schwellenwerte
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);
}
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
5 — Zusammenfassung
| Komponente | Technologie | Status |
|---|---|---|
| Sprache | C (Bare-Metal) | ✅ |
| Zielarchitektur | ARM Cortex-M | ✅ |
| Cross-Compiler | arm-none-eabi-gcc | ✅ |
| Unit Tests | Unity Framework | ✅ |
| Hardware-Simulation | QEMU (lm3s6965evb) | ✅ |
| CI/CD | GitHub Actions | ✅ |
| Git Workflow | Feature/Fix Branches, Merges | ✅ |
| Debugging | objdump, QEMU Tracing | ✅ |
Skills
6 — Ausblick
- 1Echte Hardware
Migration auf ein echtes STM32-Board mit ST-Link Flashing — Linker-Script und Startup-Code sind bereits portierbar. - 2FreeRTOS Integration
Umstellung von Bare-Metal-Loop auf FreeRTOS-Tasks für nebenläufige Sensor-Abfrage und Pumpensteuerung. - 3Statische Codeanalyse
Integration von cppcheck oder PC-lint in die CI-Pipeline für automatische Code-Qualitätsprüfung. - 4Self-Hosted Runner mit echter Hardware
GitHub Actions Self-Hosted Runner verbunden mit physischem Board für echtes Hardware-in-the-Loop Testing.