Fyzikální kabinet FyzKAB

Články Modul Arduino (něco navíc) Pozor na F-Makro a Ethernet client.print()

Pozor na F-Makro a Ethernet client.print()

Modul Arduino společně s rozšiřujícím Ethernet Shield lze velice dobře připojit k Internetu a použít jako jednoduchý řídicí server. Je jedno, zda půjde o jednoduchou meteorologickou stanici nebo vzdálené řízení serva s natáčením webové kamery, vždy budeme od modulu Arduino potřebovat nějakou internetovou odezvu. Ať již to bude zaslání kompletního přehledu počasí v podobě HTML stránky, nebo jen stránka s určitým stavovým hlášením, které potvrdí provedení zadané akce. S ohledem na jednoduchost modulu Arduino a určitou omezenost jeho prostředků je potřeba se velice pečlivě zaměřit na optimalizaci kódu, hospodaření s pamětí a i na náročnost TCP/IP provozu.

Velice zajímavý a zatím příliš nepublikovaný problém ohledně optimalizace webového serveru na modulu Arduino je řešen v článku Optimize the Arduino Webserver od Werner Rothschopf. Zejména obrovským přínosem je upozornění na problém s použitím oblíbeného F-Makra společně s odesíláním odezvy pomocí client.print().

Musíme se přiznat, že po přečtení výše uvedeného článku jsme okamžitě šli předělat všechny vzdálené úlohy ve Vzdálené internetové laboratoři, které jsou založeny na webových severech v modulu Arduino. Dovolíme si tedy zde tento inspirativní článek volně přetlumočit.

Výchozí kód

V následujícím textu budeme upravovat výstupní proceduru webového serveru nazvanou page. Pro naše účely použijeme následující kód webového serveru, kde budeme měnit obsah zmíněné procedury, kterou voláme v těle podmínky pro vypsání výstupu:

/*
Web Server
Ethernet shield attached to pins 10, 11, 12, 13
*/

#include <SPI.h>
#include <Ethernet.h>

// Enter a MAC address and IP address for your controller below.
// The IP address will be dependent on your local network:
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
IPAddress ip(192, 168, 1, 177);

EthernetServer server(80);

void setup() {
  Serial.begin(9600);
  while (!Serial) {
   ;     // wait for serial port to connect. Needed for native USB port only
  }
  Serial.println("Ethernet WebServer Example");

  Ethernet.begin(mac, ip); // start the Ethernet connection and the server:

  // Check for Ethernet hardware present
  if (Ethernet.hardwareStatus() == EthernetNoHardware) {
    Serial.println("Ethernet shield was not found. Sorry, can't run without hardware. :(");
    while (true) {
      delay(1);   // do nothing, no point running without Ethernet hardware
    }
  }
  if (Ethernet.linkStatus() == LinkOFF) {
    Serial.println("Ethernet cable is not connected.");
  }

  // start the server
  server.begin();
  Serial.print("server is at ");
  Serial.println(Ethernet.localIP());
}

void page(EthernetClient &client) {
  /*
   TOTO JE VYSTUPNI PROCEDURA, KTEROU BUDEME POSTUPNE MENIT
  */
}


void loop() {
  // listen for incoming clients
  EthernetClient client = server.available();
  if (client) {
   Serial.println("new client");
   // an HTTP request ends with a blank line
   bool currentLineIsBlank = true;
   while (client.connected()) {
    if (client.available()) {
     char c = client.read();
     Serial.write(c);
     // the HTTP request has ended, so you can send a reply
     if (c == '\n' && currentLineIsBlank) {
      // send a standard HTTP response
      page(client);   // ZDE PROCEDURU page VOLAME 
      break;
     }
     if (c == '\n') {
      // you're starting a new line
      currentLineIsBlank = true;
     } else if (c != '\r') {
      // you've gotten a character on the current line
      currentLineIsBlank = false;
     }
    }
   }
   // give the web browser time to receive the data
   delay(1);
   // close the connection:
   client.stop();
   Serial.println("client disconnected");
  }
}


Program webového serveru budeme testovat dotazem ze zařízení, které bude připojené do stejné sítě (IP: 192.168.1.xxx), na kterém ve webovém prohlížeči zadáme dotaz na adresu: http://192.168.1.177/.



Optimalizace webového serveru Arduino (Autor: Werner Rothschopf)

Před časem jsem ukázal svou verzi webserveru pro Arduino. V tomto návodu bych rád osvětlil, jak optimalizovat webserver Arduino.

Zaměříme se na následující cíle:

  1. Snížení použité programové paměti (Flash Memory nebo PROGMEM) – hodnota zobrazená kompilátorem po překladu kódu.
  2. Nízké využití statické paměti v SRAM – hodnota zobrazená jako „globals“ při překladu kódu.
  3. snížená režie TCP/IP pro zajištění rychlé odezvy a krátkých přenosových časů

Odkud začít…

Začněme jednoduchou HTTP stránkou v prostém textu. Užitná zátěž je 2× 30 znaků. Pokud zahrnete koncové CR/LF, server musí přenést užitečné zatížení 64 bajtů. Tato funkce bude sloužit jako základní příklad pro všechny další testy.

void page(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.........");
}


V závislosti na rychlosti vaší LAN bude přenos trvat cca. 50 ms. Pokud zkontrolujete komunikaci ve Wireshark, uvidíte, že server přenese 14 paketů:

obrazek - 1

Na obrázku uvidíme, že jsme nechali pouze zobrazit zdrojový kód, aby prohlížeč nevyžadoval favicon.ico. Ve Wireshark vidíte pouze pakety přenášené z webového serveru Arduino do prohlížeče pro přenášený text.

Optimalizace client.print()

Probléme je, že každý client.print zahájí samostatnou komunikaci mezi klientem a serverem. Proto byste měli snížit použitý client.print(). Pokud potřebujete oddělené řádky, vložte \r\n ručně:

void page(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");
}


Jak vidíme na následujícím obrázku, přenos nyní zabere pouze 35 ms a používá jen 6 paketů:

obrazek - 2
Pamatujme si tedy:
Čím méně použijeme jednotlivých příkazů client.print(), tím nejen méně paměti program bude potřebovat, ale především snížíme TCP provoz.

Pokud si však čísla po překladu kódu pečlivě zkontrolujeme, uvidíme mírně vyšší nárok na SRAM (globals). To je kvůli dodatečným \r\n, které byly potřeba, protože jinak je dělá každý client.println().

Dále se tedy zaměříme na aspekt úspory paměti.

F-makro a proč bychom jej NEMĚLI používat s client.print

F-Makro zabraňuje kopírování textu do statické paměti (globals), neboť je překládá do flashpaměti podobně jako program. Tímto způsobem statické texty, jako jsou různé hlášky a vypisované texty, nezabírají RAM paměť, kterou pak lze využít pro skutečné proměnné. Díky tomu se F-Macro často používá k úpravě textu pro sériové výstupy, pro texty na LCD a podobné.

Bohužel toto makro na Arduino nefunguje dobře v knihovně Ethernet s funkcí client.print(). Díky implementaci client.print() je každý znak z textu F-Makra odeslán jako samostatný TCP paket. To výrazným způsobem zvýší nejen režii, ale i dobu odpovědi a načítání v prohlížeči.

// just as demonstration
// don't use the F-Makro with client.print in production!
void page(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"));
}


Stále posíláte 64 bajtů z hlediska užitečné zátěže, ale v našem prohlížeči pociťujeme delší dobu přenosu. Wireshark hlásí 114 paketů z Arduina do prohlížeče, což pochopitelně vysvětluje i tu delší dobu odezvy.

obrazek - 3
Pamatujme si:
Pro client.print() raději F-Makro nepoužívejme! F-Makro sice snižuje globální SRAM (protože text se nekopíruje do SRAM), ale má obrovské nevýhody týkající se přenosové režie a rychlosti.

Pojďme se podívat, zda nemáme jiné možnosti.

strcpy_P, PSTR a dočasný buffer

Jak se tedy můžeme vyhnout trvalému zablokování paměti SRAM? Stačí zavést dočasnou vyrovnávací paměť! strcpy_P vám umožňuje kopírovat data z paměti programu do vaší proměnné:

void page(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"
                            "123456789a123456789b123456789c\r\n"
                            "Buffer in one Line............\r\n"));
      client.print(buffer);
}


Protože stále máte pouze jeden client.print, přenos probíhá jen v 6 paketech:

obrazek - 4

Pokud potřebujeme zkombinovat výstup do několika řádků kódu, můžete použít strcat_P ke zřetězení (připojení) dat z paměti programu do proměnné vyrovnávací paměti:

void page(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);
}


Doba přenosu je stále krátká, protože používáme pouze jeden client.print():

obrazek - 5

ale…

Věnujme pozornost velikosti vyrovnávací paměti:

  • Během běhu (když se funkce zpracovává) potřebujeme dostatek volné paměti zásobníku v paměti SRAM. Nicméně - nebude blokovat statickou paměť (globals).
  • Nesmíme překročit velikost vyrovnávací paměti (včetně závěrečného terminátoru NULL!). Pokud potřebujeme obsluhovat větší stránky, musíme buď buffer zvětšit (v příkladu je 128 bajtů), nebo musíme odeslat, pokud dosáhnete limitu bufferu (provést client.print()) a pak začít znovu shromažďovat data do bufferu.
Pamatujme si:
Dočasná vyrovnávací paměť je velmi efektivní způsob přenosu dat pomocí ethernetového klienta Arduino, ale vyžaduje zvláštní péči, pokud jde o velikost vyrovnávací paměti. Jedná se o verzi s minimalistickým využitím paměti SRAM (globals).

Knihovna StreamLib

Pokud máme větší stránky, ruční vyrovnávací paměť může být těžkopádná. Pro větší stránky lze doporučit použití knihovny StreamLib od Juraje Andrássyho, kterou lze nainstalovat přes správce knihovny prostředí Arduino IDE. Knihovna StreamLib poskytuje jednoduchý přístup k vyrovnávací paměti. Do této vyrovnávací paměti můžeme „tisknout“, jak jste zvyklí u klient/sériový/LCD dříve.

obrazek - 6

Můžeme použít všechny známé formáty, které tisk podporuje, včetně F-Makra. Jakmile dokončíme svůj výstup, ukončíme ho pomocí metody .flush() a váš výstup bude odeslán do zvoleného cíle. Pokud vyrovnávací paměť dosáhne své velikosti, knihovna sama automaticky zahájí odesílání. Vyprázdní vyrovnávací paměť a bude opět připravena přijmout další znaky.

Příklad funkce může vypadat takto:

#include <StreamLib.h>    // toto je treba dat na zacatek k ostatnim #include


void page(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();
}


Definujete vyrovnávací paměť buffer. Poté vytvoříte objekt (v příkladu message). Konstruktor přijímá 3 parametry:

  1. odkaz na klienta sítě Ethernet. Tento ethernetový klient bude použit, pokud se vyrovnávací paměť zaplní, nebo když jej ručně ukončíte příkazem .flush()
  2. odkaz na vyrovnávací paměť (buffer)
  3. velikost naší vyrovnávací paměti (použit k tomu jednoduchý příkaz sizeof)

Nyní můžete pomocí několika kroků „tisknout“ do svého objektu, jak jste zvyklí pomocí client.print(). Jak bylo řečeno, StreamLib bude data shromažďovat a odešle je, jakmile se vyrovnávací paměť zaplní, nebo dokončíme výstup pomocí .flush().

obrazek - 7

Pokud jsme již na svém webovém serveru Arduino měli velké stránky s client.print(), je velmi snadné nahradit „client“ výrazem „message“. Jen je třeba nezapomenout dokončit přenos pomocí message.flush().

void page(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();
}


Je pravda, že knihovna StreamLib bude pro svou potřebu potřebovat určitou další programovou paměť, například pro objekt message, ale protože s pomocí F-Makra pomůžete ušetřit „mraky“ SRAM, stojí za to ji implementovat.

Souhrn

Většinu webových serverů Arduino je možné optimalizovat. Na Arduinu UNO lze tak získat spoustu místa a nemusíme se tak zříkat i relativně velkých stránek HTML, CSS a JavaScriptu.

  • Především zredukujme zbytečné řádky kódu s client.print. To nám nejen pomůže omezit paměť Flash/PROGMEM, ale především i snížit provoz TCP/IP a získáme lepší přenosovou dobu.
  • I když F-Makro přináší spoustu výhod – nepoužívejte ho s Ethernet client.print(). Raději použijme vyrovnávací paměť. Určitě lze doporučit použití knihovny StreamLib, která manipulaci s vyrovnávací pamětí výrazně usnadňuje.

Odkaz na originální text článku: Optimize the Arduino Webserver

Reklama:
UPOZORNĚNÍ:
Nesouhlasíme s vyřazením Newtonových zákonů, Ohmova zákona a zákona zachování energie z učiva fyziky základních škol v České republice!