最新項目中需要使用 STM32L476 的片子。在選擇片子時,資源的多少成爲了一個比較重要的考量。在斟酌一番之後,我決定採用 LL 庫來實現本次的功能。但是在使用 LL 庫的時候發現其中並沒有處理 FLASH 的驅動 stm32l4xx_ll_flash.h
和 stm32l4xx_ll_flash.c
。同樣去其他系列芯片中,也沒有發現 LL 庫的 FLASH 驅動。於是決定自己來實現一下!
引導模式
搞過 Linux 的應該都瞭解 Boot Loader 這個東西。但是通常在單片機中,我們基本就接觸不到這個東西了。以這裏的 ST MCU 爲例,我們的代碼通常情況下就直接從 FLASH 開始執行了。其實,MCU 是支持不同的啓動模式的,下圖是 STM32L476 的 Boot 配置:
復位後,BOOT0 引腳和 nBOOT1 位的值均被鎖存。 用戶自行設置 nBOOT1 和 BOOT0 來選擇所需的引導模式。
- 從 Main memory 引導: Main memory 被映射到爲引導存儲空間(0x00000000),但仍可以從其原始存儲空間(0x08000000)中訪問。 換句話說,可以從地址 0x00000000 或 0x08000000 開始訪問 Main memory。
- 從 System memory 引導: System memory 被映射到爲引導存儲空間(0x00000000),但仍可以從其原始存儲空間(0x1FFF0000)中訪問。這部分存儲空間中,存放了 ST 的 Boot 代碼!不同的芯片裏面的代碼是有區別的,這個部分在手冊 AN2606.pdf 中有詳細的介紹。
- 從 SRAM1 引導: SRAM1 被映射到爲引導存儲空間(0x00000000),但仍可以從其原始存儲空間(0x20000000)中訪問
注意:
- 當 MCU 從 SRAM 啓動時,在應用程序初始化代碼中,必須使用 NVIC 異常表和偏移寄存器在SRAM 中重新定位向量表。
FLASH 特性
ST 衆多 MCU 中,FLASH 並不是一樣的,而且差別很大!我們在使用 FLASH 的時候,比較關心的往往就是一下幾點:
- 總的大小及組織情況
- 讀、寫、擦 時的大小限制
下面是我常用的幾個片子的 FLASH 特性介紹:
下面是我常用的幾個片子的 FLASH 組織情況比較圖:
- Main memory: 包含應用程序和用戶數據。本文我們主要就是介紹這一部分。
- Information block: 它由三部分組成:
- Option bytes: 用於硬件和內存保護用戶配置
- System memory: 包含ST專有代碼。如果需要 BootLoader 模式。則需要配置從此處啓動。
- OTP (one-time programmable) area: 一次性編程區域
下面就以 STM32L476 爲例,來簡單介紹一下一些在使用時需要注意的事項。
Read access latency
要從 FLASH 中正確讀取數據,必須在 FLASH 訪問控制寄存器(FLASH_ACR)中設置正確的等待狀態數(LATENCY)。CPU 時鐘(HCLK)的頻率和設備 VCORE 的內部電壓範圍決定了等待狀態數(LATENCY)的具體值。下表爲等待狀態和CPU時鐘頻率之間的對應關係:
STM32L476 在復位後,CPU時鐘頻率爲 4 MHz,並且在 FLASH_ACR 寄存器中配置了 0 個等待狀態(WS)。更改 CPU 頻率時,必須根據手冊推薦的流程來更改等待狀態數(WS)。
經常使用標準外設庫的應該知道,在配置時鐘的接口 static void SetSysClock(void);
(system_stm32xxxx.c
中)有如下語句:
同樣,如果使用 HAL 庫,在時鐘配置函數 void SystemClock_Config(void)
(默認在 main.c
中)也會有對於 FLASH 的配置:
上面的代碼都是使用 標準外設庫 MCU 時鐘配置工具 和 STM32Cube 生成的。當我們配置好主頻之後,兩個工具就會自動設置正確的 FLASH 等待週期數。如果我們手動移植或者是要改變主頻的時候(例如進出低功耗時),則必須注意這點!
ART Accelerator™
爲了釋放處理器的全部性能,加速器實現了指令預取隊列和分支緩存,從而提高了 64 位閃存的程序執行速度。 這個東西僅針對具有 FPU 的 MCU。這個東西主要提供了三個功能:指令預取、指令緩衝、數據緩衝。
關於這部分,默認情況下是不開啓的。我暫時有沒有用到,這裏不多介紹!
寫保護
FLASH 在上電之後,默認都是寫保護的。貌似 STM32F0 沒有寫保護。寫保護通常也分爲兩部分:FLASH_CR 保護、FLASH 地址空間保護。前者主要是通過寄存器 FLASH_KEYR 來控制,後者則通過指定的一些寄存器控制。
不同的系列對於寫保護的處理也是有區別的。詳細的需要看參考手冊即可。
讀保護
通過設置 FLASH_OPTR 寄存器的 RDP 位,然後通過應用系統復位來重新加載新的RDP選項字節,可以激活讀保護。 讀保護保護閃存主存儲器,選項字節,備份寄存器(RTC中的RTC_BKPxR)和 SRAM2。從無保護(0級)到最大保護或無調試(2級),共有三種讀取保護級別。
- Level 0: 可以對 FLASH 的 Main memory 進行讀取,編程和擦除操作。 所有操作都可以訪問option bytes,SRAM2 和備份寄存器。
- Level 1: 這是擦除 RDP 選項字節時的默認保護級別。 當 RDP 值爲不同於 0xAA 和 0xCC 的任何值時,或者即使補碼不正確,也將定義該值。
- **User mode: ** 在用戶模式(即 Boot Flash)下,可以以任意操作訪問 Flash Main memory 區域,option bytes,備份寄存器(RTC 中的 RTC_BKPxR)和 SRAM2。
- **Debug, boot RAM and boot loader modes: ** 在調試模式下或着從 Boot RAM 或 boot loader 運行代碼時,Flash Main memory 區域,備份寄存器(RTC 中的 RTC_BKPxR)和 SRAM2 完全不可訪問。 在這些模式下,對 FLASH 的讀或寫訪問會產生總線錯誤和硬故障中斷。
- Level 2: 這個級別是在 Level 1 的基礎之上的。此外,Cortex-M4調試端口、從RAM引導(Boot RAM 模式)和從系統內存引導(boot loader 模式)都不再可用。在用戶執行模式(boot FLASH mode)下,所有操作都允許在閃存主存上進行。相反,只能對選項字節執行讀操作。
Option bytes 既不能編程也不能擦除。 因此,Level 2 根本無法刪除:這是不可逆的操作。 嘗試修改Option bytes 時,FLASH_SR 寄存器中的保護錯誤標誌 WRPERR將被置位,並且可以生成中斷。
讀寫擦
讀最簡單,直接訪問要讀取的地址即可。寫和擦則需要專門的執行序列(操作多個寄存器)。寫和擦都是有固定大小限制的!!例如這裏的STM32L476 寫入的數據必須是 64 位 8 字節對齊的!擦除則可以按頁擦除、BANK 擦除、MASS 擦除。
寫和擦的編程序列具體見手冊即可,沒啥需要特殊注意的!
LL 庫 FLASH 驅動
不知道爲啥,LL 庫中沒有實現 FLASH 驅動。下面我們參照已有的其他外設的 LL 庫來實現一下 FLASH 部分。爲開源項目提交過貢獻的人應該知道,想要自己的貢獻被接受,除了實現沒有錯誤之外,代碼風格也必須與開源項目的代碼風格一致!
在實現的過程中,主要有以下這些實現部分以及需要注意的事項:
-
在
stm32l4xx_ll_system.h
中,有部分對於 FLASH 的操作(主要是針對 FLASH_ACR 寄存器),這就導致如果我們完全按照 LL 庫的編碼風格就會有部分宏和函數重名。 -
每個寄存器位,LL 庫會在每個外設自己的實現文件中重新定義一遍,並且均以 LL_ 開頭。例如,在串口驅動中,狀態寄存器的每個位會重新定義
#define LL_USART_ISR_PE USART_ISR_PE /*!< Parity error flag */
-
定義寄存器讀寫宏函數:
/** @defgroup FLASH_LL_EM_WRITE_READ Common Write and read registers Macros * @{ */ /** * @brief Write a value in FLASH register * @param __INSTANCE__ FLASH Instance * @param __REG__ Register to be written * @param __VALUE__ Value to be written in the register * @retval None */ #define LL_FLASH_WriteReg(__INSTANCE__, __REG__, __VALUE__) WRITE_REG(__INSTANCE__->__REG__, (__VALUE__)) /** * @brief Read a value in FLASH register * @param __INSTANCE__ FLASH Instance * @param __REG__ Register to be read * @retval Register value */ #define LL_FLASH_ReadReg(__INSTANCE__, __REG__) READ_REG(__INSTANCE__->__REG__) /** * @} */
-
(如果有)實現一些與運算、參數處理等相關的宏值。例如,RTC 驅動中,有用於處理 BCD 碼的宏值。
/** * @brief Helper macro to convert a value from 2 digit decimal format to BCD format * @param __VALUE__ Byte to be converted * @retval Converted byte */ #define __LL_RTC_CONVERT_BIN2BCD(__VALUE__) (uint8_t)((((__VALUE__) / 10U) << 4U) | ((__VALUE__) % 10U))
-
靜態內聯接口(寄存器操作)。這部分接口可以說是 LL 庫外設驅動的的核心代碼,任何功能都可以使用這部分函數組合成一定的寄存器操作序列來實現。這部分通常不包含任何複雜的寄存器操作序列。
-
常用處理過程的封裝(例如,寫、擦等寄存器操作序列)。這部分接口的實現放在對應的
.c
文件中。 -
LL 庫中,不同外設的代碼實現並不是很統一。當然,下面不牽扯代碼風格,其實也沒啥問題。
-
根據 HAL 庫的實現,相比於手冊推薦的操作序列,某些操作還會有額外的操作。例如,頁擦除操作。具體看下圖:
未實現部分
由於有些功能沒有用到,因此該版本驅動庫中部分功能沒有實現,具體如下:
- FLASH option bytes 部分只實現了其中的一部分接口
- 讀寫保護寄存器目前沒有實現
- Fast programming 未實現
參考
- RM0351.pdf