20. 仪表盘与HMI系统:仪表盘显示架构、Cluster HAL实现、OpenGL仪表盘渲染、HMI性能优化
各位同学,今天我们来聊聊仪表盘。嗯,仪表盘,说白了就是驾驶员面前那块屏幕。在Android Automotive系统里,它可不是一个普通的App。它得实时、稳定、安全。我最早接触这个模块时,也觉得不就是画个指针转一转吗?后来发现,水很深。
仪表盘系统,我们通常叫它Cluster。它和中央娱乐屏最大的区别是什么?是实时性。你想想看,车速、转速、转向灯,这些信息延迟100毫秒,驾驶员就会觉得不对劲。所以,它的架构设计,从一开始就要围绕“低延迟”和“高可靠性”来展开。
20.1 仪表盘显示架构
我个人习惯把仪表盘架构分成三层:数据层、服务层、渲染层。
- 数据层: 负责从CAN总线、传感器、车辆信号中获取原始数据。比如车速脉冲、发动机转速、电池SOC等。
- 服务层: 也就是Cluster HAL。它把原始数据转换成上层能理解的逻辑值。比如把脉冲频率换算成km/h。
- 渲染层: 负责把逻辑值画到屏幕上。这里通常用OpenGL ES或者Vulkan,直接操作GPU。
为什么不用普通的Android View系统?因为View系统有VSync机制,有BufferQueue,延迟不可控。我曾经在一个项目里试过用Canvas画指针,结果发现从数据到显示,延迟超过了200毫秒。后来全部改成了OpenGL直接渲染,延迟降到了30毫秒以内。
下面这张图,是我总结的典型仪表盘显示架构。你可以看到,数据流是单向的,而且渲染层直接与HWC(Hardware Composer)交互,绕开了App框架的UI线程。
20.2 Cluster HAL实现
Cluster HAL,全称是Cluster Hardware Abstraction Layer。它是连接车辆信号和上层应用的桥梁。在Android Automotive中,它通常以HIDL或AIDL服务的形式存在。
它的核心接口,我总结了一下,大概有这几个:
| 接口方法 | 功能描述 | 输入参数 | 输出参数 |
|---|---|---|---|
getVehicleSpeed() |
获取当前车速 | 无 | float (km/h) |
getEngineSpeed() |
获取发动机转速 | 无 | float (rpm) |
getTurnSignal() |
获取转向灯状态 | 无 | int (左/右/关闭) |
getBatteryLevel() |
获取电池电量 | 无 | int (百分比) |
setBrightness() |
设置仪表盘亮度 | int (0-255) | boolean |
实现Cluster HAL时,有几点要注意:
- 数据更新频率: 车速和转速建议每10-20毫秒更新一次。转向灯可以慢一些,50毫秒就够了。
- 数据校验: 从CAN总线来的数据,一定要做合理性校验。比如车速不可能超过300km/h,转速不可能超过10000rpm。我曾经遇到过CAN总线受到干扰,突然跳出一个负数的车速,差点把指针打飞了。
- 线程安全: HAL服务可能被多个客户端同时访问,内部数据结构要做好同步。
下面是一个简化的Cluster HAL实现片段,用AIDL定义接口:
// IClusterHal.aidl
package com.example.automotive.cluster;
interface IClusterHal {
float getVehicleSpeed();
float getEngineSpeed();
int getTurnSignal();
int getBatteryLevel();
boolean setBrightness(int brightness);
}
实现类里,我会用一个后台线程轮询CAN数据,然后缓存到本地变量。上层调用时,直接返回缓存值,避免阻塞。
20.3 OpenGL仪表盘渲染
好了,数据有了,接下来就是怎么画。我个人推荐用OpenGL ES 2.0或3.0。为什么?因为兼容性好,几乎所有车载GPU都支持。
仪表盘渲染的核心,其实就是画几个东西:
- 背景: 通常是圆形或半圆形的刻度盘。
- 刻度线和数字: 表示速度或转速的范围。
- 指针: 根据当前值旋转角度。
- 指示灯: 比如转向灯、远光灯、故障灯。
指针的旋转,说白了就是矩阵变换。我习惯用四元数或欧拉角来计算旋转矩阵。嗯,这里要注意,指针的旋转中心要设置在指针的根部,而不是中心点。
下面是一个简化的指针渲染代码片段:
// 指针顶点数据(以根部为原点)
float[] pointerVertices = {
0.0f, 0.0f, 0.0f, // 根部
-0.02f, 0.8f, 0.0f, // 左侧
0.02f, 0.8f, 0.0f // 右侧
};
// 旋转矩阵计算
float angle = (currentSpeed / maxSpeed) * 270.0f; // 假设0-270度范围
Matrix.setRotateM(modelMatrix, 0, angle, 0, 0, 1);
// 在顶点着色器中应用
gl_Position = uMVPMatrix * modelMatrix * vec4(aPosition, 1.0);
这里有个小技巧:指针的旋转角度,不要直接用原始数据。因为原始数据可能有抖动。我通常会加一个低通滤波器,或者做一次线性插值。这样指针转动起来会很平滑,不会一卡一卡的。
20.4 HMI性能优化
仪表盘的HMI性能,直接关系到用户体验。我见过一些项目,仪表盘掉帧、卡顿,甚至黑屏。嗯,这里我总结几个优化方向:
- 减少绘制调用: 每帧的绘制调用次数,尽量控制在50次以内。合并顶点数据,使用索引绘制。
- 纹理压缩: 仪表盘的背景图、刻度盘图,使用ETC2或ASTC压缩格式。减少纹理上传带宽。
- 避免CPU-GPU同步: 不要每帧都glReadPixels。这会强制CPU等待GPU完成渲染,造成流水线停顿。
- 使用帧缓冲对象(FBO): 对于静态背景,可以预先渲染到FBO中,每帧只需要绘制动态元素。
- 降低分辨率: 如果仪表盘是1920x720,但实际显示区域只有一部分,可以降低渲染分辨率,然后由HWC做缩放。
还有一个容易被忽略的点:内存分配。不要在渲染循环里new对象。我见过一个项目,每帧都new一个Matrix对象,结果GC频繁触发,导致掉帧。正确的做法是:在初始化时分配好所有对象,渲染时只复用。
最后,我想说一句:仪表盘系统,稳定压倒一切。功能可以少一点,但绝对不能出问题。因为驾驶员的眼睛,一直在看着它。
公众号:蓝海资料掘金营,微信deep3321