MoTeC CAN 总线协议解析

目前,越来越多的FSAE车队采用MoTeC的M84、M800等产品作为赛车的发动机ECU。MoTeC的ECU与其他同类产品相比拥有很多优势,但是与其兼容的原厂仪表板(dashboard)的价格让很多车队望而却步。虽然有一些第三方厂商推出了兼容MoTeC ECU的仪表板和数据记录仪(data logger),但是价格也并不便宜。其实我们通过一段非常简单的代码就可以在单片机上完成对MoTeC CAN总线数据流的解析,之后便可以在此基础上自行开发各类兼容MoTeC ECU的仪表板和数据记录仪。

由于笔者只能接触到MoTeC M84这一款ECU,故本文的全部内容都是基于MoTeC M84的CAN总线报文来完成的。实际上MoTeC各款ECU的CAN报文之间几乎没有差异,因此在针对其他MoTeC ECU进行开发时也可以参照本文来进行。

MoTeC M84一共可以输出4种CAN报文,这4种报文的格式各不相同,分别针对Dash/Logger、VCS/PDM、SLM、PLM。其中,针对Dash/Logger发送的数据最为全面,根据名字我们也可以猜出这一组数据流是针对仪表板和数据记录仪来设计的,所以对这组报文的解析工作也是我们自行开发仪表板和数据记录仪的基础。在MoTeC M84 ECU Manager里,这一组报文的默认ID为232(十进制),ID可以在ECU Manager里打开“Adjust->General Setup->Communications->CAN Setup”自行修改,设置窗口如下图所示,注意这里的ID设置是十进制的。
motec_m84_can_setup

通信参数

在MoTeC的相关技术文档里,并没有对这组数据流的一些基本参数(如波特率)进行说明,经过笔者的实际测试,以下参数可以保证正常与其通信。

波特率:1Mbaud
TIME_QUANTAS:8(5+2+1)
TIME_SEG1:5
TIME_SEG2:2
SYNCH_SEG:2

报文格式

MoTeC CAN报文的格式在MoTeC提供的一份文档里有详细的描述,文档的下载链接见下方,下面的内容需要搭配这份文档来阅读。

psau0015_motec_m800_set_3_data_protocol

MoTeC M84上电后会在CAN网络上持续不断的发送信息,每秒发送50组报文,通过上面的文档我们可以得知,每组报文的长度为176个字节。由于CAN总线自身的限制,MoTeC M84每次只能发送8个字节(即一帧,因为只有一个ID),所以MoTeC采用了一种简单的多帧发送机制来解决这个问题。

根据上文可知,每组报文的长度为22帧(8 x 22 = 176)。MoTeC M84会首先发送一个起始帧,这个起始帧的前三个字节为起始帧标记(三个常数:0x82、0x81、0x80),起始帧发送完毕后会依次发送后续的21帧,但是在每一帧中没有帧编号,结尾帧也没有结尾帧标记。通过CAN King实际采集的数据见下图,绿箭头处为起始帧,数据均用十进制表示,所以0x82、0x81、0x80分别为130、129、128。

data_log

MoTeC采用的这种多帧机制很明显不够可靠,和SAE J1939的多帧机制相比非常不完善,没有应对丢帧的措施。但是该机制非常简单,大大降低了实现报文解析的门槛。

报文解析

MoTeC的报文内容看起来较多,但是由于结构简单,解析起来非常容易。下面将简单描述如何在NXP MC9S12XS128单片机上用C语言来实现对报文的解析。解析的基本思路就是利用C语言中的共用体(union)共享内存的特点,让CAN帧缓存和定义好的MoTeC报文结构共用一块内存,这样就可以实现以帧的方式将数据写入内存,之后再以数据的方式读出。

具体的实现代码如下:

首先定义MoTeC CAN报文结构

struct Motec_Data {
  UINT8 Header1;
  UINT8 Header2;
  UINT8 Header3;
  UINT8 Data_Length;
  UINT16 RPM;
  UINT16 Throttle_Position;
  UINT16 Mainfold_Pressure;
  UINT16 Air_Temperature;
  UINT16 Engine_Temperature;
  UINT16 Lambda1;
  UINT16 Lambda2;
  UINT16 Exhaust_Mainfold_Pressure;
  UINT16 Mass_Air_Flow;
  UINT16 Fuel_Temperature;
  UINT16 Fuel_Pressure;
  UINT16 Oil_Temperature;
  UINT16 Oil_Pressure;
  UINT16 Gear_Voltage;
  UINT16 Knock_Voltage;
  UINT16 Gear_Shift_Force;
  UINT16 Exhaust_Gas_Temp_1;
  UINT16 Exhaust_Gas_Temp_2;
  UINT16 User_Channel_1;
  UINT16 User_Channel_2;
  UINT16 User_Channel_3;
  UINT16 User_Channel_4;
  UINT16 Battery_Volt;
  UINT16 ECU_Temperature;
  UINT16 Speed_1;
  UINT16 Speed_2;
  UINT16 Speed_3;
  UINT16 Speed_4;
  UINT16 Drive_Speed;
  UINT16 Ground_Speed;
  UINT16 Slip;
  UINT16 Aim_Slip;
  UINT16 Launch_RPM;
  UINT16 Lambda_1_Short_term_trim;
  UINT16 Lambda_2_Short_term_trim;
  UINT16 Lambda_1_Long_term_trim;
  UINT16 Lambda_2_Long_term_trim;
  UINT16 Aim_Lambda_1;
  UINT16 Aim_Lambda_2;
  UINT16 Fuel_Cut_Level;
  UINT16 Ignition_Cut_Level;
  UINT16 Ignition_Advance;
  UINT16 Load_Point;
  UINT16 Efficiency_Point;
  UINT16 Fuel_Used;
  UINT16 AUX_1;
  UINT16 AUX_2;
  UINT16 AUX_3;
  UINT16 AUX_4;
  UINT16 AUX_5;
  UINT16 AUX_6;
  UINT16 AUX_7;
  UINT16 AUX_8;
  UINT16 Fuel_Actual_Pulse_Width;
  UINT16 Fuel_Effective_Pulse_Width;
  UINT16 Fuel_Injector_Duty_Cycle;
  UINT16 Gear;
  UINT16 Sync_Position;
  UINT16 Fuel_Comp_1;
  UINT16 Fuel_Comp_2;
  UINT16 DEG_1;
  UINT16 DEG_2;
  UINT16 DEG_3;
  UINT16 DEG_4;
  UINT16 DEG_5;
  UINT16 DEG_6;
  UINT16 DEG_7;
  UINT16 DEG_8;
  UINT16 DEG_9;
  UINT16 DEG_10;
  UINT16 DEG_11;
  UINT16 DEG_12;
  UINT16 DEG_13;
  UINT16 DEG_14;
  UINT16 DEG_15;
  UINT16 DEG_16;
  UINT16 SFG_1;
  UINT16 SFG_2;
  UINT16 SFG_3;
  UINT16 SFG_4;
  UINT16 SFG_5;
  UINT16 SFG_6;
  UINT16 SFG_7;
  UINT16 SFG_8;
  UINT8 CRC_1;
  UINT8 CRC_2;
  UINT8 CRC_3;
  UINT8 CRC_4;
};

之后定义共用体,让CAN帧缓存和MoTeC报文结构体共用同一块内存。

union {
  struct Motec_Data M84_Data; //MoTeC报文结构体
  UINT8 Frames[22][8]; //CAN帧缓存,每帧8字节,共22帧
}ECU_Data;

CAN接收中断服务程序

void interrupt VectorNumber_Vcan0rx CANRx_ISR(void) {    
    UINT8 Msg_Length; //接收的数据长度
    UINT8 Index;      //接受数据的索引
    UINT8 RxData[8];  //缓冲区
    UINT8 static Counter = 0;  //帧计数器
    UINT8 i;

    Msg_Length = (CAN0RXDLR & 0x0F);  //获得接收到的帧长度
	
    //存储收到的帧
    for (Index=0; Index < Msg_Length; Index++) RxData[Index] = *(&CAN0RXDSR0 + Index); if(Counter >= 22) Counter = 0; //帧计数器到达22自动清零,避免越界
      
    //判断是否为报文头部,若是,则计数器清零
    if(RxData[0] == 0x82 && RxData[1] == 0x81 && RxData[2] == 0x80) Counter = 0;
	
    //将收到的帧存入帧缓冲区
    for(i=0;i<8;i++){
        ECU_Data.Frames[Counter][i] = RxData[i];
    }
	
    //计数器自增
    Counter++;
    
    CAN0RFLG_RXF = 1;   //清除中断标志           
}

读取解析后的数据时,只需通过访问共用体内结构体种相应的成员即可,例如下面这段代码就可以读取到ECU发出的发动机转速。

ECU_RPM = ECU_Data.M84_Data.RPM;

结语

通过采用本文提到的方法,就可以完成对MoTeC CAN总线报文的解析工作,并可以以此为基础开发各类兼容MoTeC ECU的装置。吉林大学吉速车队自2012年开始便通过本文所述的方法来解析MoTeC M84发出的数据,并以此为基础自行开发了仪表板、数据记录仪和遥测装置,这些装置也被车队成功地应用在了比赛当中。通过自行开发这些装置,一方面节省了车队的资金,另一方面也培养了队员对于嵌入式系统开发的兴趣,提升了他们在嵌入式系统开发方面的专业技术水平。

Formula SAE/Formula Student这项赛事创立的目的就是为了通过一个实际的工程项目来提升广大工科学生的专业能力,这是一个以教育为目的的赛事,而并非一项纯粹的竞争赛事。笔者希望通过将自己在参与赛事的过程当中积累的一些经验分享给大家,加速各车队的成长,从而让更多的同学关注并参与到这项赛事当中。

Copyright © 2015. All Rights Reserved.

《MoTeC CAN 总线协议解析》上有19条评论

  1. 您好,我感觉这篇文章后面用C语言解析的报文结构与 MoTeC M800 Set 1 Data Protocol.pdf 的描述不一样,这篇 pdf 中所述的报文结构有 144 字节,且无最前面3个标志字节,而后面的分析表明每一个完整的报文有176字节,这是否与前面的pdf矛盾。

          1. 好的谢谢。
            还有一个问题,文档里面没有提到报文最后的四个字节CRC的生成多项式。博主你在编程中有校验CRC值吗?

          2. 我没校验过这个CRC值,但是CRC32有标准的算法,我认为这里用的应该也是标准的算法,你可以试试。

  2. 请问这个ID在程序里面怎样具体的设置呢?这款单片机开发板附带的例程里面发送的ID是自己定义的,然而我把里面的ID改成00E8还是不能实现通信

    1. 你需要在MoTeC的ECU Manager里给你的ECU设置发送的ID,需要注意的是ECU Manager里设置的ID是十进制的。在你自己的程序里,你需要接收这个ID发送的数据,并按照本文给出的方法完成解析。

        1. 有CAN总线监测设备吗?有的话可以试着用设备观测一下是否有正常的报文收发。即使ID正确,导致不能收到报文的原因还是会有很多。

    1. 可以尝试模拟motec这个协议,然后看看CDL3会不会正确读取数据。不过我记得CDL3是支持自定义CAN报文解析的,这样的话,就不需要这么麻烦了,直接在CDL3里把你自己的CAN协议定义一下就好了。

  3. 楼主,我们利用的STM32F1采集的CAN,但是利用串口发送数据给PC,波特率必须在460800收到的报文才是正确的帧数。波特率小于460800会出现丢帧,大于460800会多帧,但是我们仪表用的串口屏最大波特率只支持115200。楼主能解释一下怎么解决这种问题吗?怎么能把波特率降下来呢?

    1. motec每秒会发送176×50=8800字节的数据,只要你的串口带宽大于这个数理论上来说就不应该发生丢帧现象。根据你的说法,不仅存在丢帧还存在多帧现象,所以我建议还是先检查你的代码。

  4. 您好CAN的这个SYNCH_SEG不是固定值1吗,而后面的计算是ss+ts1+ts2=(5+2+1)
    还是说是由CAN外设自己决定的,CAN学的不是太好

发表评论

邮箱地址不会被公开。 必填项已用*标注