Stereotyper Firmware-Code kann zu Betriebsunterbrechungen führen und die Entwickler zurück an den Anfang zwingen. Denken Sie einmal daran, was passiert, wenn Ihr Mikrocontroller z.B. die Netzwerkverbindung verliert oder es zu IP-Konflikten kommt? In diesem Beitrag überdenken wir gängige Codierungspraktiken und erkunden intelligentere Lösungen, um die Zuverlässigkeit zu erhöhen und Kosten von embedded Projekten zu reduzieren.
Viele embedded Projekte bieten zwar großartige Lösungen für Entwickler, bleiben jedoch bei wichtigen Fragen unklar. Welche Maßnahmen etwa sind zu ergreifen, wenn der Mikrocontroller während des Betriebs die Netzwerkverbindung verliert? Wie können IP-Konflikte im LAN gelöst werden? Ist es notwendig, eine Echtzeituhr (RTC) zu verwenden, um eine genaue Zeitmessung zu erhalten? Erfordert die Zeitanpassung eine Benutzerintervention? Müssen Sensoren während der Betriebszeit des MCU ständig mit Strom versorgt werden?
Man sollte kreativ denken und die gängigen Praktiken der Programmierung in Frage stellen. In diesem Artikel geht es um einige traditionelle, bzw. stereotype Programmieransätze für MCUs, die fragwürdig sind und manchmal eine Benutzerintervention erfordern, um Betriebsstörungen zu beheben. In solchen Fällen sind normalerweise Tasten und Anzeigen erforderlich, um die MCU als Korrekturmaßnahme zu überwachen, zurückzusetzen, voreinzustellen oder zu unterbrechen. Die Unterstützung solche rTasten und Anzeigen zusammen mit Sensorschnittstellen kann aber problematisch werden, wenn GPIOs knapp sind.

Abonnieren
Tag-Benachrichtigung zu Embedded & AI jetzt abonnieren!

Bei der Arbeit mit der Arduino IDE gibt es stets zwei klare Abschnitte im Code: setup() und loop(). Der erste Abschnitt dient der Initialisierung des Mikrocontrollers sowie der angeschlossenen Peripheriegeräte und Sensoren. Der zweite Abschnitt kümmert sich um die Aktivitäten zur Laufzeit, die das Lesen von Sensoren und das Ergreifen von Maßnahmen umfassen können. Die klare Trennung dieser Abschnitte kann Entwickler irreführen, die fälschlicherweise annehmen, dass ihr eingebettetes System reibungslos funktioniert, nur um dann überrascht festzustellen, dass betriebliche Störungen drohen, die sie zurück an den Entwurfstisch zwingen können. Ich habe solche Situationen erlebt und stand vor den genannten Herausforderungen. Dennoch fand ich sinnvolle Lösungen, um betriebliche Störungen zu überwinden und die Kosten für meine Projekte zu senken, indem ich einige traditionelle setup()-Aktivitäten in den loop()-Abschnitt integrierte. In den folgenden Fallstudien werde ich diese Strategie näher erläutern.

Die Wi-Fi-Konfiguration auf den setup()-Bereich beschränken

Wenn der AP aufgrund von Benutzerintervention oder einer Wiederherstellung der Stromversorgung neu gestartet wird, verliert der angeschlossene MCU die Verbindung zum WLAN und sollte daher neu gestartet werden, um die setup()-Funktion erneut auszuführen und die Wi-Fi-Konnektivität zu reinitialisieren. Daher sollte die Überprüfung der Gesundheit oder Stabilität der Wi-Fi-Verbindung regelmäßig im loop()-Bereich erfolgen, um entsprechend auf verlorene Verbindungen zum AP zu reagieren. Der Verlust der Verbindung zum Internet ist hingegen ein weiteres Problem, das für webbasierte Anwendungen erkannt werden muss, selbst bei gültiger WLAN-Konnektivität. Solche Strategien werden in veröffentlichten Projekten selten angesprochen, die anscheinend auf die Verfügbarkeit menschlicher Intervention angewiesen sind.
Glücklicherweise verfügt der ESP8266 über die sehr interessante Bibliothek ESP8266WiFi, welche eine Vielzahl von Funktionen und Möglichkeiten bietet, die in praktischen Projekten selten zu finden sind. Random Nerd Tutorials hat die Optionen zur drahtlosen Wiederverbindung, die von dieser Bibliothek bereitgestellt werden, überprüft. Eine Kombination der folgenden Anweisungen im setup()-Bereich würde das gewünschte Ergebnis erzielen:

WiFi.setAutoReconnect(true);

WiFi.persistent(true);

Alternativ kann die Überprüfung der drahtlosen Verbindung im loop()-Bereich eine Wiederherstellungsmaßnahme ermöglichen, wie zum Beispiel das Neustarten des MCU. In Bild 1 ist der Codeabschnitt dargestellt, den wir regelmäßig im setup()-Bereich verwenden, der jedoch mit einer leichten Anpassung auch im loop()-Bereich genutzt werden kann.

Bild 1: Traditionelle zu praktischer Netzwerkverbindung
Traditional to practical

Random Nerd Tutorials bietet auch eine Software-Interrupt-Alternative zur Erkennung verlorener Verbindungen und deren Wiederherstellung. Für den ESP8266 verfolge ich einen anderen Ansatz, um die Netzwerkverbindung zu überprüfen. Ich lasse den Mikrocontroller regelmäßig im loop()-Abschnitt den Zugang zu seinem WLAN-Gateway prüfen, indem ich es mit der Funktion ping(gatewayIP) aus der ESP8266Ping-Bibliothek anpinge . Dies wird als "Ping for Life" bezeichnet, das von Netzwerkadministratoren genutzt wird, um den Verbindungsstatus zu prüfen und ihn gegen Zeitüberschreitungen aufrechtzuerhalten. Wenn ein Ping fehlschlägt, zwinge ich den Mikrocontroller zum Neustart und versuche, eine Verbindung zu einem alternativen Access Point (AP) herzustellen. Ich nutze das Pingen auch, um die Erreichbarkeit des Internets zu überprüfen, ziele jedoch auf einen DNS-Server, wie den von Google (Adresse 8.8.4.4), anstelle des lokalen WLAN-Gateways, indem ich die Funktion ping(primaryDNS_IP) verwende. Wenn ein solches regelmäßiges Ping fehlschlägt, startet der Mikrocontroller neu, um sich mit einem AP mit aktiver Internetverbindung zu verbinden. Um diese Strategie umzusetzen, binde ich die ESP8266Ping-Bibliothek in meinen Sketch ein, wie im Codeausschnitt in Listing 1 gezeigt.


Listing 1: Implementierung von Ping of Life für WLAN-Gateway und einen globalen DNS-Server.

#include <ESP8266WiFi.h>
#include <ESP8266Ping.h>

IPAddress local_IP(192, 168, 1, 180);
IPAddress subnet(255, 255, 255, 0); // Subnet Mask
IPAddress gateway(192, 168, 1, 1); // Default Gateway
IPAddress primaryDNS(8, 8, 4, 4);
IPAddress secondaryDNS(8, 8, 8, 8);

// place this code segment in the loop() section for periodic checks
if (Ping.ping(gateway)) {
 Serial.println("Ping gateway Success!!");
} else {
 Serial.println("Gateway Ping Error!!");
 ESP.restart();
}

// place this code segment in the loop() section for periodic checks
if (Ping.ping(primaryDNS)) {
 Serial.println("Ping DNS Success!!");
} else {
 Serial.println("DNS Ping Error!!");
 ESP.restart();
}


Der Neustart des ESP8266-Mikrocontrollers erfolgt durch Ausführen der Funktion ESP.restart(). Diese Funktion ist im ESP-Hardware-Framework, das im Arduino IDE installiert ist, eingebettet. Um bei einem Neustart zu einem anderen AP zu wechseln, kann der Benutzer die Verbindung zu alternativen APs mit demselben Ping-Ansatz überprüfen und die drahtlose Verbindung im setup()-Abschnitt zu dem reaktionsschnellen AP herstellen. Listing 1 zeigt Codeabschnitte, die die lokale AP-Konnektivität sowie die Internetzugänglichkeit überprüfen.

Die Verwendung eines Echtzeituhr (RTC)-Moduls zur Zeiterhaltung

Ein RTC-Modul sorgt für eine präzise Zeitmessung, unabhängig vom Zustand des angeschlossenen Mikrocontrollers (MCU), da es über eine Backup-Batterie verfügt, die den kontinuierlichen Betrieb unterstützt. Bei jedem Neustart des MCUs wird die aktuelle Zeit vom RTC abgerufen und regelmäßig aktualisiert. Viele MCUs haben integrierte Timer, die in der Lage sind, Millisekunden seit dem letzten Neustart zu zählen. Ich nutze diese Funktion in meinem Code, um die Zeitvariablen des MCUs während des Betriebs zu aktualisieren. Hat der MCU Zugang zum Internet, ist es sinnvoll, NTP-Server zu kontaktieren, um die aktuelle Zeit beim Neustart zu erhalten. Ich setze beide Strategien zur Konfiguration und zum Betrieb meiner ESP8266-Module ein. Im setup()-Bereich greife ich auf einen NTP-Server zu, um die Zeit zu holen, und im loop()-Bereich verwende ich die millis()-Funktion, um die Zeit auf den aktuellen Moment zu aktualisieren. Außerdem kontaktiere ich regelmäßig den NTP-Server im loop()-Bereich, um meine softwareseitige Uhr neu anzupassen und eventuelle Abweichungen zu korrigieren. Diese Strategie ermöglicht es mir, die RTC-Integration in meinen Projekten wegzulassen und sowohl GPIO-Ressourcen als auch den Stromverbrauch zu sparen.

Im folgenden Code-Snippet zeige ich, wie man auf NTP zugreift und die lokale Uhrzeit verwaltet, um eine präzise Echtzeituhr ohne RTC zu erhalten. Ich habe zwei Hauptfunktionen eingerichtet, die die Uhr manipulieren: die Funktion getNtpTime(), die die Uhr basierend auf dem NTP-Zugriff aktualisiert (siehe Listing 2), und die Funktion update_time(), die den internen MCU-Timer verarbeitet (siehe Listing 3).


Listing 2: Aktualisieren Sie die MCU-Uhr während der NTP-Synchronisierung.

void getNtpTime() {
  epochTime = timeClient.getEpochTime();
  Serial.print("Epoch Time: ");
  Serial.println(epochTime);

  formattedTime = timeClient.getFormattedTime();
  Serial.print("Formatted Time: ");
  Serial.println(formattedTime);

  currentHour = timeClient.getHours();
  Serial.print("Hour: ");
  Serial.println(currentHour);

  currentMinute = timeClient.getMinutes();
  Serial.print("Minutes: ");
  Serial.println(currentMinute);

  currentSecond = timeClient.getSeconds();
  Serial.print("Seconds: ");
  Serial.println(currentSecond);

  weekDay = weekDays[timeClient.getDay()];
  Serial.print("Week Day: ");
  Serial.println(weekDay);

  //Get a time structure
  struct tm *ptm = gmtime((time_t *)&epochTime);

  monthDay = ptm->tm_mday;
  Serial.print("Month day: ");
  Serial.println(monthDay);

  currentMonth = ptm->tm_mon + 1;
  Serial.print("Month: ");
  Serial.println(currentMonth);

  currentMonthName = months[currentMonth - 1];
  Serial.print("Month name: ");
  Serial.println(currentMonthName);

  currentYear = ptm->tm_year + 1900;
  Serial.print("Year: ");
  Serial.println(currentYear);

  //Print complete date:
  currentDate = String(currentYear) + "-"

           + String(currentMonth) + "-" + String(monthDay);
  Serial.print("Current date: ");
  Serial.println(currentDate);

  Serial.println("");
}  


Listing 3: Wichtige Aktualisierung des internen Timers der MCU für die Uhr.

void update_time() {
  int lapse = millis() - oldSec;    // passed milliseconds since last clock update
  if (lapse >= 1000) {        // add passed seconds to clock reading
    oldSec = millis();
    currentSecond += lapse/1000;
    if (currentSecond >= 60) {    // add a minute for every 60 passed seconds
      currentSecond -= 60;
      ++currentMinute;
      if (currentMinute >= 60) {    // add an hour for every 60 passed minutes
        currentMinute -= 60;
        ++currentHour;
        if (currentHour >= 24) {    // adjust clock for AM transition
          currentHour -= 24;
        }
      }
    }
  }
  String hr, min;
  if (currentHour < 10) hr = "0" + String(currentHour);
  else hr = String(currentHour);
  if (currentMinute < 10) min = "0" + String(currentMinute);
  else min = String(currentMinute);
  formattedTime = hr + ":" + min;
}  



Im loop()-Bereich rufe ich die erste Funktion einmal pro Stunde auf, um eventuelle Uhrabweichungen auszugleichen, und die zweite Funktion einmal pro Minute, um die Minuten- und Stundenanzeigen zu aktualisieren. Zum Initialisieren der MCU-Uhr beim Neustart rufe ich die NTP-Funktion einmal im setup()-Bereich auf und deklariere globale Variablen, die notwendig sind, um das aktuelle Datum und die Uhrzeit anzuzeigen (siehe Listing 4). Diese Abfolge von Aktivitäten ermöglicht eine präzise RTC-Substitution. Leider musste ich meine Bestände an RTCs aufgeben, seit diese Strategie erfolgreich war.


Listing 4: Skizze zur Initialisierung mit Bibliotheken und globalen Variablen für die Zeitverarbeitung.

// Define NTP Client to get time
#include <NTPClient.h>
#define NTPUPDATE 3600000              // frequency of accessing the NTP, every hour

const long utcOffsetInSeconds = 7200;  // GMT +2

float oldSec = 0;    // passed seconds since last reboot

WiFiUDP ntpUDP;    // required UDP access by NTP client
NTPClient timeClient(ntpUDP, "pool.ntp.org");    // my chosen NTP server

time_t epochTime;
String formattedTime, weekDay, currentMonthName, currentDate;
int currentHour, currentMinute, currentSecond, monthDay, currentMonth, currentYear;

//Week Days
String weekDays[7] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };

//Month names
String months[12] = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" };

// Variables to save date and time
String formattedDate;
String dayStamp;
String timeStamp;  


 Dynamische IP-Adressierung von MCU-basierten Webservern

Wenn ein Webserver auf einem dynamisch zugewiesenen IP-Adressgerät betrieben wird, muss der Benutzer die IP-Adresse des Servers nach jedem Neustart aufzeichnen und diese Adresse an seine Webclients weitergeben. Eine solche Netzwerkkonfiguration erfordert einen DHCP-fähigen Access Point, wie in Listing 5 ohne den WiFi.config()-Abschnitt (Bild 2).

Bild 2: Eine DHCP-Aktivierung in der AP-Einrichtung, bei der der Pool für die dynamische
IP-Adresszuweisung zusammen mit anderen Einstellungen zur Internetzugänglichkeit festgelegt wird.

Die Aufrechterhaltung der Konnektivität von LAN-Geräten auf diese Weise ist mühsam und erfordert daher eine Überarbeitung. Aus diesem Grund beabsichtige ich, die IP-Adressen der Webserver in meinem persönlichen LAN festzulegen, um die Wartung der Webclients, die in meinem IoT-Netzwerk verteilt sind, zu erleichtern. In Listing 5 wird ein Beispiel gezeigt, in dem ich die statischen Netzwerkanmeldeinformationen meines Gartenbeleuchtungscontrollers einrichte.


Listing 5: Festgelegte WLAN-Netzwerkzugangsdaten.

IPAddress local_IP(192, 168, 1, 140); // MCU fixed IP address
IPAddress subnet(255, 255, 255, 0);  // Subnet Mask
IPAddress gateway(192, 168, 1, 1);   // Default Gateway
IPAddress primaryDNS(8, 8, 4, 4);
IPAddress secondaryDNS(8, 8, 8, 8);

const char *ssid = "YOUR_SSID";
const char *password = "YOUR_PASSWORD ";

void setup() {
 WiFi.mode(WIFI_STA);

  // Configure static wlan credentials, remove this segment for dynamic allocation
  if (!WiFi.config(local_IP, gateway, subnet, primaryDNS)) {
    Serial.println("STA Failed to configure");
  }
  else {
    Serial.println("Static IP Configuration Succeeded!");
  }

  // Connect to Wi-Fi network with SSID and password
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
 // .  .  .  .  .
}     // setup() end


Der Zugriff auf einen Webserver über seinen Domainnamen anstelle seiner IP-Adresse ist eine alternative Möglichkeit, das Problem der Serverzugänglichkeit zu lösen. Für ein MCU, das dynamisch seine IP-Adresse zugewiesen bekommt, verwende ich die mDNS-Bibliothek , die speziell für ESP8266 entwickelt wurde, um dem ESP-basierten Webserver einen Domainnamen zuzuweisen. Dieser Name wird dann in Bonjour-fähigen Webclients genutzt, um auf den Server zuzugreifen. Diese Strategie wende ich auch für Server mit fester IP-Adresse an, da sie den Zugriff auf Webserver für nicht-technische Benutzer vereinfacht, die mit IP-Adressen nicht vertraut sind. Bild 3 zeigt, wie ich meinen Gartenbeleuchtungscontroller über einen Webbrowser entweder mit seinem DNS-zugewiesenen Namen oder seiner festen IP-Adresse anspreche.

Bild 3: Zugriff auf meinen Webserver über den zugewiesenen DNS-Namen
oder die feste IP <192.168.1.140>.

Listing 6 zeigt den notwendigen Code zur Benennung der MCU im WLAN, wobei MDNS das Objekt ist, das die mDNS-Funktionen zur Aktivierung und Pflege des MCU-Netzwerknamens bereitstellt.


Listing 6: Zuweisung eines Netzwerknamens für das MCU zur einfachen Erreichbarkeit in Bonjour-fähigen Web-Clients.

#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>

ESP8266WebServer server3(80);      // mDNS activation server
String newHostname = "garden1";    // my garden controller network name
                // shall be automatically appended with .local
                // to form a legitimate domain name

void setup() {
 // .  .  .  .  .
  if (!MDNS.begin(newHostname)) {
    Serial.println("Error setting up MDNS responder!");
    while (1) {
      delay(1000);
    }
  }
  Serial.println("mDNS responder started");

  // Start the server
  server3.begin();
  Serial.println("TCP server started for mDNS activation!");

  // Add service to MDNS-SD
  MDNS.addService("http", "tcp", 80);
    . . . . . . . . .
}    // setup() end

void loop() {
  MDNS.update();
 // .  .  .  .  .
}    // loop() end


IP-Adresskonflikt von MCU im LAN

Wenn ein IoT-Netzwerk mit festen IP-Zuweisungen größer wird, sollte man die Möglichkeit in Betracht ziehen, dass versehentlich dieselbe IP mehreren Controllern zugewiesen wird. Solche Fälle führen zu IP-Konflikten im Netzwerk, wodurch die betroffenen Geräte nicht mehr über das Netzwerk erreichbar sind (siehe Bild 4). Eine zentrale Verwaltung der Netzwerkressourcen kann solche Fälle vermeiden, ist jedoch nicht immer die Realität.

Bild 4: Konfliktierende IP-Adressen im selben Netzwerk können Netzwerkfehler verursachen,
nicht nur für eingebettete Geräte, sondern auch für Benutzercomputing-Einrichtungen.

Beim Neustart initiiert der Mikrocontroller eine dynamische IP-Anfrage und pingt die vorgesehene feste IP-Adresse, um sicherzustellen, dass diese nicht bereits einem Gerät im LAN zugewiesen ist. Anschließend stellt der Mikrocontroller die Verbindung zur beabsichtigten festen IP im Abschnitt setup() her. In Listing 7 ping ich die local_IP des Mikrocontrollers, die in Listing 1 deklariert ist, bevor ich sie dem Mikrocontroller zuweise. Vor der Aktivierung dieses Codeabschnitts sollte der Mikrocontroller mit einer vorübergehend über DHCP zugewiesenen IP-Adresse mit dem LAN verbunden sein. Sollte der Ping fehlschlagen, kann ich sicher sein, dass die vorgesehene feste IP unbedenklich ist; andernfalls sollte der Mikrocontroller neu gestartet werden, um jede Minute die Konfliktauflösung zu überprüfen.


Listing 7: Pingen von IP, um Konflikte mit Netzwerkressourcen zu identifizieren.

#include <ESP8266WiFi.h>
#include <ESP8266Ping.h>

IPAddress local_IP(192, 168, 1, 140);  // MCU fixed IP address
IPAddress subnet(255, 255, 255, 0);    // Subnet Mask
IPAddress gateway(192, 168, 1, 1);     // Default Gateway
IPAddress primaryDNS(8, 8, 4, 4);
IPAddress secondaryDNS(8, 8, 8, 8);

const char *ssid = "YOUR_SSID";
const char *password = "YOUR_PASSWORD ";

void setup() {
 // .  .  .  .  .
  WiFi.mode(WIFI_STA);

  // Connect to Wi-Fi network with SSID and password using DHCP assigned IP
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  } // while end

  // check IP conflicts, restart MCU if conflicts exist
  if (Ping.ping(local_IP)) {
    Serial.println("IP conflict detected!!");
    Delay(60000);  // wait for a minute then reboot
    ESP.restart();
  } else {
    Serial.println("No IP conflict detected!!");
    // Configure static wlan credentials
    if (!WiFi.config(local_IP, gateway, subnet, primaryDNS)) {
      Serial.println("STA Failed to configure");
    } else {
      Serial.println("Static IP Configuration Succeeded!");
      // Connect to Wi-Fi network with SSID and password
      WiFi.begin(ssid, password);
      while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
      } // while end
    } // else end
  } // else end
 // .  .  .  .  .
}  // setup() end


Energieeinsparung durch Sensoren und Mikrocontroller

Der Stromverbrauch ist ein wichtiges Thema für eingebettete Systeme, die mit Batterien betrieben werden. Der Betrieb von Sensoren und Aktuatoren kann selbst bei einem im Schlafmodus befindlichen Mikrocontroller die Batterie entladen. Der Schrittmotor benötigt auch im Leerlauf Energie. In veröffentlichten Projekten wird eine einfache Schaltung verwendet, die aus zwei Transistoren und drei Widerständen besteht, um die 5,0 VDC zu steuern, die die Last über einen GPIO-Pin des Mikrocontrollers versorgt.

Der gleiche Ansatz kann auf von einem Mikrocontroller gesteuerte Sensoren angewendet werden, die entweder durch Benutzereingriff oder periodisch aktiviert werden. Ich habe einen einfacheren Schaltkreis entworfen, um entweder die DC-Stromversorgung der Sensoren oder ihre Verbindung zur Masse zu steuern. Hier präsentiere ich einen Schaltkreis, der die Verbindung der Sensoren zur Masse mittels eines NPN-Leistungstransistors unterbricht, der bis zu 1,5 A sinken kann. Die Basis des BD139-Transistors wird direkt von einem GPIO des Mikrocontrollers angesteuert (siehe Bild 5).

Bild 5: Steuerung der Sensorstromversorgung durch den Mikrocontroller (MCU).

Eine hohe Einstellung dieses GPIO (D6 in der Abbildung) erzeugt eine Bias-Spannung, die den Transistor in den Sättigungszustand versetzt und die Verbindung zwischen Kollektor und Emitter mit vernachlässigbarem Spannungsabfall schaltet. Während die gemeinsame Masseleitung der Sensoren an den Kollektor des Transistors angeschlossen ist, ist die Stromversorgung der Sensoren vollständig. Wenn der GPIO auf niedrig gesetzt wird, befindet sich der Transistor im Aus-Zustand, wodurch der Strompfad der Sensoren unterbrochen wird.

Der Leser kann die Energieeinsparung weiter erhöhen, indem er die Betriebszeit des Mikrocontrollers verringert, falls die genannten Gründe auch für den Mikrocontroller zutreffen. Ein Mikrocontroller verfügt über verschiedene Schlafmodi.


Fragen oder Kommentare?

Haben Sie Fragen oder Kommentare über diesen Artikel? Wenden Sie sich an den Autor unter drgamallabib@yahoo.co.uk oder kontaktieren Sie Elektor unter redakteur@elektor.com.

 

Abonnieren
Tag-Benachrichtigung zu Embedded Programming jetzt abonnieren!