当前位置:   article > 正文

STM32用flash保存参数实现平衡擦写的一种方法_flash均衡保存算法

flash均衡保存算法

#FLASH平衡擦写#

一、概述

简易示意图如下:

      写参数前要擦除对应的扇区 全为0XFFFFFFFF操作的最小单位为32位  uint32_t;  当一块扇区写完时,将所有有用参数复制到第二块扇区,开始写新的参数,如果所有参数写完,又重第一块参数开始写,这样就能实现平衡写的目的,所以要实现这个功能,至少需要分配2个扇区实现均衡擦写。

  1. /* 储存扇区信息的结构体 */
  2. struct SSCT_HDR
  3. {
  4.     uint32_t st;   // 状态
  5.     uint32_t cnt;      // 标号
  6.     uint32_t version;  // 版本
  7. };  // 扇区HEAD结构
  8. typedef struct
  9. {
  10.     uint16_t len:16;    
  11.     uint16_t alen:16;    
  12.     /* data */
  13. }VARLEN;
  14. struct VAR_ST  //Flash数据存储结构
  15. {
  16.     uint32_t  status;  //数据当前状态
  17.     uint32_t key;     //数据key
  18.     union
  19.     {
  20.     uint32_t len;     //数据长度  len +alen   数据实际长度+所在内存长度   内存长度必须是4的整数倍
  21.     VARLEN   len_b;
  22.         /* data */
  23.     };
  24.      
  25. };

扇区1

扇区使用状态

扇区

标号

扇区版本号

数据状态ad1

数据key

长度

len

alen1

D1

D1

D1

D1

D1

D1

数据状态ad1+

alen1/4

数据key

长度

Len2

Alen2

D2

D2

D2

D2

D2

D2

数据状态ad2+

Alen2/4

数据key

长度

Len3

Alen3

D3

D3

D3

D3

D3

D3

D3

D3

D3

D3

D3

D3

...

...

...

...

...

...

...

...

...

二、源码

  1. #include <stdbool.h>
  2. #include <stdint.h> //引用框架配置文件
  3. #define MAX_CVAR_NUM (200) //数据存储最大个数
  4. typedef struct
  5. {
  6. uint16_t len:16;
  7. uint16_t alen:16;
  8. /* data */
  9. }VARLEN;
  10. struct VAR_ST //Flash数据存储结构
  11. {
  12. uint32_t status; //数据当前状态
  13. uint32_t key; //数据key
  14. union
  15. {
  16. uint32_t len; //数据长度 len +alen 数据实际长度+所在内存长度 内存长度必须是4的整数倍
  17. VARLEN len_b;
  18. /* data */
  19. };
  20. };
  21. struct SFVAR_POINT
  22. {
  23. uint32_t key; //数据key
  24. uint8_t* flashAddr; //数据地址
  25. };
  26. typedef struct
  27. {
  28. uint8_t* sectorBaseAdr;
  29. uint32_t sectorSize;
  30. uint8_t sectorNum;
  31. void (*FlashInit_Cbk)(void); // Flash初始化函数
  32. bool (*FlashErase_Cbk)(uint8_t* addr, uint32_t size); // Flash擦除函数
  33. uint32_t (*FlashWrite_Cbk)(void* addr, const void* buf, uint32_t size); // Flash写入函数
  34. uint32_t (*FlashRead_Cbk)(void* addr, void* buf, uint32_t size); /// flash读函数
  35. // private
  36. struct
  37. {
  38. uint32_t makeTime;
  39. uint8_t* sectorUseBaseAdr; // flash 参数存储区基地址
  40. uint8_t sectorUseCnt; // Flash 所有的序号
  41. uint32_t varNum; // Flash存储数据个数
  42. uint32_t tail; // Flash当前地址
  43. uint32_t head; // 有效头部位置
  44. uint8_t swSctFlag; //扇区切换flag
  45. uint8_t* rmAdr; //需要删除的 地址
  46. struct VAR_ST pCVar; //单个参数的头部结构
  47. struct SFVAR_POINT varList[MAX_CVAR_NUM]; //数据Z指针数组
  48. } pri;
  49. }FlashPar_Prop;
  50. typedef struct
  51. {
  52. void (* const Create)(FlashPar_Prop* self); // FlashVar
  53. void (*Init)(FlashPar_Prop* self,
  54. uint32_t makeTime,
  55. uint8_t* sectorBaseAdr, // FLASH基地址
  56. uint32_t sectorSize, // flash大小
  57. uint32_t sectorNum, // flash块的个数
  58. void (*FlashInit_Cbk)(void), // flash初始化函数
  59. bool (*FlashErase_Cbk)(uint8_t* addr, uint32_t size), // flash擦除函数
  60. uint32_t (*FlashWrite_Cbk)(void* addr, const void* buf, uint32_t size), // flash写入函数
  61. uint32_t (*FlashRead_Cbk)(void* addr, void* buf, uint32_t size) // flash读函数
  62. );
  63. // API
  64. uint32_t (*RdPar)(FlashPar_Prop* self, uint32_t key, uint8_t* pRdBuf, uint32_t bufLen); ///数据读取函数
  65. MOBJ_BOOL (*WtPar)(FlashPar_Prop* self, uint32_t key, uint8_t* pWtDat, uint32_t datLen); //数据写入函数
  66. MOBJ_BOOL (*DelPar)(FlashPar_Prop* self, uint32_t key); //数据删除函数
  67. }FlashPar_Func;
  68. extern const FlashPar_Func FlashPar;

  1. #include "MFlashVar.h"
  2. #include "string.h"
  3. #include "MTime.h"
  4. /* 扇区使用情况 表示各个扇区状态*/
  5. #define SSCT_UNUSE (0xFFFFFFFF) // 未使用
  6. #define SSCT_USE (0xBBBBBBBB) // 使用中
  7. #define SSCT_DEL (0x00000000) // 删除状状态
  8. /* 某区域保存参数的状态 */
  9. #define SCVAR_UNUSE (0xFFFFFFFF) // 未使用
  10. #define SCVAR_USE (0xAAAAAAAA) // 使用中
  11. #define SCVAR_DEL (0x00000000) // 删除状状态
  12. // 表示各个数据状态
  13. /* 储存扇区信息的结构体 */
  14. struct SSCT_HDR
  15. {
  16. uint32_t st; // 状态
  17. uint32_t cnt; // 标号
  18. uint32_t version; // 版本
  19. }; // 扇区HEAD结构
  20. static int32_t FindVarAddr(FlashPar_Prop *self, uint32_t key);
  21. static uint32_t AllocVar(FlashPar_Prop *self, uint32_t len, uint32_t key);
  22. static void DelVar(FlashPar_Prop *self, uint8_t *addr);
  23. static void PrgVar(FlashPar_Prop *self, void *flashAddr, uint32_t key, uint8_t *pWtDat, uint32_t dataLen);
  24. static void LoadSector(FlashPar_Prop *self);
  25. static void LoadFVar(FlashPar_Prop *self);
  26. static void SwitchSct(FlashPar_Prop *self);
  27. /**
  28. * @brief FlashPar
  29. *
  30. */
  31. static void FlashPar_Init(FlashPar_Prop *self,
  32. uint32_t makeTime,
  33. uint8_t *sectorBaseAdr,
  34. uint32_t sectorSize,
  35. uint32_t sectorNum,
  36. void (*FlashInit_Cbk)(void),
  37. bool (*FlashErase_Cbk)(uint8_t *addr, uint32_t size),
  38. uint32_t (*FlashWrite_Cbk)(void *addr, const void *buf, uint32_t size),
  39. uint32_t (*FlashRead_Cbk)(void *addr, void *buf, uint32_t size) // flash读函数
  40. )
  41. {
  42. self->pri.varNum = 0;
  43. self->sectorBaseAdr = sectorBaseAdr;
  44. self->sectorSize = sectorSize;
  45. self->sectorNum = sectorNum;
  46. self->pri.makeTime = makeTime;
  47. self->FlashInit_Cbk = FlashInit_Cbk;
  48. self->FlashErase_Cbk = FlashErase_Cbk;
  49. self->FlashWrite_Cbk = FlashWrite_Cbk;
  50. self->FlashRead_Cbk = FlashRead_Cbk;
  51. // step1: load useing sector
  52. LoadSector(self);
  53. // step2 : load flash variable
  54. LoadFVar(self);
  55. }
  56. /**
  57. * @brief 申请地址并检查剩余地址是否足够
  58. *
  59. */
  60. static uint32_t AllocVar(FlashPar_Prop *self, uint32_t len, uint32_t key)
  61. {
  62. uint32_t pFVarAddress;
  63. uint8_t tmp, actLen;
  64. uint16_t index;
  65. /******step1 :Caculate the actual space***/
  66. tmp = len % 4;
  67. if (tmp != 0)
  68. actLen = sizeof(struct VAR_ST) + len + (4 - tmp);
  69. else
  70. actLen = sizeof(struct VAR_ST) + len;
  71. /*step2: check current sector has enough sapace*/
  72. if (self->pri.tail + actLen >= self->sectorSize)
  73. {
  74. SwitchSct(self);
  75. index = FindVarAddr(self, key);
  76. self->pri.rmAdr = self->pri.varList[index].flashAddr;
  77. }
  78. else {}
  79. /*step3: current sector has enough sapace*/
  80. if (self->pri.tail + actLen < self->sectorSize)
  81. {
  82. pFVarAddress = (uint32_t)(self->pri.sectorUseBaseAdr + self->pri.tail);
  83. self->pri.tail += actLen;
  84. }
  85. else
  86. {
  87. pFVarAddress = 0;
  88. }
  89. return pFVarAddress;
  90. }
  91. /**
  92. * @brief 删除原有变量函数
  93. *
  94. */
  95. static void DelVar(FlashPar_Prop *self, uint8_t *addr)
  96. {
  97. uint32_t st;
  98. st = SCVAR_DEL;
  99. self->FlashWrite_Cbk(addr, &st, sizeof(st));
  100. }
  101. /**
  102. * @brief 写入参数
  103. *
  104. */
  105. static void PrgVar(FlashPar_Prop *self, void *flashAddr, uint32_t key, uint8_t *pWtDat, uint32_t dataLen)
  106. {
  107. struct VAR_ST tmpVar;
  108. uint32_t tmp;
  109. uint32_t dtActLen;
  110. uint8_t *pHead = (uint8_t *)flashAddr;
  111. uint8_t *pData = (uint8_t *)flashAddr + sizeof(struct VAR_ST);
  112. tmp = dataLen % 4;
  113. if (tmp != 0)
  114. dtActLen = dataLen + (4 - tmp);
  115. else
  116. dtActLen = dataLen;
  117. tmpVar.status = SCVAR_USE;
  118. tmpVar.key = key;
  119. tmpVar.len_b.len = dataLen;
  120. tmpVar.len_b.alen = dtActLen;
  121. self->FlashWrite_Cbk(pHead, (uint8_t *)&tmpVar, sizeof(struct VAR_ST));
  122. self->FlashWrite_Cbk(pData, pWtDat, dtActLen);
  123. }
  124. /**
  125. * @brief 根据关键字查询变量
  126. *
  127. */
  128. static uint32_t FlashPar_RdPar(FlashPar_Prop *self, uint32_t key, uint8_t *pRdBuf, uint32_t bufLen)
  129. {
  130. struct VAR_ST pFVar;
  131. uint32_t ret = 0;
  132. uint8_t *pData;
  133. uint32_t len;
  134. int32_t index;
  135. // find var in ram
  136. index = FindVarAddr(self, key);
  137. if (index < 0)
  138. {
  139. return ret;
  140. }
  141. self->FlashRead_Cbk(self->pri.varList[index].flashAddr, &pFVar, sizeof(struct VAR_ST));
  142. len = pFVar.len_b.len;
  143. if (bufLen < len)
  144. {
  145. len = bufLen;
  146. }
  147. pData = self->pri.varList[index].flashAddr + sizeof(struct VAR_ST);
  148. self->FlashRead_Cbk(pData, pRdBuf, len);
  149. ret = len;
  150. return ret;
  151. }
  152. /* 根据KEY 删除一个参数 */
  153. static MOBJ_BOOL FlashPar_DelPar(FlashPar_Prop *self, uint32_t key)
  154. {
  155. uint8_t *pFVar;
  156. int32_t index;
  157. uint32_t MvDataNum;
  158. // find var in ram
  159. index = FindVarAddr(self, key);
  160. if (index < 0)
  161. {
  162. return NOT;
  163. }
  164. pFVar = self->pri.varList[index].flashAddr;
  165. MvDataNum = self->pri.varNum - (index + 1);
  166. while (MvDataNum--)
  167. {
  168. self->pri.varList[index].flashAddr = self->pri.varList[index + 1].flashAddr;
  169. self->pri.varList[index].key = self->pri.varList[index + 1].key;
  170. index++;
  171. }
  172. self->pri.varList[index].flashAddr = 0;
  173. self->pri.varList[index].key = 0;
  174. self->pri.varNum--;
  175. DelVar(self, pFVar);
  176. return YES;
  177. }
  178. /**
  179. * @brief 根据key保存一个参数
  180. *
  181. */
  182. static MOBJ_BOOL FlashPar_WtPar(FlashPar_Prop *self, uint32_t key, uint8_t *pWtDat, uint32_t datLen)
  183. {
  184. uint8_t *pNewVar;
  185. uint8_t tempdata[258] = {0};
  186. struct VAR_ST pOldVar;
  187. int32_t index;
  188. MOBJ_BOOL ret;
  189. if (datLen > 256)
  190. {
  191. }
  192. /******step1 :find old var ***/
  193. index = FindVarAddr(self, key);
  194. /******step2 :wite new var*/
  195. if (index < 0) // step2.1 old var not exist
  196. {
  197. if (self->pri.varNum >= MAX_CVAR_NUM) // check number
  198. {
  199. ret = NOT;
  200. }
  201. else if (0 == (pNewVar = (uint8_t *)AllocVar(self, datLen, key))) // alloc space
  202. {
  203. ret = NOT;
  204. }
  205. else
  206. {
  207. PrgVar(self, pNewVar, key, pWtDat, datLen);
  208. self->pri.varList[self->pri.varNum].key = key;
  209. self->pri.varList[self->pri.varNum].flashAddr = pNewVar;
  210. self->pri.varNum++;
  211. ret = YES;
  212. }
  213. }
  214. else // step2.2 old var exist
  215. {
  216. self->pri.rmAdr = self->pri.varList[index].flashAddr;
  217. self->FlashRead_Cbk(self->pri.rmAdr, &pOldVar, sizeof(struct VAR_ST));
  218. if (pOldVar.key == key)
  219. {
  220. self->FlashRead_Cbk(self->pri.rmAdr + sizeof(struct VAR_ST), tempdata, pOldVar.len_b.len);
  221. if ((pOldVar.len_b.len== datLen) && (0 == memcmp(tempdata, pWtDat, datLen)))
  222. {
  223. ret = YES;
  224. }
  225. else
  226. {
  227. pNewVar = (uint8_t *)AllocVar(self, datLen, key);
  228. if (0 == pNewVar) // alloc space
  229. {
  230. // EINT;
  231. ret = NOT;
  232. }
  233. else
  234. {
  235. PrgVar(self, pNewVar, key, pWtDat, datLen); // write new var
  236. DelVar(self, self->pri.rmAdr); // 完全删除
  237. ret = YES;
  238. }
  239. self->pri.varList[index].flashAddr = pNewVar;
  240. }
  241. }
  242. }
  243. return ret;
  244. }
  245. static int32_t FindVarAddr(FlashPar_Prop *self, uint32_t key)
  246. {
  247. int32_t i;
  248. for (i = 0; i < self->pri.varNum; i++)
  249. {
  250. if (self->pri.varList[i].key == key)
  251. return i;
  252. }
  253. return -1;
  254. }
  255. /**
  256. * @brief 加载各个扇区的状态信息
  257. *
  258. */
  259. static void LoadSector(FlashPar_Prop *self)
  260. {
  261. int32_t i;
  262. int32_t maxSctCnt = 0;
  263. uint8_t *useadd = 0;
  264. struct SSCT_HDR pSctHdr, newSctHdr;
  265. // step1 : find using sector
  266. for (i = 0; i < self->sectorNum; i++)
  267. {
  268. useadd = self->sectorBaseAdr + i * self->sectorSize;
  269. self->FlashRead_Cbk(useadd, &pSctHdr, sizeof(struct SSCT_HDR));
  270. // check the version,
  271. if ((pSctHdr.version != 0xFFFFFFFF) && (pSctHdr.version != self->pri.makeTime))
  272. {
  273. self->FlashErase_Cbk(useadd, self->sectorSize);
  274. }
  275. else
  276. {
  277. switch (pSctHdr.st)
  278. {
  279. case SSCT_UNUSE: {
  280. break;
  281. }
  282. case SSCT_USE: {
  283. if (pSctHdr.cnt >= maxSctCnt)
  284. {
  285. self->pri.sectorUseBaseAdr = useadd;
  286. self->pri.sectorUseCnt = i;
  287. maxSctCnt = pSctHdr.cnt;
  288. }
  289. break;
  290. }
  291. case SSCT_DEL: {
  292. break;
  293. }
  294. }
  295. }
  296. }
  297. // step2 : if don't find using sector them set sector0 is used
  298. if (maxSctCnt == 0)
  299. {
  300. self->pri.sectorUseBaseAdr = self->sectorBaseAdr;
  301. self->pri.sectorUseCnt = 0;
  302. self->FlashErase_Cbk(self->pri.sectorUseBaseAdr, self->sectorSize); // 擦除 实际地址需
  303. newSctHdr.st = SSCT_USE;
  304. newSctHdr.cnt = 1;
  305. newSctHdr.version = self->pri.makeTime;
  306. self->FlashWrite_Cbk(self->pri.sectorUseBaseAdr, (uint8_t *)&newSctHdr, sizeof(struct SSCT_HDR));
  307. }
  308. self->pri.tail = sizeof(struct SSCT_HDR);
  309. }
  310. /**
  311. * @brief 加载flash 区的参数信息
  312. *
  313. */
  314. static void LoadFVar(FlashPar_Prop *self)
  315. {
  316. uint8_t rFlag = 1;
  317. struct VAR_ST *pErrVar = 0;
  318. struct VAR_ST nowVar;
  319. uint32_t errNo = 0;
  320. uint8_t *pFVarAddress = 0;
  321. // uint8_t* pFVarAddress = 0;
  322. while ((self->pri.tail < self->sectorSize) && rFlag)
  323. {
  324. pFVarAddress = self->pri.sectorUseBaseAdr + self->pri.tail;
  325. self->FlashRead_Cbk(pFVarAddress, &nowVar, sizeof(struct VAR_ST));
  326. switch (nowVar.status)
  327. {
  328. // if the data was unused than over build process
  329. case SCVAR_UNUSE: {
  330. rFlag = 0; // stop research
  331. break;
  332. }
  333. case SCVAR_USE: {
  334. if ((pErrVar != 0) && (nowVar.key == pErrVar->key))
  335. {
  336. self->pri.tail += sizeof(struct VAR_ST) + nowVar.len_b.alen;
  337. DelVar(self, self->pri.varList[errNo].flashAddr); // 删除原有错误数据
  338. self->pri.varList[errNo].flashAddr = pFVarAddress;
  339. }
  340. else // nomal
  341. {
  342. self->pri.tail += sizeof(struct VAR_ST) + nowVar.len_b.alen;
  343. self->pri.varList[self->pri.varNum].key = nowVar.key;
  344. self->pri.varList[self->pri.varNum].flashAddr = pFVarAddress;
  345. self->pri.varNum++;
  346. }
  347. break;
  348. }
  349. // if deleted than jump
  350. case SCVAR_DEL: {
  351. self->pri.tail += sizeof(struct VAR_ST) + nowVar.len_b.alen;
  352. }
  353. default: // 参数报错
  354. {
  355. // self->pri.tail += sizeof(struct VAR_ST) + dtActLen;
  356. }
  357. }
  358. } // end while
  359. }
  360. /**
  361. * @brief Flash扇区切换 (暂时未确认是否ok)
  362. *
  363. */
  364. static void SwitchSct(FlashPar_Prop *self)
  365. {
  366. uint8_t data[256];
  367. struct VAR_ST pOldVar;
  368. struct SSCT_HDR pOldSctHD;
  369. uint8_t *oldSct = self->pri.sectorUseBaseAdr;
  370. self->FlashRead_Cbk(oldSct, &pOldSctHD, sizeof(struct SSCT_HDR));
  371. /********step1: find next sector******/
  372. self->pri.sectorUseCnt++;
  373. if (self->pri.sectorUseCnt >= self->sectorNum)
  374. {
  375. self->pri.sectorUseCnt = 0;
  376. }
  377. uint8_t *newSct = self->sectorBaseAdr + self->pri.sectorUseCnt * self->sectorSize;
  378. self->FlashErase_Cbk(newSct, self->sectorSize); // 擦除即将切换到的扇区 擦除地址开始后的1个扇区
  379. /********step2 : produce sector header*******/
  380. struct SSCT_HDR newSctHD = {
  381. .st = SSCT_USE,
  382. .cnt = pOldSctHD.cnt + 1,
  383. .version = self->pri.makeTime,
  384. };
  385. self->pri.sectorUseBaseAdr = (uint8_t *)newSct;
  386. self->pri.tail = sizeof(struct SSCT_HDR);
  387. // sector write pre
  388. self->FlashWrite_Cbk(newSct, (uint8_t *)&newSctHD, sizeof(struct SSCT_HDR));
  389. /********step3 :将原有参数移动到新区*************/
  390. int32_t i;
  391. uint8_t *newvaraddr;
  392. for (i = 0; i < self->pri.varNum; i++)
  393. {
  394. self->FlashRead_Cbk(self->pri.varList[i].flashAddr, &pOldVar, sizeof(struct VAR_ST));
  395. newvaraddr = (uint8_t *)((uint32_t)self->pri.sectorUseBaseAdr + self->pri.tail);
  396. self->FlashRead_Cbk(self->pri.varList[i].flashAddr + sizeof(struct VAR_ST), data, pOldVar.len_b.len); // 读数据
  397. PrgVar(self, newvaraddr, pOldVar.key, data, pOldVar.len_b.len); // 保存数据
  398. self->pri.varList[i].flashAddr = newvaraddr;
  399. self->pri.tail += sizeof(struct VAR_ST) + pOldVar.len_b.alen;
  400. if (self->pri.tail > self->sectorSize)
  401. {
  402. //扇区溢出
  403. return;
  404. }
  405. }
  406. pOldSctHD.st = SSCT_DEL;
  407. // sector write pre
  408. self->FlashWrite_Cbk(oldSct, (uint8_t *)&pOldSctHD, sizeof(struct SSCT_HDR));
  409. }
  410. /**
  411. * @brief FlashPar
  412. *
  413. */
  414. void FlashPar_Create(FlashPar_Prop *self) { memset(self, 0, sizeof(FlashPar_Prop)); }
  415. const FlashPar_Func FlashPar = {.Create = FlashPar_Create,
  416. .Init = FlashPar_Init,
  417. .RdPar = FlashPar_RdPar,
  418. .WtPar = FlashPar_WtPar,
  419. .DelPar = FlashPar_DelPar};

平衡擦写的流程主要涉及到扇区的选择和切换,以及变量的写入和删除。

1. 首先,在`FlashPar_Init`函数中,通过调用`LoadSector`函数加载扇区的状态信息。该函数会遍历所有扇区,检查状态并选择使用中的扇区作为当前扇区。

2. 接下来,在`LoadFVar`函数中,会加载当前扇区中的flash变量。函数会遍历当前扇区中的每个flash变量,将其存储到`pri.varList`数组中。同时,将当前扇区中的`tail`指针指向下一个可用的地址。

3. 当需要写入一个新的flash变量时,首先通过`AllocVar`函数申请一个地址,并检查当前扇区是否有足够的空间。如果空间不足,则需要切换到下一个扇区,并重新分配地址。然后,通过`PrgVar`函数将变量写入到flash中,并更新`pri.varList`数组和`pri.varNum`变量。如果找到了相同关键字的旧变量,则会先删除旧变量。

4. 当需要删除一个flash变量时,通过`FlashPar_DelPar`函数根据关键字找到变量的地址,并调用`DelVar`函数删除变量。

5. 当需要读取一个flash变量时,通过`FlashPar_RdPar`函数根据关键字找到变量的地址,并将变量的数据读取出来。

在上述流程中,如果当前扇区的空间不足以容纳新的flash变量,就会触发扇区切换。在`SwitchSct`函数中,先找到下一个可用的扇区,并将其标记为使用状态。然后,将当前扇区中的变量逐个移动到新的扇区,并更新变量的地址。同时,将当前扇区的状态设置为删除状态,并将新的扇区的状态设置为使用状态。

通过这样的流程,可以实现对flash的平衡擦写,避免频繁的擦写操作,延长flash的使用寿命。同时,可以方便地存储和读取变量,提供了一种简单有效的持久化存储解决方案。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/676295
推荐阅读
相关标签
  

闽ICP备14008679号