Unit-Testing für Embedded C auf PIC-Hardware klingt im ersten Moment nach „Enterprise-Disziplin“, ist aber in der Praxis eine der schnellsten Möglichkeiten, Firmware stabiler, wartbarer und schneller entwickelbar zu machen. Gerade bei PIC16/PIC18 (XC8) und auch bei PIC24/dsPIC (XC16) oder PIC32 (XC32) führen kleine Änderungen oft zu unerwarteten Seiteneffekten: ein Timer-Reload ist off, ein Zustandsautomat läuft in einen seltenen Pfad, eine ISR setzt ein Flag zu spät, oder ein Treiber verhält sich nach einem Reset anders als erwartet. Unit-Tests helfen, solche Fehler früh zu entdecken – idealerweise bevor Sie den Debugger anschließen oder stundenlang mit dem Oszilloskop suchen. Der Kern ist dabei nicht, „alles zu testen“, sondern gezielt die Logik von der Hardware zu trennen: Berechnungen, Zustandsautomaten, Protokoll-Parser, Plausibilitätsprüfungen und Fehlermanagement lassen sich hervorragend automatisiert prüfen. Und selbst hardware-nahe Treiber können Sie testen, wenn Sie die Registerzugriffe kapseln und Abhängigkeiten sauber injizieren. Dieser Artikel zeigt Ihnen, wie Sie Unit-Testing in Embedded C auf PIC-Hardware realistisch umsetzen: mit hostbasierten Tests für Geschwindigkeit, targetbasierten Tests für Timing und Integration sowie einem pragmatischen Setup in MPLAB X, das sich auch im kleinen Team ohne Overhead pflegen lässt.
Was Unit-Tests im Embedded-Kontext leisten – und was nicht
Ein Unit-Test prüft eine kleine Funktionseinheit („Unit“) isoliert von der Umgebung. In Embedded C bedeutet das typischerweise: Funktionen, die Eingaben verarbeiten und definierte Ausgaben oder Zustandsänderungen erzeugen. Das ist nicht dasselbe wie ein Systemtest oder ein Hardwaretest.
- Unit-Test: prüft Logik isoliert (z. B. Parser, Filter, State Machine, Grenzwerte).
- Integrationstest: prüft Zusammenspiel von Modulen (z. B. Treiber + Protokoll + Applikation).
- Hardwaretest: prüft reale Signale, Timing, elektrische Eigenschaften (z. B. SPI-Signalqualität, ADC-Rauschen).
Für PIC-Projekte ist die Kombination entscheidend: Host-Unit-Tests liefern schnelle Rückmeldung (Sekunden statt Minuten), Target-Tests auf PIC-Hardware liefern Sicherheit in Bezug auf Compiler, Optimierung, Registerverhalten und reale Randbedingungen.
Warum PIC-Firmware besonders von Unit-Tests profitiert
PIC-Projekte haben typische Eigenschaften, die Unit-Testing besonders wertvoll machen:
- Begrenzte Ressourcen: Kleine RAM-/Flash-Budgets erzwingen Optimierungen, die Nebenwirkungen haben können.
- Interrupt-Last: Viele Fehler entstehen durch seltene Interleavings zwischen ISR und Mainloop.
- Konfigurationsvielfalt: Fuses/Config-Bits, Clock-Quellen, BOR/WDT – kleine Änderungen beeinflussen den Startpfad.
- Hardwareabhängigkeit: Registerzugriffe, PPS/Pin-Mapping, Peripherievarianten zwischen PICs.
Unit-Tests bringen Struktur: Sie zwingen zu sauberem Design (klare Schnittstellen) und reduzieren „Trial and Error“ im Debugging.
Teststrategie in zwei Ebenen: Host und Target
Ein praxistauglicher Ansatz teilt Tests in zwei Kategorien auf:
- Host-basierte Unit-Tests: laufen auf dem PC (z. B. mit GCC/Clang). Sehr schnell, ideal für Logik.
- Target-basierte Unit-Tests: laufen auf dem PIC. Ideal für hardware-nahe Logik, Timing, Compiler-/ABI-Eigenheiten.
Der entscheidende Vorteil: Sie müssen nicht alles auf dem PIC testen. Die Mehrheit der Fehler sitzt in Logik, nicht in der elektrischen Schicht. Wenn Sie 70–90 % Ihrer Logik hostbasiert prüfen, reduzieren Sie die Debug-Zeit drastisch. Die verbleibenden 10–30 % testen Sie gezielt auf Hardware.
Voraussetzung: Hardwarezugriffe entkoppeln
Unit-Testing scheitert selten am Testframework, sondern an der Struktur des Codes. Wer überall direkt auf SFRs (Special Function Registers) zugreift, kann isoliert kaum testen. Die Lösung ist Entkopplung über klare Schichten:
- HAL (Hardware Abstraction Layer): kapselt Registerzugriffe und Peripherie-Details.
- Treiber-Schicht: nutzt HAL-Funktionen, bietet funktionale API (z. B.
uart_write()). - Applikation: nutzt Treiber, enthält Business-Logik und Zustandsautomaten.
Für Unit-Tests mocken Sie die HAL- oder Treiber-Schicht. Dadurch testen Sie die Logik deterministisch, ohne echte Hardware.
Dependency Injection in C: pragmatisch statt akademisch
In Embedded C braucht es kein komplexes Pattern. Oft reichen Funktionszeiger oder ein Interface-Struct. Beispielgedanke: Statt in der Logik direkt PORTBbits.RB0 zu lesen, rufen Sie io_read_pin(PIN_X) auf. Im Test ersetzen Sie diese Funktion durch einen Mock, der definierte Werte zurückgibt.
Testframeworks: Was sich in Embedded C bewährt
Für C sind in der Embedded-Welt besonders verbreitet:
- Unity: leichtgewichtiges C-Testframework, gut für Embedded. Unity Projektseite
- CMock: Mock-Generator, häufig in Kombination mit Unity. CMock Projektseite
- Ceedling: Build-/Test-Tooling um Unity/CMock herum, besonders komfortabel am Host. Ceedling Projektseite
Alternativ gibt es Frameworks wie CppUTest (für C/C++) oder GoogleTest (primär C++). Für reine XC8-Projekte ist ein C-Framework mit minimalem Overhead oft die beste Wahl.
Hostbasierte Tests einrichten: Schnellster ROI
Hostbasierte Tests bedeuten: Sie kompilieren Ihre Logik-Module mit einem PC-Compiler und führen die Tests lokal aus. Das geht schnell, ist ideal für CI und liefert sofort Feedback.
- Trennen Sie „pure C“ von MCU-Spezifika: Keine SFR-Header, keine compilerabhängigen Attribute im Logikmodul.
- Nutzen Sie Standardtypen:
stdint.hund klar definierte Einheiten (z. B. mV, 0,1°C). - Isolieren Sie Zeit: Keine echten Delays im Logikcode; Zeit als Parameter oder Tick-Quelle einspeisen.
Wenn Sie MPLAB X als IDE nutzen, können Sie dennoch hostbasiert testen, indem Sie ein separates Testprojekt anlegen oder die Tests außerhalb von MPLAB (z. B. per Makefile) laufen lassen und MPLAB für Firmware-Build/Debug nutzen. Als Einstieg in die Toolchain eignen sich die offiziellen Microchip-Seiten zu MPLAB X IDE und den MPLAB XC Compilern.
Targetbasierte Unit-Tests: Auf dem PIC testen, ohne Overhead zu explodieren
Target-Tests sind sinnvoll, wenn Sie:
- Timing- und ISR-Verhalten validieren müssen (z. B. Timer-ISR setzt Flag exakt einmal pro Tick).
- Compiler-/Optimierungs-Effekte prüfen wollen (volatile, Bitfelder, Memory-Layout).
- Peripherie-Interaktion verifizieren (UART-Registerfolge, ADC-Read-Sequenzen).
Target-Tests laufen als Test-Firmware auf dem PIC. Die Ergebnisse geben Sie über UART, USB-CDC, SWO (bei anderen MCUs) oder auch über ein einfaches GPIO-Protokoll aus. Häufig reicht eine serielle Ausgabe, die pro Test „PASS/FAIL“ ausgibt.
Test-Harness: Minimalistisch halten
Auf 8-Bit-PICs ist Speicher knapp. Ein Test-Harness sollte daher klein sein:
- Keine dynamische Speicherverwaltung (kein
malloc). - Kurze Testnamen oder numerische IDs.
- Test-Suites selektiv (z. B. nur Treiber A oder nur Boot-Tests).
- Ausgabe sparsam (nur Fehlerdetails, nicht jedes Detail loggen).
Mocks und Stubs: Hardware simulieren, ohne Hardware zu verlieren
Ein Mock ist eine „intelligente“ Ersatzimplementierung, die Aufrufe protokolliert und Erwartungen prüft. Ein Stub ist eine einfache Ersatzfunktion, die definierte Werte zurückliefert. Für Embedded-Unit-Tests sind beide wichtig:
- Stub: Perfekt für Sensorwerte, Zeitquellen, Flags (z. B.
adc_read()liefert 512). - Mock: Perfekt für Protokolle und Sequenzen (z. B. „UART_Write muss genau 3 Bytes senden“).
Ein typisches Muster ist, pro Hardwaremodul ein Interface zu definieren (z. B. uart_if) und in der Applikation nur über dieses Interface zu arbeiten. Im Test setzen Sie das Interface auf Mock-Implementierungen.
Was sollte auf PIC-Ebene unit-getestet werden?
Wenn Sie nur dort testen wollen, wo es wirklich zählt, priorisieren Sie:
- Zustandsautomaten: Übergänge, Timeouts, Fehlerpfade, Wiederanlauf nach Reset.
- Parser/Protokolle: CRC, Framing, Escaping, Timeouts, ungültige Frames.
- Skalierung und Grenzwerte: ADC → Einheit, Clamping, Rundung, Überläufe.
- Fehlerbehandlung: WDT-Reset-Pfade, BOR-Recovery, Safe-State-Logik.
- Treiber-Sequenzen: Initialisierung (Registerreihenfolge), Start/Stop, Interrupt-Enable/Disable.
Weniger sinnvoll als Unit-Tests (aber sinnvoll als Integrationstest) sind z. B. reine „Signalqualitäts“-Themen wie SPI-Flanken oder EMV-Probleme – das sind Messaufgaben, keine Unit-Tests.
Testdaten und Grenzfall-Design: So finden Sie echte Bugs
Der Nutzen von Unit-Testing hängt stark von den Testfällen ab. Gute Tests decken nicht nur den Standardfall ab, sondern besonders Grenzfälle:
- Min/Max-Werte: 0, Maximum des ADC, negative Werte (wenn signed), Überlaufbereiche.
- Off-by-one: Index = 0, Index = N-1, Übergänge zwischen Segmenten.
- Timing-Grenzen: Timeout exakt erreicht, knapp darunter, knapp darüber.
- Fehlformate: Protokollframes mit falscher Länge, falscher CRC, unerwarteten Bytes.
Wenn Sie eine Look-up-Tabelle nutzen, testen Sie gezielt die Interpolationsgrenzen und Segmentwechsel. Wenn Sie Festkomma nutzen, testen Sie Überläufe und Rundung.
Mess- und Ausgabekanäle für Target-Tests
Damit Target-Unit-Tests praktisch nutzbar sind, brauchen Sie eine zuverlässige Ausgabe des Ergebnisses. Bewährte Optionen:
- UART: Einfach, robust, schnell integrierbar. Ideal für „PASS/FAIL“-Logs.
- USB-CDC (falls verfügbar): Komfortabel, aber Setup komplexer.
- GPIO-Codes: Für Minimaltests: LED blinkt Fehlercode, oder Bits auf Port ausgeben.
- Debugger-Schnittstellen: In manchen Setups kann der Debugger Variablen auslesen, ist aber timing-invasiv.
Praktisch ist ein fester, maschinenlesbarer Output (z. B. eine Zeile pro Test). So können Sie später automatisiert auswerten, ob ein Build „grün“ ist.
CI-Denken ohne Overhead: Automatisierung für PIC-Teams
Auch ohne großes DevOps-Setup profitieren Teams enorm von Automatisierung. Ein pragmatischer Minimalstandard:
- Host-Unit-Tests bei jedem Commit (schnell, zuverlässig, gut als Qualitätsgate).
- Target-Smoke-Tests regelmäßig (z. B. vor Releases oder täglich), weil sie Hardwareabhängigkeiten abdecken.
- Versionierte Test-Firmware (damit klar ist, was genau auf der Hardware lief).
Wenn Sie Git nutzen, definieren Sie eine klare Struktur: /src, /tests, /hal, /drivers und /app. Das macht Builds reproduzierbar und reduziert „versteckte“ Abhängigkeiten.
Typische Stolperfallen bei Unit-Testing auf PICs
- Zu viel in ISR testen: ISR-Logik ist schwer isoliert testbar. Besser: ISR setzt Flags, Logik läuft im Mainloop und ist testbar.
- Direkte Registerzugriffe überall: Ohne HAL keine sauberen Tests. Erst kapseln, dann testen.
- Compiler-spezifische Konstrukte im Logikcode: Host-Tests brechen. Besser: Compiler-spezifisches in Plattformschicht auslagern.
- Timing durch Debugger verfälscht: Für Boot-/Timingtests lieber GPIO-Marker und serielles Logging nutzen.
- Testcode wird Produktionscode: Tests gehören in separate Targets/Build-Konfigurationen, nicht in Release-Builds.
Designregeln, die Unit-Testing automatisch erleichtern
- Pure Functions bevorzugen: Funktionen ohne Seiteneffekte sind am einfachsten testbar.
- Einheiten konsequent: Werte immer in definierten Einheiten (mV, 0,1°C), keine „magischen“ Skalierungen.
- Zustandsautomaten explizit: State und Events als klare Typen, Übergänge als Funktionen.
- Abhängigkeiten injizieren: Zeit, IO, Kommunikation über Interfaces oder Funktionszeiger.
Mit diesen Regeln entstehen Libraries und Module, die nicht nur testbar sind, sondern auch leichter zu portieren – etwa von PIC16 auf PIC18 oder von XC8 auf XC16/XC32.
Outbound-Links für Einstieg und Vertiefung
- MPLAB X IDE – offizielle Entwicklungsumgebung
- MPLAB XC Compilers – XC8/XC16/XC32 Toolchain
- Unity – leichtgewichtiges Unit-Testframework für C
- CMock – Mock-Generator für C (ideal für HAL-Mocking)
- Ceedling – Test-Runner/Build-Tooling rund um Unity/CMock
- Unit Testing – Begriff und grundlegende Einordnung
IoT-PCB-Design, Mikrocontroller-Programmierung & Firmware-Entwicklung
PCB Design • Arduino • Embedded Systems • Firmware
Ich biete professionelle Entwicklung von IoT-Hardware, einschließlich PCB-Design, Arduino- und Mikrocontroller-Programmierung sowie Firmware-Entwicklung. Die Lösungen werden zuverlässig, effizient und anwendungsorientiert umgesetzt – von der Konzeptphase bis zum funktionsfähigen Prototyp.
Diese Dienstleistung richtet sich an Unternehmen, Start-ups, Entwickler und Produktteams, die maßgeschneiderte Embedded- und IoT-Lösungen benötigen. Finden Sie mich auf Fiverr.
Leistungsumfang:
-
IoT-PCB-Design & Schaltplanerstellung
-
Leiterplattenlayout (mehrlagig, produktionstauglich)
-
Arduino- & Mikrocontroller-Programmierung (z. B. ESP32, STM32, ATmega)
-
Firmware-Entwicklung für Embedded Systems
-
Sensor- & Aktor-Integration
-
Kommunikation: Wi-Fi, Bluetooth, MQTT, I²C, SPI, UART
-
Optimierung für Leistung, Stabilität & Energieeffizienz
Lieferumfang:
-
Schaltpläne & PCB-Layouts
-
Gerber- & Produktionsdaten
-
Quellcode & Firmware
-
Dokumentation & Support zur Integration
Arbeitsweise:Strukturiert • Zuverlässig • Hardware-nah • Produktorientiert
CTA:
Planen Sie ein IoT- oder Embedded-System-Projekt?
Kontaktieren Sie mich gerne für eine technische Abstimmung oder ein unverbindliches Angebot. Finden Sie mich auf Fiverr.

