本文介绍以英创公司嵌入式PC模块为平台,以事件驱动为特色的一种通用的嵌入式系统应用程序方案,该方案满足大多数中、低端嵌入式系统需求,可广泛应用于智能测控设备、POS终端产品、工业自动化、网络通讯管理等领域。采用英创嵌入式网络模块的客户,更是可以此为基础,直接进入应用功能的软件规划及实现,从而大大节省应用程序的开发时间,同时保证应用程序的高稳定性。本应用程序方案的核心是通过对一个简单的任务命令队列进行操作,来实现各个不同的应用程序功能。下图是本方案的典型流程框图。
1、系统流程概述
在上图中表示了3种不同的流程,它们是程序代码流程、任务命令(也称为事件)流程、以及数据的流程,以下对这三种流程做一简要介绍。
程序流程
应用程序启动后,首先进行必要的程序初始化配置,便进入系统核心代码,核心程序将依次读取系统任务队列中的事件代码,并根据代码内容转入相应的程序功能模块。不同的程序功能模块对应着不同的任务,即图中所标注的任务1、任务2、任务n等等,这些任务代码的特点之一是通过内部的状态机机制来避免程序阻塞,使得程序能快速返回系统任务调度单元,从而实现任务间的切换。
任务划分的原则一般是按照应用功能或层次来划分,如任务1对原始数据进行处理,任务2对处理的结果数据进行网络传送,任务3对数据进行文件备份。为了提高系统对事件的响应速度,每个任务不宜设计得过长,就大多数嵌入式系统应用来看,可以把任务的执行时间控制在100ms之内,对需要更长执行时间的功能,可以通过内部设置状态机的方式来化解。
命令流程
系统命令,通常也称为系统事件,可由系统中多个单元产生,这些单元可以是系统的定时中断程序,与应用相关的硬件中断程序以及各个任务功能程序模块,它们根据自身的运行状况,生成必要的事件并把这些事件推入系统任务队列。进入系统任务队列的事件是完全异步的,它们按照时间顺序排列,统一由系统核心代码读取,并启动相应的任务模块对该事件进行处理,这就是所谓的事件驱动机制。在程序设计中采用事件驱动的一个直接的好处是降低了各任务间的耦合性,提高了代码的可靠性及可维护性。
命令通常可定义成枚举变量,另可考虑命令参数段,可存放若干参数或字符串。系统任务队列是一个典型的FIFO数据结构,系统为中断程序和普通的任务模块提供了发送事件的API函数。定时任务发生器是一段加载到系统定时中断中的代码,在DOS系统中一般可提秒级以上的定时事件,更小时间间隔的事件,可通过系统的其他定时器中断实现,对于一般的嵌入式应用,最小定时事件不宜小于5ms,否则会无为增加CPU的开销,降低系统性能。在命令定义中,一般会定义IDLE或NOP命令,在IDLE任务中可以放常规的数据处理,也可以放检查是否有键盘、是否有网络数据来等等,并可形成必要的事件发送到系统任务队列,以启动相应的处理。
数据流程
各个任务模块的主要功能之一就是对各级应用数据进行必要的加工,并形成新的数据。典型的数据加工可以是:
对串口来的数据进行帧格式分析,提取相关数据,即通常的通讯规约分析;
对AD采集的原始数据进行某种统计处理,提取特征数据;
读取数字输入状态,进行必要处理;
读取网络报文,进行必要的应用层规约解析
应用数据存文件,文件数据处理等等
由于每个任务的执行机会具有一定的不确定性,因此需要对数据开设一定的缓冲区,对一般的应用来说,数据处理通常都是顺序进行的,所以数据缓冲区的结构通常采用FIFO数据结构,缓冲区的数据单元即可是简单的字节、字,也可以是复合的数据结构。在英创提供的程序中,串口的数据缓冲区就是采用的FIFO数据结构,数据单元为一个字节,FIFO结构的数据缓冲区也称为环型buffer。
可以由一个任务作数据处理,另一个任务作数据传送,对多任务共享的单一数据单元,可通过设置信号灯的方法来确保数据单元的完整性,对多个数据单元,同样可考虑采用FIFO数据结构。对数据响应时间有严格要求的应用,也可以用一个任务实现数据采集处理和网络通讯全过程。
以下具体介绍实现上述方案的主要代码。建议用户在阅读本文之前,已对英创嵌入式模块的功能测试程序有了基本了解。
2、主要程序代码分析
主控流程与应用任务
#include < stdio.h > // 包含所需的C运行库
#include < dos.h >
#include “etr_tcp.h” // 英创TCP/IP库
#include “cmdrive.h” // 事件驱动API定义
int SysInit( ); // 系统初始化函数定义
void SysExit( ); // 系统退出处理
int main( )
{
int i1, len, State, ExitFlag; // 局部变量
CMD CmdCode; // 系统命令枚举变量
char CmdPar[20]; // 系统命令所带参数
i1 = SysInit( ); // 首先进行初始化
for( ExitFlag=0; ; ) // 系统主循环
{
ReloadWDT( ); // 加载watchdog
State = NET_Running( ); // 网络链路管理
CmdCode = CmdQueue.GetCmd( CmdPar ); // 从系统任务队列读取命令
switch( CmdCode )
{
case NOP: // 进行常规处理,如检查键盘、网络、串口等
NetPackagePro( ); // 做必要的网络低层处理
// 若网络接收到数据,则启动相应任务进行处理
if( NetHasData( ) ) CmdQueue.PushCmd( TASK1 );
break;
case TASK1:
i1 = Task1.Do ( ); // 也可以是普通C函数
break;
case TASK2:
i1 = Task2. Do ( );
if( i1 ) CmdQueue.PushCmd( TASK2 ); // 发送命令,以继续任务处理
break;
case TASK3:
i1 = Task3.Do ( );
break;
default: ExitFlag =1; // 非法命令,退出
}
if( ExitFlag ) break;
}
SysExit( );
return 0;
}
系统初始化程序SysInit( ),首先是对系统提供的资源进行初始化,如网络初始化、串口初始化、LCD显示初始化等等,然后是对应用定义的功能对象进行初始化,最后是安装中断服务程序,启动定时任务发生器。相应地,SysExit( )函数则主要是卸载中断,释放在初始化中分配的动态buffer。
在主循环中的NOP处理,是以网络通讯为例,客户在实际应用程序设计中可以安排其他需要的处理,如处理键盘、处理串口数据等等。对应用级任务,建议采用C++的类来实现,每个类对象应至少有2个公共函数:Init( )和Do( )函数,主控程序可以通过Do( )函数的返回值来判断处理已完成或未完成,若未完成,可发命令再启动本函数进行后续处理,在上面的程序中任务TASK2的处理就是这样做的。用C++的类对象来实现应用功能,可通过私有变量来定义处理的状态,在进行交互式的通讯处理时,如操作串口设备,FTP文件上传等,特别有用,一旦需要处理程序等待对端响应,程序就返回系统控制进行其他处理,等下次再进入该任务模块时,程序可根据当前状态继续相应的处理,这就是所谓的状态机机制。下面是应用任务的类定义:
#define ST0 0
#define ST1 1
#define ST2 2
#define ST3 3
class AppTASK
{
int State; // 私有的状态变量
int DoST0( ); // 各个分步处理
int DoST1( );
int DoST2( );
int DoST3( );
public:
int Init( ); // 对包括State在内的变量进行初始化
int Do( ); // 任务处理函数
};
在类成员函数Do( )中实现具体的状态转移:
int AppTASK::Do( )
{
int i1;
i1 = 1; // 返回值 = 1:处理未完成;=0:处理完成
switch( State )
{
case ST0:
DoST0( );
State = ST1; // 前进到下一状态
break;
case ST1:
DoST1( );
State = ST2; // 前进到下一状态
break;
case ST2:
DoST2( );
State = ST3; // 前进到下一状态
break;
case ST3:
DoST3( );
State = ST0; // 返回初始状态
I1 = 0; // 处理完成!
break;
}
return i1;
}
整个程序方案中,核心的代码是实现系统的事件驱动功能,被定义成一个C++类如下:
#if !defined(_CMDRIVE_H)
#define _CMDRIVE_H
#ifdef __cplusplus
#define __CPPARGS ...
#else
#define __CPPARGS
#endif
#include < dos.h >
enum CMD { NOP, TASK1, TASK2, TASK3, EXIT }; // 可以根据应用定义更多的命令
#define MaxCmdStack 400 // 定义系统任务队列的长度
#define PARLEN 14 // 每个命令所带参数的长度
class TaskQueue
{
static unsigned int PutIdx; // 通过2个index的操作,使CmdBuf[ ]成为
static unsigned int GetIdx; // 逻辑上的环型buffer,即FIFO数据结构
static CMD CmdBuf[MaxCmdStack];
static char CmdPar[MaxCmdStack][PARLEN];
static struct time OldTime;
static struct date OldDate;
static unsigned int TickCount; // 定时计数
static unsigned int TickSize; // 确定最小的定时间隔,可变,初值为0
static void interrupt INT1C_Handler(__CPPARGS); // 通过INT 1C实现定时任务发生器
static int ISR_PushCmd( CMD NewCmd, char* pPar=NULL ); // 中断程序中使用
public:
TaskQueue( );
~TaskQueue( );
CMD GetCmd( char* pPar=NULL ); // 读取当前队列中的命令
int PushCmd( CMD NewCmd, char* pPar=NULL ); // 填入新的命令到系统任务队列
void StartQueue( ); // 启动定时任务发生器
void StopQueue( ); // 关闭定时任务发生器
};
extern class TaskQueue CmdQueue; // 在cmdrive.cpp中定义的类变量实例
#endif
在TaskQueue类的定义中有3个核心API函数,用于实现任务队列和定时任务发生:
CMD TaskQueue::GetCmd( char* pPar ) // 从FIFO读取命令
{
CMD CmdCode;
if( GetIdx != PutIdx )
{
disable( );
CmdCode = (CMD)CmdBuf[GetIdx];
if( pPar != NULL ) memcpy( pPar, CmdPar[GetIdx], PARLEN );
GetIdx = ( GetIdx + 1 ) % MaxCmdStack;
enable( );
return CmdCode;
}
return NOP;
}
// return = -1: command aborted
// = 0: command pushed
int TaskQueue::PushCmd( CMD NewCmd, char* pPar ) // 把命令填入任务队列
{
unsigned int Idx;
if( GetIdx == 0 ) Idx = MaxCmdStack - 1;
else Idx = GetIdx - 1;
disable( );
if( PutIdx == Idx ) return -1; // 表明队列已满
CmdBuf[PutIdx] = NewCmd; // 填入命令码
if( pPar == NULL ) memset( CmdPar[PutIdx], 0, PARLEN ); // 填入参数
else memcpy( CmdPar[PutIdx], pPar, PARLEN );
PutIdx = ( PutIdx + 1 ) % MaxCmdStack; // 序号按模加1
enable( );
return 0;
}
环形缓冲区的核心是使用了一块连续的内存,并定义了两个Index序号:一个是记录往缓冲区填数的PutIdx;一个是记录从缓冲区取数的GetIdx。置数和取数是两个完全异步的过程,所以PutIdx和GetIdx移动的瞬时速度不一定相同,但平均速度一致,当PutIdx==GetIdx表明缓冲区是空的,已经无数可取,而当PutIdx-GetIdx=1时,表明缓冲区已满,不允许再存数。
void interrupt TaskQueue::INT1C_Handler(__CPPARGS) // 定时任务发生器
{
int i1;
struct time t;
struct date d;
enable( );
TickCount++; // x86的系统时钟大约55ms中断一次
if( TickCount >= TickSize )
{
GetSystime( &t ); // get current time
if( t.ti_sec != OldTime.ti_sec ) // 作整秒检查
{
ISR_PushCmd( TASK1 ); // 每秒执行一次TASK1
TickSize = 18; // 整秒对齐
TickCount = 0;
OldTime.ti_sec = t.ti_sec;
if( t.ti_min != OldTime.ti_min ) // 作整分检查
{
ISR_PushCmd( TASK2 ); // 每分钟执行一次TASK2
OldTime.ti_min = t.ti_min; // update minute then
if( OldTime.ti_hour != t.ti_hour ) // processing hour data
{
ISR_PushCmd( TASK3 ); // 每小时执行一次TASK3
OldTime.ti_hour = t.ti_hour; // update hour then
}
}
}
}
}
按照上述代码实现的方法,用户很容易实现其他时间间隔的定时任务。
3、程序程序运行测试分析
建议每个任务的每次执行时间控制在100ms,以便系统合理的分配各任务的执行时间,节约系统的数据buffer开销。对大多数应用来说,这一要求很容易得到满足。本应用程序方案首先在NetBox-II(CPU主频24MHz)进行了测试,其任务调度的时间在90us水平,对100ms的任务间隔,系统占用时间小于1%,是完全可以接受的。
对于网络应用,由于存在与对端的交互式操作,所以其整个通讯过程会超过100ms,这时合理的安排是利用等待对端响应的时间来处理系统的其它任务,因此需要在相应的任务中采用状态机的方式来实现,具体的实现会在后续的应用程序方案中介绍。
成都英创信息技术有限公司 028-8618 0660