我对嵌入式系统平台的定义很简单:能让电子产品的原因程序得以顺利开发的环境,主要包括;
- 系统软件与驱动程序
- 硬件平台
- 开发环境(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
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
#include
/************************************
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等。