CANoe编程实现FOTA车端的自动化测试(二)

这篇文章主要讲述在CANoe 11.0环境下,用CAPL编程实现仿真程序。

1. 创建仿真节点

建模之前,首先创建一个.DBC文件,也可以用一个已有的DBC文件修改。新建待仿真的空节点,如下图,只有节点名称无任何信号。然后加载到Setup。
在这里插入图片描述

2. 插入新节点

选择Insert Network Node, 然后右击新建的节点配置该节点属性。
在这里插入图片描述
选择DBC中创建的节点名:
在这里插入图片描述
设置节点属性为OSEK_TP节点(添加osek_tp.dll即可,在canoe安装目录下查找,例如 “C:\Program Files\Vector CANoe 11.0\Exec32”)
在这里插入图片描述
最后整个完整的模型:
在这里插入图片描述

3. 仿真程序实现

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

/*@!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;  //监控唤醒状态
  const int cycPepsTime = 100;    //100ms周期
}

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

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

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

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

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

on message NWM_message
{
    
    
  if(IsBUSActive == 0)
  {
    
    
    GW_message.PEPS_PowerMode = 0;
    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, 0);
  GW_message.PEPS_PowerModeValidity = 2;
  GW_message.PEPS_RemoteControlState = 0;
}
//初始化数据
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 = 0;
}

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

on preStart
{
    
    
  InitValue();
}

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

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

通用接口实现:

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

variables
{
    
    
  char gECU[10] = "%NODE_NAME%";  //此变量是获取当前通信节点的名称,此处与通信链路中的ECUName很自然的关联起来了
  enum AddressModes {
    
     kNormal = 0,
                      kExtendedBased = 1,
                      kNormalFixed = 2,
                      kMixed = 3,
  //......略去下面很多代码
}

报文解析函数的实现:

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

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

  //响应多帧请求
  if(remainderFrameCnt!=0)
  {
    
    
    if(remainderFrameCnt == consecutiveFrameCnt)
    {
    
    
      diagProcessMFRequest(reserveSid); //封装具体的应答逻辑,可以根据诊断协议获知
      IsResponse= 1;
      consecutiveFrameCnt = 0;
    }
  }
}

完成了车内ECU的仿真,启动CANoe后,仿真的ECU就可以验证TBOX的FOTA流程正确性,但是本方案只模拟了正向刷写的过程,实际刷写过程中,会有很多异常场景出现,完整的方案还需做更多的开发工作。

猜你喜欢

转载自blog.csdn.net/u014157109/article/details/120360152