آموزش ESP32آموزش اینترنت اشیا

کار با حافظه فلش ESP32 (آموزش ذخیره داده دائمی)

در این آموزش، شما یاد می‌گیرید چگونه به حافظه فلش ESP32 برای عملیات خواندن و نوشتن در Arduino IDE دسترسی پیدا کنید. این یک روش بدون استفاده از کتابخانه‌های اضافی است که می‌تواند برای برنامه‌های ساده که نیازی به سیستم فایل ندارند، مناسب باشد.

در آموزش قبلی کتابخانه EEPROM، توضیح دادیم که کتابخانه EEPROM چگونه کار می‌کند و در واقع از درایور حافظه فلش NVS استفاده می‌کند تا همان عملکردی که از یک کتابخانه EEPROM انتظار داریم را شبیه‌سازی و ارائه دهد. با این حال، این روش برای پروژه‌های جدید توصیه نمی‌شود، زیرا این کتابخانه در حال حاضر منسوخ شده و با کتابخانه جدیدی به نام Preferences جایگزین شده است.

قبل از ادامه این آموزش، باید ESP32 Arduino Core را در Arduino IDE نصب کرده باشید تا بتوانید پروژه‌ها را برای ESP32 کامپایل و اجرا کنید. اگر هنوز این کار را نکرده‌اید، از آموزش زیر برای شروع استفاده کنید:

توجه: لطفاً توجه داشته باشید که روشی که در این آموزش ارائه شده است، دسترسی کامل به کل حافظه SPI فلش ESP32 را به شما می‌دهد. اگر سعی کنید مکان حافظه‌ای خارج از پارتیشن داده NVS را بازنویسی کنید، ممکن است مشکلاتی رخ دهد. ما در این آموزش به دستورالعمل‌هایی پایبند خواهیم بود که در ادامه خواهید آموخت، اما لازم است بدانید که اگر با دقت انجام نشود، می‌تواند پرخطر باشد.

حافظه فلش ESP32

ESP32 یک حافظه فلش خارجی از نوع SPI دارد که به طور پیش‌فرض اندازه آن 4MB است. می‌توانید ماژول‌های ESP32 با حافظه 8MB یا 16MB نیز سفارش دهید. اما به طور کلی می‌گوییم 4MB است. این 4MB از فضای حافظه فلش به چند پارتیشن مختلف برای کاربردهای گوناگون تقسیم می‌شود. با این حال، می‌توانید این پارتیشن‌ها را بسته به نیاز برنامه خود اضافه، حذف یا اصلاح کنید و در این آموزش، به روشی بسیار ساده، نحوه انجام این کار را نیز نشان خواهم داد. پارتیشن‌بندی پیش‌فرض حافظه فلش ESP32 شامل موارد زیر است:

  • NVS: برای ذخیره‌سازی داده‌های کاربر
  • دو پارتیشن App: برای ذخیره برنامه کاربر + امکان انجام به‌روزرسانی OTA
  • OTAdata: برای آپدیت‌های OTA (Over-The-Air)
  • SPIFFS: سیستم فایل SPI Flash برای ذخیره و مدیریت فایل
  • CoreDump: برای اشکال‌زدایی و اهداف تشخیصی

ما به طور خاص روی پارتیشن NVS تمرکز داریم، که برای ذخیره‌سازی داده‌های کاربر در نظر گرفته شده است. این پارتیشن فضایی برابر با 0x5000 بایت (20kB) دارد و آدرس شروع آن 0x9000 است. این یعنی هر عمل نوشتن در حافظه فلش با آدرس مطلق 0x9000 وارد پارتیشن NVS خواهد شد و می‌توانید تا انتهای این پارتیشن از همان آدرس ادامه دهید.

در ادامه، جدول پارتیشن پیش‌فرض حافظه فلش ESP32 در Arduino را مشاهده می‌کنید.

کاربردهای حافظه فلش ESP32

کاربردهای حافظه فلش ESP32

حافظه‌های غیر فرّار (NVS) مانند فلش زمانی استفاده می‌شوند که لازم است مقداری داده ذخیره شود و میکروکنترلر بعد از قطع و وصل شدن برق یا ریست شدن نیز آن را یاد داشته باشد. نمونه‌های متداول داده‌هایی که باید در NVS ذخیره شوند شامل موارد زیر هستند:

  • Passwords
  • WiFi Credentials
  • User Configuration
  • App-Level Parameters
  • Sensor Last Reading
  • State Variables (برای برخی ماشین‌های حالت)
  • Sensor Calibration Data
  • و موارد بسیار دیگر…

چرخه‌های نوشتن حافظه فلش ESP32

به طور کلی، حافظه‌های فلش حدود ده برابر چرخه نوشتن/پاک‌کردن کمتری نسبت به حافظه EEPROM دارند. یک EEPROM معمولی بین 100000 تا 1000000 چرخه نوشتن/پاک‌کردن دارد، در حالی که حافظه‌های FLASH معمولاً بین 10000 تا حداکثر 100000 چرخه دارند.

بنابراین، انتظار داریم هر مکان حافظه در فلش ESP32 حدود 10000 چرخه نوشتن را تحمل کند. به بیان دیگر، اگر دائماً یک بایت داده را در یک آدرس ثابت فلش بنویسید، تنها حدود 10000 بار نوشتن طول می‌کشد تا آن مکان برای همیشه آسیب ببیند.

توجه: لطفاً هنگام استفاده از هر نوع حافظه NVS احتیاط کنید، زیرا اگر عاقلانه از آن استفاده نکنید، به‌راحتی می‌تواند برای همیشه آسیب ببیند. از نوشتن‌های غیرضروری و مکرر در NVS اجتناب کنید مگر اینکه ضروری باشد. پیاده‌سازی‌های بهتر کتابخانه‌ها معمولاً الگوریتم‌هایی برای توزیع یکنواخت استفاده از حافظه دارند تا طول عمر فلش افزایش یابد.

جدول پارتیشن سفارشی حافظه فلش ESP32 (در Arduino)

بعد از انجام چندین تست، متوجه شدم پارتیشن NVS احتمالاً پس از یک هارد ریست پاک می‌شود و داده‌ای که قبلاً نوشته شده باقی نمی‌ماند. با این حال، توانستم در زمان اجرا مقدار زیادی داده بخوانم و بنویسم و کل فضای 20kB این پارتیشن را استفاده کنم. اما پس از یک هارد ریست، همه چیز بدون دلیل واضح پاک می‌شود.

بنابراین، یک پارتیشن جدید سفارشی ایجاد می‌کنیم (نام آن را NVM، مخفف non-volatile-memory می‌گذاریم) و آن را نیز 20kB در نظر می‌گیریم. البته می‌توانید بسته به نیاز برنامه خود آن را کوچک‌تر یا بزرگ‌تر کنید.

ابتدا جدول پارتیشن پیش‌فرض حافظه فلش ESP32 در Arduino را یادآوری می‌کنیم.

کاربردهای حافظه فلش ESP32

من 0x5000 بایت (که برابر 20kB است) از پارتیشن spiffs کم می‌کنم تا پارتیشن جدید NVM را ایجاد کنم. بنابراین جدول پارتیشن جدید حافظه فلش به شکل زیر خواهد بود.

اگر در مورد این مطلب سوالی دارید در قسمت نظرات بپرسید

جدول پارتیشن سفارشی حافظه فلش ESP32

همان‌طور که می‌بینید، یک پارتیشن جدید با نام NVM و اندازه 0x5000 بایت (20kB) ایجاد کرده‌ام که از آدرس 0x290000 شروع می‌شود. جدول بالا یک فایل .csv با نام مشخص است. این فایل باید دقیقاً partitions.csv نام داشته باشد و در همان پوشه‌ای قرار گیرد که فایل Arduino sketch پروژه شما قرار دارد تا در زمان کامپایل به طور خودکار تشخیص داده شود.

به این ترتیب می‌توانید به‌سادگی جدول پارتیشن پیش‌فرض حافظه فلش ESP32 در Arduino IDE را override کنید و پارتیشن‌های سفارشی ایجاد کرده یا اندازه هر پارتیشن را تنظیم کنید.

توجه: اگر قصد ایجاد پارتیشن جدید یا تغییر اندازه پارتیشن‌های پیش‌فرض را دارید، مراقب آدرس‌های offset و فضای کل حافظه باشید. این فضا نباید از 4MB (یا هر میزان حافظه فلش برد ESP32 شما) بیشتر شود. همچنین توجه کنید که (offset هر پارتیشن = offset پارتیشن قبلی + اندازه پارتیشن قبلی).
توجه: پارتیشن app0 جایی است که کد شما در نهایت در حافظه فلش قرار می‌گیرد و باید دقیقاً از مکان 64kB شروع شود. نوشتن در این پارتیشن باعث خراب شدن کدی می‌شود که در حال اجراست و به خطاهای زمان اجرا منتهی می‌شود و میکروکنترلر مرتباً ریست خواهد شد. نیازی به تست این موضوع نیست، من قبلاً این تست را انجام داده‌ام!

توابع خواندن و نوشتن حافظه فلش ESP32

در کتابخانه داخلی ESP.h چندین تابع وجود دارد که دسترسی مستقیم به کل حافظه SPI فلش را برای انجام عملیات خواندن و نوشتن در هر آدرس از فضای حافظه فراهم می‌کنند. این یعنی مسئولیت بررسی اعتبار آدرس نوشته‌شده بر عهده ما است.

برای استفاده از این دو API (توابع)، ابتدا باید فایل هدر <Arduino.h> را شامل کنید. همان‌طور که قبلاً گفته شد، هیچ کتابخانه اضافی برای این روش نیاز نیست.

// شامل کردن Arduino.h (برای دسترسی به ESP.h)
#include <Arduino.h>

نوشتن در حافظه فلش ESP32

این تابع API نوشتن در حافظه فلش ESP32 است:

bool flashWrite(uint32_t offset, uint32_t *data, size_t size);

این تابع سه پارامتر می‌گیرد:

  • offset: فاصله از ابتدای حافظه فلش که در واقع آدرس موردنظر برای نوشتن است
  • data: اشاره‌گری به داده‌ای که باید نوشته شود
  • size: اندازه داده (تعداد بایت‌ها)

توجه داشته باشید که باید خودمان بررسی آدرس و offset را انجام دهیم و همچنین تبدیل نوع داده به و از uint32_t که ورودی این تابع است را مدیریت کنیم. همچنین باید تعداد بایت‌های نوشته‌شده به حافظه را پیگیری کنیم تا آدرس داده بعدی مشخص شود.

مثال: نوشتن یک متغیر uint32_t به نام myVar در آدرس صفر حافظه فلش:

const uint32_t NVM_Offset = 0x290000;  // مقدار offset برای پارتیشن NVS
uint32_t address = 0; // آدرس صفر
uint32_t myVar = 100; // متغیر برای ذخیره

// نوشتن در فلش
ESP.flashWrite(NVM_Offset + address, &myVar, sizeof(myVar));

توجه کنید که باید مقدار offset مربوط به ابتدای پارتیشن NVS در حافظه فلش را اضافه کنیم. به این شکل بدون استفاده از هیچ کتابخانه اضافی، متغیر را در حافظه فلش ESP32 می‌نویسیم.

خواندن از حافظه فلش ESP32

به همین صورت، برای خواندن یک مکان حافظه از حافظه فلش از تابع زیر استفاده می‌کنیم. توجه داشته باشید که باید اندازه دقیق عنصر داده‌ای که می‌خواهیم بازیابی کنیم، مشخص باشد:

bool flashRead(uint32_t offset, uint32_t *data, size_t size);

این روش به سادگی امکان خواندن و نوشتن در حافظه فلش ESP32 را بدون کتابخانه اضافی و با دسترسی کامل به کل فضای حافظه فراهم می‌کند.

توابع عمومی برای خواندن و نوشتن فلش

این دو تابع wrapper هستند که امکان خواندن و نوشتن هر نوع داده در حافظه فلش ESP32 را بدون نگرانی از نوع داده، اندازه یا هر چیز دیگری فراهم می‌کنند:

// شامل کردن Arduino.h (برای دسترسی به ESP.h)
#include <Arduino.h>

const uint32_t NVM_Offset = 0x290000;

template<typename T>
void FlashWrite(uint32_t address, const T& value) {
  ESP.flashEraseSector((NVM_Offset + address) / 4096);
  ESP.flashWrite(NVM_Offset + address, (uint32_t*)&value, sizeof(value));
}

template<typename T>
void FlashRead(uint32_t address, T& value) {
  ESP.flashRead(NVM_Offset + address, (uint32_t*)&value, sizeof(value));
}
توجه: یک عملیات FlashWrite موفق با استفاده از این API باید پیش از نوشتن داده جدید، با پاک‌سازی کل سکتور انجام شود. پیاده‌سازی بهتر می‌تواند پیروی از توالی (خواندن-اصلاح-پاک‌سازی-نوشتن) برای هر سکتور کامل هر بار که نیاز به نوشتن داده است، باشد.

مثال ذخیره رشته در حافظه FLASH برد ESP32

در این مثال، حافظه فلش را با ذخیره و خواندن یک رشته در تابع setup() آزمایش می‌کنیم.

کد مثال

// شامل کردن Arduino.h (برای دسترسی به ESP.h)
#include <Arduino.h>

const uint32_t NVM_Offset = 0x290000;
uint8_t FLASH_Address = 0;

String StrIn = "Hello From The FLASH!";
String StrOut;

template<typename T>
void FlashWrite(uint32_t address, const T& value) {
  ESP.flashEraseSector((NVM_Offset+address)/4096);
  ESP.flashWrite(NVM_Offset+address, (uint32_t*)&value, sizeof(value));
}

template<typename T>
void FlashRead(uint32_t address, T& value) {
  ESP.flashRead(NVM_Offset+address, (uint32_t*)&value, sizeof(value));
}

void setup() {
  Serial.begin(115200);
  FlashWrite<String>(FLASH_Address, StrIn);
  FlashRead<String>(FLASH_Address, StrOut);
  Serial.println(StrOut);
}

void loop() {
  delay(200);
}

فایل partitions.csv

فراموش نکنید که فایل سفارشی partitions.csv باید در همان پوشه‌ای قرار گیرد که sketch قرار دارد تا دسترسی به پارتیشن NVM به درستی انجام شود.

فایل partitions.csv

این کد یک عملیات نوشتن ساده روی حافظه فلش انجام می‌دهد که رشته StrIn را ارسال می‌کند و سپس همان آدرس را خوانده و در متغیر جدید StrOut ذخیره می‌کند و آن را چاپ می‌کند تا صحت خواندن از حافظه بررسی شود.

در مانیتور سریال، پس از هر ریست سخت، رشته به درستی از حافظه فلش خوانده می‌شود.

مثال ذخیره رشته در حافظه FLASH برد ESP32

ذخیره داده Float در حافظه فلش ESP32

در این مثال، حافظه فلش را با ذخیره و خواندن یک متغیر Float در تابع setup() آزمایش می‌کنیم.

// شامل کردن Arduino.h (برای دسترسی به ESP.h)
#include <Arduino.h>

const uint32_t NVM_Offset = 0x290000;
uint8_t FLASH_Address = 0;

float Pi = 3.14159;
float Read_Pi;

template<typename T>
void FlashWrite(uint32_t address, const T& value) {
  ESP.flashEraseSector((NVM_Offset+address)/4096);
  ESP.flashWrite(NVM_Offset+address, (uint32_t*)&value, sizeof(value));
}

template<typename T>
void FlashRead(uint32_t address, T& value) {
  ESP.flashRead(NVM_Offset+address, (uint32_t*)&value, sizeof(value));
}

void setup() {
  Serial.begin(115200);
  FlashWrite<float>(FLASH_Address, Pi);
  FlashRead<float>(FLASH_Address, Read_Pi);
  Serial.println(Read_Pi, 5);
}

void loop() {
  delay(100);
}

فایل partitions.csv

فایل سفارشی partitions.csv باید در همان پوشه sketch قرار گیرد تا پارتیشن NVM به درستی شناسایی شود.

این کد متغیر Float به نام Pi را در حافظه فلش می‌نویسد، سپس همان آدرس را خوانده و در متغیر جدید Read_Pi ذخیره می‌کند و آن را چاپ می‌کند تا صحت خواندن بررسی شود.

در مانیتور سریال، پس از هر ریست سخت، مقدار Float به درستی از حافظه فلش خوانده می‌شود.

ذخیره داده Float در حافظه فلش ESP32

مثال استفاده حافظه فلش ESP32 آردوینو

در این مثال، حافظه فلش را با ذخیره آخرین وضعیت LED آزمایش می‌کنیم. LED با یک دکمه فشاری تغییر وضعیت می‌دهد و هر بار که وضعیت آن تغییر کند، آخرین وضعیت در حافظه فلش ذخیره می‌شود. سپس برد ESP32 را ریست می‌کنیم و باید وضعیت آخر ذخیره شده را از حافظه فلش در هنگام راه‌اندازی بازیابی کند.

در این مثال، سیم‌کشی شامل یک ورودی دکمه فشاری و یک خروجی LED است.

مثال استفاده حافظه فلش ESP32 آردوینو

در ادامه، کد کامل این مثال ارائه شده است:

// شامل کردن Arduino.h (برای دسترسی به ESP.h)
#include <Arduino.h>

const uint32_t NVM_Offset = 0x290000;
uint8_t FLASH_Address = 0;

// تعریف پین خروجی LED و پین ورودی دکمه
#define LED_GPIO 25
#define BTN_GPIO 2

// متغیرهای سراسری برای خواندن دکمه و Debounce
int ledState = LOW;
int btnState = LOW;
int lastBtnState = LOW;
int lastDebounceTime = 0;
int debounceDelay = 50;

template<typename T>
void FlashWrite(uint32_t address, const T& value) {
  ESP.flashEraseSector((NVM_Offset+address)/4096);
  ESP.flashWrite(NVM_Offset+address, (uint32_t*)&value, sizeof(value));
}

template<typename T>
void FlashRead(uint32_t address, T& value) {
  ESP.flashRead(NVM_Offset+address, (uint32_t*)&value, sizeof(value));
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_GPIO, OUTPUT);
  pinMode(BTN_GPIO, INPUT);
  FlashRead<int>(FLASH_Address, ledState);
  Serial.println(ledState);
  digitalWrite(LED_GPIO, ledState);
}

void loop() {
  int reading = digitalRead(BTN_GPIO);
  if (reading != lastBtnState) {
    lastDebounceTime = millis();
  }
  if ((millis() - lastDebounceTime) > debounceDelay) {
    if (reading != btnState) {
      btnState = reading;
      if (btnState == HIGH) {
        ledState = !ledState;
        digitalWrite(LED_GPIO, ledState);
        FlashWrite<int>(FLASH_Address, ledState);
        Serial.println("State Changed & Saved To FLASH!");
      }
    }
  }
  lastBtnState = reading;
}

فایل partitions.csv

فراموش نکنید که فایل سفارشی partitions.csv باید در همان پوشه‌ای قرار گیرد که sketch قرار دارد تا دسترسی به پارتیشن NVM به درستی انجام شود.

این مثال به سادگی ورودی دکمه را می‌خواند و با استفاده از منطق Debounce، نویزهای احتمالی را فیلتر می‌کند. در صورتی که دکمه واقعاً فشرده شده باشد، LED تغییر وضعیت می‌دهد. همچنین رویداد تغییر وضعیت LED، داده‌ی ledState فعلی را در مکان FLASH_Address حافظه فلش ذخیره می‌کند.

  • setup(): در تابع setup() ارتباط سریال برای دیباگینگ و حالت پین‌ها تنظیم می‌شود. سپس آخرین وضعیت ذخیره شده LED از حافظه فلش خوانده شده و به خروجی LED اعمال می‌شود.
  • loop(): در تابع loop() بیشتر عملیات خواندن دکمه و Debounce انجام می‌شود. وقتی دکمه روشن (HIGH) و بدون نویز باشد، وضعیت LED تغییر کرده و وضعیت فعلی LED در حافظه فلش ذخیره می‌شود.

در این مثال، ESP32 پس از هر ریست، آخرین وضعیت LED را به خاطر می‌آورد. این ساده‌ترین کاربرد برای استفاده از حافظه‌های غیر فرّار است.

5 (1 نفر)

برای دریافت مطالب جدید کانال تلگرام یا پیج اینستاگرام ما را دنبال کنید.

محمد رحیمی

محمد رحیمی هستم. سعی میکنم در آیرنکس مطالب مفید قرار بدهم. سوالات مربوط به این مطلب را در قسمت نظرات همین مطلب اعلام کنید. سعی میکنم در اسرع وقت به نظرات شما پاسخ بدهم.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *