FLASH簡介
- STM32F1系列的FLASH包含程序存儲器、系統存儲器和選項字節三個部分,通過閃存存儲器接口(外設)可以對程序存儲器和選項字節進行擦除和編程,(系統存儲器用于存儲原廠寫入的BootLoader程序,用于串口下載,不允許我們修改)
- 讀寫FLASH的用途: ?? ?
- 利用程序存儲器的剩余空間來保存掉電不丟失的用戶數據 ?? ?
- 通過在程序中編程(IAP),實現程序的自我更新
在線編程(In-Circuit Programming – ICP)用于更新程序存儲器的全部內容,它通過JTAG、SWD協議或系統加載程序(Bootloader)下載程序
在程序中編程(In-Application Programming – IAP)可以使用微控制器支持的任一種通信接口下載程序
?存儲器映像參考這篇:【江協科技STM32】DMA直接存儲器存儲-學習筆記_江科 stm32 dma-CSDN博客
?閃存模塊組織
對應主存儲器,進行了分頁,分頁是為了更好地管理閃存,擦除和寫保護都是以頁為單位的,這一點和之前的W25Q64的閃存一樣,寫入前必須擦除等等。具體參考Flash操作注意事項:【STM32】SPI通信協議&W25Q64Flash存儲器芯片(學習筆記)_spi存儲芯片-CSDN博客
?FLASH基本結構
?FLASH解鎖(解除閃存鎖)
?FPEC共有三個鍵值: ?? ?
- RDPRT鍵 = 0x000000A5(解除讀保護密鑰)
- KEY1 = 0x45670123 ?? ?
- KEY2 = 0xCDEF89AB
- 解鎖: ?? ?
- 復位后,FPEC被保護,不能寫入FLASH_CR ?? ?
- 在FLASH_KEYR先寫入KEY1,再寫入KEY2,解鎖 ?? ?
- 錯誤的操作序列會在下次復位前鎖死FPEC和FLASH_CR
加鎖: ?? ?
- 置FLASH_CR中的LOCK位鎖住FPEC和FLASH_CR
?
?使用指針訪問存儲器
uint16_t Data = *((__IO uint16_t *)(0x08000000));?
這行代碼是在 C 語言中用于從特定內存地址讀取數據,并將其賦值給變量Data
。解釋如下:
uint16_t
是一種無符號 16 位整數類型,定義在<stdint.h>
頭文件中,確保了數據類型的寬度為 16 位,可表示的范圍是 0 到 65535。Data
是定義的一個uint16_t
類型的變量,用于存儲從特定內存地址讀取的數據。(__IO uint16_t *)(0x08000000)
:這部分是一個強制類型轉換。(__IO uint16_t *)
將0x08000000
這個地址值轉換為指向__IO uint16_t
類型的指針。其中__IO
通常是由編譯器定義的宏,可能表示該內存地址是可讀寫(volatile
)的,防止編譯器對該地址的訪問進行優化,確保每次訪問都是真實地從該內存地址讀寫數據。0x08000000
是一個十六進制表示的內存地址。*((__IO uint16_t *)(0x08000000))
:這部分通過指針解引用操作,從0x08000000
這個內存地址讀取一個uint16_t
類型的數據。- 最后,將從指定內存地址讀取的數據賦值給
Data
變量。
?*((__IO uint16_t *)(0x08000000)) = 0x1234;
- 指針類型強制轉換
(__IO uint16_t *)(0x08000000)
?這部分代碼將地址值?0x08000000
?強制轉換為一個指向?__IO uint16_t
?類型的指針。其中?__IO
?可能是一個特定的修飾符,通常用于表示該內存位置具有特殊的讀寫屬性(例如可能是與外設寄存器相關,允許讀寫操作),uint16_t
?表示無符號 16 位整數類型。通過這種強制類型轉換,告訴編譯器把?0x08000000
?這個地址當作是一個?__IO uint16_t
?類型數據的起始地址。 - 賦值操作
在完成指針類型強制轉換后,使用?*
?運算符對這個指針進行解引用,然后將值?0x1234
?賦給該指針所指向的內存位置。也就是將?0x1234
?這個 16 位無符號整數值寫入到了內存地址?0x08000000
?開始的兩個字節(因為?uint16_t
?是 16 位,占兩個字節)。
?實例
?指定地址下讀:
/*** 函 數:FLASH讀取一個32位的字* 參 數:Address 要讀取數據的字地址* 返 回 值:指定地址下的數據*/
uint32_t HerFlash_ReadWord(uint32_t Address)
{return *((__IO uint32_t *)(Address)); //使用指針訪問指定地址下的數據并返回
}/*** 函 數:FLASH讀取一個16位的半字* 參 數:Address 要讀取數據的半字地址* 返 回 值:指定地址下的數據*/
uint16_t HerFlash_ReadHalfWord(uint32_t Address)
{return *((__IO uint16_t *)(Address)); //使用指針訪問指定地址下的數據并返回
}/*** 函 數:FLASH讀取一個8位的字節* 參 數:Address 要讀取數據的字節地址* 返 回 值:指定地址下的數據*/
uint8_t HerFlash_ReadByte(uint32_t Address)
{return *((__IO uint8_t *)(Address));//使用指針訪問指定地址下的數據并返回
}
?
?程序存儲器全擦除
-
第一步:讀 FLASH_CR 的 LOCK 位
- 理由:LOCK 位用于鎖定閃存控制寄存器(FLASH_CR),在對閃存進行擦除等操作前,需要先了解其鎖定狀態。如果 LOCK 位 = 1,表示閃存處于鎖定狀態,不能直接進行后續的擦除操作,需要先執行解鎖過程;若 LOCK 位 = 0,則可跳過解鎖過程直接進行擦除設置。
- 操作及結果:讀取該位,得到 LOCK 位 = 1,說明閃存處于鎖定狀態。
-
第二步:執行解鎖過程
- 理由:因為第一步檢測到 LOCK 位為 1,閃存鎖定,所以必須執行解鎖過程才能對閃存控制寄存器進行操作,以實現擦除等功能。
- 操作及結果:執行解鎖過程后,將 LOCK 位置為 0,此時閃存解鎖,可以對相關寄存器進行設置。
-
第三步:置 FLASH_CR 的 MER = 1
- 理由:MER(Mass Erase)位用于選擇是否進行全擦除操作。將 MER 位置 1,表示要進行閃存全擦除操作。
- 操作及結果:將 MER 位置 1,準備執行全擦除。
-
第四步:置 FLASH_CR 的 STRT = 1
- 理由:STRT(Start)位用于啟動閃存擦除操作。當 MER 位已置 1 準備好全擦除,再將 STRT 位置 1,就可以正式啟動全擦除過程。
- 操作及結果:將 STRT 位置 1,閃存全擦除操作開始執行。
-
第五步:關注 FLASH_SR 的 BSY 位
- 理由:BSY(Busy)位用于指示閃存操作是否正在進行。在全擦除操作啟動后,需要監測該位來判斷擦除操作是否完成。當 BSY = 1 時,表示閃存操作正在進行中;當 BSY = 0 時,表示閃存操作已完成。
- 操作及結果:在全擦除操作執行過程中,BSY 位會變為 1,表示操作正在進行。等待一段時間后,當 BSY 位變為 0,說明全擦除操作完成。
-
第六步:讀出并驗證所有頁的數據
- 理由:全擦除操作完成后,需要驗證是否所有頁的數據都已被正確擦除。通過讀出所有頁的數據,并與預期的擦除后數據(一般為全 1 或特定的擦除后狀態)進行比較,來驗證擦除操作的正確性。
- 操作及結果:讀出所有頁的數據,與預期的擦除后數據進行對比,若完全一致,則說明全擦除操作成功;若有不一致的地方,則說明擦除操作可能存在問題。
實例?
/*** 函 數:FLASH全擦除* 參 數:無* 返 回 值:無* 說 明:調用此函數后,FLASH的所有頁都會被擦除,包括程序文件本身,擦除后,程序將不復存在*/
void HerFlash_EraseAllPages(void)
{FLASH_Unlock(); //解鎖FLASH_EraseAllPages(); //全擦除FLASH_Lock(); //加鎖
}
void FLASH_Unlock(void)//解鎖FLASH程序擦除控制器(解鎖)?
?void FLASH_Lock(void)//鎖定FLASH程序擦除控制器
?FLASH_Status FLASH_EraseAllPages(void)//擦除所有FLASH頁面
?注意:以上功能可用于所有STM32F10x器件??
??程序存儲器頁擦除
- 讀取鎖定狀態:首先讀取閃存控制寄存器(
FLASH_CR
)的LOCK
位,判斷閃存是否處于鎖定狀態。 - 解鎖操作(若需):若
LOCK
位為1
(鎖定狀態),執行解鎖過程;若為0
,跳過解鎖。 - 配置擦除參數:
- 置
FLASH_CR
的PER
(Page Erase,頁擦除使能)位為1
; - 在
FLASH_AR
(地址寄存器)中選擇要擦除的閃存頁; - 置
FLASH_CR
的STRT
(Start,啟動)位為1
,啟動頁擦除操作。
- 置
- 等待擦除完成:監測閃存狀態寄存器(
FLASH_SR
)的BSY
(Busy,忙)位。若BSY=1
,表示擦除仍在進行,需持續等待;若BSY=0
,表示擦除完成。 - 驗證擦除結果:擦除完成后,讀出并驗證被擦除頁的數據,確保擦除操作成功。
實例
/*** 函 數:FLASH頁擦除* 參 數:PageAddress 要擦除頁的頁地址* 返 回 值:無*/
void HerFlash_ErasePage(uint32_t PageAddress)
{FLASH_Unlock();FLASH_ErasePage(PageAddress);FLASH_Lock();
}
?FLASH_Status FLASH_ErasePage(uint32_t Page_Address)//擦除指定的FLASH頁面?
?注意:此功能可用于所有STM32F10x器件??
?程序存儲器編程
注意:這種模式下CPU以標準的寫半字的方式燒寫閃存, FLASH_CR寄存器的PG位必須置’1’。 FPEC先讀出指定地址的內容并檢查它是否被擦除,如未被擦除則不執行編程并在FLASH_SR寄存器的PGERR位提出警告(唯一的例外是當要燒寫的數值是0x0000時, 0x0000可被正確燒入且PGERR位不被置位);如果指定的地址在FLASH_WRPR中設定為寫保護,則不執行編程并在FLASH_SR寄存器的WRPRTERR位置’1’提出警告。 FLASH_SR寄存器的EOP為’1’時表示編程結束。
編程過程講解:
- 讀取鎖定狀態:首先讀取閃存控制寄存器(
FLASH_CR
)的LOCK
位,判斷閃存是否處于鎖定狀態。 - 解鎖操作(若需):若
LOCK
位為1
(鎖定狀態),執行解鎖序列;若為0
,直接進入下一步。 - 使能編程模式:將
FLASH_CR
寄存器的PG
位(Program,編程使能)置為1
,開啟編程功能。 - 寫入數據:在指定的閃存地址中寫入半字(16 位)數據。用到這句代碼 *((__IO uint16_t *)(0x08000000)) = 0x1234;
- 等待操作完成:監測閃存狀態寄存器(
FLASH_SR
)的BSY
(Busy,忙)位。若BSY=1
,表示編程操作仍在進行,需持續等待;若BSY=0
,表示編程操作完成。 - 驗證數據:讀取編程地址中的數據,檢查寫入的數據是否正確,確保編程操作成功。
?實例
/*** 函 數:FLASH編程字* 參 數:Address 要寫入數據的字地址* 參 數:Data 要寫入的32位數據* 返 回 值:無*/
void HerFLASH_ProgramWord(uint32_t Address, uint32_t Data)
{FLASH_Unlock();FLASH_ProgramWord(Address, Data);FLASH_Lock();
}/*** 函 數:FLASH編程字* 參 數:Address 要寫入數據的半字地址* 參 數:Data 要寫入的16位數據* 返 回 值:無*/
void HerFLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)
{FLASH_Unlock();FLASH_ProgramHalfWord(Address, Data);FLASH_Lock();
}
?FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data)//在指定地址編程一個字
?注意:以上功能可用于所有STM32F10x器件???
參數 | 說明 |
Address | 指定要編程的地址 |
Data | 指定要編程的數據 |
FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)//在指定地址上編程半個字?
參數 | 說明 |
Address | 指定要編程的地址 |
Data | 指定要編程的數據 |
??注意:以上功能可用于所有STM32F10x器件???
?選擇字節說明
- RDP:寫入RDPRT鍵(0x000000A5)后解除讀保護
- USER:配置硬件看門狗和進入停機/待機模式是否產生復位
- Data0/1:用戶可自定義使用
- WRP0/1/2/3:配置寫保護,每一個位對應保護4個存儲頁(中容量)
選項字節擦除?
- 檢查FLASH_SR的BSY位,以確認沒有其他正在進行的閃存操作
- 解鎖FLASH_CR的OPTWRE位
- 設置FLASH_CR的OPTER位為1
- 設置FLASH_CR的STRT位為1
- 等待BSY位變為0
- 讀出被擦除的選擇字節并做驗證?
選項字節編程?
- 檢查FLASH_SR的BSY位,以確認沒有其他正在進行的編程操作
- 解鎖FLASH_CR的OPTWRE位
- 設置FLASH_CR的OPTPG位為1
- 寫入要編程的半字到指定的地址
- 等待BSY位變為0
- 讀出寫入的地址并驗證數據?
讀取內部FLASH閃存
要實現數據掉電不丟失的存儲,那就要基于底層代碼,再建一個模塊Store,在Store模塊我們要用SRAM緩存數組來管理FLASH閃存的最后一頁,實現參數的任意讀寫和保存。因為閃存每次都是擦除,再寫入,擦除之后,還容易丟失數據,所以要想靈活管理數據,還是得靠SRAM數組,需要備份的時候,我們再統一轉到閃存里。所以在Store模塊里要先定義一個Store_數組,用于存放備份數據。
?參數存儲模塊初始化
#define STORE_START_ADDRESS 0x0800FC00
#define STORE_DATA 512uint16_t Store_Data[STORE_DATA]; //定義SRAM數字,512個數據,每個數據16位,2字節,剛好對應閃存一頁1024字節void Store_Init(void)
{/*判斷是不是第一次使用*/if(HerFlash_ReadHalfWord(STORE_START_ADDRESS) != 0xA8A8){HerFlash_ErasePage(STORE_START_ADDRESS);HerFLASH_ProgramHalfWord(STORE_START_ADDRESS, 0xA8A8);//在第一個半字寫入自己規定的標志位,用于判斷是不是第一次使用for(uint16_t i =1; i < STORE_DATA; i ++) //循環STORE_COUNT次,除了第一個標志位{HerFLASH_ProgramHalfWord(STORE_START_ADDRESS + i * 2,0x0000);//除了標志位的有效數據全部清0}}/*上電時,將閃存數據加載回SRAM數組,實現SRAM數組的掉電不丟失*/for(uint16_t i =0;i < STORE_DATA; i ++){Store_Data[i] = HerFlash_ReadHalfWord(STORE_START_ADDRESS + i * 2);//將閃存的數據加載回SRAM數組}
}
?參數存儲模塊保存數據到閃存
/*** 函 數:參數存儲模塊保存數據到閃存* 參 數:無* 返 回 值:無*/
void Store_Save(void)
{HerFlash_ErasePage(STORE_START_ADDRESS);for(uint16_t i = 0;i < STORE_DATA;i ++) //循環STORE_COUNT次,包括第一個標志位{HerFLASH_ProgramHalfWord(STORE_START_ADDRESS +i*2, Store_Data[i]);//將SRAM數組的數據備份保存到閃存}
}
?參數存儲模塊將所有有效數據清0
/*** 函 數:參數存儲模塊將所有有效數據清0* 參 數:無* 返 回 值:無*/
void Store_Clear(void)
{for(uint16_t i = 1;i < STORE_DATA;i ++){Store_Data[i] = 0x0000; //SRAM數組有效數據清0} Store_Save(); //保存數據到閃存
}
?最終梳理思路:梳理思路
其實就是,在主函數對SRAM數組Store_Data進行修改,然后在放到閃存,防止SRAM掉電丟失,然后在上電初始化的時候在把閃存的數據再讀取到SRAMStore_Data數組,實現SRAM掉電不丟失?
main函數?
#uint8_t KeyNum; //定義用于接收按鍵鍵碼的變量int main(void)
{/*模塊初始化*/OLED_Init(); //OLED初始化Key_Init(); //按鍵初始化Store_Init(); //參數存儲模塊初始化,在上電的時候將閃存的數據加載回Store_Data,實現掉電不丟失/*顯示靜態字符串*/OLED_ShowString(1, 1, "Flag:");OLED_ShowString(2, 1, "Data:");while (1){KeyNum = Key_GetNum(); //獲取按鍵鍵碼if (KeyNum == 1) //按鍵1按下{Store_Data[1] = 0x1234; //變換測試數據,斷電丟失Store_Data[2] = 0xABCD;Store_Data[3] += 3;Store_Data[4] += 4;Store_Save(); //將Store_Data的數據備份保存到閃存,實現掉電不丟失}if (KeyNum == 2) //按鍵2按下{Store_Clear(); //將Store_Data的數據全部清0}OLED_ShowHexNum(1, 6, Store_Data[0], 4); //顯示Store_Data的第一位標志位OLED_ShowHexNum(3, 1, Store_Data[1], 4); //顯示Store_Data的有效存儲數據OLED_ShowHexNum(3, 6, Store_Data[2], 4);OLED_ShowHexNum(4, 1, Store_Data[3], 4);OLED_ShowHexNum(4, 6, Store_Data[4], 4);}
}
結果?
?
注意一個問題:程序文件大小和用戶數據大小的沖突?
程序占用空間大小
?
器件電子簽名&讀取芯片ID
電子簽名存放在閃存存儲器模塊的系統存儲區域,包含的芯片識別信息在出廠時編寫,不可更改,使用指針讀指定地址下的存儲器可獲取電子簽名
閃存容量寄存器: ?? ?
- 基地址:0x1FFF F7E0 ?? ?
- 大小:16位
產品唯一身份標識寄存器: ?? ?
- 基地址: 0x1FFF F7E8 ?? ?
- 大小:96位?
int main(void)
{OLED_Init(); OLED_ShowString(1, 1, "F_SIZE:"); OLED_ShowHexNum(1, 8, *((__IO uint16_t *)(0x1FFFF7E0)), 4); //使用指針讀取指定地址下的閃存容量寄存器OLED_ShowString(2, 1, "U_ID:"); OLED_ShowHexNum(2, 6, *((__IO uint16_t *)(0x1FFFF7E8)), 4); //使用指針讀取指定地址下的產品唯一身份標識寄存器OLED_ShowHexNum(2, 11, *((__IO uint16_t *)(0x1FFFF7E8 + 0x02)), 4);OLED_ShowHexNum(3, 1, *((__IO uint32_t *)(0x1FFFF7E8 + 0x04)), 8);OLED_ShowHexNum(4, 1, *((__IO uint32_t *)(0x1FFFF7E8 + 0x08)), 8);while (1){}
}
結果?
最后記得看數據手冊,這東西是真的能看懂 學會 學到知識!!!!?