ESP8266开发之旅 基础篇④ ESP8266与EEPROM
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力。希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石。。。
QQ技术互动交流群:ESP8266&32 物联网开发 群号622368884,不喜勿喷
一、你如果想学基于Arduino的ESP8266开发技术
一、基础篇
二、网络篇
- ESP8266开发之旅 网络篇① 认识一下Arduino Core For ESP8266
- ESP8266开发之旅 网络篇② ESP8266 工作模式与ESP8266WiFi库
- ESP8266开发之旅 网络篇③ Soft-AP——ESP8266WiFiAP库的使用
- ESP8266开发之旅 网络篇④ Station——ESP8266WiFiSTA库的使用
- ESP8266开发之旅 网络篇⑤ Scan WiFi——ESP8266WiFiScan库的使用
- ESP8266开发之旅 网络篇⑥ ESP8266WiFiGeneric——基础库
- ESP8266开发之旅 网络篇⑦ TCP Server & TCP Client
- ESP8266开发之旅 网络篇⑧ SmartConfig——一键配网
- ESP8266开发之旅 网络篇⑨ HttpClient——ESP8266HTTPClient库的使用
- ESP8266开发之旅 网络篇⑩ UDP服务
- ESP8266开发之旅 网络篇⑪ WebServer——ESP8266WebServer库的使用
- ESP8266开发之旅 网络篇⑫ 域名服务——ESP8266mDNS库
- ESP8266开发之旅 网络篇⑬ SPIFFS——ESP8266 Flash文件系统
- ESP8266开发之旅 网络篇⑭ web配网
- ESP8266开发之旅 网络篇⑮ 真正的域名服务——DNSServer
- ESP8266开发之旅 网络篇⑯ 无线更新——OTA固件更新
三、应用篇
四、高级篇
EEPROM(Electrically Erasable Programmable Read-Only Memory),电可擦可编程只读存储器——一种掉电后数据不丢失的存储芯片。
EEPROM可以在不使用文件和文件系统的情况下用来固化一些数据,常见的比如用来保存SSID或者Password,保存用户设置等数据,这样就可以不用每次都通过烧写程序来改变系统运行时的初始值。
Arduino提供了完善的eeprom库,不过需要注意的是ESP8266没有硬件EEPROM,使用的是flash模拟的EEPROM。
1. 原理
EEPROM库在Arduino中经常用于存储设定数据。当然基于Arduino的ESP8266也不例外。但是,和真正的Arduino板子不一样的是,ESP8266采用的方式是将flash中某一块4K的存储模拟成EEPROM。至于为什么是4K呢?主要原因是flash是以sector为一个单位,1 sector等于4096Bytes(4KB),操作flash时是以sector为一个整体来操作。
读取操作是通过ESP8266 SDK提供的API将flash中的内容读取到Buffer中是没有限制一次就要将4K全读完,Buffer的大小由EEPROM.begin(size)决定,但是由于Buffer大小会占用内存RAM,所以务必按照实际需要来定义大小。
写入操作是通过commit将flash eeprom地址的4K 存储内容删除后才将Buffer写入flash中(也就是说就算你buffer只有4个字节,但是最终还是会刷新整个sector),原理大致如下图:

所以要确保内容被保存到flash中,需要考虑commit的时机。
2. 官方介绍
下图来源于Arduino For ESP8266对于EEPROM的介绍:

具体意思可以理解为以下几点:
- ESP8266 EEPROM的操作其实和Arduino EEPROM的操作核心思想很像,但是又有所不同。
- 和标准的EEPROM库不一样的是,你需要在读或者写操作之前先通过 EEPROM.begin(size) 来声明你需要操作的存储大小,size取值范围为4~4096字节。
- EEPROM.write() 不会立刻把内容写进flash,如果你希望保持到flash去,那么你必须调用 EEPROM.commit()。当然, EEPROM.end() 不仅也能完成commit,同时会释放申请的eeprom ram资源。
- EEPROM库跟在SPIFFS文件系统的后面(那么读者就得考虑不同的SPIFFS大小对应的地址是不一样)。
3. 库介绍
EEPROM库非常简单,请看博主总结的百度脑图:

仅仅有5个方法,但是博主在这里还是带读者深入去理解一下它们。
Arduino Core For ESP8266的源码在github上可以查找到,读者可以把它下载下来以便后续深入开发,链接位置为 ESP8266源码。
然后请找到下图位置:

3.1 begin()
该功能用于申请具体大小的ram内存空间。
函数: begin(size)
参数:
size:要申请的内存大小。
返回值: 无;
注意点:
- 入参size必须大于0。
void EEPROMClass::begin(size_t size) {
//size 必须大于0
if (size <= 0)
return;
if (size > SPI_FLASH_SEC_SIZE)
//超过4096的size,都强制变成4096
size = SPI_FLASH_SEC_SIZE;
//size最终的大小都是4个倍数,比如输入1,最终size是4
size = (size + 3) & (~3);
//In case begin() is called a 2nd+ time, don't reallocate if size is the same
if(_data && size != _size) {
delete[] _data;
_data = new uint8_t[size];
} else if(!_data) {
//创建内存buffer空间 这里需要注意
_data = new uint8_t[size];
}
_size = size;
noInterrupts();
//把具体内容读取出来
spi_flash_read(_sector * SPI_FLASH_SEC_SIZE, reinterpret_cast<uint32_t*>(_data), _size);
interrupts();
_dirty = false; //make sure dirty is cleared in case begin() is called 2nd+ time
}
- 从begin源码解析可以看出,虽然我们可以输入自定义size,但是最终会经过计算得到真正的size(4的倍数),并申请对应的内存空间,这也验证了博主上面说的flash模拟EEPROM。
- 所以 begin(1) 等同于 begin(4)。
3.2 write()
该功能用于往内存空间去写入数据。
函数: write(address,value)
参数:
address:要写入的地址位置,取值范围为内存空间的地址0~size。
val:写入的数据。
返回值: 无;
注意点:
void EEPROMClass::write(int const address, uint8_t const value) {
if (address < 0 || (size_t)address >= _size)
return;
if(!_data)
return;
// Optimise _dirty. Only flagged if data written is different.
uint8_t* pData = &_data[address];
if (*pData != value)
{
*pData = value;
_dirty = true;
}
}
从源码可以看出,写入的数据只是写入到申请的内存空间,并不是立刻写入到flash中。
3.3 read()
该功能用于读取数据操作。
函数: read(address)
参数:
address:要读取的地址位置,取值范围为内存空间的地址0~size。
返回值: 返回存储数据;
注意点:
uint8_t EEPROMClass::read(int const address) {
if (address < 0 || (size_t)address >= _size)
return 0;
if(!_data)
return 0;
//读取内存数据
return _data[address];
}
从源码看出,读取的数据也是从begin中生成的内存空间中去获取,并不会直接操作flash。操作内存的一个好处就是快。
3.4 commit()
该功能用于把内存空间的数据覆盖到flash eeprom块去。
函数: commit()
参数: 无;
返回值: 返回bool值,表示是否覆盖成功;
注意点:
bool EEPROMClass::commit() {
bool ret = false;
if (!_size)
return false;
if(!_dirty)
return true;
if(!_data)
return false;
noInterrupts();
//是否擦除eeprom sector成功
if(spi_flash_erase_sector(_sector) == SPI_FLASH_RESULT_OK) {
//把内存空间数据写入到eeprom sector
if(spi_flash_write(_sector * SPI_FLASH_SEC_SIZE, reinterpret_cast<uint32_t*>(_data), _size) == SPI_FLASH_RESULT_OK) {
_dirty = false;
ret = true;
}
}
interrupts();
return ret;
}
- 从源码看,这个方法才是真正的把数据从内存控件写回到flash空间;
- 而且,写回flash之前会把整一块sector全部擦除掉,也就意味着就算我们begin(1)最终也是会擦除4096字节空间。但是size的大小决定了内存空间的剩余量以及回写的快慢,所以根据具体情况来设置size。
3.5 end()
该功能用于写入flash,并且释放内存空间。
函数: end()
参数: 无;
返回值: 无;
注意点:
void EEPROMClass::end() {
if (!_size)
return;
//写入flash
commit();
if(_data) {
//回收内存空间
delete[] _data;
}
_data = 0;
_size = 0;
_dirty = false;
}
- 从源码看,end包含了写入flash,并且回收内存空间。
- 建议读者操作完EEPROM之后,必须调用这个方法,回收内存空间很重要。
4. EEPROM位置
4.1 EEPROM官方定义
前面,我们说到,ESP8266采用的方式是将flash中某一块4K的存储模拟成EEPROM。那么它到底在哪一个位置呢?请看看源码:
EEPROMClass::EEPROMClass(uint32_t sector)
: _sector(sector)
, _data(0)
, _size(0)
, _dirty(false)
{
}
EEPROMClass::EEPROMClass(void)
: _sector((((uint32_t)&_SPIFFS_end - 0x40200000) / SPI_FLASH_SEC_SIZE))
, _data(0)
, _size(0)
, _dirty(false)
{
}
- _sector代表的是具体哪一块4K sector。
- 重点代码在 **(uint32_t)&_SPIFFS_end - 0x40200000) / SPI_FLASH_SEC_SIZE)** ,0x40200000代表的是flash的0x00000,SPIFFS_end定义为你设置Arduino IDE的SPIFFS的大小后,再从设置中取得已设定好的值:

对于_SPIFFS_end的值具体可以参考 地址定义,这里选择其中一个 eagle.flash.4m3m.ld 来讲解怎么计算(这个为4M(3M SPIFSS)):
/* Flash Split for 4M chips */
/* sketch @0x40200000 (~1019KB) (1044464B) */
/* empty @0x402FEFF0 (~4KB) (4112B) */
/* spiffs @0x40300000 (~3052KB) (3125248B) */
/* eeprom @0x405FB000 (4KB) */
/* rfcal @0x405FC000 (4KB) */
/* wifi @0x405FD000 (12KB) */
MEMORY
{
dport0_0_seg : org = 0x3FF00000, len = 0x10
dram0_0_seg : org = 0x3FFE8000, len = 0x14000
iram1_0_seg : org = 0x40100000, len = 0x8000
irom0_0_seg : org = 0x40201010, len = 0xfeff0
}
PROVIDE ( _SPIFFS_start = 0x40300000 );
PROVIDE ( _SPIFFS_end = 0x405FB000 );
PROVIDE ( _SPIFFS_page = 0x100 );
PROVIDE ( _SPIFFS_block = 0x2000 );
INCLUDE "local.eagle.app.v6.common.ld"
代入上面的公式变成:
EEPROMClass EEPROM(((0x405FB000 - 0x40200000) / SPI_FLASH_SEC_SIZE));
其中 SPI_FLASH_SEC_SIZE定位为 4096(4K),具体定义可参考 spi_flash.h。
所以最终得到的结果是:
EEPROMClass EEPROM(1019);
4.2 EEPROM自定义
从上一节的计算,我们可以知道,根据计算公式,我们会最终得到一个具体位置的sector来描述eeprom。那么,反过来思考一下,既然官方的eeprom对应的sector地址是SPIFFS_END的下一个sector,那么在官方eeprom存储不够用的前提下,我们是否可以自己定义一个sector来继续存储更多的内容?如果可以,那么这个sector该取哪一部分呢?
很显然,如果我们没有用到SPIFFS,完全可以利用这一块区域去做我们自定义的EEPROM。这里我们选择SPIFFS的最后一个sector(为什么我们会选择它?留给读者思考)。
按照公式倒推回去:
EEPROMClass EEPROM(1019 - 1);
EEPROMClass EEPROM(((0x405FB000 - 0x40200000) / SPI_FLASH_SEC_SIZE) - 1);
最终得到我们需要的自定义公式:
EEPROMClass EEPROM((((uint32_t)&_SPIFFS_end - 0x40200000) / SPI_FLASH_SEC_SIZE) - 1);
注意点:
- 我们定义了SPIFFS最后一个sector的整个4Kbytes作为自定义EEPROM,如果使用到了SPIFFS,需要考虑是否会覆盖它。
5. 实例讲解
5.1 写数据
/*
* 功能描述:该代码向EEPROM写入100字节数据
*/
#include <EEPROM.h>
int addr = 0; //EEPROM数据地址
void setup()
{
Serial.begin(9600);
Serial.println("");
Serial.println("Start write");
EEPROM.begin(100);
for(addr = 0; addr<100; addr++)
{
int data = addr;
EEPROM.write(addr, addr); //写数据
}
EEPROM.end(); //保存更改的数据
Serial.println("End write");
}
void loop()
{
}
5.2 读数据
/*
* 功能描述:该代码从EEPROM读取100字节数据
*/
#include <EEPROM.h>
int addr = 0;
void setup()
{
Serial.begin(9600);
Serial.println("");
Serial.println("Start read");
EEPROM.begin(100);
for(addr = 0; addr<100; addr++)
{
int data = EEPROM.read(addr); //读数据
Serial.print(data);
Serial.print(" ");
delay(2);
}
//释放内存
EEPROM.end();
Serial.println("End read");
}
void loop()
{
}
5.3 清除数据
/*
EEPROM Clear
Sets all of the bytes of the EEPROM to 0.
This example code is in the public domain.
*/
#include <EEPROM.h>
void setup() {
EEPROM.begin(100);
// write a 0 to all 512 bytes of the EEPROM
for (int i = 0; i < 100; i++) {
EEPROM.write(i, 0);
}
//释放内存
EEPROM.end();
}
void loop() {
}
5.4 结构体操作
在没有应用结构体之前,不管是写入还是读取操作,我们都需要记住具体的存储位置。特别是当配置数据越来越多的时候或者别人维护的时候,非常容易出错。那么有没有办法优雅地解决这种问题呢?当然有,那就是结构体的妙用,我们不需要关注具体的位置,只需要关注数据本身。看以下代码:
/*
* 功能描述:eeprom结构体操作
*/
#include <EEPROM.h>
#define DEFAULT_STASSID "danpianjicainiao"
#define DEFAULT_STAPSW "boge"
struct config_type
{
char stassid[32];
char stapsw[64];
};
config_type config;
/*
* 保存参数到EEPROM
*/
void saveConfig()
{
Serial.println("Save config!");
Serial.print("stassid:");
Serial.println(config.stassid);
Serial.print("stapsw:");
Serial.println(config.stapsw);
EEPROM.begin(1024);
uint8_t *p = (uint8_t*)(&config);
for (int i = 0; i < sizeof(config); i++)
{
EEPROM.write(i, *(p + i));
}
EEPROM.commit();
}
/*
* 从EEPROM加载参数
*/
void loadConfig()
{
EEPROM.begin(1024);
uint8_t *p = (uint8_t*)(&config);
for (int i = 0; i < sizeof(config); i++)
{
*(p + i) = EEPROM.read(i);
}
EEPROM.commit();
Serial.println("-----Read config-----");
Serial.print("stassid:");
Serial.println(config.stassid);
Serial.print("stapsw:");
Serial.println(config.stapsw);
}
/*
*初始化
*/
void setup() {
ESP.wdtEnable(5000);
strcpy(config.stassid, DEFAULT_STASSID);
strcpy(config.stapsw, DEFAULT_STAPSW);
saveConfig();
}
/*
*主循环
*/
void loop() {
ESP.wdtFeed();
loadConfig();
}
结构体与EEPROM的结合使用,使我们脱离了存储位置的限制,就算后期需要加多一个配置,我们只需要在结构体上加上相应的字段,完全不用改动其他代码。
6. 总结
这一章,讲解了ESP8266 EEPROM的底层设计原理,讲述了内存和flash之间的关系,也讲解了方法使用,虽然简单,但是对于底层的认知,会让我们优化代码性能更加便捷。
ESP8266开发之旅 基础篇④ ESP8266与EEPROM的更多相关文章
- ESP8266开发之旅 基础篇③ ESP8266与Arduino的开发说明
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...
- ESP8266开发之旅 基础篇⑤ ESP8266 SPI通信和I2C通信
设备与设备之间的通信往往都伴随着总线的使用,而用得比较多的就当属于SPI总线和I2C总线,而恰巧NodeMcu也支持这两种总线通信,所以本章的主要内容就是讲解ESP8266 SPI和I2C总线 ...
- ESP8266开发之旅 基础篇① 走进ESP8266的世界
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...
- ESP8266开发之旅 基础篇② 如何安装ESP8266的Arduino开发环境
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...
- ESP8266开发之旅 基础篇⑥ Ticker——ESP8266定时库
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...
- ESP8266开发之旅 网络篇② ESP8266 工作模式与ESP8266WiFi库
在网络篇①中,博主主要讲解了Arduino上开发ESP8266的插件库 Arduino Core For ESP8266.但是,并没有讲到关于这个模块的工作模式,所以本篇讲着重讲解ESP826 ...
- ESP8266开发之旅 网络篇⑯ 无线更新——OTA固件更新
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...
- ESP8266开发之旅 进阶篇② 闲聊Arduino IDE For ESP8266烧录配置
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...
- ESP8266开发之旅 网络篇⑦ TCP Server & TCP Client
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...
随机推荐
- 06.Django基础五之django模型层(二)多表操作
一 创建模型 表和表之间的关系 一对一.多对一.多对多 ,用book表和publish表自己来想想关系,想想里面的操作,加外键约束和不加外键约束的区别,一对一的外键约束是在一对多的约束上加上唯一约束. ...
- Beescms_v4.0 sql注入漏洞分析
Beescms_v4.0 sql注入漏洞分析 一.漏洞描述 Beescms v4.0由于后台登录验证码设计缺陷以及代码防护缺陷导致存在bypass全局防护的SQL注入. 二.漏洞环境搭建 1.官方下载 ...
- JavaScript之深入对象(二)
上一篇随笔讲解了构造函数.原型及原型链相关的知识,今天让我们一起来探讨另一个问题:this. 一 this 的指向 1, 函数预编译过程中,this指向window 我们在讲解函数预编译过程 ...
- 在vscode中配置python环境
1.安装vscode和python3.7(安装路径在:E:\Python\Python37): 2.打开vscode,在左下角点击设置图标选择setting,搜索python path,在该路径下选择 ...
- Spring Boot (六): 为 JPA 插上翅膀的 QueryDSL
在前面的文章中,我们介绍了 JPA 的基础使用方式,<Spring Boot (三): ORM 框架 JPA 与连接池 Hikari>,本篇文章,我们由入门至进阶的介绍一下为 JPA 插上 ...
- Kubernetes 系列(五):Prometheus监控框架简介
由于容器化和微服务的大力发展,Kubernetes基本已经统一了容器管理方案,当我们使用Kubernetes来进行容器化管理的时候,全面监控Kubernetes也就成了我们第一个需要探索的问题.我们需 ...
- 为什么要学习go语言
终于等到你!Go语言--让你用写Python代码的开发效率编写C语言代码. 为什么互联网世界需要Go语言 世界上已经有太多太多的编程语言了,为什么又出来一个Go语言? 硬件限制:摩尔定律已然失效 摩尔 ...
- 【产品】PM常用的流程图
一.流程图分类 UML有很多种,大体可以分类两类:行为型的图和结构型的图.平时工作中的流程图,只要能把事情清晰的表明,用何种流程图表现形式,其实都无所谓. 但是,作为一名产品经理,共有哪些种类的流程图 ...
- Scala 多继承顺序
Trait多继承顺序: 准则: 如果有超类,则先调用超类的函数. 如果混入的trait有父trait,它会按照继承层次先调用父trait的构造函数. 如果有多个父trait,则按顺序从左到右执行. 所 ...
- 爬虫那点事,干就玩了之seleunim
目录 selenium 环境准备 代码环境 开始爬虫 操作js 截图 切换窗口 在当前窗口切换访问地址 管理cookie # 加入战队 微信公众号 # 加入战队 微信公众号 做技术我们最重要的是[做] ...