ESP8266 + ESP32: Как писать кроссплатформенный код в Arduino IDE.

Время чтения: 25 минут | Технология: IoT Development | Платформы: ESP8266, ESP32


Разработка проектов для нескольких платформ (ESP8266 и ESP32) в одном проекте может быть сложной задачей, но с правильными подходами это становится управляемым процессом. В этой статье мы рассмотрим, как эффективно использовать обе платформы в одном проекте с помощью Arduino IDE, избегая распространенных ошибок.

Подготовка Arduino IDE для работы с ESP8266 и ESP32.


Подготовка Arduino IDE для работы с ESP8266 и ESP32.

Установка необходимых пакетов

  1. Установка Arduino IDE:
    • Скачайте последнюю версию с официального сайта
    • Установите IDE, следуя инструкциям для вашей операционной системы
  2. Добавление поддержки ESP8266:
    • Откройте Arduino IDE
    • Перейдите в Файл <span class="token operator">></span> Параметры (File > Preferences)
    • В поле Дополнительные ссылки для Менеджера плат Additional Boards Manager URLs добавьте:
      http://arduino.esp8266.com/stable/package_esp8266com_index.json
              
    • Скопируйте и вставьте следующую строку в поле Дополнительные URL-адреса менеджера плат:
    • Перейдите в Инструменты > Плата > Менеджер плат… Tools > Board > Boards Manager
    • Найдите esp8266 и установите последнюю версию
  3. Добавление поддержки ESP32:
    • В поле Дополнительные ссылки для Менеджера плат Additional Boards Manager URLs добавьте:
      https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
              
    • Перейдите в Инструменты > Плата > Менеджер плат… (Tools > Board > Boards Manager)
    • Найдите esp32 и установите последнюю версию
    Найдите ESP32 и нажмите кнопку установки для esp32 от компании Espressif Systems версии 3.X.

Использование #ifdef для разделения кода.

Основы #ifdef.

#ifdef (if defined) — это директива препроцессора, которая позволяет включать или исключать части кода в зависимости от определенных макросов. Это полезно для разделения кода между разными платформами.

Пример использования #ifdef для ESP8266 и ESP32.

#ifdef ESP32
  #include <WiFi.h>
  #include <SPI.h>
#else
  #include <ESP8266WiFi.h>
  #include <SPI.h>
#endif
void setup() {
  Serial.begin(115200);
  #ifdef ESP32
    Serial.println("Running on ESP32");
  #else
    Serial.println("Running on ESP8266");
  #endif
  // Общий код для обеих платформ
  setupWiFi();
}
void loop() {
  // Общий код для обеих платформ
  mainLoop();
}

Пример с разными конфигурациями GPIO.

#ifdef ESP32
  #define LED_PIN 2
  #define BUTTON_PIN 4
#else
  #define LED_PIN 0
  #define BUTTON_PIN 14
#endif
void setup() {
  pinMode(LED_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
}
void loop() {
  if (digitalRead(BUTTON_PIN) == LOW) {
    digitalWrite(LED_PIN, HIGH);
    delay(1000);
    digitalWrite(LED_PIN, LOW);
  }
}

Советы по мультиплатформенности.

1. Организация кода

  • Разделение на файлы: Разделите код на несколько файлов, чтобы упростить управление и поддержку.
    • main.ino — основной файл с общей логикой
    • esp32_specific.cpp — код, специфичный для ESP32
    • esp8266_specific.cpp — код, специфичный для ESP8266
    • common.cpp — общие функции и реализации
  • Использование заголовочных файлов: Создайте заголовочные файлы для общих функций и констант.
#pragma once
#include "config.h"
// Объявляем функции, доступные везде
void setupWiFi();
void loopCommon();

2. Управление конфигурацией

  • Использование конфигурационных файлов: Создайте отдельный файл для конфигураций, который будет включаться в основной код.
#pragma once // Современная замена #ifndef/#define для защиты от двойного включения
// ИСПРАВЛЕНИЕ ОШИБКИ: Подключаем Arduino API для всех .cpp файлов
#include <Arduino.h> 
// --- Абстракция железа ---
#if defined(ESP32)
  #define BOARD_NAME "ESP32"
  #define LED_PIN    2
  #define BTN_PIN    4
  #define ANALOG_PIN 34
  // Логика светодиода (Прямая)
  #define LED_ON  HIGH
  #define LED_OFF LOW
#elif defined(ESP8266)
  #define BOARD_NAME "ESP8266"
  #define LED_PIN    LED_BUILTIN // Обычно GPIO 2
  #define BTN_PIN    0           // Кнопка FLASH
  #define ANALOG_PIN A0          // Только один АЦП
  // Логика светодиода (Инвертированная на большинстве NodeMCU/Wemos)
  #define LED_ON  LOW 
  #define LED_OFF HIGH
#else
  #error "Неподдерживаемая плата! Выберите ESP32 или ESP8266 в меню Tools."
#endif
// Общие константы
#define VERSION "1.0.0"
#define WIFI_SSID "Your_SSID"
#define WIFI_PASS "Your_Password"

3. Совместимость библиотек.

  • Использование совместимых библиотек: Убедитесь, что используемые библиотеки совместимы с обеими платформами.
    • Например, для работы с WiFi используйте WiFi.h для ESP32 и ESP8266WiFi.h для ESP8266.
#include "common.h"
// Условное подключение нужной библиотеки Wi-Fi
#ifdef ESP32
  #include <WiFi.h>
#else
  #include <ESP8266WiFi.h>
#endif
void setupWiFi() {
  Serial.printf("[%s] Connecting to %s", BOARD_NAME, WIFI_SSID);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  // Используем printf вместо String() для экономии памяти (важно для ESP8266)
  Serial.printf("\nWiFi connected! IP: %s\n", WiFi.localIP().toString().c_str());
}
void loopCommon() {
  // Мигаем светодиодом, используя наши абстракции LED_ON / LED_OFF
  digitalWrite(LED_PIN, LED_ON);
  delay(500);
  digitalWrite(LED_PIN, LED_OFF);
  delay(500);
}

4. Управление памятью.

  • Оптимизация использования памяти: ESP8266 имеет ограниченную память по сравнению с ESP32, поэтому оптимизируйте использование памяти.
#ifdef ESP8266
  #define MAX_DATA_SIZE 1024
#else
  #define MAX_DATA_SIZE 4096
#endif
char dataBuffer[MAX_DATA_SIZE];

5. Использование общих функций.

  • Создание общих функций: Выносите общую функциональность в отдельные функции, которые могут быть использованы обеими платформами.
void setup() {</p><p>  Serial.begin(115200);
  delay(1000); // Даем время на инициализацию Serial
  Serial.printf("\n--- Booting %s (v%s) ---\n", BOARD_NAME, VERSION);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LED_OFF);
  // Вызываем платформо-зависимую инициализацию
  setupSpecific();
  // Вызываем общую инициализацию
  setupWiFi();
}
void loop() {
  // Выполняем общую логику (мигание LED, поддержание связи)
  loopCommon();
}



Пример структуры проекта.

Первое правило мультиплатформенности: никогда не пишите всё в одном .ino файле. Разделяйте логику.

Создайте папку проекта (например, main) со следующей структурой:

CrossPlatformIoT/
│
├── main.ino    # Главный файл (точка входа)
├── config.h                # Абстракция железа (пины, макросы)
├── common.h                # Объявления общих функций
├── common.cpp              # Реализация общей логики (Wi-Fi, основной цикл)
├── esp32_specific.cpp      # Код, который скомпилируется ТОЛЬКО для ESP32
├── esp32_specific.h        
├── esp8266_specific.cpp    # Код, который скомпилируется ТОЛЬКО для ESP8266
└── esp8266_specific.h

Фундамент: config.h и решение проблемы Serial

Самая частая ошибка при выносе кода в .cpp файлы — error: 'Serial' was not declared in this scope. Решение: Явно подключать #include <Arduino.h> в заголовочный файл конфигурации.

Фундамент: config.h и решение проблемы Serial

Также мы сразу абстрагируем пины. Обратите внимание: на многих платах ESP8266 встроенный светодиод включается при подаче LOW, а на ESP32 — при HIGH. Мы решим это макросами LED_ON и LED_OFF.

config.h

#pragma once // Современная замена #ifndef/#define для защиты от двойного включения
// ИСПРАВЛЕНИЕ ОШИБКИ: Подключаем Arduino API для всех .cpp файлов
#include <Arduino.h> 
// --- Абстракция железа ---
#if defined(ESP32)
  #define BOARD_NAME "ESP32"
  #define LED_PIN    2
  #define BTN_PIN    4
  #define ANALOG_PIN 34
  // Логика светодиода (Прямая)
  #define LED_ON  HIGH
  #define LED_OFF LOW
#elif defined(ESP8266)
  #define BOARD_NAME "ESP8266"
  #define LED_PIN    LED_BUILTIN // Обычно GPIO 2
  #define BTN_PIN    0           // Кнопка FLASH
  #define ANALOG_PIN A0          // Только один АЦП
  // Логика светодиода (Инвертированная на большинстве NodeMCU/Wemos)
  #define LED_ON  LOW 
  #define LED_OFF HIGH
#else
  #error "Неподдерживаемая плата! Выберите ESP32 или ESP8266 в меню Tools."
#endif
// Общие константы
#define VERSION "1.0.0"
#define WIFI_SSID "Your_SSID"
#define WIFI_PASS "Your_Password"

Общий код (Общие библиотеки).

Библиотеки Wi-Fi называются по-разному, но их API (методы WiFi.begin, WiFi.status) почти идентичны.

common.h

#pragma once
#include "config.h"
// Объявляем функции, доступные везде
void setupWiFi();
void loopCommon();

common.cpp

#include "common.h"
// Условное подключение нужной библиотеки Wi-Fi
#ifdef ESP32
  #include <WiFi.h>
#else
  #include <ESP8266WiFi.h>
#endif
void setupWiFi() {
  Serial.printf("[%s] Connecting to %s", BOARD_NAME, WIFI_SSID);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  // Используем printf вместо String() для экономии памяти (важно для ESP8266)
  Serial.printf("\nWiFi connected! IP: %s\n", WiFi.localIP().toString().c_str());
}
void loopCommon() {
  // Мигаем светодиодом, используя наши абстракции LED_ON / LED_OFF
  digitalWrite(LED_PIN, LED_ON);
  delay(500);
  digitalWrite(LED_PIN, LED_OFF);
  delay(500);
}

Разделение специфичного кода.

Допустим, нам нужно читать АЦП. У ESP32 он 12-битный (0-4095), а у ESP8266 — 10-битный (0-1023). Мы можем написать специфичные функции, которые приведут данные к общему виду, и разнести их по разным файлам.

Лайфхак: Благодаря директивам #ifdef внутри самих файлов, мы можем даже не использовать #include "esp32_specific.h" в главном файле. Компилятор просто проигнорирует "чужой" файл.

esp32_specific.cpp

#include "config.h"


#ifdef ESP32 // Этот код скомпилируется только если выбрана ESP32


void setupSpecific() {
  Serial.println("Инициализация специфичных модулей ESP32 (например, Hall sensor)...");
  pinMode(BTN_PIN, INPUT_PULLUP);
}


int readAnalogSpecific() {
  int raw = analogRead(ANALOG_PIN);
  // Приводим 12-бит (4095) к 10-бит (1023) для совместимости
  return map(raw, 0, 4095, 0, 1023); 
}


#endif

esp8266_specific.cpp

#include "config.h"


#ifdef ESP8266 // Этот код скомпилируется только если выбрана ESP8266


void setupSpecific() {
  Serial.println("Инициализация специфичных модулей ESP8266...");
  pinMode(BTN_PIN, INPUT_PULLUP);
}


int readAnalogSpecific() {
  // На ESP8266 АЦП сразу выдает 10-бит (0-1023)
  return analogRead(ANALOG_PIN);
}


#endif

esp32_specific.h и esp8266_specific.h

В обоих файлах мы просто декларируем функции, чтобы main.ino их "видел".

#pragma once
void setupSpecific();
int readAnalogSpecific();

Главный файл (Точка входа).

Теперь собираем всё вместе. Код выглядит максимально чисто, а вся грязная работа с #ifdef спрятана внутри .cpp файлов и заголовков.

main.ino

#include "config.h"
#include "common.h"


// Подключаем заголовки. 
// "Лишние" функции просто не скомпилируются благодаря #ifdef внутри .cpp файлов
#include "esp32_specific.h"
#include "esp8266_specific.h"


void setup() {
  Serial.begin(115200);
  delay(1000); // Даем время на инициализацию Serial
  
  Serial.printf("\n--- Booting %s (v%s) ---\n", BOARD_NAME, VERSION);
  
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LED_OFF);
  
  // Вызываем платформо-зависимую инициализацию
  setupSpecific();
  
  // Вызываем общую инициализацию
  setupWiFi();
}


void loop() {
  // Выполняем общую логику (мигание LED, поддержание связи)
  loopCommon();
  
  // Читаем АЦП (каждая плата использует свою логику преобразования)
  int sensorValue = readAnalogSpecific();
  
  // Выводим раз в 5 секунд
  static unsigned long lastPrint = 0;
  if (millis() - lastPrint > 5000) {
    Serial.printf("Sensor value (normalized 10-bit): %d\n", sensorValue);
    lastPrint = millis();
  }
}

Фундамент: config.h и решение проблемы Serial



Лучшие практики мультиплатформенности:

1. Избегайте класса String в Serial.print.

Как видно в примере выше, вместо: Serial.println("Starting " + String(BOARD_NAME) + " project"); Используйте: Serial.printf("Starting %s project\n", BOARD_NAME); Это критически важно для ESP8266, так как динамическое выделение памяти под String быстро фрагментирует кучу и приводит к перезагрузкам (WDT Reset).

2. Работа с Flash-памятью (PROGMEM).

Если вам нужно хранить большие массивы данных (HTML-страницы, сертификаты), используйте макрос PROGMEM. На ESP8266 чтение из PROGMEM требует функции pgm_read_byte, а ESP32 читает её как обычную память. Чтобы код был универсальным, используйте PSTR() для строк или храните статические данные во LittleFS.

3. Различия в АЦП и ШИМ.

Никогда не вызывайте специфичные функции (например, ledcSetup на ESP32) напрямую в .ino файле. Всегда создавайте "обертки" (wrapper functions) в config.h или специфичных .cpp файлах, как мы сделали с readAnalogSpecific().

4. Глубокий сон (Deep Sleep).

API для сна различаются радикально. Используйте макросы:

void goToSleep(uint32_t seconds) {
  #ifdef ESP32
    esp_sleep_enable_timer_wakeup(seconds * 1000000ULL);
    esp_deep_sleep_start();
  #elif defined(ESP8266)
    ESP.deepSleep(seconds * 1000000ULL);
  #endif
}

5. Используйте #error для защиты

Если ваш код предназначен только для ESP-семейства, добавьте в config.h блок #else #error .... Если вы случайно скомпилируете проект для Arduino Uno, компилятор выдаст понятную ошибку, а не сотни строк непонятного спама об отсутствии WiFi.h.


Такой подход позволит вам в будущем легко добавить поддержку, например, ESP32-C3 или Raspberry Pi Pico W, просто дописав еще один бло

Заключение.

Написание кроссплатформенного кода требует дисциплины. Главный секрет — абстракция.

Выделите всё, что зависит от "железа" (номера пинов, библиотеки, разрядность АЦП), в заголовочный файл config.h и специфичные .cpp файлы. А в главном .ino файле пишите только бизнес-логику вашего устройства.

Такой подход позволит вам в будущем легко добавить поддержку, например, ESP32-C3 или Raspberry Pi Pico W, просто дописав еще один блок #elif defined(...) в конфигурации, не переписывая при этом основной код проекта!

Дополнительные материалы:

Понравилась статья Введение в мультиплатформенную разработку для ESP8266 и ESP32? Не забудь поделиться с друзьями в соц. сетях.

А также подписаться на наш канал на VK Видео, вступить в группу Вконтакте.

Спасибо за внимание!

Технологии начинаются с простого!

Фотографии к статье

Файлы для скачивания

Код ESP8266 + ESP32 Код ESP8266 + ESP32.zip4 Kb 23 Скачать

Комментарии

Ваше Имя*


Разработка проектов
EasyHMI
Умный Дом