作者: brody.zhang@foxmail.com
轉載請附本文鏈接:https://www.cnblogs.com/will-brody/articles/16157656.html
1 MCU 簡述
MCU 即單片機,適合 IO 密集、高實時要求任務(如控制、低功耗、快啟動高響應應用)。
AP (Aplication Processer) 處理器適合做高計算、低 IO 運算。針對不同系統需要設計合理的計算單元。
掌握 MCU 底層基本知識,對於軟件優化可以提供很多幫助。
2 MCU 內存映射、鏈接與跳轉
圖1:STM32F40x 內存映射
上圖為 STM32 內存空間映射,32位芯片最大尋址4GB,FLASH、RAM、外設都在此空間(Linux 高端映射不在本文討論范圍)。
通常工程中描述的地址都指鏈接地址(ld 文件或 scatter 文件),也是程序運行時所跳轉的地址。大部分 ARM 架構芯片上電從 0x0 地址開始運行,例如華大的芯片,Flash地址直接從 0x0 開始,鏈接腳本 RESET 段也從 0x0 開始。Flash 如果是 NAND 類型(不能 XIP 執行),需要上電后將程序所有內容搬運到 RAM 中,否則無法尋址。
備注:XIP 方式要求 Flash 隨機訪問讀寫,Flash 相關知識和鏈接文件各段意義不在此贅述,推薦閱讀《程序員的自我修養》學習。
提問:STM32 從 RAM 和 Flash 啟動地址是 0x20000000 和 0x0800000,為什么不是 0x0 地址?
答:根據 memory map 可以看出,STM32 根據 boot pin 配置不同,將 0x0 - 0x000FFFFF 映射到 SRAM 或者 Flash,相當於別名,可以嘗試將鏈接地址改為這個范圍,代碼執行不會受到任何影響。
3 MCU 啟動文件(基於 STM32)
3.1 MDK 版本:
跳轉到 Reset 后先初始化系統時鍾,再執行 __main() 函數。
__main 為 armcc 庫函數,先執行 __scatterload(),裝載 RW,清空 ZI。再使用 __rt_entry() 初始化 C 庫堆棧。堆棧建立用到了__user_initial_stackheap()
中斷向量表
前 16 個為 Cotex-M 內核定義,其余中斷號由廠商定義。
在鏈接文件中,將中斷向量表定義為 RESET 段並定義在可執行文件最前面,+First / LAST 可指定段位於最前或者最后,詳情可以查閱 ARMCC 手冊。
3.2 GCC 版本:
流程相似,不再贅述,請同學自己分析。
提問:0x0 填棧頂指針是必須的嗎?
答案:非必須,在 ARM 手冊中此地址為保留地址,廠商通常實現為上電自動賦值給 MSP。FreeRTOS 便利用這個值在內核啟動后重新初始化 MSP。可以測試將此值定義為0,只要在 ResetHandler 中和內核啟動后將 MSP 設置為正確值即可。
4 實時操作系統-SVC 與 PendSV
Handler mode 中斷中 |
Thread mode 非中斷中 |
|
Privileged 可訪問全部寄存器 |
中斷一定是特權模式,復位后使用 MSP |
可以在特權模式配置為非特權模式 |
Unprivileged 禁止訪問內核寄存器,例如:CONTROL 寄存器、NVIC 寄存器、MPU 寄存器 |
不存在 |
可以通過出發 SVC 進行系統調用,在中斷中進入特權模式 |
對於堆棧指針使用:
中斷中一定使用 MSP,但是在線程中根據 CONTROL 寄存器的第 1 位確定 MSP 或 PSP
4.1 SVC 軟中斷
SVC 指令(匯編調用或者函數聲明SVC)可以直接出發 SVC 中斷。
此指令可以傳遞一個參數(即 SVC 服務號),在 ARM 模式下 24 位,thumb 模式下 8 位。模式切換根據 BX 指令跳轉地址的第 0 位是否為 1 決定。
在 FreeRTOS 中實現了三個 SVC 調用,分別是啟動調度器、觸發任務調度、進入特權模式。
4.2 PendSV 可掛起中斷
RTOS 在 Systick 中斷置位 PendSV 標志位,當當前無中斷響應時進入 PendSV 進行調度。
調度基本流程:
- 入棧(線程棧)當前任務 TCB
- O(1) 位圖算法選擇下一個需要執行任務
- 切換 SP 出棧恢復任務現場
5 MPU 任務隔離
MPU 單元為不同的內存區域設置了訪問權限,特權級訪問、非特權級訪問,針對有 cache 的芯片(例如Cotex-M7系列)還可以配置是否使用 cache 避免數據不一致。
STM32F4xx 支持配置 8 個區域,支持背景區域,即不設置 MPU 的區域可配置默認訪問權限。
提問:MPU 如何保護任務棧?
答案:FreeRTOS 支持 MPU 接口。MPU 接口針對每個任務設置可訪問區域,任務運行在非特權模式,當任務切換時,MPU 寄存器也跟隨任務進行切換到該任務可訪問區域。這樣當任務越過自己的任務棧時便觸發內存訪問錯誤中斷。使用 MPU 增強了安全性,但是編程復雜度提升,無法通過全局傳參,編碼限制增加。
提問:不帶 MPU 接口的 FreeRTOS 堆棧檢測機制是怎樣的?
答案:任務切換時檢測堆棧指針是否越過棧底。缺點是無法檢測越過后在調度前又回來的情況。常見的棧檢測做法還有棧染色法,預先給任務棧刷固定魔數,通過檢查魔數是否改變統計使用率。
6 MMU、頁表、進程實現與地址隔離(以32位 rt-smart 為例講解)
ARM 架構目前有三個系列,Cotex-M、Cotex-A、Cortex-R。分別用於低成本嵌入式設備(物聯網、工控等),高端消費品(手機、無人機、顯示設備),高實時高安全場景。其中 Cotex-M 和 Cotex-R (除最新的Cortex-R82)不帶MMU,Cotex-A 系列基本都帶 MMU 組件。MMU 實質上是一種用硬件實現保證內存管理安全的方法,隔離了各個進程的運行空間。
本節以 rt-smart 在 Cotex-A 的移植分享下個人理解。
代碼可以參考:https://www.100ask.net/detail/p_5fdec53ce4b0231ba88dc8d1/8
頁表:頁表是虛擬地址和物理地址的映射,在具體的實現上,就是一個數組。按照 1M 大小划分頁表,則 32 位系統 4G 地址空間需要 4K 個頁表項。數組成員則代表數組下標 * 1M的虛擬地址所對應的物理地址。
ARM 通過 CP15 協處理器進行 MMU 控制,支持二級頁表。當設置頁表基地址(實現上是傳入頁表數組指針)使能后,CPU 尋址可通過硬件級別自動轉換。同時 MMU 支持對內存區域權限配置,例如是否可以使用 cache、writebuffer。
ARM 一級頁表和二級頁表定義:
- 一級頁表按照 1M 划分,每個頁表項代表 1M 物理地址的基地址。32位系統需要 4K 個一級頁表項,需要 16K 的空間存儲。
- 二級頁表按照 4K 划分,每個頁表項代表 4K 物理地址的基地址。4G 地址空間需要 1M 個二級頁表項,需要 4M 空間。
CPU典型尋址過程:
6.1 以 rt-smart 為例詳解 MMU 操作
需要搞明白的幾個關鍵點:
- 物理內存地址都映射在內核空間,創建進程從內核空間管理的 page 映射為用戶空間。
- 創建進程頁表時候,需要將內核頁表復制到進程頁表內核空間位置。
- 內核沒有用戶空間,rt-smart 只支持內核空間直接映射 0xC0000000 - 0xF0000000,即最大支持 768M 物理內存(Linux 中無法直接映射的內存空間使用高端映射訪問)
- 用戶進程通過 SVC 中斷進行內核服務調用
rt-smart 內存空間映射(全局視角):
用戶進程空間映射:
rt-smart 內核啟動流程:
1. 拷貝內核代碼到 RAM 中運行,此時使用物理地址,從 0x0 地址開始運行。
2. 初始化頁表,將物理地址同時映射到 0x0 和 0xC0000000,防止 CPU 使能 MMU 瞬間尋址錯誤(這里可以仔細思考下原因)。
3. 映射內核頁表到 0xC0000000,此時 CPU 訪問的都是虛擬地址,內核代碼鏈接地址在 0xC0000000 空間。當沒有進程創建時 CPU 不會訪問 0~3G 地址空間。
物理角度看進程內存映射:
如上圖,用戶程序裝載后物理上都位於頁表管理區,由 MMU 硬件保證不會訪問別的進程內存空間。當 CPU 需要訪問硬件寄存器時則通過動態映射到綠色區域尋址。
創建進程和調度分為以下幾步:
- 內核裝載用戶進程 elf:申請內存進行進程頁表映射(0~3G)(先創建一級頁表,每個頁表項都是二級頁表,裝載用到時再創建二級頁表),復制內核頁表到3~4G空間。
- 傳參進程入口函數,創建進程內核任務。rt-smart 創建進程及進程中線程時等效創建內核線程,進程創建幾個線程則對應創建幾個內核線程。同一進程創建的線程地址空間相同,不同進程地址空間不同,所有內核線程統一進行調度。
- 任務調度,若前后線程不屬於同一個進程,則觸發頁表切換操作
- 進程中用戶若想訪問硬件資源,需要通過 SVC 調用陷入內核,由內核進行真正的硬件操作,用戶無法直接尋址訪問內核資源。
- 進程間通信采用共享內存、管道等方式
提問:為什么需要二級頁表?
答:節省內存。直覺上一級頁表二級頁表全空間映射每個進程需要 16K+4M 內存,更加浪費內存,實際並非如此。
- 頁表每項需要物理上連續,1M大小的頁過大使用很不方便,當內存碎片化時很難分配。即使只使用1字節內存,也必須分配1M空間,造成內存浪費。
- 實際上根據 用戶進程空間映射 一節描述,用戶程序內存使用並不連續,大多數程序很多地址空間都不需要訪問,如果全使用 4K 大小頁表,則很多頁表項都是無用的,造成很大內存開銷。
- 通常進程無法用完0~3G的用戶空間,當用戶進程內存使用遠小於0~3G,裝載時先創建一級頁表,用不到的空間則不映射二級頁表,則既滿足了使用小頁表,也避免了用不到的空間頁表映射開銷,代價就是多一次尋址。