Betriebssystem der Mikrocontroller
Der Anwender ist frei in der Wahl des Betriebssystem für seine Mikrocontroller-Platinen, solange die folgenden Grundregeln eingehalten werden:
- muß als erstes eine im Netz eindeutige UID (64 Bit) generieren
- muß das Internode-Nachrichten-Protokoll implementieren und sich eine Adresse zwischen 1-251 zuweisen
- muß von Zeit zu Zeit ein Discovery-Paket senden, mit dem es sich bemerkbar macht
- kann in beliebigen Abständen Pakete mit gemessenen Sensordaten versenden (dafür gibt es spezielle Nachrichten)
- kann auf eingehende Pakete reagieren (z.B. reagiert bei mir ein LED-Strip-Controller-Node auf Nachrichten eines Bewegungsmelder-Nodes)
AqHomeControl liefert für meine eigenen Nodes ein einfaches Betriebssystem mit, das diese Aufgaben erfüllt und leicht erweiterbar ist.
Außerdem enthält das System noch einen Bootloader, der das Flashen einer neuen Firmware im Feld über das Inter-Node-Netzwerk erlaubt.
AqHomeControl-Betriebssystem
Übersicht
Das System weist folgenden Eigenschaften auf:
- extrem geringe Komplexizität, verwendet z.B. keinerlei Verwaltungsstrukturen für Anwendungen oder Treiber, sondern wird nur durch einsortieren von Routinen-Aufrufen organisiert, benötigt dadurch selbst kaum RAM oder FLASH-Speicher und ist auch auf den kleinsten AtTiny einsetzbar
- nur Single-Tasking (würde nur die Komplexizität und den RAM-Bedarf erhöhen)
- Ereignis-gesteuert (Ereignisse sind z.B. Timer-Interrupt, eintreffende Nachrichten etc.)
- vollständig in Assembler geschrieben
Historie
Am Anfang der Entwicklung meiner AVR-basierten Geräte hatte ich zunächst die Idee, ein einfaches Betriebssystem zu schreiben, das z.B. Treiber abstrahiert, und fortschrittliche Timer implementiert. Dabei hat sich schnell herausgestellt, dass hierzu selbst bei "leerem" System (also noch ohne Treiber und Anwendungen) schon erstaunlich viel RAM verbraucht wird, der aber z.B. für die Anwendungen benötigt wird. Zudem haben die AVRs nur sehr wenig RAM und teilweise auch nur wenig FLASH-Speicher. Daher musste eine Lösung her, die keine Verwaltungsstrukturen braucht (schon eine Sprungtabelle für Treiberfunktionen belegt 2 Byte pro Eintrag).
Das wird am ehesten erreicht, indem für jeden Treiber bzw. Anwendung ein paar wenige Einsprünge definiert werden und an entsprechenden Stellen des "Betriebssystems" aufgerufen werden.
Um das ganze modular zu halten und dennoch nicht für jedes Gerät den ganzen Code kopieren zu müssen, werden diese Aufrufe durch Precompiler-Direktiven geklammert, so dass in der Main-Datei eines Devices nur noch die Liste der zu verwendenden Treiber und Anwendungen definiert werden müssen. Um den Rest kümmert sich das System.
Beschreibung
Das System läuft auf den Mikroprozessor-Geräten (device, node), es unterscheidet zwischen Modulen (modules) und Anwendungen, (apps) wobei von beiden jeweils mehrere aktiv sein koennen.
Module sind lowlevel-Code und in erster Linie Treiber für Hardware-Geräte (z.B. den Umweltsensor SI7021) oder für Protokolle. Sie enthalten z.B. auch Interrupt-Handler.
Anwendungen hingegen liegen auf höherer Ebene und verwenden Module, um deren Funktionalität z.B. zu kombinieren oder aufzuwerten. Beispielsweise gibt es Anwendungen für den Netzwerk-Stack, für Bewegungsmelder-aktiviertes Licht, zum Berichten von Statistiken und von Sensordaten.
Zur Kommunikation mit Modulen und Anwendungen kennt das Betriebssystem verschiedene Funktionen:
| Funktion | Beschreibung | Erforderlichkeit |
|---|---|---|
| INIT | Initialisiert ein Modul oder eine Anwendung | erforderlich |
| RUN | Wird einmal pro MainLoop aufgerufen | optional |
| MSGRECVD | wird bei Empfang einer Nachricht über das Netzwerk aufgerufen | optional |
| EVERY100MS | wird einmal alle 100ms aufgerufen (system timer tick) | optional |
| EVERY1S | wird einmal pro Sekunde aufgerufen | optional |
| EVERY1M | wird einmal pro Minute aufgerufen | optional |
| EVERY1H | wird einmal pro Stunde aufgerufen | optional |
| EVERY1D | wird einmal pro Tag aufgerufen | optional |
Die Aufrufe dieser Funktionen liegen für Module in den Dateien avr/devices/all/modules_XXX.asm, z.B. liegen die Aufrufe der INIT-Funktion für Module in avr/devices/all/modules_init.asm. Falls also ein Treiber z.B. die INIT Funktion implementiert, muss der entsprechende Aufruf in diese Datei eingefügt werden.
Viele Treiber implementieren nicht alle Funktionen, daher findet sich nicht in jeder entsprechenden Datei ein solcher Aufruf.
Um die Verwendung durch die verschiedenen Platinen zu vereinfachen, werden die Aufrufe in den modules_xxx.asm und apps_xxx.asm Dateien immer durch Präprozessor-Direktiven eingeschlossen, z.B.:
#ifdef MODULES_SGP30 bigcall SGP30_EverySecond #endif
Dadurch muss in der main.asm eines Mikroprozessor-Nodes nur diese Direktive eingebaut werden, um diesen Treiber vollständig einzubinden:
#define MODULES_SGP30
Zentraler Loop
Das Betriebssystem selbst besteht eigentlich nur aus einem einfachen MainLoop.
Dieser Loop ruft als erstes alle RUN-Funktionen der Module und Anwendungen auf und legt den Prozessor dann schlafen (SLEEP). Aus diesem Schlaf wacht der Prozessor durch einen Interrupt auf. Das ist in der Regel der Timer-Interrupt, der alle 100 Millisekunden ausgelöst wird. Somit schläft das System die meiste Zeit, was den Stromverbrauch senkt.
Device-Hauptanwendung
Die Hauptanwendung eines Device/Node sollte die folgenden Labels enthalten, die vom Betriebssystem aufgerufen werden:
| Funktion | Beschreibung |
|---|---|
| onSystemStart | Wird einmal nach Initialisierung des Betriebssystem aufgerufen |
| onEveryLoop | Wird einmal pro MainLoop aufgerufen |
| onMessageReceived | wird bei Empfang einer Nachricht über das Netzwerk aufgerufen |
| onEvery100ms | wird einmal alle 100ms aufgerufen (system timer tick) |
| onEverySecond | wird einmal pro Sekunde aufgerufen |
| onEveryMinute | wird einmal pro Minute aufgerufen |
| onEveryHour | wird einmal pro Stunde aufgerufen |
| onEveryDay | wird einmal pro Tag aufgerufen |
Bootloader
Das Betriebssystem von AqHomeControl enthält auch einen Bootloader. Dieser wird bei jedem Reset angesprungen und wartet dann für einige Sekunden, ob eine Anforderung zum Flashen einer neuen Firmware eintrifft. Ist dies der Fall, wird die neue Firmware in den FLASH des Mikroprozessors geschrieben und anschliessend aufgerufen.
Beispiel
Im folgenden die Quelldatei der Hauptanwendung des Devices N29.
Dabei handelt es sich um eine Mikroprozessorplatine mit einem ATtiny84, einem Bewegungssensor, einem Temperatur-/Luftfeuchtigkeitssensor und einer Photodiode/Phototransistor zur Messung der Hellichkeit im Raum.
Man sieht hier schön, daß verschiedene Treiber einfach über Präprozessor-Direktiven (z.B. #define MODULES_SGP30) eingebunden werden. Der Code der Hauptanwendung selbst ist ansonsten leer, der Rest wird vom Betriebssystem erledigt. Es müssen lediglich die oben beschriebenen Labels angelegt werden (wie z.B. onEverySecond), die hier aber lediglich ein ret (return) ausführen.
; *************************************************************************** ; copyright : (C) 2026 by Martin Preuss ; email : martin@libchipcard.de ; ; *************************************************************************** ; * This file is part of the project "AqHome". * ; * Please see toplevel file COPYING of that project for license details. * ; *************************************************************************** ; *************************************************************************** ; Source file for temperature sensor node on AtTiny 84 ; ; This is for the full system (i.e. not the boot loader). ; ; All definitions and changes should go into this file. ; ; ; *************************************************************************** .equ clock=1000000 ; Define the clock frequency .nolist .include "include/tn84def.inc" ; Define device ATtiny84 .list .include "../defs.asm" .include "./data.asm" .include "version.asm" .include "devices/all/defs.asm" .include "common/calls.asm" .include "common/utils_io.asm" .include "common/utils_wait.asm" ; *************************************************************************** ; defines ; --------------------------------------------------------------------------- ; generic .equ NET_BUFFERS_NUM = 6 .equ NET_MSGNUMINBUF_SIZE = 8 ; max buffer nums in ringbuffer (global incoming) .equ NET_IFACE_OUTMSGBUF_SIZE = 8 ; max buffer nums in ringbuffer (per interface outbound) ; --------------------------------------------------------------------------- ; firmware settings including list of modules used #define MODULES_CLOCK #define MODULES_LED_SIMPLE #define MODULES_NETWORK #define MODULES_COM2W #define MODULES_TWI_MASTER #define MODULES_SI7021 #define MODULES_SGP30 #define MODULES_MOTION #define MODULES_BRIGHTNESS #define APPS_NETWORK #define APPS_MOTION #define APPS_REPORTSENSORS #define APPS_STATS ; --------------------------------------------------------------------------- ; defines for values .equ VALUE_ID_SI7021_TEMP = 0x01 .equ VALUE_ID_SI7021_HUM = 0x02 .equ VALUE_ID_MOTION = 0x07 .equ VALUE_ID_SGP30_TVOC = 0x09 .equ VALUE_ID_SGP30_CO2 = 0x0a .equ VALUE_ID_BRIGHTNESS = 0x0b .equ VALUE_ID_LEDSIMPLE_TIMING = 0x88 ; *************************************************************************** ; code segment .cseg .org 000000 ; --------------------------------------------------------------------------- ; Reset and interrupt vectors rjmp BOOTLOADER_ADDR ; Reset vector ; use this for flashed system reti ; EXT_INT0 rjmp com2wPcintIsr ; PCI0 reti ; PCI1 reti ; WATCHDOG reti ; ICP1 reti ; OC1A reti ; OC1B reti ; OVF1 rjmp baseTimerIrqOC0A ; OC0A reti ; OC0B reti ; OVF0 reti ; ACI reti ; ADCC reti ; ERDY reti ; USI_STR reti ; USI_OVF devInfoBlock: ; 12 bytes devInfoManufacturer: .db 'A', 'Q', 'U', 'A' devInfoId: .db DEVICEINFO_ID, 0 devInfoVersion: .db DEVICEINFO_VERSION, DEVICEINFO_REVISION ; version, revision firmwareVersion: .db FIRMWARE_VARIANT_TEMP_WINDOW, FIRMWARE_VERSION_MAJOR .db FIRMWARE_VERSION_MINOR, FIRMWARE_VERSION_PATCHLEVEL ; --------------------------------------------------------------------------- ; @routine firmwareStart @global firmwareStart: rjmp main ; @end ; --------------------------------------------------------------------------- ; @routine onSystemStart onSystemStart: ret ; @end ; --------------------------------------------------------------------------- ; @routine onMessageReceived ; ; Called on every message received onMessageReceived: clc ret ; @end ; --------------------------------------------------------------------------- ; @routine onEvery100ms ; ; Called every 100ms. Add your routine calls here. No arguments, no results. onEvery100ms: onEverySecond: onEveryMinute: onEveryHour: onEveryDay: ret ; @end ; --------------------------------------------------------------------------- ; @routine onEveryLoop ; ; Called on every loop (i.e. after awakening from sleep). ; onEveryLoop: ret ; @end ; *************************************************************************** ; includes .include "devices/all/hw_tn84.asm" .include "devices/all/includes.asm" ; --------------------------------------------------------------------------- ; defines for network interface .equ netInterfaceData = com2w_iface