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

第二章 砖、梁与门窗

2.1 模块——软件的基本砖块

一面墙和一堆砖

站在一座刚封顶的高层住宅前,你看到的是一面面整齐的砖墙。砖块横平竖直,灰缝均匀,窗户和门洞精确地留在设计图纸规定的位置。如果没有人告诉你,你很难想象这面墙在三个月前还是一堆散落在工地上的红砖。同样的材料,完全不同的形态。

堆在泥地上的散砖,每一块都是合格的:烧制温度达标,抗压强度合格,尺寸误差在国标允许范围内。但一堆合格的砖叠在一起,不会自动变成一面墙。你跳上去踩两脚,砖堆会哗啦啦塌掉。而砌好的砖墙,你可以把三层楼的重量压在上面,它能稳稳地承载几十年。

区别在哪里?在于砂浆和砌法。砖与砖之间通过砂浆粘合,每一块砖知道自己该放在哪里、和谁相邻、承受什么方向的力。砌墙的工人遵守“上下错缝、内外搭接“的法则,任何一条竖缝都不会从上通到下,因为那会形成薄弱面。每一块砖被它周围的砖约束着,同时又约束着周围的砖。这是砖墙的力量来源,也是它和一堆散砖的本质区别。

你的代码也是这样。

变量声明、函数定义、结构体、宏、条件编译:这些都是你的“砖“。C语言标准就是你的“窑“:它保证了每一行语法正确的代码在编译器看来都是合格的。但C语言标准不会教你怎么把这些合格的行组织成一个可维护的系统。就像砖厂的质检员不会教你怎么砌墙。

写代码的第一年,你的目标可能是“让编译通过“,这跟“砖块出厂质检合格“是一样的等级。

写代码的第三年,你的目标可能是“让功能正确“,这跟“一堆砖能叠起来不立刻倒塌“是一样的等级。

从第五年开始,如果你的代码还要用十年。它在十年里要经历芯片换代、CAN协议升级、OEM需求变更、ISO标准修订,你的目标必须是“让修改安全“。这,就是模块化。

你不只是在写代码。你是在砌一面将要站立十年的墙。

核心洞察:合格代码不等于可维护系统,模块化的目标是让修改安全。 编译通过只是“砖块出厂质检合格“,功能正确只是“砖堆暂时不倒“;当代码要用十年、要经历芯片换代与需求变更时,唯一的标准是“让修改安全“——这正是模块化的全部意义。


什么是模块

一个模块是一片自包含的代码单元,具备两个基本属性。

高内聚:模块内部的各个部分紧密相关,共同完成一个职责。一个CAN报文接收模块,它的解包、校验、分发、错误处理都围绕“接收CAN报文“这一个职责展开。

低耦合:模块与外部世界的交互越少越好,接口越清晰越好。一个模块不应该知道其他模块的内部实现,它只通过公开接口交互。

在C语言中,一个模块通常对应一对文件:xxx.c(实现)和 xxx.h(接口)。头文件是这个模块对外宣布的“边界“:什么可以调用、什么不能碰。

核心洞察:模块由高内聚与低耦合两个属性定义。 内聚是内部各部分紧密围绕同一职责,耦合是模块对外交互越少越好;C语言里一对 .c/.h 文件就是模块,头文件是它对外宣布的边界——什么可以调用、什么不能碰。


反面教材:4500行的main.c

你在很多嵌入式项目里见过这种文件:一个 main.c 或者 app.c,几千行。里面塞进了CAN报文接收、诊断服务处理、传感器数据滤波、电机控制逻辑、非易失存储读写。这个文件,就是一堆散砖。

每一个功能单独看都是合格的:CAN接收代码能收报文,诊断代码能响应请求,滤波代码能滤掉尖峰。但它们全部堆在同一个文件里,互相之间没有边界。

改CAN接收逻辑,你担心影响诊断;改滤波参数,你担心影响电机控制。你在一个几百行的函数里小心翼翼地移动代码,就像在拆一堵墙时不知道哪块砖是承重的。

维护这个文件的工程师,三个月前写的代码,三个月后再看,需要花一整天才能理清“这段代码为什么要在这里“。

核心洞察:没有边界的代码是一堆散砖,每块合格但整体脆弱。 几千行的 main.c 塞进CAN接收、诊断、滤波、电机控制,功能各自合格却互相牵制:改CAN怕影响诊断,改滤波怕影响电机,维护者三个月后就理不清“这段代码为什么要在这里“。


找模块边界:一起变化的东西放在一起

模块怎么划分?最简单可靠的标准:一起变化的东西放在一起,不同频率变化的东西分开。

CAN报文的格式由协议决定,它可能随车型变化;传感器滤波参数由硬件特性决定,它可能随传感器型号变化;电机控制逻辑由电机特性决定。这三样东西的变化频率和原因都不同,就应该分成三个模块。

这个标准在汽车嵌入式领域尤其重要。AUTOSAR为什么把ECU软件分成MCAL、ECU Abstraction、Services、Application四层?因为每一层的变化原因不同:芯片换了只影响MCAL,通信协议变了只影响Services,业务逻辑变了只影响Application。不同层的变化彼此隔离,修改才不会互相传染。

核心洞察:模块边界按“变化原因“划分:一起变化的放一起,不同频率变化的拆开。 CAN格式随车型变、滤波参数随传感器变、电机逻辑随电机变,应分属不同模块;AUTOSAR四层架构正是按变化原因分层,让芯片、协议、业务的变化彼此隔离、互不传染。


你不需要AUTOSAR才能模块化

AUTOSAR是模块化的标准化实现,但模块化本身是一种思维,不依赖任何特定框架。即使你的项目不用AUTOSAR,你依然可以在自己的代码里践行模块化:把一个1500行的文件拆成五个职责清晰的 .c/.h 对,每个对只做一件事,通过清晰的接口协作。

这不是“以后再用AUTOSAR时重新学习“,而是同一套思维在不同规模下的应用。理解了“一起变化的东西放在一起“,你既能在AUTOSAR里看懂四层架构为什么这样划分,也能在裸机项目里把代码组织得清晰。

核心洞察:模块化是一种思维,不依赖任何特定框架。 AUTOSAR只是它的标准化实现;即便在裸机项目里,把一个1500行的文件拆成五个职责清晰的 .c/.h 对,也是同一套思维在不同规模下的应用——看懂AUTOSAR与组织好裸机代码,靠的是同一个标准。


模块化的成本

模块化不是免费的。它有三个成本。

编译时间:模块之间通过头文件交互,修改一个头文件会触发依赖它的所有模块重新编译。但这是可控的。

代码空间:模块化的抽象层会引入少量间接调用和数据结构开销。在RAM紧张的ECU上,需要权衡。

认知成本:模块边界划分需要设计能力,划分错了反而更糟。但这是值得投入的成本。

模块化的收益远超成本:可测试性(每个模块可以独立测试)、可移植性(换芯片时只改一层)、可并行开发(不同工程师可以同时改不同模块)。

核心洞察:模块化有成本,但收益远超成本。 编译时间、代码空间、认知成本三个成本都可控;换来的可测试性、可移植性、可并行开发,是代码要用十年时必须付的代价。


本篇小结

  • 模块是软件的基本砖块:由高内聚、低耦合两个属性定义。
  • 在C语言中的落地:模块对应 .c/.h 文件对,头文件是模块的对外边界。
  • 模块划分的标准:“一起变化的东西放在一起”,按变化频率和原因分层。
  • 模块化的成本:三个成本(编译时间、代码空间、认知成本)都远小于收益(可测试性、可移植性、可并行开发)。
  • AUTOSAR与模块化:AUTOSAR的四层架构是模块化思想在汽车领域的标准化实现,但模块化的思维不依赖AUTOSAR。

【下集预告】:砖块有了,砂浆是什么?砖与砖之间的灰缝(那个定义了砖块如何交接、力如何传递的精确边界)就是接口。下一节,我们走进一扇门,看看嵌入式C语言的接口设计,为什么全局变量是承重墙上的大锤洞,以及UDS的DID如何用“编号“隐藏了“内存地址“的全部复杂性。