简介:面向Android车机开发者的高通车载影像系统EVS工程源码,聚焦车载相机在信息娱乐与驾驶辅助场景下的落地实现,适合具备一定Android HAL与C++基础、希望深入理解车载影像链路的工程师学习参考。资源共124个文件,以h头文件与cpp源文件为主体,辅以mk编译脚本、rc启动配置、xml与json配置及license、notice等说明文件,压缩包约366KB,体量轻便便于通读。内容涵盖EVS1.0与EVS2.0在硬件抽象层的实现差异、相机枚举与状态控制、配置管理及渲染封装等模块,读者可据此梳理HAL接口调用关系、理解rc文件对开机启动方式的控制逻辑,并借鉴图像增强、降噪与多路摄像头接入的组织思路。已有1651人学习下载,适合作为车机影像系统入门与二次开发的对照材料。
1. 车机影像系统落地:EVS 到底解决了什么
做车机 Android 开发的人,绕不开一个场景:挂倒挡的瞬间,中控屏必须在几百毫秒内切出后视画面,晚一点用户就开始骂。这个「快」不是靠应用层优化能解决的,它要求从摄像头出图到屏幕显示整条链路都走一条低延迟通道。EVS(Exterior View System,车外影像系统)就是高通车机平台上干这件事的那套东西——它把倒车影像、环视拼接、盲区监测这些车外相机功能,从普通 Android Camera 框架里拆出来,走一条更贴近底层的路径。
这份资源围绕高通车机 Android 平台的 EVS 展开,适合两类人:一是刚接触车机影像、想知道 EVS 和手机 Camera 开发差在哪的 Android 工程师;二是已经在做车载相机、被启动延迟和 HAL 对接折腾过的从业者。它解决的核心问题是「车外相机怎么在车机 Android 上稳定、低延迟地跑起来」,而不是教你调美颜参数。下面按「这套东西是什么 → 怎么接起来 → 坑在哪 → 怎么验证」的顺序拆。
2. EVS 与 Android Camera 框架的分层关系:为什么不能直接套手机那套
2.1 从 CameraService 到 EVS 的链路差异
手机上的相机开发,路径基本是 App → Camera2/CameraX → CameraService → Camera HAL → 驱动。这条链路功能全、抽象好,但代价是层级多、启动慢。车机上如果倒车影像也走这条路,冷启动动辄一两秒,用户早就挂完挡了。EVS 的思路是把车外相机单独拎出来,在 HAL 层之上提供一个更薄的接口,让显示端能更快拿到帧。
常见做法是:EVS 在系统里以独立服务的形式存在,相机数据从驱动经 EVS HAL 出来,直接喂给一个专用的显示 surface,绕开普通应用相机的完整管线。这里的关键不是「EVS 比 Camera2 高级」,而是「它砍掉了车外影像不需要的那部分抽象」。你要理解的第一件事是分层:底层还是那套相机硬件和驱动,中间是 EVS HAL 做格式转换和缓冲管理,上层是 EVS 服务和应用负责取流、拼接、显示。
对开发者来说,这意味着两套代码风格。普通相机你关心的是 CameraCharacteristics、CaptureRequest;EVS 你关心的是 HAL 接口定义、buffer 分配、显示时序。混着写会翻车,我见过有人想在 EVS 应用里直接 open 一个 Camera2 设备,结果两边抢同一个 sensor,画面直接黑掉。
2.2 EVS HAL 接口与数据流的关键结构
EVS HAL 的接口定义通常围绕几个核心动作:枚举可用相机、打开相机、设置输出缓冲、启动取流、拿到帧后交给显示。数据流上,帧一般以某种 YUV 或 RGB 格式在 buffer 里流转,buffer 的分配策略直接决定延迟和内存占用。这里给一段示意性的接口调用骨架,帮助理解调用顺序(具体函数名以你手上的 HAL 头文件为准):
// EVS HAL 调用顺序示意:枚举 -> 打开 -> 配流 -> 取帧 // 注意:不同平台头文件命名有差异,以下为结构示意 std::unique_ptr<IEvsEnumerator> enumerator = IEvsEnumerator::getService("evs_hal"); std::vector<CameraDesc> cameras; enumerator->getCameraList(cameras); // 1. 拿到车外相机列表 auto camera = enumerator->openCamera(cameras[0].id); // 2. 打开目标相机 StreamCfg cfg = { .width = 1280, .height = 720, .format = HAL_PIXEL_FORMAT_YCRCB_420_SP, // 车机常用 NV21 类格式 .usage = GRALLOC_USAGE_HW_CAMERA_WRITE | GRALLOC_USAGE_HW_TEXTURE }; camera->setMaxFramesInFlight(2); // 3. 限制在途帧数,压延迟 camera->startVideoStream(cfg, frameHandler); // 4. 启动取流,回调交帧这段代码的逻辑说明:第一步枚举相机,车机上可能有后视、前视、左右后视多个,靠 id 或位置字段区分;第二步打开,注意打开是有状态的,重复打开同一路会失败;第三步配流,格式和 usage 标志要和显示端匹配,否则会出现「有帧但显示不出来」的黑匣子现象;第四步启动,回调里拿到的帧要尽快消费,不能在里面做耗时操作。参数上,setMaxFramesInFlight是压延迟的关键旋钮,设太大延迟高,设太小容易丢帧,一般 2 到 3 之间试。
2.3 显示端与 EVS 的对接方式
EVS 的帧最终要显示,显示端通常是一个专用 surface 或者一个轻量应用。对接时最容易出问题的是格式和同步。格式上,相机出的 YUV 和显示端期望的格式不一致,就得在中间做转换,转换放在哪一层很讲究:放 HAL 里省应用层 CPU,但增加 HAL 复杂度;放应用层灵活,但多一次拷贝。同步上,车机影像对「帧和屏幕刷新对齐」有要求,处理不好会有撕裂感。
我一般会先把显示端做成一个最小可用的 surface,只验证「能出图」,再逐步加拼接、加叠加层。上来就搞环视拼接,出了问题根本分不清是取流错了还是拼接错了。这一步的验证方法很简单:固定显示一帧纯色或测试图,确认链路通了,再换真实相机。
3. 从零接一路后视相机:配置、取流与显示的最小闭环
3.1 环境与依赖确认
动手前先确认三件事:平台是不是高通车机方案、系统里有没有 EVS 相关服务和 HAL 库、你有没有系统权限。EVS 不是普通应用能随便碰的,很多接口要求系统级权限或者直接在 native 层跑。常见做法是先在设备的 vendor 分区或系统库里找 EVS 相关的 so 和 service 声明,确认它们存在且被 init 拉起。
# 确认 EVS 服务与 HAL 库是否存在(示意,路径以实际平台为准) adb shell getprop | grep -i evs adb shell ls /vendor/lib64/ | grep -i evs adb shell dumpsys | grep -i evs这三条命令分别看属性、看库、看服务状态。如果属性里没有任何 evs 相关项,说明这套系统可能没启用 EVS,后面所有工作都无从谈起,先找平台方确认。如果库在但服务没起,去看 init rc 里的启动配置。这一步别跳过,我见过有人写了半天应用代码,最后发现设备根本没编进 EVS 服务。
3.2 相机枚举与参数读取
确认环境后,第一步是枚举相机并读出每路的能力参数。车外相机和手机相机不同,它的参数往往在 HAL 或配置文件里写死,比如分辨率、帧率、镜头朝向。你需要拿到这些参数才能决定配流参数。
// 枚举并打印相机能力,确认朝向和分辨率 for (const auto& cam : cameras) { ALOGI("camera id=%s pos=%d", cam.id.c_str(), cam.position); for (const auto& cap : cam.caps) { ALOGI(" %dx%d fmt=0x%x", cap.width, cap.height, cap.format); } }逻辑说明:position字段用来区分前后左右,配流时按业务选对应相机;caps里列出支持的分辨率和格式,配流参数必须落在这个集合里,否则打开流会失败。参数上,后视相机常用 1280x720 或 1920x1080,帧率 30fps 居多。如果你的业务要拼接环视,四路相机的分辨率和格式最好统一,不统一会在拼接阶段引入缩放和格式转换,延迟和画质都受影响。
3.3 配流、取流与首帧显示
最小闭环的目标是:打开一路相机,配好流,拿到第一帧并显示出来。这一步跑通,后面加功能才有基础。
// 配流并等待首帧,带超时保护 camera->setMaxFramesInFlight(2); camera->startVideoStream(cfg, [](const BufferDesc& buf) { // 回调里只做移交,不做耗时处理 displayQueue.push(buf); // 交给显示线程消费 }); // 首帧超时保护:车机场景不能无限等 if (!firstFrameReceived.wait_for(std::chrono::milliseconds(500))) { ALOGE("first frame timeout, check stream cfg or sensor power"); }逻辑说明:回调里把 buffer 推进队列就返回,真正的显示在另一个线程做,避免阻塞取流;首帧等待加超时,是因为车机场景下 sensor 上电、流启动都可能出问题,无限等会让整个模块卡死。参数上,500ms 是个经验值,冷启动可能更久,但超过这个数基本说明有问题。常见失败点:配流格式和显示端不匹配导致「有帧无图」、sensor 没上电导致一直无帧、权限不足导致打开失败。每个失败点看日志的关键字不一样,配流失败看 HAL 返回码,无帧看 sensor 驱动日志。
3.4 延迟测量与基线建立
闭环跑通后,先别急着加功能,测一下端到端延迟,建立基线。方法可以是打时间戳:在帧回调里记一个时间,在显示提交时再记一个,差值就是软件侧延迟。硬件侧的真实延迟要靠外部手段,但软件基线足够你判断优化有没有效果。
// 软件侧延迟采样:回调时间 -> 显示提交时间 auto t1 = now(); displayQueue.push({buf, t1}); // 显示线程 auto item = displayQueue.pop(); auto t2 = now(); ALOGI("frame latency=%lld us", duration_cast<microseconds>(t2 - item.t1).count());这个测量不精确,但能反映趋势。基线建立后,后面调setMaxFramesInFlight、改 buffer 数量、换格式,都能用这个数对比。我一般会把基线记下来,优化前后各跑几十帧取中位数,避免单帧抖动误导判断。
4. 环视拼接与多路相机同步:四个容易翻车的点
4.1 多路取流的线程与缓冲管理
环视要同时取四路相机,如果每路一个线程各自阻塞,很容易把 CPU 吃满还丢帧。常见做法是统一的事件循环或者固定线程池,每路相机的回调只做入队,消费端按显示节奏取最新帧。缓冲管理上,每路分配固定数量的 buffer 循环使用,不要动态申请,车机上动态内存分配带来的抖动很致命。
// 四路相机共享一个消费循环,每路固定 buffer 池 for (int i = 0; i < 4; ++i) { pools[i].init(kBufferCount); // 固定数量,循环复用 cameras[i]->startVideoStream(cfgs[i], [i](const BufferDesc& b) { pools[i].pushLatest(b); // 只保留最新帧,旧帧丢弃 }); }逻辑说明:pushLatest的语义是「新帧覆盖未消费的旧帧」,环视显示只关心最新画面,积压旧帧只会增加延迟。参数上,kBufferCount一般 3 到 4 个,太少容易丢帧,太多增加延迟。这里有个反直觉的点:多路场景下「不丢帧」不是目标,「不积压」才是目标,主动丢旧帧反而让画面更跟手。
4.2 时间戳对齐与拼接时序
四路相机的帧到达时间不可能完全一致,拼接前要按时间戳对齐,否则拼出来的画面会有错位。常见做法是给每帧打上硬件或软件时间戳,拼接时取时间最接近的一组。如果某一路长期偏慢,要查那一路的 sensor 配置或链路,而不是在拼接层硬凑。
| 对齐策略 | 适用场景 | 代价 |
|---|---|---|
| 按时间戳取最近组 | 四路帧率一致 | 需要缓存少量帧 |
| 固定延迟对齐 | 某路稳定偏慢 | 整体延迟增加 |
| 不严格对齐 | 对拼接精度要求低 | 画面可能错位 |
表格里第一种是默认选择,第二种是兜底,第三种只在演示阶段用。选型理由很简单:车机环视对错位很敏感,宁可多等一两帧也不能拼歪。
4.3 拼接后的显示与叠加层
拼接完成后的图像要送显示,通常还要叠加倒车轨迹线、雷达提示这些图层。叠加层建议在 GPU 侧做,不要用 CPU 逐像素画,车机 GPU 资源相对宽裕,CPU 反而紧张。叠加层的坐标要跟拼接后的画面坐标系对齐,这里容易出的错是:拼接时做了缩放,叠加层还按原始分辨率算坐标,结果轨迹线飘到画面外。
4.4 多路场景下的资源竞争
四路相机同时跑,带宽、内存、GPU 都在抢。常见翻车是某一路突然掉帧,查半天发现是另一路配了过高分辨率把带宽吃了。解决办法是给每路设上限,配流前先算总带宽预算。这个预算没有统一公式,得看平台手册和实测,我一般会从保守配置开始,逐路加码观察稳定性。
5. 避坑与排查:EVS 落地时最常踩的五个坑
5.1 现象:应用能打开相机但一直无帧
原因通常是 sensor 没上电或者配流参数不被支持。先看 HAL 日志里配流返回码,再看 sensor 驱动有没有上电记录。解决:把配流参数改成caps里明确列出的组合,确认电源管理没把这一路关掉。
5.2 现象:有帧但屏幕黑屏
原因是帧格式和显示端 surface 格式不匹配,数据进去了但显示端解析不出来。解决:统一格式,或者在中间加一层明确的格式转换,别指望显示端自动兼容。查的时候打印实际 buffer 格式和 surface 配置对比。
5.3 现象:倒车切换慢,挂挡后要等一两秒
原因是取流没预热,挂挡才开始打开相机。解决:常驻低功耗取流或者在系统侧做预打开,把冷启动成本提前消化。参数上可以先把流以低帧率跑着,切换时提帧率,比从零打开快得多。
5.4 现象:多路环视偶发撕裂
原因是帧和显示刷新没对齐,或者 buffer 被提前复用。解决:检查 buffer 生命周期,确认显示消费完才归还;对齐策略上按时间戳取组,别让不同时刻的帧混拼。
5.5 现象:跑一段时间后内存上涨
原因是 buffer 池没循环复用,或者回调里持有引用没释放。解决:固定池大小、消费完显式归还,用内存工具定期看 native 堆。车机长时间运行,这类泄漏比手机更致命。
6. 验证与进阶:把 EVS 链路做成可回归的测试
功能跑通只是开始,车机影像要长期稳定,得有可回归的验证手段。我一般会做三层验证:第一层是链路自检,启动时枚举相机、配流、取一帧,确认基础通路;第二层是延迟基线回归,每次改动后跑固定帧数取中位数,超过阈值就报警;第三层是长时间稳定性,连续跑几小时看内存和丢帧率。
# 长时间稳定性观察:丢帧与内存趋势(示意) adb shell dumpsys evs | grep -E "drop|frame" adb shell dumpsys meminfo | grep -i evs这两条命令分别看丢帧统计和内存占用,隔一段时间采样对比。参数上,丢帧率超过千分之一就该查,内存持续上涨基本就是泄漏。
进阶用法上,可以把 EVS 的取流和显示封装成一个可配置的模块,相机路数、分辨率、对齐策略都做成配置项,换车型时只改配置不改代码。这样做的代价是初期多写一层抽象,收益是后面每接一个新车型能省大量调试时间。我踩过的最大教训是早期图快把参数写死在代码里,结果换一路相机就要重新编译整个镜像,血泪经验。
从那以后我每次接新车机影像,都强制先跑一遍链路自检和延迟基线,再动业务代码。希望帮到你。
本文还有配套的精品资源,点击获取