Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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值帧类型含义携带的数据量
0x00Single Frame (SF)一帧足矣,不用拆最多7字节(标准寻址)
0x10First Frame (FF)第一帧,后续还有6字节数据 + 2字节总长度
0x20Consecutive Frame (CF)跟进帧最多7字节(标准寻址)
0x30Flow 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_STANDARDCANTP_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帧,告知:

  1. Flow State(FS):0=CTS继续发送、1=WAIT等待、2=OVFLW溢出
  2. Block Size(BS):一次最多连续发送多少帧CF再等下一个FC
  3. 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中通过stateTimeoutCountNasNarTimeoutCount实现:

参数含义实现位置
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_CanTpRxIndicationPduR_CanTpTxConfirmation报告失败。


本篇小结

  1. CanTp通过getFrameType函数解码PCI字节(bit 7-4),区分SF/FF/CF/FC四种帧类型,是ISO 15765-2的帧级路由器。
  2. 状态机覆盖接收和发送两个方向共10个状态,用ISO15765TransferStateTypes统一管理,每个MainFunction周期推进一次。
  3. Flow Control帧由接收端根据PduR缓冲区容量动态构建,BS取配置上限与实际容量的较小值,实现流量自限。
  4. STmin的编码用了三段区间:0直接发送、1-127毫秒、241-249百微秒,体现了CAN时间精度(毫秒级)的可应用范围。
  5. 与PduR通过StartOfReception/CopyRxData/RxIndication三回调解耦,CanTp不管理数据存储位置。
  6. 六种超时参数(N_As/N_Ar/N_Bs/N_Br/N_Cs/N_Cr)覆盖了通信的所有等待点,防止无限阻塞。
  7. 物理寻址和功能寻址的接收状态机分离,功能通道只处理单帧,节省CPU。

【下集预告】:硬件传输层看完了——现在看软件架构的基石。DCM的所有行为都依赖于一套静态配置表:DspDid[]定义每个DID的数据端口和回调,DsdServiceTable[]定义每个SID的路由。这些const数组在编译期锁定,在运行期不可变——这是AUTOSAR配置艺术的核心。