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

در این آموزش، شما یاد میگیرید چگونه به حافظه فلش ESP32 برای عملیات خواندن و نوشتن در Arduino IDE دسترسی پیدا کنید. این یک روش بدون استفاده از کتابخانههای اضافی است که میتواند برای برنامههای ساده که نیازی به سیستم فایل ندارند، مناسب باشد.
در آموزش قبلی کتابخانه EEPROM، توضیح دادیم که کتابخانه EEPROM چگونه کار میکند و در واقع از درایور حافظه فلش NVS استفاده میکند تا همان عملکردی که از یک کتابخانه EEPROM انتظار داریم را شبیهسازی و ارائه دهد. با این حال، این روش برای پروژههای جدید توصیه نمیشود، زیرا این کتابخانه در حال حاضر منسوخ شده و با کتابخانه جدیدی به نام Preferences جایگزین شده است.
قبل از ادامه این آموزش، باید ESP32 Arduino Core را در Arduino IDE نصب کرده باشید تا بتوانید پروژهها را برای ESP32 کامپایل و اجرا کنید. اگر هنوز این کار را نکردهاید، از آموزش زیر برای شروع استفاده کنید:
حافظه فلش 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
حافظههای غیر فرّار (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 بار نوشتن طول میکشد تا آن مکان برای همیشه آسیب ببیند.
جدول پارتیشن سفارشی حافظه فلش ESP32 (در Arduino)
بعد از انجام چندین تست، متوجه شدم پارتیشن NVS احتمالاً پس از یک هارد ریست پاک میشود و دادهای که قبلاً نوشته شده باقی نمیماند. با این حال، توانستم در زمان اجرا مقدار زیادی داده بخوانم و بنویسم و کل فضای 20kB این پارتیشن را استفاده کنم. اما پس از یک هارد ریست، همه چیز بدون دلیل واضح پاک میشود.
بنابراین، یک پارتیشن جدید سفارشی ایجاد میکنیم (نام آن را NVM، مخفف non-volatile-memory میگذاریم) و آن را نیز 20kB در نظر میگیریم. البته میتوانید بسته به نیاز برنامه خود آن را کوچکتر یا بزرگتر کنید.
ابتدا جدول پارتیشن پیشفرض حافظه فلش ESP32 در Arduino را یادآوری میکنیم.

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

همانطور که میبینید، یک پارتیشن جدید با نام NVM و اندازه 0x5000 بایت (20kB) ایجاد کردهام که از آدرس 0x290000 شروع میشود. جدول بالا یک فایل .csv با نام مشخص است. این فایل باید دقیقاً partitions.csv نام داشته باشد و در همان پوشهای قرار گیرد که فایل Arduino sketch پروژه شما قرار دارد تا در زمان کامپایل به طور خودکار تشخیص داده شود.
به این ترتیب میتوانید بهسادگی جدول پارتیشن پیشفرض حافظه فلش ESP32 در Arduino IDE را override کنید و پارتیشنهای سفارشی ایجاد کرده یا اندازه هر پارتیشن را تنظیم کنید.
توابع خواندن و نوشتن حافظه فلش 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));
}
مثال ذخیره رشته در حافظه 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 به درستی انجام شود.

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

ذخیره داده 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 به درستی از حافظه فلش خوانده میشود.

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

در ادامه، کد کامل این مثال ارائه شده است:
// شامل کردن 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 را به خاطر میآورد. این سادهترین کاربرد برای استفاده از حافظههای غیر فرّار است.








