Handboek · hoofdstuk 37Embedded systemen en IoT
Een verbonden sensor moet ook op tijd, zuinig en veilig blijven werken
De microcontroller moet een korte puls zien terwijl de Pi een netwerkverbinding herstelt. Je scheidt snelle lokale taken van vertraagbare telemetrie. Het ontwerp verbindt interrupts, bussen, MQTT, energie en toegangsbeheer in één controleerbaar netwerk.

- Je kan interrupt- en timerwerk begrenzen en gedeelde data atomair lezen.
- Je kan een real-time deadline toetsen en slaapstanden tegen energie en verbinding afwegen.
- Je kan UART, I²C en SPI kiezen met elektrische en protocolvoorwaarden.
- Je kan MQTT-topics, Qo S, sequence en verval als een applicatiecontract ontwerpen.
- Je kan het sensornet beveiligen en het gedrag bij uitval toetsen.
Interrupt of polling: wat kan je missen?
Polling ziet een ingang alleen wanneer de lus haar leest. Een puls van 1 ms kan volledig tussen twee reads van 50 ms vallen. Een interrupt legt de gebeurtenis meteen vast, maar stopt tijdelijk de gewone taak. De handler moet kort blijven: tel of zet een vlag; loggen, wifi, wachten en complexe berekeningen horen in de voorgrond.
volatile vertelt de compiler dat een waarde buiten de gewone control flow kan veranderen. Het maakt een 32-bit read op een 8-bit AVR niet atomair. De interrupt kan tussen twee byte-reads vallen; je krijgt een samengestelde waarde die nooit bestaan heeft. Kopieer in een korte kritieke sectie en herstel de eerdere interruptstatus in algemene librarycode. De eenvoudige lescode roept de sectie alleen vanuit de voorgrond aan waar interrupts vooraf actief zijn.
| taak | plaats |
|---|---|
| puls tellen | ISR, enkele instructies |
| toerental berekenen | voorgrond |
| MQTT publiceren | gatewaytaak |
| hardware-noodstop | onafhankelijk elektrisch pad |
Timers en een vaste tijdschaal
Een hardwaretimer telt klokpulsen. Met 16 MHz en prescaler 64 wordt één tick 4 µs. Een 16-bit teller loopt dan na 65536 ticks, dus 262,144 ms, rond. Voor een compare-interval van 1 ms zijn 250 ticks nodig; bij tellen van 0 tot en met comparewaarde is het register 249. De timerconfiguratie hangt af van het bord en conflicteert soms met PWM of bibliotheken.
millis() geeft een modulo 32-tijd. Gebruik uint 32_t dt=nu-laatst, niet nu> laatst+periode. Unsigned aftrekken werkt over één rollover zolang de bedoelde tijdspanne korter is dan de tellercyclus. Een kalenderklok is voor logs, een monotone klok voor deadlines; NTP-correcties mogen geen actuatorvenster vooruit of achteruit springen.
Turbinetoerental uit flanken
- 200 flanken in 5,000 s
- n = 7,5 f (e19)
- Toerental en telresolutie
- 1f=200/5=40 Hz. Toerental n=7,5×40=300 tpm.
- 2Eén flank verandert f met 1/5=0,2 Hz en n met 1,5 tpm. Het telkwantisatie-effect wordt kleiner met een langer venster.
- 3Een 5 s-gemiddelde reageert traag op overtoeren. De lokale rembeveiliging gebruikt een apart snel, hardwarematig of getest periodemeetpad.
Een korte interrupt en een rollovervaste teller
De schets draait op een afzonderlijke turbinemonitor en hergebruikt geen bezette klimaatpin. De bron wordt tegen de gedeelde Arduino-stub gecompileerd. De stub controleert flanktelling en rollover, geen fysieke ISR-latentie.
// Afzonderlijke turbinemonitor, niet de bestaande klimaatpin2 hergebruiken.#include <Arduino.h>#include <stdint.h>volatile uint32_t flanken = 0;uint32_t laatst = 0;void puls() { ++flanken; }void setup() { pinMode(2, INPUT); attachInterrupt(digitalPinToInterrupt(2), puls, RISING); Serial.begin(115200); laatst = millis();}void loop() { uint32_t nu = millis(); uint32_t dt = nu - laatst; // modulo32, robuust tegen millis-overloop if (dt >= 5000UL) { // Alleen voorgrond met interrupts vooraf aan; houd deze sectie kort. noInterrupts(); uint32_t aantal = flanken; flanken = 0; interrupts(); laatst = nu; float f = aantal * 1000.0f / dt; Serial.print("pulsen="); Serial.print(aantal); Serial.print(",tpm="); Serial.println(7.5f*f, 1); }}Getest scenario: 200 stijgende flanken gedurende 5 s geven pulsen=200, tpm=300.0; dezelfde telling over een millis-rollover geeft dezelfde uitvoer. De gemeten werkelijke dt, niet blind 5 s, staat in de deler.
Real-time is een deadline, geen snelle gemiddelde laptop
Een hard real-time eis faalt zodra de deadline gemist wordt; een soft real-time eis tolereert aantoonbaar beperkte vertraging. Een noodstop mag niet van een gemiddelde MQTT-respons afhangen. De lokale regeltaak draait met vaste periode; de logger mag een korte achterstand bufferen, met een expliciete grens en meldgedrag.
Maak een taakbudget met worst-case uitvoeringstijd C, periode T en maximale blokkeertijd. kleiner dan 1 is noodzakelijk voor één CPU, maar bewijst niet dat een willekeurige scheduler alle deadlines haalt. Prioriteit, interrupts, niet-preëmptieve stukken en I/O blokkeren ook. In i40 duurde de langste gesimuleerde loop 48,8 ms; dat is bewijs voor dat scenario, geen universele worst-case grens.
| voorbeeldtaak | C model | T | bijdrage |
|---|---|---|---|
| regelstap | 2 ms | 1000 ms | 0,002 |
| sensorinlezen | 3 ms | 2000 ms | 0,0015 |
| logbuffer | 5 ms | 1000 ms | 0,005 |
De modelbenutting is 0,0085 = 0,85 %. Een blokkerende wifi-connect van 2 s kan ondanks dit lage getal een 1 s-deadline missen.
Een UART-frame in het tijdsbudget
- 115 200 bit/s
- bericht 120 bytes
- Minimale serialisatietijd
- 1Elk byte vraagt 10 bits; 120 bytes vragen 1200 bits.
- 21200/115200=0,0104167 s = 10,42 ms.
- 3Dit is alleen draadserialisatie. Een softwarebuffer kan eerst kopiëren, maar de fysieke uitvoer loopt door; een volle buffer kan alsnog blokkeren.
UART: twee richtingen, één elektrisch contract
UART is asynchroon: beide kanten stemmen baudrate en frame af. TX gaat naar RX en er is een gemeenschappelijke referentie, tenzij een geïsoleerde interface gebruikt wordt. Een Mega-uitgang van 5 V is niet automatisch veilig voor een 3,3 V-gateway. Gebruik geschikte niveauaanpassing; TTL-UART is geen RS-232 of RS-485.
Een eigen frame bevat versie, lengte, payload en checksum of CRC. Een regel met newline kan eenvoudiger zijn, maar krijgt een maximale lengte en parse-timeout. Negeer onbekende versies, meld beschadigde frames en resynchroniseer. Een CRC detecteert transmissiefouten; een aanvaller kan ook de CRC herberekenen, dus het is geen authenticatie. De bridge laat de bestaande lokale logger en regeling werken als zij uitvalt.
| veld | doel |
|---|---|
| versie/lengte | begrensd parsen |
| boot/seq | herstart en duplicaten |
| meetmoment/kwaliteit | leeftijd bepalen |
| CRC | toevallige transportfout |
I²C blijft een lokale bus
I²C gebruikt SDA en SCL met open-drain uitgangen en pull-ups. De stijgtijd groeit met buscapaciteit: ruwweg tᵣ≈0,8473RC tussen 30 en 70 % van de voedingsspanning. De NXP-specificatie geeft voor Standard-mode 100 kbit/s een stijgtijdgrens van 1000 ns. Lange kabels, parallelle pull-ups en verschillende spanningen moeten samen getoetst worden.
Behoud i30: SHT31 0x44, DS3231 0x68, INA226 0x40/0x41; e14 heeft ADS1115 0x48 en BME280 0x76 op zijn eigen luchtknoop. Twee sensoren met hetzelfde adres vragen een tweede bus, alternatief adres of multiplexer. Wifi vergroot het netwerkbereik zonder alle stationskabelcapaciteit aan één I²C-bus toe te voegen.
Pull-up en kabelcapaciteit
- Standard-mode grens tᵣ≤1000 ns
- Stijgtijd en ontwerpreserve
- 1tᵣ≈0,8473×1800×400×10⁻¹²=610 ns.
- 2Dat ligt 390 ns onder de grens. Bij 700 p F wordt het 1068 ns en faalt dezelfde berekening.
- 3Ook de sinkstroom bij laag niveau en de werkelijk gemeten golfvorm moeten passen; een RC-schatting vervangt de oscilloscoopcontrole niet.
SPI: snelle lokale selectie
SPI gebruikt een klok, MOSI, MISO en per slave een selectielijn. Snelheid, clock polarity/phase en woordlengte worden per apparaat afgesproken. Er is geen algemeen ingebouwd adres- of afleverprotocol. Een deselected slave moet MISO vrijgeven; anders vechten twee uitgangen. Een SD-kaart kan tijdens schrijven lang bezig zijn, ook als de SPI-klok snel is.
Behoud de SD-interface van i30 op de Mega, met CS53. Schrijf eerst een begrensde RAM-buffer en plan flush/close; bij stroomuitval is wat alleen in RAM of de kaartcache zit niet gegarandeerd bewaard. Een vlotteralarm mag geen SD-write afwachten. Pas de buffer aan het 8 k B SRAM-budget van de Mega aan en leg verliesgedrag bij vol geheugen vast.
| bus | sterkte | beperking |
|---|---|---|
| UART | eenvoudige puntverbinding | frame en levels zelf afspreken |
| I²C | veel lokale adressen | RC en adresconflicten |
| SPI | hoge lokale transfersnelheid | selectielijnen en apparaatbusy |
Wifi en MQTT: transport plus betekenis
Wifi geeft een lokaal IP-netwerk; MQTT verbindt publishers en subscribers via een broker. De Pi host de lokale broker. Een topic als habitat/kas/telemetrie benoemt een stroom; habitat/module/setpoint benoemt een gecontroleerd voorstel. Een node krijgt alleen toegang tot eigen publicaties en toegestane commando's.
Qo S0 is hoogstens eenmaal; Qo S1 minstens eenmaal en dus mogelijk dubbel; Qo S2 levert eenmaal binnen het protocolpad met meer overhead. Geen niveau bewijst dat een fysieke pompbeurt precies eenmaal is uitgevoerd. Gebruik request-ID en applicatiebevestiging met aanvaard/weigering/voltooid. Retained bewaart de laatste payload voor latere subscribers: geschikt voor status mét leeftijd, gevaarlijk voor een oud startbevel. Commando's zijn hier niet retained en hebben een vervaltijd.
Berichtcontract en energiebudget toetsen
De validator is een offline applicatietest, geen volledige MQTT-client. Geauthenticeerde transportidentiteit, boot-ID, schema, volgnummer, bereik en leeftijd werken samen. Een nieuwe boot mag sequence 0 gebruiken; de broker moet de identiteit en sessie al hebben geauthenticeerd.
import math class Ontvanger: def __init__(self): self.volgnummer = {} def verwerk(self, bericht, nu): verplicht = {"node","boot","seq","tijd_s","temperatuur_C"} if not verplicht <= bericht.keys(): return "schemafout" if type(bericht["seq"]) is not int or bericht["seq"] < 0: return "sequencefout" t,T = bericht["tijd_s"],bericht["temperatuur_C"] if not all(type(x) in (int,float) and math.isfinite(x) for x in (t,T,nu)): return "getalfout" if not -30 <= T <= 60 or not 0 <= nu-t <= 90: return "oud/bereik" sleutel=(bericht["node"],bericht["boot"]) if bericht["seq"] <= self.volgnummer.get(sleutel,-1): return "duplicaat/volgorde" self.volgnummer[sleutel]=bericht["seq"] return "aanvaard" r=Ontvanger()b={"node":"kas","boot":"sessie-a","seq":12,"tijd_s":100,"temperatuur_C":21.1}for bericht,nu in [(b,101),(b,102),(dict(b,seq=13),200), (dict(b,boot="sessie-b",seq=0,tijd_s=201),202)]: print(r.verwerk(bericht,nu))actief=120.0 # mA, gemeten ontwerpaanname voor hele knooprust=0.05 # mA, inclusief regelaar en lekstromen in rekenmodelI=(actief*2+rust*58)/60print("gemiddelde",round(I,3),"mA")print("2000mAh, 80pct bruikbaar",round(1600/I/24,1),"dagen")assert abs(I-4.048333333)<1e-7aanvaard duplicaat/volgorde oud/bereik aanvaard gemiddelde 4.048 mA 2000mAh, 80pct bruikbaar 16.5 dagen
Slaap kost ook tijd en verbinding
Een telemetrieknoop kan 2 s actief zijn en 58 s slapen. Met de expliciete rekenwaarden 120 m A actief en 0,05 m A rust is gemiddeld 4,048 m A. Een batterij van 2000 m Ah met 80 % bruikbaar houdt in dat model 16,47 dagen, vóór zelfontlading, kou en regelaarverlies. Gebruik stroommetingen van het hele bord: een powerled of USB-omzetter kan de chipwinst tenietdoen.
Bij deep sleep van de ESP32 wordt veel toestand uitgezet en blijft wifi niet gewoon verbonden. Ontwaken, DHCP, TLS en publiceren kosten tijd en energie. Reken per cyclus de werkelijke herverbinding mee. Een vaste veiligheidssensor met snelle deadline mag daarom niet dezelfde slaapstrategie gebruiken als een buitenweerlogger. Voed alarm- en regelknopen vast; slaap alleen waar gemiste tussenmetingen verantwoord zijn.
Buffer bij een netwerkgat
- 120 records
- 20 % reserve
- Buffer en terughaalgedrag
- 1120×96=11520 bytes payload. Met 20% reserve is 13 824 bytes nodig, zonder object- en bestands overhead.
- 2Dit past niet in 8 k B SRAM van de Mega. De gateway bewaart lokaal op flash of kaart en houdt de Mega-interface klein.
- 3Na reconnect stuur je oude records naar archief mét oude tijd, niet als verse regelmeting. Begrens replay zodat actuele meldingen niet verdrongen worden.
Beveiliging begint bij bevoegdheden
TLS beschermt transport vertrouwelijkheid en integriteit en controleert het certificaat van de peer. Certificaatcontrole uitzetten om een klokfout te omzeilen verwijdert een kernbescherming. Geef elke knoop een eigen credential, beperkte ACL en een intrekbaar updatepad. Een gedeeld wachtwoord op alle nodes maakt één verloren sensor een stationsincident.
Minimale toegang, veilige updates, inventaris, logbare wijzigingen en herstel horen bij het ontwerp. Firmware-updates worden ondertekend of anders aantoonbaar geauthenticeerd; test een mislukte update en terugrol. Een MQTT-payload is onbetrouwbare invoer: stel maximale grootte, vaste schema's en bereik in, voer er geen code uit. Koppel het sensornet van gastenwifi; toegang tot een topic is geen recht om de hardware-noodstop te wijzigen.
| dreiging | maatregel | test |
|---|---|---|
| spoof sensor | eigen credential + ACL | vreemde node publiceert geweigerd |
| replay commando | ID+verval+ack | oud bevel geweigerd |
| malformed payload | schema/limieten | NaN en oversize testen |
| updatefout | authenticatie+rollback | stroomuitval tijdens update |
Een protocolstandaard is een bruikbaar verhaal
OASIS definieert MQTT als publish/subscribe-berichtprotocol en benoemt de drie Qo S-niveaus en retained-gedrag. De standaard beschrijft aflevering, niet de veiligheid van een klimaatregeling. Espressif beschrijft slaapmodi en de gevolgen voor wifi. Juist die grenzen maken standaarden bruikbaar: ze vertellen wat je wel uit het transport mag afleiden en welk deel van het contract nog jouw verantwoordelijkheid blijft.
Primaire bronnen, geraadpleegd 30-09-2026: OASIS MQTT5, NXP UM10204, Microchip ATmega 2560, ESP32 slaapmodi en ESP-MQTT.
| standaard | applicatie blijft ontwerpen |
|---|---|
| MQTT | actie-ID en procesveiligheid |
| I²C | kabelbudget en adressen |
| MCU | deadline en taakbudget |
Het netwerk ondersteunt het lokale brein
De elektronische bus, softwareinterface en systeemarchitectuur moeten dezelfde tijd- en foutafspraken gebruiken. Sensorfusie krijgt kwaliteit en leeftijd; AI krijgt historische data; veiligheid krijgt een zelfstandig pad.
Bussen, adressen en UTC+1-logs blijven behouden.
Toerental uit generatorfrequentie.
Adapters met een tijd- en foutcontract.
Data veroudert en kan uitvallen.
Voor Habitat: lokale MQTT-broker op Pi, wifi-gateways, MQTT5 waar ondersteund, telemetrie elke 60 s met leeftijdgrens 90 s voor de centrale monitor. Dit vervangt geen snelle lokale regeling. Qo S1 met boot/seq; commando's niet retained, voorzien van ID en verval. Alarm- en regelknopen blijven vast gevoed; turbinefrequentie wordt met 5 s-vensters gelogd, met aparte snelle beveiliging.
In het kort
- Interrupts leggen korte gebeurtenissen vast; volatile vervangt atomair kopiëren niet.
- Deadline, blokkeertijd en uitvalgedrag bepalen de architectuur.
- UART/I²C/SPI zijn lokale elektrische contracten; wifi en MQTT vragen een applicatiecontract.
- Slaap, TLS, identiteit, updates en buffering moeten samen getoetst worden.
Wat je nu kent
- interrupt
- Een gebeurtenis die de processor een korte handler laat uitvoeren buiten de gewone programmalus.
- deadline
- Het laatste tijdstip waarop een resultaat nog aan zijn eis voldoet.
- Qo S
- De afgesproken MQTT-afleverwijze voor een bericht.
- idempotent
- Een bewerking waarvan herhaling niet nogmaals hetzelfde effect veroorzaakt.
- ACL
- Regels die bepalen welke identiteit op welke topics mag lezen of schrijven.