4.3 DSL——会话层的缓冲区管理与时序状态机
从CanTp的PCI字节到DCM的完整消息——这一步在DSL
当CanTp收到最后一帧CF并把26个字节粘合回一条完整UDS消息后,它通过PduR呼叫DslTpRxIndicationFromPduR——这时DSL的角色正式启动。
DSL的1828行代码做了三件事:
- 把来自PduR的SDU数据放进DCM内部的消息缓冲区
- 管理S3会话超时——在MainFunction里每个周期递减计数器
- 把DSP处理完的响应数据从内部缓冲区吐出给PduR→传输层
缓冲区分配——DslStartOfReception
当传输层开始接收新消息时,它必须先从DCM拿到一个写入地址——DslStartOfReception负责这件事:
BufReq_ReturnType DslStartOfReception(
PduIdType dcmRxPduId, PduLengthType tpSduLength,
PduLengthType *rxBufferSizePtr, boolean internalRequest)
{
// 在协议行配置中查找这个Rx PDU ID对应的运行时数据
runtime = findRxPduIdParentConfigurationLeafs(dcmRxPduId);
// 情况1: 外部Rx缓冲区可用?
if (runtime->externalRxBufferStatus == BUFFER_AVAILABLE) {
runtime->externalRxBufferStatus = PROVIDED_TO_PDUR;
*rxBufferSizePtr = runtime->diagnosticRequestFromTester.size;
return BUFREQ_OK;
}
// 情况2: 外部忙——试试内部本地缓冲区
// 仅当: 非内部请求 && 消息长度 <= 8字节 && 本地缓冲区为空
if (!internalRequest && (tpSduLength <= DCM_DSL_LOCAL_BUFFER_LENGTH)) {
*rxBufferSizePtr = DCM_DSL_LOCAL_BUFFER_LENGTH;
return BUFREQ_OK; // PduR 会直接写进 localRxBuffer
}
// 情况3: 全忙
*rxBufferSizePtr = 0;
return BUFREQ_OVFL;
}
双缓冲的设计精髓在于:外部缓冲区处理完整长消息,本地8字节缓冲区专门处理并发的TesterPresent(0x3E)保活。当一个51字节的0x19 DTC查询正在进行时,诊断仪同时发的0x3E心跳不能因为缓冲区满了而丢失——否则S3超时会误触发。
接收到完成——DslTpRxIndicationFromPduR
PduR传完最后一个字节后触发此函数。这里是DSL最密集的逻辑:
void DslTpRxIndicationFromPduR(..., boolean internalRequest, ...) {
// 1. 校验接收长度
if (runtime->nofBytesHandled != tpSduLength) {
releaseExternalRxTxBuffers(runtime);
return; // 长度不匹配,丢弃
}
// 2. TesterPresent特殊处理
if (isTesterPresentCommand(&runtime->diagnosticRequestFromTester)) {
// 只重置S3定时器,不需要经过DSD→DSP
runtime->S3ServerTimeoutCount = S3ServerValue;
releaseExternalRxTxBuffers(runtime);
return;
}
// 3. 如果协议尚未启动,先启动协议
if (!runtime->protocolStarted) {
StartProtocolHelper(protocol);
}
// 4. 协议抢占: 如果已有活动请求在处理中
if (protocolPreempt) {
DcmResetDiagnosticActivity();
// 发送抢占NRC: 0x7F + 原SID + busyRepeatRequest(0x21)
}
// 5. 把缓冲区交给DSD处理
runtime->externalRxBufferStatus = PROVIDED_TO_DSD;
DsdDslDataIndication(/* pduRxData, SIDTable, addrType, ... */);
}
DSL在这里做了一次TesterPresent短路优化——如果收到的消息是0x3E且suppressPosRsp=1,直接重置S3定时器并释放buffer,完全不必让DSD/DSP参与。这个优化在总线上负载最高的刷写场景下尤其重要——每秒可能有几十次0x3E保活需要处理。
S3会话超时——在DslMain中
void DslMain(void) {
// 遍历所有协议行的运行时状态
for (each runtime) {
// S3会话超时倒计
if (runtime->S3ServerStarted) {
runtime->S3ServerTimeoutCount--;
if (runtime->S3ServerTimeoutCount == 0) {
// 超时 → 切回默认会话
changeDiagnosticSession(runtime, DCM_DEFAULT_SESSION);
runtime->S3ServerStarted = FALSE;
// 安全等级自动锁住
DslInternal_SetSecurityLevel(DCM_SEC_LEV_LOCKED);
}
}
}
}
S3在每个MainFunction周期递减1——如果你的MainFunction周期是5ms且S3配置为5000ms——计数器初始值=1000,每个周期减1直到0。
响应回传——DslCopyTxData
当DSP处理完请求、DSD构建好正/负响应、DSL的Tx缓冲区被标记为DCM_TRANSMIT_SIGNALED后,传输层调用DslCopyTxData取走数据:
BufReq_ReturnType DslCopyTxData(PduIdType dcmTxPduId, ...) {
// 从 runtime->diagnosticResponseFromDsd 拷贝到 PduR 提供的缓冲区
memcpy(pduInfoPtr->SduDataPtr,
&runtime->diagnosticResponseFromDsd.data[bytesAlreadySent],
remainingToSend);
runtime->nofBytesHandled += remainingToSend;
*txDataCntPtr = remainingToSend;
return BUFREQ_OK;
}
Rx/Tx缓冲区的六态完整流转
外部Rx缓冲:
BUFFER_AVAILABLE ── DslStartOfReception() ──→ PROVIDED_TO_PDUR
PROVIDED_TO_PDUR ── 传输结束 TpRxIndication ──→ PROVIDED_TO_DSD
PROVIDED_TO_DSD ── 处理完成 release ──→ BUFFER_AVAILABLE
外部Tx缓冲:
NOT_IN_USE ── DsdDslDataIndication ──→ PROVIDED_TO_DSD
PROVIDED_TO_DSD ── DsdDspProcessingDone(DONE) ──→ DSD_PENDING_RESPONSE_SIGNALED
DSD_PENDING_RESPONSE_SIGNALED ── PduR_DcmTransmit ──→ DCM_TRANSMIT_SIGNALED
DCM_TRANSMIT_SIGNALED ── DslCopyTxData ──→ PROVIDED_TO_PDUR
PROVIDED_TO_PDUR ── TxConfirmation ──→ NOT_IN_USE
本篇小结
- DSL的核心是双缓冲机制——外部缓冲处理完整长消息,本地8字节缓冲处理并发的TesterPresent保活。
- TesterPresent在DSL层被短路优化——不经过DSD/DSP,直接重置S3定时器。
- S3会话超时在DslMain每个周期递减一次——归零后自动切回默认会话并锁住安全等级。
- Rx/Tx缓冲的六态流转保证了DCM内部的“一请求一响应“消息信箱模型——前一个请求没处理完,后一个不收。
【下集预告】:DSL把消息喂给了DSD——但DSD怎么知道0x27应该分发给哪个处理函数?怎么保证当前会话、安全等级都允许执行?怎么处理suppressPosRsp位?我们拆开DSD的SID表查找逻辑和鉴权决策树。