Arduino C++: имеет ли смысл макрос F() внутри функции?

Макрос F() полезен для хранения глобальных переменных в памяти программ (флэш-память) вместо динамической рабочей памяти, чтобы оставалось больше свободной памяти.

Однако я наткнулся на этот запутанный пример, включенный в библиотеку ESP8266. Он отлично работает, однако у меня есть некоторые сомнения по поводу использования макроса F() внутри функций. Полезно ли использовать его внутри функций?


Пример кода:

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <DNSServer.h>
#include <ESP8266mDNS.h>
#include <EEPROM.h>

/*
   This example serves a "hello world" on a WLAN and a SoftAP at the same time.
   The SoftAP allow you to configure WLAN parameters at run time. They are not setup in the sketch but saved on EEPROM.
   Connect your computer or cell phone to wifi network ESP_ap with password 12345678. A popup may appear and it allow you to go to WLAN config. If it does not then navigate to http://192.168.4.1/wifi and config it there.
   Then wait for the module to connect to your wifi and take note of the WLAN IP it got. Then you can disconnect from ESP_ap and return to your regular WLAN.
   Now the ESP8266 is in your network. You can reach it through http://192.168.x.x/ (the IP you took note of) or maybe at http://esp8266.local too.
   This is a captive portal because through the softAP it will redirect any http request to http://192.168.4.1/
*/

/* Set these to your desired softAP credentials. They are not configurable at runtime */
#ifndef APSSID
#define APSSID "TheGeekMan"
#define APPSK  "12345678"
#endif

const char *softAP_ssid = APSSID;
const char *softAP_password = APPSK;

/* hostname for mDNS. Should work at least on windows. Try http://esp8266.local */
const char *myHostname = "thegeekman";

/* Don't set this wifi credentials. They are configurated at runtime and stored on EEPROM */
char ssid[32] = "";
char password[32] = "";

// DNS server
const byte DNS_PORT = 53;
DNSServer dnsServer;

// Web server
ESP8266WebServer server(80);

/* Soft AP network parameters */
//IPAddress apIP(192, 168, 4, 1);
IPAddress apIP(8, 8, 8, 8);
IPAddress netMsk(255, 255, 255, 0);


/** Should I connect to WLAN asap? */
boolean connect;

/** Last time I tried to connect to WLAN */
unsigned long lastConnectTry = 0;

/** Current WLAN status */
unsigned int status = WL_IDLE_STATUS;

/** Is this an IP? */
boolean isIp(String str) {
  for (size_t i = 0; i < str.length(); i++) {
    int c = str.charAt(i);
    if (c != '.' && (c < '0' || c > '9')) {
      return false;
    }
  }
  return true;
}

/** IP to String? */
String toStringIp(IPAddress ip) {
  String res = "";
  for (int i = 0; i < 3; i++) {
    res += String((ip >> (8 * i)) & 0xFF) + ".";
  }
  res += String(((ip >> 8 * 3)) & 0xFF);
  return res;
}

/** Load WLAN credentials from EEPROM */
void loadCredentials() {
  EEPROM.begin(512);
  EEPROM.get(0, ssid);
  EEPROM.get(0 + sizeof(ssid), password);
  char ok[2 + 1];
  EEPROM.get(0 + sizeof(ssid) + sizeof(password), ok);
  EEPROM.end();
  if (String(ok) != String("OK")) {
    ssid[0] = 0;
    password[0] = 0;
  }
  Serial.println("Recovered credentials:");
  Serial.println(ssid);
  Serial.println(strlen(password) > 0 ? "********" : "<no password>");
}

/** Store WLAN credentials to EEPROM */
void saveCredentials() {
  EEPROM.begin(512);
  EEPROM.put(0, ssid);
  EEPROM.put(0 + sizeof(ssid), password);
  char ok[2 + 1] = "OK";
  EEPROM.put(0 + sizeof(ssid) + sizeof(password), ok);
  EEPROM.commit();
  EEPROM.end();
}

/** Handle root or redirect to captive portal */
void handleRoot() {
  if (captivePortal()) { // If caprive portal redirect instead of displaying the page.
    return;
  }
  server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
  server.sendHeader("Pragma", "no-cache");
  server.sendHeader("Expires", "-1");

  String Page;
  Page += F(
            "<html><head></head><body>"
            "<h1>HELLO WORLD!!</h1>");
  if (server.client().localIP() == apIP) {
    Page += String(F("<p>You are connected through the soft AP: ")) + softAP_ssid + F("</p>");
  } else {
    Page += String(F("<p>You are connected through the wifi network: ")) + ssid + F("</p>");
  }
  Page += F(
            "<p>You may want to <a href='/wifi'>config the wifi connection</a>.</p>"
            "</body></html>");

  server.send(200, "text/html", Page);
}

/** Redirect to captive portal if we got a request for another domain. Return true in that case so the page handler do not try to handle the request again. */
boolean captivePortal() {
  if (!isIp(server.hostHeader()) && server.hostHeader() != (String(myHostname) + ".local")) {
    Serial.println("Request redirected to captive portal");
    server.sendHeader("Location", String("http://") + toStringIp(server.client().localIP()), true);
    server.send(302, "text/plain", "");   // Empty content inhibits Content-length header so we have to close the socket ourselves.
    server.client().stop(); // Stop is needed because we sent no content length
    return true;
  }
  return false;
}

/** Wifi config page handler */
void handleWifi() {
  server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
  server.sendHeader("Pragma", "no-cache");
  server.sendHeader("Expires", "-1");

  String Page;
  Page += F(
            "<html><head></head><body>"
            "<h1>Wifi config</h1>");
  if (server.client().localIP() == apIP) {
    Page += String(F("<p>You are connected through the soft AP: ")) + softAP_ssid + F("</p>");
  } else {
    Page += String(F("<p>You are connected through the wifi network: ")) + ssid + F("</p>");
  }
  Page +=
    String(F(
             "\r\n<br />"
             "<table><tr><th align='left'>SoftAP config</th></tr>"
             "<tr><td>SSID ")) +
    String(softAP_ssid) +
    F("</td></tr>"
      "<tr><td>IP ") +
    toStringIp(WiFi.softAPIP()) +
    F("</td></tr>"
      "</table>"
      "\r\n<br />"
      "<table><tr><th align='left'>WLAN config</th></tr>"
      "<tr><td>SSID ") +
    String(ssid) +
    F("</td></tr>"
      "<tr><td>IP ") +
    toStringIp(WiFi.localIP()) +
    F("</td></tr>"
      "</table>"
      "\r\n<br />"
      "<table><tr><th align='left'>WLAN list (refresh if any missing)</th></tr>");
  Serial.println("scan start");
  int n = WiFi.scanNetworks();
  Serial.println("scan done");
  if (n > 0) {
    for (int i = 0; i < n; i++) {
      Page += String(F("\r\n<tr><td>SSID ")) + WiFi.SSID(i) + ((WiFi.encryptionType(i) == ENC_TYPE_NONE) ? F(" ") : F(" *")) + F(" (") + WiFi.RSSI(i) + F(")</td></tr>");
    }
  } else {
    Page += F("<tr><td>No WLAN found</td></tr>");
  }
  Page += F(
            "</table>"
            "\r\n<br /><form method='POST' action='wifisave'><h4>Connect to network:</h4>"
            "<input type='text' placeholder='network' name='n'/>"
            "<br /><input type='password' placeholder='password' name='p'/>"
            "<br /><input type='submit' value='Connect/Disconnect'/></form>"
            "<p>You may want to <a href='/'>return to the home page</a>.</p>"
            "</body></html>");
  server.send(200, "text/html", Page);
  server.client().stop(); // Stop is needed because we sent no content length
}

/** Handle the WLAN save form and redirect to WLAN config page again */
void handleWifiSave() {
  Serial.println("wifi save");
  server.arg("n").toCharArray(ssid, sizeof(ssid) - 1);
  server.arg("p").toCharArray(password, sizeof(password) - 1);
  server.sendHeader("Location", "wifi", true);
  server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
  server.sendHeader("Pragma", "no-cache");
  server.sendHeader("Expires", "-1");
  server.send(302, "text/plain", "");    // Empty content inhibits Content-length header so we have to close the socket ourselves.
  server.client().stop(); // Stop is needed because we sent no content length
  saveCredentials();
  connect = strlen(ssid) > 0; // Request WLAN connect with new credentials if there is a SSID
}

void handleNotFound() {
  if (captivePortal()) { // If caprive portal redirect instead of displaying the error page.
    return;
  }
  String message = F("File Not Found\n\n");
  message += F("URI: ");
  message += server.uri();
  message += F("\nMethod: ");
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += F("\nArguments: ");
  message += server.args();
  message += F("\n");

  for (uint8_t i = 0; i < server.args(); i++) {
    message += String(F(" ")) + server.argName(i) + F(": ") + server.arg(i) + F("\n");
  }
  server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
  server.sendHeader("Pragma", "no-cache");
  server.sendHeader("Expires", "-1");
  server.send(404, "text/plain", message);
}

void setup() {
  delay(1000);
  Serial.begin(9600);
  Serial.println();
  Serial.println("Configuring access point...");
  /* You can remove the password parameter if you want the AP to be open. */
  WiFi.softAPConfig(apIP, apIP, netMsk);
  WiFi.softAP(softAP_ssid, softAP_password);
  delay(500); // Without delay I've seen the IP address blank
  Serial.print("AP IP address: ");
  Serial.println(WiFi.softAPIP());

  /* Setup the DNS server redirecting all the domains to the apIP */
  dnsServer.setErrorReplyCode(DNSReplyCode::NoError);
  dnsServer.start(DNS_PORT, "*", apIP);

  /* Setup web pages: root, wifi config pages, SO captive portal detectors and not found. */
  server.on("/", handleRoot);
  server.on("/wifi", handleWifi);
  server.on("/wifisave", handleWifiSave);
  server.on("/generate_204", handleRoot);  //Android captive portal. Maybe not needed. Might be handled by notFound handler.
  server.on("/fwlink", handleRoot);  //Microsoft captive portal. Maybe not needed. Might be handled by notFound handler.
  server.onNotFound(handleNotFound);
  server.begin(); // Web server start
  Serial.println("HTTP server started");
  loadCredentials(); // Load WLAN credentials from network
  connect = strlen(ssid) > 0; // Request WLAN connect if there is a SSID
}

void connectWifi() {
  Serial.println("Connecting as wifi client...");
  WiFi.disconnect();
  WiFi.begin(ssid, password);
  int connRes = WiFi.waitForConnectResult();
  Serial.print("connRes: ");
  Serial.println(connRes);
}

void loop() {
  if (connect) {
    Serial.println("Connect requested");
    connect = false;
    connectWifi();
    lastConnectTry = millis();
  }
  {
    unsigned int s = WiFi.status();
    if (s == 0 && millis() > (lastConnectTry + 60000)) {
      /* If WLAN disconnected and idle try to connect */
      /* Don't set retry time too low as retry interfere the softAP operation */
      connect = true;
    }
    if (status != s) { // WLAN status change
      Serial.print("Status: ");
      Serial.println(s);
      status = s;
      if (s == WL_CONNECTED) {
        /* Just connected to WLAN */
        Serial.println("");
        Serial.print("Connected to ");
        Serial.println(ssid);
        Serial.print("IP address: ");
        Serial.println(WiFi.localIP());

        // Setup MDNS responder
        if (!MDNS.begin(myHostname)) {
          Serial.println("Error setting up MDNS responder!");
        } else {
          Serial.println("mDNS responder started");
          // Add service to MDNS-SD
          MDNS.addService("http", "tcp", 80);
        }
      } else if (s == WL_NO_SSID_AVAIL) {
        WiFi.disconnect();
      }
    }
    if (s == WL_CONNECTED) {
      MDNS.update();
    }
  }
  // Do work:
  //DNS
  dnsServer.processNextRequest();
  //HTTP
  server.handleClient();
}

Функция хранится в пространстве программы (памяти программы), поэтому строки внутри этой функции также компилируются в пространство программы. Так почему же они используют макрос F() для принудительного сохранения строк в программном пространстве, в то время как обычно оно уже загружено из программного пространства? Я думаю, что это ненужные накладные расходы или есть ли какие-то преимущества, чтобы сделать это таким образом?


Например, в исходном коде вы можете найти многие из этих присваиваний внутри функций:

void handleWifi()
{
  String Page;
  Page+= F("<html><head></head><body>"
           "<h1>Wifi config</h1>");
  .........
  .........
}

Здесь произойдет то, что строка будет загружена из пространства программы в динамическую память (как переменная «Страница»). Так что же здесь на самом деле? Однако я думаю, что компилятор C++ справится с задачей (оптимизацией) гораздо лучше, если это будет простое назначение.

Я прав или ошибаюсь?


person Codebeat    schedule 09.02.2019    source источник
comment
A function is stored in program space (program memory) so the strings inside this function are also compiled into program space - да, но строка копируется также в энергозависимую память (RAM). Значит дублируется.   -  person KamilCuk    schedule 10.02.2019
comment
@Kamil Cuk Кажется, ты прав, поэтому я решил провести тест, см. Также мой ответ.   -  person Codebeat    schedule 10.02.2019


Ответы (2)


Хорошо, я был неправ, извините, F() также имеет смысл внутри функций. Чтобы доказать это, я сделал несколько тестов.

Сделал несколько тестов с этим простым эскизом (раскомментируйте тот, который вы хотите протестировать):

// Printing 33 chars
// 1.
//void printStr() { Serial.println( "0123456789ABCDEFGHo!@#$%^&*()_+<>?" ); }
//void printStr() { Serial.println( F("0123456789ABCDEFGHo!@#$%^&*()_+<>?" )); }
// 2.
void printStr() { String s = "0123456789ABCDEFGHo!@#$%^&*()_+<>?"; Serial.println( s ); }
//void printStr() { String s; s+=F("0123456789ABCDEFGHo!@#$%^&*()_+<>?"); Serial.println( s ); }

void setup() {
  Serial.begin(9600);
}

void loop() {
 printStr();
 delay(1000);
}

Полученные результаты:

Без использования F() 26816 байт

The sketch uses 263136 bytes (25%) of program storage space. Maximum is 1044464 bytes.
Global variables use 26816 bytes (32%) of the dynamic memory. 
Remain 55104 bytes for local variables. Maximum is 81920 bytes.

С использованием F() 26788 байт

The sketch uses 263212 bytes (25%) of program storage space. Maximum is 1044464 bytes.
Global variables use 26788 bytes (32%) of the dynamic memory. 
Remain 55132 bytes for local variables. Maximum is 81920 bytes.

-28 байт разницы с F(). Но ладно, теперь мы точно знаем! ;-)


Обновление 11 января 2019 г.

Общая информация, почему это работает таким образом:

Оперативная память разделена на разные куски для разных целей. Есть фрагмент, в котором хранятся все глобальные и статические переменные (он же BSS и области данных). Есть стек, в котором хранятся локальные переменные, созданные внутри функции, и, наконец, куча, в которой хранятся динамические переменные.

Если вы хотите узнать больше о том, как эти фрагменты памяти связаны друг с другом, вы можете прочитать больше на Википедии.


Информация скопирована из этой статьи.

person Codebeat    schedule 10.02.2019

Вероятно, по причине, описанной здесь: http://playground.arduino.cc/Learning/Memory

при создании переменных с помощью языка Arduino, таких как:

char message[] = "I support the Cape Wind project.";

Вы копируете 33 байта (1 символ = 1 байт плюс завершающий нуль) из памяти программы в SRAM перед ее использованием. 33 байта — это не так уж много памяти в пуле из 1024 байтов, но если для эскиза требуются большие неизменяемые структуры данных — например, большой объем текста для отправки на дисплей или большая таблица поиска — с помощью флэш-память (память программ) непосредственно для хранения может быть единственным вариантом. Для этого используйте ключевое слово PROGMEM.

person Homper    schedule 09.02.2019
comment
Пустая страница при переходе по ссылке??? Это правда, однако речь идет о глобальных переменных, они ничего не упоминают о переменных внутри функций. - person Codebeat; 10.02.2019
comment
@Codebeat, Это было что-то с сайтом) Теперь он активен. И нет, речь не о глобальных переменных. Речь идет о строковых литералах (строках в двойных кавычках). Все они глобальные, потому что должны быть встроены в исполняемый файл. Обычно, когда компилятор компилирует исполняемый файл (файл .hex), все они копируются в раздел, который затем загружается в SRAM (там хранятся все константы). Но когда вы используете макрос F, компилятор оставляет их внутри кода и создает ассемблерный код (я имею в виду коды операций), который считывает эти литералы как строки из SRAM и не использует их как код. Также чтение быстрее, чем из SRAM. - person Homper; 10.02.2019
comment
Вы, похоже, правы, поэтому я решил сделать тест, см. Также мой ответ. - person Codebeat; 10.02.2019