# 【1】Jserv mini-arm-os 學習筆記_開機流程x脫離IDEx你好 contributed by AgainTW --- # 章節 * [【1】Jserv mini-arm-os 學習筆記_開機流程x脫離IDEx你好](https://hackmd.io/@nfUUgsYRTGy81y5d9AYOyg/rkJtpGdAT) * [【1】Jserv mini-arm-os 學習筆記_Extra_1_按鈕x輪詢x中斷](https://hackmd.io/@nfUUgsYRTGy81y5d9AYOyg/H1dYPF-10) * [Jserv mini-arm-os 學習筆記 (二)](https://hackmd.io/@nfUUgsYRTGy81y5d9AYOyg/B1OUX65Cp) --- # 前言 * 本系列學習筆記指在使用最新的工具鍊和硬體實現 Jserv 老師的 [mini-arm-os](https://github.com/jserv/mini-arm-os),分享我在 trace 程式碼的過程中學到的東西、根據我選用的硬體所進行的修改以及實際操作演示。過程中參考大量網路上的資源、工具的文件、原始碼和 STM32 的設計手冊。如果本系列文章有任何錯誤的地方希望各位讀者們能不吝指教。 * 我的環境 * 作業系統: windows 11 * 處理器: 11th Gen Intel® Core™ i5-11400H @ 2.70GHz * 目標裝置: stm32f429I-DISC1 ![image](https://hackmd.io/_uploads/Bkqd8jc0a.png) --- # 名詞解釋 * LSB: Least Significant Bit。最低有效位,是指一個二進位數字中的第 0 位(即最低位)。 * Thumb Mode: 是 ARM 為資源有限的系統開發的指令運行模式,是為了減少 ARM 指令所需使用的空間而存在的。ARM 指令需要 32 bit 來儲存,Thumb 則是只需要 16 bit 即可儲存。 * MPU: Memory Protection Unit。是提供記憶體保護的電腦硬體單元,它通常作為中央處理單元(CPU)的一部分來實現。 * 允許特權軟體定義記憶體區域並為每個記憶體區域分配記憶體存取權和記憶體屬性。 * 監視事務(transactions),包括來自處理器的指令獲取和資料訪問。 * RCC: Reset Clock Controllerreset。重置與時脈控制,重置的部分提供了三種類型,system、power 和 backup reset。 * flash banks: flash 內部大小定義的其中一種。STM32F42x 系列就分為 2 個 bank。 * 目的檔: Executable and Linkable Format。格式為```.ELF```。 --- # 事前環境建置 * 本系列文章是在 Windows 作業系統下進行 STM32 的裸機開發,脫離 Keil 或是 IAR 等 IDE 使用 makefile 和開源的工具鍊(toolchain)建置程式碼,因此在此之前需要先完成相關環境的設定。 * [環境設定教學](https://hackmd.io/Plntmoz_SHOC4EmgtGCZJA?view)。 --- # 初入 STM32 韌體開發 基本上拜讀 Jserv 老師的文章[「嵌入式系統建構:開發運作於STM32的韌體程式」](https://wiki.csie.ncku.edu.tw/embedded/Lab19/stm32-prog.pdf)即可。 ## 嵌入式硬體設計 * STM32 最大可定址的大小為 32 bytes,也就是 4GB 的地址空間,但實際上一般的晶片並不會配有這麼大的記憶體(Flash)。其原因之一就是我們並不想讓所有的記憶體位址全部連在一起,下圖是 STM32F427xx、STM32F429xx 系列的 memory map,它們大致被分為 8 塊(Block),每塊的大小都有 512 MB。 ![image](https://hackmd.io/_uploads/Bkyq69tR6.png) * STM32 的記憶體大致有二: * 快閃記憶體(Flash): 在沒有電源時資料仍然能儲存資料。這也是我們程式存放的位置。 * 靜態隨機儲存記憶體(SRAM): 這種記憶體只要保持通電,儲存的資料就可以恆常保持,反之停止通電時資料就會消失。 * 一般來說,SRAM 的速度快於 Flash,因此我們會將我們的程式碼燒進 Flash。在嵌入式工作時,則會將資料和指令從 Flash 搬到 SRAM,用以加速處理器的資料讀寫。 * 內建的 Flash 大小為 2 MB,下圖為 Flash 的實體記憶體位址的分類,我們可以看出 Flash 的起始位址為 0x08000000。 ![image](https://hackmd.io/_uploads/H1uzdnY0T.png) ![image](https://hackmd.io/_uploads/SywiiqtAp.png) * 內建系統 SRAM 大小為 256 Bytes,下圖為 SRAM 的映射位址。 ![image](https://hackmd.io/_uploads/B1MJFnKRp.png) * Cortex-M4 的 MPU 將 memory map 切成幾塊區域,並定義每一個區域(region)的位置、大小、存取權限還有記憶體屬性(attributes)。 * 每一個區域可以有獨立的屬性設定。 * 區域可以 overlapping。 * 可以 export 記憶體屬性給系統。 ## 嵌入式程式的運行方式 * STM32 的晶片上都有兩個腳位 BOOT0 和 BOOT1,這兩個管腳在晶片重設時的電平狀態決定了晶片重設後從哪個區域開始執行程序,也就是進入那種模式。 ![image](https://hackmd.io/_uploads/rkJAfiYRa.png) * 嵌入式處理器 Reset 後到進入 main() 函數的過程: 1. 嵌入式處理器 Reset 後會進行「取得指令—解碼—執行」的循環。ARM Cortex M4 做的第一件事,就是讀取下列兩個 32 位元整數的值。 * 從位址 0x00000000 處取出 MSP (Main Stack Point) 的初始值。 * 從位址 0x00000004 處取出 PC (program counter) 的初始值,處理器隨即自這個值所對應的位址處取值。 2. 重設中斷服務程序 Reset_Handler 會呼叫 SystemInit 函數,進行對系統時脈的初始化。 3. 程式會執行到指令```LDR R0, =__main```,然後就跳到```__main```程式段運行,```__mian```是標準函式庫中的函數,其會呼叫 c 檔案中的 main 函數。 4. 最後也就是進入 C 檔案中的 main 函數。 ```mermaid flowchart LR; Reset --> 讀取MSP和PC 讀取MSP和PC --> 系統時脈的初始化 系統時脈的初始化 --> 執行到main函數 ``` * 因此下面的程式碼即可達到我們的期望 * ```asm (".word 0x20001000");```: 即堆疊大小為 0x1000 (計算方式: 0x20001000 - 0x20000000 = 0x1000; 也就是 4KB) ```c= asm (".word 0x20001000"); // 指定 MSP 為 0x20001000 asm (".word main"); // 進入 main 函式 main() { … } ``` ## 嵌入式的 Hello World! 在嵌入式系統中,由於缺乏寬敞的螢幕,通常無法輸出文字訊息。 因此,我們可以使用 LED 來進行輸出,以此作為嵌入式的 "Hello World!"。同時,stm32f429I-DISC1 內建兩顆預設的 LED,我們將設計嵌入式的程式,交錯地讓這兩顆 LED 一亮一暗。 ### 嵌入式程式的撰寫流程 ```mermaid flowchart LR; 確認目標和功能 --> 查找相關暫存器設定 查找相關暫存器設定 --> 程式分段規劃 程式分段規劃 --> 程式分段撰寫 ``` ```mermaid flowchart LR; 程式分段撰寫 --> 暫存器設定 暫存器設定 --> 主程式撰寫 主程式撰寫 --> 連結器腳本撰寫 連結器腳本撰寫 --> Makefile撰寫 ``` ![image](https://hackmd.io/_uploads/BkoK6h9R6.png) ### 暫存器設定 1. 啟用(enable) G 號 GPIO port 的時鐘。 2. 配置 G 號 GPIO port 的 13 腳(PG13)和 14 腳(PG14)的讀寫模式。 3. 配置 G 號 GPIO port 的 PG13 和 PG14 為通用推拉輸出模式(output push-pull)。 4. 配置 G 號 GPIO port 的 PG13 和 PG14 為下拉電阻。 5. 間歇地設定 PG13 和 PG14 的值為 0 和 1,也就是控制 LED 燈的亮滅。 * 根據手冊查找暫存器設定 * 參考的手冊 * ![image](https://hackmd.io/_uploads/BJqc6X_Ra.png) * D 號 GPIO port 的時鐘設定 * ![image](https://hackmd.io/_uploads/r10Rk6F0p.png) * GPIO mode 設定 * ![image](https://hackmd.io/_uploads/ryZdKBdCp.png) * GPIO 輸出模式設定 * ![image](https://hackmd.io/_uploads/ryfNeptRa.png) * ![image](https://hackmd.io/_uploads/H1QBxatCT.png) * GPIO Push-Pull 設定 * ![image](https://hackmd.io/_uploads/ryEV_BOAa.png) * GPIO 輸出設定 * ![image](https://hackmd.io/_uploads/S13simdC6.png) ### 程式碼撰寫 這是根據 Jserv 老師文章改寫的第一版程式碼。因為是初入嵌入式設計,因此完整地呈現程式碼,之後的程式碼我會放到我的 Github 上,並只節錄重要的片段。 * Project struct ```shell / ├── main.c ├── main.ld └── makefile ``` ### ```main.c``` * 程式中有兩個 for 迴圈用來延時,STM32 執行的速度相當快,若不加延時的話,LED 燈閃爍的頻率會非常高 * 可以從 User mananul 查詢各個 register 的 memory map ```c= // set register address #define RCC_ADDR 0x40023800 #define GPIO_G_ADDR 0x40021800 #define RCC_AHB1_PERI_ENBLR_ADDR (*((volatile unsigned long *) (RCC_ADDR+0x30))) #define GPIO_G_PORTMODE (*((volatile unsigned long *) GPIO_G_ADDR)) #define GPIO_G_OUTPUT_TYPE (*((volatile unsigned long *) GPIO_G_ADDR+0x4)) #define GPIO_G_PUPD (*((volatile unsigned long *) (GPIO_G_ADDR+0xC))) #define GPIO_G_PORT_SETRESET (*((volatile unsigned long *) (GPIO_G_ADDR+0x18))) asm(".word 0x20001000"); asm(".word main"); int main() { int i; RCC_AHB1_PERI_ENBLR_ADDR |= (0x00000001 << 6); // Enalbe GPIO G clock GPIO_G_PORTMODE |= (0x00000001 << 26 | 0x00000001 << 28); // Set PG13 PG14 mode to GPIO GPIO_G_PORTMODE &= ~(0x00000001 << 27 | 0x00000001 << 29); // set other moder to 0 GPIO_G_OUTPUT_TYPE &= ~(0x00000001 << 13 | 0x00000001 << 14); //Set PG13 PG14 to push-pull GPIO_G_PUPD |= (0x00000001 << 27 | 0x00000001 << 29); //Set PG13 PG14 to pull-down while(1) { GPIO_G_PORT_SETRESET = 0x20004000; //PG13 off, PG14 on for(i=0;i<1000000;i++); GPIO_G_PORT_SETRESET = 0x40002000; //PG14 off, PG13 on for(i=0;i<1000000;i++); } return 0; } ``` ![image](https://hackmd.io/_uploads/HyBWy6KCp.png) ### ```main.ld``` * 是個極為簡化的連結腳本(linker script)。 * ```. = 0x0;```: 指定程式連結的邏輯起始位址位於 ```0x0```。 * .text 段: 此 section 保存的內容並非是英文"text"字面意思上的「文字」,而是 ARM 指令的機械碼序列,一般是唯讀。 ```c= SECTIONS { . = 0x0; .text : { *(.text) } } ``` ### ```makefile``` * makefile 教學: [簡單學 makefile:makefile 介紹與範例程式](https://www.mropengate.com/2018/01/makefile.html) * toolchain gcc 簡介: :::info **-mcpu:** 要求 gcc 針對 ARM Cortex-M4 產生對應的指令。 **-mthumb:** 指定產生 Thumb 指令,而非 ARM 指令,請留意,ARM Cortex-M3/M4 只支援 Thumb2 指令。 **-nostartfile:**: 要求連結階段不要使用標準系統起始檔案(starup file),這在沒有作業系統支援的環境是必要的,因為我們自己處理 C 語言程式 main() 函式之前的種種準備動作。 ::: * makefile 變數簡介: :::info **變數宣告**: 變數宣告時要使用 ```=``` 或 ```:=``` 給予初始值,取用時寫成```$()```,如程式碼的第一行到第二行 ::: :::info **自動化變數**: * ```$@```:目前的目標項目名稱。 * ```$<```:代表目前的相依性項目。 * ```$*```:代表目前的相依性項目,但不含副檔名。 * ```$?```:代表需要重建(被修改)的相依性項目。 ::: :::info **萬用配對字元是**: ```%``` ::: * openocd 相關語法介紹: :::info **--file | -f use configuration file [name]** 用於指定設定檔 ::: :::info **--Command | -C -reset** 盡可能讓所有定義的目標都進行重製 **--Command | -C -reset run** 讓目標運行 **--Command | -C -reset halt** 立即停止目標 **--Command | -C -reset init** 立即停止目標,並執行 reset-init 腳本 ::: :::info **--Command | -C stm32f2x unlock num** 解鎖整個 stm32 設備。num 指定對應的 flash banks。 ::: :::info **--Command | -C flash probe num** 識別 flash 或驗證已配置的 flash 的參數。num 指定對應的 flash banks。 ::: :::info **--Command | -C flash info num [sectors]** 列印有關 flash num sectors 的資訊、保護區塊清單及其狀態。num 指定對應的 flash banks。 ::: :::info **--Command | -C flash write_image [erase] [unlock] filename [offset] [type]** 將目標映像檔寫入 flash bank 內,相關的 flash 區域將在映像檔寫入之前被擦除。可指定偏移量(offset)。 ::: * 程式碼簡介: * 1~3: 定義映像檔和執行檔。 * 5~14: 定義工具鍊。 * 16~20: 定義工具鍊的參數。 * 26~27: 從 .c 檔編譯出 .o 檔。 * 29~30: 使用工具鍊中的連結器將 .o 檔根據 .ld 的鏈結腳本連結成執行檔。 * 33: 使用 objcopy 將目標文件的一部分或者全部內容拷貝到另外一個目標文件中,實現目標文件的格式轉換。 * 34: 使用 objdump 作反組譯器,將執行檔反組譯輸出,使我們能以組譯代碼形式檢視可執行檔。 * 35: 打印出執行檔的大小。 * 37~48: 燒錄映像檔至嵌入式裝置中,我們使用 openocd 進行燒錄。 * 50~51: 清除建立檔案時生成的文件,我是在 windows 下安裝 Cygwin64 Terminal 來模擬 linux 指令的執行。 ```c= PROJECT = main BIN_IMAGE = $(PROJECT).bin EXECUTABLE = $(PROJECT).elf ####################################### # binaries ####################################### # Toolchain configurations CROSS_COMPILE ?= arm-none-eabi- CC = $(CROSS_COMPILE)gcc LD = $(CROSS_COMPILE)ld OBJCOPY = $(CROSS_COMPILE)objcopy OBJDUMP = $(CROSS_COMPILE)objdump SIZE = $(CROSS_COMPILE)size ####################################### # CFLAGS ####################################### # Basic configurations CFLAGS = -mcpu=cortex-m4 -mthumb -nostartfiles .PHONY: all all: $(BIN_IMAGE) $(PROJECT).o: $(PROJECT).c $(CC) $(CFLAGS) -c $(PROJECT).c -o $(PROJECT).o $(EXECUTABLE): main.o main.ld $(LD) -T main.ld -o $(EXECUTABLE) $(PROJECT).o $(BIN_IMAGE): $(EXECUTABLE) $(OBJCOPY) -j .text -O binary $(EXECUTABLE) $(BIN_IMAGE) $(OBJDUMP) -h -S -D $(EXECUTABLE) > $(PROJECT).lst $(SIZE) $(EXECUTABLE) flash: main.bin openocd \ -f interface/stlink-v2.cfg \ -f target/stm32f4x.cfg \ -c "init" \ -c "reset init" \ -c "stm32f2x unlock 0" \ -c "flash probe 0" \ -c "flash info 0" \ -c "flash write_image erase main.bin 0x08000000" \ -c "reset run" -c shutdown || \ st-flash write main.bin 0x08000000 clean: rm -rf *.o *.bin *.elf *.list ``` ### 目的檔分析 ```mips= Sections: Idx Name Size VMA LMA File off Algn 0 .text 000000a0 00000000 00000000 00001000 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .comment 00000044 00000000 00000000 000010a0 2**0 CONTENTS, READONLY 2 .ARM.attributes 0000002e 00000000 00000000 000010e4 2**0 CONTENTS, READONLY Disassembly of section .text: 00000000 <main-0x8>: 0: 20001000 andcs r1, r0, r0 4: 00000009 andeq r0, r0, r9 00000008 <main>: 8: b480 push {r7} a: b083 sub sp, #12 ``` * 起始位址為 0x00000000 (對應```<main-0x8>```,這是 ARM exception table 的起始位址,反組譯出來的 andcs 和 andeq 指令沒有意義,我們只在意內含值) * 堆疊指標 MSP 為 0x20001000,PC 初始值是 0x00000009 * 為什麼不是偶數的 0x8,而是 0x9 呢?這是因為 GNU Toolchain 自動處理 ARM 對於位址的 LSB 必須為 1 的要求。 * 目的檔中的 section * ```.text``` * ```.comment``` * ```.ARM.attributes``` * 在更複雜的程式中,還會有```.rodata```、```.data```和```.bss```等 section,另外開發者也可以自己定義 section。 --- # 備註 * 在 window 製作 makefile 時,因為有些位址會帶有左右括弧,例如: ```Program Files (x86)```,這時會因為語法錯誤而報錯。此時我們只要將位址整個用引號框起來即可避免錯誤。 ```c= LDFLAGS = -L "C:/Program Files (x86)/Arm GNU Toolchain arm-none-eabi/13.2 Rel1/lib/gcc/arm-none-eabi/13.2.1" ``` --- # 參考 * [成大 wiki: 嵌入式系統建構:開發運作於STM32的韌體程式](https://wiki.csie.ncku.edu.tw/embedded/Lab19/stm32-prog.pdf) * [成大 wiki: General Purpose Input/Output (GPIO)](https://wiki.csie.ncku.edu.tw/embedded/GPIO) * [成大 wiki: embedded/f9-kernel](https://wiki.csie.ncku.edu.tw/embedded/f9-kernel) * [Github: stm32F4_GPIO_Demo](https://github.com/chunikuo/stm32F4_GPIO_Demo/blob/master/src/GPIO/demo1.c) * [ithelp: 【Day9】:STM32記憶體架構](https://ithelp.ithome.com.tw/m/articles/10270461) * [ithelp: Day1.準備好踏入嵌入式的第一步](https://ithelp.ithome.com.tw/articles/10263894) * [CSDN:(STM32)GPIO库函数使用一览](https://blog.csdn.net/qq_40199189/article/details/79508187) * [CSDN: STM32的启动流程](https://blog.csdn.net/qq_45570844/article/details/126511701) * [CSDN: STM32F4内部Flash读写](https://blog.csdn.net/zhang062061/article/details/114275376) * [OpenOCD official WEB](https://openocd.org/doc/html/OpenOCD-Concept-Index.html) * [hackmd: 組合語言共筆 15 : Thumb Mode](https://hackmd.io/@Ds1111/ryEVDL4qs?utm_source=preview-mode&utm_medium=rec) * [hackmd: STM32單晶片開發玩樂之路-Day 1](https://hackmd.io/@Vu7GwJH2QKGZufWabsL75g/B1x1NubU5#%E4%BB%8A%E6%97%A5%E7%9B%AE%E6%A8%99) * [hackmd: STM32F429 Discovery kit](https://hackmd.io/@GoroYeh56/GPIO#4-%E8%A3%9C%E5%85%85%E8%B3%87%E6%96%99) * [新年第一帖:关于ARM的THUMB模式](https://zhuanlan.zhihu.com/p/601267089) * [ARM Instruction Set](https://iitd-plos.github.io/col718/ref/arm-instructionset.pdf) * [STM32F429開箱與GPIO測試](https://wiki0712.github.io/2018/07/29/2018-08-05-STM32-LED-blink/) * [[STM32 學習紀錄] RCC 相關介紹](https://medium.com/@as369589j/stm32-%E5%AD%B8%E7%BF%92%E7%B4%80%E9%8C%84-rcc-%E7%9B%B8%E9%97%9C%E4%BB%8B%E7%B4%B9-bfd5667b73e1)