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.5 DSP——UDS服务实现全景

6492行的服务处理器——从Init到Main

打开Dcm_Dsp.c的第1行,光标游走在6492行代码的边缘。你看到的不是一堆杂乱的服务函数,而是一座井然有序的医院科室楼——每一个UDS SID都是一间独立的诊室,每间诊室里有一位医生(DspUdsXxx()函数),有自己的医疗设备(静态全局状态变量),有自己的病历记录方式(pending状态机)。而走廊上,一个名叫DspInit的院长每天早上巡查所有诊室,确保设备归零、病历归档。

但这座医院有一个严格的规定:不允许动态申请诊室。所有的房间都是预建的——用static关键字钉死在编译期分配的内存里。你不禁要问:为什么?

因为在嵌入式系统的世界里,malloc是一把双刃剑——它带来了灵活性,也带来了碎片化和不可预测的分配时间。一辆行驶中的汽车,ECU必须在可预测的时间窗口内完成诊断响应。如果DSP在高速行驶时突然需要分配一块内存而遭遇碎片化延迟,后果不堪设想。所以,一切都得是静态的——这不是固执,而是安全的底牌。


DspInit —— 复位所有服务状态机

想象医院早晨8点,护士长走过每一间诊室,逐一检查设备:

void DspInit(boolean firstCall) {
    uint8 i;

    // 复位安全访问状态
    dspUdsSecurityAccessData.reqInProgress = FALSE;
    for (i = 0; i < DCM_CFG_NUM_OF_SECURITY_LEVELS; i++) {
        dspUdsSecurityAccessData.secFalseAttemptChk[i].startDelayTimer
            = DELAY_TIMER_DEACTIVE;
    }

    // 复位ECU复位状态
    dspUdsEcuResetData.resetPending = DCM_DSP_RESET_NO_RESET;

    // 复位上传/下载传输状态
    TransferStatus.transferType = DCM_NO_DATA_TRANSFER;
    TransferStatus.blockSequenceCounter = 0;

    // 复位DTC设置控制
    DspDTCSetting.settingDisabled = FALSE;

    // 复位周期性DID
    for (i = 0; i < DCM_CFG_NUM_OF_PERIODICDID_IDENTIFIER; i++) {
        DspPdIdTable[i].enabled = FALSE;
    }

    // 复位动态DID缓冲区
    for (i = 0; i < DCM_CFG_NUM_OF_DYNAMIC_DEFINED_DATA_IDENTIFIER; i++) {
        dspDDD[i].defined = FALSE;
    }

    // 复位IO控制状态
    for (i = 0; i < DCM_CFG_NUM_OF_IOCONTROL_DID_IDENTIFIER; i++) {
        IOControlStateList[i].active = FALSE;
    }

    // 复位通信控制通道
    for (i = 0; i < DCM_CFG_NUMBER_OF_COMM_CHANNEL; i++) {
        DspComControlChannel[i].active = FALSE;
    }
}

每一行复位都对应着一个具体的临床场景:

静态变量所属服务复位值场景含义
dspUdsSecurityAccessData0x27 安全访问reqInProgress=FALSE取消所有进行中的安全验证
dspUdsEcuResetData0x11 ECU复位DCM_DSP_RESET_NO_RESET没有待执行的复位
TransferStatus0x34-0x37 上传下载DCM_NO_DATA_TRANSFER没有进行中的数据传输
DspDTCSetting0x85 控制DTC设置settingDisabled=FALSEDTC记录没有被关闭
DspPdIdTable[]0x2A 周期性DID读取enabled=FALSE所有周期性读取暂停
dspDDD[]0x2C 动态定义DIDdefined=FALSE所有动态DID被清除
IOControlStateList[]0x2F IO控制active=FALSE所有IO控制释放回ECU
DspComControlChannel[]0x28 通信控制active=FALSE所有通信通道恢复正常

核心洞察:DSP的所有服务状态都是静态全局变量——没有动态分配。这意味着每一条UDS服务都是一个状态机,其当前状态存储在全局数组中,在后续的MainFunction中基于前一次的状态继续推进。你可以把它理解为医院的“住院部“——有的病人(服务请求)需要在医院里待好几个MainFunction周期才能“治愈“(返回最终响应)。

让我们来看看这些全局变量的实际定义。在DSP的第296行开始,你看到一排齐整的static声明:

static DspUdsEcuResetDataType dspUdsEcuResetData;
static DspUdsSessionControlDataType dspUdsSessionControlData;
static DspUdsReadDidPendingType dspUdsReadDidPending;
static DspUdsGeneralPendingType dspUdsWriteDidPending;
static DspUdsGeneralPendingType dspUdsRoutineControlPending;
static DspUdsGeneralPendingType dspUdsSecurityAccessPending;
static DspUdsGeneralPendingType dspUdsUploadDownloadPending;
static DspUdsCommunicationControlDataType communicationControlData;
static DspUdsSecurityAccessDataType dspUdsSecurityAccessData;

每一个结构体都是一个“病历夹“,里面记录着该服务当前的“病情状态“。以DspUdsReadDidPendingType为例:

typedef struct {
    ReadDidPendingStateType state;
    const PduInfoType* pduRxData;
    PduInfoType* pduTxData;
    uint16 txWritePos;
    uint16 nofReadDids;
    uint16 reqDidIndex;
    uint16 pendingDid;
    uint16 pendingDataLength;
    uint16 pendingSignalIndex;
    uint16 pendingDataStartPos;
} DspUdsReadDidPendingType;

看到了吗?pendingSignalIndex——这就是断点续传的“行号“!当NvM的回调在下一次MainFunction才返回时,DSP就从这个索引处继续往响应缓冲区填数据。


异步服务模型——E_PENDING的魔力

DSP里的很多服务是异步的。NvM写块可能需要几毫秒,安全访问的CompareKey可能要走加密引擎——这些不能在一个MainFunction周期内完成。AUTOSAR的答案是opStatus回调参数:

// DID读取——异步版本的核心逻辑
void getDidData(...) {
    for (each signal in DID) {
        if (signal->portType == DATA_PORT_ASYNCH) {
            Dcm_OpStatusType opStatus;
            signal->AsynchDataReadFnc(signal->data, &opStatus);

            if (opStatus == DCM_READ_PENDING) {
                // 存储当前位置——下次MainFunction从这里继续
                pendingState->signalIndex = currentSignal;
                pendingState->dataLen = currentDataPos;
                return;  // 返回E_PENDING状态
            }
        }
    }
}

这个机制的精妙之处在于opStatus的双向通信:DSP传入DCM_INITIAL表示“这是第一次调用,请执行实际操作“,传入DCM_PENDING表示“这是重试,该返回就返回“。而回调函数通过opStatus告诉DSP:“还没好”(DCM_READ_PENDING)或者“好了“(DCM_READ_OK),或者“强制响应挂起“(DCM_FORCE_RCRRP)。

更完整的状态编码来自GeneralPendingStateType

typedef enum {
    DCM_GENERAL_IDLE,
    DCM_GENERAL_PENDING,
    DCM_GENERAL_FORCE_RCRRP_AWAITING_SEND,
    DCM_GENERAL_FORCE_RCRRP,
} GeneralPendingStateType;

而DSD那边的状态模型是另一套(来源Dcm_Dsd.c:54-58):

typedef enum {
    DCM_SERVICE_IDLE,
    DCM_SERVICE_PENDING,
    DCM_SERVICE_WAIT_DONE,
    DCM_SERVICE_DONE,
    DCM_SERVICE_WAIT_TX_CONFIRM
} ServicePendingStateType;

注意这个微妙的关系——DSP用GeneralPendingStateType管理内部异步操作的断点推进,而DSD用ServicePendingStateType管理外部服务调用的生命周期。DSP说“我还在忙“,DSD就挂起响应发送;DSP说“我做完了“,DSD从状态切换到DCM_SERVICE_DONE然后触发正面响应的组装和发送。

为什么要把DSP和DSD的状态机分开? 因为DSP关心的是“数据的读取/写入/验证是否完成“,DSD关心的是“这个请求的整体生命周期到了哪一步(受理→处理中→响应组装→发送确认)“。这是经典的关注点分离。


DspMain —— 推进所有异步状态机

void DspMain(void) {
    DspResetMainFunction();          // ECU复位 pending
    DspMemoryMainFunction();         // 内存读写 pending
    DspReadDidMainFunction();        // DID读取 pending
    DspRoutineControlMainFunction(); // 例行控制 pending
    DspUploadDownloadMainFunction(); // 上传下载 pending
    DspSecurityAccessMainFunction(); // 安全访问 pending
    DspJumpToBootMainFunction();     // Boot跳转 pending
    DspStartupServiceResponseMainFunction(); // 启动响应
    DspIOControlMainFunction();      // IO控制 pending
}

为什么是这个顺序? 这不是随意排列的——有四个原因:

  1. ECU复位优先:如果复位处于pending状态,先处理它。因为复位会改变ECU的运行模式,影响其他所有服务的有效性。这就像急救室优先处理心搏骤停的病人。DspResetMainFunction在前面是为了尽快完成复位请求,减少其他服务在“即将被复位“的ECU上浪费时间。

  2. 内存操作随后DspMemoryMainFunction处理内存的异步读写(通过NvM块)。如果内存读写失败,很多后续服务(如DID读取可能依赖NvM数据)会受到影响。

  3. 高优先级数据服务居中:DID读取和例行控制是比上传下载更轻量但更频繁的异步操作,放在中间位置以均衡地处理。

  4. 安全访问与Boot跳转靠后:安全访问和Boot跳转是“终结性“操作——安全访问完成后会话锁定,Boot跳转后程序失去控制权。放在后面处理,给前面的轻量级服务让出优先权。

核心设计原则是:每一个MainFunction处理一个特定的异步服务。如果一个服务没有pending状态——它的MainFunction直接返回,不消耗任何有效CPU时间。 这种“无则跳过“的模式避免了在无异步操作时浪费CPU周期。


DsdMain vs DspMain —— 为什么需要两层MainFunction?

待看DSD的Main函数(Dcm_Dsd.c:606):

void DsdMain(void)
{
    if (TRUE == dsdDslDataIndication) {
        dsdDslDataIndication = FALSE;
        DsdHandleRequest();
    }
    externalServiceMainFunction();
}

对比DspMain,你注意到关键差异了吗?DsdMain处理的是“新请求“和“外部服务的推进“,DspMain处理的是“内部异步操作的断点续传“

这背后是DCM架构的分层逻辑:

层次函数职责类比
DSL主循环缓冲区管理、超时监控挂号台:控制人流
DSDDsdMain分发新请求、推进外部服务分诊台:分配到科室
DSPDspMain推进内部异步操作各科室:继续治疗

为什么需要DspPreDsdMain——等等,我们先澄清一个事实:在Arctic Core的开源实现中,实际上并没有显式的DspPreDsdMain函数。这个概念在商业AUTOSAR栈中是存在的,用于在请求被DSD分发之前让DSP有机会预处理请求数据(比如,在0x27安全访问中在分发给子功能之前先检查安全级别)。Arctic Core通过在DSD的DsdHandleRequest内部检查Session和Security Level来达到相同的效果。


runInternalService —— 巨大switch-case的调兵遣将

当DSD确认“SID合法、session允许、security level足够“之后,它调用runInternalService来真正执行服务代码。你打开Dcm_Dsd.c:220,看到的是一个巨型switch-case:

static void runInternalService(uint8 SID)
{
    switch(SID) {
#ifdef DCM_USE_SERVICE_DIAGNOSTICSESSIONCONTROL
        case SID_DIAGNOSTIC_SESSION_CONTROL:
            DspUdsDiagnosticSessionControl(msgData.pduRxData, msgData.pdurTxPduId,
                msgData.pduTxData, msgData.sendRespPendOnTransToBoot,
                msgData.internalRequest);
            break;
#endif
#ifdef DCM_USE_SERVICE_LINKCONTROL
        case SID_LINK_CONTROL:
            DspUdsLinkControl(msgData.pduRxData, msgData.pdurTxPduId,
                msgData.pduTxData, msgData.sendRespPendOnTransToBoot);
            break;
#endif
#ifdef DCM_USE_SERVICE_ECURESET
        case SID_ECU_RESET:
            DspUdsEcuReset(msgData.pduRxData, msgData.pdurTxPduId,
                msgData.pduTxData, msgData.startupResponseRequest);
            break;
#endif
        case SID_CLEAR_DIAGNOSTIC_INFORMATION:
            DspUdsClearDiagnosticInformation(msgData.pduRxData, msgData.pduTxData);
            break;
        case SID_READ_DTC_INFORMATION:
            DspUdsReadDtcInformation(msgData.pduRxData, msgData.pduTxData);
            break;
        case SID_READ_DATA_BY_IDENTIFIER:
            DspUdsReadDataByIdentifier(msgData.pduRxData, msgData.pduTxData);
            break;
        case SID_READ_MEMORY_BY_ADDRESS:
            DspUdsReadMemoryByAddress(msgData.pduRxData, msgData.pduTxData);
            break;
        case SID_WRITE_MEMORY_BY_ADDRESS:
            DspUdsWriteMemoryByAddress(msgData.pduRxData, msgData.pduTxData);
            break;
        case SID_READ_SCALING_DATA_BY_IDENTIFIER:
            DspUdsReadScalingDataByIdentifier(msgData.pduRxData, msgData.pduTxData);
            break;
        case SID_SECURITY_ACCESS:
            DspUdsSecurityAccess(msgData.pduRxData, msgData.pduTxData);
            break;
        case SID_WRITE_DATA_BY_IDENTIFIER:
            DspUdsWriteDataByIdentifier(msgData.pduRxData, msgData.pduTxData);
            break;
        case SID_ROUTINE_CONTROL:
            DspUdsRoutineControl(msgData.pduRxData, msgData.pduTxData);
            break;
        case SID_TESTER_PRESENT:
            DspUdsTesterPresent(msgData.pduRxData, msgData.pduTxData);
            break;
        case SID_CONTROL_DTC_SETTING:
            DspUdsControlDtcSetting(msgData.pduRxData, msgData.pduTxData);
            break;
        case SID_RESPONSE_ON_EVENT:
            DspResponseOnEvent(msgData.pduRxData, msgData.rxPduId, msgData.pduTxData);
            break;
        case SID_READ_DATA_BY_PERIODIC_IDENTIFIER:
            DspReadDataByPeriodicIdentifier(msgData.pduRxData, msgData.pduTxData,
                msgData.rxPduId, msgData.txType, msgData.internalRequest);
            break;
        case SID_DYNAMICALLY_DEFINE_DATA_IDENTIFIER:
            DspDynamicallyDefineDataIdentifier(msgData.pduRxData, msgData.pduTxData);
            break;
        case SID_INPUT_OUTPUT_CONTROL_BY_IDENTIFIER:
            DspIOControlByDataIdentifier(msgData.pduRxData, msgData.pduTxData);
            break;
        case SID_COMMUNICATION_CONTROL:
            DspCommunicationControl(msgData.pduRxData, msgData.pduTxData,
                msgData.rxPduId, msgData.pdurTxPduId);
            break;
        case SID_REQUEST_DOWNLOAD:
            DspUdsRequestDownload(msgData.pduRxData, msgData.pduTxData);
            break;
        case SID_REQUEST_UPLOAD:
            DspUdsRequestUpload(msgData.pduRxData, msgData.pduTxData);
            break;
        case SID_TRANSFER_DATA:
            DspUdsTransferData(msgData.pduRxData, msgData.pduTxData);
            break;
        case SID_REQUEST_TRANSFER_EXIT:
            DspUdsRequestTransferExit(msgData.pduRxData, msgData.pduTxData);
            break;
        /* OBD services */
        case SID_REQUEST_CURRENT_POWERTRAIN_DIAGNOSTIC_DATA:
            DspObdRequestCurrentPowertrainDiagnosticData(...);
            break;
        // ... 更多OBD服务 ...
        default:
            createAndSendNcr(DCM_E_SERVICENOTSUPPORTED);
            break;
    }
}

这大约30个case覆盖了ISO 14229-1中定义的核心UDS服务,外加OBD-II服务(0x01-0x0A)。每个case都经过#ifdef条件编译——只有在编译时配置了该服务才会被包含,未配置的代码完全不存在于二进制中


DSD与DSP的协作关系

最后,让我们梳理一下DSD分发和DSP执行之间的完整关系:

  1. DSD是前台:接收到来自DSL的请求后,DSD通过lookupSidDsdServiceTable[]中查找SID配置。找到后检查session和security level。

  2. DSD决定大小:如果service配置了DspSidTabFnc(外部服务回调),DSD调用startExternalServiceProcessing,由外部SW-C处理。否则走selectServiceFunctionrunInternalService路径,这就是DSP的内部分支。

  3. DSP是后台DspMain的9个子函数不管“下一个请求是什么“——它们只关心“是否有pending的异步操作需要推进“。它们不参与请求分发。

  4. DSD也管外部pendingexternalServiceMainFunction在DsdMain中被调用,它推进ExternalService.stateDCM_SERVICE_PENDINGDCM_SERVICE_DONEDCM_SERVICE_WAIT_TX_CONFIRM

核心洞察:DSD像一个分诊台——它决定哪个病人去哪个科室。DSP是各个科室——执行具体的治疗。分诊台不知道心内科具体怎么治心脏病,心内科也不关心还有多少病人在排队。但分诊台有一个“outside-the-hospital“通道(外部服务回调),可以让病人转到外面的专科医院(其他SW-C)治疗。


本篇小结

  1. DspInit复位所有22个服务的内置状态机——全部使用静态全局数组而非动态分配,这是嵌入式系统对实时性和内存碎片化的防御。
  2. 异步服务通过opStatus + E_PENDING机制实现跨MainFunction周期的断点续传——DSP状态机记录中断位置,DSD状态机管理服务生命周期。
  3. DspMain顺序调用9个异步推进函数,顺序按“终结性“程度排列:复位→内存→数据→安全→跳转。每个函数只在对应服务有pending状态时才消耗CPU。
  4. DsdMain处理新请求分发和外部服务推进,DspMain处理内部异步断点续传——两者分工明确,前者是分诊台,后者是各科室。
  5. runInternalService中约30个case覆盖了UDS和OBD的核心SID,全部由#ifdef条件编译控制,未配置的服务代码在二进制中不存在。
  6. 6492行中大约60%是具体的DspUdsXxx处理函数,20%是DID和信号读写基础设施,20%是状态机和辅助函数。

【下集预告】:框架看完了——现在我们深入第一个具体服务——0x10诊断会话控制和0x27安全访问。这两个服务是DSP里状态机最复杂的——会话切换涉及JumpToBoot的跳转器,安全访问涉及防爆破延迟定时器。我们逐行拆。