【汽车电子】关于XCP协议开源实现XCPlite-5.3的分析与使用
文章目录
一、引言
【源码地址】:XCPlite-5.3
在vector官网,一共给出了XCP协议的三个实现版本,三个实现版本之间的实现内容如下:
- XCPlite : 基于ASAM XCP V1.4 实现的XCP轻量版
- XCP Professional Driver : 包含了ASAM规范的有用扩展,并支持基于工具的配置。可用于Vector CANbedded基础软件
- MICROSAR XCP: 包含上面一个版本的所有功能特性,并基于AUTOSAR规范。可用于Vector MICROSAR基本软件。
本文对github上由vector开源的XCP协议实现版本XCPlite-5.3进行简要分析。
二、编译运行
(1)编译环境安装:
sudo apt-get install cmake g++
(2)资源获取:
git clone https://github.com/vectorgrp/XCPlite.git
(3)编译
cd XCPlite
git checkout V5.3
cd C_Demo
mkdir build && cd build
cmake ..
make
(4)运行
./C_Demo.out -bind 192.168.1.110 -port 5555 -tcp
-bind : serverip 当前IP
-port: 固定 5555
-tcp : tcp/udp
三、CANoe下的XCP工具使用
使用CANoe的XCP工具与XCPlite模块通信搭建。(VN5650的使用的以太网口配置,参照北汇培训文档)
1、CANoe导入xcplite生成的a2l文件
2、导入时选择基于TCP的连接
3、A2L下 网络参数调整 HOST: 初次导入需要IP调整(C_demo 作为TCP server)
4 、在XCPlite启动的前,需要选择 测量 参数 , 被选择参数 后续可以更改 采样方式
5.启动 CANOE 的 xcp(确保网络配置已完成),执行
四、源码分析
4.1 概述
该版本基于ASAM XCP Protocol Layer V1.4实现。
支持以下命令:
- GET_COMM_MODE_INFO
- GET_ID GET_VERSION
- SET_MTA UPLOAD
- SHORT_UPLOAD
- DOWNLOAD
- SHORT_DOWNLOAD
- GET_CAL_PAGE
- SET_CAL_PAGE
- BUILD_CHECKSUM
- GET_DAQ_RESOLUTION_INFO
- GET_DAQ_PROCESSOR_INFO
- GET_DAQ_EVENT_INFO
- GET_DAQ_LIST_INFO
- FREE_DAQ
- ALLOC_DAQ
- ALLOC_ODT
- ALLOC_ODT_ENTRY
- SET_DAQ_PTR
- WRITE_DAQ
- WRITE_DAQ_MULTIPLE
- GET_DAQ_LIST_MODE
- SET_DAQ_LIST_MODE
- START_STOP_SYNCH
- START_STOP_DAQ_LIST
- GET_DAQ_CLOCK
- GET_DAQ_CLOCK_MULTICAST
- TIME_CORRELATION_PROPERTIES
限制:
- 不支持8位和16位CPU,仅支持在32位或64位Linux和Windows平台上进行测试
- 不支持Motorola字节序(大端)
- 不符合misra规范
- ODT的总数限制为64K
- ODT条目的总数限制为64K
- 固定的DAQ+ODT 2字节DTO头
- 固定的32位时间戳
- 仅支持动态DAQlist分配
- 不支持恢复
- 不支持事件过载指示
- DAQ不支持预分频器
- 不支持ODT优化
- 不支持Seed & key
- 不支持Flash programming
4.2 项目结构
这份源码给出了XCP的c语言实现版本和c++实现版本,结合项目实际情况,本章节对C++实现版本进行分析:
| 一级目录 | 二级目录 | 文件说明 | |
| XCPLite-5.3 | |||
| CPP_Demo | |||
| CANape | CANape配置 | ||
| CMakeLists.txt | |||
| main_cfg.h | 该文件由main_cfg.h.in生成,定义了一些关于XCP协议层与传输层以及A2L,默认ip地址与端口方面的宏 | ||
| main_cfg.h.in | 配置文件 | ||
| main.cpp | |||
| main.h | 头文件,windows实现与linux实现下的头文件有所不同 | ||
| xcp_cfg.h | XCP协议层配置参数 | ||
| xcptl_cfg.h | XCP传输层配置参数 | ||
| src | |||
| A2L.cpp | 生成A2L文件 | ||
| A2L.hpp | |||
| platform.c | |||
| platform.h | 基于win与linux平台差异,分别定义了关于键盘输入,休眠,线程,锁,Sockets方面的实现 | ||
| util.c | |||
| util.h | 定义一些有帮助的函数 | ||
| xcp.cpp | |||
| xcp.h | |||
| xcp.hpp | XCP server的C++包装器 | ||
| xcpAppl.c | |||
| xcpAppl.h | xcpLite的应用程序特定功能 | ||
| xcpLite.c | |||
| xcpLite.h | 关于XCP命令的部分实现 | ||
| xcpServer.c | |||
| xcpServer.h | 基于UDP的XCP,展示如何在应用程序中集成XCP驱动程序,为cmd处理和数据传输创建线程 | ||
| xcpTl.c | |||
| xcpTl.h | 传输层接口 | ||
4.3 源码分析
4.3.1 概述
单从项目结构上来看,XCP传输层的接口与XCP协议层的接口已经分别在xcpTl.h文件与xcpLite.h中暴露出来,XCP从节点服务的三个接口也已经在XcpServer.h中暴露,这些接口又在 xcp.hpp 中进行封装,对外表现为Xcp单例类,该单例类内部已包含A2L类成员主要负责生成必要的A2L文件),因此我认为只要暴露出Xcp单例类的接口即可完成封装任务,进而完成移植。
另外,基于标定时的实际情况,需要进行测量与标定的参数可分为两类:
- 一类是基本数据类型,包括uint8、uint16、uint32、uint64、int8、int16、int32、int64、float、double等;
- 一类是用户自定义数据类型struct。
前者可通过已经封装好的接口生成A2L文件,进行测量与标定;后者需要用户封装成类并继承自XcpObject类,才能完成对用户自定义类型的测量与标定。或者一些需要封装成Event(每隔固定周期通知一次的事件),具有关联关系的变量也可继承自XcpObject。
从概念上理解,既然用户选择将一些数据封装成结构体类型,其实也就意味着这些成员数据在某种程度上存在关联,在进行标定时,可能会由于其中一个数据成员的变化,引起另外一个或者几个成员数据的变化,这也正契合XCP中同步测量的概念,因此有必要封装类以便后续进行同步测量。
4.3.2 接口介绍
下面对需要暴露的接口进行简要介绍。
4.3.2.1 Xcp.hpp
-
Xcp::getInstance(): 获取Xcp单例。 -
Xcp::init(): 初始化从节点XCP Server,并指定IP地址、端口号以及使用的传输协议(TCP或PTP)。 -
Xcp::shutdown(): 关闭从节点XCP Server。 -
Xcp::onConnect(): 连接回调函数,在与主节点建立连接时被调用。 -
Xcp::onPrepareDaq(): 准备DAQ回调函数,在准备开始数据采集时被调用。 -
Xcp::onStartDaq(): 开始DAQ回调函数,在开始数据采集时被调用。 -
Xcp::onStopDaq(): 停止DAQ回调函数,在停止数据采集时被调用。 -
Xcp::getDaqClock(): 获取DAQ时间戳。 -
Xcp::status(): 查询XCP Server状态。 -
Xcp::connected(): 查询当前从节点是否已连接到主节点。 -
Xcp::daqRunning(): 查询数据采集是否正在进行。 -
Xcp::clearEventList(): 清除事件列表。 -
Xcp::createEvent(): 创建一个事件,并返回事件ID。 -
Xcp::getEventList(): 获取事件列表std::vector<XcpEventDescriptor>。 -
Xcp::event(): 触发指定的事件,参数为指定的event id。 -
Xcp::eventExt(): 触发指定的事件,并传递扩展数据。 -
Xcp::eventAt(): 在指定的时间戳触发事件,入参为指定的eventID,与指定的时间戳。 -
Xcp::eventExtAt(): 在指定的时间戳触发事件,并传递扩展数据(前面两个功能的融合)。 -
Xcp::getA2lAddr(): 根据指针获取A2L地址。 -
Xcp::getA2lFileName(): 获取A2L文件名。 -
Xcp::createA2L(): 创建A2L文件。 -
Xcp::closeA2L(): 关闭A2L文件。
4.3.2.2 A2L.hpp
为了方便A2L文件的生成,一些宏定义有必要了解:
// 创建待测量量
#define createMeasurement(name,comment) createMeasurement_(NULL,#name,A2lGetType(name),0,A2lGetAddr((uint8_t*)&name),1.0,0.0,NULL,comment)
#define createDynMeasurement(instanceName,name,comment) createMeasurement_(instanceName,#name,A2lGetType(name),1,A2lGetOffset(name),1.0,0.0,NULL,comment)
#define createPhysMeasurement(name,comment,factor,offset,unit) createMeasurement_(NULL,#name,A2lGetType(name),0,A2lGetAddr((uint8_t*)&name),factor,offset,unit,comment) // unsigned integer (8/16/32) with linear physical conversion rule
#define createDynPhysMeasurement(instanceName,name,var,comment,factor,offset,unit) createMeasurement_(instanceName,name,A2lGetType(var),1,A2lGetOffset(name),factor,offset,unit,comment) // named unsigned integer (8/16/32) with linear physical conversion rule
#define createMeasurementArray(name) createMeasurementArray_(NULL,#name,A2lGetType(name[0]),sizeof(name)/sizeof(name[0]),0,A2lGetAddr((uint8_t*)&name[0])) // unsigned integer (8/16/32) or double array
// 创建自定义结构体
#define createTypedefBegin(name,comment) createTypedefBegin_(#name,(int)sizeof(name),comment)
#define createTypedefParameterComponent(name) createTypedefParameterComponent_(#name,A2lGetType(name),A2lGetOffset(name))
#define createTypedefMeasurementComponent(name) createTypedefMeasurementComponent_(#name,A2lGetType(name),A2lGetOffset(name))
#define createTypedefEnd() createTypedefEnd_()
#define createTypedefInstance(instanceName,typeName,comment) createTypedefInstance_(instanceName, typeName, 0, A2lGetAddr((uint8_t*)&(instanceName)), comment)
#define createDynTypedefInstance(instanceName,typeName,comment) createTypedefInstance_(instanceName, typeName, 1, 0, comment)
// 创建标定量
#define createParameter(name,comment,unit) createParameter_(NULL,#name,A2lGetType(name),0,A2lGetAddr((uint8_t*)&name),comment,unit)
#define createDynParameter(instanceName,name,comment,unit) createParameter_(instanceName,#name,A2lGetType(name),1,A2lGetOffset(name),comment,unit)
#define createParameterWithLimits(name,comment,unit,min,max) createParameterWithLimits_(NULL,#name, A2lGetType(name), 0, A2lGetAddr((uint8_t*)&name), comment, unit, min, max)
#define createDynParameterWithLimits(instanceName,name,comment,unit,min,max) createParameterWithLimits_(instanceName,#name, A2lGetType(name), 1, A2lGetOffset(name), comment, unit, min, max)
#define createCurve(name,xdim,comment,unit) createCurve_(NULL,#name, A2lGetType(name[0]), 0, A2lGetAddr((uint8_t*)&name[0]), xdim, comment, unit)
#define createDynCurve(instanceName,name,xdim,comment,unit) createCurve_(instanceName,#name, A2lGetType(name[0]), 0, A2lGetOffset(name[0]), xdim, comment, unit)
#define createMap(name,xdim,ydim,comment,unit) createMap_(NULL,#name, A2lGetType(name[0][0]), 0, A2lGetAddr((uint8_t*)&name[0][0]), xdim, ydim, comment, unit)
#define createDynMap(instanceName,name,xdim,ydim,comment,unit) createMap_(instanceName,#name, A2lGetType(name[0][0]), 1, A2lGetOffet(name[0][0]), xdim, ydim, comment, unit)
4.3.3 DEMO程序分析
关于main函数的分析:
从节点的代码中要做的有两件事,第一是识别出待标定量与测量量,并在对应的A2L文件中创建相应的描述,以便主节点读取A2L配置文件,找到这些变量的地址。第二是依照实际的业务逻辑,在其中一部分变量之间建立关联关系,方便之后进行同步数据测量,并以event进行通知主节点。
#include "main.h"
#include "main_cfg.h"
#include "platform.h"
#include "util.h"
#include "xcp_cfg.h"
#include "xcptl_cfg.h"
#include "xcp.hpp"
#include "A2L.hpp"
//-----------------------------------------------------------------------------------------------------
// Min/Max
static class SigGen* pMaxSigGen = NULL;
static class SigGen* pMinSigGen = NULL;
static class XcpObject* minSigGenEvent = NULL;
static class XcpObject* maxSigGenEvent = NULL;
//这里分别演示了【具有某种关联关系的结构体类型】与【全局作用域的基本数据类型】的标定量
//首先是【具有某种关联关系的结构体类型】
class SigGen : public XcpObject
{
protected:
// 创建此实例的A2L描述
virtual void a2lCreateTypedefComponents(A2L* a2l)
{
a2l->createTypedefMeasurementComponent(value);//创建测量量value的A2L描述
a2l->createTypedefParameterComponent(par_cycleTimeUs);//创建标定量的A2L描述
a2l->createTypedefParameterComponent(par_ampl);
a2l->createTypedefParameterComponent(par_phase);
a2l->createTypedefParameterComponent(par_period);
a2l->createTypedefParameterComponent(par_offset);
}
public:
// 待标定量
uint32_t par_cycleTimeUs;
double par_ampl;
double par_phase;
double par_offset;
double par_period;
// 待测量量
double value;
// Task线程
std::thread* t;
SigGen(const char* instanceName, uint32_t cycleTimeUs, double ampl, double offset, double phase, double period) : XcpObject(instanceName, "SigGen", sizeof(SigGen))
{
// 构造函数中进行参数初始化
par_cycleTimeUs = cycleTimeUs;
par_ampl = ampl;
par_offset = offset;
par_phase = phase;
par_period = period;
// Init signals
value = 0;
// 开启线程
t = new std::thread(
[this]()
{
task();
});
}
void calcMinMax(double v)
{ // 结合实际业务,有时候,某个变量的变化可能具有一定的局限性,当这个变量的值大于或小于特定的数之后,应当通知主节点
if (pMaxSigGen == NULL || (pMaxSigGen != this && pMaxSigGen->value < v))
{
pMaxSigGen = this; // current task with the minimal value
maxSigGenEvent->xcpEvent((uint8_t*)pMaxSigGen); // Trigger an event when pMaxSigGen changed
}
if (pMinSigGen == NULL || pMinSigGen->value > v)
{
pMinSigGen = this; // current task with the maximum value
minSigGenEvent->xcpEvent((uint8_t*)pMinSigGen); // Trigger an event when pMinSigGen changed
}
}
void task()
{
printf("ECU task %s running\n", instanceName);
for (;;)
{
sleepNs(par_cycleTimeUs * 1000);//设定好的通知周期
// 这里给出了一段假定的算法:正弦波计算公式:y = a + A * sin(ωt + φ),并假定测量量是经过此计算公式而输出的
value = par_offset + par_ampl * sin((double)clockGet64() * M_2PI / (CLOCK_TICKS_PER_S * par_period) + par_phase);
calcMinMax(value); // 一个计算周期结束之后,判定当前的value是否触发最大值/最小值事件
xcpEvent();//一个计算周期结束之后,主动通知主节点
}
// printf("\nECU task %s stopped\n", instanceName);
}
~SigGen()
{
delete t;
}
};
//-----------------------------------------------------------------------------------------------------
// //全局待标定量
uint8_t par_uint8 = 8;
uint16_t par_uint16 = 16;
uint32_t par_uint32 = 32;
uint64_t par_uint64 = 64;
int8_t par_int8 = -8;
int16_t par_int16 = -16;
int32_t par_int32 = -32;
int64_t par_int64 = -64;
float par_float = 0.32f;
double par_double = 0.64;
int main(int argc, char* argv[])
{
printf("\nXCP on Ethernet C++ Demo\n");
if (!cmdline_parser(argc, argv))
return 0;
// XCP singleton and A2L init (using the A2L factory from Xcp)
Xcp* xcp = Xcp::getInstance();//首先,获取Xcp单例
if (!xcp->init(gOptionAddr, gOptionPort, gOptionUseTCP, FALSE))//利用给定的IP地址,端口号,传输协议进行初始化
return -1;
A2L* a2l = xcp->createA2L("CPP_DEMO");//创建A2L文件并命名,一般来讲,一个从节点只需要生成1个A2L文件
a2l->createParameter(gDebugLevel, "", "");//创建一个标定参数以控制日志输出级别
a2l->createParameter(par_uint8, "", "");//创建一些测试用的(全局作用域内的)标定参数
a2l->createParameter(par_uint16, "", "");
a2l->createParameter(par_uint32, "", "");
a2l->createParameter(par_uint64, "", "");
a2l->createParameter(par_int8, "", "");
a2l->createParameter(par_int16, "", "");
a2l->createParameter(par_int32, "", "");
a2l->createParameter(par_int64, "", "");
a2l->createParameter(par_float, "", "");
a2l->createParameter(par_double, "", "");
//在A2L中建立一个参数组,具体作用需结合CANoe标定页面操作进行推测。
//入参为这个参数组的组名,组成员个数,以及各个成员名。具体函数实现为一个可变参数函数
a2l->createParameterGroup("TestParameters", 11, "gDebugLevel", "par_int8", "par_int16", "par_int32", "par_int64", "par_uint8", "par_uint16", "par_uint32", "par_uint64", "par_float", "par_double");
// Create virtual instances of pMinSigGen and pMaxSigGen
minSigGenEvent = new XcpDynObject("pMinSigGen", SigGen);
maxSigGenEvent = new XcpDynObject("pMaxSigGen", SigGen);
//基于1个SigGen类,创建10个具有不同instanceName的实例
SigGen* sigGen[10];
for (int i = 0; i <= 9; i++)
{
std::string* s = new std::string("SigGen");
s->append(std::to_string(i + 1));
sigGen[i] = new SigGen((char*)s->c_str(), 2000, 100.0 - i * 5, 0.0, i * M_PI / 15.0, 2.0);
}
//在A2L文件中为sigGen类创建描述,由于上面10个实例同属于sigGen类,因此只需要其中一个实例调用a2lCreateTypedef即可完成创建。
sigGen[0]->a2lCreateTypedef();
// 关闭A2L文件
xcp->closeA2L();
// Main loop (health status and keyboard check)
printf("\nPress ESC to stop\n");
for (;;)
{
sleepMs(100);
if (!xcp->status())
{
printf("\nXCP server failed\n");
break;
} // Check if the XCP server is running
if (_kbhit())
{
if (_getch() == 27)
break;
} // Stop on ESC
}
// 关闭从节点的Server
xcp->shutdown();
printf("\nApplication terminated. Press any key to close\n");
while (!_kbhit())
sleepMs(100);
return 0;
}
五、动态库封装与使用
5.1 封装
封装时需要注意:这份源码中部分文件是通过c语言实现的,在正常的编译运行过程中可以正常通过C++调用C语言实现的函数,但是封装动态库时直接封装会导致未定义引用,究其原因是语言方面的限制。为了使C语言撰写的代码能够正常被调用,需要在hpp的头文件中识别出c语言的头文件,并用extern “C”{}包围,实现C和C++的混合编程。
extern "C" {//将C代码的头文件使用extern "C"包围
#include "main.h"
#include "main_cfg.h"
#include "platform.h"
#include "util.h"
#include "xcp_cfg.h"
#include "xcptl_cfg.h"
}
#include "xcp.hpp"
#include "A2L.hpp"
#include <typeinfo>
#include <thread>
#include <string>
#include <vector>
封装过程中使用到的CMakeLists如下:
cmake_minimum_required(VERSION 3.4)
set(TARGET_NAME xcp)
project(${TARGET_NAME})
set(CMAKE_CXX_FLAGS "${CAMKE_CXX_FLAGS} -std=c++14 -pthread")
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/lib)
file(GLOB_RECURSE INCS
${CMAKE_CURRENT_SOURCE_DIR}/**.hpp
${CMAKE_CURRENT_SOURCE_DIR}/**.h
)
set(CPP_Demo_SOURCES xcpAppl.c xcp.cpp xcpLite.c xcpTl.c xcpServer.c A2L.cpp platform.c util.c)
add_library(${TARGET_NAME} SHARED ${CPP_Demo_SOURCES})
file(COPY ${INCS} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/inc)
5.1 使用
将封装好的动态库.so文件复制到lib文件夹下,并将头文件复制到inc文件夹下。
项目结构如下:
.
├── build
├── CANape
│ ├── CanapeCmd.ini
│ ├── CANape.ini
│ ├── CPP_Demo.cna
│ └── XCP_104.aml
├── CMakeLists.txt
├── inc
│ ├── A2L.hpp
│ ├── main_cfg.h
│ ├── main.h
│ ├── platform.h
│ ├── util.h
│ ├── xcpAppl.h
│ ├── xcp_cfg.h
│ ├── xcp.h
│ ├── xcp.hpp
│ ├── xcpLite.h
│ ├── xcpServer.h
│ ├── xcptl_cfg.h
│ └── xcpTl.h
├── lib
│ └── libxcp.so
└── main.cpp
使用过程中的CmakeLists如下:
cmake_minimum_required(VERSION 3.4)
set(TARGET_NAME test)
project(${TARGET_NAME})
set(CMAKE_C_COMPILER "gcc")
set(CMAKE_CXX_COMPILER "g++")
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
set(CPP_Demo_SOURCES main.cpp)
set(OPTION_DEBUG_LEVEL 2 CACHE STRING "Debug output level")
option(OPTION_ENABLE_TCP "Enable TCP" 1)
option(OPTION_USE_TCP "Use TCP by default" 0)
set(OPTION_SERVER_PORT 5555 CACHE STRING "XCP default port")
set(OPTION_SERVER_ADDR {0,0,0,0} CACHE STRING "XCP IP address to bind, ANY=0.0.0.0")
option(OPTION_ENABLE_A2L_GEN "Enable A2L file generator" 1)
link_directories (${PROJECT_SOURCE_DIR}/lib)
include_directories(${PROJECT_SOURCE_DIR}/inc)
add_executable(${TARGET_NAME} ${CPP_Demo_SOURCES})
target_link_libraries(${PROJECT_NAME} libxcp.so)
#target_link_libraries(${PROJECT_NAME} m)
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
target_link_libraries(${TARGET_NAME} Threads::Threads)
之后正常编译运行。
六、总结
综上所述,大多数需要使用到的接口已经在Xcp.hpp中,通过Xcp类进行封装,使用者无需关心这份代码中Xcp协议层与传输层的具体实现,通过创建Xcp单例,将识别出的“待标定”量,“待测量”量写入A2L文件,并将其中一些有关联关系的变量建立Event机制,就能实现基本的测量与标定功能。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)