STM32WB55 BLE雙核flash擦寫程式深度解析

独爱江大白~發表於2024-05-30

簡介

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位元組構成一個扇區,整個扇區的分佈在參考手冊中

image

由於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的作用,可以用如下這個圖來表示:

image

  • 系統中註冊一個任務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暫存器,內容如下
image

image
STM32WB的主儲存區(見上圖flash劃分)可以簡單的分為兩類,一類安全flash,專門存放BLE協議棧,一般處於主儲存區的尾部,使用者無法訪問,另一類非安全flash,存放應用程式,放到主儲存區的前面,使用者可以訪問,因此如果要向flash中寫入資料,我們不僅要避開應用程式佔用的flash區域,也要避開安全flash區域,這樣安全flash的儲存起始邊界就很重要。官方的參考例程,是將要擦寫的flash扇區放到安全flash前面,這樣就能保證這塊flash是空閒可用的,當然擦寫的時候,不能超過扇區的大小,否則會碰到安全flash區域。我們可以透過下圖直觀的看到flash劃分。

image

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有兩層含義,一層表示本次準備要寫入的資料個數,一層代表上次還有多少未寫入,其實意思是一樣的,歸根結底還是因為我們的任務不能一次性將所有資料寫入完成,任務需要執行很多次,這樣上次未寫完的資料個數,就自然而然成為本次準備要寫入的資料個數了。我們透過下面這個圖就能很好的理解地址為什麼這麼算了。
image
資料的源地址計算也是同樣的道理,只不過這裡我們每寫完一個雙字,指標往後遞增一下就可以了。

程式碼

      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擦寫程式碼的解析!

相關文章