ESP32&8266兼容處理

一直以黎我覺得用電腦供電比 12V PWM 風扇調速最實用,寫 Arduino 嘅首選係有 12V DC 插頭用 esp8266 嘅 Wemos D1 R2 ,令外我新買左块都好普及用 esp32 嘅 UNO D1 R32 。咁叫 esp32 應該係第一代嘅双核 240MHz mcu 比 8266 嘅單核 160MHz 快,ram 都多好多。esp32 嘅新一代係 esp32s3 速度一樣,淘宝有块 ESP32S3 UNO;令外我有試過同 8266 速度一樣嘅 esp32c3

Wemos D1 R2 & D1 R32 with 6*4 PCB as shield pinout define

雖然都係 UNO size,但因為 Arduino UNO R3 嘅 shield 係用5V,所以 GPIO 用 3.3V 嘅 8266 大部份都唔啱用,但 shield 嘅概念好值得參考,可以用 6*4 PCB 板同 11cm 加长排母自制 shield,一般排母太幼有接觸不良問題。令外,有天線頭嘅 Wemos D1 mini pro 都好好用,可以配 mini D1 洞洞板,但我只係預插單層方便測試,所以用圆孔排母。如果想自制嘅 mini D1 同 6*4 PCB shield 兼容,可以銲一塊 6*4 PCB 底板接線 map 番咁多條 pin 到 mini D1 嘅插座。

D1 R32 & Wemos D1 mini pro accessories

D0 – D8 號唔代表 GPIO 嘅真正数字,而且Wemos D1 R2UNO D1 R32 就算 pin 嘅位置一樣,但 GPIO 嘅数字會唔同,所以想寫到兼容,首先要幫啲 pin 改茗 map 番啲 GPIO,同時方便分辨 8266 D0 – D8 號 pin 有啲唔同特質 。除左 D0 無 PWM 功能只限 boolean 嘅特質之外,其他都有 PWM 無乜大分別,所以我用 D0 黎造 BLDC 控制 CW 定 CCW 所以叫 “PIN_DIR”,或者用黎造電源開關嘅 relay;令外 D8 係獨特一定要 “低電平” 先 boot 倒机,因為 “高電平” 嘅話會入左特別嘅 SDIO mode,唔會 run Ardinuo 啲 code,需然可以用黎造 PWM,但輸出唔可以駁電平由 3.3V 上拉到 5V 裝置,又唔可以 INPUT_PULLUP 造 PWM 風扇嘅 hall sensor 測速或者一般黚制 button,但可以用黎駁有訊號先輸入高電平嘅 “光耦測速器” ,於是我叫佢造 “PIN_COUNTER”。

#if defined(ESP32)
#include <ESPmDNS.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <HTTPClient.h>
#include <Update.h> 
#include <BlynkSimpleEsp32.h>
#include <ESP32Servo.h> // (ESP32c3 not support)
#else
#include <ESP8266mDNS.h>
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESP8266HTTPClient.h>
#include <Updater.h> //update littleFS
#include <BlynkSimpleEsp8266.h>
#include <Servo.h>
#endif

#include <AceRoutine.h> // multi task manager
using namespace ace_routine;
#include <Adafruit_AS7341.h> // 11 channel colorimeter
#include <Adafruit_INA219.h> //current sensor
#include <Adafruit_MLX90614.h> //IR temp sensor
#include <Adafruit_SHT31.h> // temp & humidity sensor
#include <Adafruit_TCS34725.h> // rgb colorimeter
#include <Adafruit_VL53L0X.h> // ToF distance sensor
#include <ArduinoJson.h> 
#include <ArduinoOTA.h>
#include <DFRobot_QMC5883.h> // compass sensor
#include <ESPAsyncWebServer.h> 
#include <LittleFS.h>
#include <MAX30105.h> // heart rate sensor
#include <heartRate.h> // for MAX30105
#include <MPU9250_WE.h> // accelerometer sensor
#include <NeoPixelBus.h> // neopixel ws2812
#include <OLEDDisplayUi.h>
#include <SH1106Wire.h> // OLED display (ESP32c3 not support)
#include <SSD1306Wire.h> // OLED display (ESP32c3 not support)
#include <SparkFunSX1509.h> // I2C GPIO expander
#include <TimeLib.h> // time for ntp
#include <WiFiUdp.h>

//*******************************************************************************************
// global & class objects
//*******************************************************************************************

const char SSID[] = "XXXXXX";
const char PASSWORD[] = "*******";
bool enable_blynk = 0;
bool is_wsconnect = 0;
struct tm time_tm;
time_t time_ntp = 0;
String string_json = "";
StaticJsonDocument<512> json;
StaticJsonDocument<128> jsonRX;
HTTPClient http;
AsyncWebServer server(80);
AsyncWebSocketMessageHandler wsHandler;
AsyncWebSocket ws("/ws", wsHandler.eventHandler());
WiFiClient client;
WiFiUDP udp;

//*******************************************************************************************
// html
//*******************************************************************************************

const char INDEX_HTML[] = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
    <title>%echo-dns%.local - ESP Async Web Server</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
    <link rel="icon" href="data:,">
    <link rel='stylesheet' href='http://external-web-server.com/espwebserver.css' type='text/css'>
    <script src='https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js'></script>
    <script src='http://external-web-server.com/espwebserver.js'></script>
    </head>
    <body>
    </body>
    </html>
)rawliteral";

String processor(const String& placeholder) {
  if (placeholder == "echo-dns") {
    return DNS;
  }
  return String();
}
//*******************************************************************************************
//  hardware
//*******************************************************************************************

// GPIO pins
#if defined(ESP32)               // WEMOS D1 R32
const uint8_t PIN_RX = 3;  		// ws2812 LED
const uint8_t PIN_TX = 1;
const uint8_t PIN_ANALOG = 2;    // A0
const uint8_t PIN_DIR = 26;      // D0, no PWM, pwm motor direction
const uint8_t PIN_SCL = 25;      // D1
const uint8_t PIN_SDA = 17;      // D2
const uint8_t PIN_IN0 = 16;      // D3, button PULLUP
const uint8_t PIN_SERVO = 27;    // D4
const uint8_t PIN_PWM = 14;      // D5
const uint8_t PIN_IN1 = 34;      // D6
const uint8_t PIN_IN2 = 35;      // D7
const uint8_t PIN_COUNTER = 4;   // D8
const uint8_t PIN_R32_LED = 2;
#else  // WEMOS D1 R2 & D1 mini 8266
const uint8_t PIN_RX = 3;  		// ws2812 LED DMA+I2C method
const uint8_t PIN_TX = 1;
const uint8_t PIN_ANALOG = 17;   // A0
const uint8_t PIN_DIR = 16;      // D0, no PWM, motor direction, power on/off
const uint8_t PIN_SCL = 5;       // D1
const uint8_t PIN_SDA = 4;       // D2
const uint8_t PIN_IN0 = 0;       // D3, button PULLUP
const uint8_t PIN_SERVO = 2;     // D4, LED_BUILTIN
const uint8_t PIN_PWM = 14;      // D5
const uint8_t PIN_IN1 = 12;      // D6
const uint8_t PIN_IN2 = 13;      // D7
const uint8_t PIN_COUNTER = 15;  // D8, low to boot, don't PULLUP
#endif

// I2C Address
bool array_i2c[127];
const uint8_t ADD_COMPASS = 30;
const uint8_t ADD_LIGHT = 35; //BH1750
const uint8_t ADD_LCD = 39;
const uint8_t ADD_TOF = 41; //TIME OF FIGHT
const uint8_t ADD_TCS = 41; // RGB colorimeter
const uint8_t ADD_COLOR = 57; //AS7341 colorimeter
const uint8_t ADD_OLED = 60;
const uint8_t ADD_SX = 62; //SX1509 PORT EXPANSION
const uint8_t ADD_INA = 64; // VOLTAGE & CURRENT
const uint8_t ADD_SHT = 68; //HUMIDITY & TEMP
const uint8_t ADD_TEMP = 72; //TMP102
const uint8_t ADD_EEPROM = 80; //24LCXX 
const uint8_t ADD_HEART = 87; //MAX3010X
const uint8_t ADD_MLX = 90; //IR TEMP
const uint8_t ADD_MPU = 104; //ACELEROMETER
const uint8_t ADD_PRESSURE = 118; //BMP280
const uint8_t ADD_GAS = 119; //BME680

//*******************************************************************************************
//   multi task loop
//*******************************************************************************************

void fn_loop_1ms();
void fn_loop_40ms();
void fn_loop_1000ms();
void fn_loop_8640ms();

void loop() {
  fn_loop_1ms();
  CoroutineScheduler::loop();
}
COROUTINE(loop_40ms) {
  COROUTINE_LOOP() {
    serializeJson(json, string_json);
#if defined(ESP8266)
    MDNS.update();
#endif
    ws.cleanupClients(2);  // no more than 2 clients
    ws.textAll(string_json);
    fn_loop_40ms();
    COROUTINE_DELAY(40);
  }
}
COROUTINE(loop_1000ms) {
  COROUTINE_LOOP() {
    json["heap"] = ESP.getFreeHeap();
    if (WiFi.status() == 3) json["now"] = now();
    ArduinoOTA.handle();
    fn_loop_1000ms();
    COROUTINE_DELAY(1000);
  }
}
COROUTINE(loop_8640ms) {
  COROUTINE_LOOP() {
    fn_wifi_begin();
    fn_loop_8640ms();
    COROUTINE_DELAY(8640);
  }
}
......

我主要寫一個 .h 比晒唔同板 include,為左寫到兼容 8266 同 esp32,好多只係有小小唔同有両個版本嘅 library,但有啲只得 8266 版例如 multitask management 就要轉用 AceRoutine,重點在於 web server 我改左用 ESPAsyncWebServer 而改左唔小曲。

void fn_wemos_blynk_begin(const char DNS[], const char blynk_token[]) {
  // hardware
  json["dns"] = String(DNS);
  Serial.begin(115200);
  Serial.println("");
#if defined(ESP32)
  LittleFS.begin(true);  // format if mount failed
#else
  analogWriteFreq(38000);  // IRremote
  analogWriteRange(255);
  LittleFS.begin();
#endif
  CoroutineScheduler::setup();
  
  // network
  WiFi.hostname(DNS);
  WiFi.mode(WIFI_STA);
  server.onNotFound(fn_handle_cors_404);
  server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
      request->send(200, "text/html", INDEX_HTML, processor);
  });
  server.on("/json", HTTP_GET, fn_handle_cors_json);
  server.on("/update", HTTP_POST, [](AsyncWebServerRequest* request) {}, [](AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) {
    if (!index) {
      Serial.printf("Starting update: %s\n", filename.c_str());
      ws.textAll("Starting update: " + String(filename));
#if defined(ESP32)
      Update.begin();
#else
      Update.begin(0x200000, U_FS);// offset for Flash size: 4MB (FS:2MB OTA:~1019KB)
#endif
    }
    Update.write(data, len);
    if (final) {
      if (Update.end(true)) {
        Serial.println("Update LittleFS complete!");
        ws.textAll("Update LittleFS complete!");
      } else {
        Serial.println("Update LittleFS failed!");
        ws.textAll("Update LittleFS failed!");
      }
    }
  });

  // websocket
  ws.onEvent([](AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) { (void)len;

  if (type == WS_EVT_CONNECT) {
    is_wsconnect = true;
    ws.textAll("new client connected");
    client->setCloseClientOnQueueFull(false);
    client->ping();
  } else if (type == WS_EVT_DISCONNECT) {
    is_wsconnect = false;
    ws.textAll("client disconnected");
  } else if (type == WS_EVT_ERROR) {
    is_wsconnect = false;
  } else if (type == WS_EVT_DATA) {
    AwsFrameInfo* info = (AwsFrameInfo*)arg;
    String msg = "";
    if (info->final && info->index == 0 && info->len == len) {
      if (info->opcode == WS_TEXT) {
        DeserializationError error = deserializeJson(jsonRX, (char*)data);
         if (jsonRX["WSRX"].as<String>() == "RESTART") {
          ESP.restart();
        }
      }
    }
  }
  });

  server.addHandler(&ws).addMiddleware([](AsyncWebServerRequest* request, ArMiddlewareNext next) {
    if (ws.count() > 1) {      // if we have 2 clients or more, prevent the next one to connect
      request->send(503, "text/plain", "Server is busy");
    } else { // process next middleware and at the end the handler
      next();
    }
  });
  server.begin();
  MDNS.begin(DNS);
  MDNS.addService("http", "tcp", 80);
  fn_ota_begin(DNS, 8266);
  udp.begin(80);
  if (enable_blynk) Blynk.begin(blynk_token, SSID, PASSWORD);
  fn_i2c_begin(PIN_SDA, PIN_SCL);
  fn_ntp_sync();
}

//*******************************************************************************************
// functions
//*******************************************************************************************

void fn_handle_cors_json(AsyncWebServerRequest* request) {
  AsyncWebServerResponse* response;
  response = request->beginResponse(200, "application/json; charset=utf-8", string_json);
  response->addHeader("Access-Control-Allow-Origin", "*");
  response->addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
  response->addHeader("Access-Control-Allow-Headers", "Content-Type");
  response->addHeader("X-Content-Type-Options", "nosniff");
  response->addHeader("Content-Length", String(string_json.length()));
  response->addHeader("Cache-Control", "no-cache");
  response->addHeader("Connection", "keep-alive");
  request->send(response);
}

相比起 ESP8266 WebServerESPAsyncWebServer 用返 http port 80 就造倒 websocket 功能,而且效果明顯更穏定同順暢,但 poll rate 最快去到 25Hz 去唔到 50Hz。同一段曲轉用 esp32 嘅話令我大感意外,唔知點解竟然明顯比 8266 更窒,就算問 copilot 都無法解答攪左好耐都解決唔倒嘅問題。喺 8266 閑倒但 esp32 閑唔倒嘅 library 對我黎講只有 IRremote,相反因為我唔用 bluetooth 同 cam,所以揾唔倒有乜功能轉用 esp32 之後可以受惠。再加上用 Arduino IDE complie esp32 嘅曲明顯比 8266 慢好多;所以雖然好似好落後,但對我黎講 8266 比 esp32 更好用,但寫就盡量寫到両者兼容方便比較。

Reference: