我对嵌入式系统平台的定义很简单:能让电子产品的原因程序得以顺利开发的环境,主要包括;
所以,在嵌入式软件开发团队中一般会有一个 “系统平台组”,他们的工作主要有:
方块图示例:
一个方块对应一个工作包,且每个方块可以进一步细分为更小的方块图,如此下去便可产生工作列表。在设计系统架构时,必须注意以下原则:API必须简单明了;程序模块间的低耦合;设计范围应包含各模块的单元测试与压力测试;利用callback思想,让应用程序可以嵌入到系统模块中。接下来,分别对这些方块做进一步介绍:
该层有真正的硬件和模拟器两个东西。前一个好理解,就是我们程序真正要运行于上的硬件,模拟器是基于PC的一个程序,它能够无差别的模拟真实硬件的行为,一般价格较贵。
该层和硬件完全相关,除了要实现驱动周边设备的功能外,还得提供可让其它程序模块调用的API。驱动程序可以屏蔽所有硬件特征,上层应用只需要使用它提供的API就可以调用硬件设备,故驱动程序层也称为硬件抽象层HAL(Hardware abstraction Level)。
用于嵌入式的OS一般称为RTOS,功能一般较为简单。常见的有Linux,uclinux,ucOS,WinCE等。把操作系统分为两个部分的原因是模拟器必须另外模拟和硬件相关的功能,而与硬件无关的部分则只需要一套程序即可。
如果产品有屏幕用于交互时,就必须提供简单的视窗系统。GUI使用图形函数库(点、线、面)来实现图像显示,同时还得有处理字型显示功能。
网络通信协议、解压缩库函数、文件系统等功能模块。
之前所述各层均是系统功能,除了与硬件无关,都是通用功能。但如果能把某些近似的功能作为某个领域的专用模块库,对软件的复用也是很有益的。
当然也可以利用UML来描述这样的架构,它提供了多种图表达了不同角度下的系统:
| 观点 | 图形 | | :------------: | :----------------------------------------------------------: | | 使用者模型观点 | 用例图(User Case Diagram) | | 结构模型观点 | 类图(Class Diagram) | | 行为模型观点 | 顺序图(Sequence Diagram)与协作图(Collaboration Diagram);状态图(State Diagram);活动图(Activity Diagram) | | 实作模型观点 | 组件图(Component Diagram) | | 环境模型观点 | 部署图(Deployment Diagram) |
登录后复制
while(1)
{
os_MSG new_msg;
if(os_get_msg(&new_msg))
{
//在消息队列中有新的消息,则处理新的消息
os_process_msg(&new_msg);
}
else
{
//没有新消息到达,则进入待机模式(idle mode)
//当有新的消息到达时,系统会自动离开待机模式,并继续执行该循环语句
drv_enter_idle_mode();
}
}
在设计程序时要注意把与硬件相关的和硬件无关的模块分开。因为硬件无关的模块可以重用到其它项目里,而硬件相关的模块可移植性和可重用性一般都很低。在嵌入式领域,在设计阶段就把硬件相关和硬件无关的模块明确区分开,而且各模块间只能使用公开的API来沟通。例如:
嵌入式系统中,通常会通过一个配置文件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
}
这些可调整的系统配置应尽量在设计阶段的初期就要制定好,而且必须统一管理,并明确定义各个配置的意义,作为负责系统架构设计人员的遵循。每个配置都必须时一个绝对独立的模块,当其是否加入到系统中去时,不会对其它程序模块造成影响。
系统架构与模块规划的设计工作结束后,接下来会将各个模块交给程序开发人员进行细部设计,此时必须有人去制定系统程序和应用程序的写作风格!
系统程序也有许多风格上的限制,但总体没有那么强。例如,在前述数据流架构图中的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;
}
...
应用程序编写风格规范的思想比较简单,在此需要强调的注意事项有两个:
一份好的API文件应该包含:
嵌入式操作系统包括uC/OS、Embedded Linux、uCLinux、FreeRTOS、Android等,绝大多数嵌入式操作系统都是实时操作系统,他们具有以下特性:
操作系统的核心任务还是任务调度,有些嵌入式系统包含了调试子系统,在执行时期可以和PC的程序沟通,执行调试命令或送出调试信息。
下面以只有一个主任务的产品为例,因为客户有省电的需求,所以当主任务没事做的时候,系统会自行休眠,并将控制权交给优先级较低的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交互关系图及实际伪代码:
如果确定采用多任务系统,那么必须在设计阶段就要把调度算法确定下来,而且在实际coding前,每个程序开发者都要清楚系统的调度算法。因为不同的算法所采用的编程方法或者说同步机制是不一样的。
常见的3种需要任务调度的时机有:
在确定选择哪种调度算法前,需要考虑任务是否可抢占的(preemptive)和任务的执行顺序和执行时间是否是可预测的(determinative)。
1、选择合适的调度算法,尽量消除任务间执行顺序的不确定性;
2、注意对临界段(Critical section)的保护;
不像Linux或Windows,区分了用户模式和内核模式,且不同每个应用程序有自己的地址空间,不会相互干扰。在RTOS中,所有程序共享地址空间,一不小心就可能会相互影响,再加上随机的中断影响,程序的执行顺序无法准确预测。为此,系统必须提供对共享变量或某程序段的保护机制。 其中, 禁止中断是最有效的保护机制之一,但频繁禁止中断也会导致系统实时性的降低。为此,系统又提供了互斥锁(Mutex)和信号量(Semaphore)等机制。但使用他们也要注意防止死锁(deadlock)和饥饿(starvation)的发生。通过模块化,降低系统的复杂度,减少变量共享的机会(临界段的数量)才是王道! 随着现在嵌入式的发展,Linux一般都用来作为功能复杂嵌入式系统的平台,那么如何将原有RTOS的程序移植到Linux中呢?一种方法是:让RTOS上的多个task,执行与linux的一个process(进程)中。使得该产品的核心功能可在linux上正常运行,然后如果想添加扩展功能,可在linux里新增process来实现。
source tree 用来规范整个系统源代码的结构,决定哪个代码文件放在哪个目录下。一般的原则是要使系统程序的目录架构便于在其它项目中使用,即满足可移植性。基本要做到区分硬件相关、硬件无关、产品相关、产品无关。下图是一个范例:
所谓的程序风格规范,不仅使命名规格,还规范了程序代码中至少应该具有哪些信息,以及程序编写的注意事项。
以下是一个范例:
登录后复制
// 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 <osos_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
好的头文件应该达到只需要看该模块的头文件就可以在它的程序中会使用该模块的要求。
c #ifndef XXX_OS_MSG_QUEUE_H #define XXX_OS_MSG_QUEUE_H //XXX_OS_MSG_QUEUE_H是一个在其它地方不会用到或定义 。。。实际内容 #endif
代码编写规范(coding style)
有很多静态测试工具可以帮助我们进行代码的review,例如C语言编译器就自带代码静态检测工具,对查出的问题会分别以“Warning”和“error”来指出。除此之外,在嵌入式领域,还有专门针对嵌入式C编程的代码静态测试规则,最流行的就是MISRA C,其定义了21类共141个规则,这些规则又分为强制性规则(Required)和推荐规则(Advisory)。很多嵌入进静态测试工具里会包含有该规则集,这些工具有PC-Lint、LDRA Testbed、LogiScope/Rule-Check等。
免责声明:本文系网络转载或改编,未找到原创作者,版权归原作者所有。如涉及版权,请联系删