一、引言

【源码地址】: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实现。

支持以下命令:

  1. GET_COMM_MODE_INFO
  2. GET_ID GET_VERSION
  3. SET_MTA UPLOAD
  4. SHORT_UPLOAD
  5. DOWNLOAD
  6. SHORT_DOWNLOAD
  7. GET_CAL_PAGE
  8. SET_CAL_PAGE
  9. BUILD_CHECKSUM
  10. GET_DAQ_RESOLUTION_INFO
  11. GET_DAQ_PROCESSOR_INFO
  12. GET_DAQ_EVENT_INFO
  13. GET_DAQ_LIST_INFO
  14. FREE_DAQ
  15. ALLOC_DAQ
  16. ALLOC_ODT
  17. ALLOC_ODT_ENTRY
  18. SET_DAQ_PTR
  19. WRITE_DAQ
  20. WRITE_DAQ_MULTIPLE
  21. GET_DAQ_LIST_MODE
  22. SET_DAQ_LIST_MODE
  23. START_STOP_SYNCH
  24. START_STOP_DAQ_LIST
  25. GET_DAQ_CLOCK
  26. GET_DAQ_CLOCK_MULTICAST
  27. 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 
 CANapeCANape配置
CMakeLists.txt 
main_cfg.h该文件由main_cfg.h.in生成,定义了一些关于XCP协议层与传输层以及A2L,默认ip地址与端口方面的宏
main_cfg.h.in配置文件
main.cpp 
main.h头文件,windows实现与linux实现下的头文件有所不同
xcp_cfg.hXCP协议层配置参数
xcptl_cfg.hXCP传输层配置参数
src  
 A2L.cpp生成A2L文件
A2L.hpp 
platform.c 
platform.h基于win与linux平台差异,分别定义了关于键盘输入,休眠,线程,锁,Sockets方面的实现
util.c 
util.h定义一些有帮助的函数
xcp.cpp 
xcp.h 
xcp.hppXCP server的C++包装器
xcpAppl.c 
xcpAppl.hxcpLite的应用程序特定功能
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机制,就能实现基本的测量与标定功能。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐