嵌入式系統開發環境主要包括:
- 集成開發工具
- 交叉編譯器
- 批處理文件
- makefile
- Link Script
- 調試工具
- 下載工具
- 其它工具(Offline Tools)
- 模擬器
- 版本控制工具
接下來分別講解以上各個工具:
1、集成開發工具
一般CPU廠商會提供針對該CPU的集成開發環境(IDE),但在實際應用中,大多數嵌入式項目開發公司都還是會使用自己開發的環境。一是項目某些功能的特殊性要求,二是並不是所有CPU型號都有相應的IDE。
2、 Cross-Tools
Cross-Tools包含:
- Cross-Assembler
- Cross-Compiler
- Cross-Library
- Cross-Linker
- dump工具(將可執行文件轉換為匯編語言代碼的相關信息)
- 調試工具(GNU gdb)
以GNU Tool-Chain中的C編譯器gcc為例,以下是一些編譯時的選項:
- -Werror:將所有警告信息變為錯誤信息,一旦有警告信息產生,就不產生目標文件。
- -S : 編譯時輸出匯編語言代碼。
- -C : 編譯時僅產生目標文件
- -E : 只執行預處理,不產生目標文件
- -D : 編譯時定義宏常數
- -O、-O2、-O3: 優化等級。
- -g : 編譯時加入調式信息,使之可以使用GDB進行調試
以GNU Tool-Chain中的C鏈接器Linker為例,以下是一些選項:
- -T : 制定鏈接腳本文件
- -Map :連接時產生map文件,其中包含了程序中所有symbol的地址信息。
GNU tool chain可以支持許多不同的CPU,使用者可以根據需求設定配置。例如arm-elf-gcc就是會以elf格式產生ARM機器碼的C編譯器,而68K-coff-ld就是會以COFF格式產生68000機器碼的linker。
3、Make
make是用來進行自動編譯的程序工具,只要在makefile中詳細敘述要用什么工具(例如cross-compiler)對哪個文件(.c、.obj、…)做何種處理(產生不優化的目標文件),make同時還會檢查這些文件是否過期,如果過期會僅僅自動重新編譯需要編譯的(make會比較文件之間的依存關系與日期,以決定某個文件是否需要重新編譯),而通常的批處理程序(例如windows下的.bat程序),在有某些文件更新后,需要重新編譯所有文件。
《programming with GNU Software/GNU程序設計》
《Managing Project with make/make 項目開發工具》
以上兩本書有對make使用的詳細說明。
(1) makefile里的重要概念
- Target(目標):就是想要產生的文件名稱。
- Dependency(依賴):定義兩個文件是否存在依存關系。
- Prerequisite(必備文件):一些能建立target的文件,一個target通常由多個文件建立。
- UP to Date(新版):假設某個文件比它所依賴的文件還要新,則表示這個文件有了新版本。
makefile的基本語法:
#文件名:sample.mak
Target:Dependency list
command1
command2
要執行上述makefile的命令是:make -f sample.mak
,如果沒有使用-f指定makefile文件的話,make會在當前目錄下尋找名為“makefile”的文件。此外如果沒有指定targe的話,make會以makefile中第一個target文件名當作目標名。
(2)makefile舉例
- 下例為makefile中的宏定義示例:
#File Name : DEFINE.MAK
#
#定義其它makefile中會用到的宏,思想和C語言的#define一樣
#
!IFNDEF _DEFINE_MAK
_DEFINE_MAK = DEFINED
#
#定義項目相關文件所在的磁盤機編號
#
PRJ_DRIVER = Y:
#
#定義項目工具所在目錄
#
PRJ_TOOLS_ROOT = $(PRJ_DRIVER)\Tools
#
#定義編譯器所在目錄
#
GNU_CC = $(GNU33_ROOT)\kcc33
GNU_LK = $(GNU33_ROOT)\ld
GNU_AR = $(GNU33_ROOT)\ar
#
#定義項目程序所在目錄
#
SRC_ROOT = $(PRJ_DRIVER)\Project2020
SRC_INC = $(SRC_ROOT)\include
#
#當編譯時傳入-DXXX參數,其效果如同在程序中寫了#define XXX
#
PRJ_CONFIG = -DPRJ_2020 -DCPU_ARM9 -DLCD_160X160
#
#定義執行C compiler時的參數
#
?CFLAGS= -c -gstabs -mlong-calls -fno-builtin -mgda=0 -mdp=1 -O3
-I$(GNU_INCLUDE)
-I$(SRC_INC)
-I$(PRJ_CONFIG)
#
#定義執行linker時的參數
#
LDFLAGS= -T main.lds -Map $(TARGET).map -N
#...
#...
!ENDIF
- 下例為一個較復雜的范例:
#
#在makefile中,也可以和include一樣,包含其它makefile
#
!IF "$(_DEFINE_MAK)" == ""
!INCLUDE DEFINE.MAK
!ENDIF
#
#定義各模塊包含的object file,每個object都是一個target
#
MODEL1_OBJS = m1_001.obj m1_002.obj m1_003.obj
MODEL2_OBJS = m2_001.obj m2_002.obj
#
# 項目中所有需要的object file
#
OBJS = $(MODEL1_OBJS) $(MODEL2_OBJS)
#
#定義會用到的庫函數
#
LIBS = $(GNU_LIB)\libgcc.a
#
#第一個target產生最終可執行文件main.elf,
#和main.elf有依賴關系的target有:所有的object file,main.mak,Link Script
#"$@"表示target本身,即main.elf
#
main.elf : $(OBJS) main.mak main.lds
$(GNU_LK) $(LDFLAGS) -o $@ $(OBJS) $(LIBS)
#
# $* 表示target名稱去掉擴展名
# $@ 表示target本身
# $< 表示XXX.c
#
m1_001.obj : $(SRC_ROOT)\m1\m1_001.c $(SRC_INC)\m1.h
$(GNU_CC) $@ $(CFLAGS) $*.c
m1_002.obj : $(SRC_ROOT)\m1\m2_001.c $(SRC_INC)\m1.h
$(GNU_CC) $@ $(CFLAGS) $*.c
...
- 當需要重復處理幾個擴展名一樣的文件時,通常可以使用make的預設編譯規則。例如當需要以同樣的規則編譯所有以.obj為擴展名的target時可以采用如下語句:
?.c.obj:;$(GNU_CC) $@ $(CFLAGS) $<
預設編譯規則語法說明:
.c.obj:; 此行目錄用來規范target為.obj文件,依賴為.c文件的預設編譯規則
在設定預編譯規則時依然可以使用宏
(3)非文件名稱的Target
clean:
del $(OBJS)
del main.elf
del main.bin
上述makefile語句中僅有target,沒有dependency,意味着該target是一定會去執行下文的del命令。一般用於重新編譯所有文件前執行。
這種非文件名target也可作為其它target的dependency,用於當要make某個target時,先去執行一系列指令的效果:
build_all : clean
...
...
(4)版本控制
在系統正式發布之前,程序代碼中肯定會包含許多用於調試的代碼行。但實際中,由於嵌入式系統的存儲資源有限,不可能將含有調試代碼的程序作為最終代碼燒進板子。所以在設計時,一般會設計兩個版本(調試版和發行版)。當然,當程序開發完,我們不可能用手動的方式一個個去刪除這些調試代碼。此時可以采用C語言中條件編譯的思想,見下文分析:
調試版批處理文件:make_debug.bat
REM ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
REM Make_debug.bat
REM~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
REM 設定Windows/DOS的系統環境變量
REM
set BUILD_MODE = Debug
REM make我們的程序
REM
make target
發行版批處理文件:make_release.bat
REM ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
REM Make_release.bat
REM~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
set BUILD_MODE = Release
make target
makefile:
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#makefile
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
!IF "$(BUILD_MODE)" == "Debug"
# 如果BUILD_MODE等於“Debug”,則設定編譯時期的參數CFLAGS_DEBUG = __DEBUG_VERSION
# 反之,則設定編譯時期的參數為空
#
CFLAGS_DEBUG = -D__DEBUG_VERSION
!ENDIF
target:
gcc $(CFLAGS_DEBUG) xxx.c
#-D參數用來在編譯時期設定宏變量“__DEBUG_VERSION”
4、Link Script
制作可執行文件的流程中,要先把所有的程序文件編譯成目標文件,接下來就是通過鏈接器linker將所有的目標文件與庫文件鏈接為可執行文件。而具體如何鏈接,連接到哪個地址就是通過擴展名為.ld的連接腳本文件來指定了。
有操作系統的情況下,不同的程序有自己的地址空間,而且相互之間互不干涉。這種程序都在RAM(內存)中執行,所有程序只要從同一個起始地址連接在一起就好。但嵌入式程序很多時候是沒有操作系統的,系統和程序通常在在同一個地址空間,且往往連硬盤都沒有,程序只能在ROM或flash中執行。但數據則只能被尋址在RAM中,所以連接時要告訴linker,程序段要被尋址到哪里(ROM的起始地址),數據段要被尋址到哪里(RAM的起始地址)。
(1)程序區段的結構
- text段:即代碼段,執行期間text段的內容不會改變,其可以直接在ROM里執行,無須載入到內存。
- Read-only-data(rodata)段:定義為const的變量,以及字符串都會歸類到rodata段。,其也是直接在ROM里執行。
- Data段 :有初值的全局變量放在這個段。在連接時期,這些初值必須加入到可執行文件中,但要被尋址到RAM的地址;在執行時期,這些變量被存儲在ROM中,但必須被載入到RAM中才能使用,因為他們的值是可變的。所以,data段會被加入ROM,但卻要尋址到RAM中。
- bss段:沒有初值的全局變量會被歸類到bss段。因為無初值,所以不必加入到程序中,只要在連接時將其尋址到RAM即可。執行時期也沒有載入的問題,但機器RESET后,由系統主動將整個bss段清零。
(2)link script 的內容
執行時期存儲器的使用狀況:

- LMA(Load Memory Address)與VMA(Virtual Memory Address)
數據會被放置在ROM,但執行時必須載入到RAM,則在ROM中(最終存儲的地址)的地址稱為LMA,而在RAM中(執行時期)的地址就是VMA。
試着寫一下具有如下連接要求的link script:
- 系統中有一塊ROM,它的起始地址是0xC00000,另有一塊RAM,起始地址為0.
- 可執行文件包含text、rodata、data段,其中text段和rodata段在ROM里執行即可,所以被尋址到0xC00000,而rodata跟在text后面。
- bss段因為沒有初值,所以不會占據可執行空間或ROM空間,它會被尋址到RAM的起始地址0.
- data段比較復雜,它的內容也必須包含在可執行文件內,在執行時期它必須被載入到RAM里。所以data的VMA在RAM中,跟在bss段之后,而LVA則跟在rodata段后面。
擴展:當希望某段程序以更快的速度執行,則只需要將其LMA在ROM里,VMA則尋址到RAM中,在執行前將其從ROM中載入到RAM里。

/*********************************************************
Link Script sample
存儲器地址配置:ROM起始地址(0xC00000),RAM起始地址(0)
輸出ARM9機器碼,可執行文件格式為elf32
**********************************************************/
OUTPUT_FORMAT("elf32-arm9")
OUTPUT_ARCH(arm9)
SEARCH_DIR(.);
SECTIONS
{
/*****************************************
定義text段,起始地址(VMA)從0xC00000開始,
若沒有指定LMA,表示LMA起始地址同VMA。
*****************************************/
.text 0xC00000:
{
/* 定義變量__START_text,句號.表示當前的VMA,即0xC00000 */
__START_text = . ;
/* *(.text)表示將當前目錄中所有的.text段加入到這個段*/
*(.text);
/* 定義變量__END_text,目前VMA應該是0xC00000加上所有.text段的size總和 */
__END_text = . ;
}
/*****************************************
定義rodata段,起始地址(VMA)從__END_text開始(跟在text段之后),
若沒有指定LMA,表示LMA起始地址同VMA。
*****************************************/
.rodata __END_text :
{
__START_rodata = . ;
*(.rodata);
__END_rodata = . ;
}
/*****************************************
定義bss段,起始地址(VMA)從0開始,
若沒有指定LMA,表示LMA起始地址同VMA。
*****************************************/
.bss 0x00000000:
{
__START_bss = .;
*(.bss);
__END_bss = .;
}
/* 定義可在程序中使用的變量__SIZE_BSS,表示bss段的大小。*/
__SIZE_BSS = __END_bss - __START_bss;
/*****************************************
定義data段,其LMA應該在ROM,而VMA在RAM。
所以,VMA跟在bss段后面,LMA跟在rodata段之后
*****************************************/
.data __END_bss : AT(__END_rodata)
{
__START_data = .;
*(.data);
__END_data = .;
}
/*定義變量__START_data_LMA,表示data段的起始地址*/
__START_DATA_LMA = LOADADDR(.data);
/* 定義可在程序中使用的變量__SIZE_DATA,表示data段的大小。*/
__SIZE_DATA = __END_data - __START_data;
/***********************************************
speed_up模塊的VMA和LMA都是跟在data段之后,
它會被加到可執行文件中,但執行時要載入到RAM才能執行
**************************************************/
.text_speed_up __END_data : AT(__START_data_LMA + SIZEOF(.data))
{
__START_text_speed_up = .;
speed_up_main.o(.text);
speed_up_main.o(.rodata);
speed_up_main.o(.data);
__END_text_speed_up = .;
/* 為便於說明,假設該模塊沒有bss段*/
}
__START_text_speed_up_LMA = LOADADDR(.text_speed_up);
__SIZE_TEXT_SPEED_UP = __END_text_speed_up - __START_text_speed_up;
}
將某個程序模塊(speed_up)傳輸到速度較快的存儲器上執行的代碼如下:
extern unsigned char * __START_text_speed_up;
extern unsigned char * __START_text_speed_up_LMA;
extern int __SIZE_TEXT_SPEED_UP;
void copy_data_section(void)
{
//一個字節一個字節的傳輸(性能較差)
int i;
unsigned char *dest = __START_text_speed_up;
unsigned char *src = __START_text_speed_up_LMA;
for(i=0; i<__SIZE_TEXT_SPEED_UP; i++)
dest[i] = src[i]
}
為bss段賦予0的代碼為:
extern unsigned char * __START_bss;
extern int __SIZE_BSS;
void clear_bss_section(void)
{
int i;
unsigned char * dest = __START_bss;
for(i=0; i<__SIZE_BSS;i++)
dest[i] = 0;
}
(3)Map File或符號表(Symbol Table)
連接后除了產生可執行文件外,通常還要求產生map文件(GNU linker ‘ld’ 的-m參數),其用於記錄項目中每一個Symbol(程序中所有函數、庫函數、全局變量及鏈接器自動產生的各個區段起始和結束地址的變量)的LMA與VMA的對應關系。通過該map文件可以得到如下信息:
- 程序各區段的尋址是否正確;
- 程序各區段的大小;即ROM和RAM的使用量;
- 程序中各符號的地址;
- 各個符號在存儲器中的順序關系
- 各個程序文件的存儲用量
當連接完畢,下載可執行文件到實際存儲器中前,一般需要查看map文件,以確定各區段的起始地址和大小符合自己的設想。下圖是一個map文件的部分截圖:

5、ROM Maker
可執行文件的格式多樣(ELF、COFF、HEX、S-Record、EXE等),但最終要燒到板子ROM里的是二進制文件,所以當得到可執行文件后,還需要通過ROM Maker將其轉化為純二進制文件才能執行燒錄。當然,因為嵌入式系統通常沒有硬盤,所以除了可執行二進制文件之外的文件也必須同時和他們一起轉換成一個總的單一的二進制文件(Image File),這個過程稱為make ROM。具體流程見下圖:

這里說的除了二進制可執行文件之外的一起加入到Image File的文件常見的有圖片(JPG文件等),常見的作法是通過將該圖片文件按字節轉化為常量C數組,並給予一個名稱,在程序中就可以通過直接操作存儲器來使用這些數據,從而避免使用文件系統,但擴展性不強,更新麻煩。所以當這些圖片文件有很多時,還是建議使用文件系統來管理較為方便。
實際上,我們一般會開發一個工具,把某目錄下的所有文件一一轉化為C array的程序,同時會產生一個.h頭文件,其中包含所有代表數據挖掘的C array聲明與每一個數組的大小,使用時只要include這個.h文件就可以使用這些array。
- 文件系統映像
實際上文件系統就是一種訪問數據的接口而已,它是一套實現了數據的存儲、分級組織、訪問和獲取等操作的抽象數據類型。文件系統存儲在哪、用什么樣的格式都有可能,如果系統對文件沒有寫入需求,文件系統照樣可以存儲在ROM中。
在嵌入式系統中,如果沒有寫入數據的需求,我一般會采用一個索引表格,記錄文件名及其在存儲器中的文件首地址和文件大小。為了查找方便,該索引表格一般位於最高地址處。

6、Offline Tools
用於開發階段且必須自己開發並執行於PC上的工具叫做offline tools。這些工具可分為6大類:
-
程序產生器(Program General)
- 系統配置設定工具:一個運行於PC上,用於選擇系統配置(某些功能開關、LCD分辨率等),並自動產生.h文件或make-file。
- Resource Manager:要加到系統中的字符串、圖形或數據文件為resource。例如用於UI界面的小圖形文件必須轉為C array並加到程序中,其它程序則通過resource ID對應到該array。該工具可以使我們僅通過編輯資源文件名,即可自動轉出包含所有resource ID定義的.h文件,以及包含這些resource內容的C array的.c文件。
-
Data Maker:一般嵌入式系統無法使用諸如mySQL這種數據庫,必須根據應用的特性自行設計。
- File System:嚴格來說文件系統也是數據庫,只是它的單元是文件而非記錄。
- Database:所謂的database基本上會包含數據文件與多個層級的索引表(index table)。在CPU算力有限二點前提下,數據庫格式與壓縮算法的設計尤為重要。
- 產品內置的文件:例如一些MP3內置音樂或電子書內置書籍等不可被用戶刪除的文件必須與用戶可編輯的文件區分開,而這就涉及多個文件存儲區域或多個文件系統。
- 產品信息或預設出廠設定:對於一些經常變動的信息(廠商、日期等),我們會傾向於不要寫死在程序里,而是將這些信息存儲在file或Database中,讓系統在運行時取得其中的信息即可。
Image Maker:它的功能就是制作要最后燒入存儲器中的image。該映像中不僅包含程序,可能還包含產品信息、FIle System image、Database等。

- 下載工具:除了用燒錄器寫image進存儲器外,還需要提供局部下載的下載工具,因為有時候僅僅是更新部分程序或文件系統里的某些文件。
- 量產工具:廠商有些信息(廠商名、批號、日期等)只有在燒錄前才可決定,此時需要提供一個工具給廠商,將這些信息寫入image特定位置后才進行存儲器燒錄。
- 模擬器
- 其它工具
7、下載與執行
嚴格來說,所謂的ROM是無法燒錄的。在量產前,要委托專業的Mask-ROM制造商根據我們提供的image File進行一次性燒錄。在開發階段,我們要選用其它可重復讀寫的替代品,通常如NOR-flash、EPROM或EEPROM等。而要將數據寫入一般有以下幾個做法:
- 先利用ICE下載到RAM執行並測試。
- 燒錄器:要先把存儲器的Chip放在燒錄器的socket上,然后利用PC操作廠商提供的燒錄程序,選擇image file並執行燒錄即可。(開發階段板子的存儲器先設計成通過socket與板子連接,我們只要把燒好的IC放在socket上夾好即可)
- ROM-Emulator:該工具就是模擬EPROM/EEPROM,它的一端接到板子的socket上,另一端接到PC上。通過廠商提供的程序,可以將image file下載到ROM模擬器的存儲器內,機器連接着模擬器就如同接着一顆真正的ROM一樣。
- Update程序:在實際板子可以通過某種方式(USB、RS232、網線)與PC連接並傳輸數據的話,我們可以開發一個update程序模塊用於接收PC端的image file內容並將其寫入NOR flash中,如此一來就完成了更新機器上程序版本的功能。
8、版本控制
無論是嵌入式系統或一般軟件項目,只要涉及多人協作開發,就一定要做版本控制。當軟件開發達到某個里程碑或有重大突破時,管理者可以為當前版本取一個名字,我們稱之為Lable或tag,以后任何人都可以從版本控制服務器下載某lable時間點的所有程序。
當系統需要開發一個新功能時,可以建立一個分支(branch),並在該分支上進行開發。成功后在與主分支(master)進行合並。