实时嵌入式系统的软件一般由C语言编写,程序结构基本上都是这样的:
// 主程序
int main(void)
{
init(); // 初始化
while(1){
tick(); // 业务逻辑
}
return 0;
}
// 计时器
static unsigned int g_timer_tick_cnt = 0;
// 时钟中断回调
void isr_timer(){
g_timer_tick_cnt += 1;
}
// IO中断回调
void isr_can_io(){
// CAN总线通信
}
void isr_uart_io(){
// 串口通信
}
本文主要介绍如何使用面向对象的思想,用C构造一个模块化的程序。这个程序主要包括(1)基础模块的封装,包括通讯协议、传感器、步进电机等;(2)任务状态机的设计,并以配置数据驱动的方式实现业务流程。(3)可视化的流程配置工具,以及配套的调试平台搭建。最终,在嵌入式系统上运行的程序由C编写,开发者通过json定义具体的业务流程之后,由一个Python程序自动生成C语言的配置数据。
封装一重奏:基础组件封装
先从基础组件的封装开始,例如定时器。系统初始化的时候,需要设置时钟回调函数,以及触发该回调的时钟震荡次数,这个跟使用的硬件有关,在此假定设置的时钟回调函数是1ms执行一次。通常情况,需求是:有多个计时器同时运行,实现超时(timeout)计时和定时(interval)计时。于是我们定义出他们的类型和接口:
// 定义计时器类型:间隔、超时
enum TIMER_RECORD_TYPE {
TIMER_TYPE_INTERVAL,//间隔
TIMER_TYPE_TIMEOUT //超时
};
typedef unsigned long ulong_t;
typedef ulong_t time_ul_t;
typedef unsigned int uint_t;
// 启动计时器
void TimerStart(uint_t id);
// 停止计时器
void TimerStop(uint_t id);
// 是否已超时
int IsTimeOut(uint_t id);
/*
* Tap操作:适用TIMER_TYPE_INTERVAL类型
* 对指定(id)的间隔定时器,返回未处理的interval的个数,并标记为已处理
*/
int TimerTap(uint_t id);
// 单个计时器心跳
void TimerTick(uint_t id);
// 全部计时器心跳
void AllTimersTick(void);
因为C语言无法定义类和成员方法,于是我们用id作为对象的标识,并把ID作为接口方法的第一个参数,于是我们在程序里使用的时候就可以简单的这样写:
初始化的时候:
TimerStart(TIMER_LED_INTERVAL);
业务流程的循环里:
// 指示灯输出
if(TimerTap(TIMER_LED_INTERVAL)>0){
led_bling_bling();
}
关闭计时器
TimerStop(TIMER_LED_INTERVAL);
有了这些接口之后,后续就可以直接使用接口完成其它任务了。实现这些接口的时候,也只需要关注“计时器”本身,尽量让一个模块足够的聚焦其自身的功能定位。
extern uint_t G_TIMERS_NUMBER;
extern TIMER_CONFIG g_cfg_timer[];
extern TIMER_STATUS g_st_timer[];
time_ul_t GetCurrentSysTickCounter(){
return g_timer_tick_cnt ;
}
// 计时器状态
enum TIMER_RECORD_STAGE {
TIMER_READY,
TIMER_RECORDING,
TIMER_DONE
};
// 计时器配置:ID、类型、时长(ms)
typedef struct{
uint_t timerId;
uint_t type; // 间隔、超时
uint_t time_ms;
}TIMER_CONFIG;
// 计时器运行状态值
typedef struct{
time_ul_t tmStartTime; // 起始时刻
uint_t tmStage; // 运行阶段
uint_t intervalCnt; // 适用TIMER_TYPE_INTERVAL类型: 当前时刻距离起始时刻经过的间隔(interval)数量
uint_t tapCnt; // 适用TIMER_TYPE_INTERVAL类型: 记录最后一次Tap时的intervalCnt
}TIMER_STATUS;
// 根据计数器折算成毫秒(ms)时长
uint_t CalcTimeMS(time_ul_t start, time_ul_t end)
{
return end - start;
}
void TimerStart(uint_t id)
{
g_st_timer[id].tmStartTime = GetCurrentSysTickCounter();
g_st_timer[id].tmStage = TIMER_RECORDING;
g_st_timer[id].intervalCnt = 0;
g_st_timer[id].tapCnt = 0;
}
// 启动计时器: 指定超时毫秒数
void TimerStartCountDown(uint_t id,uint_t ms){
g_cfg_timer[id].time_ms = ms;
TimerStart(id);
}
uint_t TimerElapsedMs(uint_t id){
return CalcTimeMS(g_st_timer[id].tmStartTime, GetCurrentSysTickCounter());
}
// 计时器心跳处理
void TimerTick(uint_t id)
{
if (TIMER_RECORDING == g_st_timer[id].tmStage){
uint_t elapsed_ms = CalcTimeMS(g_st_timer[id].tmStartTime, GetCurrentSysTickCounter());
if (TIMER_TYPE_INTERVAL == g_cfg_timer[id].type){
g_st_timer[id].intervalCnt = elapsed_ms/g_cfg_timer[id].time_ms;
}
else{
if (elapsed_ms >= g_cfg_timer[id].time_ms){
g_st_timer[id].tmStage = TIMER_DONE;
}
}
}
}
void TimerStop(uint_t id)
{
g_st_timer[id].tmStartTime = 0;
g_st_timer[id].tmStage = TIMER_READY;
}
int IsTimeOut(uint_t id)
{
TimerTick(id);
if (TIMER_DONE == g_st_timer[id].tmStage){
return 1;
}
return 0;
}
int TimerTap(uint_t id)
{
TimerTick(id);
if (TIMER_RECORDING == g_st_timer[id].tmStage && TIMER_TYPE_INTERVAL == g_cfg_timer[id].type){
uint_t elapsed = g_st_timer[id].intervalCnt - g_st_timer[id].tapCnt;
if (elapsed > 0){
g_st_timer[id].tapCnt = g_st_timer[id].intervalCnt;
return elapsed;
}
}
return 0;
}
上面定义好了接口和实现,接下来就可以将系统里需要实现的计时器全部配置出来了,就好像把所有的计时器“对象”全部定义出来:
enum TIMER_IDS {
TIMER_LED_INTERVAL,
// ...其它计时器ID
TIMERS_NUMBER
};
const uint_t G_TIMERS_NUMBER = TIMERS_NUMBER;
TIMER_CONFIG g_cfg_timer[TIMERS_NUMBER] = {
{TIMER_LED_INTERVAL, TIMER_TYPE_INTERVAL, 500}, // LED
// ...
};
TIMER_STATUS g_st_timer[TIMERS_NUMBER] = {0};
void AllTimersTick(void)
{
uint_t i = 0;
for (i = 0; i < G_TIMERS_NUMBER; i++)
{
TimerTick(i);
}
}
基于同样的思路,可以把项目里其它要用到的步进电机、传感器都封装出来,并把对象都定义出来。一般来说,对于实时系统里封装的接口主要是四种:
XXX_Start(id, params);
XXX_Stop(id);
XXX_GetCurrentStatus(id);
XXX_Tick(id);
通过Enum把所有的对象ID都定义出来(就像上面的计时器ID)之后,就可以在合适的地方使用XXX_Start()来启动一个XXX(比如计时器、步进电机),把XXX_Tick()放到while循环里,在其它需要的地方,使用XXX_GetCurrentStatus()获取对象的执行状态(比如电机的执行步数已完成),最后在其它合适的地方调用XXX_Stop()来停止。
封装二重奏:通讯模块
通讯模块的封装其实也是遵循类似的原则,只不过需要将几种组件组合起来才行,主要包括:缓冲区、协议解析。缓冲区分为CAN总线收发、串口收发等不同IO通道的缓冲区,其封装思路也是跟前面的组件封装思路一致:用不同的ID作为不同对象的标识,通过统一的接口对缓冲区进行操作。
// 定义每个缓存的最大长度(1<<8 = 256)
#define BUFFER_MAX_SZ (1<<8)
#define BUFFER_SZ_MASK (BUFFER_MAX_SZ-1)
#define BUFFER_FULL_SZ BUFFER_SZ_MASK
// 缓存对象定义
typedef struct {
unsigned char ucBuff[BUFFER_MAX_SZ];
uint_t ulHeadIdx;
uint_t ulTailIdx;
} BUFF_STATUS;
// 缓存对象初始化
uint_t BufferInit(uint_t id);
uint_t BuffersAllInit(void);
// 判断缓存对象是否已满
uint_t BufferIsFull(uint_t id);
// 判断缓存对象是否为空
uint_t BufferIsEmpty(uint_t id);
// 获取缓存对象剩余数量
uint_t BufferGetLeftSize(uint_t id);
// 获取缓存对象当前可用数量
uint_t BufferGetCount(uint_t id);
// 往指定缓存追加数据
uint_t BufferPush(uint_t id, uchar_t* pInData, uint_t uInDataLen);
// 从指定缓存提取数据
uint_t BufferPop(uint_t id, uchar_t* pOutData, uint_t uOutDataLen);
// 获取头部指针,返回内容长度
uint_t BufferGetHeadPoint(uint_t id, uchar_t** pOutData);
// 获取指定位置的字节内容
uchar_t BufferGetChar(uint_t id, uint_t posIdx);
协议解析则主要是定义每个通信命令的ID、参数长度、回调函数等信息:
// 定义指令处理函数指针类型
typedef int (*CMD_PROC_FUNC)(uchar_t* pPacket);
typedef struct {
uint_t cmdId; // 命令序号
ushort_t cmdCode; // 命令编码
uint_t isFixedLen; // 是否固定长度
uint_t paramsLenth; // 命令参数的长度
CMD_PROC_FUNC procFunc; // 命令处理函数
} CMD_CFG;
enum CMD_IDS{
CMD_0,
CMD_1,
CMD_2,
CMD_NUMBER
};
// 注意:命令配置顺序必须与CMD_IDS一致
CMD_CFG g_cfg_cmd[CMD_NUMBER] = {
// cmdId, cmdCode, isFixedLen, paramsLenth, procFunc
// 序号 编码 是否固定长度 命令参数的长度 命令处理函数
{CMD_0, 0x0001, 1, 0, CmdProcFun0},
{CMD_1, 0x0002, 1, 0, CmdProcFun1},
{CMD_2, 0x0003, 1, 0, CmdProcFun2},
};
接下来,就是(1)从IO通信的接受数据回调函数里网缓冲区推送(Push)数据,(2)从缓冲区里识别报文数据、丢弃异常数据、调用报文命令回调函数,(3)构造通信报文并发送出去,并且发送的过程也可以是借用缓冲区完成数据发送:构造报文、将报文推送至发送缓冲区(在接口里使用发送缓冲区的ID)、从发送缓冲区里读取内容并通过硬件接口发送出去。在这个过程中,之所以使用缓冲区的原因,主要是为了在数据收发和数据处理之间提供一层缓冲层,使得程序运行更具弹性,不会因为数据处理耽误了硬件响应,也不会因为硬件响应耽误了数据处理。不过这种”弹性“也是相对的,需要具体问题具体分析。
从CAN接收数据,在硬件回调函数中往缓冲区推送数据,其中,CAN_RECV_BUFFER就是一个缓冲区的ID。
void isr_can_recv(ushort_t MsgNum)
{
while(MsgNum--){
can_message_receive(CAN0, CAN_FIFO0, &receive_message);
if((CAN_FF_EXTENDED == receive_message.rx_ff) ){
CommBufferPush(CAN_RECV_BUFFER, receive_message.rx_data, receive_message.rx_dlen);
}
}
}
从缓冲区识别报文
// 判断Buffer中是否有完整有效的包,如果是返回其长度,否则返回0, skipsz返回无效的数据的数量
uint_t IsValidPacketFromBuffer(uint_t buffId, uint_t *skipsz){
uint_t i = 0;
uint_t sz = BufferGetCount(buffId);
uchar_t dataLen = 0;
// uchar_t checkSumInBuff = 0;
// 检查缓冲区长度
if(sz<9){
return 0;
}
// 检查包头
for (i = 0; i < sz && PACKET_HEADER != BufferGetChar(buffId, i); ++i);
if (i > 0) {
*skipsz = i;
return 0;
}
// 判断数据长度字段是否与命令的定义匹配
dataLen = BufferGetChar(buffId, 2);
if( dataLen > (MAX_FRAME_LEN-5) ){
*skipsz = 2;
return 0;
}
// 判断数据是否完整
if( sz < dataLen+5 ){
return 0;
}
if(PACKET_TAILER != BufferGetChar(buffId, dataLen+4)){
*skipsz = dataLen + 4;
return 0;
}
// 返回有效长度
return dataLen+5;
}
/*
* 从Buffer中识别并填充一个包,返回实际填充的字节数
* 注意1:如果识别失败,会清除Buffer中的异常数据,并返回0
* 注意2: 如果识别成功,仅返回第一包有效数据
*/
uint_t FillPacketFromBuffer(uchar_t* pPackBuff, uint_t buffSz, uint_t buffId){
do{
uint_t skip_sz = 0;
uint_t valid_sz = IsValidPacketFromBuffer(buffId, &skip_sz);
if (0 == valid_sz){
if (skip_sz > 0){
BufferPop(buffId, 0, skip_sz);
continue;
}
else{
break;
}
}
else{
BufferPop(buffId, pPackBuff, valid_sz);
return valid_sz;
}
}while(!BufferIsEmpty(buffId));
return 0;
}
接下来就是报文处理的循环处理了
void PackProcess(void)
{
uchar_t frameBuf[MAX_FRAME_LEN] = {0};
uint_t frameLen = FillPacketFromBuffer(frameBuf, MAX_FRAME_LEN, CAN_RECV_BUFFER);
if (0 == frameLen)
{
return;
}
PacketProcess(frameBuf);
}
while (1){
PackProcess();
}
至此一个通信数据处理流程的框架基本上就搭建好了。
// 未完待续....