原创内容,转载请注明出处

   接上篇,本文主要讲CAPL编程详细实现,软件环境CANoe  11.0

一、Simulation Setup

  1、建模之前,首先创建一个.DBC文件。如果不会,可以用一个已有的DBC文件修改。新建待仿真的空节点,如下图,只有节点名称无任何信号。然后加载到Setup

CAPL编程实现诊断刷写,车联网FOTA流程自动化测试(代码篇)-LMLPHP

  

  2、新插入节点,选择Insert Network Node, 然后右击新建的节点配置该节点属性。

CAPL编程实现诊断刷写,车联网FOTA流程自动化测试(代码篇)-LMLPHP

选择DBC中创建的节点名,此处很有用

CAPL编程实现诊断刷写,车联网FOTA流程自动化测试(代码篇)-LMLPHP

设置节点属性为OSEK_TP节点(添加osek_tp.dll即可,在canoe安装目录下查找,我的是 "C:\Program Files\Vector CANoe 11.0\Exec32")CAPL编程实现诊断刷写,车联网FOTA流程自动化测试(代码篇)-LMLPHP

我的整个模型建完如下图(ECU太多,未截图完整):CAPL编程实现诊断刷写,车联网FOTA流程自动化测试(代码篇)-LMLPHP

可能大家会有疑问,关于这个网络模型的合理性。

疑问1. 如此多的节点,运行负载如何,会不会不足以支撑,变得不够实时性?

答:我的硬件是CANoe89系列,是最强悍的一款。完全可以支撑这么多节点。 而且按CANoe官方介绍的说法,理论上这种模型可支持无限多个节点,只是会降低速率。当然canoe对PC的运存要求比较高,需一台强悍的电脑承载。

ISO11898标准规定标准的1M/s CAN网络的最大总线长度40m, 最多允许存在30个节点,各节点支路最长为0.3m,如果网络以较低的速度运行则可支持更多的节点,总线长度也可增加。

高速总线的标准最大速率500k/s, 而支持超过30个节点的低速总线的速率为125k/s或更低, 低速CAN网络普遍能支持50个或更多的节点。

疑问2.目前才20几个ECU,复杂度不算太高,当ECU数量更多时,是否会造成编码量过大,可维护性变得极差?

答:上一篇的介绍过系统框架和通信模型,此模型非常简便的支持节点热增减,各ECU之间的耦合度降到最低,互不牵连。设计时抽取了通用接口,即使是二次开发也是非常简单的。

二、代码实现

  此处选择GW节点作为样例讲解。其中涉及的环境变量和系统变量在代码中出现时再做说明

1、ECU应用层行为仿真
 /*@!Encoding:936*/
 includes
 {
   #include "GenericNode.cin"     //此处是一个造好的轮子,可见canoe提供的\OSEK_TP_MultiChannel  Demo
 }

 variables
 {
   msTimer PhysRespTimer;  //物理寻址应答定时器
   msTimer FuncRespTimer;  //功能寻址应答定时器
   msTimer GWMessageTimer;    //ECU外发消息定时器,周期性的往总线发报文
   message 0x111 GW_message;    //此处是随便举例的报文,假设GW的tx报文就是id=0x111
   message 0x222 NWM_message;  //监控唤醒状态
   ;    //100ms周期
 }

 //每100ms发送一帧gw报文到总线,ecu信号仿真
 on timer GWMessageTimer
 {
   output(GW_message);
   setTimer(GWMessageTimer, cycPepsTime);
 }

 //模拟按键弹起,物理寻址
 on timer PhysRespTimer
 {
     //注意此处的系统变量格式, ECUName::链路名::变量名, 本篇章节一介绍的在setup处建立节点时,要求配置选择数据库的节点名将在此处生效
   @sysvar::GW::Conn1::sysSendData = ;
 }

 //模拟按键弹起,功能寻址
 on timer FuncRespTimer
 {
   @sysvar::GW::Conn2::sysSendData = ;  //注意此处链路名与上一函数不一样,区分物理寻址和功能寻址主要体现在这里
 }
 //监控一个环境变量,整车电源模式。 备注:环境变量可在DBC中创建
 on envVar PEPS_PwrMode
 {
   varPowerMode = getValue(PEPS_PwrMode); //先略过此变量的定义位置,全局变量记录电源状态
   GW_message.PEPS_PowerMode = varPowerMode;
   )
   {
     BCM_ATWS = ;  //车身安全锁报警状态变量,略过定义处
   }
   )//休眠
   {
     InactiveGW();
   }
   else
   {
     ActiveGW();
   }
 }

 //模拟按键按下,物理寻址
 void diagPhysRespMessage()
 {
   if(IsResponse){
   @sysvar::GW::Conn1::sysSendData = ;
   setTimer(PhysRespTimer, N_As);
   }
 }

 //模拟按键按下,功能寻址
 void diagFuncRespMessage()
 {
   if(IsResponse){
   @sysvar::GW::Conn2::sysSendData = ;
   setTimer(FuncRespTimer, N_As);
   }
 }

 on message NWM_message
 {
   )
   {
     GW_message.PEPS_PowerMode = ;
     ActiveGW();  //设备被唤醒,升级定时器触发后 激活信号
   }
 }

 //处理来自诊断仪的物理寻址访问GW请求
 on message 0x701   //此处是捏造的物理寻址诊断ID,根据产品实际的来变更
 {
   diagReqMsg=this;
   writeDbgLevel(level_1, "---physical diagnostic request, id = 0x%x", diagReqMsg.id);
   SetValue(); //获取当前应回复值
   diagParseReqMessage();      //解析请求内容
   diagPhysRespMessage();    //应答请求

 }

 //处理来自诊断仪的功能寻址访问GW请求
 on message 0x7EE    //此处是捏造的功能寻址诊断ID,根据产品实际的来变更
 {
   diagReqMsg=this;
   writeDbgLevel(level_1, "---functional diagnostic request, id = 0x%x", diagReqMsg.id);
   diagParseReqMessage();
   diagFuncRespMessage();
 }

 //初始化仿真的通信信号值
 void InitGWValue()
 {
   putValue(PEPS_PwrMode, );
   GW_message.PEPS_PowerModeValidity = ;
   GW_message.PEPS_RemoteControlState = ;
 }
 //初始化数据
 void InitValue()
 {
     //以下是从配置文件读取 GW接到诊断请求时的应答的数据
   getProfileString("GW", gEntry_1, gDefautStr, cOEMInfo, gLenEntry_1, gFileName);
   putValue(GWOEMNumber, cOEMInfo); //EPS OEM  NO.
 }

 //获取ECU的回复参数
 void SetValue()
 {
   getValue(GWOEMNumber, cOEMInfo);
 }

 on start
 {
   InitGWValue();
   ActiveGW();
 }

 //停止仿真通信报文
 void InactiveGW()
 {
   cancelTimer(GWMessageTimer);
   IsBUSActive = ;
 }

 //仿真通信报文
 void ActiveGW()
 {
   setTimer(GWMessageTimer, cycPepsTime);
   IsBUSActive = ;
 }

 on preStart
 {
   InitValue();
 }

 //获取实时更新的OEM版本号
 on envVar GWOEMNumber
 {
   ];
   getValue(GWOEMNumber, cOEMInfo);
   snprintf(dest, elcount(dest), "\"%s\"", cOEMInfo);
   writeProfileString("GW", gEntry_1, dest, gFileName);
 }

 //数据对外发送的统一变量,所有ECU发送数据时通过它外传
 on envVar varDataToTransmit
 {
   getValue(varDataToTransmit, cEnvVarBuffer);
 }

以上代码,实现了ECU的通信信号仿真,不同的ECU之间的差异在于信号数量不一样、物理请求与功能请求的应答的链路的ECUName不一致, 诊断ID不一致。其余逻辑上完全一致。所以说二次开发很简单,只需要复制代码后  修改此三处即可完成新节点的增加

2.通用接口实现
 includes
 {
   #include "GenericConn1.cin"
   #include "GenericConn2.cin"  //造好的轮子  建立链路,分别实现物理寻址与功能寻址
   #include "Common.cin"   //通用接口封装在此处
 }

 variables
 {
   ] = "%NODE_NAME%";  //此变量是获取当前通信节点的名称,此处与通信链路中的ECUName很自然的关联起来了
   ,
                       kExtendedBased = ,
                       kNormalFixed = ,
                       kMixed = ,
   //......略去下面很多代码
 }
diagParseReqMessage()实现,解析总线上的诊断请求报文
 /***********************************************************
 * description  : 解析收到的报文
 * creation date: 2018/11/13
 * author       : XXX
 * revision date:
 * revision log :
 * modifier     :
 ***********************************************************/
 void diagParseReqMessage()
 {
   byte fBValue;
   byte hNibble; //高四位
   byte lNibble; //低四位
   byte sid = 0x0;
   byte reserveSid = 0x0; //针对多帧请求的服务有效,特别预留

   int remainderBLen;     //剩余未传输字节
   ;
   ;
   //获取首字节信息
   fBValue = diagReqMsg.);
   writeDbgLevel(level_1, "---The First Byte: 0x%02x", fBValue);
   hNibble = (fBValue>>) & 0xf;
   lNibble = fBValue & 0xf;
   //writeDbgLevel(level_1, "high 4 bits=%d, low 4 bits=%d", hNibble, lNibble);
   IsResponse= ; //初始化时默认不发送应答,需要发送应答时置位1
   //解析高字节信息
   if(0x0 == hNibble) //单帧
   {
     SF_DL = lNibble;
     sid = diagReqMsg.);
     writeDbgLevel(level_1, "SF: SF_DL=%d, sid=0x%x", SF_DL, sid);
     if(0x2e==sid){//写入服务
       subServiceId = ((diagReqMsg.)<<)&);
       writeDbgLevel(level_1, "---SF:sid=0x%02x, ssid=0x%x---", sid, subServiceId);
     }
     else if(0x31==sid) //擦写 05 71 01 FF 01 04 AA AA
     {
       checkSum = (diagReqMsg.)<<) | (diagReqMsg.)<<)
       |(diagReqMsg.)<<) | diagReqMsg.);
        writeDbgLevel(level_1, "---SF:crc or flush, 0x%x---", checkSum);
     }
     diagProcessSFRequest(sid); //根据实际服务回复应答内容
   }
   else if(0x1 == hNibble) //多帧首帧
   {
     FF_DL = ((lNibble<<)&);
     reserveSid = diagReqMsg.);
     remainderFrameCnt = ; //回复0值
     consecutiveFrameCnt = ;  //置0连续帧
     remainderBLen = (FF_DL - );
     writeDbgLevel(level_1, "---MF:sid=0x%02x", reserveSid);
     if(reserveSid==0x2e){
       subServiceId = ((diagReqMsg.)<<)&);
       writeDbgLevel(level_1, "---MF:ssid=0x%x---", subServiceId);
     }
     else if(reserveSid==0x36) //经验, 将数据放置在左边,可避免少写=的异常
     {
       transferDataSN = diagReqMsg.);
       writeDbgLevel(level_1, "---MF:data sn=0x%x---", transferDataSN);
     }
     else if(reserveSid==0x31) //校验
     {
       checkSum = (diagReqMsg.)<<) | (diagReqMsg.)<<)
       |(diagReqMsg.)<<) | diagReqMsg.);
       writeDbgLevel(level_1, "---MF:crc or flush, 0x%x---", checkSum);
       IsCRCDone = ; //已校验过 刷写完成
     }

      == )
     {
       remainderFrameCnt = remainderBLen/;
     }
     else
     {
       remainderFrameCnt = remainderBLen/ + ;
     }
     writeDbgLevel(level_1, "MF: FF_DL=%d,remainder frame count=%d", FF_DL, remainderFrameCnt);
   }
   else if(0x2 == hNibble) //连续帧
   {
     SN = lNibble;
     consecutiveFrameCnt += ;
     writeDbgLevel(level_1, "CF: SN=%x, current count=%d", SN, consecutiveFrameCnt);
     sid = 0x0;
   }
   else if(0x3 == hNibble) //流控帧
   {
     FS = lNibble;
     BS = diagReqMsg.);
     STmin = diagReqMsg.);
     writeDbgLevel(level_1, "FC: FS=%d, BS=%d, ST min=%d", FS, BS, STmin);
     sid = 0x0;
   }
   else
   {
     writeDbgLevel(level_1, "error frame");
   }

   //响应多帧请求
   )
   {
     if(remainderFrameCnt == consecutiveFrameCnt)
     {
       diagProcessMFRequest(reserveSid); //封装具体的应答逻辑,可以根据诊断协议获知
       IsResponse= ;
       consecutiveFrameCnt = ;
     }
   }
 }
  以上就完成了车内ECU的仿真,启动CANoe后,仿真的ECU就可以验证TBOX的FOTA流程正确性啦。

本方案只算个半成品,只模拟了正向刷写的过程,实际刷写过程中,会有很多异常场景出现。所以需要根据产品的OTA规范来封装重试机制,否定应答处理机制等。

还可以配合开发控制面板或模拟器,同步车身的状态,控制车内信号的变化等。



05-11 16:09