ESP8266 Non-OS SDK開發探坑之四-用戶非易失參數安全存儲到flash

ESP8266 Non-OS SDK開發探坑之四-用戶非易失參數安全存儲到flash

Starting with ESP8266 — Light a LED

Starting with ESP8266 (2)–Touch to control relay status-circuit design & electronic components selection

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

ESP8266 Flash 讀寫說明

本文先討論了用戶參數存放區域問題,然後調用系統api進行數據安全讀寫。

首先根據官方的文檔

ESP8266 SDK 入門指南

或者結合博文【ESP8266開發入坑1—-點亮LED】提到的flash佈局說明,

不支持雲端升級(即NON-FOTA)的用戶數據在eagle.irom0text.bin之後,而改文件的大小和保存地址在指南里有:

結合這兩個文件,我們就可以計算出用戶參數區的首地址:

比如512-non-fota,用戶數據區首地址 = eagle.irom0text.bin的首地址 0x10000 + 大小 368*1024 = 0x6C000,

而實際用的是扇區數而不是字節地址數,所以需要0x6C000/4096 = 0x5C,

這裏計算注意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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章