2015年11月13日 星期五

ESP8266 函式庫記憶體耗損比較

在測試 WeeESP8266 函式庫時發現, 程式好像也沒多長, 但編譯後竟然吃掉非常多的 SRAM (動態記憶體) 與 Flash (程式記憶體), 甚至會讓部分函式破功 (例如列出附近可用 AP), 我想會不會是因為函式庫裡許多沒用到的函式還是會占用動態記憶體呢? 於是就回頭把自己土法煉鋼的函式庫改版, 完成後我迫不及待想知道它在記憶體耗損方面, 是否真的有比 WeeESP8266 函式庫好很多呢? 參考 :

# ESP8266 函式庫 v2
# ITEAD WeeESP8266 函式庫測試

其實並非每一個函式都會在應用中使用到, 對 ESP8266 的物聯網應用而言, 最常用的當然就是 createTCP() 與 send() 這兩個函式, 其他的函式幾乎用不到, 因為 ESP8266 會記住工作模式, 連線多寡, 以及上一次連線的 AP 之 SSID 與密碼, 只要一送電就會馬上連線. 除非下指令去改這些參數, 否則都會被記在不會揮發的 Flash 記憶體裡. 所以如果能卸除這些不會用到的函式, 就不會虛耗記憶體了. 但是對於封裝好的 WeeESP8266 函式庫而言, 這就難辦了, 只有自己寫的函式庫才能這麼做.

以下我就以向 Thingspeak 物聯網服務伺服器傳送 "隨機溫度" 為例, 來測試看看滿載的 WeeESP8266 函式庫與減載後的 v2 在記憶體耗損上有何差異. 對於一般的 IoT 應用, 只要用到自己寫的函式庫裡的五個函式而已 : get_response(), reset(), start_tcp(), send_data(), 以及 release().

在測試前先來看一個空白的 Arduino 程式的記憶體損耗 :

void setup() {}
void loop() {}


編譯後幾乎沒耗損多少記憶體, 訊息如下 :

草稿碼使用了 450 bytes (1%) 的程式存儲空間。最大值為 30,720 bytes。
全域變數使用了 9 bytes (0%) 的動態記憶體,剩餘 2,039 bytes 供局部變數。最大值為 2,048 bytes 。

如果載入 SoftwareSerial 與 WeeEsp8266 函式庫 :

#include "ESP8266.h"
#include <SoftwareSerial.h>

SoftwareSerial sSerial(10,11); //(RX,TX) 與 ESP8266 介接的軟體串列埠
ESP8266 wifi(sSerial);  //建立名為 wifi 的 ESP8266 物件

void setup() {}
void loop() {}


編譯後程式增加逾 2KB, 變數增加逾 100B, 占了約 6~8%, 訊息如下 :

草稿碼使用了 2,654 bytes (8%) 的程式存儲空間。最大值為 30,720 bytes。
全域變數使用了 132 bytes (6%) 的動態記憶體,剩餘 1,916 bytes 供局部變數。最大值為 2,048 bytes 。


測試 1 使用 WeeESP8266 函式庫 :

測試 1 : WeeESP8266

#include "ESP8266.h"
#include <SoftwareSerial.h>

SoftwareSerial sSerial(10,11); //(RX,TX) 與 ESP8266 介接的軟體串列埠
ESP8266 wifi(sSerial);  //建立名為 wifi 的 ESP8266 物件
byte c; //傳遞給 Thingspeak 的變數
String GET;  //GET 字串

void setup() {
  sSerial.begin(9600);  //設定軟體序列埠速率 (to ESP8266)
  Serial.begin(9600);  //設定軟體序列埠速率 (to PC)
  Serial.println("*** SoftSerial connection to ESP8266 ***");
  }

void loop() {
  test();
  delay(16000);  //Thingspeak limit
  }

void test() {
  if (wifi.restart()) {Serial.println("Reset ESP8266 ... OK");}  //重設 ESP8266
  else {Serial.println("Reset ESP8266 ... NG");}
  delay(5000);  //等待 ESP8266 與 AP 連線
  c=random(10,50);
  if (wifi.createTCP("184.106.153.149",80)) {  //連線 Thingspeak
    Serial.println("Connect Thingspeak ... OK");
    GET="GET /update?api_key=NO5N8C7T2KINFCQE&field1=" + String(c) + "\r\n";
    const char *data=GET.c_str();
    if (wifi.send((const uint8_t*)data, strlen(data))) {Serial.println("Send GET/ ... OK");}
    else {Serial.println("Send GET/ ... NG");}  
    }
  else {Serial.println("Connect Thingspeak ... NG");}
  wifi.releaseTCP();
  }

此程式編譯後的訊息為 :

草稿碼使用了 9,578 bytes (31%) 的程式存儲空間。最大值為 30,720 bytes。
全域變數使用了 908 bytes (44%) 的動態記憶體,剩餘 1,140 bytes 供局部變數。最大值為 2,048 bytes 。

可見程式耗掉 31% 的 Flash 記憶體, 而變數耗掉 44% 的 SRAM.

這裡我用了一個隨機變數 c 代表攝氏溫度, 利用 random() 函式產生 10~50 間不斷變化的溫度值, 將其放在 GET 方法字串中傳送至 Thingspeak 伺服器. 要注意的是, 傳給 WeeESP8266 的 send() 函式之參數 data 必須是一個指向字元陣列的指標, 這裡用字串的 c_str() 函式將字串轉成字元陣列. 其次是, 此陣列需用 const 宣告為常數. 參考 :

# Convert string to const char* issue [duplicate]

還有一點是非常重要, 傳送字串後面要自行加上跳行 "\r\n", 否則雖然 send() 回傳 true 表示成功, 事實上資料並未成功地寫入伺服器.

下面就是這個 "隨機溫度" 在 Thingspeak 上的圖形 :


接下來測試我的 WiFi 函式庫 v2, 只要留下 get_response(), start_tcp(), send_data(), 以及 release() 這四個函式即可, 其餘用不到都刪掉 :

測試 2 : TonyWiFi v2

#include <SoftwareSerial.h>

SoftwareSerial sSerial(10,11); //(RX,TX) 與 ESP8266 介接的軟體串列埠
byte c; //傳遞給 Thingspeak 的變數
String GET;  //GET 字串

void setup() {
  sSerial.begin(9600);  //設定軟體序列埠速率 (to ESP8266)
  Serial.begin(9600);  //設定軟體序列埠速率 (to PC)
  Serial.println("*** SoftSerial connection to ESP8266 ***");
  }

void loop() {
  test();
  delay(16000);  //Thingspeak limit
  }

void test() {
  if (reset()) {Serial.println("Reset ESP8266 ... OK");}  //重設 ESP8266
  else {Serial.println("Reset ESP8266 ... NG");}
  delay(5000);  //等待 ESP8266 與 AP 連線
  if (start_tcp("www.google.com",80)) {  //連線 Google 首頁
    Serial.println("Connect Google ... OK");
    if (send_data("GET /")) {Serial.println("Send GET/ ... OK");}
    else {Serial.println("Send GET/ ... NG");}  
    }
  else {Serial.println("Connect Google ... NG");}
  c=random(10,50);
  if (start_tcp("184.106.153.149",80)) {  //連線 Thingspeak
    Serial.println("Connect Thingspeak ... OK");
    GET="GET /update?api_key=NO5N8C7T2KINFCQE&field1=" + String(c);
    if (send_data(GET)) {Serial.println("Send GET/ ... OK");}
    else {Serial.println("Send GET/ ... NG");}  
    }
  else {Serial.println("Connect Thingspeak ... NG");}
  }

String get_response(int timeout, String term="OK\r\n") {  //取得 ESP8266 的回應字串
  String str="";  //儲存接收到的回應字串
  unsigned long t=millis() + timeout;  //計算 timeout 的時戳
  while (millis() < t) {  //還沒到達 timeout 時間
    if (sSerial.available()) { //若軟體序列埠接收緩衝器有資料
      str.concat((char)sSerial.read());  //串接回應字元    
      if (str.endsWith(term)) {break;}  //檢查是否已收到預期回應終結字串
      //if (str.lastIndexOf(term) != -1) {break;}  //檢查是否已收到預期回應終結字串
      }
    }
  str.trim();  //去除頭尾空白字元 (含跳行)
  return str;  //傳回回應字串
  }

boolean reset() {
  sSerial.println("AT+RST");  //重設 ESP8266
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000, "]\r\n");  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

boolean start_tcp(String address, int port) {  //單一連線用
  sSerial.println("AT+CIPSTART=\"TCP\",\"" + address + "\"," + String(port));
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000, "Linked");  //取得 ESP8266 回應字串
  if (str.indexOf("Linked") != -1) {return true;}
  else {return false;}
  }

boolean send_data(String s) {  //單一連線用
  String s1=s + "\r\n";  //務必加上跳行
  sSerial.println("AT+CIPSEND=" + String(s1.length()));
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000, ">");  //取得 ESP8266 回應字串
  if (str.indexOf(">") != -1) {  //收到 > 開始傳送資料
    //Serial.println("Sending ... \r\n" + s1);
    sSerial.println(s1); //傳送資料
    sSerial.flush();  //等待序列埠傳送完畢
    str=get_response(10000, "Unlink");  //取得 ESP8266 回應字串
    if (str.indexOf("+IPD") != -1) {  //傳送成功會自動拆線 Unlink
      //Serial.println(str);    
      return true;
      }
    else {  //傳送不成功須自行拆線
      release();  //關閉 IP 連線
      return false;
      }
    }
  else {  //傳送不成功須自行拆線
    release();  //關閉 IP 連線
    return false;
    }
  }

boolean release() { //單一連線
  sSerial.println("AT+CIPCLOSE");  //關閉 IP 連線
  sSerial.flush();  //等待序列埠傳送完畢
  String str=get_response(10000);  //取得 ESP8266 回應字串
  if (str.indexOf("OK") != -1) {return true;}
  else {return false;}
  }

編譯訊息如下 :

草稿碼使用了 8,882 bytes (28%) 的程式存儲空間。最大值為 30,720 bytes。
全域變數使用了 712 bytes (34%) 的動態記憶體,剩餘 1,336 bytes 供局部變數。最大值為 2,048 bytes 。

可見, 同樣的功能, 減載的 WiFi v2 在程式記憶體耗損上只是少一些而已, 但在動態記憶體上卻少了 10%, 確實有比較省.

   

沒有留言 :