一种高效、安全的嵌入式通信协议

1、帧格式定义

        通信帧格式定义如下:

Magic

length

CRC16

command

count

data

1字节

2字节

2字节

2字节

1字节

N字节

        从magic到count总共8个字节,作为帧头;后面N字节的data作为command的数据内容。由于CAN协议一帧有效数据一般最多8字节,所以最好将帧头设定为8字节。

        说明:

        Magic:用于识别通信帧开始位置,可以固定为0xFA;

        Length:帧长,所有数据字节之和,等于帧头8字节 + 数据N字节;

        CRC16:使用CRC16校验算法,多项式、初始值根据公司情况自定义;最好一个公司或部门使用同一套多项式和初始值;

        Command:命令字,根据该命令字决定要执行动作;

        Count:命令计数,为每一个命令创建一个计数器,每执行一次计数一次;;

        Data:根据命令来确定具体字节大小。

2、命令类型/帧类型

        将command的某几个bit用于识别帧类型,目的是便于解析通信证,以下为一个可用的例子,将高4位用于帧类型:

  1. 心跳帧:command字为0x0xxx,仅用于判断通信是否正常,接收方收到心跳帧后,必须立即回复已收到;不需要通信两端都发送心跳帧,只需要一端发送另一端回复即可;
  2. 请求帧:command字为0x1xxx,请求帧用于指使接收方执行某一具体动作;
  3. 事件帧:command字为0x2xxx,用于将事件通知给接收方;
  4. 应答帧:command字为0x8xxx,用于告知发送方:我已经收到请求帧/事件帧/心跳帧
  5. 结果帧:command字为0x9xxx,对于某些请求指令的执行可能需要很久,接收方收到该请求帧后立即回复应答帧,在执行完成后回复结果帧;
  6. 应答结果帧:command字为0xAxxx,对于某些请求指令的执行可能立马就能执行完成,如获取版本号,可立即回复应答结果帧,表示:数据已收到,且执行完成。
  7. 数据帧:command字为0xBxxx,由于数据帧会持续不断的定时发送,所以接收方收到数据帧后不需要给出回复; 

3、命令计数

        这里使用count字节来对指令做执行计数;在发生方和接收方都需要为每个指令开辟一个字节空间用于记录指令执行次数;

        如果接收方接收到的count=0,说明发送方刚开机,这时不论接收方的本地count是多少,都必须执行,并刷新对应命令的count值;

        如果接收方收到的count≠0,且与本地count值相同,说明该指令已经执行过了,是重复指令,就不执行该指令;

        如果接收方收到的count≠0,且与本地count值不同,说明该指令还没执行过,需要立即启动执行流程。

        使用命令计数机制主要目的是防止重传机制导致的命令被多次重复执行。场景举例:

        如软件或硬件的重传机制导致电机运动10mm命令在某种意外情况发了两次,本来只想走10mm的,接收方收到两次该指令,结果走了20mm。如果使用命令计数机制,接收方每次收到该命令就将该字节记录下来,只有该字节跟本地记录的不一样才执行,这样就可避免这种意外。

        拓展:也有人会指出:这种为每一个命令都增加一个字节作为命令计数,会额外消耗较多内存,可以使用2~4个字节,用于帧计数:每发送一帧就累计加一一次。

        分析:如果使用2字节,最大计数65535,假设20ms通信一次,65535*20/1000=1310s,即1310s以后该计数值就会归零并重新计数;这就可能导致以下场景发生:某命令的执行可能需要1310s才能执行完成,在第1310s执行完的同时又收到了新的该指令,这时由于命令和计数值相同,导致命令计数功能认为这是一条重传指令,不予执行。

        有人会说:这里2字节既然这么快就重新计数了,可以使用3字节或4字节!没错,使用多个字节确实可以满足延长重新计数的时间,可是增加的字节会给通信带来额外的通信负担啊!

        我不建议这么玩不仅仅是因为增加了额外的通信负担:一方面,我们的通信命令一般也就在100种以内,能达到200种已经算是非常非常复杂的系统了;现在的单片机RAM基本都在8K以上,200个字节对于8K来说并不多;另一方面,我们并不是每一个指令都对重复执行敏感,大部分指令多执行几次基本无关紧要,这些指令完全可以不理会该计数字节。

4、通信故障

        通信故障是指:持续M1毫秒没有收到心跳或者持续M1毫秒没有收到心跳回复,这时候可以触发通信故障。

5、请求失败

        请求失败是指:请求帧发出去后M2毫秒内没有收到对应的响应帧,这时候可以触发请求失败。

6、通信框架

        为了使整个通信代码结构化以提高可理解性、可移植性,我们可以将通信架构分为四层:物理层、驱动层、协议层、应用层。

        物理层:指硬件部分,如CAN、USART等外设及其线路;

        驱动层:直接控制通信设备数据发送、接收的代码,这部分代码可以直接操作硬件,将这些代码做成接口供上层调用,如CAN驱动接口、USART驱动接口;

        协议层:负责发送数据的打包(使用发送协议)、接收数据的解包(使用接收协议);

        应用层:作为发送方需要负责发送数据、等待响应、解除等待响应;作为接收方需要根据解析到的指令做相关动作,如通知xx任务执行请求指令、通知发送任务应答请求指令等。

        作为发送方,应用层中数据发送后,如果需要应答,开始等待应答;可以开辟一个发送任务,只负责数据发送;等待结果帧、等待应答可以放发送任务中,也可以放在具体的业务流程中,对比分析如下:

        方案1:如果将等待应答放在发送任务中,发送任务就需要准备一张等待应答表,该表需要记录每条请求帧的发送开始时间、待收到的应答指令、发送数据的拷贝;收到应答后将该应答表中的对应记录删除即可;没有收到应答则需要判断是否需要超时重发。不好的地方主要在于使用应答表会消耗内存;如果应答表中的数据拷贝改成数据指针,其消耗空间就小很多;如果有N个任务,应答表的深度定义为N即可,这是基于一个习惯性规定:一个任务在没有收到回应前不允许再发数据(可以通过查询应答表来判断本任务是否可以发下一条数据)。

       方案2:如果将等待应答放在具体业务流程的任务(称为任务A)中,每个这样的任务都要自己判断是否收到应答、是否需要重发;这样发送任务免去了应答表的管理变得简单了,任务A自己当然可以直接知道能否发送下一条数据;这样不好的地方在于:每个这样的任务都要执行同样的动作:判断是否收到应答、是否需要重发,而这些事情与本任务的功能之间的内聚等级较低:为逻辑内聚,固不太合适。然而,如果是一个比较简单的程序,总代码量很少,业务也少且简单,将等待应答放在具体业务流程里面比较合适,放在发送任务里面反而将整个系统搞得很复杂。

        作为接收方,应用层中收到数据后,如果收到的是应答信息,需要通知发送流程/发送任务:用于解除该应答等待;如果收到的是请求帧、心跳帧、事件帧,立即通知发送任务:回复应答信号,如果能立即给出结果,同时回复应答信号和结果信号;如果是请求帧,且不能立即给出结果,还需要通知相关处理流程来执行该请求帧。

7、请求响应机制

        在请求帧发出后,发送方判断该次通信是否结束的依据是:

        1、确认接收方到收到了该请求帧;这时使用应答帧:请求帧的发送方收到对应的应答帧,即认为接收方收到了该请求帧;

        2、确认接收方执行了该请求帧;这时使用结果帧:请求帧的发送方收到对应的结果帧,即认为接收方执行该请求帧。

        这时不少人就会有疑问:接收方直接回复结果帧不就行了,请求帧发送方收到结果帧后,一定能确认接收方收到了该请求帧的!

        确实是这样的,所以发送方可以根据命令情况决定自己是等待应答帧还是等待结果帧,接收方则根据约定的同样的逻辑决定是否需要直接回复结果帧。

        有的人在这个地方脑筋转不过来,所以前面定义了:应答结果帧。该帧即表示接收方收到了请求帧,同时也表示接收方已经执行完了该请求帧。

        等明白了结果帧已经包含了应答帧这层意思之后,就会觉得应答结果帧其实是多余的。

8、重传机制

        对于数据帧、心跳帧,由于他们本身就是以固定时间间隔发送的,所以就不需要重传;当然,对于采样数据点数量有严格要求的场景,根据情况可能也需要重传。

        对于应答帧、结果帧、应答结果帧,他只是请求帧、心跳帧的一种响应机制,所以也不需要重传。

         对于请求帧,由于需要知道命令被执行的结果,所以必须要有超时重传机制。

        扩展:这里建议将重传动作放在各个应用层,而不是放在协议层/通信层;协议层/通信层只需要开放一个带重传机制的接口即可。考虑因素包括:

        一方面,从模块独立性来看,如果协议层只负责发送、接收,以及是否存在通信故障,这样通信模块的内聚程度高,与其它模块之间的耦合度也很低;

        另一方面,依然从模块独立性考虑,只有应用层才会关心命令是否执行成功,他们也知道该命令预计会执行多久,如果在各个应用中决定重传时间间隔、重传次数,在收到回复后可直接解除/退出重传。

        重传机制例程如下:

bool SendWithRepeat(uint8_t *p_src, uint8_t num, uint16_t reply_cmd, uint8_t reply_cnt, uint8_t repeat_num, uint32_t wait_time)
{
    AckMessage msg;

    for(uint8_t i = 0; i < repeat_num; i++)
    {
        SentData(p_src, num);
        
        if(TRUE == WaitNotify(&msg, wait_time)) // 等待收到响应消息
        {

            ……
            if((reply_cmd == msg.cmd) && (reply_cnt == msg.cnt)) // 命令和命令计数一致才算响应成功
            {
                return TRUE;
            }
            
        }
    }

    return FALSE;
}

        留个问题给大家思考:

        既然结果帧兼具应答帧的功能,取消应答帧会如何?

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值