我對嵌入式系統平台的定義很簡單:能讓電子產品的原因程序得以順利開發的環境,主要包括;
- 系統軟件與驅動程序
- 硬件平台
- 開發環境(compiler、調試與下載工具)
- 模擬器
- 程序編寫規范
所以,在嵌入式軟件開發團隊中一般會有一個 “系統平台組”,他們的工作主要有:
-
系統架構設計與實現
-
嵌入式操作系統設計與實現
-
API設計與實現
-
存儲器使用配置(規范某個模塊或程序能使用的存儲器地址范圍)
-
開發環境設計
-
模擬器設計與實現
-
系統整合(整合驅動程序、系統程序、子系統、庫函數與應用程序)
-
版本制作
1、系統架構設計

(1) 在系統架構設計前:需要清楚產品規格
- 硬件規格:
- CPU速度:根據應用程序或算法的復雜度來確定CPU速度;
- 存儲器容量:根據程序大小、靜態空間、動態空間的大小;
- 外圍設備的性能:
- 產品特性:使用的環境、銷售地區、目標用戶族群及應用特點、范圍等。
- 驅動程序、系統、子系統與應用程序各層間的溝通接口。
- 是否使用面向對象觀念設計系統
- 人力與進度
- 質量要求
- 實時性需求
- 多任務需求
- 電源、可擴展性、可移植性等要求
(2) 系統架構的表述:方塊圖或UML
方塊圖示例:

一個方塊對應一個工作包,且每個方塊可以進一步細分為更小的方塊圖,如此下去便可產生工作列表。在設計系統架構時,必須注意以下原則:API必須簡單明了;程序模塊間的低耦合;設計范圍應包含各模塊的單元測試與壓力測試;利用callback思想,讓應用程序可以嵌入到系統模塊中。接下來,分別對這些方塊做進一步介紹:
- 硬件層
該層有真正的硬件和模擬器兩個東西。前一個好理解,就是我們程序真正要運行於上的硬件,模擬器是基於PC的一個程序,它能夠無差別的模擬真實硬件的行為,一般價格較貴。
- 驅動程序層
該層和硬件完全相關,除了要實現驅動周邊設備的功能外,還得提供可讓其它程序模塊調用的API。驅動程序可以屏蔽所有硬件特征,上層應用只需要使用它提供的API就可以調用硬件設備,故驅動程序層也稱為硬件抽象層HAL(Hardware abstraction Level)。
-
操作系統層
用於嵌入式的OS一般稱為RTOS,功能一般較為簡單。常見的有Linux,uclinux,ucOS,WinCE等。把操作系統分為兩個部分的原因是模擬器必須另外模擬和硬件相關的功能,而與硬件無關的部分則只需要一套程序即可。
-
和硬件相關的部分
與硬件相關的功能有:多任務功能、中斷控制、實時時鍾RTC以及定時器等。
-
和硬件無關的部分
與硬件相關的功能有:進程間通信、同步機制、動態存儲器功能等
-
-
圖形函數庫、GUI子系統
如果產品有屏幕用於交互時,就必須提供簡單的視窗系統。GUI使用圖形函數庫(點、線、面)來實現圖像顯示,同時還得有處理字型顯示功能。
-
其它子系統
網絡通信協議、解壓縮庫函數、文件系統等功能模塊。
-
應用程序層
之前所述各層均是系統功能,除了與硬件無關,都是通用功能。但如果能把某些近似的功能作為某個領域的專用模塊庫,對軟件的復用也是很有益的。
-
- 基於產品特色的專屬功能庫函數:將許多可共享的模塊抽取出來成為庫函數。
- 應用程序層:利用上述各層提供的API,實現產品功能的程序
當然也可以利用UML來描述這樣的架構,它提供了多種圖表達了不同角度下的系統:
觀點 | 圖形 |
---|---|
使用者模型觀點 | 用例圖(User Case Diagram) |
結構模型觀點 | 類圖(Class Diagram) |
行為模型觀點 | 順序圖(Sequence Diagram)與協作圖(Collaboration Diagram);狀態圖(State Diagram);活動圖(Activity Diagram) |
實作模型觀點 | 組件圖(Component Diagram) |
環境模型觀點 | 部署圖(Deployment Diagram) |
(3) 數據流

- 兩個輸入源:外圍設備狀態發生變化,系統通過“輪詢(Polling)”或“中斷(interrupt)”兩種方法獲得輸入數據。
- 觸發驅動程序:若系統采用輪詢方式,則驅動程序由監督程序引發;若采用中斷方式,則相應的中斷處理程序(ISR)會被執行。
- ISR判斷硬件狀態的變化,同時向系統層傳遞消息。可通過設定全局變量、消息隊列等。
- 系統會有一個無限循環,循環的工作就是檢查或等待新的硬件事件的到達。若有則處理,若無則進入idle 模式。
while(1)
{
os_MSG new_msg;
if(os_get_msg(&new_msg))
{
//在消息隊列中有新的消息,則處理新的消息
os_process_msg(&new_msg);
}
else
{
//沒有新消息到達,則進入待機模式(idle mode)
//當有新的消息到達時,系統會自動離開待機模式,並繼續執行該循環語句
drv_enter_idle_mode();
}
}
- 系統層決定是否將消息送給應用程序。其溝通方式可以是:應用程序對欲處理的硬件事件注冊回調函數(callback function),則當該時間內發生時,系統會自動執行應用程序的事件處理函數。
(4) 可重用性及可移植性
在設計程序時要注意把與硬件相關的和硬件無關的模塊分開。因為硬件無關的模塊可以重用到其它項目里,而硬件相關的模塊可移植性和可重用性一般都很低。在嵌入式領域,在設計階段就把硬件相關和硬件無關的模塊明確區分開,而且各模塊間只能使用公開的API來溝通。例如:

(5) 可擴展性及可調整性
嵌入式系統中,通常會通過一個配置文件sys_config.h
文件來定義一些宏,然后采用條件式編譯去選擇系統所需要包含的模塊。例如:
/************************************************ File Name :sys_config.h Function: 通過定義合適的宏,去編譯包含特定模塊的程序 ************************************************/
//常數定義
//
#define _HW_CONFIG_FALSE 0
#define _HW_CONFIG_TRUE 1
#define KME_MODEL_A 1
#define KME_MODEL_B 2
#define KME_MODEL_C 3
//定義產品名稱
//
#define PRODUCT_NAME KME_MODEL_B
//定義系統是否支持某些硬件的驅動程序
//
#define HW_MUSIC_MP3_SUPPORT _HW_CONFIG_FALSE
#define HW_MR_SENSOR_SUPPORT _HW_CONFIG_FALSE
#define HW_REMOTE_CONTROLLER_SUPPORT _HW_CONFIG_FALSE
#define HW_SD_CARD_SUPPORT _HW_CONFIG_FALSE
//定義LCD相關屬性
//
#if (PRODUCT_NAME == KEM_MODEL_A)
//產品2的LCD分辨率為160x160
//
#define LCD_RESOLUTION_WIDTH 160
#define LCD_RESOLUTION_HEIGHT 160
#else
#define LCD_RESOLUTION_WIDTH 160
#define LCD_RESOLUTION_HEIGHT 240
#endif
#define LCD_LCD_COLOR_LEVEL 8
#define WITH_TOUCH_PANEL _HW_CONFIG_TRUE
//定義相同是否支持某些子系統
//
#define SYS_TCPIP_SUPPORT _HW_CONFIG_FALSE
#define SYS_FAT32_SUPPORT _HW_CONFIG_FALSE
/******************************************* File Name :my_function.c Function:條件式編譯范例 ******************************************/
#include <sys_config.h>
void my_function(void)
{
#ifdef(PRODUCT_NAME == KEM_MODEL_B)
//和產品B有關的程序段
//
product_B();
#else
//這段程序給產品B之外的產品使用
//
not_product_B();
#endif
#if(HW_MUSIC_MP3_SUPPORT == _HW_CONFIG_TRUE)
//和播放MP3有關的程序段
//
play_mp3();
#else
// 系統不支持MP3時采用這段程序
//
do_nothing();
#endif
}
這些可調整的系統配置應盡量在設計階段的初期就要制定好,而且必須統一管理,並明確定義各個配置的意義,作為負責系統架構設計人員的遵循。每個配置都必須時一個絕對獨立的模塊,當其是否加入到系統中去時,不會對其它程序模塊造成影響。
2、API與程序風格設計
系統架構與模塊規划的設計工作結束后,接下來會將各個模塊交給程序開發人員進行細部設計,此時必須有人去制定系統程序和應用程序的寫作風格!
(1) 系統程序風格
系統程序也有許多風格上的限制,但總體沒有那么強。例如,在前述數據流架構圖中的message-dispatcher,它是一個和產品有關的系統模塊,不同產品可能會有不同的需求,但基本的程序架構應該是一樣的,從設計文件中的sample code 或 pseudo code 可以獲得范例。
// sample code of message dispatcher
// - forever loop
//
void os_message_dispatcher(void)
{
struct os_msg new_msg;
struct os_event new_event;
while(1)
{
//取得新的消息
//
if(os_get_msg(&new_msg) == TRUE)
{
//driver層送來新的消息
//
new_event = os_preprocess_message(&new_msg);
if(new_event == NULL)
continue; //事件已經處理,無須再網上傳遞
if(new_evnet.owner != NULL)
{
//將事件傳遞給指定的應用程序或對象
//
os_send_sys_event(&new_event);
}
else
{
//處理新事件,方法視具體產品而定
//1. 送給current/active AP
//2. 送給所有的AP或對象,由其自己決定
//
os_process_new_event(&new_event);
}
}
else
{
//暫時沒有硬件信息,讓系統進入待機模式
//
os_enter_idle_mode();
}
}
}
所以,當使用這個系統來開發某個產品時,可以直接拿這個范例來修改后使用。系統設計文件中除了提供范例外,還可能會規范一種系統程序編寫風格,這是基於系統架構與設計理念而來的。例如,系統時采用面向結構的思想,所有模塊都應該表現出以“以處理信息為主”的特性。以下為一個典型的面向結構系統模塊的程序風格:
// 系統模塊程序風格規范
//重要原則:
// 聲明這個模塊中處理各種信息的靜態函數(類似對象的method)
// 1. 這些函數只有這個模塊會調用
// 2.每種信息有其專用的處理函數
//
static int xxx_msg_1_processor(struct message * new_msg);
static int xxx_msg_2_processor(struct message * new_msg);
static int xxx_msg_3_processor(struct message * new_msg);
static int xxx_msg_default_processor(struct message * new_msg);
/**************************************************** foo模塊信息處理程序:foo_module_basic_message_processor ****************************************************/
int foo_module_basic_message_processor(struct message * new_msg)
{
int msg_type = new_msg->message_type;
switch(msg_type)
{
case MSG_TYPE_001:
return xxx_msg_1_processor(new_msg);
case MSG_TYPE_002:
return xxx_msg_2_processor(new_msg);
case MSG_TYPE_003:
return xxx_msg_3_processor(new_msg);
default:
xxx_msg_default_processor(new_msg);
}
return MSG_PASS; //繼續讓其它模塊處理這個消息
}
/************************************************* foo模塊信息1處理程序:foo_msg_1_processor *************************************************/
static int foo_msg_1_processor(struct message * new_msg)
{
//處理第一種類信息的程序代碼
//
...
//已處理完畢,系統無須再將此信息送給其它模塊
return MSG_PROCESSED;
}
...
(2) 應用程序風格
應用程序編寫風格規范的思想比較簡單,在此需要強調的注意事項有兩個:
- 制定應用程序編寫時應注意的限制事項,並且要求應用程序員遵守。如程序的生命周期、使用的資源等
- 如果應用程序采用面向對象方法設計,但由於編譯器的限制或出於性能考慮只能使用C語言來開發時,會有一些規范或建議。我會在專門的文章中詳細敘述。
(3)API
一份好的API文件應該包含:
- 該模塊的功能說明與使用范圍
- 數據結構與常數說明
- 各個函數的功能、參數與返回值的意義說明
- 足夠多的范例
- 注意事項及限制
- 相關函數
例如:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-KdkoMEhG-1605799763797)(C:\Users\liang\AppData\Roaming\Typora\typora-user-images\image-20201115222915437.png)]
3、嵌入式操作系統
嵌入式操作系統包括uC/OS、Embedded Linux、uCLinux、FreeRTOS、Android等,絕大多數嵌入式操作系統都是實時操作系統,他們具有以下特性:
- 可移植性高(Portable)
- ROMable:通常小型嵌入式系統沒有磁盤設備及文件系統的思想,所以系統必須可在ROM里直接執行。
- 可調整性(Scalable)與可重組性(Configurable)
- 多任務(Multi-Tasking)與任務管理
- 可調整的任務調度算法(Scheduling Algorithm)
- Task(Threads)的同步機制:semaphore、Mutex等
- Task間通信機制(IPC):message queue、mail box
- 中斷機制
- 存儲器管理
- 資源管理
操作系統的核心任務還是任務調度,有些嵌入式系統包含了調試子系統,在執行時期可以和PC的程序溝通,執行調試命令或送出調試信息。
(1)嵌入式系統Task架構
下面以只有一個主任務的產品為例,因為客戶有省電的需求,所以當主任務沒事做的時候,系統會自行休眠,並將控制權交給優先級較低的idle task,由idle task負責讓CPU進入睡眠模式。
當有硬件事件發生時,其ISR會將硬件事件傳入main task的message queue中,此時CPU會從睡眠模式中醒來,idle task繼續執行,接着喚醒(Wakeup)main task。因為main task的優先級較高,所以系統會將CPU使用權交給main task,main task可以處理新來的硬件事件,處理完后又回去sleep,系統再將控制權交給idle task以進入待機模式。
下圖說明了系統中task與ISR交互關系圖及實際偽代碼:
(2)多任務編程注意事項
A. 多任務系統執行流程
B. 任務調度時機
如果確定采用多任務系統,那么必須在設計階段就要把調度算法確定下來,而且在實際coding前,每個程序開發者都要清楚系統的調度算法。因為不同的算法所采用的編程方法或者說同步機制是不一樣的。
常見的3種需要任務調度的時機有:
- boot階段結束,系統要選出第一個執行的任務
- 與調度有關的系統功能(sleep、delay、wakeup-task、wait_event等)
- 發生硬件中斷,ISR執行完畢后。
C. RTOS多任務系統的特性
- 多任務系統 = 多個用戶的任務 + 調度器
- 任務的執行點(enter point):一個C語言函數
- 活動任務(Active Task):Function + Context(上下文)。
- 系統會將各種任務的信息控制塊(Task-Control-Block)存儲在若干個鏈表(list)中,調度器根據這些控制塊來進行調度。如下圖所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-0muBguUh-1605799763812)(https://cdn.jsdelivr.net/gh/Leon1023/leon_pics/img/20201116203347.png)]
D. RTOS多任務系統的常見調度算法
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-pTHzFgVB-1605799763813)(https://cdn.jsdelivr.net/gh/Leon1023/leon_pics/img/20201116204903.png)]
在確定選擇哪種調度算法前,需要考慮任務是否可搶占的(preemptive)和任務的執行順序和執行時間是否是可預測的(determinative)。
E. RTOS多任務系統的注意事項
1、選擇合適的調度算法,盡量消除任務間執行順序的不確定性;
2、注意對臨界段(Critical section)的保護;
不像Linux或Windows,區分了用戶模式和內核模式,且不同每個應用程序有自己的地址空間,不會相互干擾。在RTOS中,所有程序共享地址空間,一不小心就可能會相互影響,再加上隨機的中斷影響,程序的執行順序無法准確預測。為此,系統必須提供對共享變量或某程序段的保護機制。
其中,禁止中斷是最有效的保護機制之一,但頻繁禁止中斷也會導致系統實時性的降低。為此,系統又提供了互斥鎖(Mutex)和信號量(Semaphore)等機制。但使用他們也要注意防止死鎖(deadlock)和飢餓(starvation)的發生。通過模塊化,降低系統的復雜度,減少變量共享的機會(臨界段的數量)才是王道!
隨着現在嵌入式的發展,Linux一般都用來作為功能復雜嵌入式系統的平台,那么如何將原有RTOS的程序移植到Linux中呢?一種方法是:讓RTOS上的多個task,執行與linux的一個process(進程)中。使得該產品的核心功能可在linux上正常運行,然后如果想添加擴展功能,可在linux里新增process來實現。
4、Source Tree設計與程序風格規范
source tree 用來規范整個系統源代碼的結構,決定哪個代碼文件放在哪個目錄下。一般的原則是要使系統程序的目錄架構便於在其它項目中使用,即滿足可移植性。基本要做到區分硬件相關、硬件無關、產品相關、產品無關。下圖是一個范例:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-HPDNExyR-1605799763816)(https://cdn.jsdelivr.net/gh/Leon1023/leon_pics/img/20201116222300.png)]
-
程序風格規范(Programming Style Convention)
所謂的程序風格規范,不僅使命名規格,還規范了程序代碼中至少應該具有哪些信息,以及程序編寫的注意事項。
- 文件用途描述
- 作者與日期
- 修改履歷
- 用特殊顯眼的符號區分段落
- 每個function必須詳述用途、各個參數意義、返回值意義
- 每個全局變量的用途
- 數據結構中每個組成元素的意義
- 多寫注解
- 縮進整齊(使用tab而不是空格)
- 如果for、while、嵌套if語句過長,則在結尾大括號處要有注解說明這個循環或判斷的內容。
- 不要吝嗇空白行
- 一個程序文件大小控制在1000—2000行,一個函數的行數控制在50行(一頁)左右。
以下是一個范例:
// os_message_queue.c
//
/*********************************************************** 程序名稱:os_message_queue.c 所在目錄:library/os 項目名稱:Typhoon 2020 創建者 :S202001(工號)Leon George 程序用途:。。。。。。。。 版權聲明:Copyright (C) KME S/W Co.Ltd. All Right Reserved 維護信息:2020/11/1 created by Leon George 2020/11/7 Add new API - emptyQueue() by Leon George 。。。 *************************************************************/
/************************************ INCLUDE FILE *************************************/
#include <system_config.h>
#include <os\os_message_queue.h>
/************************************ CONSTANT Definition *************************************/
//常數名稱的所有字母大寫
//只會在本文件中用到的常數不必定義在.h文件中
//常數的詳細用途解釋。。。
#define OS_MSGQ_MAX_ENTRY_NO 20
。。。
/************************************ 數據結構與數據類型定義 *************************************/
//一般數據結構定義在.h文件中,除非它是靜態的
//數據結構的詳細用途
//
struct messageQueue
{
//詳細解釋各組成元素的用途與約束
struct message mQueue[OS_MSGO_MAX_ENTRY_NO];
//使用時的注意事項:initial Value must be 0
short mqFront;
short mqRear;
}
/************************************ Global Variable Definition *************************************/
//“p"表示pointer
//全局變量:首字母大寫
//全局變量的詳細用途解釋
//
struct messageQueue * pSystemMessageQueue = NULL;
/************************************ 靜態函數聲明 *************************************/
//靜態函數不會聲明在.h文件中
//靜態函數的用途、參數、返回值解釋
//
static void os_msgq_internal_func(...);
//用特殊顯眼的符號區分程序段落
/************************************ FUNCTION NAME:os_msgq_initMessageQueue 函數用途:。。。 參數描述:。。。 返回值描述:。。。 特殊算法:。。。 注意事項:。。。 *************************************/
short os_msgq_initMessageQueue(struct messageQueue *newMQueue)
{
//局部變量定義,盡量描述其用途
//“p”表示指針
//局部變量首字母小寫
struct messageQueue *pnew_MQueue = NULL;
int i;
if(newMQueue != NULL)
{
//如果又用到任何特殊技巧,一定要寫明
...
for(i=0; i < OS_MSGQ_MAX_ENTRY_NO; i++)
{
...
}//程序中的縮進必須整齊
}//如果內容太長,要在結尾說明該語句內容
}
...
//end of program - os_message_queue.c
-
頭文件規范(header file)
好的頭文件應該達到只需要看該模塊的頭文件就可以在它的程序中會使用該模塊的要求。
-
所有程序文件的規定
-
避免重復include造成的重復定義。
-
#ifndef XXX_OS_MSG_QUEUE_H #define XXX_OS_MSG_QUEUE_H //XXX_OS_MSG_QUEUE_H是一個在其它地方不會用到或定義 。。。實際內容 #endif
-
-
常數和宏定義必須描述清楚他們的用途
-
定義數據結構(struct、union、enum)和數據類型(typedef)時,必須詳述其意義
-
頭文件應只包含函數或變量的聲明(declaration),而非定義(definition),不要在.h文件中實現函數或定義變量
-
代碼編寫規范(coding style)
//程序中的縮進必須整齊
}//如果內容太長,要在結尾說明該語句內容
}
…
//end of program - os_message_queue.c
* **頭文件規范(header file)**
好的頭文件應該達到**只需要看該模塊的頭文件就可以在它的程序中會使用該模塊**的要求。
* 所有程序文件的規定
* 避免重復include造成的重復定義。
* ```c
#ifndef XXX_OS_MSG_QUEUE_H
#define XXX_OS_MSG_QUEUE_H
//XXX_OS_MSG_QUEUE_H是一個在其它地方不會用到或定義
。。。實際內容
#endif
```
* 常數和宏定義必須描述清楚他們的用途
* 定義數據結構(struct、union、enum)和數據類型(typedef)時,必須詳述其意義
* 頭文件應只包含函數或變量的聲明(declaration),而非定義(definition),不要在.h文件中實現函數或定義變量
**代碼編寫規范(coding style)**
有很多靜態測試工具可以幫助我們進行代碼的review,例如C語言編譯器就自帶代碼靜態檢測工具,對查出的問題會分別以“Warning”和“error”來指出。除此之外,在嵌入式領域,還有專門針對嵌入式C編程的代碼靜態測試規則,最流行的就是**MISRA C**,其定義了21類共141個規則,這些規則又分為強制性規則(Required)和推薦規則(Advisory)。很多嵌入進靜態測試工具里會包含有該規則集,這些工具有PC-Lint、LDRA Testbed、LogiScope/Rule-Check等。