一、簡介
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;
}
}
}