ARTICLE · 1103875
车载 Android 车控软件栈·第2篇 | 总览 · 车控全链路:一次调温从应用到空调控制器
导读:应用里一行
setProperty(),要穿过两条进程边界、一条芯片边界和一条车载总线,才能到达空调控制器。本篇沿"主驾调到 22℃"这一个动作走完整条链路:各层如何相连,同一个温度值在每层的形态,以及写入与回报两条路径上容易被误解的细节——setProperty()返回时,空调其实还没执行。
本文基于 Android 16 QPR2(android-16.0.0_r4,VHAL V4)源码。AOSP 范围内的类名与
文件:行号以该 tag 为准;VHAL 以下属于 vendor 与整车实现,本文只描述官方文档可以印证的通用形态。
主驾把空调温度从 20℃ 调到 22℃。手指离开屏幕,指令就已经发出;但出风口的风真正变暖,还要等空调控制器收到指令、调整风门和水阀,这中间隔着多个进程、至少两颗芯片和一条车载总线。
上一篇从 CarService 的视角给出了整个框架的全景。本篇换一个视角,沿着"一次调温"这一个动作,把从应用到空调控制器的整条链路走一遍:先看链路由哪些层组成、每层之间以什么方式相连,再看同一个温度值在每一层的形态,最后用两张时序图分别画出"指令下发"和"状态回报"。后续各篇拆解的每一个类,都能在本篇的这张图上找到位置。
一、全链路架构

整条链路可以按"谁负责"分为三段:
其中,vendor 指车机方案商或 Tier1;VHAL 实现即 IVehicleHardware 的实现。
1.1 Android 框架段:三个进程
这一段在上一篇已经给出了全景,这里只沿调温的路径列出经过的组件:
应用进程:空调应用调用 car-lib 的 CarPropertyManager.setProperty()。car-lib 是编译进应用的 Java 库,运行在应用自己的进程里。com.android.car进程:CarPropertyService接收请求并检查权限,交给 hal 层的PropertyHalService和VehicleHal,再由AidlVehicleStub调用 VHAL 的 AIDL 接口。VHAL 进程:AOSP 提供的通用层 DefaultVehicleHal实现IVehicle接口,负责请求校验、订阅管理、心跳等与具体车辆无关的逻辑,然后把请求交给 vendor 实现的IVehicleHardware。
这一段的两条进程边界都是 binder:应用与 CarService 之间是 ICarProperty,CarService 与 VHAL 之间是 IVehicle 和回调接口 IVehicleCallback。
1.2 VHAL 以下:通道、MCU 与车载网络
从 IVehicleHardware 往下,就离开了 AOSP 的范围。官方文档对 VHAL 以下的描述比较概括:VHAL 通过驱动的 ioctl 等方式与车载网络通信;与车辆的连接,既可以是应用处理器直连微控制器,也可以经由车辆微控制器(VMCU)中转。结合量产方案的常见做法,VHAL 以下通常包括三部分:
SoC 与 MCU 之间的通道。 车机的应用处理器(SoC)一般不直接连接 CAN 总线,而是通过一颗 MCU 接入车辆网络。SoC 与 MCU 之间的物理通道因平台而异,常见的有 SPI、UART、以太网,在单芯片多核或虚拟化方案中也可能是核间通信或跨虚拟机通信;通道上运行的是 vendor 自定义的私有协议,通常带有校验、确认、重传和心跳。AOSP 的参考实现中还有另一种形态: GRPCVehicleHardware把 VHAL 请求经 gRPC 转发给另一台机器或虚拟机上的服务(hardware/interfaces/automotive/vehicle/aidl/impl/current/grpc/)。MCU。 MCU 运行自己的固件,负责接入车载网络、在总线上收发报文,并与整车电源管理协同。它启动快、实时性好,在 Android 尚未启动或已经休眠时仍可工作。 车载网络与对手件。 空调控制指令最终以总线报文的形式发给空调控制器。车载网络可能是 CAN、CAN FD、LIN 或车载以太网;空调控制器收到指令后,驱动鼓风机、风门电机、水阀等执行器,并通过温度传感器闭环调节。
其中有一项分工没有固定答案:CAN 信号的编码和解码,既可以放在 VHAL 侧,也可以放在 MCU 侧。 放在 VHAL 侧,SoC 与 MCU 之间传递的是整帧报文,由 VHAL 按信号定义把物理值换算为报文中的位;放在 MCU 侧,传递的是信号级的数据,由 MCU 组帧。同一个方案里,下行和上行的分工也可能不同。这取决于整车的电子电气架构和方案设计,也是不同项目的 VHAL 实现差异最大的地方。
1.3 各条边界的连接方式
ICarProperty | ||
IVehicle | ||
其中,CarService 与 VHAL 之间还有回调接口 IVehicleCallback,这条边界同时是 system 与 vendor 的分界,接口受 VINTF 稳定性约束;VHAL 通用层指 DefaultVehicleHal,硬件实现指 IVehicleHardware;VHAL 与 MCU 之间的私有协议运行在 SPI、UART、以太网等通道上,也可能是核间或跨虚拟机通信。
一次调温的指令要依次穿过两条进程边界和一条芯片边界,再经过一条车载总线,才能到达空调控制器;前两条边界由 AOSP 定义,之后的部分由 vendor 和整车厂实现。
二、同一个温度值在各层的形态
"22℃"这个值,在每一层都以不同的形态存在。以主驾座位为例,从上到下依次是:
应用: setProperty(Float.class, HVAC_TEMPERATURE_SET, SEAT_ROW_1_LEFT, 22.0f)。属性 ID 为0x15600503,区域为主驾。car-lib → CarService: CarPropertyValue<Float>,经ICarProperty.setProperty()跨进程传递(CarPropertyManager.java:2992)。CarService 内部: HalPropValue。PropertyHalService把CarPropertyValue转换为统一的 HAL 值对象,屏蔽 AIDL 与 HIDL 两代接口的差异(PropertyHalService.java:1291-1299)。CarService → VHAL: SetValueRequest,内含VehiclePropValue。以批量请求发出,数据量大时通过共享内存传递(AidlVehicleStub.java:1082)。VHAL → vendor 实现:同一组 SetValueRequest,由DefaultVehicleHal校验后交给IVehicleHardware.setValues()(DefaultVehicleHal.cpp:607、690)。SoC → MCU:vendor 私有消息,内容是信号级数据还是整帧报文,取决于编解码放在哪一侧。 车载网络:CAN 报文中的一个信号。按信号定义的精度(factor)和偏移(offset)把 22.0 换算为原始整数,写入报文的指定位。 空调控制器:目标温度,控制器据此调节执行器。
在 Android 框架段,这个值始终保持"属性 ID + 区域 + 浮点数"的语义,只是载体从 Java 对象换成 AIDL 结构体;到了 VHAL 以下,它才被翻译成车辆网络能理解的格式。车属性是 Android 与车辆之间的抽象边界:边界之上只谈属性,边界之下才谈报文和信号。
三、写入路径:一次 setProperty 的全程
下图画出了"主驾调到 22℃"的指令从应用到空调控制器的全过程。为便于阅读,CarService 内部只保留关键节点,VHAL 以下按通用形态绘制:

这张图有两个细节需要特别注意。
第一,setProperty() 返回的时刻,只能说明请求已经发到了总线上。IVehicle 只提供批量、异步的 setValues,官方文档明确说明 get 和 set 操作可能在实际完成之前就返回,结果通过回调送达。CarPropertyManager.setProperty() 表现为同步调用,是因为 CarService 在异步接口之上等待了这个回调(AidlVehicleStub.java:363-376),最长等待 10 秒,超时则抛出错误(AidlVehicleStub.java:96-97、1225-1227)。
回调意味着什么,由接口契约规定(IVehicle.aidl:117-124):onSetValues 在请求已经发到车辆总线、或者发送失败之后调用;总线协议支持确认时,在收到确认之后调用。对 CAN 这类没有确认机制的总线,回调成功并不代表新值已经生效。在 SoC 与 MCU 分离的架构中,"发到总线"还要经过跨芯片通道,VHAL 实现依据 MCU 的反馈判断何时回调,不同方案的做法并不统一。无论哪种做法,控制器是否真正执行,都不在这个回调的语义之内。
第二,总线下发与状态回报是两件事。 空调控制器一般不会针对某一条设定指令单独应答,是否执行成功,要看它随后周期性或事件触发地发出的状态报文。这也决定了应用层判断"设置是否生效"的正确方式:订阅属性事件,以回报的值为准。
写入路径的同步返回最多代表请求已经发到总线;车辆是否真正执行,只能通过随后的状态回报确认。
四、回报路径:状态回到各个应用的过程
空调控制器调整完成后,在状态报文中带上新的设定温度。这个值要回到空调应用,也可能同时要回到 SystemUI 的状态栏、仪表或语音助手。回报路径如下:

与写入路径相比,回报路径有三个不同之处:
一对多。 写入由一个应用发起,回报则要送到所有订阅了该属性的客户端。多个应用对同一属性的订阅,在 CarService 内部合并成一份再下发给 VHAL,采样率取最大值;事件回来后,再由 CarPropertyService按各客户端的订阅分发(CarPropertyService.java:1055)。不等待。 CarService 发往应用的回调是 oneway 调用,发出即返回,一个处理缓慢的应用不会阻塞其他应用。 沿途可能过滤。 VHAL 以下的实现通常会做"值未变化不上报"; DefaultVehicleHal按各客户端的订阅参数分发,硬件实现指定了批处理窗口时,还会把窗口内的事件合并为一次回调(参考实现默认不批处理,IVehicleHardware.h:151-154);CarService 按每个客户端要求的频率过滤。应用收到的事件,是经过层层筛选之后的结果。
如果控制器的状态报文长时间没有到达,常见的做法是由 VHAL 实现把相关属性置为不可用状态,应用据此可以知道信号已经失效,而不是继续显示一个过期的值。
回报路径是一对多的:同一个状态从控制器出发,经 VHAL、CarService 逐层分发,送达每一个订阅了它的应用。
五、链路上的关键设计点
把写入和回报两条路径放在一起看,可以归纳出几个贯穿整条链路的设计点。它们在后续篇目中都会展开:
异步语义。 从 IVehicle接口开始,写入就是异步的;越往下,"调用返回"与"车辆执行"之间的距离越远。应用应当以属性事件作为执行结果的依据,对时延敏感的调用使用setPropertiesAsync()。订阅合并。 应用发起的订阅先在 car-lib 中按进程合并,再在 CarService 中跨进程合并,VHAL 看到的是 CarService 合并后的一份订阅;VHAL 再把 CarService 与其他 native 客户端的订阅合并后交给硬件实现。后续篇目会逐层展开。 权限集中。 应用不能绕过 CarService 直接访问 VHAL,SELinux 策略只允许声明为 VHAL 客户端的系统组件访问它。控制空调需要 CONTROL_CAR_CLIMATE权限,由 CarService 统一检查。故障的传播。 VHAL 进程死亡时,CarService 不重连而是整体重启,并波及所有客户端;跨芯片通道中断或总线报文超时,则通常表现为属性变为不可用。这两类故障在应用侧的表现不同,排查时需要区分。 system 与 vendor 的分界。 IVehicle是 VINTF 稳定接口,把随 Android 版本升级的框架段与随车型变化的 vendor 实现隔开;车型差异被收敛在IVehicleHardware的实现和 VHAL 以下的通道与信号定义中。
结语
一次调温,在应用层只是一行 setProperty();在链路上,它是两次跨进程调用、一次跨芯片传输和一帧总线报文,结果再沿另一条路径回到每一个关心它的应用。本篇的架构图和两张时序图,是后续各篇的地图。后续篇目将从这条链路的最上端 car-lib 开始,逐层深入。
参考资料
Vehicle system isolation:VHAL 与车载网络、VMCU 的关系 Vehicle HAL overview、VHAL interface:VHAL 接口与 get/set 的异步语义 Reference implementation: DefaultVehicleHal与IVehicleHardware的两层结构、GRPCVehicleHardwareUse VHAL with the native client:应用应通过 Car API 访问 VHAL CarPropertyManager: setProperty与异步接口的说明AOSP 源码( android-16.0.0_r4):packages/services/Car、hardware/interfaces/automotive/vehicle
如果觉得有帮助,欢迎点赞、在看、转发三连。
系列导航:《车载 Android 车控软件栈》从 CarService 全景出发,沿应用 → car-lib → CarService → VHAL 这条链路逐层拆解 Android Automotive 的车控框架;另一个系列《车载 Camera 软件栈》讲应用层如何获取摄像头画面。公众号点「合集」看全部。