4.9 CanTp——ISO 15765-2的实现
1747行的帧拆解机——从CAN报文到UDS报文
打开CanTp.c的第1行。你面前不是一篇代码,而是一条流水线——CAN控制器每收到一个8字节的报文,就把它送进这条流水线,经过帧类型判别、状态机路由、缓冲区组装三个工位,最终产出一个完整的、连续的UDS诊断报文,交给PduR——再从PduR进入DCM的诊断世界。
但这条流水线有个隐藏的设计哲学:它不知道UDS的存在。对CanTp而言,它处理的只是“长度超过CAN一帧承载上限的连续数据块“。至于这些数据块是UDS请求还是OBD报文,CanTp概不关心。
为什么CanTp要独立于DCM? 因为ISO 15765-2是一个通用传输协议——不仅UDS用它,SAE J1939的诊断、网络管理、甚至某些标定系统也用。AUTOSAR把它抽出来作为独立的通信服务层,让上层(PduR→DCM)只需面对PduIdType——一个抽象的ID,而不需要知道下面的CAN帧是怎么拼起来的。
PCI字节——帧类型的判据
在CAN总线上,每帧最多8个字节(经典CAN)或64个字节(CAN FD)。那些超过8字节的UDS报文怎么传?ISO 15765-2的答案:拆成多帧,用第一个字节的bit 7-4编码帧类型。
#define ISO15765_TPCI_MASK 0x30
#define ISO15765_TPCI_SF 0x00 /* Single Frame */
#define ISO15765_TPCI_FF 0x10 /* First Frame */
#define ISO15765_TPCI_CF 0x20 /* Consecutive Frame */
#define ISO15765_TPCI_FC 0x30 /* Flow Control */
这0x30的掩码就像一个快速血检——取出第一个字节的高4位,立刻知道这个帧属于哪种类型:
| PCI值 | 帧类型 | 含义 | 携带的数据量 |
|---|---|---|---|
| 0x00 | Single Frame (SF) | 一帧足矣,不用拆 | 最多7字节(标准寻址) |
| 0x10 | First Frame (FF) | 第一帧,后续还有 | 6字节数据 + 2字节总长度 |
| 0x20 | Consecutive Frame (CF) | 跟进帧 | 最多7字节(标准寻址) |
| 0x30 | Flow Control (FC) | 流控帧 | 控制信息,3字节 |
CanTp的getFrameType函数就是这个血检的执行者:
static ISO15765FrameType getFrameType(
const CanTp_AddressingFormantType *formatType,
const PduInfoType *CanTpRxPduPtr) {
ISO15765FrameType res = INVALID_FRAME;
uint8 tpci = 0;
boolean validFrameType = TRUE;
switch (*formatType) {
case CANTP_STANDARD:
tpci = CanTpRxPduPtr->SduDataPtr[0];
break;
case CANTP_EXTENDED:
tpci = CanTpRxPduPtr->SduDataPtr[1];
break;
default:
validFrameType = FALSE;
break;
}
if (validFrameType) {
switch (tpci & ISO15765_TPCI_MASK) {
case ISO15765_TPCI_SF:
res = SINGLE_FRAME;
break;
case ISO15765_TPCI_FF:
res = FIRST_FRAME;
break;
case ISO15765_TPCI_CF:
res = CONSECUTIVE_FRAME;
break;
case ISO15765_TPCI_FC:
res = FLOW_CONTROL_FRAME;
break;
default:
break;
}
}
return res;
}
为什么要把CANTP_STANDARD和CANTP_EXTENDED分开? 因为扩展寻址模式在第一个字节放目标地址(TA),PCI信息在第二个字节。如果不区分,你会把目标地址当成帧类型码来解析——那就像把快递地址当货物清单来看,全乱套了。
帧长度解码——SF的4-bit长度 vs FF的12-bit长度
单帧只能携带0-7字节的有效载荷,这个长度编码在PCI字节的低4位(SF_DL)。但首帧可能携带4095字节(12-bit),需要从PCI字节的低4位(高4位)加第二个字节的8位(低8位)拼接而成。getPduLength函数处理了这个差异:
static PduLengthType getPduLength(
const CanTp_AddressingFormantType *formatType,
const ISO15765FrameType iso15765Frame,
const PduInfoType *CanTpRxPduPtr) {
PduLengthType res = 0;
switch (iso15765Frame) {
case SINGLE_FRAME:
if(CanTpRxPduPtr->SduLength > MAX_SEGMENT_DATA_SIZE){
// CAN FD: 单帧长度在第二个字节
res = CanTpRxPduPtr->SduDataPtr[tpci_offset+1] & ISO15765_TPCI_DL_FD;
}else{
// 经典CAN: 单帧长度在第一个字节的低4位
res = CanTpRxPduPtr->SduDataPtr[tpci_offset] & ISO15765_TPCI_DL;
}
break;
case FIRST_FRAME:
// 12-bit总长度:(PCI低4位 << 8) + 第二字节
res = CanTpRxPduPtr->SduDataPtr[tpci_offset + 1]
+ ((PduLengthType)((CanTpRxPduPtr->SduDataPtr[tpci_offset]) & 0xf) << 8);
break;
}
return res;
}
核心洞察:Single Frame使用4-bit长度(0-7字节范围),但Frame N_PCI的第一个nybble已经足够。First Frame需要12-bit长度(覆盖ISO 15765-2的4095字节上限),拆成4-bit高+8-bit低拼接。这不是设计缺陷——这是CAN每帧只有8字节约束下的最优编码密度。
状态机全貌——从UNINITIALIZED到IDLE
CanTp的整个生命周期由ISO15765TransferStateTypes枚举定义:
typedef enum {
UNINITIALIZED,
IDLE,
SF_OR_FF_RECEIVED_WAITING_PDUR_BUFFER,
RX_WAIT_CONSECUTIVE_FRAME,
RX_WAIT_SF_SDU_BUFFER,
RX_WAIT_CF_SDU_BUFFER,
TX_WAIT_STMIN,
TX_WAIT_TRANSMIT,
TX_WAIT_FLOW_CONTROL,
TX_WAIT_TX_CONFIRMATION
} ISO15765TransferStateTypes;
这是接收方向和发送方向的双状态机,但它们共享同一个枚举。让我们画出接收端的状态机:
UNINITIALIZED
│ CanTp_Init()
▼
IDLE ──────────────────────────────────────┐
│ CanIf_RxIndication → SF │
▼ │
SF_OR_FF_RECEIVED_WAITING_PDUR_BUFFER │
│ PduR_StartOfReception → BUFREQ_OK │ 传输完成
│ → 直接交付PduR_RxIndication │
▼ │
RX_WAIT_CONSECUTIVE_FRAME │
│ CanIf_RxIndication → CF │
├──→ 每帧copySegmentToPduRRxBuffer │
├──→ BS计数到0 → sendFlowControlFrame │
├──→ 最后一帧 → PduR_RxIndication ────┘
│
├──→ 缓冲区满 → RX_WAIT_CF_SDU_BUFFER
│ │ 等待上层缓冲区空闲
│ └──→ 缓冲区可用 → RX_WAIT_CONSECUTIVE_FRAME
│
RX_WAIT_SF_SDU_BUFFER
│ PduR缓冲区忙,SF数据暂存本地
└──→ 缓冲区可用 → IDLE → PduR_RxIndication
发送端更简单:
IDLE
│ CanTp_Transmit()
▼
TX_WAIT_TRANSMIT
│ sendNextTxFrame → CanIf_Transmit
▼
TX_WAIT_TX_CONFIRMATION
│ CanIf_TxConfirmation
├──→ 还有帧 → TX_WAIT_STMIN (延时) → TX_WAIT_TRANSMIT
├──→ BS计数到0 → TX_WAIT_FLOW_CONTROL → handleFlowControlFrame
└──→ 传输完成 → IDLE → PduR_TxConfirmation
为什么Send接收FC的状态叫TX_WAIT_FLOW_CONTROL,而不是TX_WAIT_FC? 这是AUTOSAR的命名规范——宁可长但要含义明确。TX_WAIT_FLOW_CONTROL让人一看就知道是在等待流量控制帧,而不是别的什么。
Flow Control帧的构建——“我还能收多少
当接收端收到FF后,它向发送端回应一个Flow Control帧,告知:
- Flow State(FS):0=CTS继续发送、1=WAIT等待、2=OVFLW溢出
- Block Size(BS):一次最多连续发送多少帧CF再等下一个FC
- Separation Time(STmin):连续两帧CF之间的最小间隔
static void sendFlowControlFrame(
const CanTp_RxNSduType *rxConfig,
CanTp_SimplexChannelPrivateType *rxRuntime,
BufReq_ReturnType flowStatus) {
PduInfoType pduInfo;
uint8 sduData[8]; // 栈上声明8字节缓冲区
pduInfo.SduDataPtr = &sduData[0];
if (rxConfig->CanTpAddressingFormant == CANTP_EXTENDED) {
sduData[indexCount++] = rxRuntime->iso15765.extendedAddress;
}
switch (flowStatus) {
case BUFREQ_OK:
sduData[indexCount++] = ISO15765_TPCI_FC | ISO15765_FLOW_CONTROL_STATUS_CTS;
// 计算Block Size
if (rxConfig->CanTpAddressingFormant == CANTP_EXTENDED) {
computedBs = (rxRuntime->sizeBuffer / MAX_PAYLOAD_SF_EXT_ADDR) + 1;
} else {
computedBs = (rxRuntime->sizeBuffer / MAX_PAYLOAD_SF_STD_ADDR) + 1;
}
if (computedBs > rxConfig->CanTpBs) {
computedBs = rxConfig->CanTpBs;
}
sduData[indexCount++] = rxRuntime->iso15765.BS;
sduData[indexCount++] = (uint8) rxConfig->CanTpSTmin;
break;
case BUFREQ_E_BUSY:
sduData[indexCount++] = ISO15765_TPCI_FC | ISO15765_FLOW_CONTROL_STATUS_WAIT;
break;
case BUFREQ_E_OVFL:
sduData[indexCount++] = ISO15765_TPCI_FC | ISO15765_FLOW_CONTROL_STATUS_OVFLW;
break;
}
canReceivePaddingHelper(rxConfig, rxRuntime, &pduInfo);
}
BS的计算很有意思:它用sizeBuffer(PduR上层缓冲区剩余容量)除以每帧有效载荷字节数,得到还能接收的帧数。如果算出来的值超过了配置的CanTpBs上限,就取上限——这是一种流量自限机制,防止上层缓冲区被淹没。
STmin——发送端的时间纪律
STmin(Separation Time minimum)是接收端要求的连续两帧CF之间的最小间隔。CanTp在发送端这样处理:
if (txRuntime->iso15765.STmin == 0) {
// STmin=0,立即发送下一帧!
resp = sendNextTxFrame(txConfig, txRuntime);
} else {
// 需要等待STmin后再发送
if (txRuntime->iso15765.STmin < 0x80) {
// 0x01-0x7F: 毫秒值
txRuntime->iso15765.stateTimeoutCount =
ConvertMsToMainCycles(txRuntime->iso15765.STmin) + 1;
} else if (txRuntime->iso15765.STmin > 0xF0 && txRuntime->iso15765.STmin < 0xFA) {
// 0xF1-0xF9: 100微秒步进
txRuntime->iso15765.stateTimeoutCount = 1;
} else {
// 0x80-0xF0: 保留,用127ms兜底
txRuntime->iso15765.stateTimeoutCount =
ConvertMsToMainCycles(0x7F) + 1;
}
txRuntime->iso15765.state = TX_WAIT_STMIN;
}
ConvertMsToMainCycles把毫秒转换成MainFunction的周期数:
static uint32 ConvertMsToMainCycles(uint32 ms) {
return (ms/CanTp_ConfigPtr->CanTpGeneral->main_function_period);
}
假设MainFunction周期是5ms,STmin=10ms,就是2个周期后发送下一帧。
与PduR的通信——三个核心回调
CanTp与上层PduR之间的接口只有三个回调:
接收方向:
// 1. 告知PduR:新的接收开始了,总长度是多少
ret = PduR_CanTpStartOfReception(rxConfig->PduR_PduId,
rxRuntime->transferTotal, &rxRuntime->sizeBuffer);
// 2. 把收到的数据拷贝到PduR分配的缓冲区
ret = PduR_CanTpCopyRxData(rxConfig->PduR_PduId, &tempPdu,
&rxRuntime->sizeBuffer);
// 3. 接收完成,通知PduR
PduR_CanTpRxIndication(rxConfig->PduR_PduId, NTFRSLT_OK);
发送方向:
// 1. 从PduR的上层缓冲区拷贝待发送的数据
ret = PduR_CanTpCopyTxData(txConfig->PduR_PduId, &pduInfo,
NULL_PTR, &txRuntime->sizeBuffer);
// 2. 通知PduR发送结果
PduR_CanTpTxConfirmation(txConfig->PduR_PduId, NTFRSLT_OK);
为什么PduR要参与缓冲区管理? 因为AUTOSAR的设计原则是:传输层(CanTp)只负责帧的拆解和组装,不负责数据的最终存储位置。上层(PduR→DCM)决定数据放在哪里、用多大缓冲区——这就是经典的分层解耦。
CanTp_MainFunction——每一毫秒的巡回查房
void CanTp_MainFunction(void)
{
if( CanTpRunTimeData.internalState != CANTP_ON ) {
return;
}
for( uint32 i=0; i < CANTP_MAX_NO_CHANNELS; i++ ) {
// 检查每个通道的TX状态
if (pduTxId != INVALID_PDU_ID) {
txSduStateMachine(txConfigListItem, txRuntimeListItem);
}
// 检查每个通道的RX状态
if (pduRxId != INVALID_PDU_ID) {
rxPhySduStateMachine(rxConfigListItem, rxRuntimeListItem);
}
// 检查功能寻址接收
if (pduRxFuncId != INVALID_PDU_ID) {
rxFncSduStateMachine(rxConfigListItem, rxRuntimeListItem);
}
}
}
这里有一个微妙的细节:物理寻址和功能寻址的接收使用了不同的状态机函数——rxPhySduStateMachine处理所有的CF超时、缓冲区等待,而rxFncSduStateMachine只处理RX_WAIT_SF_SDU_BUFFER状态(因为功能寻址只接收单帧)。这种精简化避免了功能寻址通道上不必要的超时检查。
RxIndication——当CAN报文到达时
CanIf每收到一个CAN报文,调用CanTp_RxIndication,把报文交给CanTp处理:
void CanTp_RxIndication(PduIdType CanTpRxPduId,
PduInfoType *CanTpRxPduPtr)
{
frameType = getFrameType(addressingFormat, CanTpRxPduPtr);
switch (frameType) {
case SINGLE_FRAME:
handleSingleFrame(rxConfigParams, runtimeParams,
CanTpRxPduPtr, CanTpRxNSduId);
break;
case FIRST_FRAME:
handleFirstFrame(rxConfigParams, runtimeParams,
CanTpRxPduPtr, CanTpRxNSduId);
break;
case CONSECUTIVE_FRAME:
handleConsecutiveFrame(rxConfigParams, runtimeParams,
CanTpRxPduPtr);
break;
case FLOW_CONTROL_FRAME:
handleFlowControlFrame(txConfigParams, runtimeParams,
CanTpRxPduPtr);
break;
}
}
四条分支对应四种帧类型。但要注意:Flow Control帧被路由到发送端的处理函数——因为FC是发送端正在等待的回应。这就像一个精明的快递分拣员把收到的“OK继续发“确认件投递到发货窗口,而不是收货窗口。
CanTp_Init——开机复位
void CanTp_Init( const CanTp_ConfigType* CfgPtr)
{
CanTp_ConfigPtr = CfgPtr;
for (uint8 i=0; i < CANTP_MAX_NO_CHANNELS; i++) {
initTx15765RuntimeData(
&CanTpRunTimeData.runtimeDataList[i].SimplexChnlList[CANTP_TX_CHANNEL]);
initRx15765RuntimeData(
&CanTpRunTimeData.runtimeDataList[i].SimplexChnlList[CANTP_RX_CHANNEL]);
initRx15765RuntimeData(
&CanTpRunTimeData.runtimeDataList[i].functionalChnl);
}
CanTpRunTimeData.internalState = CANTP_ON;
}
每个通道有三个单工信道:TX物理、RX物理、RX功能。全部复位到IDLE状态——就像一个医院早上清空所有病房,准备接收新病人。
定时参数——N_As/N_Ar/N_Bs/N_Br/N_Cs/N_Cr
CanTp定义了6个关键定时参数,在CanTp_SimplexChannelPrivateType中通过stateTimeoutCount和NasNarTimeoutCount实现:
| 参数 | 含义 | 实现位置 |
|---|---|---|
| N_As | 发送方等待CAN总线确认的超时 | TX_WAIT_TX_CONFIRMATION状态下递减 |
| N_Ar | 接收方等待CAN总线确认的超时 | 通过NasNarPending标志控制 |
| N_Bs | 发送方等待Flow Control的超时 | TX_WAIT_FLOW_CONTROL状态下递减 |
| N_Br | 接收方等待向PduR请求缓冲区的超时 | RX_WAIT_CF_SDU_BUFFER状态下递减 |
| N_Cs | 发送方等待连续帧发送的超时 | TX_WAIT_TRANSMIT状态下递减 |
| N_Cr | 接收方等待连续帧接收的超时 | RX_WAIT_CONSECUTIVE_FRAME状态下递减 |
每个定时器都是递减计数器,单位是MainFunction周期。当计数器归零时,触发超时错误,通过PduR_CanTpRxIndication或PduR_CanTpTxConfirmation报告失败。
本篇小结
- CanTp通过
getFrameType函数解码PCI字节(bit 7-4),区分SF/FF/CF/FC四种帧类型,是ISO 15765-2的帧级路由器。 - 状态机覆盖接收和发送两个方向共10个状态,用
ISO15765TransferStateTypes统一管理,每个MainFunction周期推进一次。 - Flow Control帧由接收端根据PduR缓冲区容量动态构建,BS取配置上限与实际容量的较小值,实现流量自限。
- STmin的编码用了三段区间:0直接发送、1-127毫秒、241-249百微秒,体现了CAN时间精度(毫秒级)的可应用范围。
- 与PduR通过
StartOfReception/CopyRxData/RxIndication三回调解耦,CanTp不管理数据存储位置。 - 六种超时参数(N_As/N_Ar/N_Bs/N_Br/N_Cs/N_Cr)覆盖了通信的所有等待点,防止无限阻塞。
- 物理寻址和功能寻址的接收状态机分离,功能通道只处理单帧,节省CPU。
【下集预告】:硬件传输层看完了——现在看软件架构的基石。DCM的所有行为都依赖于一套静态配置表:DspDid[]定义每个DID的数据端口和回调,DsdServiceTable[]定义每个SID的路由。这些const数组在编译期锁定,在运行期不可变——这是AUTOSAR配置艺术的核心。