MCAL、ECU Abstraction、Service、RTE、ASW 到底怎么分工?代码到底该放哪一层?
上一篇我们把 AUTOSAR Classic 的完整软件架构过了一遍。
如果还没看,可以先看这里:
一张图看懂 AUTOSAR Classic 完整软件架构
上一篇解决的是:
AUTOSAR Classic 里面到底有哪些层,每一层大概有什么东西。
但实际做项目的时候,真正让人头疼的往往不是“MCAL 是什么”“RTE 是什么”。
而是这种问题:
我现在要写一段代码,这段代码到底应该放哪里?
比如来了一个非常普通的需求:
KL15 接在 MCU 的 P33.0 上。
当 KL15 为高电平时,VCU 认为整车已经上低压,开始执行后续状态切换。
如果是一个普通裸机项目,这件事可能几分钟就写完了:
if(Dio_ReadChannel(KL15_PIN)==STD_HIGH){VehicleState=VEHICLE_STATE_WAKEUP;}代码能不能跑?
当然能。
问题是:
这段代码应该写在 ASW 里面吗?
如果不应该,那:
Dio_ReadChannel()应该在哪里调用?
P33.0 和 KL15 的对应关系应该谁知道?
“KL15 有效以后进入 Wakeup”又应该谁负责?
这才是 AUTOSAR 分层真正开始有意思的地方。
一、先别急着背层级,先拆一个 KL15
我们先把刚才那个需求拆开。
实际硬件是:
KL15 │ │ 12V / 电气信号 ▼ 输入电路 │ ▼ MCU P33.0软件需要最终得到:
KL15 = ON然后应用软件根据这个状态决定:
车辆进入唤醒状态乍一看,这就是一个功能。
但如果仔细拆,会发现里面其实混了三件完全不同的事情。
第一件:
P33.0 当前是高电平还是低电平?
第二件:
P33.0 这个输入,在这块 ECU 上代表的是 KL15。
第三件:
KL15 有效以后,车辆应该做什么。
这三件事看起来很接近,实际上属于三个完全不同的抽象层级。
比较合理的关系应该是:
P33.0 │ ▼ Dio │ ▼ IoHwAb / ECU Abstraction │ ▼ RTE │ ▼ VehicleState SWC把它翻译成人话:
Dio: 我只知道 P33.0 是高还是低。 IoHwAb: 我知道这个输入在 ECU 上叫 KL15。 ASW: 我只关心 KL15 有没有上电。 至于 KL15 接 P33.0 还是 P20.4,我不关心。这就是 AUTOSAR 分层真正想解决的问题。
二、MCAL 最好不要知道“KL15”是什么
先从最下面说起。
P33.0 是 MCU 的一个具体 Pin。
真正读取它的,是 MCAL 里面的 Dio Driver。
类似:
Dio_LevelType level;level=Dio_ReadChannel(DioConf_DioChannel_P33_0);Dio Driver 知道:
PORT33 Pin0 Input High / Low但从软件架构上讲,它最好不知道:
这个 Pin 是 KL15为什么?
因为“KL15”是 ECU 的电气功能定义。
而:
P33.0是 MCU 的硬件资源定义。
这两件事情不是一个层级。
今天的原理图可能是:
KL15 → P33.0下一版硬件有可能变成:
KL15 → P20.4如果应用代码里面到处都是:
Dio_ReadChannel(DioConf_DioChannel_P33_0);那硬件一改,应用代码也得跟着改。
这就意味着:
应用层已经和 MCU 硬件绑死了。
这显然不是 AUTOSAR 想要的结果。
三、那谁负责把 P33.0 变成 KL15?
这时候就到了 ECU Abstraction。
上一篇我们已经讲过 ECU Abstraction 是什么,这里不再重复定义。
这次只看它在项目里到底干什么。
可以做一个非常简单的封装:
Std_ReturnTypeIoHwAb_GetKl15State(boolean*state){Dio_LevelType level;level=Dio_ReadChannel(DioConf_DioChannel_KL15);if(level==STD_HIGH){*state=TRUE;}else{*state=FALSE;}returnE_OK;}从这层开始,软件里面第一次真正出现:
KL15而不是:
P33.0这一步非常关键。
因为从现在开始,上层软件看到的已经不再是 MCU 引脚。
而是 ECU 的功能信号:
KL15 BrakeSwitch WakeupInput IgnitionState CrashInput这就是为什么叫:
ECU Abstraction。
它抽象掉的已经不是单纯的 MCU,而是:
这块 ECU 的具体硬件连接。
四、ASW 最好连“Dio”这个单词都不要看到
继续往上。
现在 VehicleStateManager 需要知道 KL15 状态。
如果直接这么写:
if(Dio_ReadChannel(DioConf_DioChannel_KL15)==STD_HIGH){VehicleState=VEHICLE_STATE_WAKEUP;}短期看没什么问题。
甚至很多实际项目里确实有人这么写。
但从架构上看,这里已经出现了一个很明显的问题:
ASW ↓ 直接调用 ↓ MCAL中间所有抽象层都被绕过去了。
这意味着 VehicleStateManager 必须知道:
KL15 是一个 DIO这其实已经不应该是 ASW 关心的事情了。
因为对于应用软件来说:
KL15 是通过 DIO 采集的和:
KL15 是通过 SPI SBC 读取的理论上没有区别。
应用只应该看到:
Kl15State = TRUE至于这个 TRUE 是怎么来的,是硬件层的事情。
所以更合理的应用代码可能只是:
boolean kl15State;Rte_Read_Kl15State(&kl15State);if(kl15State==TRUE){VehicleState=VEHICLE_STATE_WAKEUP;}这时候 ASW 终于干净了。
它只处理:
KL15有效 ↓ 车辆状态切换没有:
P33.0 Dio PORT Input Channel这些东西。
五、这时候再看 RTE,就容易理解多了
很多人学 AUTOSAR 时,最容易把 RTE 学得很玄乎。
其实放到刚才这个例子里就很简单。
下面已经有一个:
KL15 State上面的 VehicleState SWC 需要它。
中间谁负责把数据接起来?
RTE。
于是:
IoHwAb │ │ KL15 State ▼ RTE │ ▼ VehicleState SWC应用里面只看到:
Rte_Read_Kl15State(&kl15State);甚至它都不知道:
这个数据到底是谁提供的。
这就是 RTE 非常重要的一个价值:
让软件组件之间不需要互相知道对方的具体实现。
六、那为什么不能所有东西都直接 Rte_Read?
这里再补一个实际项目里很容易产生的误区。
有人看到这里可能会觉得:
那我是不是所有输入全部做成 Rte_Read 就完事了?
也不是。
AUTOSAR 分层不是为了“所有函数外面套一个 Rte_”。
真正重要的是:
不同的软件层,只知道自己应该知道的信息。
比如还是 KL15。
MCAL 应该知道:
哪个 Port 哪个 Pin 怎么读硬件ECU Abstraction 应该知道:
这个 Pin 在 ECU 上是什么功能ASW 应该知道:
KL15 有效以后车辆怎么响应RTE 只是把这些不同世界连接起来。
所以分层的重点不是 API 长什么样。
而是:
知识边界。
谁应该知道什么。
谁不应该知道什么。
这个思维比背 AUTOSAR 模块分类重要得多。
七、再看一个 ADC,问题会更明显
KL15 还算简单。
换一个稍微复杂一点的。
比如 VCU 上有一路模拟量:
油门踏板传感器。
硬件可能是:
0~5V ↓ MCU ADC Channel现在问一个问题:
ADC 采集到的 Raw Value 转成实际电压,应该放哪一层?
再进一步:
电压转成 0~100% 的踏板开度,又应该放哪一层?
这两个事情其实并不一样。
假设 ADC 读取:
Adc_ValueGroupType adcRaw;Adc_ReadGroup(...,&adcRaw);得到:
2456这是一个纯硬件量。
它属于 MCAL 能理解的世界。
继续往上:
2456 ↓ 2.31 V这是硬件电气量转换。
这类事情通常更适合放在:
IoHwAb / ECU Abstraction再往上:
2.31 V ↓ 43.5%这时候就开始接近功能定义了。
比如踏板的:
0.5V = 0% 4.5V = 100%还可能包含:
死区 合理性判断 双路踏板校验 故障降级这些已经越来越接近应用功能。
最终可能变成:
ADC Raw ↓ MCAL ↓ Voltage ↓ IoHwAb ↓ Pedal Position ↓ RTE ↓ TorqueControl SWC但实际项目里,到底在哪里做电压转换、在哪里做百分比换算,并没有一句放之四海而皆准的答案。
要看:
公司软件架构 功能安全分区 软件复用要求 具体传感器设计但判断原则还是一样:
越靠近硬件的知识,越往下放。
越靠近车辆功能的知识,越往上放。
这个原则非常实用。
八、CAN 更能说明“为什么不能乱跨层”
再看大家最熟悉的 CAN。
假设 VCU 要发送:
VCU_HVStatus很多人刚接触 AUTOSAR 时,会看到下面几个函数:
Can_Write()CanIf_Transmit()Com_SendSignal()Rte_Write()然后开始混乱:
到底应该用哪个?
答案其实不是看“哪个函数能把报文发出去”。
而是看:
你现在站在哪一层。
如果你在 ASW
比如 VehicleStateManager 算出来:
VCU_HVStatus = HV_READY你应该表达的是:
我的应用信号变成 HV_READY 了。
所以应用层更合理的是:
Rte_Write_VCU_HVStatus(HV_READY);它不应该知道:
CAN ID PDU Hardware Object CAN Controller到 COM
COM 负责的是:
Signal I-PDU Update Bit Timeout Signal Group Transmission Mode所以它看到的是:
VCU_HVStatus和:
这个 Signal 属于哪个 I-PDU到 PduR
PduR 更不关心:
VCU_HVStatus 是不是高压状态它只关心:
这个 PDU 应该往哪里路由。
到 CanIf
CanIf 开始知道:
这个 PDU 应该走哪个 CAN Driver 哪个 Tx PDU 哪个 Controller但它依然不会直接去操作 CAN Controller 寄存器。
最后到 Can Driver
真正调用硬件的才是:
Can_Write()这时候已经到了 MCAL。
所以一条完整发送链路大概是:
ASW │ │ Rte_Write ▼ RTE │ ▼ COM │ ▼ PduR │ ▼ CanIf │ ▼ Can Driver │ ▼ CAN Controller现在再回头看一个问题:
ASW 能不能直接调用 Can_Write()?
技术上:
很多情况下你确实能做到。
架构上:
基本不应该这么干。
因为一旦这么做,ASW 就开始知道:
Hardware Object PDU Handle CAN Driver Controller应用和底层立刻耦合起来。
九、Can_Write 能调用,不代表你就该调用
这个问题其实很值得单独说一下。
实际开发中经常会出现一种情况:
编译能过 功能能跑 测试也没问题于是大家觉得:
这代码没问题。
但 AUTOSAR 项目里,“能不能跑”和“架构对不对”其实是两个问题。
例如:
Dem_SetEventStatus(...);某些项目里 ASW 直接调用它可能完全能工作。
但如果整个项目定义的是:
SWC ↓ RTE ↓ Dem那应用直接调用:
Dem_SetEventStatus()就相当于:
绕过 RTE这未必会立刻产生 Bug。
但会留下几个问题:
软件组件失去独立性 代码复用困难 依赖关系变复杂 软件组件测试困难 架构规则越来越乱所以 AUTOSAR 项目里经常需要区分两个概念:
Functionally Correct 和 Architecturally Correct也就是:
功能上是对的不代表:
架构上是对的做大型 ECU 项目,这个区别非常重要。
十、NvM 也是一样
再举一个很典型的例子。
应用软件需要保存:
驾驶模式记忆比如用户上次选择的是:
SPORT下次上电以后还要恢复。
最粗暴的方法:
ASW ↓ 直接写 Flash理论上完全可以。
但如果这么做,应用就必须知道:
Flash地址 擦写方式 Sector Page 写入时序 数据有效性 掉电保护这些全部变成应用层的负担。
所以 AUTOSAR 才会设计:
ASW ↓ RTE ↓ NvM ↓ MemIf ↓ Fee / Ea ↓ Fls / Eep ↓ Hardware应用只想表达:
把这个数据保存下来。
至于底下到底:
什么时候写 写到哪里 怎么做冗余 怎么做 Block 管理不应该由 VehicleMode SWC 来操心。
十一、Service Layer 为什么看起来特别杂?
到这里再回头看 Service Layer,理解起来会比背定义简单很多。
因为这一层其实放的是 ECU 的各种“公共能力”。
比如:
Dem Dcm NvM ComM EcuM BswM COM PduR这些模块彼此做的事情完全不一样。
所以第一次看 AUTOSAR 架构图时,会觉得:
为什么这些乱七八糟的东西都放在 Service Layer?
其实它们有一个共同点:
都不是某一个具体应用功能独享的。
比如 Dem。
不是只有:
TorqueControl需要故障管理。
可能:
ThermalManagement VehicleState Charging EnergyManagement全部都需要。
NvM 也一样。
COM 也一样。
ComM、EcuM 更明显。
它们提供的是:
整个 ECU 共用的基础软件能力。
所以叫 Service。
从这个角度看,就比死记“Service Layer 里面有哪些模块”自然很多。
十二、外部芯片到底算 MCAL,还是 ECU Abstraction?
这个问题在实际项目里非常常见。
比如 ECU 上有一颗 SBC:
TLE8888通过 SPI 和 MCU 通信。
那 TLE8888 Driver 属于 MCAL 吗?
一般来说:
不是标准 MCAL。
因为 MCAL 主要面对的是 MCU 内部外设。
比如:
SPI ADC CAN PORT PWMTLE8888 是 ECU 板子上的外部器件。
它可能通过:
SPI Driver去访问。
关系更像:
Application / BSW ↓ TLE8888 Driver ↓ SPI ↓ MCU QSPI这里 SPI 属于 MCAL。
而 TLE8888 的控制逻辑,根据具体软件架构,可能被放在:
ECU Abstraction或者:
CDD十三、什么时候会用 CDD?
CDD 也就是:
Complex Device Driver。
项目里经常遇到一些硬件或者功能,AUTOSAR 标准模块并没有很好地覆盖。
比如:
特殊电源芯片 特殊 AFE 定制 ASIC FPGA 复杂硬件控制 强实时功能这时候可能就会做一个 CDD。
比如:
ASW ↓ RTE ↓ CDD_TLE8888 ↓ SPI ↓ MCU或者某些 CDD 直接访问硬件。
这也是 AUTOSAR 很现实的一面。
它并没有要求:
所有东西必须严格按照标准模块一级一级走。
而是:
标准能够解决的,尽量走标准架构。
特殊问题,允许通过 CDD 等方式解决。
否则很多真实 ECU 根本没法做。
十四、实际项目里最怕的不是跨层,而是“没人知道为什么跨层”
这里说一个比较实际的经验。
做 AUTOSAR 项目,千万不要把“不能跨层”理解成绝对铁律。
真实项目一定会存在一些:
特殊需求 性能要求 历史代码 供应商限制 芯片限制 Bootloader接口 复杂驱动 功能安全设计导致某些调用并不完全符合最漂亮的架构图。
这很正常。
真正的问题不是:
有没有跨层。
而是:
为什么跨层,有没有明确设计。
如果大家都知道:
这个模块因为 XX 性能原因, 经架构评审允许直接访问 XX Driver。这是设计。
但如果工程里面变成:
这里为了方便直接调一下 CanIf。 那里为了省事直接调一下 Dio。 再那里直接操作寄存器。做着做着就会发现:
软件分层只剩下 PPT 里的架构图。
代码里面已经完全不是那么回事了。
这才是最麻烦的。
十五、判断一段代码放哪层,我一般会问三个问题
实际开发的时候,我觉得没有必要把问题搞得特别复杂。
看到一段代码,先问三个问题。
第一个问题:它知道具体硬件吗?
比如代码里面出现:
P33.0 ADC Group 3 CAN Controller 1 SPI Channel 2 GTM TOM那它大概率已经非常靠近:
MCAL / Driver第二个问题:它知道 ECU 上这个硬件是干什么的吗?
比如:
KL15 BrakeSwitch PowerRelay WakeupPin SensorSupply已经开始从:
MCU资源变成:
ECU硬件功能那通常已经到了:
ECU Abstraction / IoHwAb / CDD这个范围。
第三个问题:它是不是在决定车辆应该怎么做?
比如:
KL15有效以后是否进入Ready SOC低于多少开始限扭 什么条件允许上高压 什么时候允许快充 什么时候进入故障降级这种代码基本已经属于:
ASW这三个问题在实际工作中非常好用。
十六、再把 KL15 从头走一遍
现在重新看文章开头那个需求:
KL15 接在 P33.0。
KL15 有效以后,让 VCU 进入唤醒状态。
合理的软件责任可以拆成:
MCAL
负责:
读取 P33.0例如:
Dio_ReadChannel(...)它不管:
这个 Pin 为什么要读。ECU Abstraction / IoHwAb
负责:
P33.0 ↓ KL15 State它知道:
这个硬件输入在这块 ECU 上代表 KL15。
RTE
负责:
把 KL15 State 送给需要它的 SWCASW
负责:
KL15 = ON ↓ Vehicle State Transition它知道:
KL15 有效以后整车应该怎么运行。
但不知道:
KL15 到底接在哪个 Pin 上。
这样整个软件边界就非常清楚。
十七、再看一次 ADC
同样:
MCU ADC Channel ↓ Adc Driver ↓ IoHwAb ↓ RTE ↓ Application各层关心的事情分别是:
Adc Driver “这个 ADC Channel 采到了多少?”↓
IoHwAb “这个值代表某一路 ECU 模拟输入。”↓
ASW “这个传感器现在是什么状态, 接下来车辆应该怎么控制?”十八、再看一次 CAN
CAN Controller ↓ Can Driver ↓ CanIf ↓ PduR ↓ COM ↓ RTE ↓ ASW各层继续逐渐忘掉硬件细节。
越往上:
CAN Controller Hardware Object PDU I-PDU Signal Vehicle Function你会发现这是一个很有意思的过程。
AUTOSAR 从下往上,其实一直在做一件事:
不停地把“硬件语言”翻译成“功能语言”。
最下面说:
PORT33.0 = HIGH中间变成:
KL15 = ON最上面变成:
车辆应该唤醒CAN 也是一样。
最下面是:
Controller / Mailbox / Frame中间是:
PDU / Signal最上面变成:
SOC VehicleSpeed TorqueRequest HVStatus如果从这个角度理解 AUTOSAR,很多东西就不需要硬背了。
十九、为什么大型 ECU 必须这么折腾?
看到这里,还是会有人问:
直接写不是简单很多吗?
是。
小项目绝对是直接写更简单。
比如:
一个 MCU 几十个 IO 一条 CAN 几千行代码你完全可以:
ReadPin();ReadAdc();SendCan();很快就做完了。
但是实际量产 ECU 往往是:
几百甚至上千个 CAN Signal 几十个诊断 DID 几百个 DTC 多路 CAN / LIN 大量 IO / ADC / PWM Bootloader 网络管理 睡眠唤醒 多核 功能安全 几十甚至上百个应用组件而且还有:
应用供应商 BSW供应商 MCAL供应商 整车厂 Tier1 测试团队 标定团队共同开发。
这个时候真正麻烦的已经不是:
代码能不能写出来。
而是:
这么多人写的代码怎么保证互相不搅在一起。
AUTOSAR 分层的价值就在这里。
二十、所以 MCAL、ECU Abstraction、Service、RTE、ASW 到底是什么关系?
看到最后,其实可以不用再背一遍定义。
只需要记住这一条变化:
硬件资源 ↓ MCAL ↓ ECU硬件功能 ↓ ECU Abstraction ↓ 公共基础能力 ↓ Service Layer ↓ RTE ↓ 车辆功能 ↓ ASW不过再次强调:
这不是严格的调用顺序图。
真实 AUTOSAR 里面存在不同的软件路径。
比如 CAN:
ASW ↓ RTE ↓ COM ↓ PduR ↓ CanIf ↓ Can而 IO 可能是:
ASW ↓ RTE ↓ IoHwAb ↓ DioNvM 又是:
ASW ↓ RTE ↓ NvM ↓ MemIf ↓ Fee ↓ Fls所以不要试图把所有 AUTOSAR 模块强行排成一条直线。
真正需要理解的是:
每一层负责什么信息,每一层不应该知道什么信息。
最后
上一篇我们看 AUTOSAR 架构图的时候,更多是在认地图:
这里是 ASW 这里是 RTE 这里是 Service 这里是 ECU Abstraction 这里是 MCAL这一篇真正想说的是:
地图不是拿来背的。
实际做项目的时候,更有价值的问题应该是:
我现在这段代码,到底属于硬件资源、ECU硬件功能,还是车辆功能?
一旦这个问题能够想清楚,后面再去看:
Dio Adc CanIf PduR COM NvM Dem Dcm就会轻松很多。
因为它们不再是一堆缩写。
你会开始知道:
它为什么在这里。
以及:
它为什么不能随便跑到另外一层去。
下一篇预告
AUTOSAR 里的 SWC、Runnable、Port、Interface 到底是什么?
前面几篇我们一直在讲:
ASW RTE BSW下一篇开始正式进入应用软件。
比如:
一个 SWC 到底是什么? Runnable 是不是一个普通 C 函数? Sender/Receiver Port 是怎么通信的? Client/Server 又是什么? Rte_Read、Rte_Write 到底是怎么生成出来的?这些东西理解以后,才算真正开始走进 AUTOSAR 应用软件开发。
车控码农 · AUTOSAR 从 0 到 1
尽量不背概念。
从真实 ECU 开发的问题出发,一点一点把 AUTOSAR 拆开。