这篇文章主要讲述在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流程正确性,但是本方案只模拟了正向刷写的过程,实际刷写过程中,会有很多异常场景出现,完整的方案还需做更多的开发工作。