程式的運行方式

在開始進行程式開發前,我們先來探討最簡單的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)不停地閃爍。

接下來,我們要逐一分析相關的暫存器設定,並確定這些暫存器的值。

  1. 啟用(enable) D號GPIO port的時鐘

  2. 配置D號GPIO port的腳9 (PD9)為通用推拉輸出模式(output push-pull)

  3. 間歇地設定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 << 5); /* IOPDEN = 1 */

GPIOD_CRH = 0x44444414;

while (1) {

GPIOD_BSRR = (1 << 25); /* ON */

for (c = 0; c < 100000; c++);

GPIOD_BSRR = (1 << 9); /* OFF */

for (c = 0; c < 100000; c++);

}

}

[ simple.ld ]

SECTIONS {

. = 0x0;

.text : {

*(.text)

}

}

[ Makefile ]

CROSS_COMPILE ?= arm-none-eabi-

.PHONY: all

all: blink.bin

blink.o: blink.c

$(CROSS_COMPILE)gcc -mcpu=cortex-m3 -mthumb -nostartfiles -c blink.c -o blink.o

blink.out: blink.o simple.ld

$(CROSS_COMPILE)ld -T simple.ld -o blink.out blink.o

blink.bin: blink.out

$(CROSS_COMPILE)objcopy -j .text -O binary blink.out blink.bin

clean:

rm -f *.o *.out *.bin

下面來分析前述3個檔案。

blink.c需要注意的是,程式中有兩個for迴圈用來延時,STM32執行的速度相當快,若不加延時的話,LED燈閃爍的頻率會非常高,讀者不妨自行計算(時鐘頻率在稍後介紹)。閃爍頻率非常高的後果,就是人眼覺察不到閃爍,從而讓人誤認LED燈一直亮著。

simple.ld是個極為簡化的連結腳本(linker script),其中”. = 0x0”指定程式連結的邏輯起始位址位於0x0,而程式中的所有符號進行重定位(relocate)時,參考的起始位址為0x0,這部分的背景知識可參考《Linker and Loader》。按照之前的分析,STM32對內建Flash的存取可透過兩種不同的位址,因此我們這裡把起始位址換成0x08000000也行。

Makefile是非常簡單的GNU make檔案,《Managing Projects with GNU Make》這本書對Makefile的寫法有詳細和深入的探討。我們可察覺到Makefile中指定編譯器的若干參數“-mcpu=cortex-m3 -mthumb -nostartfiles”,其中:

  • -mcpu: 要求gcc針對ARM Cortex-M3產生對應的指令

  • -mthumb: 指定產生Thumb指令,而非ARM指令,請留意,ARM Cortex-M3/M4只支援Thumb2指令

  • -nostartfile: 要求連結階段不要使用標準系統起始檔案(starup file),這在沒有作業系統支援的環境是必要的,因為我們自己處理C語言程式main()函式之前的種種準備動作

Makefile 中描述的編譯流程,可參見下方《The Definitive Guide To ARM Cortex M3》的圖例,清楚得知自原始程式碼、編譯器(gcc)、組合語言程式碼、組譯器(as)、目的檔、連結器腳本、連結器(ld)、以及藉由objcopy工具來產生二進位檔案的種種步驟:

進一步考慮到模擬器和除錯器(debugger)的操作流程,則是以下示意圖:

有了這三個檔案,我們只需在終端機執行make命令,make程式依據指定的編譯和連結規則,自動產生出程式的映像檔blink.bin。

既然程式不複雜,相信編譯出來的結果也不該太難,我們來分析吧。

先進行反組譯,使用objdump工具:

# arm-none-eabi-objdump -D blink.out

下面是結果的片段:

CROSS_COMPILE ?= arm-none-eabi-

blink.out:     file format elf32-littlearm

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的要求。

進一步去分析編譯出來的目的檔(格式為ELF; Executable and Linkable Format)中有哪些section,由objdump的輸出可見到.text、.comment,以及.ARM.attributes等3個section(均以”.”開頭),其中跟程式的執行相關的只有.text section。在更複雜的程式中,還會有.rodata、.data,和.bss等section。另外開發者也可以自己定義section。對這些不同section的處理,以後會有更詳細的描述。

接著我們來研究即將要燒錄(flash write)到 Flash 裡頭的二進位檔案,使用od工具程式:

# od -t x1 blink.bin

0000000 00 10 00 20 09 00 00 00 80 b4 83 b0 00 af 00 23

0000020 7b 60 11 4b 20 22 1a 60 10 4b 11 4a 1a 60 11 4b

0000040 4f f0 00 72 1a 60 00 23 7b 60 02 e0 7b 68 01 33

0000060 7b 60 7a 68 0c 4b 9a 42 f8 d9 0a 4b 4f f4 00 72

0000100 1a 60 00 23 7b 60 02 e0 7b 68 01 33 7b 60 7a 68

0000120 05 4b 9a 42 f8 d9 e2 e7 18 10 02 40 04 14 01 40

0000140 14 44 44 44 10 14 01 40 9f 86 01 00

0000154

位元順序為little-endian。

程式的燒錄和執行

當程式撰寫並順利編譯連結後,就可將產生的二進位檔案燒錄到晶片內建的 Flash 中,並試著執行測試。燒錄程式碼有不少可用的機制,本文以OpenOCD搭配OpenJTAG作為解說。

燒錄步驟如下:

1.  執行OpenOCD

# openocd -f openocd.cfg -f stm32.cfg

2.  在終端機中執行以下命令,進行Flash的擦拭消除(erase)和燒錄

# telnet localhost 4444

Trying ::1...

Trying 127.0.0.1...

Connected to localhost. Escape character is '^]'. Open On-Chip Debugger

> halt

target state: halted

target halted due to debug-request, current mode: Thread

xPSR: 0x81000000 pc: 0x0800002a msp: 0x200000d8

> poll

background polling: on TAP: stm32.cpu (enabled) target state: halted

target halted due to debug-request, current mode: Thread

xPSR: 0x81000000 pc: 0x0800002a msp: 0x200000d8

> 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.  開始測試

發現燈亮了嗎?如果沒亮的話,請仔細檢查步驟是否正確、暫存器的值是否充分設定。

简单C语言程序的执行过程相关推荐

  1. c语言程序的执行过程压栈,汉诺塔---手写出栈压栈过程实现

    代码实现: 1 #include 2 3 //函数的形参A.B.C不一定代表的是A.B.C柱子,递归传参的时候会变化! 4 void hanoit(int n,char A,char B,charC) ...

  2. c语言程序的执行过程

    编译,编译程序读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,再由汇编程序转换为机器语言,并且按照操作系统对可执行文件格式的要求链接生成可执行程序. C源程序头 ...

  3. python语言是编译型语言-Python程序的执行过程原理(解释型语言和编译型语言)...

    Python是一门解释型语言? 我初学Python时,听到的关于Python的第一句话就是Python是一门解释型语言,我就这样一直相信下去,直到发现.pyc文件的存在,如果真是解释型语言,那么生成的 ...

  4. python语言采用编译执行方式_Python程序的执行过程 解释型语言和编译型语言

    我初学Python时,听到的关于Python的第一句话就是,Python是一门解释性语言,我就这样一直相信下去,直到发现了*.pyc文件的存在.如果是解释型语言,那么生成的*.pyc文件是什么呢?c应 ...

  5. c语言编写51单片机中断程序,执行过程是怎样的?

    Q:c语言编写51单片机中断程序,执行过程是怎样的? 例如程序: #include<reg52.h>   void main(void)   {    EA=1;      //开放总中断 ...

  6. python 二进制流转图片_Python零基础入门到精通-5.1节:Python程序的执行过程

    教程引言: 系统地讲解计算机基础知识,Python的基础知识, 高级知识,web开发框架,爬虫开发,数据结构与算法,nginx, 系统架构.一步步地帮助你从入门到就业. 5.1.1 在命令行中执行Py ...

  7. python运行程序-Python程序的执行过程

    1.C++和C都是属于编译型语言,本来的.c文件都是用高级语言编写的,计算机是不能识别高级语言的,所以,必须要通过编译,链接等手段,将.c文件转换成可执行文件,可执行文件就是纯二进制文件,然后计算机才 ...

  8. python采用编译型方式执行_Python程序的执行过程 解释型语言和编译型语言

    我初学Python时,听到的关于Python的第一句话就是,Python是一门解释性语言,我就这样一直相信下去,直到发现了*.pyc文件的存在.如果是解释型语言,那么生成的*.pyc文件是什么呢?c应 ...

  9. Python程序的执行过程 解释型语言和编译型语言

    转载地址:http://www.cnblogs.com/kym/archive/2012/05/14/2498728.html 1.C++和C都是属于编译型语言,本来的.c文件都是用高级语言编写的,计 ...

最新文章

  1. 2014.5.2—北京爱情故事
  2. BZOJ 4422 Cow Confinement (线段树、DP、扫描线、差分)
  3. [转载] 七龙珠第一部——第019话 天下第一武道会开始
  4. java的OutOfMemoryError: PermGen space实战剖析
  5. HikariPool-1 - Exception during pool initialization. Could not create connection
  6. Japanese(Shift-Jis)的编码范围
  7. vue单文件组件导入导出
  8. b站coderwhy老师_Vue项目开发-仿蘑菇街电商APP
  9. 学习ARM开发(15)
  10. 62 旋转字符串(Rotate String)
  11. 可乐吧 LayaBOX LayaAIR谢成鸿曾10年亏了1亿 如今做引擎创办Layabox融资1个亿
  12. Caused by: java.io.FileNotFoundException: class path resource [applicationContext.xml] cannot be ope
  13. 和用户一起做设计的时代
  14. 利用mkdocs部署静态网页至GitHub pages (相当于做一个个人网站)
  15. 港科夜闻|香港科技大学副校长汪扬教授接受《经济导报》记者专访,解码大湾区创科基因...
  16. 桌面计算机休眠快捷键,win7系统电脑休眠快捷键的操作方法
  17. Java生成证书类pdf
  18. 用Python绘制樱花树、玫瑰花各种表白代码,从此不再缺女朋友
  19. !pdftex error (font expansion): auto expansion is only possible with scalable fonts
  20. 我与计算机视觉-[CUDA]-[Opencv.Resize的CPU实现和GPU实现]

热门文章

  1. android opengl es 绘制余弦曲线,Android OpenGL ES - 绘制线、面
  2. Objective-C中MRC和ARC的自我理解
  3. foreach 二维java_教你如何用for和foreach循环遍历java中的二维数组
  4. 1、用Anaconda配置Windows环境下的tensorflow(CPU版本)
  5. Stanford机器学习笔记-1.线性回归
  6. 使用 C++ 的 StringBuilder 提升 4350% 的性能
  7. 从零开始学习jQuery (七) jQuery动画-让页面动起来!
  8. Java访问指示符 访问修饰符
  9. Java并发编程(7):使用synchronized获取互斥锁的几点说明
  10. OpenCV-Python教程(9)(10)(11): 使用霍夫变换检测直线 直方图均衡化 轮廓检测