编辑推荐: |
本文主要介绍了autosar
E2E相关内容。希望对你的学习有帮助。
本文来自于微信公众号车端软件开发,由火龙果软件Linda编辑,推荐。 |
|
本文不是解读规范。而是理解与场景相结合,噫在说E2E这个事情本身。可能没法直接套用,但是读完之后,你会在各种变化中都明白个大概。然后看看规范就知道怎么使用了。
1. overview
E2E 的保护概念是针对软件在运行时数据交换的保护。运行时数据发生问题的地方可能有很多。
数据链路,外围收发,单片机硬件,传输过程等等等等。外围电路的电池干扰等也都可能会对数据造成影响。这里End
2 End 就是为了检验出数据有没有发生问题。
从数据 发送端 到 接收端,可能是两个ECU 也可以是两个SWC, 甚至数据的上下环节都可以做E2E校验。下面以ECU之间的端到端检测例子。
Overview of E2E communication protection between
a sender and a receiver
1.1 Pdu 简单介绍
所以这个数据到底是什么?我们有必要了解一下不同位置的数据的名字。因为在保护的过程是需要有针对性的。而且不同的层级保护路径覆盖是不一样的。也就意味着安全等级是可能不一样的。
传统互联网分层的报文与autosar定义的相对比。
应用层——消息
传输层——数据段/报文段(segment) (注:TCP叫TCP报文段,UDP叫UDP数据报,也有人叫UDP段)
网络层——分组、数据包(packet)
链路层——帧(frame)
物理层——P-PDU(bit)
互联网的定义
autosar的定义
从上面图可以看出来,在autosar体系中,只要从驱动出来到了中间件(BSW) 统一都叫做PDU.
只是前缀可能不一致。更为具体细致的pdu了解可以见下图,不过和本文关系不大。
现在我们知道了autosar体系中,每个层级的pdu 简单定义。下面我们来从几个角度说E2E 本身的事情。
1.2 保护路径
从过上面不同位置的PDU 的介绍,我们在数据保护的角度就需要找到可能发生数据出问题的路径,然后针对性的保护。
可以说有很多路径都会出现问题。所以我们就需要制定“比较好”的解决方案。
这里比较好可以从成本,安全性,便捷性等方面去考虑。所以这里引出了三种保护方案
1.3 Frame & pdu
通过前面的学习,我们知道,在autosar体系的内部,所有流动的数据,都可以叫做PDU. 我们以总线报文的收发来作为例子说一下。
1.3.1 pdu分类
说到总线报文和软件内部的PDU 进行保护,那么问题来了,frame 和 pdu的关系呢?注意了 E2E
保护的是什么。保护的是PDU. 可以理解为报文(frame)的一部分。我们来详细说一下 frame,
pdu
在Arxml文件中定义的有如下类型:
说这个好像也没啥用,我的目的是为了说 frame = pdu*n + 额外信息 这里的n >=
1.
也就是说一帧frame 里面可以包含几个pdu的话,这帧报文就可能需要多次保护,因为保护的单位是pdu.
说到这个要说一下现在autosar 里面的定义了。
1.3.2 Autosar定义的PDU
对于普通CAN报文来说,一个message对应一个PDU,对于CANFD报文来说,引入Container
PDU和contained I-PDU实现一个message对应多组PDU。
考虑这样做的优势:
不同的contained I-PDU可以映射到不同的Container PDU的不同位置。不仅提高了灵活性,而且也会降低总线负载率。
有了这些知识,我们来聊一下autosar提到的三种E2E 实现方式。
2. E2E 实现方式
从保护的角度来说,最终的算法无论是callout 还是 wrapper 还是transformer
都是一样的。假设用CRC8 来计算,那么最终给PDU 添加的计算算法都是CRC8, 下面三种方式不一样的指的是路径不一样,和集成方式不一样。我们详细说一说。
2.1 Callout
Callout 指的是com callout. 举个例子,我们有一个pdu从asw 发出来,经过RTE
到达了COM. COM中维护的PDU 配置 可以选择callout.
COM 中 PDU 配置界面
所以就是说 PDU 的tx 的内容 需要调用一个函数,然后继续往下走。这里面调用的函数就是callout.
我们在这里面去调用E2E 的相关接口。把PDU 内部的数据进行计算,进行更新。
然后 从COM 出去之后的数据就是经过了E2E 计算,赋值的数据包了。
从链路上来看,有一段处于空白,就是ASW 到COM 的阶段,这里面是没有保护的,所以这里是有可能发生数据错误的。我们从callout的角度来说,没办法去识别出来。
从下图也可以看出来调用callout的位置。
callout
所以callout 的保护只是发送端的com 到 对等接收端的com. 如果com之上发生了错误,
这种E2E方法是无法检测到的。所以为了可靠性,我们需要更进一步的去覆盖保护范围
2.2 Wrapper
说不出来词形容wrapper. 用一个 不昏不素来形容吧。wrapper的实现有两种。目的是什么呢。目的就是
保护 pdu从swc发出的那一刻 一直到对端swc 接收到pdu的时候。
确实挺好的设计,但是为什么说不昏不素呢。autosar 给了两种方式。
2.2.1 Wrapper behind SWC
大家都知道SWC 发送 接收 pdu 走的是 RTE_Read,,,,,, 和 RTE_Write,,,,,,
两种实现方式一种是 可以说是修改了RTE 的接口,在调用RTE 的接口的时候,先走了一遍 E2ELib.
也就是说在SWC 的下方多了个Wrapper的适配层。如下图所示
保护的内容确实挺好,但是是不是每一个SWC 都需要这样的一个适配层。而且SWC 没办法调到原生的RTE
接口了。
2.2.2 Wrapper within SWC - Transmission Manager
然后呢,又来了个不昏不素的wrapper 实现。
既然想要原生的RTE 接口,那就把这个适配层放在RTE 接口和 SWC 之间。但是问题还是一样,每个SWC
都需要有这么个适配层。如下图,
所以可能就是前面提到的两个缺点,引入了一种新的方式,保护的方式,内容,路径和wrapper 是一致的。只是从系统的角度去实现。
2.3 Transformer
为什么说是系统的角度去实现,因为transformer 在做系统描述的时候,就已经去设计好了这一点。不需要在SWC
的设计阶段去做保护。
可以看得到,在系统描述文件的角度,已经把pdu 和 相对应的E2E 都连接好了。最终的实现是在RTE
内部进行实现。
那么问题来了,系统描述怎么知道pdu呢。毕竟在系统层面还是前面提到的frame啊。这里我CSDN
找了个写的比较好的。抄一段来说明一个message 包含多个pdu的概念。-- 来自 CSDN Autosar开发笔记
2.3.1 CANFD ContainerPDU定义
ContainerPDU并不是frame,但可以设置ContainerPDU包含frame所有的数据位。ContainerPDU是包含在frame中的。对于一个CANFD-frame,定义如下:
注:若CANFD报文实际只有8字节,那么就和普通报文一样,定义一个I-Signal-PDU就够了,不需要引入容器PDU。
Container-I-PDU定义如下:
Header Type有三种选择:
1.ShortHeader 2.LongHeader 3.NoHeader
Autosar中定义如下:
IpduM支持两种不同的动态Container Pdu的头大小(参见ECUC_IpduM_00183:
IpduMContainerHeaderSize):
IPDUM_HEADERTYPE_SHORT, 24位ID, 8位长度
IPDUM_HEADERTYPE_LONG, 32位ID, 32位长度
如果是选择的ShortHeader,那么实际数据位中会有三个byte为ID,一个byte为DLC,8个byte为数据位
在Container-PDU定义页可以选择包含的PDU及设置PDU对应的ID。
2.3.2 CANFD Signal-I-PDU定义
这个Signal-I-PDU就类似普通的CAN报文,里面定义了具体的信号信息,及layout信息。
2.3.3 CANFD实际数据解析
CANFD带Container的报文,实际数据长度为24,包含两个Signal-I-PDU(每个12个byte)
对应的Signal-I-PDU还可以继续展开解析后的信号具体信息
回归主题,有了上面的介绍,Transformer 带来的好处就是 无需手动代码。
SWC 自己的接口实现,无需关注自己的read 和 write 是否需要 E2E, 因为他所调用的RTE
接口,已经在系统描述的阶段做好了E2E.
2.3.4 总结
好了这里三种方式说完了。说了半天也没说到具体的计算算法,其实保护算法本身没有什么含量。关键的点在于知道什么时候该用这三种方式,针对性的对我们的系统进行设计。
3. E2E 保护机制
前面系统的层面去说了E2E 的为什么,和怎么去选择,去设计。这一章我们说一下具体E2E 内部是怎么实现的。为什么不在前面说,因为实现的本身是一样的。
思路是下图这样的。
那么我们从比较书面的方式去解释。
就拿E2E_Profile1A 来举例子
3.1 E2E_Profile1A 概述
上面我手画的保护信息值得就是下图
注意规定好了保护方式后,保护的报文的某些字节需要放什么数据,是已经确定好了的。
最终形成的PDU 如下。
这里问个问题,上图是存在于哪里?
答:上图是被保护的单元,可能共同属于一个frame, 也可能就是一个frame 甚至属于多个frame的集合。
好了pdu,frame 的概念后面不提了。
那么我们怎么去实现呢。
3.2 E2E_Profile1A 实现
我们以发送pdu, 来保护这个pdu 为例子说明。
C Std_ReturnType E2E_P01Protect(const E2E_P01ConfigType* ConfigPtr, E2E_P01ProtectStateType* StatePtr, uint8* DataPtr)
|
看一下这里面的形参
C /** @brief Protects the array/buffer to be transmitted using the E2E profile 1.
* This includes checksum calculation, handling of counter and Data ID.
*
* @param ConfigPtr Pointer to static configuration.
* @param StatePtr Pointer to port/data communication state.
* @param DataPtr Pointer to Data to be transmitted.
*
* @return Error code (E2E_E_INPUTERR_NULL || E2E_E_INPUTERR_WRONG || E2E_E_INTERR || E2E_E_OK).
*/
|
我们需要对pdu进行参数配置。配置的内容如下
这些的信息如果看清楚了前面我手画的信息,就很容易明白了,实际上就是各个参数的位置,长度,以及判断失败的标准。
第二个参数 是counter. E2E 是有CRC + Counter的。这个counter 也是需要这个算法本身去维护的。
第三个就是数据本身。
跟着源码一步一步解释
C Std_ReturnType E2E_P01Protect(const E2E_P01ConfigType* ConfigPtr, E2E_P01ProtectStateType* StatePtr, uint8* DataPtr) {
Std_ReturnType status; status = E2E_E_OK; Std_ReturnType returnValue = checkConfigP01(ConfigPtr);
if (E2E_E_OK != returnValue) { status = returnValue; }
else if ((StatePtr == NULL_PTR) || (DataPtr == NULL_PTR)) { status = E2E_E_INPUTERR_NULL; }
else { /* Put counter in data*/ if ((ConfigPtr->CounterOffset % 8) == 0) { DataPtr[ConfigPtr->CounterOffset/8] = (DataPtr[(ConfigPtr->CounterOffset/8)] & 0xF0u) | (StatePtr->Counter & 0x0Fu); } else { DataPtr[ConfigPtr->CounterOffset/8] = (DataPtr[ConfigPtr->CounterOffset/8] & 0x0Fu) | ((StatePtr->Counter<<4) & 0xF0u); }
/* Put counter in data for E2E_P01_DATAID_NIBBLE */ // ASR4.2.2 if (ConfigPtr->DataIDMode == E2E_P01_DATAID_NIBBLE) { if ((ConfigPtr->DataIDNibbleOffset % 8) == 0) { DataPtr[ConfigPtr->DataIDNibbleOffset/8] = (DataPtr[(ConfigPtr->DataIDNibbleOffset/8)] & 0xF0u) | ((uint8)((ConfigPtr->DataID>>8) & 0x0Fu)); } else { DataPtr[ConfigPtr->DataIDNibbleOffset/8] = (DataPtr[ConfigPtr->DataIDNibbleOffset/8] & 0x0Fu) | ((uint8)((ConfigPtr->DataID>>4) & 0xF0u)); } }
/* Calculate CRC */ DataPtr[(ConfigPtr->CRCOffset/8)] = calculateCrcP01(ConfigPtr, StatePtr->Counter, DataPtr);
/* Update counter */ StatePtr->Counter = (StatePtr->Counter+1) % 15; }
return status; }
|
在保护数据的前面,需要对Counter进行赋值。前面介绍到了counter的位置是固定的,也是有配置信息,的所以进行了赋值。不过有个需要注意的点就是mode。
E2E_P01DataIDMode = E2E_P01_DATAID_NIBBLE
这个是什么意思呢。
这里的DataID 的一部分,需要放到counter的位置里面去。所以说 在更新counter的时候。用到了一部分DataId.
其他的点都很好理解。
这里counter已经更新完毕。我们开始计算CRC
C uint8 calculateCrcP01(const E2E_P01ConfigType* Config, uint8 Counter, const uint8* Data)
|
在计算CRC, 需要用到什么。
DataId 需要计算
Counter 需要计算
Data 需要计算。
DataId 如何计算去觉得Config->DataIDMode 的配置。
唯一的数据 ID 用于验证每个传输的安全相关数据元素的身份。
二字节Data ID计算一字节CRC有以下四种包含方式:1. E2E_P01_DATAID_BOTH:CRC中包含两个字节(双ID配置),先低字节后高字节(请参见变体
1A - PRS_E2EProtocol_00227)或 2. E2E_P01_DATAID_ALT:取决于计数器的奇偶校验(交替
ID 配置),包括高字节和低字节(参见变体 1B - PRS_E2EProtocol_00228)。对于偶数计数器值,包括低字节,对于奇数计数器值,包括高字节。3.
E2E_P01_DATAID_LOW:只包含低字节,从不使用高字节。这相当于数据 ID(在给定应用进程中)只有
8 位的情况。
这就是计算过程。
那么具体怎么算的呢。这个就是比较标准的做法。不多解释,直接给码
C uint8 Crc_CalculateCRC8(const uint8* Crc_DataPtr, uint32 Crc_Length, uint8 Crc_StartValue8, boolean Crc_IsFirstCall) {
uint8 crc = 0; /* Default return value if NULL pointer */ #if Crc_8_Mode == CRC_8_TABLE static const uint8 Crc_8_Tab[256] = {0x0, 0x1d, 0x3a, 0x27, 0x74, 0x69, 0x4e, 0x53, 0xe8, 0xf5, 0xd2, 0xcf, 0x9c, 0x81, 0xa6, 0xbb, 0xcd, 0xd0, 0xf7, 0xea, 0xb9, 0xa4, 0x83, 0x9e, 0x25, 0x38, 0x1f, 0x2, 0x51, 0x4c, 0x6b, 0x76, 0x87, 0x9a, 0xbd, 0xa0, 0xf3, 0xee, 0xc9, 0xd4, 0x6f, 0x72, 0x55, 0x48, 0x1b, 0x6, 0x21, 0x3c, 0x4a, 0x57, 0x70, 0x6d, 0x3e, 0x23, 0x4, 0x19, 0xa2, 0xbf, 0x98, 0x85, 0xd6, 0xcb, 0xec, 0xf1, 0x13, 0xe, 0x29, 0x34, 0x67, 0x7a, 0x5d, 0x40, 0xfb, 0xe6, 0xc1, 0xdc, 0x8f, 0x92, 0xb5, 0xa8, 0xde, 0xc3, 0xe4, 0xf9, 0xaa, 0xb7, 0x90, 0x8d, 0x36, 0x2b, 0xc, 0x11, 0x42, 0x5f, 0x78, 0x65, 0x94, 0x89, 0xae, 0xb3, 0xe0, 0xfd, 0xda, 0xc7, 0x7c, 0x61, 0x46, 0x5b, 0x8, 0x15, 0x32, 0x2f, 0x59, 0x44, 0x63, 0x7e, 0x2d, 0x30, 0x17, 0xa, 0xb1, 0xac, 0x8b, 0x96, 0xc5, 0xd8, 0xff, 0xe2, 0x26, 0x3b, 0x1c, 0x1, 0x52, 0x4f, 0x68, 0x75, 0xce, 0xd3, 0xf4, 0xe9, 0xba, 0xa7, 0x80, 0x9d, 0xeb, 0xf6, 0xd1, 0xcc, 0x9f, 0x82, 0xa5, 0xb8, 0x3, 0x1e, 0x39, 0x24, 0x77, 0x6a, 0x4d, 0x50, 0xa1, 0xbc, 0x9b, 0x86, 0xd5, 0xc8, 0xef, 0xf2, 0x49, 0x54, 0x73, 0x6e, 0x3d, 0x20, 0x7, 0x1a, 0x6c, 0x71, 0x56, 0x4b, 0x18, 0x5, 0x22, 0x3f, 0x84, 0x99, 0xbe, 0xa3, 0xf0, 0xed, 0xca, 0xd7, 0x35, 0x28, 0xf, 0x12, 0x41, 0x5c, 0x7b, 0x66, 0xdd, 0xc0, 0xe7, 0xfa, 0xa9, 0xb4, 0x93, 0x8e, 0xf8, 0xe5, 0xc2, 0xdf, 0x8c, 0x91, 0xb6, 0xab, 0x10, 0xd, 0x2a, 0x37, 0x64, 0x79, 0x5e, 0x43, 0xb2, 0xaf, 0x88, 0x95, 0xc6, 0xdb, 0xfc, 0xe1, 0x5a, 0x47, 0x60, 0x7d, 0x2e, 0x33, 0x14, 0x9, 0x7f, 0x62, 0x45, 0x58, 0xb, 0x16, 0x31, 0x2c, 0x97, 0x8a, 0xad, 0xb0, 0xe3, 0xfe, 0xd9, 0xc4}; #endif
/* @req SWS_BSW_00212 NULL pointer checking */ if (Crc_DataPtr != NULL_PTR) {
crc = (TRUE == Crc_IsFirstCall) ? Crc_8_StartValue : (Crc_StartValue8 ^ Crc_8_Xor);
#if Crc_8_Mode == CRC_8_RUNTIME crc = calculateCRC8(Crc_DataPtr, Crc_Length, crc, Crc_8_Polynomial); #elif Crc_8_Mode == CRC_8_TABLE for (uint32 byte = 0; byte < Crc_Length; byte++) { crc = Crc_8_Tab[crc ^ *Crc_DataPtr]; Crc_DataPtr++; } #endif
/* Only XOR value if any calculation was done */ crc = crc ^ Crc_8_Xor; }
return crc; }
|
|