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.10 配置系统——DspDid[]与DsdServiceTable[]的静态艺术

编译期凝固的智慧——当ROM替代RAM成为架构师

你打开Dcm_Lcfg.h——807行,每一行都不是逻辑代码,而是结构体定义。这些结构体不执行任何运算,它们只做一件事:定义形状。就像医院建设前的蓝图——不是告诉你哪个病人该吃什么药,而是告诉你这栋楼有几个科室,每个科室有几个诊室,每个诊室配什么设备。

AUTOSAR配置系统的核心理念可以用一句话概括:一切可变性在编译期凝固,运行期只做查表和跳转

在传统的PC程序中,当你要添加一个新DID时,你可能在运行时调用addDid(),传入数据长度、读取回调、安全级别。但在AUTOSAR里,所有这些信息在Dcm_Cfg.c中作为const数组编译进ROM,运行时你只能遍历、查找——不能增删改。

为什么? 因为汽车ECU的Flash比RAM大得多(典型的ZynqMP Cortex-R5上有几MB Flash但对RAM极为吝啬),而且const数组放在Flash中不消耗RAM。更关键的是:静态配置意味着没有运行时内存分配失败的风险——这正是功能安全(ISO 26262)对ASIL等级系统的基本要求。


配置之根——Dcm_ConfigPtr

一切配置的入口只有一个全局指针:

extern const Dcm_ConfigType DCM_Config;
extern const Dcm_ConfigType *Dcm_ConfigPtr;

Dcm_ConfigType是一个“三合一“的顶层容器:

typedef struct {
    const Dcm_DspType *Dsp;       // DSP配置
    const Dcm_DsdType *Dsd;       // DSD配置
    const Dcm_DslType *Dsl;       // DSL配置
    const Dcm_ComMChannelConfigType *DcmComMChannelCfg;
} Dcm_ConfigType;

DCM的三个子模块DSP、DSD、DSL各有一套配置指针,均从这个根部可达。运行时,代码里到处是Dcm_ConfigPtr->Dsl->DslProtocol->DslProtocolRowList这样的“指针链“——一层层向下穿透。

核心洞察Dcm_ConfigPtr是整个DCM配置系统的心脏。它不是一个数据库——它是一个。树根在DCM_Config,树干是Dsp/Dsd/Dsl三个分支,枝叶是每个SID的认证要求、每个DID的数据端口、每个协议的定时参数。这棵树的全部节点都在编译期确定,运行期通过指针遍历——O(1)的指针跳转比任何哈希表都快。


DSD配置——DsdServiceTable[]的SID路由表

DSD的配置核心是Dcm_DsdServiceTableType

typedef struct {
    uint8                       DsdSidTabId;          // 表ID
    const Dcm_DsdServiceType    *DsdService;          // 服务列表
    boolean                     Arc_EOL;
} Dcm_DsdServiceTableType;

而每一个Dcm_DsdServiceType定义了单个SID的全部路由信息:

typedef struct {
    uint8                           DsdSidTabServiceId;              // SID号
    boolean                         DsdSidTabSubfuncAvail;           // 是否有子功能
    const Dcm_DspSecurityRowType    * const *DsdSidTabSecurityLevelRef;  // 安全级别引用
    const Dcm_DspSessionRowType     * const *DsdSidTabSessionLevelRef;   // 会话级别引用
    Dcm_DsdDspSidTabFncType         DspSidTabFnc;                    // 外部服务回调(可选)
    const Dcm_DsdSubServiceType     *const DsdSubServiceList;        // 子功能列表(可选)
    boolean                         Arc_EOL;
} Dcm_DsdServiceType;

这就是DSD执行lookupSid查表时遍历的结构体。每个SID配置了:

  • DsdSidTabSecurityLevelRef:执行此服务需要满足的安全级别(const Dcm_DspSecurityRowType * const *——注意这两层指针:外层是数组指针,内层是配置行的指针)
  • DsdSidTabSessionLevelRef:执行此服务需要在哪个会话下
  • DspSidTabFnc:外部服务回调函数指针——如果非NULL,DSD走外部服务路径
  • DsdSubServiceList:子功能列表,用于如0x31(RoutineControl)这种有子功能的SID

DSD在收到请求后,通过lookupSid遍历DsdServiceTable[]

static boolean lookupSid(uint8 sid, const Dcm_DsdServiceType **sidPtr)
{
    boolean returnStatus = FALSE;
    *sidPtr = NULL_PTR;
    const Dcm_DsdServiceType *service;

    if(NULL_PTR != msgData.serviceTable) {
        service = msgData.serviceTable->DsdService;
        while ((service->DsdSidTabServiceId != sid)
            && (FALSE == service->Arc_EOL)) {
            service++;
        }

        if (FALSE == service->Arc_EOL) {
            *sidPtr = service;
            returnStatus = TRUE;
        }
    }
    return returnStatus;
}

简单遍历,没有哈希表、没有二分查找。为什么不用更快的算法? 因为诊断服务表通常只有20-30个SID配置,线性遍历已足够快。用二分查找需要有序维护(增加配置错误风险),用哈希表需要运行时初始化(违背“编译期凝固“原则)。线性遍历的O(n)在n=30时只有30次比较——在几百MHz的Cortex-R5上可以忽略不计。


DSP配置——DspDid[]的DID定义森林

DSP的配置核心是DspDid[]——不是变量名,而是配置结构体数组。Dcm_DspDidType是DCM中结构体最复杂、信息密度最高的类型:

typedef struct Dcm_DspDidType {
    const Dcm_DspDidInfoType            *DspDidInfoRef;             // DID信息引用
    const struct Dcm_DspDidType         * const *DspDidRef;         // 子DID引用(嵌套DID)
    const Dcm_DspSignalType             * const DspSignalRef;       // 信号列表引用
    uint16                              DspDidIdentifier;           // DID编号
    uint16                              DspNofSignals;              // 信号数量
    uint16                              DspDidDataByteSize;         // 数据总字节大小
    uint16                              DspDidDataScalingInfoSize;  // 缩放信息大小
    Dcm_RoeActivateCallbackFncType      DspDidRoeActivateFnc;       // ROE激活回调
    uint8                               DspDidRoeEventId;           // ROE事件ID
    boolean                             Arc_EOL;
} Dcm_DspDidType;

每一个信号引用到Dcm_DspDataType,它定义了信号的端口类型、回调函数集、数据宽度、字节序:

typedef struct {
    const Dcm_DspDataInfoType               *DspDataInfoRef;
    Dcm_CallbackReadDataLengthFncType       DspDataReadDataLengthFnc;
    Dcm_CallbackConditionCheckReadFncType   DspDataConditionCheckReadFnc;
    Dcm_CallbackReadDataFncType             DspDataReadDataFnc;       // 读取回调
    Dcm_CallbackWriteDataFncType            DspDataWriteDataFnc;      // 写入回调
    Dcm_CallbackGetScalingInformationFncType DspDataGetScalingInfoFnc;
    Dcm_CallbackFreezeCurrentStateFncType   DspDataFreezeCurrentStateFnc;
    Dcm_CallbackResetToDefaultFncType       DspDataResetToDefaultFnc;
    Dcm_CallbackReturnControlToECUFncType   DspDataReturnControlToEcuFnc;
    Dcm_CallbackShortTermAdjustmentFncType  DspDataShortTermAdjustmentFnc;
    Dcm_DataPortType                        DspDataUsePort;           // 端口类型
    uint16                                  DspDataBitSize;           // 数据位宽
    NvM_BlockIdType                         DspNvmUseBlockID;         // NvM块ID(可选)
    DcmDspDataType                          DspDataType;              // 数据类型
    DcmDspDataEndianessType                 DspDataEndianess;         // 字节序
} Dcm_DspDataType;

这里的DspDataUsePort是信号最关键的分岔路口:

typedef enum {
    DATA_PORT_NO_PORT,
    DATA_PORT_BLOCK_ID,
    DATA_PORT_ASYNCH,       // 异步读取(通过回调,可能返回PENDING)
    DATA_PORT_SYNCH,        // 同步读取(直接取数据指针)
    DATA_PORT_ECU_SIGNAL,   // ECU信号
    DATA_PORT_SR             // SR信号
} Dcm_DataPortType;

DSP在读取DID数据时,遍历所有信号,根据DspDataUsePort选择不同的读取路径:

  • DATA_PORT_SYNCH:直接调用SynchDataReadFnc(data),同步返回
  • DATA_PORT_ASYNCH:调用AsynchDataReadFnc(data, &opStatus),可能在NvM回调后才返回
  • DATA_PORT_ECU_SIGNAL:通过ECU信号回调读取

Arc_EOL——静态数组的终止哨兵

你注意到几乎每个配置结构体末尾都有一个字段:

boolean Arc_EOL;

这是Arctic Core特有的数组终止哨兵模式——Arc_EOL替代了传统C中的NULL指针结尾。遍历配置数组时,代码会检查当前元素的Arc_EOL是否为TRUE来决定是否终止:

while ((!serviceRequestNotification->Arc_EOL) && (returnCode != E_REQUEST_NOT_ACCEPTED)) {
    // 处理当前元素
    serviceRequestNotification++;
}

为什么用Arc_EOL而不是NULL指针? 因为Arc_EOL是结构体的一个字段,可以在配置生成工具(Vector DaVinci Configurator或类似工具)中自动设置为TRUE来标记数组末尾,比用NULL指针结尾更不容易出错——NULL对于未被配置工具显式初始化的字段来说是默认值,可能会产生意外终止。

同样的模式也出现在Session和Security的配置数组中:

typedef struct {
    const Dcm_DspSecurityRowType *DspSecurityRow; // (0..31)
} Dcm_DspSecurityType;

typedef struct {
    const Dcm_DspSessionRowType *DspSessionRow; // (0..31)
} Dcm_DspSessionType;

DID的授权链——Session与Security的双重关卡

每个DID不是想读就能读的。在Dcm_DspDidAccessType中,每个DID的Read/Write/Control操作都有独立的Session和Security授权:

typedef struct {
    const Dcm_DspDidReadType    *DspDidRead;
    const Dcm_DspDidWriteType   *DspDidWrite;
    const Dcm_DspDidControlType *DspDidControl;
} Dcm_DspDidAccessType;

typedef struct {
    const Dcm_DspSessionRowType     * const *DspDidReadSessionRef;
    const Dcm_DspSecurityRowType    * const *DspDidReadSecurityLevelRef;
} Dcm_DspDidReadType;

注意这里的* const *——两层指针,外层const保护指针本身不被修改,内层const保护指向的Session配置行不被修改。这是一个典型的AUTOSAR配置链:

  1. DID → Dcm_DspDidAccessTypeDcm_DspDidReadType
  2. Dcm_DspDidReadType*DspDidReadSessionRef → 指向一个Session行的指针数组
  3. 运行时:遍历这个Session指针数组,检查当前会话是否匹配

每个Session行本身也有丰富的配置:

typedef struct {
    uint32                      DspSessionP2ServerMax;        // P2 Server最大值
    uint32                      DspSessionP2StarServerMax;    // P2* Server最大值
    Dcm_SesCtrlType             DspSessionLevel;              // 会话级别(Default/Extended/Programming)
    Dcm_SesCtrlType             ArcDspRteSessionLevelName;    // RTE会话级别名
    Dcm_DspSessionForBootType   DspSessionForBoot;            // Boot跳转策略
    boolean                     Arc_EOL;
} Dcm_DspSessionRowType;

其中DspSessionForBoot枚举定义了切换到此会话后是否触发Boot跳转:

typedef enum {
    DCM_NO_BOOT,
    DCM_OEM_BOOT,
    DCM_SYS_BOOT,
    DCM_OEM_BOOT_RESPAPP,
    DCM_SYS_BOOT_RESPAPP
} Dcm_DspSessionForBootType;

DSL配置——DslProtocolRowList[]的协议森林

DSL管理的是通信协议层。它的配置根在Dcm_DslProtocolType

typedef struct {
    const Dcm_DslProtocolRxType     *DslProtocolRxGlobalList;     // 全局RX列表
    const Dcm_DslProtocolTxType     *DslProtocolTxGlobalList;     // 全局TX列表
    const Dcm_DslPeriodicTxType     *DslProtocolPeriodicTxGlobalList;
    const Dcm_DslProtocolRowType    *DslProtocolRowList;          // 协议行列表
} Dcm_DslProtocolType;

每个Dcm_DslProtocolRowType代表一条通信协议路径:

struct Dcm_DslProtocolRowType_t {
    Dcm_ProtocolType                        DslProtocolID;
    boolean                                 DslProtocolIsParallelExecutab;
    uint16                                  DslProtocolPreemptTimeout;
    uint8                                   DslProtocolPriority;
    Dcm_ProtocolTransTypeType               DslProtocolTransType;     // 传输类型1或2
    const Dcm_DslBufferType                 *DslProtocolRxBufferID;   // RX缓冲区
    const Dcm_DslBufferType                 *DslProtocolTxBufferID;   // TX缓冲区
    const Dcm_DsdServiceTableType           *DslProtocolSIDTable;     // 此协议的SID表
    const Dcm_DslProtocolTimingRowType      *DslProtocolTimeLimit;    // 定时参数
    const Dcm_DslConnectionType             *DslConnections;          // 连接列表
    Dcm_DslRunTimeProtocolParametersType    *DslRunTimeProtocolParameters; // 运行时参数
    boolean                                 DslSendRespPendOnTransToBoot;
    boolean                                 Arc_EOL;
};

这是配置系统的终极威力——每个协议路径都有自己的SID表、自己的缓冲区、自己的定时参数。一个CAN诊断请求和DoIP诊断请求通过不同的ProtocolRow进入DCM,但共享同一个DSP服务实现。DSL配置告诉系统:“从CAN来的请求走RX PduID=42的通道,从DoIP来的请求走RX PduID=43的通道,但它们都查同一个DsdServiceTable


配置树的全景图

让我们把整个配置体系画成一棵树:

DCM_Config
├── Dsp (Dcm_DspType)
│   ├── DspDid[] (Dcm_DspDidType) ──→ 每个DID的定义
│   │   ├── DspDidInfoRef ──→ DspDidAccess(DspDidRead/DspDidWrite/DspDidControl)
│   │   │   └── **SessionRef ──→ Dcm_DspSessionRowType[] (P2/P2*时间等)
│   │   │   └── **SecurityRef ──→ Dcm_DspSecurityRowType[] (种子/密钥等)
│   │   ├── DspSignalRef ──→ Dcm_DspDataType[] (回调指针、端口类型、位宽等)
│   │   └── DspDidRef ──→ 子DID引用(嵌套DID支持)
│   ├── DspSession[] ──→ Dcm_DspSessionRowType[] (会话级别配置)
│   ├── DspSecurity[] ──→ Dcm_DspSecurityRowType[] (安全访问配置)
│   ├── DspRoutine[] ──→ 例行控制配置
│   ├── DspEcuReset[] ──→ ECU复位配置
│   ├── DspMemory ──→ 内存地址范围配置
│   └── DspComControl ──→ 通信控制配置
├── Dsd (Dcm_DsdType)
│   └── DsdServiceTable[] (Dcm_DsdServiceTableType)
│       └── DsdService[] (Dcm_DsdServiceType) ──→ 每个SID
│           ├── DsdSidTabSessionLevelRef ──→ Session引用
│           ├── DsdSidTabSecurityLevelRef ──→ Security引用
│           ├── DspSidTabFnc ──→ 外部服务回调(可选)
│           └── DsdSubServiceList[] ──→ 子功能列表(可选)
└── Dsl (Dcm_DslType)
    ├── DslBuffer[] ──→ 缓冲区配置
    ├── DslProtocol ──→ DslProtocolRowList[]
    │   ├── DslProtocolSIDTable ──→ 引入DSD的SID表
    │   ├── DslProtocolRxBufferID / DslProtocolTxBufferID
    │   ├── DslProtocolTimeLimit ──→ P2/P2*/S3定时参数
    │   └── DslConnections ──→ 物理连接配置
    ├── DslDiagResp ──→ 响应挂起配置
    └── DslServiceRequestNotification ──→ 通知回调

核心洞察:这棵配置树中,Session/Security配置行被多个地方复用——同一个DspSessionRow被DID的Read引用、被SID的路由引用、被子功能引用。这不是复制,而是指针共享。修改一个Session的配置(比如P2时间),所有依赖它的服务自动生效。这种“写一次,处处引用“的模式是静态配置相比动态配置的最大优势:无冗余导致的无不一致


编译期配置 vs 运行时分配——终极对比

特性编译期静态配置(AUTOSAR)运行时动态分配(传统模式)
内存位置Flash(ROM)RAM(堆)
内存大小编译时确定运行时可变
查找速度指针跳转 O(1)取决于数据结构
碎片化风险存在
分配失败风险存在
配置错误检测编译期运行时
配置修改需重新编译刷写可运行时热更新
功能安全适用性高(ASIL B-D适用)

汽车行业的答案很明确:选第一个。编译期配置的“僵化“正是它的力量——在汽车这种一次写入、十万次运行的产品中,运行时资源的确定性比灵活性重要一万倍。


本篇小结

  1. 整个DCM配置以Dcm_ConfigPtr为根,形成Dsp/Dsd/Dsl三叉树,所有配置为const、全部在Flash中,运行时不分配任何内存。
  2. DsdServiceTable[]以线性遍历方式匹配SID——30个元素的O(n)比任何高级数据结构的常数开销更优。
  3. DspDid[]包含每个DID的信号列表、端口类型(SYNCH/ASYNCH/ECU_SIGNAL)、读写回调函数指针——DSP据此选择同步或异步数据路径。
  4. Arc_EOL是Arctic Core特有的数组终止哨兵,替代NULL指针标记数组末尾,减少配置工具错误。
  5. Session/Security配置行通过双层指针(* const *)被DID和SID多处共享引用,实现一处配置的全局生效。
  6. DslProtocolRowList[]让每个通信协议路径拥有独立的缓冲区、SID表、定时参数,同时共享同一个DSP实现。
  7. 静态配置的“僵化“是功能安全要求下的必然选择——零分配失败、零碎片化、零运行时不确定性(启动后可预测)。

【下集预告】:AUTOSAR源码分析到此结束——从DCM全景到DSL缓冲区、从DSD路由到DSP服务实现、从CanTp传输到Lcfg配置,四层架构已全部拆解。接下来进入第5章——从零构建一套教学级UDS实现(uds-lite),用C语言在TCP上模拟DoIP诊断,亲手走一遍从编解码到全线测试的完整闭环。