授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力。但愿你们分享给你周边须要的朋友或者同窗,说不定大神成长之路有博哥的奠定石。。。git
QQ技术互动交流群:ESP8266&32 物联网开发 群号622368884,不喜勿喷github
1、基础篇web
2、网络篇编程
- 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固件更新
3、应用篇网络
4、高级篇app
EEPROM(Electrically Erasable Programmable Read-Only Memory),电可擦可编程只读存储器——一种掉电后数据不丢失的存储芯片。
EEPROM能够在不使用文件和文件系统的状况下用来固化一些数据,常见的好比用来保存SSID或者Password,保存用户设置等数据,这样就能够不用每次都经过烧写程序来改变系统运行时的初始值。
Arduino提供了完善的eeprom库,不过须要注意的是ESP8266没有硬件EEPROM,使用的是flash模拟的EEPROM。dom
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的时机。函数
下图来源于Arduino For ESP8266对于EEPROM的介绍:
具体意思能够理解为如下几点:oop
EEPROM库很是简单,请看博主总结的百度脑图:
仅仅有5个方法,可是博主在这里仍是带读者深刻去理解一下它们。
Arduino Core For ESP8266的源码在github上能够查找到,读者能够把它下载下来以便后续深刻开发,连接位置为 ESP8266源码。
而后请找到下图位置:
性能
该功能用于申请具体大小的ram内存空间。
函数: begin(size)
参数:
size:要申请的内存大小。
返回值: 无;
注意点:
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 }
该功能用于往内存空间去写入数据。
函数: 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中。
该功能用于读取数据操做。
函数: 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。操做内存的一个好处就是快。
该功能用于把内存空间的数据覆盖到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,而且释放内存空间。
函数: end()
参数: 无;
返回值: 无;
注意点:
void EEPROMClass::end() { if (!_size) return; //写入flash commit(); if(_data) { //回收内存空间 delete[] _data; } _data = 0; _size = 0; _dirty = false; }
前面,咱们说到,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) { }
/* 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);
从上一节的计算,咱们能够知道,根据计算公式,咱们会最终获得一个具体位置的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);
注意点:
/* * 功能描述:该代码向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() { }
/* * 功能描述:该代码从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() { }
/* 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() { }
在没有应用结构体以前,不论是写入仍是读取操做,咱们都须要记住具体的存储位置。特别是当配置数据愈来愈多的时候或者别人维护的时候,很是容易出错。那么有没有办法优雅地解决这种问题呢?固然有,那就是结构体的妙用,咱们不须要关注具体的位置,只须要关注数据自己。看如下代码:
/* * 功能描述: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的结合使用,使咱们脱离了存储位置的限制,就算后期须要加多一个配置,咱们只须要在结构体上加上相应的字段,彻底不用改动其余代码。
这一章,讲解了ESP8266 EEPROM的底层设计原理,讲述了内存和flash之间的关系,也讲解了方法使用,虽然简单,可是对于底层的认知,会让咱们优化代码性能更加便捷。