程式的運行方式
在開始進行程式開發前,我們先來探討最簡單的C程式如何運作。
為了使程式足夠簡單,我們可讓CPU直接從Flash上取得指令(fetch instruction)並執行,而且程式中沒用到全域變數,因此編譯出來的目的檔(object file)中是data section長度是0,如此一來,避免了初始化RAM的步驟,因為data section是可讀寫的,如果目的檔中有data section,我們就必須在程式的啟動過程中,將data section複製到RAM中,方可確保程式得以正常工作。
程式的執行環境
我們來定義一下程式執行時的記憶體映射 (memory map):
Flash的陰影區域表示保存的程式映像(program image),程式執行過程中的堆疊(stack)當然只能存於 RAM 中,示意圖右方黑點標註堆疊頂端指標。
Reset後程式的運行流程
一旦處理器Reset後,就會進行「取得指令—解碼—執行」的循環,也就是3-stage pipeline。因此,PC (program counter; 以下PC均指此暫存器,而非個人電腦)暫存器在Reset後的值就顯得關鍵。在《The Definitive Guide To ARM Cortex M3》中 (簡體中文電子書),我們知道,在離開Reset狀態後,ARM Cortex M3所做的第一件事,就是讀取下列兩個 32位元整數的值:
-
從位址0x00000000處取出MSP (Main Stack Point)的初始值
-
從位址0x00000004處取出PC的初始值,處理器隨即自這個值所對應的位址處取值
可用下方示意圖說明:
要注意,從位址0x00000004處取出PC的初始值,裡頭的LSB 必須為1 (稍後的實驗可察覺這點)。為什麼呢?當一個例外處理程式(exception handler)的位址在LSB設定為1,代表該例外處理程式運作於Thumb模式(Thumb mode),對ARM Cortex-M3來說,這是必要的,因為該處理器核心只支援Thumb-2指令集,而不支援ARM模式(也稱ARM code或ARM state),當然,該例外處理程式都運行於Thumb模式,底下節錄《The Definitive Guide To ARM Cortex M3》的描述:
With the introduction of the Thumb-2 instruction set, it is now possible to handle all processing requirements in one operation state. There is no need to switch between the two. In fact, the Cortex-M3 does not support the ARM code. Even interrupts are now handled with the Thumb state. (Previously, the ARM core entered interrupt handlers in the ARM state.) Since there is no need to switch between states, the Cortex-M3 processor has a number of advantages over traditional ARM processors, such as:
-
No state switching overhead, saving both execution time and instruction space
-
No need to separate ARM code and Thumb code source files, making software development and maintenance easier
-
It’s easier to get the best efficiency and performance, in turn making it easier to write software, because there is no need to worry about switching code between ARM and Thumb to try to get the best density/performance
以上是ARM對Cortex M3核心定義的行為,那STMicroelectronics作為晶片的製造商,是如何實作的呢?從STMicroelectronics的Reference Manual 得知,STM32系列處理器的引導模式(BOOT)設定,以及不同的模式下處理器的行為。我們在意最簡單的情況:系統從內建的Flash啟動,也就是BOOT0=0的情況。
稍早提過,內建的Flash起始位址為0x08000000,這是否意味著Cortex M3無法自Flash中取得系統Reset後需要的MSP和PC初始值嗎?STM32為了解決這問題,提出的解決方案是位址別名(alias),簡單來說,STM32內建的Flash有兩套定址空間,除了能從0x08000000存取外,也可自位址別名(即0x00000000開始)存取。
基於以上的分析,我們來總結一下我們程式映像該存放哪些資訊。
-
MSP初始值
-
PC 初始值(LSB必須為1)
-
程式的text section、data section等
下面的程式碼即可達到我們的期望:
asm (".word 0x20001000");
asm (".word main");
main() { … }
上述程式碼中,我們指定MSP為0x20001000,即堆疊大小為0x1000 (計算方式: 0x20001000 - 0x20000000 = 0x1000; 也就是4KB),對於Hello World等級的程式來說,4KB大小的堆疊應該綽綽有餘。需要留意的是,GNU Toolchain能自動幫我們處理好PC初始值LSB必須為1的議題,我們只要確保main這個符號是個C函式就行。其實,相較於在GNU/Linux或Microsoft Windows環境中開發程式,有一定程度的區隔,main函式不一定是C程式的第一個被執行的地方,甚至可任意命名,這裡以main來命名,其實也是為了方便理解起見。畢竟,我們沒有利用GNU Toolchain提供的啟動程式碼(startup),也就沒有必要依據toolchain的要求,來命名進入點函式。一般來說,我們稱這樣不需要理會既定作業系統規範(主要指OS ABI [Application Binary Interface],如calling convention等議題)的執行環境為bare-metal。
程式都在做什麼?
程式就算再簡單,也不可沒有輸出,不然我們如何檢驗功能呢?在嵌入式系統中,往往缺乏寬敞的螢幕,讓Hello World程式得以輸出字串,因此,我們只能落入俗套,改玩點燈遊戲。
為了點燈,程式需要操作那些週邊硬體呢?操作這些週邊硬體時,需要讀寫那些暫存器呢?
由於點燈程式相對單純,我們只需要操作STM32的GPIO (General Purpose I/O),即可達到目的。對於GPIO的操作涉及到的幾個暫存器,可查閱ST提供的參考手冊,實務還得對照開發板的硬體電路,才能確定暫存器值的設定。在?開發板上,D號GPIO的9腳連著一個LED 燈,下圖即為點燈相關的原理圖,程式運行的效果就讓LED(DS4)不停地閃爍。
接下來,我們要逐一分析相關的暫存器設定,並確定這些暫存器的值。
-
啟用(enable) D號GPIO port的時鐘
-
配置D號GPIO port的腳9 (PD9)為通用推拉輸出模式(output push-pull)
-
間歇地設定PD9的值為0和1,也就是控制LED燈的亮滅
暫存器的設定自然得查詢STMicroelectronics的參考手冊,以下幫讀者列出相關的描述:
-
APB2 peripheral clock enable register (RCC_APB2ENR)
位址:0x40021000 + 0x18
Reset後的值:0x00000000
把(IOPD EN)設為 1,即可啟用GPIOD的時鐘
-
Port configuration register high (GPIOx_CRH) (x=D)
位址:0x40011400 + 0x4
Reset後的值:0x44444444
依據我們的需求:
-
CNF9[1:0] = 00 (General purpose output push-pull)
-
MODE[1:0 ] = 01 (輸出模式,最大速度為10MHz)
-
Port bit set/reset register (GPIOx_BSRR) (x=D)
位址:0x40011400 + 0x10
Reset後的值:0x00000000
亮燈:GPIOD9輸出低電位,BR9 = 1
滅燈:GPIOD9輸出高電位,BS9 = 1
至此,硬體週邊控制的細節都已經清楚,我們終於可以開始撰寫程式了。
程式碼分析
第一個版本的程式相當簡單,請見以下:
[ blink.c ]
#define GPIOD_CRH (*((volatile unsigned long *) (0x40011400 + 0x4)))
#define GPIOD_BSRR (*((volatile unsigned long *)(0x40011400 + 0x10)))
#define RCC_APB2ENR (*((volatile unsigned long *)(0x40021000 + 0x18)))
asm(".word 0x20001000");
asm(".word main");
int main()
{
unsigned int c = 0;
RCC_APB2ENR = (1 flash protect_check 0
device id = 0x10016414 flash size = 512kbytes
successfully checked protect state
> stm32x mass_erase 0
stm32x mass erase complete
> flash write_bank 0 /tmp/blink.bin 0
not enough working area available(requested 16384, free 16336)
wrote 144 bytes from file /tmp/blink.bin to flash bank 0 at offset 0x00000000 in 0.175022s (0.803 kb/s)
> reset
JTAG tap: stm32.cpu tap/device found: 0x3ba00477 (mfg: 0x23b, part: 0xba00, ver: 0x3)
JTAG tap: stm32.bs tap/device found: 0x06414041 (mfg: 0x020, part: 0x6414, ver: 0x0)
3. 開始測試
發現燈亮了嗎?如果沒亮的話,請仔細檢查步驟是否正確、暫存器的值是否充分設定。