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.3 模块化配置——67个mod.mk与三层配置体系

15行的模块简历

diagnostic/Dcm/Dcm.mod.mk。15行。如果你习惯看CMakeLists.txt里成百上千行的add_library声明,你可能会怀疑自己看错了:就这?

#Dcm
obj-$(USE_DCM) += Dcm.o
obj-$(USE_DCM) += Dcm_Dsp.o
obj-$(USE_DCM) += Dcm_Dsd.o
obj-$(USE_DCM) += Dcm_Dsl.o
obj-$(USE_DCM) += Dcm_ROE.o
obj-$(USE_DCM) += Dcm_Internal.o
obj-$(USE_DCM) += Dcm_LCfg.o
ifeq ($(filter Dcm_Callout_Stubs.o,$(obj-y)),)
obj-$(USE_DCM) += Dcm_Callout_Stubs.o
endif

inc-$(USE_DCM) += $(ROOTDIR)/diagnostic/Dcm/inc
inc-$(USE_DCM) += $(ROOTDIR)/diagnostic/Dcm/src
vpath-$(USE_DCM) += $(ROOTDIR)/diagnostic/Dcm/src

六种变量、一个条件守卫、零冗余。这就是Arctic Core整个模块化配置体系的缩影:67个.mod.mk文件,每个都是一个AUTOSAR模块的“物料清单“,用同一套声明式语法精确描述:这个模块由哪些源文件构成、需要哪些头文件路径、源代码散落在哪个目录。

而你眼前的Dcm(诊断通信管理)模块,是整车诊断协议栈的核心,承载着UDS诊断服务的路由、分发、处理和会话管理。它在ISO 14229标准中定义了数十种诊断服务:读取故障码、写入数据、安全访问、例程控制…每一条诊断命令从OBD接口进入,经过Dcm的处理链,最终触发ECU内部的状态变化。

就是这样复杂的核心模块,它的构建声明只有15行。

核心洞察:一个复杂模块的构建声明只有15行。承载UDS诊断路由与处理的Dcm,只需六种变量、一个条件守卫、零冗余地声明源文件、头文件路径与源码目录。67个.mod.mk都是同一套声明式语法的“物料清单“——每一行只声明一个事实,无过程、无冗余,这是对“最小必要信息“原则的极致践行。


声明式设计:三个变量定义一个模块

.mod.mk的精髓在于三个条件变量:

变量含义示例
obj-$(USE_XXX)当模块开启时,加入编译的目标文件obj-$(USE_DCM) += Dcm.o
inc-$(USE_XXX)当模块开启时,加入头文件搜索路径inc-$(USE_DCM) += $(ROOTDIR)/diagnostic/Dcm/inc
vpath-$(USE_XXX)当模块开启时,加入VPATH源文件搜索目录vpath-$(USE_DCM) += $(ROOTDIR)/diagnostic/Dcm/src

这套设计的精妙在于:不需要任何显式的条件判断。$(USE_DCM)在Makefile的语义中就是一个空字符串(未定义时)或y(定义时)。如果USE_DCM未定义,obj-就是一个完全不同的变量名,不影响任何东西;如果USE_DCM=y,obj-y就把Dcm的所有.o文件注入构建。这个trick利用了GNU Make的变量名拼合特性,免去了所有ifeq/endif包裹:声明即配置。

Dcm模块还有一个精巧的条件守卫:

ifeq ($(filter Dcm_Callout_Stubs.o,$(obj-y)),)
obj-$(USE_DCM) += Dcm_Callout_Stubs.o
endif

这行代码的意思是:“如果项目自己的makefile已经提供了Dcm_Callout_Stubs.o(一个定制的回调实现),就不要用默认的stub版本。“这种低优先级默认提供、允许上层覆盖的模式,在AUTOSAR中称为“回调存根模式”:基础软件提供默认空实现,应用层可以选择替换。

核心洞察:变量名本身可以携带条件语义——声明即配置。obj-$(USE_DCM)的变量名拼合让空串或y决定内容注入与否,免去一切ifeq/endif包裹;而那个filter条件守卫实现了“低优先级默认提供、允许上层覆盖“的回调存根模式:默认行为对扩展封闭,对上层的替换开放。无需接口与依赖注入容器,一个条件表达式就完成编译期的策略模式。


67个模块,同一套语法

boards/board_common.mk的第7-14行定义了所有可用模块的统一声明入口:

MOD_AVAIL+=NVM MEMIF FEE FLS EEP EA
MOD_AVAIL+=XCP CANIF CANTP FRTP LINTP LINIF COM DCM DEM FIM DET DLT ...
MOD_AVAIL+=COMM NM CANNM CANSM LINSM FRSM FRNM IPDUM ETHSM SECOC
MOD_AVAIL+= RAMLOG CRC CDDPDUR CPL CAL RAMTST DLTUARTCOM LDCOM ...

然后从238行开始,每个模块被统一include:

DCM-MOD-MK?=$(ROOTDIR)/diagnostic/Dcm/Dcm.mod.mk
include $(DCM-MOD-MK)

?=的妙用:如果应用层在build_config.mk中定义了自己的DCM路径(例如集成MCAL供应商提供的版本),就用应用层的;否则用Arctic Core默认的。这是对供应链灵活性的直接支持:OEM可能从不同Tier-1供应商采购同一个AUTOSAR模块的不同实现。

tests_common.mk展示了模块依赖的另一面:测试环境中,只编译被测试模块需要的最小依赖集合:

ifeq (USE_DCM, $(findstring USE_DCM,$(def-y)))
inc-y += $(ROOTDIR)/diagnostic/Dcm/inc
endif

不是所有模块都要链接,只是需要头文件来分析接口。这种“弱依赖“(只需要接口声明,不需要实现)和“强依赖“(需要完整的链接)的区分,是模块化测试的基础。

核心洞察:67个模块用同一套声明式语法接入,?=把供应链的灵活性直接写进构建。DCM-MOD-MK?=让应用层能以自有实现覆盖Arctic Core默认模块——OEM可以从不同Tier-1供应商采购同一个AUTOSAR模块的不同实现,构建系统自动适配。测试侧对“弱依赖/强依赖“的区分,则是模块化测试能够成立的地基:只测最小依赖集合,接口与实现分离。


Fls的三层配置:从宏到结构体到校验和

模块构建声明解决的是“编译什么“的问题,而模块参数配置则是另一维度的工作。以Flash驱动(Fls)为例,它的配置分为三个层次:

第一层:预编译配置(Fls_Cfg.h)

这是编译期配置,通过#define宏控制代码生成:

#define FLS_AC_LOAD_ON_JOB_START     STD_OFF  /* NO SUPPORT */
#define FLS_CANCEL_API               STD_OFF  /* NO SUPPORT */
#define FLS_COMPARE_API              STD_ON
#define FLS_DEV_ERROR_DETECT         STD_ON
#define FLS_GET_JOB_RESULT_API       STD_ON
#define FLS_GET_STATUS_API           STD_ON
#define FLS_SET_MODE_API             STD_OFF  /* NO SUPPORT */
#define FLS_USE_INTERRUPTS           STD_OFF  /* NO SUPPORT */
#define FLS_VERSION_INFO_API         STD_ON

每个宏对应AUTOSAR Fls规范中的一个配置开关。STD_OFF的开关不仅禁用了API,对应的代码分支也不被编译:#if(FLS_COMPARE_API==STD_ON)这样的条件编译让最终固件不包含任何未使用功能的代码。这在汽车行业是“安全需求“。ISO 26262要求:未使用的功能不得存在于最终二进制中,因为它们可能引入未测试的代码路径。

版本兼容性校验也在这里:

#if !(((FLS_SW_MAJOR_VERSION == 2) && (FLS_SW_MINOR_VERSION == 0)) )
#error Fls: Configuration file expected BSW module version to be 2.0.*
#endif

如果拿Fls 3.0的配置文件去编译Fls 2.0的源码,#error直接炸掉编译,比运行时崩溃好一万倍。

物理参数:

#define FLASH_SECTOR_SIZE     0x10000u
#define FLASH_NUM_SECTORS     0x100u
#define FLASH_PAGE_SIZE       0x8u
#define FLS_TOTAL_SIZE        (FLASH_NUM_SECTORS * FLASH_SECTOR_SIZE)

这些值直接决定了驱动代码中循环展开次数、缓冲区大小、地址校验逻辑。错一个值轻则写坏Flash,重则变砖。这就是为什么这些参数通过#define做编译期常量:地址运算全在编译期完成,零运行时开销。

第二层:链接时配置(Fls_Cfg.c)

这是运行期配置,通过结构体实例化:

const FlashType flashInfo[] = {
    {
        .FlsBaseAddress= 0x00000000U,
        .FlsTotalSize  = 0x01000000U,
        .sectCnt       = 256U,
        .bankSize      = 0x01000000U,
        .regBase       = 0xE000D000U,
        .sectAddr = {
            0x00000000U,
            0x00010000U,
            0x00020000U,
            ...
            0x00FF0000U,
        }
    }
};

const Fls_ConfigType FlsConfigSet[] = {
    {
        .FlsInfo              = flashInfo,
        .FlsAcErase           = 0u,
        .FlsAcWrite           = 0u,
        .FlsMaxReadFastMode   = 0x100u,
        .FlsMaxReadNormalMode = 0x100u,
        .FlsMaxWriteFastMode  = 0x100u,
        .FlsMaxWriteNormalMode= 0x100u,
        .FlsJobEndNotification  = Fee_JobEndNotification,
        .FlsJobErrorNotification= Fee_JobErrorNotification,
        .FlsProtection        = 0u,
    }
};

注意sectAddr:256个扇区的起始地址,每个是精确的0x00010000递增,也就是每扇区64KB。这里选择显式列出,而不是“写个循环生成“,因为Flash的扇区布局是芯片物理属性,必须逐扇区定义。在汽车行业,Flash驱动配置文件的正确性通常需要独立的配置审查流程。

这里还有一个模块间耦合的经典体现:Fls的回调函数FlsJobEndNotification和FlsJobErrorNotification指向了Fee(Flash EEPROM Emulation)模块的函数。这意味着Fls驱动完成一次Flash操作后,会主动通知Fee模块。在AUTOSAR架构中,Fls在硬件抽象层(MCAL),Fee在ECU抽象层(ECU Abstraction Layer),低层通知高层的垂直回调模式,是分层架构中解耦的关键机制。

第三层:后构建配置(Post-Build)

前两层已经覆盖了编译期和链接期的配置。但AUTOSAR还定义了第三层:后构建(Post-Build)配置:

def-$(CFG_POSTBUILD) += POSTBUILD_ADDRESS=$(POSTBUILD_ADDRESS_START)
def-$(CFG_POSTBUILD) += POSTBUILD_ADDRESS_END=$(POSTBUILD_ADDRESS_STOP)

后构建配置的思想是:某些配置参数(如checksum、标定数据)在固件编译完成后才能确定值,它们占据Flash的一个固定区域,可以在编译后、量产前独立更新。rules.mk中的SREC裁剪规则正是为这个场景服务的:它把Flash镜像切分为“不变代码区“和“可变配置区“两部分,后者可以在不重新编译的情况下更新。

核心洞察:Fls的三层配置本质是变更粒度管理。预编译#define决定代码生成,物理参数错一个就写坏Flash甚至变砖,任何改动触发全量重建与全部回归测试;链接时结构体显式列出每个扇区地址,变更通常只需重新链接;Post-Build区域则可在不重新编译的情况下独立更新。正确区分这三层,决定了配置管理流程的效率与安全边界。


构建比喻:BOM与物料管理

建筑师的设计图(AUTOSAR ARXML配置)定义了_“建筑物应该有什么功能”:走廊多宽、房间多大、消防设施在哪个位置。但施工经理需要的是“每层楼需要什么材料”_:水泥多少袋、钢筋多少吨、砖块多少块。.mod.mk文件就是建筑施工的BOM(Bill of Materials)。

Dcm.mod.mk的15行告诉施工经理:诊断模块需要7块预制板(7个源文件),这些预制板在diagnostic/Dcm/src仓库,规格书在diagnostic/Dcm/inc档案室。当总调度下令“USE_DCM=y“时,自动从仓库调取这些物料,按规格书加工。

Fls的三层配置则对应建筑材料的三个标准:第一层(#define)是混凝土标号(C30还是C40),在设计阶段确定;第二层(结构体)是钢筋的精确位置,每一根钢筋绑扎在哪,按设计图执行;第三层(Post-Build)是竣工验收时的强度检测,混凝土凝固后才能测,但检测点位置早已预留。

三层配置的分离还有一个隐含的安全意义:如果预编译配置变了一个比特,整个固件必须重新编译并通过全部回归测试。而链接时配置的结构体变更可能只需要重新链接。Post-Build区域则可以独立更新而完全不影响已测试的代码。这是对配置变更风险的精细化分级管理。

核心洞察:.mod.mk是施工BOM,三层配置是建筑材料的三个质检标准。设计图定义“建筑物要什么功能“,BOM回答“每层楼需要什么材料“;混凝土标号(#define)在设计阶段确定、钢筋绑扎(结构体)按图执行、竣工验收强度检测(Post-Build)预留检测点。预编译变更→全量重建(最重最安全),链接时变更→重新链接(中等),后构建变更→局部覆盖(最轻但地址精度要求最高)。声明你所拥有的,而非描述如何获取。


本篇小结

  • 67个.mod.mk的声明式模块描述:Arctic Core的模块化配置体系以67个.mod.mk文件为核心,每个文件用obj-$(USE_MODULE)、inc-$(USE_MODULE)、vpath-$(USE_MODULE)三种条件变量实现零冗余的声明式模块描述。
  • Dcm.mod.mk的15行定义:Dcm.mod.mk以15行定义诊断通信管理模块的7个源文件、2个头文件目录、1个源码目录,并通过条件守卫支持回调存根的上下层覆盖。
  • Fls的三层配置体系:Fls驱动展示了AUTOSAR的三层配置体系:预编译#define控制编译期代码生成、链接时结构体定义运行期参数、后构建SREC裁剪实现独立更新的配置区段。
  • 最小必要信息原则:整个体系是对“最小必要信息“原则的践行:每一行只声明一个事实,零过程零冗余。

【下集预告】:BOM确认了材料没问题,施工经理完成了浇筑,但材料强度达标吗?结构安全的“试验报告“在哪里?下一节,我们走进testCommon目录,看EmbUnit测试框架如何在宿主机上跑嵌入式单元测试,看XML输出如何接入CI流水线,看gcov和BullseyeCoverage如何用覆盖率数据证明“每一袋水泥都被检验过“。