ESP8266开发之旅 基础篇④ ESP8266与EEPROM

授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力。但愿你们分享给你周边须要的朋友或者同窗,说不定大神成长之路有博哥的奠定石。。。git

QQ技术互动交流群:ESP8266&32 物联网开发 群号622368884,不喜勿喷github

1、你若是想学基于Arduino的ESP8266开发技术

1、基础篇web

  1. ESP8266开发之旅 基础篇① 走进ESP8266的世界
  2. ESP8266开发之旅 基础篇② 如何安装ESP8266的Arduino开发环境
  3. ESP8266开发之旅 基础篇③ ESP8266与Arduino的开发说明
  4. ESP8266开发之旅 基础篇④ ESP8266与EEPROM
  5. ESP8266开发之旅 基础篇⑤ ESP8266 SPI通讯和I2C通讯
  6. ESP8266开发之旅 基础篇⑥ Ticker——ESP8266定时库

2、网络篇编程

  1. ESP8266开发之旅 网络篇① 认识一下Arduino Core For ESP8266
  2. ESP8266开发之旅 网络篇② ESP8266 工做模式与ESP8266WiFi库
  3. ESP8266开发之旅 网络篇③ Soft-AP——ESP8266WiFiAP库的使用
  4. ESP8266开发之旅 网络篇④ Station——ESP8266WiFiSTA库的使用
  5. ESP8266开发之旅 网络篇⑤ Scan WiFi——ESP8266WiFiScan库的使用
  6. ESP8266开发之旅 网络篇⑥ ESP8266WiFiGeneric——基础库
  7. ESP8266开发之旅 网络篇⑦ TCP Server & TCP Client
  8. ESP8266开发之旅 网络篇⑧ SmartConfig——一键配网
  9. ESP8266开发之旅 网络篇⑨ HttpClient——ESP8266HTTPClient库的使用
  10. ESP8266开发之旅 网络篇⑩ UDP服务
  11. ESP8266开发之旅 网络篇⑪ WebServer——ESP8266WebServer库的使用
  12. ESP8266开发之旅 网络篇⑫ 域名服务——ESP8266mDNS库
  13. ESP8266开发之旅 网络篇⑬ SPIFFS——ESP8266 Flash文件系统
  14. ESP8266开发之旅 网络篇⑭ web配网
  15. ESP8266开发之旅 网络篇⑮ 真正的域名服务——DNSServer
  16. ESP8266开发之旅 网络篇⑯ 无线更新——OTA固件更新

3、应用篇网络

  1. ESP8266开发之旅 应用篇① 局域网应用 ——炫酷RGB彩灯
  2. ESP8266开发之旅 应用篇② OLED显示天气屏
  3. ESP8266开发之旅 应用篇③ 简易版WiFi小车

4、高级篇app

  1. ESP8266开发之旅 进阶篇① 代码优化 —— ESP8266内存管理
  2. ESP8266开发之旅 进阶篇② 闲聊Arduino IDE For ESP8266配置
  3. ESP8266开发之旅 进阶篇③ 闲聊 ESP8266 Flash
  4. ESP8266开发之旅 进阶篇④ 常见问题 —— 解决困扰
  5. ESP8266开发之旅 进阶篇⑤ 代码规范 —— 像写文章同样优美
  6. ESP8266开发之旅 进阶篇⑥ ESP-specific APIs说明

    EEPROM(Electrically Erasable Programmable Read-Only Memory),电可擦可编程只读存储器——一种掉电后数据不丢失的存储芯片。
    EEPROM能够在不使用文件和文件系统的状况下用来固化一些数据,常见的好比用来保存SSID或者Password,保存用户设置等数据,这样就能够不用每次都经过烧写程序来改变系统运行时的初始值。
    Arduino提供了完善的eeprom库,不过须要注意的是ESP8266没有硬件EEPROM,使用的是flash模拟的EEPROMdom

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),原理大体以下图:
image
    因此要确保内容被保存到flash中,须要考虑commit的时机。函数

2. 官方介绍

    下图来源于Arduino For ESP8266对于EEPROM的介绍:
image
    具体意思能够理解为如下几点:oop

  • 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库很是简单,请看博主总结的百度脑图:
image
    仅仅有5个方法,可是博主在这里仍是带读者深刻去理解一下它们。
    Arduino Core For ESP8266的源码在github上能够查找到,读者能够把它下载下来以便后续深刻开发,连接位置为 ESP8266源码
    而后请找到下图位置:
image
    性能

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的大小后,再从设置中取得已设定好的值:
    image
        对于_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之间的关系,也讲解了方法使用,虽然简单,可是对于底层的认知,会让咱们优化代码性能更加便捷。

相关文章
相关标签/搜索