用面向对象的思想编写实时嵌入式C程序

实时嵌入式系统的软件一般由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();
}

至此一个通信数据处理流程的框架基本上就搭建好了。

// 未完待续....

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值