4.6 文档与追溯——Sphinx多架构文档与@req需求追溯
代码的意图,比代码活得久
代码的意义在运行时,而代码的意图在文档中。嵌入式系统有一个残酷的事实:代码的平均寿命是10到15年,一辆车的生命周期里,最初写代码的团队大概率早已解散。当第三代维护工程师打开一个OS调度器源文件,看到/** @req SWS_Os_00239 */,他不需要猜测这行代码“为什么要这样写“,他可以追溯到AUTOSAR OS规范的239号需求,理解这行代码要实现什么功能,边界条件是什么,和哪些其他模块有交互。
这就是文档与追溯的基础设施。它不产生可执行代码,但它在代码的整个生命周期里持续产生价值:它是建筑物交付时的全套竣工图和操作手册,是写在每面墙上的“承重墙:N5-308号需求,不可拆除“。
核心洞察:代码的意图必须比代码活得久。代码的平均寿命10到15年,一辆车的生命周期里最初写代码的团队大概率早已解散;
/** @req SWS_Os_00239 */让第三代维护工程师不必猜测“为什么这样写“,而能追溯到AUTOSAR规范条目、边界条件与模块交互。文档基础设施不产生可执行代码,却在代码的整个生命周期持续产生价值——这是文档与追溯存在的全部理由。
Sphinx多架构文档生成
Arctic Core的文档系统基于Sphinx(Python文档生成器),但构建入口仍然是一份Makefile:build_doc.mk。它的注释第一行就给出了使用方式:
# Invocation:
# From any directory
# - make -f ../../../../scripts/sphinx/build_doc.mk quick-start
#
# There should always be a index.rst file
关键参数在第41-48行:
ifdef doc_mcu
doc_name=$(doc_module)_$(doc_type)_$(doc_mcu)
conf_opt=-m $(doc_module) -t $(doc_type) -a $(doc_mcu)
tag_opt=-t $(doc_mcu)
else
doc_name=$(doc_module)_$(doc_type)
conf_opt=-m $(doc_module) -t $(doc_type)
endif
三要素决定文档的输出粒度:
- doc_module:哪个模块(如Can、Dcm、Os)
- doc_type:哪种文档类型(如DD=设计描述、UG=用户手册)
- doc_mcu:哪个目标MCU(可选,用于生成特定芯片的OSAL手册)
例如doc_module=Os, doc_type=dd, doc_mcu=mpc5744p生成的是:MPC5744P芯片的OS抽象层设计描述文档。而省略doc_mcu则生成通用的跨MCU文档。
核心洞察:文档的输出粒度由doc_module、doc_type、doc_mcu三要素决定,多架构支持靠的是参数化而非维护多份文档。同一个
build_doc.mk构建入口,指定模块、文档类型(DD/UG)、可选的MCU,就能从一份源产出通用或芯片特化的手册:doc_mcu=mpc5744p得到该芯片的OSAL设计描述,省略则得到跨MCU通用文档。设计描述文档(DD)作为法定交付物,由此实现了“一份源、多形态“。
RST预处理管道
第50-51行暴露了文档系统的一个精巧设计:
%.rst: %.rstp
gcc -E -P -x c -traditional-cpp -Dcfg_$(doc_mcu) $< > $@
RST源文件先写成.rstp(RST预处理文件),然后通过GCC的C预处理器处理后再生成.rst:
-E:只做预处理,不编译-P:不生成行号标记-x c:强制以C语言模式处理-traditional-cpp:使用传统预处理模式(避免GNU扩展语法的干扰)-Dcfg_$(doc_mcu):定义目标MCU的预处理宏
这意味着在.rstp文件中,你可以写这样的条件:
.. only:: mpc5744p
.. [PPC-E200ZX] e200z4 Power Architecture™ Core Reference Manual
当doc_mcu=mpc5744p时,GCC会处理-Dcfg_mpc5744p,Sphinx的only指令判断条件成立,引用文献就出现在文档中。当doc_mcu=mpc560x时,引用的是另一个文献。不同芯片的硬件参考手册在生成时自动切换,同一份.rstp源文件,通过编译期宏切换,产生不同的最终文档。
conf.py的生成同样采用了模板模式:
conf.py: $(SPHINXDIR)/conf_main.py $(SPHINXDIR)/create_conf.py
$(Q)echo Creating $<
cp $(SPHINXDIR)/conf_main.py conf.py
python $(SPHINXDIR)/create_conf.py $(conf_opt) >> conf.py
先将通用配置conf_main.py复制为conf.py,再通过create_conf.py脚本追加模块特定的配置。Sphinx的配置是Python代码,所以这种“模板基类+脚本拼接“的方式本质上是用Python动态生成Python配置。
最终的多格式输出(第60-64行):
all: conf.py $(rstp-y)
$(SPHINXBUILD) $(tag_opt) -b html . obj_doc/html
$(SPHINXBUILD) $(tag_opt) -b latex . obj_doc/latex
pushd obj_doc/latex && pdfLatex $(doc_name).tex && pdfLatex $(doc_name).tex
一个make all命令同时产出HTML和PDF格式,前者给在线查阅,后者给正式评审签字。两次pdfLatex调用是为了正确生成交叉引用和目录,LaTeX需要两次编译才能解析内部引用,常规操作。
核心洞察:让编译器处理文档的条件编译,与代码的条件编译完全同构。
.rstp经gcc -E预处理器输出.rst,-Dcfg_$(doc_mcu)让不同芯片的硬件参考手册在生成时自动切换;conf.py也是“模板基类+脚本拼接“,本质是用Python动态生成Python配置。文档编写者只需会#ifdef就能写条件文档——用一个已经用熟的范式解决新问题,是降低工程复杂度的有效策略。
@reqSettings与@tagSettings:源码中的元数据
在Fls_Cfg.h的头部:
/** @tagSettings DEFAULT_ARCHITECTURE=ZYNQ */
/** @reqSettings DEFAULT_SPECIFICATION_REVISION=4.1.2 */
这两个标签是文档生成系统的指令,不是C代码的编译选项:
@tagSettings DEFAULT_ARCHITECTURE=ZYNQ:告诉文档生成器,这个配置文件的适用架构是Zynq。如果当前文档的目标架构是Tricore,这个文件的内容就不会被包含。@reqSettings DEFAULT_SPECIFICATION_REVISION=4.1.2:告诉需求追踪系统,这个文件中的@req标签默认对应AUTOSAR 4.1.2版规范。这意味着文件中的@req SWS_Fls_00308不需要每次都写上“对应4.1.2版规范第308条“,版本号在文件头声明一次,全文继承。
不同的模块有不同的规范版本:
/** @reqSettings DEFAULT_SPECIFICATION_REVISION=4.3.0 */
/** @reqSettings DEFAULT_SPECIFICATION_REVISION=4.2.2 */
/** @reqSettings DEFAULT_SPECIFICATION_REVISION=4.0.3 */
这反映了AUTOSAR生态的一个现实:不同的AUTOSAR基础软件模块可能遵循不同版本的标准。一个ECU上可能同时运行着符合4.3.0的OS模块、符合4.2.2的StbM模块、符合4.0.3的NvM模块。@reqSettings让文档生成系统精确识别每个@req所属的规范版本,而不需要维护外部版本映射表。
@tagSettings还有一个实际用途:
/** @tagSettings DEFAULT_ARCHITECTURE=GENERIC */
标记GENERIC意味着这个模块的实现与硬件架构无关,它不需要硬件特定的适配层,可以在任何平台上使用。对于文档生成器来说,GENERIC模块的文档会出现在所有架构的文档版本中,而ZYNQ或TMS570标记的模块只在对应架构的文档中出现。
核心洞察:
@reqSettings/@tagSettings把规范版本与适用架构声明为文件级默认值——减少重复输入就是减少熵增。一个ECU上可能并存4.0.3到4.3.0不同版本的AUTOSAR模块,若每条@req都手写版本号,开发者会写错、会忘记、会懒得写,而规范版本一旦不对,需求追溯全链条作废。把版本号提升为文件头一次声明、全文继承,是最小熵增解,也让文档生成器无需外部版本映射表。
@req注释的实践深度
通过前面的分析我们已经看到,Arctic Core全代码库有7300多处@req标签。这些标签不仅存在于头文件的类型定义中,更深入到业务逻辑的每一行实现:
/* @req SWS_StbM_00198 */ /* @req SWS_StbM_00199 */
STBM_DET_REPORTERROR((status != E_NOT_OK),STBM_SERVICE_ID_GET_CURRENT_TIME,
STBM_E_PARAM,status);
STBM_DET_REPORTERROR((NULL != timeStampPtr),STBM_SERVICE_ID_GET_CURRENT_TIME,
STBM_E_PARAM_POINTER,E_NOT_OK);
这里两条@req标签精确标注了StbM_GetCurrentTime函数的参数校验逻辑,哪条输入参数检查对应哪条需求。这种追溯的粒度精确到“语句级别“。如果功能安全审计员提问:“请证明需求SWS_StbM_00197(禁止空指针输入)在代码中有实现”,开发者可以直接定位到NULL != timeStampPtr这一行,而不是“它在GetCurrentTime函数里“。
在Fls_Cfg.h中,结构体成员的追溯更加精确:
typedef struct {
/** @req SWS_Fls_00109 */
/** @req SWS_Fls_00110 */
void (*FlsJobEndNotification)();
void (*FlsJobErrorNotification)();
#if (USE_FLS_INFO==STD_ON)
/** @req SWS_Fls_00355 */
const struct Flash *FlsInfo;
#else
const Fls_SectorType *FlsSectorList;
#endif
每个结构体成员的类型、成员的存在、甚至条件编译分支,全都有追溯标签。这是ISO 26262-8第10章“软件工具使用的置信度“和ISO 26262-6第7章“软件架构设计“的硬性证据需求。
核心洞察:追溯粒度要精确到语句级,才经得起功能安全审计的提问。“请证明需求SWS_StbM_00197(禁止空指针输入)已实现“的答案不是“它在GetCurrentTime函数里”,而是
NULL != timeStampPtr这一行。结构体成员的类型、成员的存在与否、甚至条件编译分支都有@req标签——精确到语句的追溯,正是ISO 26262-8第10章工具置信度与-6第7章架构设计的硬性证据需求。
MemMap.h:跨编译器的内存映射抽象
integration/MemMap.h是Arctic Core中一个外表低调但哲学深厚的文件:
/* REFERENCE
* MemoryMapping.pdf
*
* DESCRIPTION
* This file is used to map memory areas to specific sections, for example
* a calibration variable to a specific place in ROM.
*/
#include "Arc_MemMap.h"
/* SeqTest */
#define SeqTest_SEC_VAR_CLEARED_UNSPECIFIED
#define SeqTest_SEC_VAR_INIT_UNSPECIFIED
仅29行,但它背后是AUTOSAR最精妙的设计之一:内存映射(Memory Mapping)。
在AUTOSAR中,每个模块的.c文件在使用前都会include自己的*_MemMap.h文件,其中定义了START_SEC_CODE、STOP_SEC_CODE、START_SEC_VAR_CLEARED等宏。这些宏实际上是对编译器特定section语法的包装:
- GCC:
__attribute__((section(".text"))) - IAR:
#pragma location=".text" - Diab:
#pragma section ".text" - 其他编译器都有各自的语法
MemMap.h抽象了这一层,模块代码只需要写一段:
#define DCM_START_SEC_CODE
#include "Dcm_MemMap.h"
void Dcm_Init(void) { ... }
#define DCM_STOP_SEC_CODE
#include "Dcm_MemMap.h"
编译时根据当前编译器,宏展开为对应的section语法。这样开发者不需要关心目标编译器是什么,MemMap机制提供了跨编译器的统一内存布局接口。
这在汽车行业具有实际意义:同样的应用代码可能要用GCC开发调试,以便使用gcov覆盖率;但量产时要用Green Hills编译,以获得更好的优化和调试器支持。如果代码中直接写了__attribute__((section(...))),切换编译器后需要全部修改。MemMap机制消解了这种耦合。
核心洞察:MemMap.h以29行统一了六种编译器的section语法,消解了跨工具链耦合。GCC的
__attribute__((section))、IAR的#pragma location、Diab的#pragma section被包装成START_SEC_CODE/STOP_SEC_CODE宏,模块代码不再关心目标编译器是谁。开发用GCC以便用gcov覆盖率、量产切Green Hills以获得更好优化与调试器支持——切换工具链不再需要改一行应用代码,这正是“一头统一接口、一头原生语法“的跨工具链抽象。
@req追溯与Sphinx文档的协同
@req标签和Sphinx文档系统在同一套基础设施中协同工作。流程是这样的:
- 开发者在C源码中写
/** @req SWS_Fls_00109 */。 - 文档生成脚本扫描所有
@req标签,建立需求到代码行的映射。 - Sphinx生成设计描述文档时,自动插入_“此模块实现了以下AUTOSAR需求:SWS_Fls_00108, SWS_Fls_00109, SWS_Fls_00110, …”_
- CI流水线运行需求覆盖率检查:“AUTOSAR Fls规范定义了58条需求,其中56条在代码中有
@req标签,2条标记为不适用。”
这就是从规范到代码再到文档的完整追溯环。它让软件架构文档(SAD)、软件设计文档(SDD)、源代码、单元测试报告、静态分析报告,所有这些文档和证据通过@req需求编号形成了一张互联的网。
在system/Os/doc/dd/index.rst中,我们可以看到文档本身的详细程度:
OSAL Design Description for |mcu|
==============================================
Hardware Architecture
==================================================
This chapter gives a brief overview of the e200 core and gives a
background to the following chapters.
The technical reference manuals for the e200 start from a high
description level and down to the more device specific documents.
Example for MPC5744P (that contains a e200z4 core):
- EREF: A Programmer's Reference Manual for Freescale Power
Architecture Processors
- e200z4 Power Architecture™ Core Reference Manual
- The RM for the MPC5744P
文档不仅描述了OSAL层的软件设计,还建立了与硬件参考手册的引用关系。这意味着一个维护工程师可以从一行OS调度代码开始,追溯到设计描述文档,再追溯到芯片的用户手册寄存器描述,实现三层追溯逐级深入。
核心洞察:@req与Sphinx协同,构成从规范到代码再到文档的完整追溯环。开发者写
@req、脚本扫描建立映射、Sphinx自动插入“本模块实现了哪些需求“、CI再跑需求覆盖率检查(58条需求56条已实现、2条不适用)——SAD、SDD、源码、测试与静态分析报告通过需求编号织成一张互联的网。一个维护工程师可从一行OS调度代码追溯到设计描述文档,再追到芯片手册的寄存器描述,实现三层逐级深入。
构建比喻:竣工图与操作手册
建筑施工完成之后,必须交付两样东西:竣工图(As-Built Drawings)和操作维护手册(O&M Manual)。竣工图记录了“实际建成的是什么“,和设计图可能有偏差,但必须是最终形态的精确记录。操作维护手册告诉后面的物业管理者:每面墙后面是什么管线、电箱的额定电流是多少、承重墙在哪里。
Sphinx生成的设计描述文档就是竣工图,它是与当前代码版本精确一致的、自动生成的技术文档。.rstp文件经C预处理器处理后,文档中的宏(如|release|)会随代码版本自动更新。conf.py中的版本号(version = u'0.1', release = u'2.0.0')让每一版文档都知道自己对应哪个软件release。
@req标签则是写在每面墙上的铭牌:“非承重隔墙,SWS_00308号需求,可局部改造“或者“抗震剪力墙,SWS_00109号需求,U类构件,禁止任何改造”。当十年后有人要在这栋楼上加一层,他们不需要调出原始施工团队的图纸,他们看墙上的铭牌就知道哪些能动哪些不能动。
MemMap机制是建筑的基础构造图:“所有隔断墙用轻钢龙骨+石膏板(GCC的__attribute__),或砖砌体(IAR的#pragma),但功能上是一样的隔断功能。” 设计师不需要在每一面墙上标注“这面墙用砖“或“这面墙用轻钢“,构造图已经定义了所有隔墙的统一做法。
核心洞察:交付给十年后的不是代码,而是竣工图与操作手册。Sphinx自动生成的设计描述文档是与当前代码版本精确一致的竣工图——
.rstp经C预处理器后宏随版本自动更新,conf.py的release号让每版文档都知道自己对应哪个软件版本;@req标签是写在每面墙上的铭牌,告诉后来者哪些墙能动、哪些不能动;MemMap是基础构造图,统一定义所有隔墙的做法。它们让这栋楼能被人持续维护下去。
收尾的思考
【第四章结语】
你用六天时间走完了Arctic Core经典平台的工程基础设施全貌。第一天,你看到一条make命令背后6种编译器52块板子的递归构建体系,一个用Makefile语法实现的构建抽象层。第二天,你走进rules.mk的548行施工现场,看到编译规则如何精确控制从C文件到目标代码的每一步工艺,依赖追踪如何做到零外部工具依赖。第三天,你在67个.mod.mk文件中发现了声明式模块配置的极致:15行定义一个诊断协议栈,三种变量描述一个模块的全部。第四天,你理解了宿主测试如何在x86开发机上跑嵌入式代码,EmbUnit如何通过三个宏接入CI流水线,7300处@req如何让需求和代码互锁。第五天,你翻开au-misra3.lnt的1835行,看到MISRA C:2012的全部143条规则如何被编码为可执行的静态检查,每一条诊断都有规范条目的自动翻译。第六天,你站在Sphinx文档生成器和MemMap抽象层面前,看到了这个工程最持久的承诺:让代码十年后仍然可读、可懂、可证,而非仅仅现在能跑。
这六个部分共同构成了一个完整工程基础设施的六个支柱:构建定义了如何从源码到烧录文件,模块化定义了如何管理67个模块的编译依赖,测试定义了如何验证软件逻辑的正确性,静态分析定义了如何防止编码层面的安全隐患,文档与追溯定义了如何让代码的意图超越作者的在职时间。它们彼此独立又相互咬合,编译器插件生成的依赖文件被构建引擎追踪,构建引擎编译时内嵌的lint检查引用文档中的需求标签,测试框架通过@req标签与设计描述交叉验证。
你合上文件,回到刚刚打开Arctic Core根目录的那一刻。一个makefile。你那时不知道,这558个字节背后,是一个无声运转了十多年的工程机器。
本篇小结
- Sphinx多格式文档构建:Arctic Core的文档与追溯基础设施以Sphinx为核心,通过
build_doc.mk的Makefile封装实现多模块、多MCU、多格式(HTML/PDF)的文档构建。 - RST条件编译管道:RST源文件经过GCC C预处理器的管道(
.rstp→.rst)实现了文档内容的条件编译,支持按目标架构自动切换引用文献和示例代码。 - @reqSettings与@tagSettings:
@reqSettings和@tagSettings在源码头部声明模块的规范版本和适用架构,使7300多处@req标签能精确追溯到AUTOSAR规范条目,构成从需求到代码到文档的完整双向追溯链。 - MemMap跨编译器抽象:
MemMap.h以29行代码为6种编译器提供了统一的内存section映射抽象层,展现了跨工具链抽象的最终形态:一头是项目代码的统一接口,另一头是各编译器的原生语法。
【下集预告】:你参观完了Arctic Core这座真实的楼:构建、模块化、测试、静态分析、文档追溯,六个支柱环环相扣。这些都是“别人的工程“。下一章,你不再参观别人的楼,你亲手建造一座。第5章,eng-lite从零构建:一个700多行代码的教学级嵌入式工程工具链,覆盖构建、静态分析、单元测试、CI流水线和需求追溯的全部核心环节。现在,你是建筑师。