# STM32 開發板學習筆記
## 硬體
- STMicroelectronics NUCLEO-F446RE

本開發板所配備的 MCU 名稱為 STM32F446RE,此外它還配備了燒錄器 ST-LINK/V2-1 (開發板上靠近 USB 接口的那一小塊區域),因此不需外接燒錄器即可透過 USB 將程式燒錄進 MCU,同時也支援除錯功能 (SWD, Serial Wire Debug)。
此外,這塊板子還相容 Arduino 的腳位,可以直接將 Arduino 擴充板插上

- 開發板命名規則

## 為何要用 ARM-Cortex-M 處理器?
>Processor 是 MCU 的一部分,但 MCU 不只有 Processor!MCU 還有記憶體 (Flash、RAM)、周邊 (GPIO、I2C、ADC 等)一起組成完整的小電腦,而 Processor 相當於 MCU 的大腦。
這一款處理器主要應用於嵌入式系統當中,可以針對不同需求選擇不同型號處理器,例如智慧手環具有低功耗、低效能之特性,因此可選擇有此特性的處理器。以下整理其他優點:
1. 具有低成本、低功耗、低 silicon area。
2. 為 32 bit 之處理器,但其價位幾乎等同於 8 、 16 bit 傳統處理器。
3. 可以將基於此款處理器的 MCU 應用在超低功耗的高效能應用中。
4. 處理器可以根據 MCU 製造商的需求進行訂製,訂製內容包括以下 (etc.)
- 浮點運算單元 (Floating point unit, FPU) → 用來加速浮點數計算(例如科學計算、運算量大的演算法)
- 數位訊號處理單元(DSP unit) → 用來加速像是音訊、影像處理這類需要大量數學運算的應用
- 記憶體保護單元(MPU, Memory Protection Unit) → 用來強化系統安全,例如防止程式錯誤存取錯誤記憶體區域
5. 它具有非常強大且易於使用的中斷控制器,它支持 240 個外部中斷。
6. 對 RTOS 非常友好,它提供一些例外、處理器操作模式、訪問級別配置等功能,這些有助於開發 RTOS 的安全。
7. Cortex-M 系列的處理器,使用了一種叫 Thumb 指令集(Thumb instruction set)的技術。好處是能夠保有 32 位元 ARM 指令的性能,但用 16 位元的格式來表達,這樣可以讓程式更小、更省記憶體。
> 不過,Cortex-M 的處理器是不能直接執行傳統的 ARM 指令集(像是 ARM7 那種全 32 位元的指令)的,只能跑 Thumb 格式的指令。
## ARM-Cortex-M Processor 中有什麼?
一個處理器內部其實是由 Core 和其他外圍設備所組成

外部設備包含:
- ITM:其用途是**產生 debug 訊息** (可用來即時除錯),可以在 STM32 跑程式的時候,即時把文字訊息「偷」出來印在電腦的除錯視窗上,而且不會讓 MCU 停下來!
> 如果用一般的 printf 印字,會佔用 MCU 的 UART 或 USB 資源,而且會慢 (因為要排隊送資料)。但 ITM 是透過硬體級別的通道 (透過 SWV 傳),超級快,基本不會拖慢你的程式。 (對於一些要求速度和即時性的嵌入式系統來說,非常重要)
在 ITM 中有一個屬於它的暫存器叫做 FIFO,我們只要將 `printf` 寫入 FIFO 中,我們想要的 debug 資訊就會從 SWO 出來。
- SWD (Serial Wire Debug):SWD 是一種 MCU 用來「燒錄、除錯」的通訊協定,主要功能有
- 下載 (燒錄) 程式到 MCU 裡
- 即時除錯 (比如下中斷點、單步執行、讀寫記憶體)
- 讀取 / 修改 MCU 裡的暫存器、記憶體
簡單來說,SWD 是讓開發板和電腦之間可以燒錄、除錯的高速通道,其基本上具有兩個 pin 「資料線 SWDIO」(Data) + 「時脈線 SWCLK」(Clock),SWDIO 負責傳資料,雙向通訊 (讀 MCU 內容、寫 MCU 內容);SWCLK 負責提供同步時脈,讓資料穩定傳輸 (否則雙方不知道何時該讀該寫)。
若需要用到 trace 功能,MCU 可額外開通一個腳位 SWO (Serial Wire Output) 用來讀取 debug 訊息 (由 ITM 產生),再透過 SWV (Serial Wire Viewer) 這條通道來**傳送 debug 訊息**到電腦,因此 SWD 可具有 2 + 1 個 pin (2 debug pin, 1 trace pin)。
:notes: Trace 的分工可以比喻成:ITM 是工廠,產生 debug 訊息;SWV 是馬路,把訊息送到電腦上讓你看到 (像 printf 那種訊息)。
:::info
處理器的「Core」(核心) 指的是可以獨立執行指令的一套基本電路單元,裡面主要有:
- ALU (Arithmetic Logic Unit):算術邏輯單元,負責做加減乘除、邏輯運算 (像是 AND、OR、NOT)。
- 寄存器 (Registers):超快速的小型記憶體,用來暫存資料或指令。
- 控制單元 (Control Unit):負責解讀指令,控制資料要送到哪裡、怎麼運算。
- 暫存器檔 (Register File):存很多組寄存器,幫助資料快速流動。
- 程式計數器 (Program Counter, PC):記錄目前要執行哪一條指令。
- 有時候還會包含 FPU 與 DSP unit (依訂製需求)。
:notes: 處理器又稱為中央處理器 (Central Processing Unit, CPU),一個 CPU 所擁有的核心可能不只一顆。
:::
## 處理器的操作模式
ARM-Cortex-M 系列的處理器提供兩種操作模式:**Thread Mode** (執行序模式)、**Handler Mode** (處理者模式)。
1. 所有的應用程式碼都會在處理器的 Thread Mode 下執行,因此他又可稱為 **User Mode**。
2. 所有的例外 (Exception) 處理與中斷 (Interrupt) 處理都會在處理器的 Handler Mode 下進行。
3. 處理器在默認情況下都是由 Thread Mode 開始。
4. 無論何時,當 Core 碰到了系統例外、外部中斷時,都會將模式切換成 Handler Mode,並且去執行對應的 ISR (Interrupt Service Routine, 中斷服務程序)。
## 處理器的 Access Level
> ARM-Cortex-M 系列處理器
1. 處理器提供兩個 Access Levels:
- Privileged Access Level (PAL)
- Non-Privileged Access Level (NPAL)
2. 如果 code 是在 PAL 情況下執行的,則它具有完整的權限可以存取**所有**處理器的特定資源與受限制的暫存器。
3. 如果 code 是在 NPAL 情況下執行的,則它可能沒有權限存取處理器的某些受限制暫存器。
4. 默認情況下,code 都會在 PAL 情況下執行。
5. 當處理器在 Thread mode 時,是能夠將處理器改為 NPAL 的,一旦在 Thread mode 情況下將 PAL 改為 NPAL,則無法再將它改回 PAL,除非切換操作模式到 Handler mode。
6. Handler mode 總是以 PAL 來執行。
7. 如果想要修改 Access Level,可以使用處理器的 **CONTROL Register** 來修改。
## Core Registers
Core Registers 是指存在於處理器核心當中的 registers,我們可以在 Cortex-M4 處理器的 generic user guide 手冊中搜尋 Core Registers。

從上圖的配置可以知道:
- General-purpose registers (R0~R12, Total 13):用來做 data operation、data manipulation、store data、store address...etc 使用的。
- Stack Pointer (SP, R13):用來追蹤 stack memory。其中 PSP = Process Stack Pointer;MSP = Main Stack Pointer。
- Link Register (LR, R14):當遇到 subroutines、function calls 和 exceptions 時,LR 會儲存 return address (返回地址),以供 PC 返回原本的位置。當重置時,處理器會設定 LR 為 0xFFFFFFFF。
- Program Counter (PC, R15):它包含了當下準備執行的指令地址。當 reset 時,處理器會將 PC 載入地址 0x00000004 的值,也就是 reset handler 的地址。PC 的 bit 0 ( Bit [0] ) 在 reset 時會被載入 EPSR 的 T-bit,並且該值必須是 1。
- Program Status Register (PSR):它包含了當前執行程式的狀態,其實它的內部有三個不同的暫存器 APSR、IPSR、EPSR。
:notes: 所有的 Registers 都是 32 bits。
### Link Register (LR)
以下圖解 LR 的功用

首先,可以看到有兩個函式 fun1 和 fun2,其中 fun1 裡面呼叫了 fun2,因此他們關係為 fun1 是 Caller (呼叫者)、fun2 是 Callee (被呼叫者)。
當 fun1 呼叫 fun2 時,PC 會載入 fun2 的地址 (這在組合語言中稱為 jump),同一時間,LR 會儲存**下一個指令的地址**作為 **Return Address**。
當 fun2 執行完,PC 會載入 LR 所存的地址,回到 fun1 的後續指令中。
:::spoiler 小實作
我們可以打開[操作模式切換](https://hackmd.io/Z6PG01rUQTy06uuWjsbUSg?view#%E8%A7%B8%E7%99%BC-Interrupt-%E4%BB%A5%E6%B8%AC%E8%A9%A6%E6%93%8D%E4%BD%9C%E6%A8%A1%E5%BC%8F%E5%88%87%E6%8F%9B)的那個實作檔案來觀察 LR 的變化。
在 Debug 模式時,可以從右側的狀態欄觀察由機器碼轉回的組合語言 (此手段稱為 Disassembly),並且可以開啟 Instruction Stepping Mode 來一行一行運行組合語言。

可以發現當運行到第 36 行的程式碼 `generate_interrupt` 時,使用了組合語言 `bl` ,它是 Branch with Link 的縮寫,其功能是跳轉 (Branch) 到某個函數 (address) 並同時保存返回位置 (Link) 到 LR 中。
由 Disassembly 可以知道,下一個指令 (第 38 行程式碼) 的地址是 `0x8000246`,因此這個值會被存進 `lr`。當我們執行下一行組合語言時,程式進到 `generate_interrupt` 函式中,此時透過 Register 狀態欄觀察 `lr` 的值,可以發現它變成 `0x8000247`。

其實 `0x8000246` 和 `0x8000247` 是相同的地址,只是 `lr` 是要儲存給 PC 跳轉用的地址,而 PC 的 lsb (第 0 個 bit) 是有特別用途的:「bit 0 是用來告訴 processor 接下來要執行的指令是 ARM ISA 還是 Thumb ISA」。當 T-bit 為 0,則指令是 ARM ISA;反之,則是 Thumb ISA。詳細說明請見 [Program Status Register (PSR)](https://hackmd.io/Z6PG01rUQTy06uuWjsbUSg?view#Program-Status-Register-PSR) 的 EPSR register。
接著我們看 `generate_interrupt` 函式的最後一行組合語言 (第 29 行程式碼 `}`)

那一條 `bx` 指令是 Branch and Exchange 的縮寫,其功能是跳轉到某個暫存器中儲存的位址。在這邊是跳回 caller 用的。觀察圖中可以發現它後面接的暫存器是 `lr`,因此這一行是用來跳回 return address。
:::
### Program Status Register (PSR)
從 Cortex-M4 generic user guide 搜尋這個暫存器的配置

可以看到 PSR 這 32 bits 的暫存器中,還包含了三個不同的暫存器,它們個別佔用了不同的空間,並且彼此不重疊:
#### Application Program Status Register (APSR)
總共佔用 5 bits (bit 27~31),它包含了所有的 Conditional flags (條件旗標, 有 N、Z、C、V、Q),它們各自代表的意思如下

**這些 flags 會根據 ALU 的運算結果來自動更新**,提供系統判斷運算結果是否是負的 (N is set?)、是否為零 (Z is set?)、是否有進位或借位產生 (C is set?) 等等。
:::info
:notes: Flags 的使用
在**組合語言**中,可以直接對 APSR 的 flags 寫出條件指令,例如
```asm
CMP R0, R1 ; 比較 R0 和 R1 (其實在做 R0 - R1,ALU 會根據結果更新 APSR)
BEQ label ; BEQ = Branch if EQual,代表如果 Z = 1 (兩數相等),就跳轉到 label
```
其他還有很多會用到 APSR flags 的指令,例如:

而在高階語言 (如 C / C++),編譯器會幫你處理好,讓你無需關心 APSR,但底層其實還是在用它。例如以下 C 程式碼:
```c
if (a == b) { doSomething(); }
```
這段語法會由編譯器自動轉成上面的組合語言語句,你不需要碰 APSR,甚至不知道它存在。
:::
#### Interrupt Program Status Register (IPSR)
由暫存器配置圖可以知道,它佔用了 9 bits (bit 0~8),將 Interrupt Service Routine (ISR) 的例外類型號碼儲存在 ISR_NUMBER 當中,我們可以透過該數值知道處理器碰到了哪種例外狀況。使用方式展示在[測試操作模式切換](https://hackmd.io/Z6PG01rUQTy06uuWjsbUSg?both#%E8%A7%B8%E7%99%BC-Interrupt-%E4%BB%A5%E6%B8%AC%E8%A9%A6%E6%93%8D%E4%BD%9C%E6%A8%A1%E5%BC%8F%E5%88%87%E6%8F%9B)專案中。
#### Execution Program Status Register (EPSR)
它佔用了 PSR 的 9 bits (bit 10~15, 24~26),其中 bit 24 稱為 T-bit (Thumb state bit)。
接著來介紹一下 T-bit:
- 有些 ARM 處理器能夠支援 ARM-Thumb 互通,也就是說它能在 ARM state 和 Thumb state 做切換。
- 處理器必須在 ARM state 下才能夠執行 ARM ISA (Instruction Set Architecture) 的指令;反之,必須在 Thumb state 才能去執行 Thumb ISA 的指令。
- 如果 T-bit 為 set (1),則處理器會認為接下來要執行的指令是來自 Thumb ISA。
- 如果 T-bit 為 unset (0),則處理器會認為接下來要執行的指令是來自 ARM ISA。
- Cortex Mx 系列的處理器皆不支援 ARM state,因此 T-bit 的值必須總是 1。若沒有維持其值的話,則是違法的,並會造成 Usage fault 的例外。
- PC 的 lsb (Least Significant Bit, 最低有效位元,也就是 bit 0) 和 T-bit 是關聯的,**當我們想要將一個值或地址載入 PC 以前,則該值的 Bit [0] 會先被載入到 T-bit,而剩餘的位元才會被載入到 PC 作為指令地址用。也就是說要載入到 PC 的地址,會先清除掉 lsb,然後再向右移動 1 bit,才會載入到 PC 中**。
- 大多數情況,編譯器都會幫我們把要載入 PC 之值的第 0 個 bit 設為 1,所以我們在寫高階語言 (如 C) 時不用特別注意。**唯一需要注意的是當你直接使用 raw value 或 raw address 時,值一定要是奇數的,否則會造成例外** (例如 `fun_ptr = (*void)0x20001008`,然後再解參考該函數指標會導致 T-bit unset)。
- 這就是為甚麼 vector table 中所有 vector addresses 的值都會 +1 的原因,是為了讓處理器知道要執行的指令屬於 Thumb ISA。
實作與例外展示在專題[嘗試設置錯誤 T-bit 並觀察影響](https://hackmd.io/Z6PG01rUQTy06uuWjsbUSg?both#%E5%98%97%E8%A9%A6%E8%A8%AD%E7%BD%AE%E9%8C%AF%E8%AA%A4-T-bit-%E4%B8%A6%E8%A7%80%E5%AF%9F%E5%BD%B1%E9%9F%BF)中。
## Memory Mapped & Non Memory Mapped Registers
暫存器有分為 Memory Mapped Register (記憶體對映) 與 Non Memory Mapped Register 兩種,接著介紹它們的差異。
1. Non Memory Mapped Register
所有我們在 [Core Register](https://hackmd.io/Z6PG01rUQTy06uuWjsbUSg?both#Core-Registers) 當中的暫存器都屬於這一類,它們存在於處理器的內核,而不是記憶體的某個位址中,因此**不具有獨特的記憶體地址**可供 C 語言的解參考 (dereference) 或指標操作來存取 (C 語言是針對記憶體空間中的變數與裝置進行操作的)。若想要在程式中操作這些暫存器,必須透過**組合語言指令** (如 MOV、MRS) 或編譯器提供的 intrinsic function 來進行。
2. Memory Mapped Register
外設 (peripherals) 指的是實體功能模組,種類分為:第一種,由 ARM 公司設計,處理器內核標配的外設,如 NVIC、MPU、SCB、DEBUG 等等,稱為 Processor Specific Peripheral,每間向 ARM 公司購買處理器內核的廠商 (vendor) 都不能修改這個部分;第二種,由處理器 / MCU 廠商購買 ARM 授權的處理器內核,並依據需求加入的外設,稱為 Microcontroller Specific Peripheral (又稱 Vendor Specific Peripheral)。

這些外設的暫存器都是 Memory Mapped Register,它們是**連接外設與 CPU 的橋樑**,就像是「操作介面」一般。這些暫存器的地址會「映射」到 MCU 的記憶體空間中 (通常是某個固定範圍),因此透過這些暫存器所對應的一塊「特殊記憶體區域」,我們可以像操作變數一樣,用 C 語言直接對它讀寫,來控制外設。
## ARM GCC Inline Assembly Coding
Inline Assembly Code (內嵌組合語言) 是用來在 C 程式中寫純組合語言用的,不同編譯器的 syntax (句法) 不同,必須根據所使用的編譯器來使用正確的 syntax。
### 使用場合
你可能會好奇 Inline Assembly Code 會用在哪裡?為何我們需要在 C 程式中寫組合語言?以下兩種情境將會說明使用時機。
1. 想要將 C 的變數 `data` 的內容移到 ARM register `R0` 中,你該怎麼辦到?
2. 想要將 `CONTROL` register 的內容移到 C 的變數 `control_reg.` 中,該如何實現?
以上的操作都無法直接透過 C 語言達成,因為高階語言會自動安排變數與暫存器,若想指定變數與暫存器,需要比 C 還要底層的操作才能做到,那就是組合語言。
把組合語言寫在 C 程式中有幾個好處:
- 易於整理和管理
- 避免語言切換的 overhead
若想另外寫 `.s` (純組合語言) 檔案,要額外處理呼叫規約 (Calling Convention) 與連結方式。
- 組合語言不能單獨完成邏輯流程
真正的應用仍然以 C 為主,Inline Assembly Code 是用來「補充」特定 CPU 行為,而不是取代整個程式邏輯。
### ARM GCC syntax
ARM GCC 編譯器所使用的 Inline Assembly Code syntax 如下
```c
Assembly Instruction:
MOV R0,R1
Inline Assembly Statement:
__asm volatile("MOV R0,R1");
or
asm volatile("MOV R0,R1"); // "__" 可以忽略
```
當使用多個組合語言,可以考慮兩種寫法:
```c
Assembly Instruction:
LDR R0,[R1] // Read
LDR R1,[R2]
ADD R1,R0 // Modify
STR R1,[R3] // Store
Inline Assembly Statement:
void fun_add(void)
{
// method 1
__asm volatile ("LDR R0,[R1]");
__asm volatile ("LDR R1,[R2]");
__asm volatile ("ADD R1,R0");
__asm volatile ("STR R1,[R3]");
// method 2
__asm volatile (
"LDR R0,[R1]\n\t"
"LDR R1,[R2]\n\t"
"ADD R1,R0\n\t"
"STR R1,[R3]\n\t"
)
}
```
可以一行一行寫,也可以一次寫在同一個裡面,注意 method 2 裡的**指令間沒有 `,` 做區隔,並且在指令末端都要加 `\n\t`**。
Inline Assembly Statement 的形式如下圖 (僅限 ARM GCC 編譯器):

- volatile:是一個 optional type qualifier (選擇性的修飾符),加上這個修飾符可以防止編譯器自動幫你 optimize 你的組合語言 (code 部分),若沒有加入 volatile,編譯器可能會修改你的 code。
- code:以一段字串的形式輸入組合語言,如同上面看到的範例,不過因為上面的範例只有 code 的部分,沒有其他參數,所以省略冒號了,有沒有省略都沒差。
```c
// code
__asm volatile ("MOV R0,R1");
// equal to
__asm volatile ("MOV R0,R1":::);
```
- input operand list:input operand 的功能是把 C 的變數作為 source,輸入到組合語言中做使用,例如將 C 的變數 `val` 存進 register。
input / output operand 的格式都一樣,是由 constraint string + 小括號中 C expression,如下所示
```c
"<Constraint string>"(<'C' expression>)
// Constraint string = constraint character + constraint modifier
```
我們藉由 [Inline Assembly Statement 練習 (2)](https://hackmd.io/Z6PG01rUQTy06uuWjsbUSg?both#Inline-Assembly-Statement-%E7%B7%B4%E7%BF%92-2-Input-Operand) 來說明,下圖可以了解 input operand 的使用方式。

由於圖中例子沒有 output operand,因此 code 中的 `%0` 會由 input operand 的第一個取代。而圖中的 constraint string 只使用了 constraint character,沒有用到 modifier 所以沒寫上去。關於各個 character、modifier 所代表的意思,參見下表:

- output operand list:它的功能是讓 C 的變數可以作為組合語言的 destination,因此我們可以將暫存器儲存的值 write 進 C 的變數中。
它的語法:
```c
int val;
__asm volatile("LDR %0,=0x20":"=r"(val));
```
與 input operand 用法差不多,**唯一要注意的是 output operand 一定要加上 constraint modifier `=`**,從上面表格中可以知道這個 modifier 代表 operand 是 **write-only**,即 `val` 只可被寫入,不能被讀取。
- clobber list
## Reset Sequence
Reset Sequence 其實就是處理器進行 reset 後,在開始執行 main function 以前所做的初始化流程。處理器的 reset 其實很簡單,可以透過開發板上的 reset 按鍵進行、重新上電處理器也會進行 reset。
### 流程
以下是 Cortex-M 處理器的 Reset Sequence:
1. 當你 reset 處理器,PC 會載入 `0x00000000`。
2. 處理器會讀取記憶體位址 `0x00000000` 的值到 MSP (Main Stack Pointer register) 中 (MSP = value of `0x00000000`)。也就是說,處理器首先會初始化 Stack Pointer。
3. 接下來,處理器會讀取記憶體位址 `0x00000004` 的值到 PC,而 `0x000000004` 所儲存的值是 reset handler 的地址。
4. PC jumps to the reset handler。
5. reset handler 其實是我們自己根據初始化需求所編寫的 C 程式碼或組合語言。
6. 在 reset handler 中,我們會呼叫 main function,銜接回我們的應用中。
以上流程可以參考下方示意圖:

> 圖中的 reset handler 地址寫 `0x20001000`,但由於 Cortex-M4 一定是使用 Thumb ISA,照理來說載入 PC 的地址的 lsb 一定是 1,目前還不清楚講師投影片中為何地址是偶數的,但可以確定投影片的地址應該是錯的。
### Vector Table (向量表)
它是一張儲存中斷服務程序 (ISR) 與例外處理函數位址的表格 (跳轉地址),它在處理器開機時就存在於記憶體的特定位置。這張表告訴處理器在遇到「某種事件」時該跳去哪個位址執行對應的處理函式。而 stack pointer 的初始值位址、reset handler 的位址都可以透過此表來找到。
以下列出 Vector Table 的一些例子:
| Offset | 名稱 | 說明 |
| -------- | -------- | -------- |
| 0x00 | Initial SP value | 一開機時的 Stack Pointer 初始值 |
| 0x04 | Reset Handler | Reset 時 CPU 要跳去執行的程式碼 |
| 0x08 | NMI Handler | 非遮蔽中斷 |
| 0x0C | HardFault Handler | 嚴重錯誤處理程式 |
| ... | ... | 其他中斷與例外的 handler |
### Reset Handler
它通常會寫在 project 的 Startup file 裡,在 reset 後,處理器透過 vector table 找到它的位址並且執行它。

如果我們打開 Startup file 看到以下部分程式碼:

其中第 58 行組合語言就是在 reset 後,第一段被執行的指令。
Reset Handler 的功用主要有兩個:
- 做一些較前期的初始化。
- 最後呼叫 user program,也就是 main function。
在呼叫 main function 以前,Reset Handler 「必須」做三項初始化,否則 main 可能無法正常運行:

## Memory Map and Bus Interfaces
Memory map (記憶體映射圖) 就是處理器的「**地址地圖**」,讓你知道不同的地址範圍對應到哪種記憶體或哪個硬體設備的暫存器,幫助 CPU 用讀寫記憶體的方式來控制整個系統。
### 基礎知識與原理
在介紹 memory map 以前,先來了解一下 MCU 讀寫資料的基本知識及原理。
- Addressable memory location
是指「你可以直接存取的最小記憶體單位」。常見是 byte-addressable (一個記憶體地址可儲存 1 byte 的容量),也有 bit-addressable。
- Address Bus (Address Chanel)
是一組用於**指定地址**的控制信號線 (Bus 的翻譯為匯流排,可知是某種硬體設計),CPU 可以根據 Address Bus 的地址資訊,來 unlock 對應地址的暫存器或記憶體空間,以便後續的資料存取。Cortex-Mx 處理器的 Address Bus 具有 **32 bits** 的大小 (取決於硬體,例如它具有 32 條信號線),因此可以指定的地址範圍為 0 ~ $2^{32}-1$。
- Data Bus (Data Chanel)
是一組用來**傳輸資料**的訊號線,其會根據 Address Bus 所指定的地址,將資料傳進對應的暫存器或記憶體空間中。Cortex-Mx 處理器的 Data Bus 同樣具有 **32 bits** 的大小,這表示它在同一時間下,一次可以傳 32 bits 的資料。
:::info
:notes: 記憶體的容量限制是怎麼決定的?
其實記憶體的容量限制是根據 Address Bus 的寬度設計出來的。也就是說:
- 設計者決定 MCU 的 address bus 要用幾條線 (幾個 bits)。
- 如果用了 32 條線,那最多就能表示 4GB 的記憶體空間。因為通常記憶體空間為 byte-addressable,代表一個記憶體地址可以儲存 1 byte 的資料,$2^{32}$ 個地址則可以存 $2^{32}$ byte 的資料,也就是 4GB。
- 因此 Address Bus 和記憶體容量是「總線寬度 → 對應記憶體空間容量」的關係。
:::
我們可以根據下圖看到 CPU 和一些 MCU 外設 registers、記憶體的關係,**各個設備之間是透過 Bus 連接的**:

有一些可以注意的地方:
- 有兩種類型的 Memory,其中 Code memory 用來儲存**程式碼**的;Data memory 則儲存程式碼中的一些**暫時資料**。
- 假設今天處理器想要將 ADC 中某個 register 的資料存到 Data memory,流程是什麼?**第一步**、會先在 address bus 產生該 register 的地址,當地址 match 上後,register 會變為 unlocked 狀態,並將資料釋出到 data bus 上,並存入處理器的某個 register 中。**第二步**、再做一次同樣的事情,這次是將資料從處理器內部的 register 存進 Data memory 中。有沒有覺得這一系列的動作莫名的熟悉?沒錯,這就是組合語言的 「load」 和 「store」 在硬體層面實際做的事。
### Memory Map
如同在基礎知識部分提到的,大多數 Cortex-Mx 系列處理器的應用,都是使用 32 bits 的 address bus,它可以表示的地址範圍為 0 ~ 0xFFFFFFFF,因此對於 byte-addressable 的記憶體來說,其容量具有 4 GB 之多。
在這一段 addressable memory range 中,處理器劃分並固定了幾種不同的區塊,分別針對不同的用途來做使用,並且不可擅自更改,我們將定義用途、範圍的依據稱為 Memory Map:

:::info
Memory Map 可以比喻是「整個城市的地圖」,上面劃分了各種用途的地段 (像是 code 區、data 區、外設區、保留區...),但這座城市裡可能有些地段根本沒蓋房子 (沒裝置),或者只蓋了一間小屋 (小容量的 SRAM)。
:::
接著說明各個 region:
#### Code (0 ~ 0x1FFFFFFF)
儲存程式碼和指令的記憶體區域,也就是前面所說的 code memory。

- 不同類型的 code memory
在 Code region 中,可以實作不同類型的 code memory,他們各自有不同的用途。其中縮寫的原名為:ROM (Read-Only Memory)、OTP (One-Time Programmable)、EEPROM (Electrically Erasable Programmable Read-Only Memory)
| 類型 | 非揮發性 | 可重複寫入 | 擅長 | 應用範例 |
| -------- | -------- | --- | --- | -------- |
| ROM | ✅ | ❌ | 不需更改的程式 | Bootloader、工廠寫死的固定韌體 |
| OTP | ✅ | ❌ (只可寫入一次) | 單次燒錄 | 唯一識別碼、安全參數 |
| Flash | ✅ | ✅ (整個區塊擦除重寫) | 常用的程式區 | 主程式儲存、大量靜態資料 |
| EEPROM | ✅ | ✅ (只需擦除字節重寫) | 小資料頻繁寫入 | 使用者設定、感測器校正、計數等 |
:notes: 「非揮發性記憶體」是指當電流關掉後,所儲存的資料不會消失的資料儲存裝置。
- 處理器 reset 後,參照的 vector table 資訊預設是儲存在 code region 中。
#### SRAM (0x20000000 ~ 0x3FFFFFFF)
這一個 region 是用來連接 SRAM 的,它的大小有 512 MB 並且接續在 Code region 後面。SRAM 是一個 Data memory,用途是儲存 temporary data。

有幾點須注意:
- SRAM region 的前 1 MB 為 bit-addressable,其餘才是 byte-addressable。
- 在這一個 region 中,你仍可以儲存、執行程式碼或指令。
#### Peripheral (0x40000000 ~ 0x5FFFFFFF)
用於連接 「MCU」 的 on-chip peripherals (如 RTC、ADC、TIMERS),**處理器的 peripherals 並不會使用到這個區域** (如 NVIC)。

- 和 SRAM 一樣,Peripheral region 的前 1 MB 為 bit-addressable。
- 這個 region 是 eXecute Never (XN) region,意即無法在此區域執行程式碼,否則會觸發例外。這是為了避免 code injections attack 的風險,以防外設傳輸程式碼進來執行。
#### External RAM (0x60000000 ~ 0x9FFFFFFF)
先簡單介紹一下 RAM (Random-access memory) 是什麼,它是一種可隨時讀寫的記憶體,速度很快,通常作為運作中程式的臨時資料儲存媒介。它分為兩種類型,SRAM (剛提到過) 和 DRAM。RAM 有一個重要的特性,就是它屬於**揮發性記憶體**,當系統失去電源後,記憶體的資料就會遺失。
當你需要更多的記憶體大小來執行一些容量較大的專案,例如牽扯到圖像、影像、音訊等等的專案,你可以使用這個 region 來連接 external RAM。

- External RAM region 的大小有 1 GB。
- 可以在 External RAM region 執行程式。
#### External Device (0xA0000000 ~ 0xDFFFFFFF)
可以用於 external device 或是 shared memory。

- External Device region 的大小有 1 GB。
- External Device region 也是 eXecute Never (XN) region。
#### Private Peripheral Bus (0xE0000000 ~ 0xE00FFFFF)
Private peripherals 即為 [Memory Mapped & Non Memory Mapped Registers](https://hackmd.io/Z6PG01rUQTy06uuWjsbUSg?view#Memory-Mapped-amp-Non-Memory-Mapped-Registers) 中所提到的 Processor specific peripherals,包含 NVIC、System timer、System control block 等等,它們的 register 即是在這個 region 做使用。

- 大小有 511 MB。
- 是 eXecute Never (XN) region。
### Bus protocol and bus interfaces
如同[基礎知識與原理](https://hackmd.io/Z6PG01rUQTy06uuWjsbUSg?both#%E5%9F%BA%E7%A4%8E%E7%9F%A5%E8%AD%98%E8%88%87%E5%8E%9F%E7%90%86)所提到的,Bus 是用於進行 MCU 中各裝置之間通訊的硬體線路,這邊將介紹 Bus 的協議,以及不同種類的 Bus interfaces。
在 Cortex Mx 系列處理器中,Bus interfaces 是由 ARM 公司所設計的 Advanced Microcontroller Bus Architecture (AMBA) 規格進行規範,它管理了晶片系統中,on-chip communication 的標準。
> 註:「on-chip」 指的是 MCU 晶片上內建的硬體模組,非外接模組。
AMBA 規格支援了一些 Bus 協議:
- AHB Lite (AMBA High-performance Bus)
- APB (AMBA Peripheral Bus)
以下介紹它們的用途:
- AHB Lite bus 主要用於 **main bus interfaces**。
- APB bus 則用於 PPB (Private Peripheral Bus) 與一些 on-chip peripheral 的存取。存取過程中,會以 AHB-APB bridge 作為連接兩種協議的橋樑。
- AHB Lite bus 是 **high-speed** 的硬體設施,主要應用於需要**高操作速度的外設**。
- APB bus 則是相對 **low-speed** 的硬體設施,大部分不要求高操作速度的外設都會連接到這個 bus,這個設計可以幫助 vendors 降低功耗。
接著來看一下處理器的 Bus interfaces 有哪些:

可以看到處理器總共有 4 個 Bus interfaces,分別為 I-CODE、D-CODE、System、PPB,並且它們都是**透過 AHB bus 傳輸的**。
分別來看看它們的用途:
- 處理器有兩個 Bus interfaces 專門去存取 CODE memory (也就是 CODE region) 的資訊,分別是 I-CODE bus 和 D-CODE bus。
- I-CODE bus:負責**獲取指令、讀取 vector table**。
- D-CODE bus:負責**存取 data**,如常數 data 等等。
- System bus 介面則是負責存取 **Data memory** (SRAM、RAM 等等) 以及許多 **MCU 的 on-chip peripherals** 像是 ADC、DAC、TIMERS、CAN,另外也負責了 DEVICE region。
> memory 雖然也是 on-chip 的,但並不歸類於 peripherals。
- PPB bus 即是用於存取 [PPB region](https://hackmd.io/Z6PG01rUQTy06uuWjsbUSg?view#Private-Peripheral-Bus-0xE0000000-%EF%BD%9E-0xE00FFFFF) 的資訊。
:::info
:notes: 這邊可以注意到,CODE memory 和 DATA memory 雖然都是 memory,但處理器是用不同的 bus interface 來進行的。
:::
想要深入了解我們所使用之處理器型號的硬體配置,可以查看處理器的規格表。舉例來說,以下是 STM32F446RE 的 block diagram 片段:

可以看到 Cortex-M4 處理器在左上角,並且它列出了處理器的所有 bus interfaces,可以觀察到這些 bus 都連接到一個叫 「AHB BUS MATRIX」 的硬體中,它其實是 Cortex-M 系列 MCU 中的「多主多從匯流排」架構,它的功能是**讓多個「bus master」與多個 「bus slave」 (比如記憶體或外設)通訊,不會互相阻塞**。
>AHB Bus Matrix 是 SoC 設計的一部分,會由 vendor 負責整合與最佳化性能,ARM 只提供 Cortex-M 核心的標準設計 (AHB protocol 也是其中之一)。
其中,bus master 指的是可以**主動發起資料傳輸**的元件,像是 processer、USB、DMA、Ethernet 等等,有些我們可以在上圖中左側那排看到。**只要某個模組能「主動提出需求」,它就是 bus master**。
:::info
:notes: AHB Bus Matrix 想成是一個多對多的資料交換站,幫你處理資料從哪裡來,要送到哪裡,怎麼送。
:::
此外,當我們同時有多個 bus masters 要使用 bus 時,就會需要一項規則來控管誰先用、誰等一下,而擔任這個角色的就是 「Arbiter」(仲裁器),它的主要功能有:
- 根據某種規則 (優先權 / round robin) 決定誰獲得 bus 控制權。
- 防止 bus 壅塞或 deadlock。
:::info
:notes: 根據以上各設備的關係,可以想像整個 MCU 就像一個物流中心:
- 🚚 Bus master 就是不同的發貨人 (CPU、DMA、USB)
- 📦 Bus slave 就是收貨站 (RAM、Flash、Peripheral)
- 🚦 Bus arbiter 就是交通指揮員,決定誰先通過
- 🧠 AHB Bus Matrix 就是物流配送網,讓每個貨都能順利送達不塞車
:::
由 block diagram 可以知道,AHB1 是 main bus,大多數的 peripherals 都透過這個 bus 傳資料。
順著 AHB1 可以看到以下裝置:

這個就是 AHB-APB bridge,它可以在 AHB 信號與 APB 信號之間做轉換,也就是**銜接 AHB 協議與 APB 協議的橋樑**,一些對於操作速度要求沒那麼高的外設可以透過 APB 協議存取,而當處理器想要存取這些 APB 協議的外設時,會從 system bus interface (S-BUS) 出發,並透過 AHB bus matrix 連接到 AHB1 (也就是 main bus),最終再經由 AHB-APB bridge 轉到對應的 APB bus 中,完成通訊。
這邊稍微注意一下,圖中可以發現 APB bus 有兩條,APB1 和 APB2。而它們的通訊速度也不盡相同,可以由下圖得知:

## Project
### 透過 ITM 印出 Hello World
此小專案將實作如何把 `printf` 寫入 ITM 的 FIFO 暫存器中,並透過 MCU 的 SWO 引腳與 SWV 通道,將打印訊息傳回電腦,並顯示在 Console 上。
首先,複製以下程式碼
:::spoiler
```c
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// Implementation of printf like feature using ARM Cortex M3/M4/ ITM functionality
// This function will not work for ARM Cortex M0/M0+
// If you are using Cortex M0, then you can use semihosting feature of openOCD
/////////////////////////////////////////////////////////////////////////////////////////////////////////
//Debug Exception and Monitor Control Register base address
#define DEMCR *((volatile uint32_t*) 0xE000EDFCU )
/* ITM register addresses */
#define ITM_STIMULUS_PORT0 *((volatile uint32_t*) 0xE0000000 )
#define ITM_TRACE_EN *((volatile uint32_t*) 0xE0000E00 )
void ITM_SendChar(uint8_t ch)
{
//Enable TRCENA
DEMCR |= ( 1 << 24);
//enable stimulus port 0
ITM_TRACE_EN |= ( 1 << 0);
// read FIFO status in bit [0]:
while(!(ITM_STIMULUS_PORT0 & 1));
//Write to ITM stimulus port0
ITM_STIMULUS_PORT0 = ch;
}
```
:::
找到專案中的檔案 `\src\syscalls.c`,並將複製的程式碼丟進去。完成後,在同個檔案中可以找到 `_write()` 函式,它是比 `printf()` 還要更低級別的函式呼叫 (`printf` 層層剝開後實際上是呼叫 `_write`),它可以決定 `printf` 的資料最終要送往哪個設備。我們想將資料送到 ITM 中,因此做以下修改
```c
__attribute__((weak)) int _write(int file, char *ptr, int len)
{
(void)file;
int DataIdx;
for (DataIdx = 0; DataIdx < len; DataIdx++)
{
// __io_putchar(*ptr++);
ITM_SendChar(*ptr++);
}
return len;
}
```
接著,要確認專案的 SWV 有開啟,才能順利收到 ITM 的訊息,開啟路徑為:對專案按右鍵找到 Debug As → Debug Configurations → Debugger (調試器) 下滑即可看到。
開始 Debug 專案後,要開啟 ITM 專用的 Console,開啟路徑是:視窗上方 Window → Show View → SWV → SWV ITM Data Console。
打開 ITM Console 後,要選擇正確的 ITM port,因為 ITM 總共有 32 個 port,而我們在程式碼中是選擇將資訊傳到 port 0,要選對才看的到訊息。設定路徑:ITM Console 右上角設定圖示 → ITM Stimulus Ports → 勾選 0。
設定完後,要 **Start Trace** (很重要),位置在 ITM Console 設定圖示旁的那個紅色點點。接著就可以開始 Run Code 了。
Console 成功印出 Hello World

### 觸發 Interrupt 以測試操作模式切換
本實作要測試 operation mode 的切換,注意在默認情況下,處理器都會以 Thread mode 進行,只有在遇到系統例外和中斷時才會切換到 Handler mode,因此實作中的程式碼透過觸發 software interrupt 的方式來觀察處理器的模式切換。
首先將以下程式碼完整的覆蓋專案的 `main.c`。
:::spoiler
```c
/**
******************************************************************************
* @file main.c
* @author Auto-generated by STM32CubeIDE
* @version V1.0
* @brief Default main function.
******************************************************************************
*/
#if !defined(__SOFT_FP__) && defined(__ARM_FP)
#warning "FPU is not initialized, but the project is compiling for an FPU. Please initialize the FPU before use."
#endif
#include<stdio.h>
#include<stdint.h>
/* This function executes in THREAD MODE of the processor */
void generate_interrupt()
{
uint32_t *pSTIR = (uint32_t*)0xE000EF00;
uint32_t *pISER0 = (uint32_t*)0xE000E100;
//enable IRQ3 interrupt
*pISER0 |= ( 1 << 3);
//generate an interrupt from software for IRQ3
*pSTIR = (3 & 0x1FF);
}
/* This function executes in THREAD MODE of the processor */
int main(void)
{
printf("In thread mode : before interrupt\n");
generate_interrupt();
printf("In thread mode : after interrupt\n");
for(;;);
}
/* This function(ISR) executes in HANDLER MODE of the processor */
void RTC_WKUP_IRQHandler(void)
{
printf("In handler mode : ISR\n");
}
```
:::
可以注意到程式碼中一共有三個 functions,分別是 `main` 、 `generate_interrupt` (產生 software interrupt) 和 `RTC_WKUP_IRQHandler` (ISR 中斷服務程序),我們可以輕易地判斷出 `main` 和 `generate_interrupt` 是在 Thread mode 下執行,`RTC_WKUP_IRQHandler` 則是在碰到中斷時,於 Handler mode 下執行。
在實作此專案時,記得要開啟 SWV,開啟路徑可以參閱 [Hello World 專案](https://hackmd.io/Z6PG01rUQTy06uuWjsbUSg?both#%E9%80%8F%E9%81%8E-ITM-%E5%8D%B0%E5%87%BA-Hello-World),然後就可以進入 Debug 模式。
但我們該如何觀察處理器現在是什麼模式?我們可以透過 Cortex-M4 處理器的 generic user guide 手冊來查詢 **Interrupt Program Status Register**,可以看到以下表格

其中的值 ISR_NUMBER 可以協助我們查看處理器現在的狀態,當該值為 0 時代表處理器目前是 Thread mode,而當它是 「0 以外的值」 時代表處理器碰到了例外或中斷並處於 Handler mode,此時透過 ISR_NUMBER 的值我們也可以查表知道是哪一種的例外 / 中斷觸發了處理器的 Handler mode。
我們可以透過 Program Status Register 來觀察 ISR_NUMBER 的變化,此暫存器的名稱為 `xpsr`,打開暫存器狀態視窗的方式為:Window → Show View → Register → 右側出現 Register 欄位,將欄位中的 General Register 下拉選單打開,找到 `xpsr`,此時會看到下圖:

我們觀察 xpsr 的 Binary 數值可以知道處理器目前的模式,根據上面的 IPSR 表格,我們可以知道 ISR_NUMBER 是以 9 個位元表示 (bit 0~8),因此我們看 xpsr 對應的 bits 即可。從圖中可以看到目前 ISR_NUMBER 的值是 0,因此目前處理器是 Thread mode。
接著,為了方便觀察處理器的模式變化,我們在 ISR 程式碼的位置加一個 break point,來讓系統停留在 Handler mode。
```c
void RTC_WKUP_IRQHandler(void)
{
● printf("In handler mode : ISR\n");
}
```
開始執行程式碼,當停止在 break point 時,ISR_NUMBER 變了:

此時 ISR_NUMBER = 19,我們查表知道了觸發處理器進入 Handler mode 的是 IRQ3 情況,這也符合我們的預期,因為在程式碼中我們為了產生 IRQ3 而刻意觸發了 software interrupt。
```c
void generate_interrupt()
{
...
//generate an interrupt from software for IRQ3
*pSTIR = (3 & 0x1FF);
}
```
繼續執行程式碼,當脫離 ISR 後,ISR_NUMBER 又恢復成 0 了,表示處理器又變回 Thread mode。
### Inline Assembly Statement 練習 (1) Assembly Code
本專案將實際操作 inline assembly statement,包含從記憶體 `load` 兩個值,並且將它們進行 `add`,最後將結果 `store` 到記憶體中。
首先我們要先了解基本的組合語言:
- `LDR` (Load Register)
- 用途:從記憶體讀資料,放入暫存器。
- 語法:`LDR R0,[R1]` → 將位於 `R1` 所指向之記憶體位置的值,載入到 `R0`。
- 範例:
```assembly
LDR R0,=0x20000000 ; 把常數地址 0x20000000 放入 R0(符號加載)
LDR R1,[R0] ; 從記憶體位置 0x20000000 讀取值,放進 R1
```
- `ADD`
- 用途:把兩個暫存器的值相加,結果放入另一個暫存器中。
- 語法:`ADD R0,R1,R2` → `R0` = `R1` + `R2`
- 範例:
```assembly
MOV R1,#5
MOV R2,#3
ADD R0,R1,R2 ; R0 = 5 + 3 = 8
```
- `STR` (Store Register)
- 用途:把暫存器的值存入記憶體。
- 語法:`STR R0,[R1]` → 把 `R0` 的內容寫入 `R1` 所指向之記憶體位置。
- 範例:
```assembly
MOV R0,#0x123 ; 把常數寫入 R0
LDR R1,=0x20000000 ; R1 = 目的記憶體地址
STR R0,[R1] ; 將 R0 的值寫入記憶體位置 0x20000000
```
- `MOV`
- 用途:將一個值 (立即數或另一個暫存器的值) 複製到某個目的暫存器。
- 語法:`MOV <目的暫存器>,<來源>`
→ `<來源>` 可以是
1. 一個立即數 → 要加 `#` 符號。
2. 另一個暫存器。
- 範例:
```assembly
MOV R0,#5 ; 將數值 5 存到 R0 中
MOV R1,#0xFF ; 將 0xFF 存到 R1 中
MOV R2,R1 ; 將 R1 的值複製到 R2
```
:::info
:notes: 立即數 (immediate value, `#`) & 符號加載 (literal load, `=`)
從上面的組合語言功能可以發現,有些常數前面是加 `#`,有的則是加 `=`,這是根據數值的大小以及組合語言用法決定。
- `MOV` 只能用於值較小的立即數 (在 Thumb 指令集通常限制在 8-bit 可表示的值,例如 0~255 或某些變形),超過就會報錯。因此在常數前面要加 `#` 代表使用立即數。
- `LDR` 只能透過符號加載,因此要在常數前面加上 `=`。符號加載是透過編譯器做類似查表 (literal pool) 的方式載入較大的數。
以下是符號加載在做的事:
```asm
// when you write
LDR R0,=0x20000000
// PC 相對尋址,去 literal pool 中抓值 (實際組合語言做的事,可透過 Disassembly 觀察)
LDR R0, [PC, #offset_to_literal]
// somewhere in code memory
.literal_pool:
.word 0x20000000
```
:::success
想直接放數字 → 用 `MOV Rn,#num`
想放一個大數或變數地址 → 用 `LDR Rn,=value`
:::
有了以上基礎,接下來就可以開始實作了。開啟新的專案,並將以下程式碼複製到 main 檔中
```c=
int main(void)
{
__asm volatile("LDR R1,=#0x20001000"); // 講師混用 =#,我不確定有沒有差,
__asm volatile("LDR R2,=#0x20001004"); // 我測試 = 和 =# 的結果是一樣啦..
__asm volatile("LDR R0,[R1]");
__asm volatile("LDR R1,[R2]");
__asm volatile("ADD R0,R0,R1");
__asm volatile("STR R0,[R2]");
for(;;);
}
```
說明一下這邊在做的操作,首先 3、4 行先將我們想要的地址 (常數) 存進 `R1`、`R2` 中 (可以透過右側 Register 表觀察數值變化),接著就要將它們所存之地址指向的記憶體空間內部儲存的值 `[R1]`、`[R2]` load 到暫存器中。
不過我們還沒設定記憶體儲存的值是多少,可以透過路徑:Window → Show View → Memory 來打開下方操作介面,點擊 Monitors 右方的 + 號,並輸入地址 (如 `0x20001000`) 就能看到該地址附近所有記憶體空間所儲存的值,我們可以對表格按右鍵 → Format 來設定記憶體的格式,因為我們只是測試,因此設定 Row Size = 1、Column Size = 1。並且,在記憶體空間直接設定 `0x20001000` 所存的值是 `06`、`0x20001004` 所存的值是 `04`。
設定好記憶體空間儲存的值後,可以開始 step over 第 5、6 行,記憶體的值就會分別被載入 `R0` 和 `R1` 中,此時可以發現原本儲存在 `R1` 的常數地址 `0x20001000` 已經被 `R2` 所指向之記憶體的值 `04` 蓋過去了。

接著就一直 step over 並且一邊觀察 Register 的數值變化,第 8 行 `STR` 指令則可以透過下方 Memory Monitors 來觀察 `R2` 記憶體空間的數值變化。
如此一來,就成功使用 inline assembly statement 來進行底層操作,修改 Register 與記憶體的資料了。
### Inline Assembly Statement 練習 (2) Input Operand
本專案要實作將 C 程式的變數值移到暫存器當中,並初步了解 constraint string 的功能。首先複製以下程式碼到 main 檔中:
```c=
int main(void)
{
int val = 50;
__asm volatile("MOV R0,%0": :"r"(val));
for(;;);
}
```
上面程式碼想要做的事,是將變數 `val` 的值存進暫存器 `R0` 中,當中使用的 constraint character 是 `r`,我們透過 [ARM GCC syntax](https://hackmd.io/Z6PG01rUQTy06uuWjsbUSg?both#ARM-GCC-syntax) 中的 constraint string 表格即可知道,`r` 代表著 general register,意即 code 中 `%0` 的部分到時候會由 general register 取代,然後再複製給 `R0`。
這是什麼意思呢?我們先 build project,然後透過路徑:對專案按右鍵 → Properties → Location 右側 Show In System Explore 按鍵,即可導航到專案資料夾,接著進入專案資料夾 → Debug 資料夾 → 專案名稱的 LIST file,裡面呈現了編譯器生成的 Disassembly 程式碼,我們可以藉此觀察 main 中第 4 行程式碼產生的組合語言。

發現有兩個指令產生,第一個指令是先將 `val` 的值載入到暫存器 `R3`,到了第二個指令再把 `R3` (source register) 的值複製到 `R0` 當中。會使用到暫存器就是因為我們設定的 constraint character `r`。
如果我們使用別的 constraint character 呢?這次我們改為使用 `i` 看看。不過 `i` 是給 immediate value 用的,所以 C expression 要輸入 immediate value,如下:
```c
__asm volatile("MOV R0,%0": :"i"(0x50));
```
再去看看 Disassembly

發現這次沒有透過 source register 複製值了,而是以 immediate value `#80` (後面的註解 `@ 0x50` 告訴你它是由誰轉成十進位) 取代 `%0` ,複製到 `R0` 中。另外補充,`mov` 後面的 `.w` 是指令寬度的限定修飾子 (width specifier),是編譯器標註的,用來讓你知道該指令是用 32-bit 編碼方式儲存的。
了解 constraint string 的用途後,可以執行程式碼,並觀察 Register 欄位 `R0` 的數值變化,看看 C 變數的值是否有成功存進目的地暫存器 (destination register) 中。
### Inline Assembly Statement 練習 (3) Output Operand
本專案會練習操作 output operand 和 input、output operand 同時使用的情況。
#### 讀取 CONTROL register 的值到 C variable
首先,嘗試將 CONTROL register 的值存到 C 變數 `control_reg` 裡,方法如下:

可以看到使用了組合語言 `MRS`,它的功能我們可以由 generic user guide 查閱,位置在:Chapter 3. The Cortex-M4 Instruction Set → Miscellaneous Instructions 可以看到。

由描述,可以知道這個指令是用在 sepcial registers 的,而 CONTROL register 就是 special register 的其中一個,因此這邊會使用它 (描述中的 「register」 指的是 general purpose register)。注意,查看 `MRS` 的指令說明就可以知道它所使用的 destination 必須要是 register,因此 constraint character 必須要用 `r`,用 `i` 的話會出錯。
如果將上方程式碼編譯後,由 `.LIST` 檔查看其 disassembly 如下:

可以看到先由 `R3` 儲存 CONTROL register 的值,再將 `R3` 的值存進 `[r7, #4]` 之記憶體空間裡。其中 `#4` 代表著「偏移量」,故 `r7, #4` 的意思是「`R7` 所儲存之地址加上 4 bytes」,因此可以知道編譯器將變數 `control_reg` 儲存在該處;而 `[ ]` 代表 deference 的意思,也就是該**記憶體空間地址所儲存的值**。
#### Copy the content of C variable to another variable
延伸應用練習,這個案例是將一個 C 的變數複製到另一個 C 的變數,soruce 和 destination 都是 C 變數,因此 input operand 和 output operand 同時使用到了,需注意的是指令後面 `%` 的 index (由左而右,0, 1, ...)。

> 可以加 volatile
#### Copy the content of a pointer into another variable
這個延伸的重點是 source 為指標,若要讀取的是地址所存的值的話,指令記得要做 deference `[ ]`。

我們來看看它的 disassembly:

其實我在這邊有點混淆指標和 register 所扮演的角色,因此在此做整理加強一下記憶。
- `p2 = (int*)0x20000008`
- 因為要存的地址是大數,故透過[符號加載](https://hackmd.io/Z6PG01rUQTy06uuWjsbUSg?view=&stext=17942%3A204%3A0%3A1746865109%3AqHYahJ) `[pc, #12]`,將 `0x20000008` 存入 `R3` 中。
- 將 `0x20000008` 從 `R3` 存入 `r7, #8` 地址中,代表 `p2` 這個變數儲存在記憶體地址 `R7 + 8` 裡。
- `__asm volatile("LDR %0,[%1]":"=r"(p1):"r"(p2));`
- 將 `r7, #8` 儲存的值 (`0x20000008`) 載入 `R3`。
- `[r3, #0]` 其實等價於 `[r3]`,因為偏移量為 0。而這一條指令是將 `R3` 所存之地址 `0x20000008` 當中儲存的值 (也就是 `*p2`,看記憶體在該地址存什麼值) 載到 `R3`。
- 最後,從 `R3` 將值存到 `r7, #4` 地址中,也就是存進 `p1`。
:::info
:thinking_face: Thinking
這個部分地址與解參考容易讓我混淆,總之,**地址就是資料儲存在記憶體中的位址;解參考就是去打開地址所儲存的內容物**。
另外,指標和 register 所扮演的角色其實有時候有點相似,它們都可以用來儲存記憶體地址。不過指標是 C 語言的變數,**其底層也是用到 register 來存資料** (看上面的 disassembly),而且 register 的應用更加地廣,它還能儲存其他數值。
:::
### 修改處理器的 Access Level (用途解析)
本專案將修改 thread mode 的 access level 變為 unprivileged,並觀察之後若嘗試存取需要權限的資料時,會發生什麼狀況。
本專案會使用[觸發 Interrupt 以測試操作模式切換](https://hackmd.io/Z6PG01rUQTy06uuWjsbUSg?both#%E8%A7%B8%E7%99%BC-Interrupt-%E4%BB%A5%E6%B8%AC%E8%A9%A6%E6%93%8D%E4%BD%9C%E6%A8%A1%E5%BC%8F%E5%88%87%E6%8F%9B)專案的程式碼進行測試,並會進行修改,修改部分如下:
```c=
void change_access_level_unpriv(void)
{
// read
__asm volatile("MRS R0,CONTROL");
// modify
__asm volatile("ORR R0,R0,#0x01");
// write
__asm volatile("MSR CONTROL,R0");
}
int main(void)
{
printf("In thread mode : before interrupt\n");
change_access_level_unpriv();
generate_interrupt();
printf("In thread mode : after interrupt\n");
for(;;);
}
void HardFault_Handler(void)
{
printf("Hard fault detected\n");
while(1);
}
```
其中 `change_access_level_unpriv` 是新加入的 function,作用是修改 access level 到 unprivileged,而修改方法我們可以查看 generic user guide 中 CONTROL register 的說明:

可以看到第 0 個 bit 為 nPRIV,它可以用來修改 privilege level,因此我們要做的就是 set CONTROL register 的第 0 個 bit,讓 access level 變為 unprivileged。
看到程式碼第 4 行,首先透過指令 `MRS` 來把 special register `CONTROL` 的值傳到 `R0` 中。
再來就是要修改 bit 0 的值了,這裡可以使用指令 `ORR` 來做 OR 運算,可以在 instruction set 中的 general data processing instruction 找到它的說明,語法大致是:
```
ORR {Rd},Rn,Operand2
Rd: Specifies the destination register.
Rn: Specifies the register holding the first operand.
Operand2: Is a flexible second operand.
```
因此我們可以將 `R0` 和 immediate value `#0x01` 做 OR 運算,再把結果存進 `R0` 中,就成功 set 第 0 個 bit 了,如程式碼第 6 行。
最後,將修改的值存進 CONTROL register 就大功告成,我們使用指令 `MSR` 來將 `R0` 的值存進 special register `CONTROL` 中,如程式碼第 8 行,此時 access level 就成功改成 unprivileged 了。
另外,`HardFault_Handler` 是用來處理例外的 handler,先知道有這個東西就好。在它裡面加了一個無窮迴圈 `while(1)` 的目的是要困住程式的運行,其原因會在待會說明 unprivileged 用途的時候說到。
現在我們來看看原程式碼觸發中斷的 function:
```c=
void generate_interrupt()
{
uint32_t *pSTIR = (uint32_t*)0xE000EF00;
uint32_t *pISER0 = (uint32_t*)0xE000E100;
//enable IRQ3 interrupt
*pISER0 |= ( 1 << 3);
//generate an interrupt from software for IRQ3
*pSTIR = (3 & 0x1FF);
}
```
程式碼中 `pSTIR` 和 `pISER0` 所指向的記憶體地址,其實是 Cortex-M4 處理器特定的系統控制暫存器地址,而從第 7、10 行程式碼可以看到正在嘗試做解參考來修改暫存器所儲存的值。然而,這些暫存器是需要權限才可以操作的,若處理器的 access level 是處於 unprivileged 的話,會碰到例外 (HardFault),並且 PC 會 jump to `HardFault_Handler`。
原理都知道了,接著就可以開始執行程式碼,觀察 register 的數值變化。開始執行後,首先觀察 CONTROL register 的值:

可以看到 bit 0 (nPRIV) 是 unset 的,因為默認情況下,code 都會以 privileged access level 執行。不過,可以發現它其實只有顯示第 0 個 bit 而已,因為 IDE 通常只會顯示與當前 processor 狀態「有意義且已啟用的欄位」。
接著,透過 function `change_access_level_unpriv` 來修改 CONTROL register 的值後,可以觀察到 nPRIV 的值變成了 1,也就代表處理器 access level 現在是 unprivileged。

成功在 thread mode 修改 access level 了,現在嘗試存取看看需要權限的系統暫存器會發生甚麼事。
當執行到上方的第 7 行程式碼時,可以發現程式跳轉到 `HardFault_Handler`,並且由 `xpsr` register 查看發現 ISR_NUMBER = 3

由 [ISR_NUMBER 表格](https://hackmd.io/Z6PG01rUQTy06uuWjsbUSg?view=&stext=16739%3A57%3A0%3A1747123090%3A_YBjVV) 可知,例外狀況確實為 HardFault。我們也可以透過路徑 Window → Show View → Fault Analyzer (中譯:故障分析器) 查看報錯資訊。

可以發現存取特定暫存器若不具有權限是屬於 usage fault。實作到這邊就算結束了。
#### 用途
那麼修改 access level 對我們有甚麼用處?這邊舉嵌入式系統的一種應用為例:RTOS (Real-Time Operating System)。RTOS 分為兩個部分:Kernel 與 User task,其中要限制 User task 不能夠擅自修改系統層級的設定,例如不能擅自觸發中斷等等。因此可以將權限更改為 unprivileged,這樣當 User task 嘗試存取需要權限的資訊時,處理器就會報錯。在 RTOS 中, User task 若需要任何服務,可以呼叫特定 function,然後通過 kernel 端來服務。
:::info
:notes: 無窮迴圈 `while(1);`
在 `HardFault_Handler` 中可以看到寫了一個無窮迴圈,它的作用其實是「安全中止」。一旦進入 HardFault,代表發生無法容忍的錯誤,讓程式繼續跑下去可能會破壞資料或造成不預期行為,故使用它來中止程式運行下去。
它有幾項好處:
- 方便偵錯與分析
當你把開發板連上 debugger (如 Keil、STM32CubeIDE、GDB),看到程式卡在 HardFault_Handler(),你可以:
- 檢查堆疊內容 (找出造成錯誤的指令與原因)
- 檢查暫存器 (像是 LR, PC, xPSR)
- 找出錯誤的 memory access
- 在開發階段提醒開發者發生錯誤
比方說你程式碼中不小心存取了未初始化指標,這類錯誤通常會導致 HardFault,這時就會直接被「丟進」 `while(1)`,開發者可以立刻注意到問題而不是讓錯誤默默地過去。
在一些高可靠度的實務中,甚至會在 handler 加入記錄錯誤狀態 (log)、重啟系統、切換到備援任務、通知外部監控系統等功能,不會單純只做安全中止;而在開發期間可以加入 debug 輸出,例如透過 printf 或 LED 閃爍來提示錯誤來源。
:::
#### 如何切換回 Privileged Access Level?
實作中,當我們將權限切換到 umprivileged access level (CONTROL = 1) 後,由於不再具有存取 CONTROL register 資料的權限,因此若一直處於 thread mode 的話,是不可能切換回 privileged access level (CONTROL = 0) 的。
只有唯一一種方法能夠實現切換,那就是**觸發例外或中斷**,因為**當進入到 handler mode 後處理器會無視 CONTROL register bit 0 的值強行變成 PAL**,此時就能夠去修改 CONTROL register 的值。當處理器回到 thread mode 時,會參考 CONTROL register 來判斷 access level 是 privileged 還是 unprivileged,這就是切換回 PAL 的唯一途徑。

### 嘗試設置錯誤 T-bit 並觀察影響
我們使用[修改處理器的 Access Level](https://hackmd.io/Z6PG01rUQTy06uuWjsbUSg?view=&stext=25866%3A485%3A0%3A1747915304%3ATpNy_u) 的程式碼來觀察,並將 main 檔做以下更改
```c=
int main(void)
{
printf("In thread mode : before interrupt\n");
void (*fun_ptr)(void);
fun_ptr = change_access_level_unpriv;
fun_ptr();
printf("In thread mode : after interrupt\n");
for(;;);
}
```
其中第 5 行程式碼的 `fun_ptr` 稱為「函式指標」,它可以指向其他函式的地址,如同第 7 行程式碼就是將指標指向 `change_access_level_unpriv` 這個函式。這邊可以注意一下,由於函式指標在指向時,會做隱式轉型,所以 `fun_ptr = change_access_level_unpriv` 和 `fun_ptr = &change_access_level_unpriv` 其實是一樣的。
接著,第 9 行為函式指標的「解參考」,也就是像一般函式使用一樣在後面加上 `();`,此時 PC 就會跳轉到 `fun_ptr` 所指向的地址,也就是去執行 `change_access_level_unpriv` 函式。這邊也有一點可以了解一下,就是 `fun_ptr();` 和 `(*fun_ptr)();` 都可以達成一樣的目的。
接著執行程式,一行一行執行,當跑完第 7 行程式碼後,可以看到右手邊 Variables 欄位中的變化,`fun_ptr` 指向的函式地址為 `0x8000205`,是一個奇數地址。

然而,我們透過 LIST 檔來檢查 `change_access_level_unpriv` 函式的地址,發現這個函式的第一條指令地址為 `0x8000204`:

這是因為編譯器自動幫我們處理了 lsb (要給 T-bit 的資訊),讓 `change_access_level_unpriv` 的地址在被存入 `fun_ptr` 以前,先變為奇數的。
而當程式碼執行到第 9 行時,地址 `0x8000205` 的 lsb 會複製給 T-bit,剩餘的位元則會向右 1 bit 進行對齊,並載入給 PC 作為跳轉地址使用。
我們觀察一下 Disassembly,可以看到:

其中的 `R3` 儲存著 `0x8000205`,而 `blx` 代表 「Branch with Link and exchange instruction set.」,他的功能和 `bl` 差不多,都是讓 PC 跳轉到對應地址,但它還會**根據 `R3` 所儲存地址的 lsb 來更新 T-bit,而 `bl` 則是永遠使用 Thumb state 來執行程式**。
接著以 Instruction stepping mode 來單行指令執行,PC 跳轉到 `change_access_level_unpriv` 後,我們觀察一下暫存器資料:

從上途中可以看到的資訊有:
- `R3`:`0x8000205`,包含了跳轉地址與 T-bit 資訊。
- `PC`:`0x8000204`,載入了跳轉地址。
- `xpsr`:這個是 EPSR 的資訊,我們由 Binary 可以看到第 24 位元 (T-bit) 為 set,代表處理器為 Thumb state。
接著,我們來測試直接給函式指標一個偶數的 raw address 會怎樣,我們將 `fun_ptr` 用 `change_access_level_unpriv` 的常數地址初始化:
```c
fun_ptr = (void*)0x08000204;
```
接著執行程式:

我們可以看到若使用偶數的 raw address 來初始化函式指標,編譯器不會自動幫你設定 lsb,因此 `fun_ptr` 指向的地址仍是 `0x08000204`。
接著繼續單行指令執行,當一執行跳轉指令 `blx` 後,會發現運行點跳轉到 `HardFault_Handler` 了。透過 Show View → 故障分析器 (Fault Analyzer) 查看錯誤資訊,可以看到:

資訊表示我們切換到無效的狀態,也就是說 Cortex-Mx 處理器系列只有 Thumb ISA,但我們給處理器看的 T-bit 資訊卻是 unset (0,因為地址是偶數),因此導致處理器使用 ARM state 去執行 Thumb 的指令。
## 環境設置
### FPU is not initialized
若編譯後出現這個警告訊息,只要透過設定流程:對 project 按右鍵 → Properties → C/C++ Build → Settings → MCU/MPU Settings → Floating point unit 改成 None → Floating point ABI 改成 Software implementation。