大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家講的是嵌入式開發里的project文件。
前面兩節課里,痞子衡分別給大家介紹了嵌入式開發中的兩種典型input文件:源文件(.c/.h/.s)、鏈接文件(.icf)。痞子衡要再次提問了,還有沒有input文件呢?答案確實是有,但這次真的是有且僅有了,本文要介紹的主角project文件也屬於半個input文件。為什么說是半個?因為project文件不僅包含開發者指定的input信息,還包含很多其他輔助調試的input/output信息,算是嵌入式開發中承前啟后的文件。而本文側重點在於project文件中與開發者應用相關的input信息,僅當得到了這些input信息,再加上前面介紹的source和linker文件,那么你就已經得到了application所有的信息,你可以用它們來可以生成無歧義的可執行image binary。
隨着嵌入式軟件工程的發展,為了應對日益復雜的需求,現代IDE的功能也越來越強大了,IDE版本更迭讓人應接不暇,Keil MDK已然踏入5.0時代,IAR EWARM更是進入了8.0時代,IDE各有千秋,但本文要講的內容卻是每個IDE必須具有的基本功能,還是繼續以IAR EWARM為例開始今天的內容:
一、標准IDE功能
在開始今天的主題之前,痞子衡覺得有必要先簡要給大家科普一下標准IDE應該具有的功能。現代IDE基本都是由組件構成,嵌入式開發中的每個階段都對應着相應的組件,由這些組件去實現各階段的需求。
1.1 IDE組件
標准嵌入式開發應該至少包括以下6個階段,而IAR里對於每個階段都有1個或多個組件:
- 輸入(IAR Editor):編輯源文件代碼。
- 編譯(ICCARM、IASMARM):編譯源文件代碼生成可執行二進制機器碼。
- 分析(C-STAT、MISRA-C):編譯過程中檢查代碼中潛在的問題。
- 鏈接(ILINK):鏈接可執行二進制機器碼到指定ARM存儲空間地址。
- 下載(I-jet、flashloader):將鏈接好的可執行二進制機器碼下載進芯片內部非易失性存儲器。
- 調試(C-SPY、C-RUN):在線調試代碼在芯片中執行情況。
project文件主要用來記錄整合上述6個階段的所有開發需求。
1.2 IDE文件類型
既然IDE有很多組件,那么同時也會存在不同類型的文件以存儲這些組件的所需要的信息。IAR里支持的文件擴展類型非常多,痞子衡在這里僅列舉你所創建的工程根目錄下的與工程同名的擴展文件,相信你一定會覺得眼熟。
.eww // Workspace file
.ewp // IAR Embedded Workbench project
.ewd // Project settings for C-SPY
.ewt // Project settings for C-STAT and C-RUN</td>
.dep // Dependency information
本文要講的內容都包含在.ewp文件里,ewp文件記錄了開發者為應用指定的不可缺少的input信息,沒有這些信息,application工程是不完整的。換句話說,如果你得到了application的所有source文件和linker文件,但沒有ewp文件的話,可能導致最終生成的image binary文件是不同的。
Note:更多IAR支持的擴展文件類型請查閱IAR軟件安裝目錄下\IAR Systems\Embedded Workbench xxx\arm\doc\EWARM_IDEGuide.ENU.pdf文檔里的File types一節。
二、解析project(ewp)文件
前面痞子衡鋪墊了很多IDE/project基礎概念,該是直奔主題的時候了,本文主角ewp工程文件到底包含哪些開發者指定的input信息?痞子衡從下面3個方面為大家揭秘:
2.1 源文件組織
一個稍微復雜一點的嵌入式工程,應用代碼行數應該是以百行/千行為單位計算的(此處僅指的是由開發者自己創建的文件與代碼),我們在組織代碼的時候肯定不會只創建一個.c文件,單文件會導致代碼功能模塊結構不清晰,不方便工程的管理與維護。
當我們為工程創建多個文件時,就會涉及到一個必然問題:引用路徑問題(所以路徑信息就是本文要說的第一個input信息)。當源文件數目較多時,通常我們會創建不同文件夾把相同功能的源文件都放在一起,當編譯器開始編譯.c源文件時會搜索include語句所包含的頭文件。熟悉C語言的朋友肯定清楚下面兩種不同include語句的用法:
#include <file.h> // 引用編譯器類庫下的頭文件(IDE安裝路徑)
#include "file.h" // 引用當前工程下的頭文件(project路徑)
所以在ewp文件里會包含路徑信息,所有路徑都應該列在Options->C/C++ Compiler->Preprocessor下有Additional include directories里,這個路徑既可以是當前PC的絕對路徑,也可以是以ewp文件為基准的相對路徑,為了保證工程可以在任意PC任意位置下正常編譯,推薦使用如下相對路徑方式列出所有路徑:
ewp當前路徑:$PROJ_DIR$/
ewp下級路徑:$PROJ_DIR$/xxFolder/
ewp上級路徑:$PROJ_DIR$/../
說到路徑問題,痞子衡在這里順便給大家介紹一種經典的嵌入式工程文件目錄組織方式:
\projectDir
\doc --放置工程文檔
\bsp --放置bsp(板級)相關的source file
\linker --工程linker文件
\src --板級相關的源文件(比如pinout,clock等)
\builds\xxBuild\.ewp --工程ewp文件
.eww --工程workspace文件
\src --放置bsp無關的source file
\platform --芯片頭文件及CMSIS文件
\drivers --芯片片內外設driver
\include --要被所有source引用的頭文件
\startup --標准的startup code
\utilities --標准的通用函數
\middleware --獨立的中間件
\components --板級外設組件driver
\application --當前應用主邏輯代碼
2.2 全局宏定義
經常使用條件編譯的朋友肯定知道workspace文件與project文件的關系,一個項目通常只會有一個eww文件,但卻可能會有多個ewp文件,這是因為源代碼里常常會有條件編譯,我們有時候會給項目不同的配置從而編譯出不同的結果(速度優先/面積優先,特性控制...),這些配置就是由全局宏定義來實現的,打開Options->C/C++ Compiler->Preprocessor下的Defined symbols,在框內寫入你需要定義的全局宏:
MACRO1 // 等價於源文件里的#define MACRO1 (1)
MACRO2=2 // 等價於源文件里的#define MACRO2 (2)
全局宏信息就是本文要說的第二個input信息,如果全局宏信息丟失,有時候工程編譯並不會報錯,因為編譯器在處理如下普遍用法里的條件編譯語句時會默認未定義的宏為0,而在處理普遍用法里的條件編譯語句則會報錯,所以推薦大家使用第二種條件編譯用法來規避全局宏問題。
// 普遍用法
#if MACRO
// your code block 1
#else
// your code block 2
#endif
// 推薦用法
#if !defined(MACRO)
#error "No valid MACRO defined!"
#elif (MACRO == 1)
// your code block 1
#else
// your code block 2
#endif
2.3 編譯選項
編譯選項包含了編譯器所需要的所有信息,代碼需經過編譯器編譯才能生成二進制機器碼,不同的編譯器選項配置會生成不同的機器碼,那么需要指定哪些選項呢?打開project的Options選項卡,分別設置下表item:
Position | Item | Description |
---|---|---|
General Options->Target-> | Processor variant->Core | 指定ARM內核版本 |
Endian mode | 指定內核大小端模式 | |
Floating point settings->FPU | 指定內核支持的FPU版本 | |
General Options->Library Configuration-> | Library | 選擇C/C++動態鏈接庫版本 |
General Options->Library Option 2-> | Heap selection | 選擇HEAP實現版本 |
C/C++ Compiler-> | Language 1->Language | 指定編程語言類型 |
Language 1->C dialect | 指定C語言標准 | |
Language 1->Language conformance | 選擇對標准C/C++的遵循程度 | |
Language 2->Plain 'char' is | 選擇對char的符號性默認處理方法 | |
Language 2->Floating-point semantics | 選擇對浮點數的處理遵循C標准的程度 | |
Code->Process mode | 指定內核指令集模式 | |
Code->Position-independence | 選擇要生成位置無關代碼的對象 | |
Optimizations->Level | 選擇優化等級 |
Note:更多ewp文件中option解釋請查閱IAR軟件安裝目錄下\IAR Systems\Embedded Workbench xxx\arm\doc\EWARM_IDEGuide.ENU.pdf文檔里的General Options和Compiler Options倆小節。
編譯設置信息就是本文要說的第三個input信息,當在project中組織好源文件並設置好正確的全局宏定義和編譯選項,那么恭喜你,你的application設計工作已經基本完成了。
三、創建demo工程
為方便后續課程的進行,本節課在最后順便創建一個demo工程,以下是demo工程的信息:
IDE: IAR EWARM v8.11.2
Device: NXP MKL25Z128VLH4
project layout:
\D\myProject\bsp\builds\demo\demo.ewp
\D\myProject\bsp\linker\iar\KL25Z128xxx4_flash.icf
\D\myProject\bsp\src\startup_MKL25Z4.s (僅保留前16個系統中斷)
\D\myProject\bsp\src\system_MKL25Z4.c (僅做關閉WDOG操作)
\D\myProject\bsp\src\system_MKL25Z4.h
\D\myProject\bsp\helloArm.eww
\D\myProject\src\platfrom\CMSIS
\D\myProject\src\platfrom\devices\MKL25Z4
\D\myProject\src\startup\reset.s
\D\myProject\src\startup\startup.c
\D\myProject\src\startup\startup.h
\D\myProject\src\application\main.c
\D\myProject\src\application\task.c
\D\myProject\src\application\task.h
// main.c
//////////////////////////////////////////////////////////
#include "task.h"
const uint32_t s_constant = 0x7f;
int main(void)
{
uint32_t l_variable = 0x7f;
if (s_constant == l_variable)
{
normal_task();
ram_task();
heap_task();
}
while (1);
}
// task.c
//////////////////////////////////////////////////////////
#include "task.h"
static uint32_t s_variable0;
__no_init uint32_t n_variable1;
static uint32_t s_variable2 = 0x5a;
static uint8_t s_array[16];
void normal_task(void)
{
s_variable0 *= 2;
}
__ramfunc void ram_task(void)
{
n_variable1++;
}
void heap_task(void)
{
uint8_t *heap = (uint8_t *)malloc(16 * sizeof(uint8_t));
if (heap != NULL)
{
memset(heap, 0xa5+s_variable2, 16);
memcpy(s_array, heap, 16);
s_variable0 = (uint32_t)heap;
free(heap);
}
}
番外一、幾個小技巧
又來到痞子衡番外時間了,細心的朋友看到上表有兩處標藍,是的沒錯,今天的番外內容就是標藍的項目有關。
技巧1:運行於異構雙核
目前嵌入式產品越來越復雜,對MCU的性能要求也越來越高,各大ARM廠商也在不斷推出性能越來越強勁的ARM MCU產品,超高主頻,雙核,四核MCU已經不鮮見了。對於其中的一些異構雙核MCU產品,有時在開發中會有這樣的需求:你有一份的middleware會被異構雙核同時調用,而兩個不同內核的指令集有可能是不一致的,怎么解決這個問題?有朋友會想到分別在每個核下面都編譯一份binary放置於存儲器不同位置,運行時各自指向對應的binary,這是一個辦法,但比較浪費存儲空間,且有可能會搞混淆導致誤調用。有沒有更好的方法?
為了能做到Cortex-M軟件重用,ARM公司在設計Cortex-M處理器時為其賦予了處理器向下兼容、軟件二進制向上兼容特性。通俗的話來說就是在較低版本處理器上編譯的代碼可以在較高版本處理器上執行。所以解決方法就是選用異構雙核里較低版本的內核在編譯middleware,這樣這份middleware可以同時被兩個核調用。
技巧2:生成PIC代碼
經常和bootloader打交道的朋友肯定知道,代碼在經過鏈接階段生成binary文件后,這個binary並不是可以放在任意位置的,必須放到linker文件指定的位置,如果位置沒有放正確,可能會導致執行出錯。究其原因,是因為編譯器在匯編源代碼時因為一些策略並不總是將所有function都匯編成位置無關代碼。如果我們借助於IDE編譯選項將middleware匯編成PIC代碼,那么我們可以在工程中直接加入middleware的binary,然后借助linker的自定義section功能將其放置於任意某個位置,最后只要為這個middleware binary建立一個以binary首地址為基准的函數指針地址列表即可無障礙調用這個middleware。
技巧3:引用.c文件
在項目開發中,我們在一個workspace下會創建多個project,常常是因為不同project需要包含不同的.c文件以完成不同的功能。那么能不能只創建一個project呢能實現不同功能呢?當然可以!通常情況下我們在.c文件中只會用#include "xx.h"語句來引用.h頭文件,其實我們也同樣可以引用.c文件,比如這樣#include "xx.c",只是需要注意盡量不要在.h文件中引用.c文件(除非該.h只會被一個.c文件include)。看到這里的朋友如果腦洞再大一點,你甚至可以做到工程里只需要添加一個.c文件,而其他.c文件全部由添加進工程的那個.c文件逐級(僅能單級)引用進工程。
至此,嵌入式開發里的project文件痞子衡便介紹完畢了,掌聲在哪里~~~
歡迎訂閱
文章會同時發布到我的 博客園主頁、CSDN主頁、微信公眾號 平台上。
微信搜索"痞子衡嵌入式"或者掃描下面二維碼,就可以在手機上第一時間看了哦。