C程序是如何運行的
STM32F0xx 系列是ARM Cortex-M0架構,地址空間32位,也就是4G Bytes的訪問范圍。數據和代碼使用同一編址,下圖是地址空間的布局:
實際上單片機用到的資源很少,地址空間大部分都沒有內容。我使用的STM32F072C8T6帶有64kB的Flash ROM, 16kB的SRAM,起始分別是 0x08000000 和 0x20000000. (由于有硬件映射功能,在0x00000000也就是最低地址,還可以訪問ROM或者RAM的內容). 單片機片上外設的寄存器,則分布在更高的地址空間。讀寫這些寄存器,在CPU看來和讀寫內存(RAM)操作是一樣的。
所以,C語言訪問設備寄存器,和訪問內存中的一個變量一樣。只要知道寄存器的地址,通過一個指針訪問就可以實現讀寫。上一貼子我的程序中引用了 RCC, GPIOA, TIM6 這三個(結構)指針,它們的值(也就是地址)以及類型(代表訪問的內容)定義在 stm32f0xx.h 這個頭文件中。因為設備寄存器太多了哇,如果每一個都定義一個指針就太煩瑣了,所以把按功能劃分定義成組,每組用一個C語言的結構類型表示,寫起來也更清晰。而寄存器里面的位描述也可以定義成一些宏,在讀程序的時候就知道是什么意思了。如果有興趣,可以把 stm32f0xx.h 文件和STM32F0的手冊對照著閱讀。
好,假設已經熟悉寄存器操作了,知道怎么配置寄存器實現想要的功能,那么就可以寫C程序讓STM32工作了。現在需要一個工具來將C程序翻譯成機器代碼——編譯器,或者是叫做工具鏈(Tool chain)。Keil MDK-ARM 或者 IAR-EWARM 開發環境都帶有各自的編譯器,不過我更偏向于用開源的GCC-ARM. 在launchpad.net上可以下載到編譯好的arm-gcc工具鏈zip包,將它解壓縮,加到PATH里面就可以直接用了,很方便(很精簡吧)。
OK,現在來編譯上面那個mini.c文件,命令行: arm-none-eabi-gcc -c -Os -mcpu=cortex-m0 -mthumb mini.c gcc的參數 -c 是表示僅編譯,-Os 是優化代碼大小,-mcpu=cortex-m0 -mthumb 是指定指令集的,因為ARM有不同的版本。對了,include的頭文件還沒弄到呢。要編譯通過需要把 stm32f0xx.h 這個文件找來。我的建議是下載ST提供的 "STM32F0x2 USB FS Device Library" 程序庫,把里面需要的頭文件等等扒出來。在 stm32f0xx.h 中還包含了另外幾個頭文件,一并弄出來放到工程目錄下。
如果編譯成功,將得到 mini.o 目標文件。可以用 arm-none-eabi-objdump -S mini.o 反匯編看看翻譯成什么代碼了。
E:\arm\test072\mini>arm-none-eabi-objdump -S mini.o
mini.o: file format elf32-littlearm
Disassembly of section .text.startup:
00000000 <main>:
0: 4b16 ldr r3, [pc, #88] ; (5c <main+0x5c>)
2: 2280 movs r2, #128 ; 0x80
4: 6959 ldr r1, [r3, #20]
6: 0292 lsls r2, r2, #10
8: 430a orrs r2, r1
a: b510 push {r4, lr}
c: 2180 movs r1, #128 ; 0x80
e: 615a str r2, [r3, #20]
10: 2290 movs r2, #144 ; 0x90
12: 0249 lsls r1, r1, #9
14: 05d2 lsls r2, r2, #23
16: 6011 str r1, [r2, #0]
18: 69da ldr r2, [r3, #28]
1a: 2110 movs r1, #16
1c: 430a orrs r2, r1
1e: 61da str r2, [r3, #28]
20: 4b0f ldr r3, [pc, #60] ; (60 <main+0x60>)
22: 4a10 ldr r2, [pc, #64] ; (64 <main+0x64>)
24: 851a strh r2, [r3, #40] ; 0x28
26: 2290 movs r2, #144 ; 0x90
28: 32ff adds r2, #255 ; 0xff
2a: 62da str r2, [r3, #44] ; 0x2c
2c: 2205 movs r2, #5
2e: 801a strh r2, [r3, #0]
30: 4a0d ldr r2, [pc, #52] ; (68 <main+0x68>)
32: 7812 ldrb r2, [r2, #0]
34: 8a1c ldrh r4, [r3, #16]
36: 2101 movs r1, #1
38: 4809 ldr r0, [pc, #36] ; (60 <main+0x60>)
3a: 420c tst r4, r1
3c: d0fa beq.n 34 <main+0x34>
3e: 8a04 ldrh r4, [r0, #16]
40: 438c bics r4, r1
42: 8204 strh r4, [r0, #16]
44: 2090 movs r0, #144 ; 0x90
46: 2480 movs r4, #128 ; 0x80
48: 05c0 lsls r0, r0, #23
4a: 408c lsls r4, r1
4c: 2a00 cmp r2, #0
4e: d102 bne.n 56 <main+0x56>
50: 6184 str r4, [r0, #24]
52: 1c0a adds r2, r1, #0
54: e7ee b.n 34 <main+0x34>
56: 8504 strh r4, [r0, #40] ; 0x28
58: 2200 movs r2, #0
5a: e7eb b.n 34 <main+0x34>
5c: 40021000 .word 0x40021000
60: 40001000 .word 0x40001000
64: 0000270f .word 0x0000270f
68: 00000000 .word 0x00000000
如上,其實里面就一個main函數。但是 main 的入口地址還沒有確定,而且它還使用了一個static型的內存變量,地址也還沒有確定。可以用 arm-none-eabi-nm mini.o 來查看模塊里面的全局符號表:
E:\arm\test072\mini>arm-none-eabi-nm mini.o
00000000 b a.4686
00000000 T main
那么,怎么讓程序放到ROM中合適的地址,并運行呢?如果熟悉C語言編程就知道還有一步——鏈接,才能確定符號的地址。但是,到目前為止我們還沒有告訴GCC地址的布局,也就是RAM從哪里開始,代碼放在哪里。因為ARM的器件很多,這并不是統一的,所以需要提供一些信息給鏈接程序。具體地,需要一個Linker Script, 可以從軟件包中找到 STM32F072C8_FLASH.ld (或者用近似的來修改得到)
/*
*****************************************************************************
** File : stm32_flash.ld
*****************************************************************************
*/
/* Entry Point */
ENTRY(Reset_Handler)
/* Highest address of the user mode stack */
_estack = 0x20003FFF; /* end of RAM */
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200; /* required amount of heap */
_Min_Stack_Size = 0x400; /* required amount of stack */
/* Specify the memory areas */
MEMORY
{
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 64K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 16K
}
/* Define output sections */
SECTIONS
{
/* The startup code goes first into FLASH */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(4);
} >FLASH
/* The program code and other data goes into FLASH */
.text :
{
. = ALIGN(4);
}
這里面 -L 參數是添加標準庫文件的搜索路徑,雖然暫時并沒有用到C標準庫里面的東西,但是Linker Script里面引用了標準庫文件。-o 指定輸出的目標文件。這么就快要得到最終的機器碼了,不過好象還缺少了什么…… arm-none-eabi-ld: warning: cannot find entry symbol Reset_Handler; defaulting to 08000000
linker給了一個警告:找不到入口地址 Reset_Handler 的值,設成了默認 0x08000000. 下面再用objdump -S反匯編看一下
E:\arm\test072\mini>arm-none-eabi-objdump -S mini.elf
mini.elf: file format elf32-littlearm
Disassembly of section .text:
08000000 <main>:
8000000: 4b16 ldr r3, [pc, #88] ; (800005c <main+0x5c>)
8000002: 2280 movs r2, #128 ; 0x80
8000004: 6959 ldr r1, [r3, #20]
8000006: 0292 lsls r2, r2, #10
8000008: 430a orrs r2, r1
800000a: b510 push {r4, lr}
800000c: 2180 movs r1, #128 ; 0x80
800000e: 615a str r2, [r3, #20]
現在 main() 被放到ROM最開始去了,這好象是對的?如果了解ARM Cortex-M0下就知道這樣錯了,因為最開始應該是中斷向量表。我們還沒有編寫Linker Script中的 .isr_vectors 段的內容。而且,一上來初始化堆棧指針等工作都沒有做就直接運行 main() 了也不合適吧?還缺少了初始化代碼。
原來是這樣,中斷向量表在這里進行了描述,還有設置堆棧,初始化全局變量的代碼,然后跳轉到 main 執行。好了,這樣就該差不多了。這個匯編程序是GNU AS的語法,可以用 arm-none-eabi-gcc 來直接匯編 arm-none-eabi-gcc -c startup_stm32f072.s
我這個是最簡化的例子,使用最簡化的軟件工具,不過已經包含了基本的C語言框架。