【正点原子STM32连载】 第四十五章 FLASH模拟EEPROM实验 摘自【正点原子】STM32F103 战舰开发指南V1.2

第四十五章 FLASH模拟EEPROM实验

STM32本身没有自带EEPROM,但是STM32具有IAP(在应用编程)功能,所以我们可以把它的FLASH当成EEPROM来使用。本章,我们将利用STM32内部的FLASH来实现第三十六章实验类似的效果,不过这次我们是将数据直接存放在STM32内部,而不是存放在NOR FLASH。
本章分为如下几个小节:
45.1 STM32 FLASH简介
45.2 硬件设计
45.3 软件设计
45.4 下载验证

45.1 STM32 FLASH简介

不同型号的STM32,其FLASH容量也有所不同,最小的只有16K字节,最大的则达到了1024K字节。战舰开发板选择的是STM32F103ZET6,其FLASH容量为512K字节,属于大容量产品(另外还有中容量和小容量产品),大容量产品的闪存模块组织如表45.1.1所示:
在这里插入图片描述

表45.1.1 大容量产品闪存模块组织表
STM32的闪存模块由主存储器、信息块和闪存存储器接口寄存器等3部分组成。
主存储器,该部分用来存放代码和数据常数(如const类型的数据)。对于大容量产品,其被划分为256页,每一页2K字节(注意:小容量和中容量产品每页只有1K字节)。从上表可以看出主存储器的起始地址就是0x08000000,B0、B1都接GND的时候,就是从0x08000000开始运行代码的。
信息块,该部分分为2个小部分,其中启动程序代码,用来存储ST自带的启动程序,用来串口下载代码,当B0接3V3,B1接GND的时候,运行的就是这部分代码。用户选中字节,则一般用于配置写保护、读保护等功能,本章不作介绍了。
闪存存储器接口寄存器,该部分用于控制闪存读写等,是整个闪存模块的控制结构。
对主存储器和信息块的写入由内嵌的闪存编程/擦除控制器(FPEC)管理;编程与擦除的高电压由内部产生。
在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正确地进行。既在进行写或擦除操作时,不能进行代码或数据的读取操作。
45.1.1 闪存的读取
内置闪存模块可以在通用地址空间直接寻址,任何32位数据的读操作都能访问闪存模块的内容并得到相应的数据。读接口在闪存端包含一个读控制器,还包含一个AHB接口与CPU衔接。这个接口的主要工作是产生读内存的控制信号并预取CPU要求的指令块,预取指令块仅用于在I-Code总线上的取指操作,数据常量是通过D-Code总线访问的。这两条总线的访问目标是相同的闪存模块,访问D-Code将比预取指令优先级高。
这里要特别留意一个闪存等待时间,因为CPU运行速度比FLASH快得多,STM32F103的FLASH最快访问速度≤24Mhz,如果CPU频率超过这个速度,那么必须加入等待时间,比如我们一般使用72Mhz的主频,那么FLASH等待周期就必须设置为2,该设置通过FLASH_ACR寄存器设置。
例如,我们要从地址addr,读取一个半字(半字为16位,字为32位),可以通过如下的语句读取:
data = (vu16)addr;
将addr强制转换为vu16指针,然后取该指针所指向的地址的值,即得到了addr地址的值。类似的,将上面的vu16改为vu8,即可读取指定地址的一个字节。相对FLASH读取来说,STM32 FLASH的写就复杂一点了。下面我们介绍STM32闪存的编程和擦除。
45.1.2 闪存的编程和擦除
STM32的闪存编程是由FPEC(闪存编程和擦除控制器)模块处理的,这个模块包含7个32位寄存器,它们分别是:
FPEC键寄存器(FLASH_KEYR)
选择字节键寄存器(FLASH_OPTKEYR)
闪存控制寄存器(FLASH_CR)
闪存状态寄存器(FLASH_SR)
闪存地址寄存器(FLASH_AR)
选择字节寄存器(FLASH_WRPR)
其中FPEC键寄存器总共有3个键值:
RDPRT键 = 0X0000 00A5
KEY1 = 0X4567 0123
KEY2 = 0XCDEF 89AB
STM32复位后,FPEC模块是被保护的,不能写入FLASH_CR寄存器;通过写入特定的序列到FLASH_KEYR寄存器可以打开FPEC模块(即写入KEY1和KEY2),只有在写保护被解除后,我们才能操作相关寄存器。
STM32闪存的编程每次必须写入16位(不能单纯的写入8位数据),当FLASH_CR寄存器的PG位为‘1’时,在一个闪存地址写入一个半字将启动一次编程;写入任何非半字的数据,FPEC都会产生总线错误。在编程过程中(BSY位为’1’),任何读写内存的操作都会使CPU暂停,直到此次闪存编程结束。
同样,STM32的FLASH在编程的时候,也必须要求其写入地址的FLASH是被擦除了的(其值必须是0xFFFF),否则无法写入,在FLASH_SR寄存器的PGERR位将得到一个警告。
STM32的FLASH编程过程如图45.1.2.1所示:
在这里插入图片描述

图45.1.2.1 STM32闪存编程过程
从上图可以得到闪存的编程顺序如下:
1)检查FLASH_CR的LOCK是否解锁,如果没有则先解锁
2)检查FLASH_SR寄存器的BSY位,以确认没有其他正在进行的编程操作
3)设置FLASH_CR寄存器的PG位为‘1’
4)在指定的地址写入要编程的半字
5)等待BSY位变为‘0’
6)读出写入地址并验证数据
前面提到,我们在STM32的FLASH编程的时候,要先判断缩写地址是否被擦出了,所以,我们有必要再介绍一下STM32的闪存擦除,STM32的闪存擦除分为两种:页擦除和整片擦除。页擦除过程如图45.1.2.2所示:
在这里插入图片描述

图45.1.2.2 STM32闪存页擦除过程
从上图可以看出,STM32的页擦除顺序为:
1)检查FLASH_CR和LOCK是否解锁,如果没有则先解锁
2)检查FLASH_SR寄存器的BSY位,以确认没有其他正在进行的闪存操作
3)设置FLASH_CR寄存器的PER位为‘1’
4)用FLASH_AR寄存器选择要擦除的页
5)设置FLASH_CR寄存器的STRT位为‘1’
6)等待BSY位变为‘0’
7)读出被擦除的页并做验证
本章我们只用到了STM32页擦除功能,整片擦除功能我们在这里就不介绍了。
45.1.3 FLASH寄存器
通过上面的讲解,我们基本对STM32闪存的读写执行步骤有所了解。接下来,我们介绍本实验需要用到的一些FLASH寄存器。
 FPEC键寄存器(FLASH_KEYR)
FPEC键寄存器描述如图45.1.3.2所示:
在这里插入图片描述

图45.1.3.2 FLASH_KEYR寄存器
该寄存器主要用来解锁FPEC,必须在该寄存器写入特定的序列(KEY1和KEY2)解锁后,才能对FLASH_CR寄存器进行写操作。
 FLASH控制寄存器(FLASH_CR)
FLASH控制寄存器描述如图45.1.3.3所示:
在这里插入图片描述

图45.1.3.3 FLASH_CR寄存器
该寄存器我们本章只用到了它的LOCK、STRT、PER和PG等4个位。
LOCK位,该位用于指示FLASH_CR寄存器是否被锁住,该位在检测到正确的解锁序列后,硬件将其清零。在一次不成功的解锁操作后,在下次系统复位之前,该位将不再改变。
STRT位,该位用于开始一次擦除操作。在该位写入1,将执行一次擦除操作。
PER位,该位用于选择页擦除操作,在页擦除的时候,需要将该位置1。
PG位,该位用于选择编程操作,在往FLASH写数据的时候,该位需要置1。
其他位,我们就不在这里介绍了,请大家参考《STM32F10xxx闪存编程参考手册》。
 闪存状态寄存器(FLASH_SR)
闪存状态寄存器描述如图45.1.3.4所示:
在这里插入图片描述

图45.1.3.4 FLASH_SR寄存器
该寄存器主要用来指示当前FPEC的操作编程状态。由于寄存器中描述比较详细,这里就不重复了。
 闪存地址寄存器(FLASH_AR)
闪存地址寄存器描述如图45.1.3.5所示:
在这里插入图片描述

图45.1.3.5 FLASH_AR寄存器
该寄存器在本章,我们主要用来设置要擦除的页。
关于STM32 FLASH的介绍,我们就介绍到这里。更详细的介绍,可以参考《STM32F10xxx闪存编程参考手册》。
45.2 硬件设计

  1. 例程功能
    按键KEY1控制写入FLASH的操作,按键KEY0控制读出操作,并在TFTLCD模块上显示相关信息,还可以借助USMART进行读取或者写入操作。LED0闪烁用于提示程序正在运行。
  2. 硬件资源
    1)LED灯
    LED0 – PB5
    2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
    3)正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
    4)独立按键
    KEY0 – PE4 KEY1 – PE3
    45.3 程序设计
    45.3.1 FLASH的HAL库驱动
    FLASH在HAL库中的驱动代码在stm32f1xx_hal_flash.c和stm32f1xx_hal_flash_ex.c文件(及其头文件)中。
  3. HAL_FLASH_Unlock函数
    解锁闪存控制寄存器访问的函数,其声明如下:
    HAL_StatusTypeDef HAL_FLASH_Unlock(void);
    函数描述:
    用于解锁闪存控制寄存器的访问,在对FLASH进行写操作前必须先解锁,解锁操作也就是必须在FLASH_KEYR寄存器写入特定的序列(KEY1和KEY2)。
    函数形参:

    函数返回值:
    HAL_StatusTypeDef枚举类型的值。
  4. HAL_FLASH_Lock函数
    锁定闪存控制寄存器访问的函数,其声明如下:
    HAL_StatusTypeDef HAL_FLASH_Lock (void);
    函数描述:
    用于锁定闪存控制寄存器的访问。
    函数形参:

    函数返回值:
    HAL_StatusTypeDef枚举类型的值。
  5. HAL_FLASH_Program函数
    闪存写操作函数,其声明如下:
    HAL_StatusTypeDef HAL_FLASHEx_Program(uint32_t TypeProgram, uint32_t Address,
    uint64_t Data);
    函数描述:
    该函数用于FLASH的写入。
    函数形参:
    形参1是TypeProgram用来区分要写入的数据类型,取值可为字节、半字、字和双字,用户根据写入数据类型选择即可。
    形参2是Address用来设置要写入数据的FLASH地址。
    形参3是Data是要写入的数据类型。该参数默认64位,如果你要写入小于64位的数据,比如16位,程序会进行类型转换。
    函数返回值:
    HAL_StatusTypeDef枚举类型的值。
  6. HAL_FLASHEx_Erase函数
    闪存擦除函数,其声明如下:
    HAL_StatusTypeDef HAL_FLASHEx_Erase(FLASH_EraseInitTypeDef *pEraseInit,
    uint32_t *SectorError);
    函数描述:
    该函数用于大量擦除或擦除指定的闪存扇区。
    函数形参:
    形参1是FLASH_EraseInitTypeDef结构体类型指针变量。
typedef struct
{
  uint32_t TypeErase;  		/* 擦除类型(Page擦除 / BANK级别批量擦除) */
  uint32_t Banks;        	/* 擦除的Bank编号(批量擦除时才有效) */    
  uint32_t PageAddress; 	/* 擦除页面地址 */
  uint32_t NbPages;      	/* 擦除的页面数 */
} FLASH_EraseInitTypeDef;

成员变量TypeErase用来设置擦除类型,是page擦除还是BANK级别的批量擦除,取值为FLASH_TYPEERASE_PAGES或者FLASH_TYPEERASE_MASSERASE,这个比较好理解,如果一次擦除一个Bank下面的所有Page,那么需要选择FLASH_TYPEERASE_MASSERASE。成员变量Banks用来设置要擦除的Bank编号,这个只有设置为批量擦除的时候才有效。成员变量PageAddress用来设置要擦除页面的地址。成员变量NbPages用来设置要擦除的页面数。
形参2是uint32_t类型指针变量,存放错误码,0xFFFFFFFF值表示扇区已被正确擦除,其它值表示擦除过程中的错误扇区。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
5. FLASH_WaitForLastOperation函数
等待FLASH操作完成函数,其声明如下:
HAL_StatusTypeDef FLASH_WaitForLastOperation(uint32_t Timeout);
函数描述:
该函数用于等待FLASH操作完成。
函数形参:
形参1是FLASH操作超时时间。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
45.3.2 程序流程图
在这里插入图片描述

图45.3.2.1 FLASH模拟EEPROM实验程序流程图
45.3.3 程序解析

  1. STM FLASH驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。STM FLASH驱动源码包括两个文件:stmflash.c和stmflash.h。
    stmflash.h头文件做了一些比较重要的宏定义,定义如下:
/* FLASH起始地址 */
#define STM32_FLASH_BASE        0x08000000  		/* STM32 FLASH 起始地址 */
#define STM32_FLASH_SIZE        0x80000         	/* STM32 FLASH 总大小 */
/* STM32F103扇区大小 */
#if STM32_FLASH_SIZE < 256 * 1024
#define STM32_SECTOR_SIZE  1024		/* 容量小于256K的F103, 扇区大小为1K字节 */
#else
#define STM32_SECTOR_SIZE  2048		/* 容量大于等于256K的F103, 扇区大小为2K字节 */
#endif

STM32_FLASH_BASE和STM32_FLASH_SIZE分别是FLASH的起始地址和FLASH总大小,这两个宏定义随着芯片是固定的,我们战舰开发板的F103芯片FLASH是512K字节,所以STM32_FLASH_SIZE宏定义值为0x80000。
下面我们开始介绍stmflash.c的程序,下面先介绍一下stmflash写操作函数,源码如下:

/**
 * @brief     	在FLASH 指定位置, 写入指定长度的数据(自动擦除)
 * @note     	该函数往 STM32 内部 FLASH 指定位置写入指定长度的数据
 *              	该函数会先检测要写入的扇区是否是空(全0XFFFF)的?, 如果
 *             	不是, 则先擦除, 如果是, 则直接往扇区里面写入数据.
 *              	数据长度不足扇区时,自动被回擦除前的数据
 * @param     	waddr  	: 起始地址 (此地址必须为2的倍数!!,否则写入出错!)
 * @param     	pbuf   	: 数据指针
 * @param      	length 	: 要写入的 半字(16位)数
 * @retval    	无
 */
uint16_t g_flashbuf[STM32_SECTOR_SIZE / 2]; /* 最多是2K字节 */
void stmflash_write(uint32_t waddr, uint16_t *pbuf, uint16_t length)
{
    uint32_t secpos;    		/* 扇区地址 */
    uint16_t secoff;    		/* 扇区内偏移地址(16位字计算) */
    uint16_t secremain; 		/* 扇区内剩余地址(16位字计算) */
    uint16_t i;
    uint32_t offaddr;   		/* 去掉0X08000000后的地址 */
    FLASH_EraseInitTypeDef flash_eraseop;
uint32_t erase_addr;   	/* 擦除错误,这个值为发生错误的扇区地址 */

   if(waddr<STM32_FLASH_BASE||(waddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))
    {
        return; 				/* 非法地址 */
}

HAL_FLASH_Unlock();		/* FLASH解锁 */

    offaddr = waddr - STM32_FLASH_BASE;     		/* 实际偏移地址 */
    secpos = offaddr / STM32_SECTOR_SIZE;		/* 得到扇区编号 */
    secoff = (offaddr % STM32_SECTOR_SIZE) / 2;	/* 在扇区内的偏移(2B为基本单位) */
secremain = STM32_SECTOR_SIZE / 2 - secoff;	/* 扇区剩余空间大小 */

    if (length <= secremain)
    {
        secremain = length;	/* 不大于该扇区范围 */
}

    while (1)
    {
        stmflash_read(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE,
 g_flashbuf, STM32_SECTOR_SIZE / 2);	/* 读出整个扇区的内容 */

        for (i = 0; i < secremain; i++)		/* 校验数据 */
        {
            if (g_flashbuf[secoff + i] != 0XFFFF)
            {
                break; 						/* 需要擦除 */
            }
        }

        if (i < secremain) 					/* 需要擦除 */
        { 
            flash_eraseop.TypeErase = FLASH_TYPEERASE_PAGES;	/* 选择页擦除 */
            flash_eraseop.NbPages = 1;						/* 要擦除的页数 */
            flash_eraseop.PageAddress = secpos * STM32_SECTOR_SIZE +  
STM32_FLASH_BASE;	/* 要擦除的起始地址 */
            HAL_FLASHEx_Erase(&flash_eraseop, &erase_addr);

            for (i = 0; i < secremain; i++)         		     /* 复制 */
            {
                g_flashbuf[i + secoff] = pbuf[i];
            }
            stmflash_write_nocheck(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE, 
   g_flashbuf, STM32_SECTOR_SIZE / 2); /* 写入整个扇区 */
        }
        else
        {	   /* 写已经擦除了的,直接写入扇区剩余区间 */
            stmflash_write_nocheck(waddr, pbuf, secremain); 
        }

        if (length == secremain)
        {
            break; 					/* 写入结束了 */
        }
        else       					/* 写入未结束 */
        {
            secpos++;             		/* 扇区地址增1 */
            secoff = 0;             	/* 偏移位置为0 */
            pbuf += secremain;      	/* 指针偏移 */
            waddr += secremain * 2; 	/* 写地址偏移(16位数据地址,需要*2) */
            length -= secremain;    	/* 字节(16位)数递减 */

            if (length > (STM32_SECTOR_SIZE / 2))
            {
                secremain = STM32_SECTOR_SIZE / 2;	/* 下一个扇区还是写不完 */
            }
            else
            {
                secremain = length; 	/* 下一个扇区可以写完了 */
            }
        }
}
    HAL_FLASH_Lock(); 				/* 上锁 */
}

该函数用于在STM32的指定地址写入指定长度的数据。函数的实现基本类似SPI章节的norflash_write函数,不过该函数对于写入地址是有要求,必须保证以下两点:
1、写入地址必须是用户代码区以外的地址。
2、写入地址必须是2的倍数。
第1点比较好理解,如果把用户代码给擦了,可想而知你运行的程序可能就被废了,从而很可能出现死机的情况。第2点则是STM32 FLASH的要求,每次必须写入16位,如果你写的地址不是2的倍数,那么写入的数据,可能就不是写在你要写的地址了。
另外,该函数的g_flashbuf数组,也是根据所用STM32的FLASH容量来确定的,战舰STM32开发板的FLASH是512K字节,所以STM_SECTOR_SIZE的值为2048,故该数组大小为2K字节。
stmflash_write函数实质是调用stmflash_write_nocheck函数进行实现,下面再来看一下stmflash_write函数代码,其代码如下:

/**
 * @brief     	不检查的写入
              	这个函数的假设已经把原来的扇区擦除过再写入
 * @param     	waddr  	: 起始地址 (此地址必须为2的倍数!!,否则写入出错!)
 * @param     	pbuf   	: 数据指针
 * @param   		length 	: 要写入的 半字(16位)数
 * @retval    	无
 */
void stmflash_write_nocheck(uint32_t waddr, uint16_t *pbuf, uint16_t length)
{
uint16_t i;

    for (i = 0; i < length; i++)
    {
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, waddr, pbuf[i]);
        waddr += 2; 		/* 指向下一个半字 */
    }
}

该函数的实现依靠flash的HAL库驱动HAL_FLASH_Program进行实现。由于前面已经对HAL_FLASH_Program进行说明,这里就不作展开说明了。
接下来,讲解一下STM FLASH读相关的函数,写函数也有调用到读函数,其代码如下:

/**
 * @brief      	从指定地址读取一个半字 (16位数据)
 * @param      	faddr   : 读取地址 (此地址必须为2的倍数!!)
 * @retval     	读取到的数据 (16位)
 */
uint16_t stmflash_read_halfword(uint32_t faddr)
{
    return *(volatile uint16_t *)faddr;
}

/**
 * @brief     	从指定地址开始读出指定长度的数据
 * @param     	raddr : 起始地址
 * @param     	pbuf  : 数据指针
 * @param      	length: 要读取的半字(16位)数,即2个字节的整数倍
 * @retval      	无
 */
void stmflash_read(uint32_t raddr, uint16_t *pbuf, uint16_t length)
{
uint16_t i;

    for (i = 0; i < length; i++)
    {
        pbuf[i] = stmflash_read_halfword(raddr); 	/* 读取2个字节 */
        raddr += 2; 									/* 偏移2个字节 */
    }
}

前面也提及到STM32对FLASH写入,其写入地址的值必须是0xFFFFFFFF,所以读函数主要是读取地址的值,以给写函数调用检验,确保能写入成功。读函数实现比较简单,这里就不做展开了。
2. main.c代码
在main.c里面编写如下代码:
const uint8_t g_text_buf[] = {“STM32 FLASH TEST”}; /* 要写入的FLASH字符串数组 */

#define TEXT_LENTH sizeof(g_text_buf) 				/* 数组长度 */

/* SIZE表示半字长(2字节), 大小必须是2的整数倍, 如果不是的话, 强制对齐到2的整数倍 */
#define SIZE TEXT_LENTH / 2 + ((TEXT_LENTH % 2) ? 1 : 0)

/* 设置FLASH 保存地址(必须为偶数,且其值要大于本代码所占用FLASH的大小 + 0X08000000) */
#define FLASH_SAVE_ADDR 0X08070000 

int main(void)
{
    uint8_t key = 0;
    uint16_t i = 0;
uint8_t datatemp[SIZE];

    HAL_Init();                          		/* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);		/* 设置时钟, 72Mhz */
    delay_init(72);                        		/* 延时初始化 */
    usart_init(115200);                  		/* 串口初始化为115200 */
    usmart_dev.init(72);                		/* 初始化USMART */
    led_init();                            		/* 初始化LED */
    lcd_init();                             		/* 初始化LCD */
key_init();                            		/* 初始化按键 */

    lcd_show_string(30,  50, 200, 16, 16, "STM32", RED);
    lcd_show_string(30,  70, 200, 16, 16, "FLASH EEPROM TEST", RED);
    lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "KEY1:Write  KEY0:Read", RED);

    while (1)
    {
        key = key_scan(0);

        if (key == KEY1_PRES) 	/* KEY1按下,写入STM32 FLASH */
        {
            lcd_fill(0, 150, 239, 319, WHITE);
            lcd_show_string(30, 160, 200, 16, 16, "Start Write FLASH....", RED);
            stmflash_write(FLASH_SAVE_ADDR, (uint16_t *)g_text_buf, SIZE);
            lcd_show_string(30, 150, 200, 16, 16, "FLASH Write Finished!", RED);
        }

        if (key == KEY0_PRES)	/* KEY0按下,读取字符串并显示 */
        {
            lcd_show_string(30, 150, 200, 16, 16, "Start Read FLASH.... ", RED);
            stmflash_read(FLASH_SAVE_ADDR, (uint16_t *)datatemp, SIZE);
            lcd_show_string(30, 150, 200, 16, 16, "The Data Readed Is:  ", RED);
            lcd_show_string(30, 170, 200, 16, 16, (char *)datatemp, BLUE);
        }

        i++;
        delay_ms(10);

        if (i == 20)
        {
            LED0_TOGGLE();		/* 提示系统正在运行 */
            i = 0;
        }
    }
}

主函数代码逻辑比较简单,当检测到按键KEY1按下后往FLASH指定地址开始的连续地址空间写入一段数据,当检测到按键KEY0按下后读取FLASH指定地址开始的连续空间数据。
最后,我们将stmflash_read_word和test_write函数加入USMART控制,这样,我们就可以通过串口调试助手,调用STM32F103的FLASH读写函数,方便测试。
45.4 下载验证
将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示的内容如图45.4.1所示:
在这里插入图片描述

图45.4.1程序运行效果图
通过先按KEY1按键写入数据,然后按KEY0读取数据,得到如图45.4.2所示:
在这里插入图片描述

图45.4.2 操作后的显示效果图
本实验的测试,我们还可以借助USMART,调用:stmflash_read_word和test_write函数进行测试!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/33312.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MVTEC 3D dataset

官网&#xff1a;https://www.mvtec.com/company/research/datasets/mvtec-3d-ad/downloads https://www.mvtec.com/company/research/datasets/mvtec-3d-adhttps://www.mvtec.com/company/research/datasets/mvtec-3d-ad 数据大小&#xff1a;13个G 1. 介绍 MVTec 3D异常检测…

OpenCV 学习笔记(C++)(1.4W字)

一切图像皆Mat OpenCV中图像对象的创建与复制 Mat基本结构 Mat对象数据组成&#xff1a;头部和数据部分&#xff0c;头部存储图像的属性&#xff08;大小、宽高、图像类型&#xff1a;浮点数类型、字节类型、16位整型、32位整型、双精度浮点型&#xff0c;通道数量和获取途径…

【Soft-prompt Tuning for Large Language Models to Evaluate Bias 论文略读】

Soft-prompt Tuning for Large Language Models to Evaluate Bias 论文略读 INFORMATIONAbstract1 Introduction2 Related work3 Methodology3.1 Experimental setup 4 Results5 Discussion & Conclusion总结A Fairness metricsB Hyperparmeter DetailsC DatasetsD Prompt …

【CSS3系列】第八章 · 伸缩盒模型

写在前面 Hello大家好&#xff0c; 我是【麟-小白】&#xff0c;一位软件工程专业的学生&#xff0c;喜好计算机知识。希望大家能够一起学习进步呀&#xff01;本人是一名在读大学生&#xff0c;专业水平有限&#xff0c;如发现错误或不足之处&#xff0c;请多多指正&#xff0…

服务器配置与操作

服务器配置与操作 一、连接远程服务器 推荐用xshell 或者 finalshell 或者 winSCP 或者 FileZilla xshell下载地址&#xff1a;https://xshell.en.softonic.com/ 二、服务器配置 2.1 安装JDK 2.1 方法一&#xff1a;在线安装 yum list java* yum -y install java-1.8.0-ope…

利用jmeter测试java请求

jmeter和loadrunner一样包含了测试脚本开发、测试执行、以及测试结果统计三个部分。只是jmeter没有脚本开发工具&#xff0c;因此测试java请求的脚本选择在eclipse中进行。 首先介绍如何用eclipse编写接口性能测试脚本。 针对"Java请求"类型的测试&#xff0c;需要…

系列五、NotePad++下载安装

一、下载 链接&#xff1a;https://pan.baidu.com/s/1U2f74vfBJIds7W2wJYnBxg?pwdyyds 提取码&#xff1a;yyds 二、安装 2.1、安装NotePad 解压NotePad-x64.zip至指定目录即可&#xff0c;例如 2.2、安装NppFTP 2.2.1、查看NotePad对应的位数&#xff08;32位or64位&a…

文本分析-使用jieba库实现TF-IDF算法提取关键词

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

程序员找工作难!拿到外包公司的 offer 我应该去么?

引言 前一阵子有一个帖子引起了非常广泛的讨论&#xff0c;描述的就是一个公司的外包工作人员&#xff0c;加班的时候因为吃了公司给员工准备的零食,被公司的HR当场批评&#xff01;这个帖子一发出来&#xff0c;让现在测试行业日益新增的外包公司备受关注。那么外包公司和非外…

驱动开发:内核读写内存多级偏移

让我们继续在《内核读写内存浮点数》的基础之上做一个简单的延申&#xff0c;如何实现多级偏移读写&#xff0c;其实很简单&#xff0c;读写函数无需改变&#xff0c;只是在读写之前提前做好计算工作&#xff0c;以此来得到一个内存偏移值&#xff0c;并通过调用内存写入原函数…

【RF-SSA-LSTM】随机森林-麻雀优化算法优化时间序列预测研究(Python代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

JavaScript内存管理和闭包

1 JavaScript内存管理 2 垃圾回收机制算法 3 闭包的概念理解 4 闭包的形成过程 5 闭包的内存泄漏 一个函数只有调用了外部的变量&#xff0c;才算是闭包。函数内和函数外会写成闭包。 深入JS闭包-闭包的访问过程 <!DOCTYPE html> <html lang"en"> &l…

海气相互作用 - 全球水循环过程及其量级

全球水循环过程及其量级 单位&#xff1a;Sv106m3/s&#xff0c;大气/陆地/海洋(103 km3)径流1.3 Sv≈台湾暖流1.1 Sv≈白令海峡0.9-1.1 Sv 从涡度平衡的角度说明为什么大洋强化发生在西边界而非东边界 有且只有在大洋西边界强化&#xff0c;才可以使得摩擦力产生一个正的涡…

pytorch搭建AlexNet网络实现花分类

pytorch搭建AlexNet网络实现花分类 一、AlexNet网络概述分析 二、数据集准备下载划分训练集和测试集 三、代码model.pytrain.pypredict.py 一、AlexNet网络 概述 使用Dropout的方式在网络正向传播过程中随机失活一部分神经元&#xff0c;以减少过拟合 分析 对其中的卷积层、…

Spring Bean的生命周期解读

目录 1. Spring IOC容器 1.1 Spring IOC 容器的设计 1.1.1 BeanFactory 1.1.2 ApplicationContext 1.2 Spring Bean的生命周期 1.2.1 BeanDefinition 1.2.2 InstantiationAwareBeanPostProcessor和BeanPostProcessor 1.2.3 测试生命周期 1. Spring IOC容器 1.1 Spring …

数据库信息速递 DataStax与谷歌合作将向NoSQL AstraDB引入向量搜索技术

开头还是介绍一下群&#xff0c;如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题&#xff0c;有需求都可以加群群内有各大数据库行业大咖&#xff0c;CTO&#xff0c;可以解决你的问题。加群请联系 liuaustin3 &#xff0c;在新加的朋友会分到2群&#xff08;共…

2023-06-23:redis中什么是缓存击穿?该如何解决?

2023-06-23&#xff1a;redis中什么是缓存击穿&#xff1f;该如何解决&#xff1f; 答案2023-06-23&#xff1a; 缓存击穿是指一个缓存中的热点数据非常频繁地被大量并发请求访问&#xff0c;当该热点数据失效的瞬间&#xff0c;持续的大并发请求无法通过缓存获取到数据&…

数学建模-数据的处理

MATLAB数学建模方法与实践&#xff08;第3版&#xff09;——读书笔记 数据的准备数据获取数据处理缺失值处理噪音过滤数据集成数据归约数据变换标准化离散化 数据统计基本描述性统计分布描述性统计 数据可视化数据降维主成分分析&#xff08;PCA&#xff09;相关系数降维 数据…

【数据结构】排序

插入排序 把当前遍历到的元素前的元素序列是排好序的,把当前元素放到前边的序列中进行排序。 直接插入排序 不带哨兵 void InsertSort(int A[],int n) { int i,j,temp; for(i1;i<n;i) if(A[i]<A[i-1]) { tempA[i]; for(ji-1;j>0 && A[j]>temp;--j) A[j…

网络安全系统教程+学习路线(自学笔记)

一、什么是网络安全 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“安全运维”则研究防御技术。 无论网络、Web、移动、桌面、云等哪个领域&#xff0c;都有攻与防两面…