Arduino Webserver optimieren

Im ersten Teil haben wir bereits den Arduino Webserver kennen gelernt. In diesem Teil geht es um die Optimierung des Arduino Webservers.

Dabei verfolgen wir drei Ziele

  1. Reduktion des Programmspeichersbedarfs (wenig Flash Memory bzw. PROGMEM) - der Wert der nach dem Kompilieren als "Der Sketch verwendet..." ausgewiesen wird.
  2. Geringer Static Memory Bedarf im SRAM - der Wert der Nach dem Kompilieren unter "globals" ausgewiesen wird).
  3. Eine schlanke Datenübertragung für schnelle Übertragungszeiten.

Ausgangslage und Vergleichbarkeit

Als Ausgangslage nehmen wir eine einfache HTTP Seite im Textformat mit einer Payload von 2x30 Zeilen. Mit den jeweils zwei Zeichen CR LF werden somit insgesamt 64 Byte ausgegeben:

void page1(EthernetClient &client)
{
  client.println("HTTP/1.0 200 OK");
  client.println("Content-Type: text/plain");
  client.println();
  client.println("123456789a123456789b123456789c");
  client.println("Output with 5 println.........");
}

Abhängig von deiner LAN Geschwindigkeit dauert die Ausgabe etwa 50ms. Wenn du dir die LAN Kommunikation in Wireshark ansiehst, erkennst du, dass der Arduino etwa 14 Pakete übermittelt:

Diese Ausgabe verwenden wir nun zum Vergleich mit den weiteren Varianten. Im großen Bild siehst du auch, dass ich im Firefox den Quelltext anzeigen lasse - damit verhindere ich die Anforderung des favicon.ico und im Netzwerk-Monitor und in Wireshark siehst du ausschließlich die HTTP Seite. Wir werden dies bei jedem weiteren Versuch so machen.

client.print() reduzieren

Für jedes client.print erfolgt eine separate Kommunikation zwischen deinem Client und dem Arduino Webserver. Daher sollst du deinen Code so aufbauen dass du so wenig client.print wie möglich sendest. Den Zeilenumbruch \r\n kannst du auch manuell einfügen:

void page2(EthernetClient &client)
{
  client.print  ("HTTP/1.0 200 OK\r\n"
                 "Content-Type: text/plain\r\n"
                 "\r\n"
                 "123456789a123456789b123456789c\r\n"
                 "Output with 1 print ..........\r\n");
}

Die Übertragung dauert nur mehr etwa 35ms und erfolgt in 6 Paketen:

Merke:

"wenig" client.print() helfen dir, einerseits Flash zu sparen (weil weniger Funktionsaufrufe im Programmcode) und andererseits werden weniger TCP/IP Pakete übermittelt was die Übertragungszeit verkürzt. Wenn du ganz genau nachrechnest wirst du sehen, dass diese Variante geringfügig mehr Static Memory (globals) benötigt. Das ist zu beobarchten wenn du client.println ablöst und den \r\n im Text angibst. Wir werden aber noch weitere Optimierungen vornehmen, daher gehe zunächst diesen Weg ruhig mit.

F-Makro / PROGMEM - und warum es bei client.print nicht verwendet werden soll

Das F-Makro wird gerne bei Fixtexten eingesetzt. Das klappt auch ausgezeichnet bei der seriellen Schnittstelle oder bei Ausgaben auf ein LCD und verhindert das Texte aus dem Programmspeicher in den Static Memory ("globals") kopiert werden. Beim client.print ist das am Arduino jedoch leider suboptimal. Auf Grund der Implementierung des client.print für das F-Makro wird jedes Zeichen einzeln an den Client gesandt. Das erhöht die TCP/IP Kommunikation und bei größeren Seiten wirst du das auch in einer langsameren Gesamtübertragungszeit erkennen:

// just as demonstration
// don't use the F-Makro with client.print in production!
void page3(EthernetClient &client)  
{
  client.print(F("HTTP/1.0 200 OK\r\n"
                 "Content-Type: text/plain\r\n"
                 "\r\n"
                 "123456789a123456789b123456789c\r\n"
                 "Output with F-Makro...........\r\n"));
}

Im Network Monitor von Firefox fällt zunächt die wesentlich längere Übertragung auf. Obwohl es immer noch genau die 64 Byte Payload sind. So wie aktuell das F-Makro Helper implementiert ist, wird jedes Zeichen einzeln übertragen. Wireshark protokolliert 114 Pakete vom Arduino zum Browser.

Dadurch erklärt sich auch die längere Laufzeit. Daher rate ich von der Verwendung des F-Makro in Verbindung mit client.print / client.println dringend ab.

Das F-Makro bei client.print() führt zwar zu einer Verringerung des globalen SRAM - aber hat massive Nachteile bei der Datenübertragung. Das F-Makro wirkt sich sehr negativ auf die Übertragungsgeschwindigkeit aus!

strcpy_P, PSTR und der temporäre Buffer

Du könntest einen dedizierten Buffer anlegen, diesen Buffer sukzessive Befüllen und "in einem Rutsch" an den Client ausliefern. Fixtexte können wieder im Flash/PROGMEM gespeichert werden. Um den Buffer aus dem PROGMEM in den Buffer zu kopieren, verwendest du am besten strcpy_P:

void page4(EthernetClient &client)
{
  char buffer[128] {'\0'};
  //memcpy(buffer, '\0', 128);
  strcpy_P(buffer, PSTR("HTTP/1.0 200 OK\r\n"
                        "Content-Type: text/plain\r\n"
                        "\r\n" 
                        "123456789a123456789b123456789c\r\n"
                        "Buffer in one Line............\r\n"));
  client.print(buffer);
}

Dadurch dass wieder nur ein client.print ausgegeben wird, sind auch nur 6 Pakete zur Übertragung notwendig

Wenn du den Output aus mehreren Zeilen zusammensetzen musst, dann kannst du zum Verketten der c-Strings ein strcat bzw. ein strcat_P verwenden. strcat_P verwendest du für Fixtexte damit diese nicht im Static Memory ("globals") des SRAM gehalten werden müssen:

void page5(EthernetClient &client)
{
  char buffer[128] {'\0'};
  strcpy_P(buffer, PSTR("HTTP/1.0 200 OK\r\n"
                        "Content-Type: text/plain\r\n"
                        "\r\n"));
  strcat_P(buffer, PSTR("123456789a123456789b123456789c\r\n"));
  strcat_P(buffer, PSTR("Buffer with several lines.....\r\n"));
  client.print(buffer);
}

Auf die Laufzeit hat das nur geringfügig, auf die Übertragungszeit hat das keine Auswirkung - du sendest ja weiterhin nur ein client.print:

Vorsichtig musst du mit der Größe des Buffers sein:

  • Für den Buffer muss zur Laufzeit ein ausreichend großer SRAM am Stack zur Verfügung stehen. Dieser Speicher wird nur für die Dauer der laufenden Funktion belegt. Es belegt aber keinen Static Memory ("globals").
  • Der Buffer (im Beispiel 128 Byte) darf inkl. Nullterminator nicht überschritten werden! Wenn du größere Seiten übertragen musst, muss du entweder den Buffer größer machen, oder zwischendurch separate client.print lossenden und den Buffer neu befüllen.

Die Verwendung eines temporären Buffers ist ein sehr effiziente Methode, Daten mit dem Ethernet Client zu übertragen, bedarf aber einiger Sorgfalt. Damit wird am meisten SRAM (globals) gespart.

Die StreamLib Library

Die manuelle Verwaltung eines Buffers kann mühsam werden. Aber auch dafür gibt es eine Lösung. Direkt über die Arduino IDE ist die StreamLib Library installierbar. Die Streamlib Library stellt einen Ausgabebuffer zur Verfügung. Du "druckst" in gewohnter Weise in den Buffer. Dabei kannst du zwischen F-Makro, Fixtexte aus dem SRAM oder auch Variablen mischen. Wenn alle Ausgaben fertig sind, schickst du mit einem abschließenden .flush() die gesammelten Daten in einem einzigen client.print in das Netzwerk. Dabei kümmert sich die Streamlib Library um die Buffergröße: wenn du mehr Daten in den Buffer schreibst als der Buffer groß ist, beginnt die die StreamLib Library mit der Ausgabe, löscht den Buffer und kann weitere Daten im Eingangsbuffer übernehmen.

Ein Beispielcode sieht wie folgt aus:

#include <StreamLib.h>

void page6(EthernetClient &client)
{
  const size_t MESSAGE_BUFFER_SIZE = 64;
  char buffer[MESSAGE_BUFFER_SIZE];  // a buffer needed for the StreamLib
  BufferedPrint message(client, buffer, sizeof(buffer));
  message.print(F("HTTP/1.0 200 OK\r\n"
                  "Content-Type: text/plain\r\n"
                  "\r\n"
                  "123456789a123456789b123456789c\r\n"
                  "Streamlib with F-Makro........\r\n"));
  message.flush();
}

Zunächst braucht es einen buffer als Zwischenspeicher. Anschließend legst du ein Objekt (im Beispiel message) an. Der Konstruktor benötigt 3 Parameter:

  • eine Referenz auf deinen Ethernet Client. An diesen Ethernet Client erfolgt die Ausgabe wenn der Buffer voll ist oder wenn du mit .flush() die Ausgabe anstößt
  • eine Referenz auf den zuvor angelegten Buffer
  • die Größe des Buffers.

Nun kannst du in einem oder mehreren Schritten in dieses Objekt "drucken" (streamen) so wie du es auch von einem client.print gewohnt bist. Der Unterschied ist jedoch, dass die StreamLib deine Ausgaben im Buffer sammelt und die Daten erst dann sendet, wenn der Buffer voll ist - oder wenn du ihn mit .flush() final leerst.

Wenn du heute bereits große Teile des Webservers mit client.print ausgestattet hast, dann kannst du relativ einfach auf die StreamLib.h umbauen: Buffer anlegen, BufferedPrint Objekt anlegen, die client.print austauschen auf message.print und am Ende nicht auf den message.flush vergessen.

void page7(EthernetClient &client)
{
  const size_t MESSAGE_BUFFER_SIZE = 64;
  char buffer[MESSAGE_BUFFER_SIZE];  // a buffer needed for the StreamLib
  BufferedPrint message(client, buffer, sizeof(buffer));
  message.print(F("HTTP/1.0 200 OK\r\n"
                  "Content-Type: text/plain\r\n"
                  "\r\n"));
  message.print(F("123456789a123456789b123456789c\r\n"));
  message.print(F("StreamLib with several print..\r\n"));
  message.flush();
}

Die Streamlib Library benötigt etwas zusätzlichen PROGMEM gegenüber der manuellen Verwaltung des Buffers.

Zusammenfassung

Viele Arduino Webserver lassen sich optimieren. Auch auf einem Arduino UNO muss man nicht auf umfangreiche HTML-Seiten, ein CSS oder JavaScript mangels Speicherplatz verzichten. Reduziere Codezeilen und verwende so wenig client.print Ausgaben wie möglich. Das spart nicht nur Flash/PROGMEM, sondern wird auch den TCP/IP Verkehr im Netzwerk reduzieren und somit Übertragungszeit spürbar verkürzen.

Auch wenn das F-Makro für andere Komponenten oft Vorteile bringt, solltest du beim client.print() darauf verzichten. Am wenigsten Flash verbrauchst du bei der Verwendung eines eigenen Buffers den du mit Textteilen aus dem PROGMEM befüllst. Sehr praktisch ist die StreamLib Library, da hier die manuelle Verwaltung des Buffers entfällt.

Links

Die mit Sternchen (*) gekennzeichneten Verweise sind sogenannte Affiliate/Provision-Links. Wenn du auf so einen Verweis klickst und über diesen Link einkaufst, bekomme ich von deinem Einkauf eine (kleine) Provision. Für dich verändert sich der Preis dadurch nicht. Ich empfehle nur Produkte die ich selber besitze und wenn ich überzeugt bin, dass sie für andere interesssant sind.

 

Protokoll

First upload: 2020-11-08 | Version: 2022-03-22