陶新成 发表于 2019-1-10 16:50:25

复习老掉牙的知识-STM32F407实现FLASH模拟EEPROM

程序使用办法和注意事项在代码下面介绍
主要代码如下:
#if !defined(_FLASH_H)
#define _FLASH_H
#define FLASH_ADR 0x08010000
#define u8 unsigned char
#define u16 unsigned int
#define u32 unsigned long int
union union_temp16
{
unsigned int un_temp16;
unsigned char un_temp8;
}my_unTemp16;
typedef struct
{
       u8 apn;
       u8 useName;
       u8 password;
       u8 serverIP;
       u8 port;
       u8 useCall;
}configStruct;
void FlashWriteStr( u32 flash_add, u16 len, u16* data )
{       u16 byteN = 0;
       FLASH_Unlock();
       //FLASH_ErasePage(flash_add);
       while( len )
       {
                        my_unTemp16.un_temp8 = *(data+byteN);
                        my_unTemp16.un_temp8 = *(data+byteN+1);
                        FLASH_ProgramHalfWord( flash_add+byteN , my_unTemp16.un_temp16 );

                        if( 1==len ){
                        break;
                        }
                        else{
                                byteN += 2;
                                len -= 2;
                        }
                }
                FLASH_Lock();}
void FlashReadStr( u32 flash_add, u16 len, u16* data )
{
        u16 byteN = 0;
        while( len )
        {
                my_unTemp16.un_temp16 = *(vu16*)(flash_add+byteN);
                if( 1==len )
                {
                        *(data+byteN) = my_unTemp16.un_temp8;
                        break;
                }
                else
                {
                        *(data+byteN) = my_unTemp16.un_temp8;
                        *(data+byteN+1) = my_unTemp16.un_temp8;
                        byteN += 2;
                        len -= 2;
                }
        }
}
#endif
使用办法:使用该程序无需调用多余库函数,写入时调用FlashWriteStr(FLASH_ADR,2,&data);读函数时使用FlashReadStr(FLASH_ADR,2,&data);
读写数据均需要使用数组。
注意事项:
1、在写函数时关掉了擦除扇区功能,FLASH_ErasePage(flash_add);函数可以在一个区块内完成数据在不同地址的独立读写功能
2、读写函数必须使用两字节以上定义方式例如int,long等,因为FLASH存储一次写入16Bit.
3、使用时着重注意看门狗,如果喂狗时间太长建议关闭看门狗功能,或者做好互斥
据资料说使用FLASH模拟EEPROM的寿命比EEPROM少一个数量级,不知哪位对此有相对客观的见解!

lzg1987 发表于 2019-1-10 17:14:45

据资料说使用FLASH模拟EEPROM的寿命比EEPROM少一个数量级,不知哪位对此有相对客观的见解!
好像只有10000次,他这个应用肯定是一些不经常改变的数据,例如校准数据。
要是一天变三次的数据估计会把flash写坏。

tomzbj 发表于 2019-1-10 17:54:31

用st官方的不就行了, 写N次才擦除一次
我用的:
flash_eeprom.h
/// @file flash_eeprom.h
#ifndef _FLASH_EEPROM_H
#define _FLASH_EEPROM_H

void FLASH_EEPROM_Config(void);
void FLASH_EEPROM_ReadAll(void);
void FLASH_EEPROM_WriteWord(unsigned short addr, unsigned short data);
unsigned short FLASH_EEPROM_ReadWord(unsigned short addr);
void FLASH_EEPROM_WriteData(unsigned short addr, void* data, int num);
void FLASH_EEPROM_ReadData(unsigned short addr, void* data, int num);

#endif


flash_eeprom.c
#include "flash_eeprom.h"
#include "usart.h" // for debug

// 在bootloader里只能读到bootloader区的结尾, 因此这时END_OF_CODE的值并不正确.
// 不过bootloader区不会很长, 因此只要在APP区读到END_OF_CODE不冲突就可以了.
#define END_OF_CODE ((unsigned long)&_etext + ((unsigned long)&_edata - (unsigned long)&_sdata))

#define PAGE_SIZE 2048    // for HD devices
//#define PAGE_SIZE 1024   // for LD & MD devices

//#define FLASH_SIZE ((*(unsigned long*)0x1ffff7cc) & 0xffff)   // for stm32f0xx
#define FLASH_SIZE ((*(unsigned long*)0x1ffff7e0) & 0xffff)   // for stm32f10x

//#define PAGE_MAIN (0x08000000 + (FLASH_SIZE - 3) * 1024)         // for LD & MD devices
//#define PAGE_BACKUP (0x08000000 + (FLASH_SIZE - 2) * 1024)

#define PAGE_MAIN (0x08000000 + (FLASH_SIZE - 6) * 1024)      // for HD devices
#define PAGE_BACKUP (0x08000000 + (FLASH_SIZE - 4) * 1024)      // for HD devices

#define MAIN_ADDR(x) (PAGE_MAIN + (x))
#define BACKUP_ADDR(x) (PAGE_BACKUP + (x))
#define LOAD_ADDR(x) (*(unsigned short*)MAIN_ADDR(x))
#define LOAD_DATA(x) (*(unsigned short*)MAIN_ADDR((x) + 2))
#define LOAD_BACKUP_ADDR(x) (*(unsigned short*)BACKUP_ADDR(x))
#define LOAD_BACKUP_DATA(x) (*(unsigned short*)BACKUP_ADDR((x) + 2))

#define MAX_DUMMY_ADDR (PAGE_SIZE / 8 * 3 - 1)      // LD & MD devices: 191

extern int _etext, _sdata, _edata;

/// @brief 取得第一个空地址
/// @param None
/// @retval 返回第一个空地址, 若不存在则返回-1
static int GetFirstEmptyAddr(void)
{
    unsigned short i;
    for(i = 0; i < PAGE_SIZE; i += 4) {
      if(*(unsigned long*)MAIN_ADDR(i) == 0xffffffff)
            return i;
    }
    return -1;// Full
}

/// @brief 从后备页读取数据
/// @param dummy_addr 伪地址
/// @retval 读到的数据, 若未读到则返回-1 (此处应有更好的处理方式)
static int ReadWordFromBackup(unsigned short dummy_addr) {
    int addr = 1020;
    while(addr >= 0) {
      if(LOAD_BACKUP_ADDR(addr) == dummy_addr)
            return LOAD_BACKUP_DATA(addr);
      addr -= 4;
    }
    return -1;      // Not found
}

/// @brief 主存储页满, 利用后备页进行轮转
/// @param None
/// @retval None
static void Rotate(void)
{
    FLASH_Unlock();
    // 1. Erase Backup Page
    FLASH_ErasePage(PAGE_BACKUP);
    // 2. Copy Main Page to Backup Page. Add checksum later!
    for(int i = 0; i < 1024; i += 4) {
      FLASH_ProgramWord(BACKUP_ADDR(i), *(unsigned long*)MAIN_ADDR(i));   // full copy
    }
    // 3. Erase Main Page
    FLASH_ErasePage(PAGE_MAIN);
    // 4. Copy Backup Page to Main Page. Add Checksum Later!
    for(int i = 0; i <= MAX_DUMMY_ADDR; i++) {
      int data = ReadWordFromBackup(i);
      if(data != -1)
            FLASH_EEPROM_WriteWord(i, data);
    }
    FLASH_Lock();
}

/// @brief 写入16位字
/// @param dummy_addr 伪地址
/// @param data 16位数据
/// @retval None
void FLASH_EEPROM_WriteWord(unsigned short dummy_addr, unsigned short data)
{
    if(dummy_addr > MAX_DUMMY_ADDR) {
      uprintf(USART_USB, "Address out of range!\n");
      return;
    }
    int addr = GetFirstEmptyAddr();
    if(addr == -1) {            // Page full, rotate needed
      Rotate();
      addr = GetFirstEmptyAddr();
    }
    unsigned org_data = FLASH_EEPROM_ReadWord(dummy_addr);
    if(org_data == data)      // skip writing if data unchanged
      return;

    FLASH_Unlock();
    FLASH_ProgramHalfWord(MAIN_ADDR(addr), dummy_addr);
    FLASH_ProgramHalfWord(MAIN_ADDR(addr) + 2, data);
    FLASH_Lock();
}

/// @brief 读取16位数据
/// @param dummy_addr 伪地址
/// @retval 读到的数据, 若未读到则返回0xffff
unsigned short FLASH_EEPROM_ReadWord(unsigned short dummy_addr)
{
    int addr = GetFirstEmptyAddr();
//    _dbg(); uprintf(USART_USB, "%d\n", addr);

    if(addr == 0 || addr == -1)
      return 0xffff;          // No data
    while(addr >= 0) {
      if(LOAD_ADDR(addr) == dummy_addr)
            return LOAD_DATA(addr);
      addr -= 4;
    }
    return 0xffff;            // Dummy_addr not found
}

/// @brief 读取全部数据, 仅用于测试
/// @param None
/// @retval None
void FLASH_EEPROM_ReadAll(void)
{
    // todo
    for(int i = 0; i < 1024; i += 4) {
      uprintf(USART_USB, "%04d: %04x %04x", i,
                *(unsigned short*)MAIN_ADDR(i),
                *(unsigned short*)MAIN_ADDR(i + 2) );
      if(i % 24 == 20)
            uprintf(USART_USB, "\n");
    }
}

/// @brief 写入数据
/// @param addr 伪地址
/// @param data 指向待写入数据缓冲区的指针
/// @param num 需要写入的数据字节数
/// @retval None
void FLASH_EEPROM_WriteData(unsigned short addr, void* data, int num)
{
//    uprintf(USART_USB, "%04x %08x %d\n", addr, (unsigned long)data, num);
    if(num % 2 != 0)
      num++;
    while (num > 0) {
//      _dbg();
      FLASH_EEPROM_WriteWord(addr, *(unsigned short*)data);
      addr++;
      data += 2;
      num -= 2;
    }
}

/// @brief 读取数据
/// @param addr 伪地址
/// @param data 指向待读取数据缓冲区的指针
/// @param num 需要读取的数据字节数
/// @retval None
void FLASH_EEPROM_ReadData(unsigned short addr, void* data, int num)
{
    if(num % 2 != 0)
      num++;
    while(num > 0) {
      *(unsigned short*)data = FLASH_EEPROM_ReadWord(addr);
      addr++;
      data += 2;
      num -= 2;
    }
}

/// @brief 初始化, 检查主存储页与代码页是否冲突
/// @param None
/// @retval None
void FLASH_EEPROM_Config(void)
{
    uprintf(USART_USB, "%lu %08x\n", FLASH_SIZE, END_OF_CODE);
    // Todo: flash config (Get base addr, ...)
    if(END_OF_CODE >= PAGE_MAIN) {
      uprintf(USART_USB, "Program code exceeds!\n");
      while(1);
    }
}

kinsno 发表于 2019-1-10 20:54:22

tomzbj 发表于 2019-1-10 17:54
用st官方的不就行了, 写N次才擦除一次
我用的:
flash_eeprom.h


不听看明白这段话

“// 在bootloader里只能读到bootloader区的结尾, 因此这时END_OF_CODE的值并不正确.
// 不过bootloader区不会很长, 因此只要在APP区读到END_OF_CODE不冲突就可以了.”

愿听你的详解。。。

t3486784401 发表于 2019-1-10 21:24:37

话说 ST 缺少内置 EEPROM 也都是历史问题了

tomzbj 发表于 2019-1-10 21:48:03

kinsno 发表于 2019-1-10 20:54
不听看明白这段话

“// 在bootloader里只能读到bootloader区的结尾, 因此这时END_OF_CODE的值并不正确.


我这个是用倒数第三页作为主存储区, 倒数第二页作为后备区用来轮转 (最后一页可能会另作他用)。
所以在程序启动时要判断一下,实际有用的程序大小就是_etext加上_edata减去_sdata。如果.text和.data写到了倒数第三页, 则直接报错。
想想其实查一下倒数第三页是否全ff也就可以了。但是理论上确实有可能全ff但却是有用数据的情况,所以还是算了。

bootload一般也就几k,顶多十几k,肯定不会写到倒数第几页去(不然app往哪写),所以在bootloader里肯定不会报错。
一般应该也不会有在bootloader里写eeprom的需求,顶多读个参数之类,所以这里无所谓了。

tomzbj 发表于 2019-1-10 21:52:28

t3486784401 发表于 2019-1-10 21:24
话说 ST 缺少内置 EEPROM 也都是历史问题了

可能是工艺问题?STM32L系列就都有

t3486784401 发表于 2019-1-10 22:00:46

tomzbj 发表于 2019-1-10 21:52
可能是工艺问题?STM32L系列就都有

早期的 ST 目测是工艺/专利有问题,导致没法内嵌 EEPROM,这点比起 ATMEL 差很多(AVR标配EEP)。
FLASH 模拟的 EEPROM,只能说是个补丁,本质上是损耗均衡算法。

不过要是 EEPROM 也用损耗均衡算法,就远不是 FLASH 模拟能赶上的了。

meirenai 发表于 2019-1-10 22:23:27

最好不要用内部flash存数据了,会导致cpu暂停长达 几十毫秒 数量级。
有可能导致中断丢失。

tomzbj 发表于 2019-1-10 22:30:20

t3486784401 发表于 2019-1-10 22:00
早期的 ST 目测是工艺/专利有问题,导致没法内嵌 EEPROM,这点比起 ATMEL 差很多(AVR标配EEP)。
FLASH...

也就存储少量参数用,要存的东西多了还是得上外部eeprom
不过stm32f10x的i2c又不好用, 只能gpio模拟, 而且i2c eeprom写入还得延迟几个ms,也不爽

所以后来如果有外部spi flash,有文件系统的话,就直接在文件系统里写个文件来存储参数代替eeprom了。

tomzbj 发表于 2019-1-10 22:32:33

meirenai 发表于 2019-1-10 22:23
最好不要用内部flash存数据了,会导致cpu暂停长达 几十毫秒 数量级。
有可能导致中断丢失。 ...

这个还好, 可以接受
有个大坑是如果用dma+dac读取flash里的波形数据然后输出, 写flash的暂停会导致输出波形相位混乱。
不过解决办法也简单, 把波形数据放在sram里就行了,写flash过程中波形不会中断。

meirenai 发表于 2019-1-10 22:49:43

tomzbj 发表于 2019-1-10 22:32
这个还好, 可以接受
有个大坑是如果用dma+dac读取flash里的波形数据然后输出, 写flash的暂停会导致输出 ...

奥,理解错了。

t3486784401 发表于 2019-1-10 23:31:32

tomzbj 发表于 2019-1-10 22:30
也就存储少量参数用,要存的东西多了还是得上外部eeprom
不过stm32f10x的i2c又不好用, 只能gpio模拟,...

看来这个 i2c 不好用是实锤了,前阵子只是看到有人在说,猜测是:基本功能还行,抗干扰太差容易卡死。

看来 ST 还是欠一点,当初 atmel 卖给 PIC 的时候,ST 也只能眼巴巴瞅着

陶新成 发表于 2019-1-11 11:11:15

昨天程序经过测试发现在同时读写的情况下出现程序死机情况,因此有改进了一些,希望没有误导大家

#include "main.h"       

#define   QJ_BZ
#include "Global_Variable.h"

#include "stm32f4xx_it.h"
#include "stm32f4xx.h"
#include "stm32f4xx_flash.h"
#include "USART1.H"
#include "USART2.H"


unsigned char Timer_SendData_Flag;
unsigned char Timer_Collect_Flag;
unsigned char IWDG_Time_Conts;

int SCR1_Compensate;
int SCR2_Compensate;

unsigned char Rec_Flag;
unsigned char Rec_Buf;
///////////////////////////////////////////////////

unsigned int data ={0x71,0x12,0x23,0x54};
unsigned int data1={0x72,0x22,0x23,0x54};
unsigned int data2={0x73,0x32,0x23,0x54};
unsigned int data3={0x74,0x42,0x23,0x54};
unsigned int data4={0x75,0x52,0x23,0x54};
unsigned int data5={0x76,0x62,0x23,0x54};
unsigned int data6={0x77,0x72,0x23,0x54};
unsigned int data7={0x78,0x82,0x23,0x54};
unsigned int data8={0x79,0x92,0x23,0x54};

////////////////////////////////////////////////////////////////////////////////////////

//////////////
//用户根据自己的需要设置
#define STM32_FLASH_SIZE 512                       //所选STM32的FLASH容量大小(单位为K)
#define STM32_FLASH_WREN 1//使能FLASH写入(0,不是能;1,使能)
////////////////////////////////////////////////////////////////////////////////////////

//////////////
#if STM32_FLASH_SIZE<256
#define STM_SECTOR_SIZE 4096 //字节
#else
#define STM_SECTOR_SIZE        4096
#endif               
//FLASH起始地址
#define STM32_FLASH_BASE 0x08000000         //STM32 FLASH的起始地址
//FLASH解锁键值


        u32 mUseHalfWord;//
        u32 mStartAddress;//

        u32 startAddress=(0x08000000+1000);
        u32 useHalfWord=1;
//读取指定地址的半字(16位数据)
//faddr:读地址(此地址必须为2的倍数!!)
//返回值:对应数据.
u16 ReadHalfWord(u32 faddr);
//WriteAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数 ??
void Write_NoCheck(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite) ;
//从指定地址开始写入指定长度的数据
//WriteAddr:起始地址(此地址必须为2的倍数!!)
//pBuffer:数据指针
//NumToWrite:半字(16位)数(就是要写入的16位数据的个数.)
void Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite);


//从指定地址开始读出指定长度的数据
//ReadAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数
void Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead) ;


u16 STMFLASH_BUF;//最多是2K字节
void STFLASH(uint32_t startAddress,u32 useHalfWord)
{
        if(startAddress%STM_SECTOR_SIZE!=0)//不是页的开始,将开始处设置为下一个页开始的地


                startAddress+=(STM_SECTOR_SIZE-(startAddress%STM_SECTOR_SIZE));
        mStartAddress=startAddress;
        mUseHalfWord=useHalfWord;
}

//读取指定地址的半字(16位数据)
//faddr:读地址(此地址必须为2的倍数!!)
//返回值:对应数据.

u16ReadHalfWord(u32 faddr)
{
        return *(vu16*)faddr;
}
//WriteAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数   
void Write_NoCheck(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)
{
       
        u16 i;
        for(i=0;i<NumToWrite;i++)
        {
                FLASH_ProgramHalfWord(WriteAddr,pBuffer);
          WriteAddr+=2;//地址增加2.
        }
}
//从指定地址开始写入指定长度的数据
//WriteAddr:起始地址(此地址必须为2的倍数!!)
//pBuffer:数据指针
//NumToWrite:半字(16位)数(就是要写入的16位数据的个数.)
void Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)
{
        u32 secpos;           //扇区地址
        u16 secoff;           //扇区内偏移地址(16位字计算)
        u16 secremain; //扇区内剩余地址(16位字计算)          
        u16 i;   
        u32 offaddr;   //去掉0X08000000后的地址
        if(WriteAddr<STM32_FLASH_BASE||(WriteAddr>=(STM32_FLASH_BASE

+1024*STM32_FLASH_SIZE)))return;//非法地址
        FLASH_Unlock();                                                //解锁
        offaddr=WriteAddr-STM32_FLASH_BASE;                //实际偏移地址.
        secpos=offaddr/STM_SECTOR_SIZE;                        //扇区地址0~127 for

STM32F103RBT6
        secoff=(offaddr%STM_SECTOR_SIZE)/2;                //在扇区内的偏移(2个字节为基本单

位.)
        secremain=STM_SECTOR_SIZE/2-secoff;                //扇区剩余空间大小   
        if(NumToWrite<=secremain)secremain=NumToWrite;//不大于该扇区范围
        while(1)
        {        /*
                Read(secpos*STM_SECTOR_SIZE

+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//读出整个扇区的内容
                for(i=0;i<secremain;i++)//校验数据
                {
                        if(STMFLASH_BUF!=0XFFFF)break;//需要擦除          
                }
                if(i<secremain)//需要擦除
                {
                        FLASH_ErasePage(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE);//擦除

这个扇区
                        for(i=0;i<secremain;i++)//复制
                        {
                                STMFLASH_BUF=pBuffer;          
                        }
                        Write_NoCheck(secpos*STM_SECTOR_SIZE

+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//写入整个扇区
                }else */Write_NoCheck(WriteAddr,pBuffer,secremain);//写已经擦除了的,直接

写入扇区剩余区间.                                   
                if(NumToWrite==secremain)break;//写入结束了
                else//写入未结束
                {
                        secpos++;                                //扇区地址增1
                        secoff=0;                                //偏移位置为0        
                           pBuffer+=secremain;        //指针偏移
                        WriteAddr+=secremain;        //写地址偏移          
                           NumToWrite-=secremain;        //字节(16位)数递减
                        if(NumToWrite>(STM_SECTOR_SIZE/2))secremain=STM_SECTOR_SIZE/2;//

下一个扇区还是写不完
                        else secremain=NumToWrite;//下一个扇区可以写完了
                }       
        };       
        FLASH_Lock();//上锁
}

//从指定地址开始读出指定长度的数据
//ReadAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数
void Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead)
{
        u16 i;
        for(i=0;i<NumToRead;i++)
        {
                pBuffer=ReadHalfWord(ReadAddr);//读取2个字节.
                ReadAddr+=2;//偏移2个字节.       
        }
}
const u8 TEXT_Buffer[]={"Flash_test"};

#define SIZE sizeof(TEXT_Buffer)                //数组长度
#define FLASH_SAVE_ADDR0X0800FC00                //设置FLASH 保存地址(必须为偶数,且其值

要大于本代码所占用FLASH的大小+0X08000000)
u8 datatemp;


///////////////////////////////////////////////////
int main(void)
{
       unsigned char sendconts,senddat;
       unsigned int tab1;
       SystemInit();
       USART1_Configuration(115200);
       NVIC_USART1_Configuration();
       
       //Write(FLASH_SAVE_ADDR,(u16*)TEXT_Buffer,SIZE);//写数据,第一次下载程序到32,

第二次注释掉此行,断电重新编译下载 //keil watch查看datatemp数组的数据正是之前写进去的数



               
                Write(FLASH_SAVE_ADDR,(u16*)data,4);
                Write(FLASH_SAVE_ADDR+100,(u16*)data1,4);
                Write(FLASH_SAVE_ADDR+200,(u16*)data2,4);
                Write(FLASH_SAVE_ADDR+300,(u16*)data3,4);
                Write(FLASH_SAVE_ADDR+400,(u16*)data4,4);
                Write(FLASH_SAVE_ADDR+500,(u16*)data5,4);
                Write(FLASH_SAVE_ADDR+600,(u16*)data6,4);
                Write(FLASH_SAVE_ADDR+700,(u16*)data7,4);
                Write(FLASH_SAVE_ADDR+800,(u16*)data8,4);
               
       while(1)
       {
                       
                       for(i=0;i<9;i++)
                       {
                               Read(FLASH_SAVE_ADDR+i*100,(u16*)tab1,4);
                       senddat = tab1;
                       senddat = tab1;
                       senddat = tab1;
                       senddat = tab1;
                       senddat = senddat+senddat+senddat+senddat;
                       USART_Byte_Send(USART1, senddat);
                       USART_Byte_Send(USART1, senddat);
                       USART_Byte_Send(USART1, senddat);
                       USART_Byte_Send(USART1, senddat);
                       USART_Byte_Send(USART1, senddat);
                       USART_Byte_Send(USART1, senddat);
                       USART_Byte_Send(USART1, senddat);
                       USART_Byte_Send(USART1, senddat);
                         delay_ms(5000);                       
                       }
   }
}
   



yunqing_abc 发表于 2019-1-11 13:46:31

mark.flash 损耗均衡算法

yanyanyan168 发表于 2019-1-11 17:26:31

mark.flash 损耗均衡算法+1

justdomyself 发表于 2019-1-12 07:28:23

这个好…………

cl1cl1cl1cl1 发表于 2019-1-12 08:45:48

eeprom写flash 均衡算法
页: [1]
查看完整版本: 复习老掉牙的知识-STM32F407实现FLASH模拟EEPROM