簡介
STM32WB55的flash擦除有兩種機制,一種是隻有單核執行下的flash擦除,這種模式下,flash擦除的步驟同其他STM32的flash擦除一樣,直接呼叫HAL庫中flash擦除的庫函式即可;另一種是雙核執行下的flash擦除,這種模式下,因為兩顆CPU核心都會訪問地址匯流排,可能會有訪問衝突,為了解決這個問題,ST引入了硬體訊號量機制,因此,在雙核執行下,即當微控制器執行BLE應用時,要想擦除flash,就要結合硬體訊號量來綜合處理,執行步驟比單核下要複雜的多,今天我們就來解析一下雙核flash擦除驅動是怎樣執行的。
準備變數
在APP_BLE_Init函式中,我們在BLE服務初始化之後,廣播啟動之前,新增如下程式碼
/******************** START FLASH TEST SPECIFIC INITIALIZATION *************************/
NbrOfSectorToBeErased = CFG_NBR_OF_FLASH_SECTOR_TO_PROCESS;
NbrOfDataToBeWritten = CFG_NBR_OF_FLASH_SECTOR_TO_PROCESS * 512;
FlashProcessStatus = FLASH_PROCESS_FINISHED;
FlashOperationReq = FLASH_ERASE_REQ;
UTIL_SEQ_RegTask(1 << CFG_TASK_FLASH_OPERATION_REQ_ID, UTIL_SEQ_RFU, FlashOperationProc);
/* Select which mechanism is used by CPU2 to protect its timing versus flash operation */
SHCI_C2_SetFlashActivityControl(FLASH_ACTIVITY_CONTROL_SEM7);
/**
* The error flag shall be cleared before moving forward
*/
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_OPTVERR);
/******************** END FLASH TEST SPECIFIC INITIALIZATION ***************************/
變數定義如下
uint32_t NbrOfSectorToBeErased;
uint32_t NbrOfDataToBeWritten;
typedef enum
{
FLASH_PROCESS_FINISHED,
FLASH_PROCESS_STARTED,
}FlashProcessStatus_t;
typedef enum
{
FLASH_ERASE_REQ,
FLASH_WRITE_REQ,
}FlashOperationReq_t;
#define CFG_NBR_OF_FLASH_SECTOR_TO_PROCESS (1)
- NbrOfSectorToBeErased直接賦值為一個宏,表示本次要處理的flash扇區個數,STM32WB55的flash每4K位元組構成一個扇區,整個扇區的分佈在參考手冊中
由於flash的擦除只能按扇區擦,即當我們要向flash寫入新資料時,首先要擦除一個4K位元組扇區,然後才能向這個已經擦除成功的扇區內寫入資料。
-
NbrOfDataToBeWritten表示本次要寫入的資料的個數,注意STM32WB55寫入資料時,必須以雙字格式寫入,即資料的最小寫入單位是64bit,用位元組表示的話,就是一次性要寫入4個位元組,因此這個變數表示的含義,是64bit的資料的個數,而非位元組個數,這一點非常重要,因此如果要寫滿一個扇區,則需要寫滿 4096 / 8 = 512 個位元組。我們一般是定義一個uint64_t的陣列,然後將要寫入的資料拼接成每4個位元組一組,填充進該陣列,然後將該陣列的元素一個一個寫進flash。
-
FlashProcessStatus 表示flash擦寫任務的執行結果,在雙核系統運用中,我們專門啟動一個後臺任務來處理flash事務,這個任務執行一次,並不能保證flash擦寫完全成功,因為在任務執行時,需要獲取硬體訊號量,如果暫時獲取不到,任務就會先結束(不阻塞等待),並且返回FLASH_PROCESS_STARTED,表示這個任務的擦寫操作還未完成,之後任務會被排程器重新啟動,重新啟動後的任務根據這個標誌判斷是要繼續擦寫flash。
-
FlashOperationReq 表示任務執行的階段,因為我們讓擦除和寫入操作都由同一個任務完成,那這個任務某一階段到底是要執行擦除函式還是執行寫入函式,就是靠這個變數做區分的。
FlashProcessStatus和FlashOperationReq的作用,可以用如下這個圖來表示:
-
系統中註冊一個任務FlashOperationProc,用來專門負責flash區域資料的更新
-
SHCI_C2_SetFlashActivityControl(FLASH_ACTIVITY_CONTROL_SEM7);
這個函式在shci.h檔案中有解釋
/** * SHCI_C2_SetFlashActivityControl * @brief Set the mechanism to be used on CPU2 to prevent the CPU1 to either write or erase in flash * * @param Source: It can be one of the following list * - FLASH_ACTIVITY_CONTROL_PES : The CPU2 set the PES bit to prevent the CPU1 to either read or write in flash * - FLASH_ACTIVITY_CONTROL_SEM7 : The CPU2 gets the semaphore 7 to prevent the CPU1 to either read or write in flash. * This requires the CPU1 to first get semaphore 7 before erasing or writing the flash. * * @retval Status */
意思就是說透過該函式,讓CPU2使用bit位還是使用訊號量7來阻止CPU1對flash的讀寫。
-
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_OPTVERR);
這句程式碼清空了FLASH由於上電可能導致的錯誤狀態位,保證後面關於flash的HAL庫函式能夠正常執行,建議每次在處理有關flash的應用之前都呼叫這句程式碼對錯誤狀態位清理一下
flash擦寫任務
這幾句程式碼理解完成後,我們接下來看執行flash擦寫的專用任務函式FlashOperationProc
void FlashOperationProc(void)
這個FlashOperationProc任務,是官方給我們提供的現成可用的flash擦寫任務,我們直接將這個任務函式新增到應用中即可,有關於該任務執行的步驟,我已經在程式碼中新增了註釋,供大家參考,這裡我帶大家看一些關鍵點
首先,整個任務大的框架就是一個if,一個else,透過判斷FlashOperationReq變數是FLASH_ERASE_REQ還是FLASH_WRITE_REQ來確定執行擦除還是寫入,這個我們在分析FlashOperationReq變數的作用時已經說過了。
程式碼
first_secure_sector_idx = (READ_BIT(FLASH->SFR, FLASH_SFR_SFSA) >> FLASH_SFR_SFSA_Pos);
這裡涉及一個flash暫存器,內容如下
STM32WB的主儲存區(見上圖flash劃分)可以簡單的分為兩類,一類安全flash,專門存放BLE協議棧,一般處於主儲存區的尾部,使用者無法訪問,另一類非安全flash,存放應用程式,放到主儲存區的前面,使用者可以訪問,因此如果要向flash中寫入資料,我們不僅要避開應用程式佔用的flash區域,也要避開安全flash區域,這樣安全flash的儲存起始邊界就很重要。官方的參考例程,是將要擦寫的flash扇區放到安全flash前面,這樣就能保證這塊flash是空閒可用的,當然擦寫的時候,不能超過扇區的大小,否則會碰到安全flash區域。我們可以透過下圖直觀的看到flash劃分。
STM32WB不同系列flash大小不一樣,安全flash的邊界也不一樣,我們可以透過讀取FLASH->SFSA暫存器來獲取安全flash的起始地址,以此來確定與應用程式的邊界,獲取到安全flash的起始地址後,我們往前讓出幾個扇區,然後把資料寫入到這個扇區就行了。注意我們從這個暫存器中讀到的數值,並不是直接可用的地址,而是該地址所在的扇區頁的編號,例如我們讀取flash為1MB的晶片,讀到的值為CE,表示安全flash是從第CE(206)個扇區開始的。這樣,變數first_secure_sector_idx就存放了安全flash扇區的起始編號。
接下來,將FlashProcessStatus變數值轉成FLASH_PROCESS_STARTED,表示flash任務正在執行。
程式碼
NbrOfSectorToBeErased = FD_EraseSectors(first_secure_sector_idx - CFG_OFFSET_OF_FLASH_SECTOR_TO_PROCESS, NbrOfSectorToBeErased);
透過呼叫驅動函式FD_EraseSectors擦除指定的扇區,函式的第一個入口引數為要擦除的起始扇區的編號,這裡我們把first_secure_sector_idx減去我們想要往前讓出的扇區的個數,就是我們要擦除的扇區的編號,我們設定為4,從安全flash邊界往前讓出4個扇區進行擦除,第二個入口引數為要擦除的扇區的個數,我們設定為1,讓其擦除一個扇區即可。
#define CFG_OFFSET_OF_FLASH_SECTOR_TO_PROCESS (4)
我們先不進FD_EraseSectors函式內部,先知道這個函式有個返回值,返回的是還沒有被擦除的扇區的個數,只要返回值不是0,就說明還有扇區沒有擦除完,如果是這樣,則進程式碼
/**
* There are still sectors to be erased
* Request the background to run one more time the task
*/
UTIL_SEQ_SetTask( 1<<CFG_TASK_FLASH_OPERATION_REQ_ID, CFG_SCH_PRIO_0);
return;
退出當前任務 ,重新啟用當前任務,交由排程器重新排程,下次任務執行時繼續擦除。
如果返回值為0,則進程式碼if(NbrOfSectorToBeErased == 0)中,變數值修改
FlashOperationReq = FLASH_WRITE_REQ;
FlashProcessStatus = FLASH_PROCESS_FINISHED;
NbrOfSectorToBeErased = CFG_NBR_OF_FLASH_SECTOR_TO_PROCESS;
其中FlashOperationReq修改,表示當前擦操作已經完成,接下來任務執行時,可以執行寫操作。FlashProcessStatus修改,表示當前的flash擦除操作已經完成了,NbrOfSectorToBeErased值恢復為初始值,為後面任務再次被呼叫執行擦除時做準備。
接下來,進入for迴圈,執行程式碼
p_data_flash = (uint64_t*)(FLASH_BASE + ((loop1 + first_secure_sector_idx - CFG_OFFSET_OF_FLASH_SECTOR_TO_PROCESS)*FLASH_SECTOR_SIZE*1024));
表示從我們剛才擦除的地址開始讀取資料,看是不是都擦寫成了0xFF(flash被擦除後的資料就是0xFF),透過(loop1 + first_secure_sector_idx - CFG_OFFSET_OF_FLASH_SECTOR_TO_PROCESS)來計算扇區下標,然後乘上FLASH_SECTOR_SIZE*1024即扇區下標對應的實際地址。
#define FLASH_SECTOR_SIZE (4) /* a sector on stm32wb55xx is 4K bytes */
p_data_flash將存放要檢查的扇區的起始地址,迴圈 for(loop2 = 0; loop2 < (FLASH_SECTOR_SIZE128); loop2++) 表示從當前這個p_data_flash地址開始,以雙字(8個位元組)為單位檢查資料,扇區大小為4 * 1K,1K下有128個雙字,那麼4K下就有4128個雙字,即一個扇區下要檢查的雙字個數,這樣就確定好了迴圈次數,然後以64bit地址遞增讀取雙字並判斷即可。
然後我們看FlashOperationProc任務中,有關寫入資料的操作
NbrOfDataToBeWritten = FD_WriteData(FLASH_BASE
+ ((first_secure_sector_idx - CFG_OFFSET_OF_FLASH_SECTOR_TO_PROCESS)*FLASH_SECTOR_SIZE*1024)
+ (((CFG_NBR_OF_FLASH_SECTOR_TO_PROCESS*512) - NbrOfDataToBeWritten)*8),
FlashDataToWriteTab + (CFG_NBR_OF_FLASH_SECTOR_TO_PROCESS*512) - NbrOfDataToBeWritten,
NbrOfDataToBeWritten);
任務呼叫驅動函式FD_WriteData來實現資料的寫入(寫入資料前必須保證FLASH扇區已經被擦除),同樣,我們先不進FD_WriteData函式里面檢視細節,只要知道它用來寫入資料就行,它的返回值是剩餘的未寫入的資料個數,這裡的資料個數是以雙字為單位的。函式的第一個入口引數是要寫入的資料的目標地址,第二個入口引數是資料的源地址,第三個是要寫入的資料個數,同樣以雙字為單位,我們來分析這個公式
FLASH_BASE + ((first_secure_sector_idx - CFG_OFFSET_OF_FLASH_SECTOR_TO_PROCESS)*FLASH_SECTOR_SIZE*1024)
+ (((CFG_NBR_OF_FLASH_SECTOR_TO_PROCESS*512) - NbrOfDataToBeWritten)*8)
((first_secure_sector_idx - CFG_OFFSET_OF_FLASH_SECTOR_TO_PROCESS) * FLASH_SECTOR_SIZE * 1024)得到的是要寫入的扇區首地址,(CFG_NBR_OF_FLASH_SECTOR_TO_PROCESS*512)表示要處理的扇區裡面雙字單元的個數,這個數減去現在準備要寫入的資料個數,再乘上8就是當前要寫的資料的目標地址,這裡的NbrOfDataToBeWritten有兩層含義,一層表示本次準備要寫入的資料個數,一層代表上次還有多少未寫入,其實意思是一樣的,歸根結底還是因為我們的任務不能一次性將所有資料寫入完成,任務需要執行很多次,這樣上次未寫完的資料個數,就自然而然成為本次準備要寫入的資料個數了。我們透過下面這個圖就能很好的理解地址為什麼這麼算了。
資料的源地址計算也是同樣的道理,只不過這裡我們每寫完一個雙字,指標往後遞增一下就可以了。
程式碼
for(loop1 = 0; loop1 < (CFG_NBR_OF_FLASH_SECTOR_TO_PROCESS*512); loop1++)
迴圈讀取剛才寫入的資料是否與源資料相等,驗證寫入過程,如果FD_WriteData的返回值不為0,則退出當前任務,並且啟用任務,讓排程器重新排程,繼續寫入過程,這跟擦除是一樣的。
至此,我們的flash擦寫任務程式碼分析完畢,我們做個總結:
- 這個任務被排程後,執行完畢並不一定完全擦除或者完全寫入資料,它會根據驅動函式的返回值,重新啟動自身,讓排程器重新排程自己,重新嘗試擦寫
- 這個任務有兩個關鍵變數,一個變數負責該任務本次做擦除還是寫入,一個變數負責該任務繼續之前的擦除或者寫入,還是可以進入到下一個階段。
驅動函式
好,接下來我們分析剛才漏掉的兩個驅動函式,這兩個函式在官方的驅動檔案flash_driver.c檔案中,先看擦除
/**
* @brief Implements the Dual core algorithm to erase multiple sectors in flash with CPU1
* It calls for each sector to be erased the API FD_EraseSingleSector()
*
* @param FirstSector: The first sector to be erased
* This parameter must be a value between 0 and (SFSA - 1)
* @param NbrOfSectors: The number of sectors to erase
* This parameter must be a value between 1 and (SFSA - FirstSector)
* @retval Number of sectors not erased:
* Depending on the implementation of FD_WaitForSemAvailable(),
* it may still have some sectors not erased when the timing protection has been
* enabled by either CPU1 or CPU2. When the value returned is not 0, the application
* should wait until both timing protection before retrying to erase the last missing sectors.
*
* In addition, When the returned value is not 0:
* - The Sem2 is NOT released
* - The FLASH is NOT locked
* - SHCI_C2_FLASH_EraseActivity(ERASE_ACTIVITY_OFF) is NOT called
* It is expected that the user will call one more time this function to finish the process
*/
uint32_t FD_EraseSectors(uint32_t FirstSector, uint32_t NbrOfSectors);
在flash_driver.h檔案中,有該函式的詳細描述,這個函式專門用來在雙核系統中執行多個扇區的擦除,第一個入口引數是第一個要被擦除的扇區的編號,第二個入口引數是要擦除的扇區的個數,返回值為還未擦除的扇區的個數,由於時序保護機制,所有的扇區並非可以在一個連續的時間段內完全擦除,因此當返回值非0時,應用程式需要等待定時保護結束再重新嘗試擦除。函式內部透過變數single_flash_operation_status來確定扇區是否擦除成功,如果不成功,則修改對應的返回值,返回該函式,下次重新嘗試。關鍵程式碼
/**
* Take the semaphore to take ownership of the Flash IP
*/
while(LL_HSEM_1StepLock(HSEM, CFG_HW_FLASH_SEMID));
HAL_FLASH_Unlock();
/**
* Notify the CPU2 that some flash erase activity may be executed
* On reception of this command, the CPU2 enables the BLE timing protection versus flash erase processing
* The Erase flash activity will be executed only when the BLE RF is idle for at least 25ms
* The CPU2 will prevent all flash activity (write or erase) in all cases when the BL RF Idle is shorter than 25ms.
*/
SHCI_C2_FLASH_EraseActivity(ERASE_ACTIVITY_ON);
透過獲取訊號量來獲取對flash的操作權,並且解鎖flash,並透過shci指令向CPU2傳送一個指令,通知CPU2 flash擦除操作將要執行,當CPU2接收到這個指令,它使能基於flash擦除的BLE時序保護處理機制,這種機制使得只有當 BLE RF 閒置至少 25ms 時,才會執行擦除快閃記憶體活動,當 BL RF 空閒時間短於 25 ms時,CPU2 在任何情況下都會阻止所有快閃記憶體活動(寫入或擦除)。
接下來,呼叫迴圈體,迴圈擦除每個扇區
for(loop_flash = 0; (loop_flash < NbrOfSectors) && (single_flash_operation_status == SINGLE_FLASH_OPERATION_DONE) ; loop_flash++)
{
single_flash_operation_status = FD_EraseSingleSector(FirstSector+loop_flash);
}
迴圈體的截止條件除了扇區個數外,還有單次扇區擦除的結果狀態,如果某個扇區擦除的狀態為無效,則結束這個迴圈。之後透過程式碼
if(single_flash_operation_status != SINGLE_FLASH_OPERATION_DONE)
{
return_value = NbrOfSectors - loop_flash + 1;
}
else
{
/**
* Notify the CPU2 there will be no request anymore to erase the flash
* On reception of this command, the CPU2 will disables the BLE timing protection versus flash erase processing
* The protection is active until next end of radio event.
*/
SHCI_C2_FLASH_EraseActivity(ERASE_ACTIVITY_OFF);
HAL_FLASH_Lock();
/**
* Release the ownership of the Flash IP
*/
LL_HSEM_ReleaseLock(HSEM, CFG_HW_FLASH_SEMID, 0);
return_value = 0;
}
返回還有多少個扇區未擦除,注意由於for迴圈,loop_flash至少會加1,因此這裡有一個NbrOfSectors - loop_flash + 1的操作,總之return_value一定表示有多少個扇區沒有處理完畢,如果當前要擦除的這個扇區沒有處理完畢,也要算到沒有處理的扇區裡面。如果能夠正常完成for迴圈,說明給定的扇區已經全部擦除完成,此時向CPU2 傳送shci指令,告知擦除操作已經完成,CPU2於是禁用flash擦除相對應的時序保護,時序保護將持續到下一次RADIO事件結束。然後是FLASH上鎖,釋放flash使用訊號量,這跟上面的操作是對稱的。
接下來看單一扇區擦除函式,這個函式的入口引數只有一個,即需要擦除的扇區編號
/**
* @brief Implements the Dual core algorithm to erase one sector in flash with CPU1
*
* It expects the following point before calling this API:
* - The Sem2 is taken
* - The FLASH is unlocked
* - SHCI_C2_FLASH_EraseActivity(ERASE_ACTIVITY_ON) has been called
* It expects the following point to be done when no more sectors need to be erased
* - The Sem2 is released
* - The FLASH is locked
* - SHCI_C2_FLASH_EraseActivity(ERASE_ACTIVITY_OFF) is called
*
* The two point above are implemented in FD_EraseSectors()
* This API needs to be used instead of FD_EraseSectors() in case a provided library is taking
* care of these two points and request only a single operation.
*
* @param FirstSector: The sector to be erased
* This parameter must be a value between 0 and (SFSA - 1)
* @retval: SINGLE_FLASH_OPERATION_DONE -> The data has been written
* SINGLE_FLASH_OPERATION_NOT_EXECUTED -> The data has not been written due to timing protection
* from either CPU1 or CPU2. On a failure status, the user should check
* both timing protection before retrying.
*/
SingleFlashOperationStatus_t FD_EraseSingleSector(uint32_t SectorNumber);
函式的註釋中寫的很清楚,在呼叫這個函式前,需要獲取flash訊號量,flash解鎖,通知CPU2 flash擦除要執行,結束這個函式呼叫後,使用對稱的操作。函式的返回值是擦除的狀態,成功或失敗,失敗是因為時序保護機制導致的。函式內部程式碼如下,註釋寫的很清楚,它做了一個小的等待後,直接呼叫函式ProcessSingleFlashOperation,這個函式很重要,負責擦寫,第一個入口參數列示是擦除操作還是寫入操作,第二個引數代表本次操作的扇區編號,第三個入口引數為0時無意義。我們接下來就到這個函式里面一探究竟。
SingleFlashOperationStatus_t FD_EraseSingleSector(uint32_t SectorNumber)
{
SingleFlashOperationStatus_t return_value;
/* Add at least 5us (CPU1 up to 64MHz) to guarantee that CPU2 can take SEM7 to protect BLE timing */
for (volatile uint32_t i = 0; i < 35; i++);
/* The last parameter is unused in that case and set to 0 */
return_value = ProcessSingleFlashOperation(FLASH_ERASE, SectorNumber, 0);
return return_value;
}
程式碼如下:
static SingleFlashOperationStatus_t ProcessSingleFlashOperation(FlashOperationType_t FlashOperationType,
uint32_t SectorNumberOrDestAddress,
uint64_t Data)
這個函式是一個區域性函式,沒有標頭檔案介紹,我們直接看內部執行流程,首先是區域性變數
SemStatus_t cpu1_sem_status;
SemStatus_t cpu2_sem_status;
WaitedSemStatus_t waited_sem_status;
SingleFlashOperationStatus_t return_status;
uint32_t page_error;
FLASH_EraseInitTypeDef p_erase_init;
waited_sem_status = WAITED_SEM_FREE;
p_erase_init.TypeErase = FLASH_TYPEERASE_PAGES;
p_erase_init.NbPages = 1;
p_erase_init.Page = SectorNumberOrDestAddress;
兩個硬體訊號量狀態cpu1_sem_status和cpu2_sem_status用來表示是否時序保護機制允許flash操作,等待狀態waited_sem_status表示當時序保護機制阻止flash操作時應該如何處理。page_error將被HAL庫函式使用,p_erase_init是HAL庫函式呼叫時需要的入口結構體。我們還是按先全域性,後區域性的流程看這個函式。
接著程式碼
do
{
/**
* When the PESD bit mechanism is used by CPU2 to protect its timing, the PESD bit should be polled here.
* If the PESD is set, the CPU1 will be stalled when reading literals from an ISR that may occur after
* the flash processing has been requested but suspended due to the PESD bit.
*
* Note: This code is required only when the PESD mechanism is used to protect the CPU2 timing.
* However, keeping that code make it compatible with the two mechanisms.
*/
while(LL_FLASH_IsActiveFlag_OperationSuspended());
UTILS_ENTER_CRITICAL_SECTION();
/**
* Depending on the application implementation, in case a multitasking is possible with an OS,
* it should be checked here if another task in the application disallowed flash processing to protect
* some latency in critical code execution
* When flash processing is ongoing, the CPU cannot access the flash anymore.
* Trying to access the flash during that time stalls the CPU.
* The only way for CPU1 to disallow flash processing is to take CFG_HW_BLOCK_FLASH_REQ_BY_CPU1_SEMID.
*/
cpu1_sem_status = (SemStatus_t)LL_HSEM_GetStatus(HSEM, CFG_HW_BLOCK_FLASH_REQ_BY_CPU1_SEMID);
if(cpu1_sem_status == SEM_LOCK_SUCCESSFUL)
{
/**
* Check now if the CPU2 disallows flash processing to protect its timing.
* If the semaphore is locked, the CPU2 does not allow flash processing
*
* Note: By default, the CPU2 uses the PESD mechanism to protect its timing,
* therefore, it is useless to get/release the semaphore.
*
* However, keeping that code make it compatible with the two mechanisms.
* The protection by semaphore is enabled on CPU2 side with the command SHCI_C2_SetFlashActivityControl()
*
*/
cpu2_sem_status = (SemStatus_t)LL_HSEM_1StepLock(HSEM, CFG_HW_BLOCK_FLASH_REQ_BY_CPU2_SEMID);
if(cpu2_sem_status == SEM_LOCK_SUCCESSFUL)
{
/**
* When CFG_HW_BLOCK_FLASH_REQ_BY_CPU2_SEMID is taken, it is allowed to only erase one sector or
* write one single 64bits data
* When either several sectors need to be erased or several 64bits data need to be written,
* the application shall first exit from the critical section and try again.
*/
if(FlashOperationType == FLASH_ERASE)
{
HAL_FLASHEx_Erase(&p_erase_init, &page_error);
}
else
{
HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, SectorNumberOrDestAddress, Data);
}
/**
* Release the semaphore to give the opportunity to CPU2 to protect its timing versus the next flash operation
* by taking this semaphore.
* Note that the CPU2 is polling on this semaphore so CPU1 shall release it as fast as possible.
* This is why this code is protected by a critical section.
*/
LL_HSEM_ReleaseLock(HSEM, CFG_HW_BLOCK_FLASH_REQ_BY_CPU2_SEMID, 0);
}
}
UTILS_EXIT_CRITICAL_SECTION();
if(cpu1_sem_status != SEM_LOCK_SUCCESSFUL)
{
/**
* To avoid looping in ProcessSingleFlashOperation(), FD_WaitForSemAvailable() should implement a mechanism to
* continue only when CFG_HW_BLOCK_FLASH_REQ_BY_CPU1_SEMID is free
*/
waited_sem_status = FD_WaitForSemAvailable(WAIT_FOR_SEM_BLOCK_FLASH_REQ_BY_CPU1);
}
else if(cpu2_sem_status != SEM_LOCK_SUCCESSFUL)
{
/**
* To avoid looping in ProcessSingleFlashOperation(), FD_WaitForSemAvailable() should implement a mechanism to
* continue only when CFG_HW_BLOCK_FLASH_REQ_BY_CPU2_SEMID is free
*/
waited_sem_status = FD_WaitForSemAvailable(WAIT_FOR_SEM_BLOCK_FLASH_REQ_BY_CPU2);
}
}
while( ((cpu2_sem_status != SEM_LOCK_SUCCESSFUL) || (cpu1_sem_status != SEM_LOCK_SUCCESSFUL))
&& (waited_sem_status != WAITED_SEM_BUSY) );
這是一個相當大的迴圈,先執行,輪詢PESD位,我們前面有提到過,時序保護有兩種方式,一種是使用硬體訊號量保護,另一種是透過這個PESD位,這個函式是為了相容這兩種方式,所以這裡新增了對PESD位的輪詢,這樣,如果應用程式選擇PESD位來做時序保護,也能直接呼叫這個函式。在使用PESD位來做時序保護時,如果這個位置置1,則CPU1會停到這裡,直到等到PESD位清零再執行下面的flash操作,然後呼叫UTILS_ENTER_CRITICAL_SECTION程式碼進入臨界段,在多工作業系統中,要在此處檢查是否有其他任務阻止flash操作,當flash處理正在進行時,CPU 不能再訪問快閃記憶體,在此期間嘗試訪問flash會導致 CPU 停止執行,
CPU1 禁止快閃記憶體處理的唯一方法是採取 CFG_HW_BLOCK_FLASH_REQ_BY_CPU1_SEMID訊號量。因此這裡呼叫程式碼
cpu1_sem_status = (SemStatus_t)LL_HSEM_GetStatus(HSEM, CFG_HW_BLOCK_FLASH_REQ_BY_CPU1_SEMID);
來獲取硬體訊號量,檢視是否有其他任務在執行flash操作,如果這個訊號量能拿到,則繼續獲取CPU2訊號量
cpu2_sem_status = (SemStatus_t)LL_HSEM_1StepLock(HSEM, CFG_HW_BLOCK_FLASH_REQ_BY_CPU2_SEMID);
如果這個訊號量也能拿到,說明CPU2目前沒有做時序保護,可以進行flash操作,要注意,CPU2預設使用的是PESD位來做時序保護,因此最前面的透過shci指令通知CPU2使用硬體訊號量作為時序保護方法的程式碼很重要。
當兩個硬體訊號量全部獲取到,此時可以執行的操作是,擦除一個扇區或者寫一個雙字資料到flash,如果有更多扇區需要擦除或者更多資料寫入,則需要退出當前臨界段程式碼重新進入該函式繼續執行。接下來根據傳進來的第一個入口引數,決定是擦除還是寫資料。
if(FlashOperationType == FLASH_ERASE)
{
HAL_FLASHEx_Erase(&p_erase_init, &page_error);
}
else
{
HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, SectorNumberOrDestAddress, Data);
}
這裡就直接呼叫HAL庫函式去處理了,我們後面再分析這兩個庫函式。
接下來
LL_HSEM_ReleaseLock(HSEM, CFG_HW_BLOCK_FLASH_REQ_BY_CPU2_SEMID, 0);
釋放CPU2硬體訊號量,由於CPU2會輪詢這個訊號量,因此要儘快釋放,使得CPU2有機會執行下一次flash操作時對應的時序保護操作,這也是為什麼這段程式碼處於臨界段的原因。
然後退出臨界段。
接下來執行判斷
if(cpu1_sem_status != SEM_LOCK_SUCCESSFUL)
{
/**
* To avoid looping in ProcessSingleFlashOperation(), FD_WaitForSemAvailable() should implement a mechanism to
* continue only when CFG_HW_BLOCK_FLASH_REQ_BY_CPU1_SEMID is free
*/
waited_sem_status = FD_WaitForSemAvailable(WAIT_FOR_SEM_BLOCK_FLASH_REQ_BY_CPU1);
}
else if(cpu2_sem_status != SEM_LOCK_SUCCESSFUL)
{
/**
* To avoid looping in ProcessSingleFlashOperation(), FD_WaitForSemAvailable() should implement a mechanism to
* continue only when CFG_HW_BLOCK_FLASH_REQ_BY_CPU2_SEMID is free
*/
waited_sem_status = FD_WaitForSemAvailable(WAIT_FOR_SEM_BLOCK_FLASH_REQ_BY_CPU2);
}
函式 FD_WaitForSemAvailable 的內容如下:
__WEAK WaitedSemStatus_t FD_WaitForSemAvailable(WaitedSemId_t WaitedSemId)
{
/**
* The timing protection is enabled by either CPU1 or CPU2. It should be decided here if the driver shall
* keep trying to erase/write the flash until successful or if it shall exit and report to the user that the action
* has not been executed.
* WAITED_SEM_BUSY returns to the user
* WAITED_SEM_FREE keep looping in the driver until the action is executed. This will result in the current stack looping
* until this is done. In a bare metal implementation, only the code within interrupt handler can be executed. With an OS,
* only task with higher priority can be processed
*
*/
return WAITED_SEM_BUSY;
}
這兩個判斷其實很精妙,其實這個函式FD_WaitForSemAvailable中的內容是可以根據入口引數進行修改的,當我們前面獲取訊號量失敗後,可以透過這個函式,確定既然失敗了,是繼續往下走,還是迴圈的檢查直至獲取到訊號量,而且兩個訊號量到底哪個獲取不到,需要迴圈檢查,這些是可以透過FD_WaitForSemAvailable來定製的,比方我們可以將FD_WaitForSemAvailable的內容設定為,獲取不到CPU1硬體訊號量時,返回WAITED_SEM_FREE,這樣可以在CPU1訊號量未獲取到時繼續執行迴圈,當獲取不到CPU2硬體訊號量時,返回WAITED_SEM_BUSY,使其退出當前迴圈。
我們現在看的例程裡面FD_WaitForSemAvailable並沒有對入口引數進行區分,都是返回WAITED_SEM_BUSY,那就只要兩個其中一個獲取不到,就退出當前迴圈。
最後是迴圈的判斷條件
while( ((cpu2_sem_status != SEM_LOCK_SUCCESSFUL) || (cpu1_sem_status != SEM_LOCK_SUCCESSFUL))
&& (waited_sem_status != WAITED_SEM_BUSY) );
只要其中一個訊號量沒有獲取成功,並且FD_WaitForSemAvailable的返回值為WAITED_SEM_FREE,則繼續這個迴圈,我們目前返回值都是BUSY,那自然而然只要有一個訊號量獲取失敗,迴圈就結束了。
然後是等待FLASH忙標記
while(__HAL_FLASH_GET_FLAG(FLASH_FLAG_CFGBSY));
接著
if(waited_sem_status != WAITED_SEM_BUSY)
{
/**
* The flash processing has been done. It has not been checked whether it has been successful or not.
* The only commitment is that it is possible to request a new flash processing
*/
return_status = SINGLE_FLASH_OPERATION_DONE;
}
else
{
/**
* The flash processing has not been executed due to timing protection from either the CPU1 or the CPU2.
* This status is reported up to the user that should retry after checking that each CPU do not
* protect its timing anymore.
*/
return_status = SINGLE_FLASH_OPERATION_NOT_EXECUTED;
}
由於waited_sem_status初始值為free,如果是busy則一定獲取訊號量失敗,並且迴圈退出了,因為如果是free,則迴圈一定會執行,此時busy說明操作沒有完成,返回未完成狀態,如果是free,則操作完畢,迴圈結束,返回完成狀態。
這是擦除驅動函式,接下來看寫入資料驅動函式
/**
* @brief Implements the Dual core algorithm to write multiple 64bits data in flash with CPU1
* The user shall first make sure the location to be written has been first erase.
* Otherwise, the API will loop for ever as it will be not able to write in flash
* The only value that can be written even though the destination is not erased is 0.
* It calls for each 64bits to be written the API FD_WriteSingleData()
*
* @param DestAddress: Address of the flash to write the first data. It shall be 64bits aligned
* @param pSrcBuffer: Address of the buffer holding the 64bits data to be written in flash
* @param NbrOfData: Number of 64bits data to be written
* @retval Number of 64bits data not written:
* Depending on the implementation of FD_WaitForSemAvailable(),
* it may still have 64bits data not written when the timing protection has been
* enabled by either CPU1 or CPU2. When the value returned is not 0, the application
* should wait until both timing protection before retrying to write the last missing 64bits data.
*
* In addition, When the returned value is not 0:
* - The Sem2 is NOT released
* - The FLASH is NOT locked
* It is expected that the user will call one more time this function to finish the process
*/
uint32_t FD_WriteData(uint32_t DestAddress, uint64_t * pSrcBuffer, uint32_t NbrOfData);
註釋中提到,要呼叫這個函式前必須保證扇區已經被擦除,否則這個API將一直迴圈,未擦除時只能寫入資料0,第一個入口引數時要寫入的地址,第二個是源資料的地址,第三個是要寫入的雙字的個數。
進入函式內部,single_flash_operation_status變數作用同擦除驅動函式一樣,記錄單次flash操作狀態,然後是獲取訊號量,解鎖flash,接著呼叫迴圈體
for(loop_flash = 0; (loop_flash < NbrOfData) && (single_flash_operation_status == SINGLE_FLASH_OPERATION_DONE) ; loop_flash++)
{
single_flash_operation_status = FD_WriteSingleData(DestAddress+(8*loop_flash), *(pSrcBuffer+loop_flash));
}
這一步也跟擦除一樣,迴圈結束,如果返回值非0,表示的是未寫入的雙字的個數。
然後呼叫
/**
* @brief Implements the Dual core algorithm to write one 64bits data in flash with CPU1
* The user shall first make sure the location to be written has been first erase.
* Otherwise, the API will loop for ever as it will be not able to write in flash
* The only value that can be written even though the destination is not erased is 0.
*
* It expects the following point before calling this API:
* - The Sem2 is taken
* - The FLASH is unlocked
* It expects the following point to be done when no more sectors need to be erased
* - The Sem2 is released
* - The FLASH is locked
*
* The two point above are implemented in FD_WriteData()
* This API needs to be used instead of FD_WriteData() in case a provided library is taking
* care of these two points and request only a single operation.
*
* @param DestAddress: Address of the flash to write the data. It shall be 64bits aligned
* @param Data: 64bits Data to be written
* @retval: SINGLE_FLASH_OPERATION_DONE -> The data has been written
* SINGLE_FLASH_OPERATION_NOT_EXECUTED -> The data has not been written due to timing protection
* from either CPU1 or CPU2. On a failure status, the user should check
* both timing protection before retrying.
*/
SingleFlashOperationStatus_t FD_WriteSingleData(uint32_t DestAddress, uint64_t Data);
注意這個函式第一個入口引數傳入的是要寫入資料的地址,因此在前面的迴圈體中,因為每次是寫入雙字,即8個位元組,因此每次迴圈有DestAddress+(8*loop_flash),而pSrcBuffer本身是雙字指標,因此只要自身遞增就可以,我們看到FD_WriteSingleData第一個入口引數不變,還是資料要寫入的地址,第二個入口引數變成了要寫入的資料值,這裡一定要注意。函式的返回值的含義跟FD_EraseSingleSector是一樣的,內容
SingleFlashOperationStatus_t FD_WriteSingleData(uint32_t DestAddress, uint64_t Data)
{
SingleFlashOperationStatus_t return_value;
return_value = ProcessSingleFlashOperation(FLASH_WRITE, DestAddress, Data);
return return_value;
}
這裡最終呼叫ProcessSingleFlashOperation函式,只不過這裡傳的第一個引數成了FLASH_WRITE,第三個引數不為0了,ProcessSingleFlashOperation前面已經分析過了,這裡不再贅述。
HAL庫函式
我們接下來看ProcessSingleFlashOperation中的兩個庫函式,一個用來擦除,擦除時,傳入的引數為
HAL_FLASHEx_Erase(&p_erase_init, &page_error);
注意,要擦除的扇區編號已經在前面傳給了結構體p_erase_init
p_erase_init.TypeErase = FLASH_TYPEERASE_PAGES;
p_erase_init.NbPages = 1;
p_erase_init.Page = SectorNumberOrDestAddress;
這個函式的內容如下
/**
* @brief Perform an erase of the specified FLASH memory pages.
* @note Before any operation, it is possible to check there is no operation suspended
* by call HAL_FLASHEx_IsOperationSuspended()
* @param[in] pEraseInit Pointer to an @ref FLASH_EraseInitTypeDef structure that
* contains the configuration information for the erasing.
* @param[out] PageError Pointer to variable that contains the configuration
* information on faulty page in case of error (0xFFFFFFFF means that all
* the pages have been correctly erased)
* @retval HAL Status
*/
HAL_StatusTypeDef HAL_FLASHEx_Erase(FLASH_EraseInitTypeDef *pEraseInit, uint32_t *PageError)
{
HAL_StatusTypeDef status;
uint32_t index;
/* Check the parameters */
assert_param(IS_FLASH_TYPEERASE(pEraseInit->TypeErase));
/* Process Locked */
__HAL_LOCK(&pFlash);
/* Reset error code */
pFlash.ErrorCode = HAL_FLASH_ERROR_NONE;
/* Verify that next operation can be proceed */
status = FLASH_WaitForLastOperation(FLASH_TIMEOUT_VALUE);
if (status == HAL_OK)
{
if (pEraseInit->TypeErase == FLASH_TYPEERASE_PAGES)
{
/*Initialization of PageError variable*/
*PageError = 0xFFFFFFFFU;
for (index = pEraseInit->Page; index < (pEraseInit->Page + pEraseInit->NbPages); index++)
{
/* Start erase page */
FLASH_PageErase(index);
/* Wait for last operation to be completed */
status = FLASH_WaitForLastOperation(FLASH_TIMEOUT_VALUE);
if (status != HAL_OK)
{
/* In case of error, stop erase procedure and return the faulty address */
*PageError = index;
break;
}
}
/* If operation is completed or interrupted, disable the Page Erase Bit */
FLASH_AcknowledgePageErase();
}
/* Flush the caches to be sure of the data consistency */
FLASH_FlushCaches();
}
/* Process Unlocked */
__HAL_UNLOCK(&pFlash);
return status;
}
這個函式最終會呼叫FLASH_PageErase實現扇區的擦除,注意這裡擦除時只擦除一個扇區,多個扇區擦除是要迴圈呼叫單個扇區擦除的函式的。
寫入函式
HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, SectorNumberOrDestAddress, Data);
內容也比較簡單
/**
* @brief Program double word or fast program of a row at a specified address.
* @note Before any operation, it is possible to check there is no operation suspended
* by call HAL_FLASHEx_IsOperationSuspended()
* @param TypeProgram Indicate the way to program at a specified address
* This parameter can be a value of @ref FLASH_TYPE_PROGRAM
* @param Address Specifies the address to be programmed.
* @param Data Specifies the data to be programmed
* This parameter is the data for the double word program and the address where
* are stored the data for the row fast program.
*
* @retval HAL_StatusTypeDef HAL Status
*/
HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data)
{
HAL_StatusTypeDef status;
/* Check the parameters */
assert_param(IS_FLASH_TYPEPROGRAM(TypeProgram));
assert_param(IS_ADDR_ALIGNED_64BITS(Address));
assert_param(IS_FLASH_PROGRAM_ADDRESS(Address));
/* Process Locked */
__HAL_LOCK(&pFlash);
/* Reset error code */
pFlash.ErrorCode = HAL_FLASH_ERROR_NONE;
/* Verify that next operation can be proceed */
status = FLASH_WaitForLastOperation(FLASH_TIMEOUT_VALUE);
if (status == HAL_OK)
{
if (TypeProgram == FLASH_TYPEPROGRAM_DOUBLEWORD)
{
/* Check the parameters */
assert_param(IS_FLASH_PROGRAM_ADDRESS(Address));
/* Program double-word (64-bit) at a specified address */
FLASH_Program_DoubleWord(Address, Data);
}
else
{
/* Check the parameters */
assert_param(IS_FLASH_FAST_PROGRAM_ADDRESS(Address));
/* Fast program a 64 row double-word (64-bit) at a specified address */
FLASH_Program_Fast(Address, (uint32_t)Data);
}
/* Wait for last operation to be completed */
status = FLASH_WaitForLastOperation(FLASH_TIMEOUT_VALUE);
/* If the program operation is completed, disable the PG or FSTPG Bit */
CLEAR_BIT(FLASH->CR, TypeProgram);
}
/* Process Unlocked */
__HAL_UNLOCK(&pFlash);
/* return status */
return status;
}
結構也同擦除一樣,會執行寫入一個雙字的操作,最終操作的還是暫存器。
至此,我們完成了STM32WB55 雙核系統應用下flash擦寫程式碼的解析!