为什么移植CCP
我所在的公司做新能源汽车的动力系统集成,行业内CCP应用较为广泛,故移植CCP。第一为贴近行业前沿,第二为适配主机厂家ECU做准备。主要做的工作就是使用CCP协议替换掉之前自定义的上、下位机通讯协议。
简要介绍
在标定系统中,上位机和下位机的通信方式有很多种,其中CCP就是符合ASAM标准的应用非常广泛的一种。
CCP(CAN calibration protocol)协议采用主从通信方式,即只有一台主设备,而从设备(被标定的ECU)可以有多个,在同一时刻主设备只能与其中的某一个从设备直接进行会话。当两者建立连接后,主设备再发送的命令其他从设备依然可以接收,但是只有处于连接状态的ECU才会对报文做出响应。通信结束后,主设备发送断开连接命令,才能与其他设备进行会话。
CCP报文帧
CCP协议含有两种报文帧,即接收和发送,其ID由用户自行定义。命令和数据则通过8Byte的报文数据来进行交互,由主设备发出的普通命令均要求从设备反馈握手信息,握手信息中包含命令的返回码和错误码。
- 命令接收报文(CRO)
CRO(command receive object)用于主设备向从设备发送命令。报文的第一字节代表命令(CMD)。报文的第二字节表示命令计数器CTR(command counter),用于记录当前命令执行次数。其余的6个字节表示命令参数。从设备每接收到一条命令后,必须返回一个ODT帧。
- 命令发送报文(DTO)
DTO(data transmission object)用于从设备向主设备发送信息。DTO共有以下三种形式:
- CRM(command return message):CRO报文反馈。
- Event Message:当从设备内部发生处理错误(繁忙、等待、丢失等)时,从设备自行向主设备发送,报告状态,请求暂缓当前命令进程。
- DAQ-DTO(data acquisition-DTO):用于DAQ模式,由从设备向主设备以固定周期发送。
- 命令接收报文(CRO)
关于接收发送报文的每个字节对应的是什么意义,“CCP21.PDF”这个文档中有详细的说明,文档的位置在源码包解压后的“CCP/DOC/”文件夹下。
另外,以上介绍的所有信息在“CCP/DOC”文件夹下的文档中都可以找到依据,我只是把我觉得比较重要的点提取出来。
- DAQ模式
DAQ模式是从设备以固定周期向主设备自动上传数据的一种高效采集模式。在DAQ模式下,主设备通过命令设置主动上传数据的地址和相关信息,之后发送开始命令,从设备开始上传数据。
代码移植
网上找到Vector的开源代码,抛开硬件平台相关的东西不谈,CCP源码文件主要有三个:
文件名 描述 CRC.C Vector提供的CCP Driver的源代码 CCP.H CCP Driver的头文件 CCPPAR.h CCP Driver的配置文件
我的移植是在stm32f407上实现的,创建文件
CCPInterface.c :1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68void CCPInterfaceInit(void)
{
//初始化CRO,即从设备接收ID
DEBUG_CAN_HalInit(CCP_CRO_ID, 0xFFFFFFFF, &DebugRevHandler, &pRxMsg, &pTxMsg);
//调用CCP初始化
ccpInit();
}
void CCPInterfaceIRQCalc(void)
{
if(CAN_GetFlagStatus(DEBUG_CAN_PORT,CAN_FLAG_RQCP0|CAN_FLAG_RQCP1|CAN_FLAG_RQCP2))
{
//接收到主设备发来的报文,调用该函数解析
ccpSendCallBack();
}
}
void CCPInterface1msCalc(void)
{
//1ms事件 用于RAM数据的循环显示 这里的1表示通道事件编号为1,
//对应主设备发来的CC_START_STOP命令帧的byte5 该帧的详细信息后面会有说到
//每个通道事件编号对应唯一的DAQ发送周期
ccpDaq(1);
}
void CCPInterface50msCalc(void)
{
//50ms事件 用于波形显示时其他数据显示
ccpDaq(2);
}
/*! \fn void ccpSend(CCP_BYTEPTR msg )
* \brief 定义ccpSend以供CCPdriver调用
* \param none
* \return none
*/
void ccpSend(CCP_BYTEPTR msg )
{
pTxMsg->ExtId = CCP_DTO_ID;
pTxMsg->RTR = CAN_RTR_DATA;
pTxMsg->IDE = CAN_ID_EXT;
pTxMsg->DLC = 8;
pTxMsg->Data[0] = msg[0];
pTxMsg->Data[1] = msg[1];
pTxMsg->Data[2] = msg[2];
pTxMsg->Data[3] = msg[3];
pTxMsg->Data[4] = msg[4];
pTxMsg->Data[5] = msg[5];
pTxMsg->Data[6] = msg[6];
pTxMsg->Data[7] = msg[7];
DEBUG_CAN_TxMessage();
}
void ccpUserBackground( void )
{
}
CCP_MTABYTEPTR ccpGetPointer(CCP_BYTE ext, CCP_DWORD addr )
{
CCP_BYTE ext_local;
//伪读取,防止警告
ext_local = ext;
ext = ext_local;
return (CCP_MTABYTEPTR)addr;
}
对于 CCP.H修改:1
2#define CCP_INTEL // CCP_MOTOROLA 这个根据你硬件平台是大端还是小端进行修改
#define CCP_WRITE_EEPROM //如果你需要通过CCP对EE进行读写,这个宏要打开
对于 CCPPAR.H修改:1
2
3
4
5
6
7
8
9
10
11
12#define CCP_STATION_ID "ECU00001" /* Plug&Play station identification */
#define CCP_DTO_ID 0x100000A /* CAN identifier ECU -> Master */
#define CCP_CRO_ID 0x100000C /* CAN identifier Master -> ECU */
/*----------------------------------------------------------------------------*/
/* CCP Data Acuisition Parameters */
#define CCP_DAQ /* Enable synchronous data aquisition in ccpDaq() */
#define CCP_MAX_ODT 60 /* Number of ODTs in each DAQ lists 每个DAQList的ODT数目不超过60个 */
#define CCP_MAX_DAQ 2 /* Number of DAQ lists DAQList的个数不超过2个,原因暂时未知*/
至此,CCP的底层代码移植完成。
主设备如何访问ECU
- 单个数据访问
主设备对ECU的单个数据访问很简单,执行以下步骤即可:
CONNECT -> 交换ID -> 设置内存传输地址 -> 访问ECU数据 - 主设备对ECU设置DAQ
上位机对某个ECU设置DAQ应该按下图所示执行。

命令详解:
connect
CC_GET_DAQ_SIZE
com[2] 为DAQList number
com[4..7] DTO专用列表编号标识符(没用到)
crm[3] 为该DAQList的大小
crm[4] 为第一个ODT的PID
- SET_DAQ_PTR
com[2] 设置DAQList number
com[3] 设置哪个ODT
com[4] 哪个字节
- CC_WRITE_DAQ
com[2] 设置DAQList的大小(这里只能为1)
com[4..7] DAQList的入口地址()
- CC_START_STOP
com[2] start(0)/stop(1)
com[3] DAQList number
com[4] 最后一个ODT的编号
com[5] Event Channel number(目前为1)
com[6..7] DAQList 的周期
(例:com[6..7] = 1 周为10ms com[6..7] = 2 周期为20ms )
其中:
com[0..7] 表示上位机发给ECU的数据
crm[0..7] 表示ECU返回给上位机的数据
举个栗子:
设置DAQList1的ODT0装载u32数据0xadcd5678,周期为1S,触发事件编号为1,起始地址为0xd000 0000
则上位机的执行顺序应如下,这里的数据省略显示了每个命令的第0个字节
connect
GET_DAQ_SIZE
0x550100 00000000
DAQList number = 1
- SET_DAQ_PTR
0x550100 00000000
DAQList number = 1
ODTNum = 0
IDx = 0
- WRITE_DAQ
0x550100 000000d0
DAQList Size = 1
addr = 0xd000 0000
- SET_DAQ_PTR
0x550100 01000000
DAQList number = 1
ODTNum = 0
IDx = 1
- WRITE_DAQ
0x550100 010000d0
DAQList Size = 1
addr = 0xd000 0001
- SET_DAQ_PTR
0x550100 02000000
DAQList number = 1
ODTNum = 0
IDx = 2
- WRITE_DAQ
0x550100 020000d0
DAQList Size = 1
addr = 0xd000 0002
- SET_DAQ_PTR
0x550100 03000000
DAQList number = 1
ODTNum = 0
IDx = 3
- WRITE_DAQ
0x550100 030000d0
DAQList Size = 1
addr = 0xd000 0003
- START_STOP
0x550101 00016400
Command :开始
DAQList number = 1
LastODT = 0
事件编号 :1
周期(0064) :10ms 100 = 1S 这里的100,是因为在ECU中,ccpdaq(1)这个函数是每10ms调用一次,如果该函数1ms调用一次,则数据上传的周期为 1ms 100 = 100ms
经过上面一系列命令后,上位机接收到的数据为:0x 3c 78 56 cd ad 00 00 00,周期为1s
若要再添加数据,可以在DAQList1中更改ODTNum,循环使用3 - 10的命令,即可设置,只是要注意ODTNum的上限在CCPPAR.H中已经定义,不要超出
由上面的栗子可知:数据周期与DAQList编号绑定,如果想要不同周期的数据,在发送start/stop命令时,同时更改DAQList编号和周期数据即可,当然相应的ECU中要有对该编号的ccpdaq()函数调用,其参数为DAQList编号。
后记
我的这次移植也没有完全使用CCP的所有功能,例如程序的烧写、数据传输过程中的CRC校验、主从设备连接之前的秘钥校验等几个功能就没有实现。但是有了这次移植的基础,后续如果需要相关的功能,应该能使用较短的时间实现。
源码中实际上还有一些数据类型转换不规范,导致编译器报警告,时间的原因就没有去处理。如果你恰巧看到了这篇文章也要做同样的事情,建议阅读源码之前多去看看源码包里的文档,对程序结构的梳理很有帮助。