ARM 之十二 Cortex-M 内核异常处理、异常定位方法、在线调试、Keil MDK-ARM 的使用
??Cortex-M 內核本身提供了非常強大的異常處理機制。它可以非常有效的捕捉非法的內存訪問以及其他一些異常。而我們常用的開發工具的異常處理就是使用了 Cortex-M 核的異常處理機制。
??在 ARM 平臺上開發,開發工具的選擇其實并不是很多,基本可以分為三大類:Keil MDK-ARM、IAR for ARM、GCC for ARM 系,其中用的比較多的基本就是 Keil MDK-ARM、IAR for ARM 這倆。而 GCC for ARM 系的 IDE 有很多,但是他們統一都是使用 GCC for ARM 作為編譯器構建套件,IDE 都是各家自定義的。例如,ST 有 STM32CubeIDE、SEGGER 有 Embedded Studio。
工欲善其事必先利其器,下面我們就先從開發工具開始說起!
構建(Build)
??從源文件到可執行文件,需要經過預處理、編譯、連接等一系列的處理過程,這個過程就被稱為構建(Build) 。下圖是一個典型的 ARM 程序構建流程圖示:
構建使用的工具通常叫做 編譯套件(Compile Collection) 或 構建套件(Build Collection) 。目前,ARM 提供了兩個版本的編譯套件,下面是 ARM 的編譯套件示意圖:
最新的 ARM Compiler 6.x 使用了 LLVM 的 Clang 編譯前端;ARM Compiler 5.x 使用的是愛迪生編譯前端。通常,編譯套件提供的工具都是命令行工具,且會獨立于 IDE 獨立提供(特例 IAR 就沒有獨立提供)。相對于桌面構建套件 GCC,它沒有提供命令行的調試器!
下面是個對比圖
關于 ARM 平臺的各種編譯套件,詳見博文 ARM 之七 主流編譯器(armcc、iar、gcc for arm)詳細介紹
由于種種原因,構建也被稱為編譯!嚴格來說,編譯只是構建中的一步。
集成開發環境(IDE)
??通常,編譯套件提供的工具都是命令行工具。使用起來相對比較麻煩!集成開發環境(IDE,Integrated Development Environment )應運而生。
??集成開發環境(IDE,Integrated Development Environment )是用于提供程序開發環境的應用程序,一般包括代碼編輯器、編譯器、調試器和圖形用戶界面等工具。集成了代碼編寫功能、分析功能、編譯功能、調試功能等一體化的開發軟件服務套。Keil MDK-ARM 就是其中之一了:
- 代碼編輯器:Keil uVision5(代碼高亮使用的是 Scintilla)
- 調試器:Keil uVision5 (含各種分析工具)
- 編譯套件:ARM 自家的 ARM Compiler 編譯套件
下面我們就以 Keil MDK-ARM 為例來介紹一下!
Run-Time Environment
??Keil MDK-ARM 除了提供了 IDE 的功能外,還額外提供了一些嵌入式的軟件解決方案,這些軟件解決方案就是 Keil MDK-ARM 中的 Run-Time Environment。新建項目時,在選擇設備后,RTE 窗口會自動打開。也可以通過菜單:Project ---> Mange ---> Run-Time Environment 打開。
其中,我們比較熟悉的應該是 CMSIS,無論用任何 IDE 來開發 ARM 平臺的 MCU,CMSIS 都是必需的!可以看到里面有實時操作系統 RTOS、網絡驅動、USB 驅動、文件系統等我們平時開發能用到的幾乎所有東西。在使用時:
- 綠色:當前所選的組件表示沒有任何問題
- 黃色:當前所選的組件有其他問題沒有解決,需要進一步的選擇其他配套組件或者操作。上圖中 Validation Output 欄中會有說明
- 紅色:軟件組件與其他組件沖突或沒有安裝在計算機上。上圖中 Validation Output 欄中會有說明
- 灰色:當前不可用。不適用于選定的 MCU。上圖中 Validation Output 欄中會有說明
??在實際工作中,我們項目更多的是自己手動移植,即使使用 Keil 建立項目,往往也僅僅是使用了 CMSIS 這一小部分,具體對比如下:
map 文件
??鏈接器列表文件或 Map 文件包含有關鏈接/定位過程的大量信息。在 Keil 中,需要通過 Project -> Options for Target -> Listing 界面如下的配置才可以輸出 map 文件:
其中,各選項的基本功能如下:
-
Select Folder for Listings…: 選擇存放清單文件的文件夾
-
Page Width: 為清單文件指定每行字符數
-
Page Length: 為清單文件指定每頁的行數
-
Assembler Listing: 為匯編源文件創建列表文件,對應產生 源文件名.lst 的文件
- Cross Reference: 列出有關符號的交叉引用信息,包括它們的定義位置以及宏的內部和外部的使用位置
-
C Compiler Listing: 為 C 源文件創建列表文件,對應產生 源文件名.txt 的文件 和 源文件名.lst 的文件
-
C Preprocessor Listing: 指示編譯器生成預處理文件。 宏調用將被展開并且注釋將被刪除 對應產生 源文件名.i 的文件
-
Linker Listing: 讓鏈接器為目標項目創建映射文件(map 文件)。對應的 armlink 參數為 --list=filename ,如果不選擇則不會生成文件,對應生成 用戶指定名.map 的文件。生成的 MAP 文件如下圖所示:
通常需要配合以下參數一起使用:- Memory Map: 包含一個內存映射,其中包含鏡像中每個加載區,執行區和輸入節的地址和大小,包括調試和鏈接器生成的輸入節。對應的 armlink 參數為 --map
- Callgraph: 以 HTML 格式創建函數的靜態調用圖文件。 調用圖給出了鏡像中所有函數的定義和參考信息。對應的 armlink 參數為 --callgraph 。該項會獨立生成一個 配置的輸出名.htm 的文件。如下圖所示:
其中顯示了詳細的調用關系。最重要的是,其中還有使用的棧的大小! - Symbols: 列出本地,全局和鏈接器生成的符號以及符號值。對應的 armlink 參數為 --symbols
- Cross Reference: 列出輸入節之間的所有交叉引用。對應的 armlink 參數為 --xref
- Size Info: 給出鏡像中每個輸入對象和庫成員的代碼和數據(RO 數據,RW 數據,ZI 數據和調試數據)的大小的列表。對應的 armlink 參數為 --info sizes
- Totals Info: 提供輸入對象和庫的代碼和數據(RO 數據,RW 數據,ZI 數據和調試數據)大小的總和。對應的 armlink 參數為 --info totals
- Unused Section Info: 列出從鏡像文件中刪除的所有未使用的部分。對應的 armlink 參數為 --info unused
- Veneers Info: 提供鏈接器生成的 Thumb/ARM 膠合代碼的詳細信息。對應的 armlink 參數為 --info veneers
- ARM/Thumb交互(ARM/Thumb interworking)是指對匯編語言和 C/C++ 語言的 ARM 和 Thumb 代碼進行連接的方法,它進行兩種狀態(ARM 和 Thumb)間的切換。
- 膠合代碼(Veneer):在進行 ARM/Thumb 交互時,有時需使用額外的代碼,這些代碼被稱為 膠合代碼(Veneer)。
- AAPCS 定義了 ARM 和 Thumb 過程調用的標準。
關于 Map 文件的詳細介紹,見博文 ARM 之十 ARMCC(Keil) map 文件(映射文件)詳解
分散加載文件
??分散加載文件是對分散加載機制的內容描述,也被稱為分散描述文件。分散加載文件包含一個或多個加載域。 每個加載域可以包含一個或多個執行域。 一個典型的分散加載文件如下圖所示:
分散加載文件的語法使用標準的 Backus-Naur Form (BNF) 表示法。關于 BNF 可以去 ARM 官網了解。
更詳細的信息,請參考博文ARM 之十三 armlink(Keil) 分散加載機制詳解 及 分散加載文件的編寫 。
Initialization file(.ini file)
??Initialization file 是 Keil uVision5 調試器的腳本文件。調試器腳本文件是包含調試器命令的純文本文件。這些文件不是由工具創建的,而是由使用者創建它們來滿足自己的特定需求。通常,它們用于配置調試器,或在運行程序之前設置或初始化某些東西。Keil 中在如下地方指定調試器腳本文件:
Keil uVision 用戶指南中介紹的所有命令以及調試函數都可以在腳本文件中使用。部分命令的使用與在調試界面中 Command Window 中直接輸入命令效果是一樣的。可用命令與函數如下圖:
具體每個命令的用法,請參考手冊。下面我們來介紹一些常用的命令或者操作:
LOAD 指令用來指示 μVision 調試器加載目標文件,格式:LOAD path\filename [options]。μVision 分析文件的內容以確定文件類型(如果無法確定文件類型,則會顯示錯誤消息)。
path\filename 必選項,取值如下:
- Absolute Object File 或 ELF/DWARF File: 由鏈接器/定位器生成的,并包含完整的符號調試信息,類型信息和與調試信息一起轉換的行號的文件。 如果未指定其他選項,則調試器將執行目標復位并提供內存映射設置。
- Intel HEX File: 由 Object-to-Hex-Converter 程序產生,不包含符號調試信息,類型信息和行號信息。 僅在 CPU 指令級別支持程序測試。 不支持源代碼級和符號調試。 加載 HEX 文件時,不執行目標復位。 因此,可能需要發出顯式的 RESET 命令。
[options] 是可選項,取值如下:
- INCREMENTAL:將調試信息添加到現有符號表中。 這樣可以進行多應用程序調試。
- NOCODE: 僅加載符號信息,而忽略代碼記錄。 NOCODE 防止現有程序代碼被覆蓋。 此選項需要預先加載監視器的 CPU 驅動程序(MON51,MON251或 MON166)。
- NORESET:(僅在某些目標上可用)防止在加載程序后生成 RESET 信號。 對于不存在該選項的目標,請改用INCREMENTAL 選項,該選項可以有效地執行相同的操作。
示例:
直接在 RAM 中調試代碼,必須使用調試器腳本文件,具體步驟如下:
腳本文件的內容如下所示:/*----------------------------------------------------------------------------* Name: DebugInRAM.ini* Purpose: RAM Debug Initialization File*----------------------------------------------------------------------------*/FUNC void Setup (void) {SP = _RDWORD(0x20000000); // Setup Stack PointerPC = _RDWORD(0x20000004); // Setup Program CounterXPSR = 0x01000000; // Set Thumb bit_WDWORD(0xE000ED08, 0x20000000); // Setup Vector Table Offset Register }LOAD %L INCREMENTAL // Download to RAM Setup();g, main
至此,就可以愉快的在內存中調試程序了!
調試前配置某些寄存器。例如,我們在調試時,需要先關閉看門狗。默認 Jlink 會自動給我們處理,ST-link 則需要我們自己處理。
/*-------------------------------------------------------------------** Define the function to enable the trace port**-----------------------------------------------------------------*/FUNC void EnableTPIU(void) {_WDWORD(0xE0042004, 0x000000E0); // Set 4-pin tracing via DBGMCU_CR}/*-------------------------------------------------------------------** Invoke the function at debugger startup**-----------------------------------------------------------------*/EnableTPIU();可以直接在 Keil MDK-ARM 的安裝目錄中搜索 *.ini,會在 PACK 目錄下找到很多類型的調試器腳本文件!
??注意,在某些 MCU 項目中,Keil 會自動生成 ./DebugConfig/xxxx.dbgconf 的文件。 該文件具有與調試初始化文件相同的功能,但是它僅用于可以實現寄存器分配與設備無關的那些 MCU中,所以,并不是所有 MCU 都又該文件!該文件實質上替代了調試初始化文件,如果可用,建議使用該文件。 它還允許通過配置向導而不是手工編碼來配置調試器。 要打開此文件進行編輯:Options For Target -> Debug -> Settings button -> Pack tab
注意,該功能貌似支持 ARM 自己的仿真器 ULINK!!!
異常處理
??程序 BUG 的處理一直都是一件讓人頭疼的事,調試則給我們指出了一條路。下面我們介紹一下在 Keil MDK-ARM 調試中,如何分析處理一些異常情況。
??異常的處理嚴格來說和 IDE 并沒有關系,因為它本質上是內核提供的一項功能!ARM?Cortex?-M 處理器實現了一個有效的異常模型,可捕獲非法的內存訪問和一些不正確的程序條件。 為了盡早發現問題,所有Cortex-M處理器都包含一個故障異常機制。 如果檢測到故障,則觸發相應的故障異常,并執行其中一個故障異常處理程序。 異常主要有如下幾種:
- HardFault: HardFault 是默認異常,可以由于異常處理期間的錯誤或因為異常無法由其他任何異常機制處理而觸發。
- MemManage: 檢測對內存管理單元(MPU)中定義的區域的內存訪問沖突; 例如,僅從具有讀/寫訪問權限的內存區域執行代碼。
- BusFault: 在指令獲取,數據讀/寫,中斷向量獲取以及中斷(進入/退出)時寄存器堆疊(保存/恢復)時檢測內存訪問錯誤。
- UsageFault: 檢測未定義指令的執行,加載/存儲多字節時的未對齊的內存訪問。 啟用后,還將檢測除零和其他未對齊的內存訪問。
??HardFault 異常始終處于啟用狀態,并且具有固定的優先級(高于其他中斷和異常,但低于不可屏蔽中斷NMI)。 因此,在禁用故障異常或在執行故障異常處理程序的過程中發生故障的情況下,將執行 HardFault 異常。
??所有其他故障異常(MemManage fault、BusFault和UsageFault)都具有可編程的優先級。復位后,這些異常被禁用,并可能在系統或應用軟件中使用系統控制塊(SCB)中的寄存器啟用。
異常優先級提升
??在某些情況下,具有可配置優先級的故障會被視為 HardFault。 這稱為優先級升級,故障描述為升級為 HardFault。 在以下情況下會升級為 HardFault:
- 故障處理程序會導致與正在處理的故障相同類型的故障。 之所以升級為HardFault,是因為處理程序無法搶占自身(它必須具有與當前優先級相同的優先級)。
- 故障處理程序導致的故障與正在處理的故障具有相同或更低的優先級。 這是因為新故障的處理程序無法搶占當前正在執行的故障處理程序。
- 發生故障,并且未啟用該故障的處理程序。
??如果在進入BusFault 處理程序時在堆棧推送期間發生 BusFault,則 BusFault 不會升級為 HardFault。 這意味著,如果損壞的堆棧導致故障,即使處理程序的堆棧推送失敗,故障處理程序也會執行。
異常類型
??下表顯示了故障類型,故障處理程序,故障狀態寄存器以及指示發生故障的寄存器位名稱。
SCB
系統控制塊(SCB)提供系統實現信息和系統控制。這包括系統異常的配置、控制和報告。
異常控制寄存器
系統控制塊(SCB)的一些寄存器用于控制故障異常:
- CCR: 配置和控制寄存器(CCR)用于控制 UsageFault 的行為,用于零除和未對齊的內存訪問。寄存器地址 0xE000ED14,默認值為 0,按位使用,置 1 有效!
在使用 Keil 調試時,可以直接使用 GUI 界面查看:
- SHP: 系統處理程序優先級寄存器(SHP)控制異常優先級。寄存器地址 0xE000ED18,共有 12 個字節,復位值為 0,按字節使用。通常如下
- SHP[0]: Memory Management Fault 的優先級
- SHP[1]: BusFault 的優先級
- SHP[2]: UsageFault 的優先級
- SHCSR: 系統處理程序控制和狀態寄存器(SHCSR)啟用系統處理程序,并指示 BusFault,MemManage 故障和 SVC 異常的掛起狀態。寄存器地址 0xE000ED24,默認值為 0,按位使用,置 1 有效!
異常狀態寄存器
下表列出了故障狀態寄存器和故障地址寄存器的名稱,并顯示了每個寄存器的存儲器地址。
在使用 Keil 進行調試時,可以使用如下 GUI 界面查看:
關于寄存器中每個位的具體含義,參見 ARM 官方文檔: Using Cortex-M3/M4/M7 Fault Exceptions .pdf。
示例
??ARM 官方提供了用于驗證以上異常的示例,我們可以從 www.keil.com/appnotes/docs/apnt_209.asp 頁面上下載,下載之后我們可以下載自己的 MCU 中進行測試。需要注意的是,示例僅僅是針對于 Cortex-M 核的,所以在調試的時候沒法選擇對應的下載算法,這個時候就可以用我們上面提到的內存調試了!
調試
??這里我只介紹如何進行在線調試。在線調試是指不打斷當前程序運行的情況下進入調試狀態,以方便觀察程序當前運行狀態,便于查找問題的調試方法。這種調試方法在 PC 端非常常見,就是被稱為 Attach 的調試方式。其實,這個調試方式僅僅是 Keil 沒有,如果使用其他類似的調試工具的話,在線調試一般都有:
Keil 在默認情況下不支持在線調試,Keil 如果要進行在線調試,必須手動配置如下:
定位異常位置
在 Keil 的調試狀態下,定位異常發生的位置主要有以下兩種方法:
根據異常的類型,調試器將突出顯示引起異常的指令或引起故障的指令之后的指令。 這取決于導致異常的指令是否實際完成了執行。
??異常會保存寄存器 R0 ~ R3,R12,PC 和 LR 以及 MSP 或 PSP 的狀態(取決于發生異常時使用的堆棧)。 當前鏈接寄存器 LR 包含用于正在處理的異常的 EXC_RETURN 值,該值指示哪個堆棧保存了來自應用程序上下文的保留寄存器值。 如果 EXC_RETURN 的位 2 為零,則使用主堆棧(保存 MSP),否則使用進程堆棧(PSP)。
我們以上面的例子來具體看看如何操作:
查看寄存器窗口中寄存器的值
跳轉到異常后,LR 寄存器的值為 0xFFFFFFF9 = b_11111111111111111111111111111001,bit 2 = 0,表示主堆棧(MSP)包含最近存儲的寄存器值。MSP 的值為 0x20011158
??這里需要注意,在正常的 C 語言函數調用中,LR 應該是 PC 的值,但是進入異常時放的值被稱為 EXEC_RETURN,該值會在異常返回時被用到。下圖是個正常函數調用時寄存器的情況:
在 Memory 窗口中,查看堆棧。這個堆棧就保存了 mcu 跳轉到異常之前的信息
- 返回地址 = 0x20001478 就是導致異常的指令的地址
- R0 ~ R3、R12、LR 這些是發生異常之前寄存器中的值(可以與上面進入異常之前的圖中的值對比)
在異常入口處被壓入棧空間的數據塊被稱為棧幀
在 Disassembly 窗口中,轉到 返回地址中的顯示的地址
需要注意的是,換個地址其實是匯編語句的地址,如果直接看源碼,可能位置稍有偏差!
跟蹤(Trace)
??基于 Arm Cortex-M 處理器的設備使用 Arm CoreSight 技術引入了強大的調試和跟蹤功能。調試就像照相機,程序(在斷點處)停下來才能通過 JTAG/SWD 接口看調試信息;跟蹤(Trace)就像錄像機,可以通過 ETM 接口紀錄、回放整個調試過程。
ITM
??ITM(Instrument Trace Macrocell,測量跟蹤宏單元)是一個應用程序驅動的跟蹤源,它支持 printf 樣式調試來跟蹤操作系統(OS)和應用程序事件,并發出診斷系統信息。該塊的主要用途是:
- 支持 printf 風格調試
- 跟蹤操作系統和應用程序事件
- 發出診斷系統信息(DWT 生成這些包,ITM 發出它們)
- 時間戳功能 時間戳是相對于數據包發出的。 ITM 包含一個 21 位計數器來生成時間戳。 Cortex?-M4時鐘或串行線查看器(SWV)輸出的位時鐘速率為計數器提供時鐘。
??ITM 發出的數據包將輸出到 TPIU(跟蹤端口接口單元)。 TPIU 的格式化程序會添加一些額外的數據包,然后將完整的數據包序列輸出到調試器主機。在 STM32 中使用 ITM 之前,必須啟用調試異常和監視器控制寄存器的 TRCEN 位。
??串行線查看器(Serial Wire Viewer,SWV)提供來自 Cortex-M3/M4/M7 設備內各種來源的實時數據跟蹤信息。 當系統處理器繼續全速運行時,它通過 SWO 引腳傳輸。所以,ITM 機制要求使用 SWD 方式接口。
ETM
??ETM(Embedded Trace Macrocell,嵌入式跟蹤宏單元)可以重建程序執行。 使用數據監視點和跟蹤(DWT)組件或指令跟蹤宏單元(ITM)跟蹤數據,以及使用嵌入式跟蹤宏單元(ETM)執行跟蹤指令。
參考
總結
以上是生活随笔為你收集整理的ARM 之十二 Cortex-M 内核异常处理、异常定位方法、在线调试、Keil MDK-ARM 的使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: LoRa 之一 旧版驱动(sx12xxD
- 下一篇: ARM 之十四 ARMv9 架构前瞻