前言:
可能有兄弟胸中有点疑问, Vector ETAS等软件包都有XCP,你移植个吊,但是我的MICORSAR BSW中没发现XCP静态代码,倒是在CFG中发现了XCP配置选项,难道这部分代码全是动态生成?
好吧,不论如何现在也不知道怎么在工具里配置XCP,先移植Vector Xcp Basic, 顺便读下代码, 加深一下理解。这个软件包大家可以在网上找找,或者Vector官网找找, 免费的,可以满足基本的标定/测量需求。
本篇文章的主要目的是从配置和代码角度学习一下XCP协议,至于能否直接用于项目请君自行斟酌。
首先说明我们配置的是Xcp on CAN, 所以必须你的MCU要能收发标准CAN消息,当然只要你搞清楚了移植步骤,之后迁移到Xcp on CAN FD 或者 on Tcp/ip都是可以的。
Mcu 的CanDrv 至少要提供像CanTransmit(Canid, DLC, void * data)这样的函数, CAN接收到的MSG 需要传入XcpCommand((vuint32*)&XcpRequest[0])进行处理;所以即使你是STM32+FreeRtos这样的组合也是可以移植Xcp协议的。
- 环境 硬件->NXP S32K148EVB 软件->Vector Davinci套件
- 工具配置
所以我们先新建立一个工程, 命名为XcpDemo, 我们把工程中BSW部分,MCAL部分都已经配置好了,我们这里只讨论如何添加Xcp这一个模块。
我们首先要改下DBC文件,这样导入CFG中就能生成基本的通信配置
BU_: INCA MyECU
BO_ 1920 XcpResponse: 8 MyECU
SG_ XcpRes : 0|64@1+ (1,0) [0|1.84467440737096E+019] "" INCA
BO_ 1888 XcpRequest: 8 INCA
SG_ XcpReq : 0|64@1+ (1,0) [0|1.84467440737096E+019] "" MyECU
通过CANdb++添加如下两个Nodes, Msgs, Sigs CANID Master -> Slave 0x760
Slave->Master 0x780 两帧MSG各包含一个Signal SG_XcpRes和SG_XcpReq, 都占8Bytes。
打开Developer
新建如图所示两个SWCs 其中TestSWC是用来对标定和测量变量做测试的,内容如下。
#include "Dio.h"
#pragma default_variable_attributes = @ ".caliConst_Ram"
static uint16 cali_test1 = 3000;
#pragma default_variable_attributes =
#pragma default_variable_attributes = @ ".monitorRam"
static uint8 moni_u8 = 3;
static uint16 moni_u16 = 0;
static float32 moni_float = 1.2F;
static uint32 moni_u32 = 0;
#pragma default_variable_attributes =
FUNC(void, TestSWC_CODE) Runnable_Test(void) /* PRQA S 0850 */ /* MD_MSR_19.8 */
{
/**********************************************************************************************************************
* DO NOT CHANGE THIS COMMENT! << Start of runnable implementation >> DO NOT CHANGE THIS COMMENT!
* Symbol: Runnable_Test
*********************************************************************************************************************/
if(cali_test1 > 3000){
LedOn();
}else{
LedOff();
}
if(moni_u8 >= 255){
moni_u8 = 0;
}
if(moni_u16 >= 255){
moni_u16 = 0;
}
if(moni_u32 > 1000){
moni_u32 = 0;
}
if(moni_float > 100.0F){
moni_float = 0.0F;
}
moni_u8++;
moni_u16++;
moni_u32++;
moni_float = moni_float + 1.0F;
/**********************************************************************************************************************
* DO NOT CHANGE THIS COMMENT! << End of runnable implementation >> DO NOT CHANGE THIS COMMENT!
*********************************************************************************************************************/
}
可以看到我们定义了一个标定变量和四个观测变量(后面自己手动加上去的,如果用Simulink自动生成代码的话,可以将标定和测量变量都生成到同一个文件), 至于"#pragma default_variable_attributes = .monitorRam" 是IAR编译器特定的宏,同时需要修改链接文件.icf,定义flash和ram分区, 将标定和观测变量放到自己的分区中去,这里不展开讨论。
XcpIf这个SWC是Xcp接口
在XcpIf 中定义3个Runable 它们的Trigger 分别为
由于Xcp需要使用BSW中ComM的接口, 所以需要定义一个Service Port, 并引用默认存在的ComM接口(C/S类型)
同时定义两个引用S/RInterface 的端口(Pport 向外发送Xcp Response, Rp 接收Tester发送的Xcp Request)
将两个SWC拖入ECU_Compoment中, 将XcpIf 的两个端口右键
代表这两个端口不做内部连接, 而是需要和外部其他ECU进行Sender/Reciver交互的, Rte 会自动识别并将这个两个Port的Read/Write操作转化为Com_ReadSignal/Com_SendSignal, 当然既然是Signal还有做DataMapping
这是Mapping好的Port中的Data Element 和对应的CAN Signal
想要RTE生成正确的代码还需要再CFG中将SWC和内部BSW建立连接
才能生成如下代码
# define Rte_Read_Rp_XcpRequest_De_XcpRequest Rte_Read_XcpIf_Rp_XcpRequest_De_XcpRequest
# define Rte_Read_XcpIf_Rp_XcpRequest_De_XcpRequest(data) (Com_ReceiveSignal(ComConf_ComSignal_XcpReq_oXcpRequest_oCAN_42940af5_Rx, (data))) /* PRQA S 3453 */ /* MD_MSR_19.7 */
# define Rte_Call_ComM_UserRequest_RequestComMode(arg1) (ComM_RequestComMode((ComM_UserHandleType)0, arg1)) /* PRQA S 3453 */ /* MD_MSR_19.7 */
XcpDAQHandler为DAQ功能周期发送DTO,触发周期为1ms, 但是上位机可以通过Prescaler分别控制每个DAQList的上传周期。比如DAQList0->Prescaler = 10,那么XcpDAQHandler里会对DAQList0->Prescaler自减, 当其为0才真正通过CAN将DTO发送出去。
XcpRxHandler 触发为接收到ID 为0X760(Master->Slave)MSG
FUNC(void, RTE_CODE) Rte_COMCbk_XcpReq_oXcpRequest_oCAN_42940af5_Rx(void) /* PRQA S 0850 */ /* MD_MSR_19.8 */
{
if (Rte_InitState == RTE_STATE_INIT)
{
/* scheduled trigger for runnables: XcpRxHandler */
(void)SetEvent(OsTask_Xcp, Rte_Ev_Run_XcpIf_XcpRxHandler); /* PRQA S 3417 */ /* MD_Rte_Os */
}
} /* PRQA S 6010, 6050 */ /* MD_MSR_STPTH, MD_MSR_STCAL */
可以看到这个CallBack是RTE自动生成的,会在CAN控制器接收到0x760消息的回调函数里进行匹配和逐级调用
XcpIfInit是协议初始化
3个Runables 需要Map到Task中去才能运行起来
TASK(OsTask_Xcp) /* PRQA S 3408, 1503 */ /* MD_Rte_3408, MD_MSR_14.1 */
{
EventMaskType ev;
/* call runnable */
XcpIfInit();
for(;;)
{
(void)WaitEvent(Rte_Ev_Run_XcpIf_XcpDAQHandler | Rte_Ev_Run_XcpIf_XcpRxHandler); /* PRQA S 3417 */ /* MD_Rte_Os */
(void)GetEvent(OsTask_Xcp, &ev); /* PRQA S 3417 */ /* MD_Rte_Os */
(void)ClearEvent(ev & (Rte_Ev_Run_XcpIf_XcpDAQHandler | Rte_Ev_Run_XcpIf_XcpRxHandler)); /* PRQA S 3417 */ /* MD_Rte_Os */
if ((ev & Rte_Ev_Run_XcpIf_XcpDAQHandler) != (EventMaskType)0)
{
/* call runnable */
XcpDAQHandler();
}
if ((ev & Rte_Ev_Run_XcpIf_XcpRxHandler) != (EventMaskType)0)
{
/* call runnable */
XcpRxHandler();
}
}
} /* PRQA S 6010, 60
在生成的XcpIf模板中进行代码编写
FUNC(void, XcpIf_CODE) XcpDAQHandler(void) /* PRQA S 0850 */ /* MD_MSR_19.8 */
{
uint8 i = 0;
for (i = 0; i < xcp.Daq.DaqCount; i++)
{
vuint8 event = xcp.Daq.u.DaqList[i].eventChannel;
XcpEvent(event);
}
}
FUNC(void, XcpIf_CODE) XcpIfInit(void) /* PRQA S 0850 */ /* MD_MSR_19.8 */
{
Rte_Call_ComM_UserRequest_RequestComMode(COMM_FULL_COMMUNICATION);
XcpInit();
}
FUNC(void, XcpIf_CODE) XcpRxHandler(void) /* PRQA S 0850 */ /* MD_MSR_19.8 */
{
uint8 XcpRequest[8] = {0};
Rte_Read_Rp_XcpRequest_De_XcpRequest(XcpRequest); /*将CMD读到XcpRequest中*/
XcpCommand((vuint32*)&XcpRequest[0]);
}
对三个Runable进行代码添加
其中XcpCommand XcpInit xcp.Daq.u.DaqList 等都是在Vector 提供的XcpBaic 代码包中定义和实现的,这里只需正确使用。
#include "Appl_Cbk.h"
#include "CanIf.h"
#include "XcpBasic.h"
#include "Os.h"
这是XcpIf.c 自己添加的头文件
#define XCP_RESPONSE_CANIF_PDUID 0
static vuint8 xcpResourceMask = 0;
void Xcp_Cbk_XcpResponseTxSuccess(void)
{
XcpSendCallBack();
}
void ApplXcpSend( vuint8 len, BYTEPTR msg )
{
PduInfoType PduInfoPtr;
if(len > 8){
len = 8;
}
PduInfoPtr.SduLength = len;
PduInfoPtr.SduDataPtr = msg;
CanIf_Transmit(XCP_RESPONSE_CANIF_PDUID, &PduInfoPtr);
}
void ApplXcpSendFlush(void)
{
}
void ApplXcpInit()
{
}
vuint8 ApplXcpGetSeed( vuint8 resourceMask, vuint8 *seed )
{
/*RM_CAL_PAG|RM_DAQ|RM_PGM|RM_STIM 可分别返回不同SEED*/
xcpResourceMask = resourceMask;
seed[0] = 0;
seed[1] = 1;
seed[2] = 2;
seed[3] = 3;
seed[4] = 4;
seed[5] = 5;
return 6;
}
vuint8 ApplXcpUnlock( const vuint8 * key, vuint8 length )
{
/* Ckeck the key */
if((length == 2) && (key[0] == 0xBE) && (key[1] == 0xEF)){
xcp.ProtectionStatus &= ~xcpResourceMask; /* Reset the appropriate resource protection mask bit */
return 1;
}else{
return 0; /*Key not correct*/
}
}
MTABYTEPTR ApplXcpGetPointer( vuint8 addr_ext, vuint32 addr ) {
addr_ext = addr_ext;
// /*S32K为小端格式*/
// vuint32 new_addr = ( ((addr & 0XFF000000) >> 24) | ((addr & 0X00FF0000) >> 8) |
// ((addr & 0X0000FF00) << 8) | ((addr & 0X000000FF) << 24));
/*如果上位机默认按小端格式传输则下位机无需更改字节序*/
return (MTABYTEPTR)addr;
}
void ApplXcpInterruptDisable()
{
SuspendAllInterrupts();
}
void ApplXcpInterruptEnable( void )
{
ResumeAllInterrupts();
}
ApplXcpSend 这一类函数都是在XcpBasic.h中申明,但是没有具体实现,需要用户自己实现的函数,它们只在Xcp协议内部使用,大家可以参考代码包中的例程。
这里说下为啥ApplXcpSend 调用CanIf_Transmit 而不是之前生成的
Rte_Write_XcpIf_Pp_XcpResponse_De_XcpResponse(uint64 data);
因为这个函数实际会调用Com_SendSignal , 如果发送Response走Com->PduR->CanIf->CanDrv
的话Com中的Signal DLC是事先定好的,比如8Bytes那么每次都会发8Bytes,而Xcp Response 是需要根据不同响应动态改变DLC的,且Com发送PDU是有Periodic 和 Trigger两种模式,前一种模式是按照DBC中设定的周期周期发送,后一种无需计算周期直接发送,这里不如直接用CanIf层进行发送来的方便, 但是在Com中需配置
也就是SG_XcpRes 发送成功的Notification函数Xcp_Cbk_XcpResponseTxSuccess
在里面放上XcpBasic.c中实现的XcpSendCallBack才正确。
配置就说这么多,这样框架就搭好了,大家自行琢磨。
- 代码
我们可以看到这是Vector 提供的代码, 其主要逻辑就是在XcpBasic.c中实现,大家可以把它们集成到自己的工程中去,进行编译。
我们来理一下Xcp协议栈的运行步骤
Tester-> send msg 0x760 + 8bytes -> ECU CAN Transiver -> Ecu CAN Interrupt -> CanIf_RxIndication -> PduR_RxIndication -> Com_RxIndication -> 对Signal XcpRequest 进行解包并拷贝至信号的缓存区-> 调用Rte_COMCbk_XcpReq_oXcpRequest_oCAN_42940af5_Rx callback激活Task(Xcp_Task) -> 调用Runable XcpRxHandler() -> Rte_Read_Rp_XcpRequest_De_XcpRequest 将缓存区的Signal 数据读出并传入XcpCommand -> Xcp 协议栈进行响应-> 如需向Tester 发出Response, 调用ApplXcpSend接口。
那么我们首先应该分析XcpCommand这个最重要的函数, 它其实就是一个状态机
void XcpCommand( const vuint32* pCommand )
{
const tXcpCto* pCmd = (const tXcpCto*) pCommand; /* PRQA S 0310 */ /* MD_Xcp_0310 */
vuint8 err;
if (CRO_CMD==CC_CONNECT)
{
CRM_CMD = 0xFF; /* No Error */
xcp.CrmLen = 1; /* Length = 1 */
#if defined ( XCP_ENABLE_DAQ )
if ( (xcp.SessionStatus & (SessionStatusType)SS_RESUME) == 0 )
{
XcpFreeDaq();
#if defined ( XCP_ENABLE_SEND_QUEUE )
xcp.SendStatus = 0; /* Clear all transmission flags */
#endif
}
#endif /* XCP_ENABLE_DAQ */
#if defined ( XCP_ENABLE_SEED_KEY )
/* Lock all resources. */
xcp.ProtectionStatus = (vuint8)RM_CAL_PAG|RM_DAQ|RM_PGM|RM_STIM;
#endif
xcp.SessionStatus = (SessionStatusType)SS_CONNECTED;
xcp.CrmLen = CRM_CONNECT_LEN;
/* Versions of the XCP Protocol Layer and Transport Layer Specifications. */
CRM_CONNECT_TRANSPORT_VERSION = (vuint8)( (vuint16)XCP_TRANSPORT_LAYER_VERSION >> 8 );
CRM_CONNECT_PROTOCOL_VERSION = (vuint8)( (vuint16)XCP_VERSION >> 8 );
CRM_CONNECT_MAX_CTO_SIZE = kXcpMaxCTO;
CRM_CONNECT_MAX_DTO_SIZE_WRITE(kXcpMaxDTO); /* PRQA S 3109 */ /* MD_MSR_14.3 */
#if defined ( XCP_ENABLE_CALIBRATION_PAGE )
CRM_CONNECT_RESOURCE = RM_CAL_PAG; /* Calibration */
#else
CRM_CONNECT_RESOURCE = 0x00; /* Reset resource mask */
#endif
#if defined ( XCP_ENABLE_DAQ )
CRM_CONNECT_RESOURCE |= (vuint8)RM_DAQ; /* Data Acquisition */
#endif
CRM_CONNECT_COMM_BASIC = 0;
#if defined ( XCP_ENABLE_COMM_MODE_INFO )
CRM_CONNECT_COMM_BASIC |= (vuint8)CMB_OPTIONAL;
#endif
#if defined ( XCP_CPUTYPE_BIGENDIAN )
CRM_CONNECT_COMM_BASIC |= (vuint8)PI_MOTOROLA;
#endif
goto positive_response;
}
else{
if ( (xcp.SessionStatus & (SessionStatusType)SS_CONNECTED) != 0 ){
#if defined ( XCP_ENABLE_SEND_QUEUE )
if ( (xcp.SendStatus & (vuint8)(XCP_CRM_PENDING|XCP_CRM_REQUEST)) != 0 )
{
xcp.SessionStatus |= (SessionStatusType)SS_ERROR;
END_PROFILE(1); /* Timingtest */
/* No response */
return;
}
#endif
#if defined ( XCP_ENABLE_GET_SESSION_STATUS_API )
xcp.SessionStatus |= (SessionStatusType)SS_POLLING;
#endif
CRM_CMD = 0xFF; /* No Error */
xcp.CrmLen = 1; /* Length = 1 */
switch (CRO_CMD){
case CC_SYNC:
xcp.CrmLen = CRM_SYNCH_LEN;
CRM_CMD = PID_ERR;
CRM_ERR = CRC_CMD_SYNCH;
break;
#if defined ( XCP_ENABLE_COMM_MODE_INFO )
case CC_GET_COMM_MODE_INFO:
{
xcp.CrmLen = CRM_GET_COMM_MODE_INFO_LEN;
CRM_GET_COMM_MODE_INFO_DRIVER_VERSION = (vuint8)( ((CP_XCP_VERSION & 0x0F00) >> 4) |
(CP_XCP_VERSION & 0x000F) );
CRM_GET_COMM_MODE_INFO_COMM_OPTIONAL = 0;
CRM_GET_COMM_MODE_INFO_QUEUE_SIZE = 0;
CRM_GET_COMM_MODE_INFO_MAX_BS = 0;
CRM_GET_COMM_MODE_INFO_MIN_ST = 0; /*查UDS相关概念*/
}
break;
#endif
case CC_DISCONNECT:
xcp.CrmLen = CRM_DISCONNECT_LEN;
XcpDisconnect();
break;
...代码太多就不拷贝了
大家可以仔细阅读
}
这里总结下Xcp 标定/测量的流程
首先肯定要建立连接
M->S FF 00 00 00 00 00 00 00 请求建立连接
S->M FF 连接成功
M->S F8 00 01 00 00 00 00 00 解锁RM_CAL_PAG 标定权限
S->M FF 06 01 02 03 04 05 06 Slave发回6Bytes 种子
M->S F7 02 BE EF 00 00 00 00 发回KEY进行解锁
S-M FF 解锁成功,可以标定
M->S SET_MTA 传入需要标定变量的地址(Master根据A2L文件)
M->S Download Size+Data 这样存在RAM中的标定变量就可以被改写了
测量的话分为Polling 和 DAQ, 其中Polling较为简单,但效率较低,不适合速度快一点的采集,DAQ模式较为复杂,这里就不具体分析代码了,只有弄明白什么是DAQList,Odt ,OdtEntryAddress, OdtEntrySize以及它们在这套代码中是如何分配内存的就可以了
需注意由于这里采用CAN通信,一个DTO最多发送8个字节, 其中第一个字节是Odt编号,也就说真正用于发送Data的只有7个字节,而Xcp不像UDS或Tcp那样可以发连续帧,有FlowCtrl
一个Dto发送一个Odt包含这个Odt中所有EntryAddres中存放的EntrtySize大小的数据
所以一个Odt中最多可包含MaxDto-1个Entry 并且要Size 都为1Bytes
也就说我有4个 uint32 moni_vars 那么一个Odt肯定放不下, 需要四个Odt
DAQ 简要的流程是 FreeDAQ -> Allocate DAQList -> Allocate Odt -> Allocate Entry ->
WriteDAQ (为每个Entry 写上变量的具体地址和大小,比如 uint32 moni_var1 adress:0x1FFF8000 size:4bytes)-> Start DAQ(这里可以分别为每个DAQList 设置Prescaler, 即采样周期)
相当于先在ECU中建立好一张采样表,并设定周期开始采样。
其他需要注意的是一定要打开XCP_ENABLE_SEND_QUEUE这个宏,否则DAQ会丢帧,另外根据工具(上位机)的不同需要注意一下上位机传数据的大小段格式。
附上Xcp_Cfg.h
#if !defined(__XCP_CFG_H__)
#define __XCP_CFG_H__
/* define here configuration parameters for customizing XcpBasic driver */
#define V_MEMROM0
#define kXcpMaxCTO 8
#define kXcpMaxDTO 8
#define V_MEMROM0
#define XCP_DISABLE_COMM_MODE_INFO
#define XCP_DISABLE_SERV_TEXT
#define XCP_DISABLE_SERV_TEXT_PUTCHAR
#define XCP_DISABLE_SERV_TEXT_PRINT
#define XCP_DISABLE_CALIBRATION_PAGE
#define XCP_TRANSPORT_LAYER_VERSION 0x100
#define XCP_DISABLE_UNALIGNED_MEM_ACCESS
#define XCP_ENABLE_USE_BYTE_ACCESS
#define XCP_CPUTYPE_LITTLEENDIAN
#define XCP_ENABLE_SEND_EVENT /*send event支持*/
#define XCP_ENABLE_CALIBRATION
#define XCP_ENABLE_PARAMETER_CHECK
#define XCP_ENABLE_SEED_KEY
#define XCP_ENABLE_DAQ
#define XCP_DISABLE_DAQ_HDR_ODT_DAQ
#define XCP_ENABLE_DAQ_PROCESSOR_INFO
#define XCP_ENABLE_DAQ_RESOLUTION_INFO
#define XCP_ENABLE_DAQ_PRESCALER
#define XCP_ENABLE_DAQ_OVERRUN_INDICATION
#define XCP_ENABLE_SEND_QUEUE
#define kXcpDaqMemSize (1024*2)
#endif /* __XCP_CFG_H__ */