FLASH模擬EEPROM實驗

2020-08-14 19:09:35

一、簡介

STM32F407ZGT6 的 FLASH 容量爲 1024K 位元組,STM32F40xx/41xx 的快閃記憶體模組組織如圖所示:
在这里插入图片描述
STM32F4 的快閃記憶體模組由:主記憶體儲器、系統記憶體、OPT 區域和選項位元組等 4 部分組成。
主記憶體儲器,該部分用來存放程式碼和數據常數(如 const 型別的數據)。分爲 12 個磁區,前 4個磁區爲 16KB 大小,然後磁區 4 是 64KB 大小,磁區5~11 是 128K 大小,不同容量的 STM32F4,擁有的磁區數不一樣,比如我們的 STM32F407ZGT6,則擁有全部 12 個磁區。從上圖可以看出主記憶體儲器的起始地址就是 0X08000000, B0、B1 都接 GND 的時候,就是從 0X08000000 開始執行程式碼的。
系統記憶體,這個主要用來存放 STM32F4 的 bootloader 程式碼,此程式碼是出廠的時候就固化在 STM32F4 裏面了,專門來給主記憶體儲器下載程式碼的。當 B0 接 V3.3,B1 接 GND 的時候,從該記憶體啓動(即進入串列埠下載模式)。
OTP 區域,即一次性可程式化區域,共 528 位元組,被分成兩個部分,前面 512 位元組(32 位元組爲 1 塊,分成 16 塊),可以用來儲存一些用戶數據(一次性的,寫完一次,永遠不可以擦除!!),後面 16 位元組,用於鎖定對應塊。選項位元組,用於設定讀保護、BOR 級別、軟體/硬體看門狗以及器件處於待或停止模式下的復位。
快閃記憶體記憶體介面暫存器,該部分用於控制快閃記憶體讀寫等,是整個快閃記憶體模組的控制機構。 在執行快閃記憶體寫操作時,任何對快閃記憶體的讀操作都會鎖住匯流排,在寫操作完成後讀操作才能 纔能正確地進行;既在進行寫或擦除操作時,不能進行程式碼或數據的讀取操作。STM32F4 快閃記憶體的程式設計位數可以通過 FLASH_CR 的 PSIZE 欄位設定,PSIZE 的設定必須和電源電壓匹配,見表:
在这里插入图片描述
由於我的開發板用的電壓是 3.3V,所以 PSIZE 必須設定爲 10,即 32 位並行位數。擦除或者程式設計,都必須以 32 位爲基礎進行。 STM32F4 的 FLASH 在程式設計的時候,也必須要求其寫入地址的 FLASH 是被擦除了的(也
就是其值必須是 0XFFFFFFFF),否則無法寫入。

STM32F4 的標準程式設計步驟如下:
1,檢查 FLASH_SR 中的 BSY 位,確保當前未執行任何 FLASH 操作。
2,將 FLASH_CR 暫存器中的 PG 位置 1,啓用 FLASH 程式設計。
3,針對所需記憶體地址(主記憶體儲器塊或 OTP 區域內)執行數據寫入操作:
—並行位數爲 x8 時按位元組寫入(PSIZE=00)
—並行位數爲 x16 時按半字寫入(PSIZE=01)
—並行位數爲 x32 時按字寫入(PSIZE=02)
—並行位數爲 x64 時按雙字寫入(PSIZE=03)
4,等待 BSY 位清零,完成一次程式設計。
按以上四步操作,就可以完成一次 FLASH 程式設計。不過有幾點要注意:1,程式設計前,要確保要寫如地址的 FLASH 已經擦除。2,要先解鎖(否則不能操作 FLASH_CR)。3,程式設計操作對OPT 區域也有效,方法一模一樣。 我們在 STM32F4 的 FLASH 程式設計的時候,要先判斷縮寫地址是否被擦除了,所以,我有必要再介紹一下 STM32F4 的快閃記憶體擦除,STM32F4 的快閃記憶體擦除分爲種:磁區擦除和整片擦除。
磁區擦除步驟如下:
a,檢查 FLASH_CR 的 LOCK 是否解鎖,如果沒有則先解鎖
b,檢查 FLASH_SR 暫存器中的 BSY 位,確保當前未執行任何 FLASH 操作
c,在 FLASH_CR 暫存器中,將 SER 位置 1,並從主記憶體儲塊的 12 個磁區中選擇要擦除的 磁區 (SNB)
d,將 FLASH_CR 暫存器中的 STRT 位置 1,觸發擦除操作
e,等待 BSY 位清零

二、實驗準備

需要新增的韌體庫檔案有 stm32f4xx_gpio.c 、stm32f4xx_rcc.c、misc.c、stm32f4xx_flash.c、stm32f4xx_fsmc.c (並引入相應的.h檔案)。

三、實驗步驟

1.鎖定解鎖函數

上面講解到在對 FLASH 進行寫操作前必須先解鎖,解鎖操作也就是必須在 FLASH_KEYR 暫存器寫入特定的序列(KEY1 和 KEY2),韌體庫函數實現很簡單:

void FLASH_Unlock(void)

同樣的道理,在對 FLASH 寫操作完成之後,我們要鎖定 FLASH,使用的庫函數是:

void FLASH_Lock(void)

2.寫操作函數
韌體庫提供了四個 FLASH 寫函數:

FLASH_Status FLASH_ProgramDoubleWord(uint32_t Address, uint64_t Data); 
FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data); 
FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data); 
FLASH_Status FLASH_ProgramByte(uint32_t Address, uint8_t Data); 

這幾個函數從名字上面還是比較好理解意思,分別爲寫入雙字,字,半字,位元組的函數。因爲我的板子是32 位並行位數,所以只用到FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data)函數

3.擦除函數
韌體庫提供2個 FLASH 擦除函數:

FLASH_Status FLASH_EraseSector(uint32_t FLASH_Sector, uint8_t VoltageRange); 
FLASH_Status FLASH_EraseAllSectors(uint8_t VoltageRange); 

一個是用來擦除某個 Sector,一個使用來擦除全部的 sectors。
4.獲取 FLASH 狀態
獲取 FLASH 狀態主要呼叫的函數是:

FLASH_Status FLASH_GetStatus(void)

5.等待操作完成函數
在執行快閃記憶體寫操作時,任何對快閃記憶體的讀操作都會鎖住匯流排,在寫操作完成後讀操作才能 纔能正 確地進行;既在進行寫或擦除操作時,不能進行程式碼或數據的讀取操作。 所以在每次操作之前,我們都要等待上一次操作完成這次操作才能 纔能開始。使用的函數是:

FLASH_Status FLASH_WaitForLastOperation(void) 

返回值是 FLASH 的狀態,這個很容易理解,這個函數本身我們在韌體庫中使用得不多,但是在韌體庫函數體中間可以多次看到。

6.讀 FLASH 特定地址數據函數

有寫就必定有讀,而讀取 FLASH 指定地址的數據的函數韌體庫並沒有給出來,這裏提供從指定地址一個讀取一個字的函數:

u32 STMFLASH_ReadWord(u32 faddr) 
{ 
 return *(vu32*)faddr;  
} 

四、軟體設計

stmflash.c 程式碼

#include "stmflash.h"

//	 
//本程式只供學習使用,未經作者許可,不得用於其它任何用途
//STM32內部FLASH讀寫 驅動程式碼	   
// 

//解鎖STM32的FLASH
void STMFLASH_Unlock(void)
{
	FLASH->KEYR=FLASH_KEY1;	//寫入解鎖序列.
	FLASH->KEYR=FLASH_KEY2; 
}
//flash上鎖
void STMFLASH_Lock(void)
{
	FLASH->CR|=(u32)1<<31;//上鎖
}
//得到FLASH狀態
//返回值:
//0,操作完成
//1,忙 
//2,操作異常 
u8 STMFLASH_GetStatus(void)
{	
	 u32 res=0;		
	res=FLASH->SR;  
	if(res&(1<<16))return 1;   		//忙
	else if(res&(1<<4))return 2;	//操作異常 
	else if(res&(1<<5))return 2;	//操作異常 
	else if(res&(1<<6))return 2;	//操作異常 
	else if(res&(1<<7))return 2;	//操作異常 
	return 0;						//沒有任何狀態/操作完成.
} 
//等待操作完成
//time:要延時的長短(單位:10us)
//返回值:
//0,完成
//2,操作異常
//0XFF,超時       
u8 STMFLASH_WaitDone(u32 time)
{
	u8 res;
	do
	{
		res=STMFLASH_GetStatus();
		if(res!=1)break;//非忙,無需等待了,直接退出.
		delay_us(10);
		time--;
	 }while(time);
	 if(time==0)res=0xff;//TIMEOUT
	 return res;
}
//擦除磁區
//sectoraddr:磁區地址,範圍是:0~11.
//0~3,16K磁區;4,64K磁區;5~11,128K磁區.
//返回值:執行情況
u8 STMFLASH_EraseSector(u32 sectoraddr)
{
	u8 res=0;
	res=STMFLASH_WaitDone(200000);//等待上次操作結束,最大2s    
	if(res==0)
	{ 
		FLASH->CR&=~(3<<8);	//清除PSIZE原來的設定
		FLASH->CR|=2<<8;	//設定爲32bit寬,確保VCC=2.7~3.6V之間!!
		FLASH->CR&=~(0X1F<<3);//清除原來的設定
		FLASH->CR|=sectoraddr<<3;//設定要擦除的磁區 
		FLASH->CR|=1<<1;	//磁區擦除 
		FLASH->CR|=1<<16;	//開始擦除		  
		res=STMFLASH_WaitDone(200000);//等待操作結束,最大2s  
		if(res!=1)			//非忙
		{
			FLASH->CR&=~(1<<1);//清除磁區擦除標誌.
		}
	}
	return res;
}
//在FLASH指定地址寫一個字
//faddr:指定地址(此地址必須爲4的倍數!!)
//dat:要寫入的數據
//返回值:0,寫入成功
//    其他,寫入失敗
u8 STMFLASH_WriteWord(u32 faddr, u32 dat)
{
	u8 res;	   	    
	res=STMFLASH_WaitDone(0XFF);	 
	if(res==0)//OK
	{
		FLASH->CR&=~(3<<8);	//清除PSIZE原來的設定
		FLASH->CR|=2<<8;	//設定爲32bit寬,確保VCC=2.7~3.6V之間!!
 		FLASH->CR|=1<<0;	//程式設計使能
		*(vu32*)faddr=dat;	//寫入數據
		res=STMFLASH_WaitDone(0XFF);//等待操作完成,一個字程式設計,最多100us.
		if(res!=1)//操作成功
		{
			FLASH->CR&=~(1<<0);//清除PG位.
		}
	} 
	return res;
} 



//讀取指定地址的一個字(32位元數據) 
//faddr:讀地址 
//返回值:對應數據.
u32 STMFLASH_ReadWord(u32 faddr)
{
	return *(vu32*)faddr; 
}  



//獲取某個地址所在的flash磁區
//addr:flash地址
//返回值:0~11,即addr所在的磁區
u8 STMFLASH_GetFlashSector(u32 addr)
{
	if(addr<ADDR_FLASH_SECTOR_1)return 0;
	else if(addr<ADDR_FLASH_SECTOR_2)return 1;
	else if(addr<ADDR_FLASH_SECTOR_3)return 2;
	else if(addr<ADDR_FLASH_SECTOR_4)return 3;
	else if(addr<ADDR_FLASH_SECTOR_5)return 4;
	else if(addr<ADDR_FLASH_SECTOR_6)return 5;
	else if(addr<ADDR_FLASH_SECTOR_7)return 6;
	else if(addr<ADDR_FLASH_SECTOR_8)return 7;
	else if(addr<ADDR_FLASH_SECTOR_9)return 8;
	else if(addr<ADDR_FLASH_SECTOR_10)return 9;
	else if(addr<ADDR_FLASH_SECTOR_11)return 10; 
	return 11;	
}
//從指定地址開始寫入指定長度的數據
//特別注意:因爲STM32F4的磁區實在太大,沒辦法本地儲存磁區數據,所以本函數
//         寫地址如果非0XFF,那麼會先擦除整個磁區且不儲存磁區數據.所以
//         寫非0XFF的地址,將導致整個磁區數據丟失.建議寫之前確保磁區裡
//         沒有重要數據,最好是整個磁區先擦除了,然後慢慢往後寫. 
//該函數對OTP區域也有效!可以用來寫OTP區!
//OTP區域地址範圍:0X1FFF7800~0X1FFF7A0F(注意:最後16位元組,用於OTP數據塊鎖定,別亂寫!!)
//WriteAddr:起始地址(此地址必須爲4的倍數!!)
//pBuffer:數據指針
//NumToWrite:字(32位元)數(就是要寫入的32位元數據的個數.) 
void STMFLASH_Write(u32 WriteAddr,u32 *pBuffer,u32 NumToWrite)	
{ 
	u8 status=0;
	u32 addrx=0;
	u32 endaddr=0;	
  	if(WriteAddr<STM32_FLASH_BASE||WriteAddr%4)return;	//非法地址
	STMFLASH_Unlock();									//解鎖 
 	FLASH->ACR&=~(1<<10);			//FLASH擦除期間,必須禁止數據快取!!!搞了我兩晚上才發現這個問題!
	addrx=WriteAddr;				//寫入的起始地址
	endaddr=WriteAddr+NumToWrite*4;	//寫入的結束地址
	if(addrx<0X1FFF0000)			//只有主記憶體儲區,才需要執行擦除操作!!
	{
		while(addrx<endaddr)		//掃清一切障礙.(對非FFFFFFFF的地方,先擦除)
		{
			if(STMFLASH_ReadWord(addrx)!=0XFFFFFFFF)//有非0XFFFFFFFF的地方,要擦除這個磁區
			{   
				status=STMFLASH_EraseSector(STMFLASH_GetFlashSector(addrx));
				if(status)break;	//發生錯誤了
			}else addrx+=4;
		} 
	}
	if(status==0)
	{
		while(WriteAddr<endaddr)//寫數據
		{
			if(STMFLASH_WriteWord(WriteAddr,*pBuffer))//寫入數據
			{ 
				break;	//寫入異常
			}
			WriteAddr+=4;	//字長爲32,所以偏移4個位元組
			pBuffer++;
		} 
	}
	FLASH->ACR|=1<<10;		//FLASH擦除結束,開啓數據fetch
	STMFLASH_Lock();//上鎖
} 

//從指定地址開始讀出指定長度的數據
//ReadAddr:起始地址
//pBuffer:數據指針
//NumToRead:字(32位元)數
void STMFLASH_Read(u32 ReadAddr,u32 *pBuffer,u32 NumToRead)   	
{
	u32 i;
	for(i=0;i<NumToRead;i++)
	{
		pBuffer[i]=STMFLASH_ReadWord(ReadAddr);//讀取4個位元組.
		ReadAddr+=4;//偏移4個位元組.	
	}
}

//測試用///
//WriteAddr:起始地址
//WriteData:要寫入的數據
void Test_Write(u32 WriteAddr,u32 WriteData)   	
{
	STMFLASH_Write(WriteAddr,&WriteData,1);//寫入一個字 
}

stmflash.h 程式碼

#ifndef __STMFLASH_H__
#define __STMFLASH_H__
#include "common.h" 
//	 
//本程式只供學習使用,未經作者許可,不得用於其它任何用途
//ALIENTEK STM32F407開發板
//STM32內部FLASH讀寫 驅動程式碼	   
//正點原子@ALIENTEK
//技術論壇:www.openedv.com
//建立日期:2014/5/9
//版本:V1.0
//版權所有,盜版必究。
//Copyright(C) 廣州市星翼電子科技有限公司 2014-2024
//All rights reserved									  
// 



//FLASH起始地址
#define STM32_FLASH_BASE 0x08000000 	//STM32 FLASH的起始地址

//FLASH 磁區的起始地址
#define ADDR_FLASH_SECTOR_0     ((u32)0x08000000) 	//磁區0起始地址, 16 Kbytes  
#define ADDR_FLASH_SECTOR_1     ((u32)0x08004000) 	//磁區1起始地址, 16 Kbytes  
#define ADDR_FLASH_SECTOR_2     ((u32)0x08008000) 	//磁區2起始地址, 16 Kbytes  
#define ADDR_FLASH_SECTOR_3     ((u32)0x0800C000) 	//磁區3起始地址, 16 Kbytes  
#define ADDR_FLASH_SECTOR_4     ((u32)0x08010000) 	//磁區4起始地址, 64 Kbytes  
#define ADDR_FLASH_SECTOR_5     ((u32)0x08020000) 	//磁區5起始地址, 128 Kbytes  
#define ADDR_FLASH_SECTOR_6     ((u32)0x08040000) 	//磁區6起始地址, 128 Kbytes  
#define ADDR_FLASH_SECTOR_7     ((u32)0x08060000) 	//磁區7起始地址, 128 Kbytes  
#define ADDR_FLASH_SECTOR_8     ((u32)0x08080000) 	//磁區8起始地址, 128 Kbytes  
#define ADDR_FLASH_SECTOR_9     ((u32)0x080A0000) 	//磁區9起始地址, 128 Kbytes  
#define ADDR_FLASH_SECTOR_10    ((u32)0x080C0000) 	//磁區10起始地址,128 Kbytes  
#define ADDR_FLASH_SECTOR_11    ((u32)0x080E0000) 	//磁區11起始地址,128 Kbytes  


void STMFLASH_Unlock(void);					//FLASH解鎖
void STMFLASH_Lock(void);				 	//FLASH上鎖
u8 STMFLASH_GetStatus(void);				//獲得狀態
u8 STMFLASH_WaitDone(u32 time);				//等待操作結束
u8 STMFLASH_EraseSector(u32 sectoraddr);  //擦除磁區


u8 STMFLASH_WriteWord(u32 faddr, u32 dat);	//寫入字


u32 STMFLASH_ReadWord(u32 faddr);		  	//讀出字  


void STMFLASH_Write(u32 WriteAddr,u32 *pBuffer,u32 NumToWrite);		//從指定地址開始寫入指定長度的數據
void STMFLASH_Read(u32 ReadAddr,u32 *pBuffer,u32 NumToRead);   		//從指定地址開始讀出指定長度的數據

//測試寫入
void Test_Write(u32 WriteAddr,u32 WriteData);								   
#endif

main.c 程式碼

#include "led.h"
#include "lcd.h"
#include "key.h"  
#include "stmflash.h" 
//FLASH模擬EEPROM 實驗 


#define FLASH_SAVE_ADDR  0X0800C004 	//必須爲4的倍數


u32 num[]={999,100,101};	//要寫入的數據

int main(void)
{      
	u16 i=0;
	u32 temp[3];	//讀出的數據快取到該陣列
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //設定系統中斷優先順序分組2
	delay_init();         //初始化延時函數
	LED_Init();					  //初始化LED
 	LCD_Init();           //初始化LCD FSMC介面和顯示驅動
	KEY_Init();
	
 	BRUSH_COLOR=RED;//設定字型爲紅色 
	LCD_DisplayString(30,50,16,"Explorer STM32F4");	
	LCD_DisplayString(30,70,16,"FLASH EEPROM TEST");	
	LCD_DisplayString(30,110,16,"2020/08/14"); 
	LCD_DisplayString(30,130,16,"KEY0:Write  KEY1:Read");
	while(1)
	{
		key_scan(0);  		//按鍵掃描函數	
		if(keydown_data==KEY0_DATA)	//KEY0按下,寫入STM32 FLASH
		{
			LCD_Fill_onecolor(0,170,239,319,WHITE);//清除半屏    
 			LCD_DisplayString(30,170,16,"Start Write FLASH....");
			STMFLASH_Write(FLASH_SAVE_ADDR,(u32 *)num,3);
			LCD_DisplayString(30,170,16,"FLASH Write Finished!");//提示傳送完成
		}
		if(keydown_data==KEY1_DATA)	//KEY1按下,讀取字串並顯示
		{
 			LCD_DisplayString(30,170,16,"Start Read FLASH.... ");
			STMFLASH_Read(FLASH_SAVE_ADDR,(u32 *)temp,3);
			LCD_DisplayString(30,170,16,"The Data Readed Is:  ");//提示傳送完成
			LCD_DisplayNum(30,190,temp[0],4,16,1);	//LCD數位顯示
			LCD_DisplayNum(100,190,temp[1],4,16,1);	//LCD數位顯示
			LCD_DisplayNum(170,190,temp[2],4,16,1);	//LCD數位顯示
		}
		i++;
		delay_ms(10);  
		if(i==20)
		{
			LED0=!LED0;//提示系統正在執行	
			i=0;
		}		   
	}    
}