ESP8266 Non-OS SDK开发探坑之四-用户非易失参数安全存储到flash
【Starting with ESP8266 — Light a LED】
【Starting with ESP8266(3) — Touch to control Relay-Programming & PCB design】
原博客链接:
http://www.straka.cn/blog/start-esp8266-4-flash-parameter-securely-save-load/
由于ESP8266系统可以自动保存系统参数到flash完成上电自动选择wifi工作模式和wifi连接参数等,但用户有时也需要保存一些非易失的数据,这就需要用户将信息写入flash,如果进一步考虑,写入flash的时候必须整扇区(4kb)擦除,然后再写入,所以存在一定的时长,如果为了数据完整性、安全性考虑,就必须考虑写入的时候突然掉电的风险。官方对这个问题是有说明的。见文档:
https://www.espressif.com/zh-hans/support/download/documents?keys=&field_type_tid%5B%5D=14
本文先讨论了用户参数存放区域问题,然后调用系统api进行数据安全读写。
首先根据官方的文档
或者结合博文【ESP8266开发入坑1—-点亮LED】提到的flash布局说明,
不支持云端升级(即NON-FOTA)的用户数据在eagle.irom0text.bin之后,而改文件的大小和保存地址在指南里有:
结合这两个文件,我们就可以计算出用户参数区的首地址:
比如512-non-fota,用户数据区首地址 = eagle.irom0text.bin的首地址 0x10000 + 大小 368*1024 = 0x6C000,
而实际用的是扇区数而不是字节地址数,所以需要0x6C000/4096 = 0x6C,
这里计算注意16-10进制转换。同理可以计算出fota布局下的用户参数区地址。
而用户参数区的大小除了首地址外还需要结尾地址决定,这个其实就是RF-CAL区的首地址,也就是blank.bin的存放地址。
我这里都算好了,并利用 user_rf_cal_sector_set里对flash布局的swtich顺便完成了该地址计算:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | uint32 ICACHE_FLASH_ATTR user_rf_cal_sector_set(void) { enum flash_size_map size_map = system_get_flash_size_map(); uint32 rf_cal_sec = 0;
bool bFota; #ifdef FOTA bFota = true; #else bFota = false; #endif
switch (size_map) { case FLASH_SIZE_4M_MAP_256_256: rf_cal_sec = 128 - 5; UserParamStartSect = bFota? 0x3C:0x6C; break;
case FLASH_SIZE_8M_MAP_512_512: rf_cal_sec = 256 - 5; UserParamStartSect = bFota? 0x7C:0xCC; break;
case FLASH_SIZE_16M_MAP_512_512: rf_cal_sec = 512 - 5; UserParamStartSect = bFota? 0x7C:0xD0; break; case FLASH_SIZE_16M_MAP_1024_1024: rf_cal_sec = 512 - 5; UserParamStartSect = bFota? 0xFC:0xD0; break;
case FLASH_SIZE_32M_MAP_512_512: rf_cal_sec = 1024 - 5; UserParamStartSect = bFota? 0x7C:0xD0; break; case FLASH_SIZE_32M_MAP_1024_1024: rf_cal_sec = 1024 - 5; UserParamStartSect = bFota? 0xFC:0xD0; break;
case FLASH_SIZE_64M_MAP_1024_1024: rf_cal_sec = 2048 - 5; UserParamStartSect = bFota? 0xFC:0xD0; break; case FLASH_SIZE_128M_MAP_1024_1024: rf_cal_sec = 4096 - 5; UserParamStartSect = bFota? 0xFC:0xD0; break; default: rf_cal_sec = 0; UserParamStartSect = 0; break; }
return rf_cal_sec; } |
好了,有了参数保存地址,我们再看看参数的保存和加载吧。
官方SDK API文档有说明其保护机制:
https://www.espressif.com/sites/default/files/2C-ESP8266_Non_OS_SDK_API_Reference__CN.pdf
这个官方有API的我就不造轮子了,如果有更多要求的可以自己实现。
这里我就定义了通用方法放到FlashParam头和实现文件中
先是定义要保存的参数结构体:
1 2 3 4 5 6 7 8 9 10 11 | struct FlashProtectParam{ uint16 ConfHolder; //uint32 ConfMagic; uint8 WorkStatus; uint8 Domain; //0: use ip, 1:use domain uint16 RemotePort; union{ struct ip_addr IP; uint8 Domain[DOMAIN_LEN+1]; }RemoteAddr; }; |
其中ConfHolder是方便调试用的, 在user_config.h里有定义这个宏:
1 | #define CONF_HOLDER 0x2A15 |
每当修改完代码烧录到芯片,如果修改了CONF_HOLDER宏的值,那么从flash读取参数的时候先比对ConfHolder,不一样就会重置系统参数,或者将宏的参数保存,如果不修改,那么就不用重新配置,新烧录的程序继续用之前芯片保存的数据。
所以加载的代码就复杂些了。
WorkStatus的每一位标记一个工作状态信息,方便上电恢复工作状态,这里我的定义是:
1 2 3 4 5 6 7 | #define WEB_SERV_BIT 0x01 #define TCP_SERV_BIT 0x02 #define MQTT_CLIENT_BIT 0x04
#define WIFI_CONF_BIT 0x10 #define REMOTE_SERV_CONF_BIT 0x20 #define MQTT_CONF_BIT 0x40 |
分别定义了是否开启web server 、tcp client需要连接的远程服务器信息是否配置等。
RemoteAddr保存了tcp-client需要连接的远程服务器地址信息,RemotePort保存了tcp-client需要连接的远程服务器端口信息,Domain标记远程服务器信息是域名还是ip,其实这个标记有点多余,因为可以对RemodeAddr进行STR->IP的转换或验证,如果失败就按域名进行DNS解析。
后文探坑7就对DNS解析做了解释。
flash参数的加载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | bool ICACHE_FLASH_ATTR LoadFlashProtParam(){ if(!system_param_load(SYS_PROTECT_SECT,0, &stFlashProtParam, sizeof(struct FlashProtectParam))){ stFlashProtParam.WorkStatus = WEB_SERV_BIT; system_param_save_with_protect(SYS_PROTECT_SECT,&stFlashProtParam,sizeof(struct FlashProtectParam)); TRACE("load flash protected param failed!\r\n"); } if(stFlashProtParam.ConfHolder != CONF_HOLDER){ stFlashProtParam.ConfHolder = CONF_HOLDER; stFlashProtParam.WorkStatus = WEB_SERV_BIT;
system_param_save_with_protect(SYS_PROTECT_SECT,&stFlashProtParam,sizeof(struct FlashProtectParam));
wifi_set_opmode(SOFTAP_MODE); //
struct softap_config config; wifi_softap_get_config(&config); // Get config first.
//generate special ssid to avoid conflict uint8 ssid[32]; os_memset(ssid, 0, 32); os_memcpy(ssid, "ESP_", 4); uint8 APMac[6]; wifi_get_macaddr(SOFTAP_IF,APMac); uint8 i; for(i=0;i<6;i++){ uint8 tmp = APMac[i]%62; if(tmp<26){ ssid[4+i] = tmp+'a'; }else if(tmp<52){ ssid[4+i] = tmp+'A'-26; }else{ ssid[4+i] = tmp+'0'-52; } } os_memset(config.ssid, 0, 32); os_memcpy(config.ssid, ssid, os_strlen(ssid));
os_memset(config.password, 0, 64); os_memcpy(config.password, INIT_AP_PASSWD, sizeof(INIT_AP_PASSWD)); config.authmode = AUTH_WPA_WPA2_PSK; config.ssid_len = 0;// or its actual length, read until string end config.max_connection = 4; // how many stations can connect to ESP8266 softAP at most.max 4 //config.channel = 1; //support 1~13 //config.ssid_hidden = 0;//default 0 //config.beacon_interval = 100;//100~60000ms, default 100
wifi_softap_set_config(&config);// Set ESP8266 softap config, ensure to call this func after softap enabled, execute immediatelly } TRACE("flash protected param:\r\nWork Status:%02x\r\n",stFlashProtParam.WorkStatus); if(stFlashProtParam.Domain){ TRACE("remote domain:%s:%d\r\n",stFlashProtParam.RemoteAddr.Domain,stFlashProtParam.RemotePort); }else{ TRACE("remote:"IPSTR":%d",IP2STR(&stFlashProtParam.RemoteAddr.IP.addr),stFlashProtParam.RemotePort); }
return true; } |
函数如上文完成了CONF_HOLDER的比对和重置,并多此一举的对ssid进行了设置,主要是为了防止固定ssid的重合导致多个设备无法区分,所以根据mac地址映射到了26个字母大小写和10个数字的可视字符,当然base64编码也可以。
密码当然就设成固定的初始密码,如大家有别的需求可以自行替换。
参数保存特别简单:
1 2 3 4 | bool ICACHE_FLASH_ATTR SaveFlashProtParam(){ return system_param_save_with_protect(SYS_PROTECT_SECT,&stFlashProtParam,sizeof(struct FlashProtectParam)); } |
stFlashProtParam毕竟全局都要用为了方便就弄成了全局变量。
此外,为了大家DIY方便,我将自定义的flash写入方法封装了方便的版本,主要是考虑到原始的FLASH读写接口是按扇区来的,读取还好,写入需要整扇区擦除,然后写入,那么,很容易造成误删数据,所以该函数就将每次擦除和写入操作合并,先读取出整个扇区保存到临时对象,然后修改临时对象,然后正扇区写入,这里有些问题,比如内存空间问题,需要一下子开辟整扇区4KB的栈或堆空间,而且效率也是个问题,用前需要细致考虑下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | SpiFlashOpResult ICACHE_FLASH_ATTR SPIFlashEraseWrite(uint32 sect,uint32 offset,uint32 *src, uint32 size){ if(offset+size>4096){ return SPI_FLASH_RESULT_ERR; } uint32 data[4096]; /* uint32 *data = (uint32 *)os_malloc(4096); if(data==NULL) return SPI_FLASH_RESULT_ERR; */ SpiFlashOpResult ret; ret = spi_flash_read(sect*4096,data,4096); if(ret!=SPI_FLASH_RESULT_OK) return ret; os_memcpy(data+offset,src,size); spi_flash_erase_sector(sect); return spi_flash_write(sect*4096,data,4096); } |
代码见:
https://github.com/atp798/BlogStraka/tree/master/ESP8266_NONOS_SDK-2.2.1-WebServer/
原博客:
http://www.straka.cn/blog/start-esp8266-4-flash-parameter-securely-save-load/