简介:面向Android车机开发者的高通车载影像系统EVS工程源码,聚焦车载相机在信息娱乐与驾驶辅助场景下的落地实现,适合具备一定Android HAL基础、希望深入理解车载影像链路的工程师学习参考。压缩包共124个文件,约366KB,以46个h头文件与43个cpp源文件为主体,承载EVS HAL接口定义与相机枚举、状态控制、渲染等核心逻辑;另有8个mk构建脚本、5个rc启动配置及xml、json等配置资源,便于直接编译并调整开机启动方式。资源同时覆盖EVS1.0与EVS2.0两代实现的差异,涉及硬件抽象层适配、图像增强与降噪处理、多路摄像头接入等关键环节。已有1651人学习下载,读者可借此梳理车载影像系统的模块划分与调用关系,理解HAL如何屏蔽底层硬件差异,并参考rc配置掌握服务启动控制方法,为后视、环视及ADAS相关功能的开发与移植提供可复用的工程范本。
1. 车机倒车影像为什么总慢半拍:从一次冷启动说起
冬天早上第一次挂倒挡,中控屏要黑上两秒才出画面,这种体验在不少车机上真实存在。问题往往不在摄像头,而在 Android 车机里那套 EVS(Exterior View System)影像通路的启动时序——相机节点、HAL 服务、显示合成三者的初始化顺序稍有错位,第一帧就要等。这份高通车机 Android 车载影像系统 EVS 的工程代码,拆的就是这条链路:从 Camera HAL 到 EVS Manager,再到 SurfaceFlinger 合成上屏,覆盖倒车影像、环视拼接、多路相机切换几个典型场景。适合正在做 android 车机 车载相机 适配的开发者,尤其是需要把 EVS 跑通、再按车型改配置的人。下面按「能干什么 → 怎么跑 → 怎么改」的顺序拆。
2. 工程结构与影像通路:EVS 在车机里到底占哪几层
2.1 从目录看模块边界
拿到源码先别急着编译,先看目录怎么分层。车机 EVS 工程一般沿用 Android 的 vendor 分区组织方式,核心目录大致是这几块:
evs_project/ ├── device/ # 板级配置,车型差异集中在这里 │ └── <platform>/ │ ├── evs/ │ │ ├── config/ # 相机数量、分辨率、显示区域配置 │ │ └── sepolicy/ # evs 相关 SELinux 策略 │ └── manifest.xml # HAL 服务注册 ├── vendor/ │ └── evs/ │ ├── hal/ # EVS Camera/Display HAL 实现 │ ├── manager/ # EVS Manager 服务,负责相机枚举与状态 │ └── app/ # 倒车/环视应用层 └── frameworks/ └── base/ └── services/ └── car/ # CarService 中与 EVS 联动的部分这个结构的关键在于:车型差异全部收敛到 device 目录,HAL 和 Manager 保持平台无关。我一般会先确认device/<platform>/evs/config下的配置文件,因为相机路数、每路的分辨率、显示布局都从这里读,改车型基本只动这一层。sepolicy 单独放,是因为 EVS 服务要访问/dev/video*节点和显示设备,权限没配对,服务起来就挂。
2.2 一条帧从相机到屏幕的完整链路
EVS 的影像通路可以拆成四段,每段都有明确的进程边界:
第一段是Camera HAL 采集。EVS Camera HAL 实现IEvsCamera接口,内部通常用 V4L2 打开/dev/videoN,配置格式(常见是 NV12 或 YUYV),然后通过startVideoStream把帧回调给上层。这里有个细节:EVS 要求低延迟,所以 HAL 里一般用VIDIOC_DQBUF的非阻塞模式轮询,而不是等中断。
第二段是EVS Manager 分发。Manager 是个常驻服务,负责枚举可用相机、维护每路相机的订阅者列表。倒车应用和环视应用可能同时要同一路相机,Manager 做引用计数,最后一个订阅者退出才真正停流。
第三段是应用层取帧与处理。倒车应用拿到IEvsCameraStream回调后,把帧交给 GPU 做格式转换或拼接(环视场景),再送显示。
第四段是EVS Display 上屏。EVS 有自己的 Display HAL,绕过 SurfaceFlinger 直接送 framebuffer,这是低延迟的关键——少一层合成就少几毫秒。
// 简化后的帧回调链路,HAL 侧 class EvsCameraStream : public IEvsCameraStream { Return<void> deliverFrame(const BufferDesc& buffer) override { // buffer.memHandle 是 gralloc 句柄,直接映射避免拷贝 void* vaddr = nullptr; mapper_->lock(buffer.memHandle, GRALLOC_USAGE_SW_READ_OFTEN, &vaddr); // 交给下游:格式转换或直接送 display processFrame(vaddr, buffer.width, buffer.height, buffer.format); mapper_->unlock(buffer.memHandle); // 归还 buffer 给 HAL,否则几帧后就会卡住 camera_->doneWithFrame(buffer); return Void(); } };这段代码的逻辑是:deliverFrame是 HAL 主动回调,帧数据通过 gralloc 句柄传递,避免大块内存拷贝。lock拿到虚拟地址后处理,处理完必须doneWithFrame把 buffer 还回去——这是 EVS 的 buffer 循环机制,不还就会在几帧之后彻底停流,是新手最容易翻车的地方。参数上,GRALLOC_USAGE_SW_READ_OFTEN表示 CPU 要频繁读,如果只做 GPU 处理可以换成对应的 GPU 用法标志,减少 cache 操作。
2.3 编译与启动:把 EVS 服务拉起来
编译前先确认两件事:板级配置里相机节点存在(ls /dev/video*),以及 manifest 里注册了 EVS HAL 服务。编译命令按 Android 版本略有差异,常见做法是:
# 在源码根目录 source build/envsetup.sh lunch <product>-userdebug # 单独编 EVS 相关模块,比整编快得多 m EvsCameraService EvsManager evs_app # 推送到设备 adb root && adb remount adb push out/target/product/<product>/vendor/bin/evs_* /vendor/bin/编完先别急着测倒车,手动起服务验证链路:
# 起 HAL 服务 adb shell /vendor/bin/evs_camera_service & # 起 Manager adb shell /vendor/bin/evs_manager & # 看服务是否注册成功 adb shell dumpsys evsdumpsys evs会列出当前枚举到的相机、每路的订阅状态和帧率。如果相机列表为空,先查 sepolicy 有没有拒绝(adb shell dmesg | grep avc),再查/dev/video*权限。这一步跑通,说明 HAL 和 Manager 的链路是活的,剩下的就是应用层和显示。
3. 核心模块拆解:HAL 采集、Manager 分发与显示合成
3.1 EVS Camera HAL 的初始化与流控制
HAL 层是整个通路的源头,初始化顺序错了后面全白搭。典型的初始化流程是:打开设备节点 → 查询能力(支持的格式、分辨率)→ 设置格式 → 申请 buffer → 启动流。每一步失败都要有明确的错误码返回,否则上层只能看到「相机不可用」这种黑匣子信息。
// EvsCamera::open 的核心逻辑 int EvsCamera::open(const char* deviceName) { fd_ = ::open(deviceName, O_RDWR | O_NONBLOCK); if (fd_ < 0) return -errno; // 查询能力,确认支持 NV12 struct v4l2_capability cap; if (ioctl(fd_, VIDIOC_QUERYCAP, &cap) < 0) return -errno; // 设置格式:车机倒车常用 1280x720 NV12 struct v4l2_format fmt = {}; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = 1280; fmt.fmt.pix.height = 720; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_NV12; if (ioctl(fd_, VIDIOC_S_FMT, &fmt) < 0) return -errno; // 申请 4 个 buffer,双缓冲不够,倒车场景容易丢帧 struct v4l2_requestbuffers req = {}; req.count = 4; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; if (ioctl(fd_, VIDIOC_REQBUFS, &req) < 0) return -errno; return 0; }这段的关键参数:O_NONBLOCK是必须的,EVS 不能阻塞在 open 上;V4L2_PIX_FMT_NV12是车机最常见的格式,GPU 转换成本低;buffer 数量给 4 而不是 2,是因为倒车场景对丢帧零容忍,双缓冲在调度抖动时容易断流。VIDIOC_REQBUFS之后还要对每个 buffer 做VIDIOC_QUERYBUF和mmap,代码略长,逻辑就是建立内核 buffer 到用户态的映射。
流控制上,startVideoStream里把 buffer 全部VIDIOC_QBUF入队,再VIDIOC_STREAMON;停止时反过来。这里有个容易忽略的点:停止流之前要先等所有在途 buffer 归还,否则下次启动会拿到脏 buffer。
3.2 EVS Manager 的相机枚举与订阅管理
Manager 的职责是「谁要哪路相机、给谁」,核心是两张表:相机表和订阅表。相机表在启动时通过遍历 HAL 服务构建,订阅表在运行时动态维护。
// 订阅逻辑简化 bool EvsManager::subscribe(camera_id_t id, const sp<IEvsCameraStream>& stream) { std::lock_guard<std::mutex> lock(mutex_); auto it = cameras_.find(id); if (it == cameras_.end()) return false; auto& cam = it->second; cam.subscribers.push_back(stream); // 第一个订阅者才真正开流 if (cam.subscribers.size() == 1) { cam.camera->startVideoStream(this); } return true; } void EvsManager::unsubscribe(camera_id_t id, const sp<IEvsCameraStream>& stream) { std::lock_guard<std::mutex> lock(mutex_); auto& cam = cameras_[id]; cam.subscribers.erase( std::remove(cam.subscribers.begin(), cam.subscribers.end(), stream), cam.subscribers.end()); // 最后一个订阅者退出才停流,避免频繁开关 if (cam.subscribers.empty()) { cam.camera->stopVideoStream(); } }逻辑说明:引用计数式的订阅管理,保证多应用共享同一路相机时不重复开流。mutex_保护两张表,因为订阅可能来自不同 binder 线程。参数上,stream是上层实现的回调接口,Manager 自己不处理帧,只做转发——deliverFrame里遍历订阅者逐个回调。这里要注意回调不能持锁,否则一个慢订阅者会拖死整条链路,常见做法是拷贝订阅者列表后释放锁再回调。
3.3 显示合成:绕过 SurfaceFlinger 的低延迟路径
EVS Display HAL 直接操作显示控制器,把帧送到指定图层。倒车影像要求「挂挡即出图」,走 SurfaceFlinger 的合成队列会多出 1~2 帧延迟,所以 EVS 单独开了一条路。
// EvsDisplay::postBuffer 简化 int EvsDisplay::postBuffer(buffer_handle_t handle) { // 直接送 DRM plane,跳过合成 int ret = drmModeSetPlane(drm_fd_, plane_id_, crtc_id_, fb_id_, 0, 0, width_, height_, 0, 0, width_ << 16, height_ << 16); if (ret < 0) return -errno; // 等待 vsync 再返回,保证帧真正上屏 drmWaitVBlank(drm_fd_, &vblank); return 0; }这段用 DRM 接口直接设 plane,drmModeSetPlane的参数里,源区域和目标区域都用 16.16 定点数表示,width_ << 16就是整屏铺满。drmWaitVBlank是同步点,不等的话上层以为上屏了实际还在队列里,倒车场景会出现「画面出来了但已经挂完挡」的错觉。显示这块的坑在于多图层叠加:倒车影像、雷达提示、UI 控件可能在不同 plane,zpos 配错就会出现影像被盖住。
4. 避坑与排查:EVS 调试中最常见的五个问题
4.1 现象:相机枚举为空,dumpsys 看不到任何设备
原因通常是 sepolicy 拒绝或设备节点权限不对。EVS 服务以独立域运行,默认没有/dev/video*的读权限。解决:在device/<platform>/evs/sepolicy下补allow evs_camera_device video_device:chr_file rw_file_perms;,重新编 sepolicy 并刷入。排查时先看dmesg | grep avc,有 denied 日志就对症下药,别盲目改权限。
4.2 现象:出图几秒后卡死,日志无报错
这是 buffer 没归还的典型症状。deliverFrame里处理完必须调doneWithFrame,如果中间有异常分支提前 return,buffer 就漏了。解决:用 RAII 封装 buffer 归还,或者在所有返回路径上统一归还。我一般会在 HAL 里加一个在途 buffer 计数,卡死时 dump 出来看哪个 buffer 没回来。
4.3 现象:倒车影像延迟明显,挂挡后要等一两秒
先分清是冷启动慢还是每帧慢。冷启动慢多半是相机节点初始化耗时,可以在系统启动时就预打开相机(保持低功耗待流状态);每帧慢则查显示路径,确认是否误走了 SurfaceFlinger 合成。用dumpsys SurfaceFlinger看 EVS 图层是否被合成,如果是,检查 Display HAL 的 plane 配置。
4.4 现象:多路相机切换时画面撕裂或错帧
原因是切换时旧流的 buffer 还在途,新流已经开始送帧。解决:切换前先stopVideoStream并等待所有 buffer 归还,再启动新流;显示侧加一帧同步,确保 plane 切换和 vsync 对齐。这个坑在环视拼接场景尤其常见,四路相机切换频繁,时序没对齐就花屏。
4.5 现象:编译通过但服务起不来,logcat 报 HAL 注册失败
查 manifest.xml 里 EVS HAL 的 interface 声明是否和实际实现的版本号一致。Android 的 HAL 注册对版本敏感,android.hardware.automotive.evs@1.1和1.0混用就会注册失败。解决:统一 manifest 和实现里的版本宏,重新编vendor.image刷入。另外确认init.rc里有对应的 service 定义,别只编了二进制忘了加启动项。
5. 二次开发:按车型改配置与验证方法
改车型是这类工程最高频的需求,核心原则是只动 device 层,不碰 HAL 和 Manager。相机路数、分辨率、显示布局都在device/<platform>/evs/config下的配置文件里,常见格式是 JSON 或 XML,字段大致是相机 ID、设备节点、格式、显示区域坐标。改完配置先做静态验证:用evs_manager --validate-config之类的自检命令(工程里一般会带)确认字段合法,再上板实测。
验证方法我习惯分三步走。第一步单路验证:只开一路相机,用dumpsys evs看帧率是否稳定在 30fps,丢帧计数是否为零。第二步多路并发:同时订阅倒车和环视,确认 Manager 的引用计数正确,停掉一路另一路不受影响。第三步时序验证:模拟挂挡信号,用atrace抓 EVS 相关 tag,看从信号到首帧上屏的耗时,这个数字是倒车体验的硬指标,一般要求 500ms 以内。
改造时有个技巧值得记:把车型差异做成配置表而不是代码分支。我见过把不同车型逻辑写成if (model == A) ... else if (model == B) ...的代码,加到第五个车型就没人敢动了。正确做法是配置驱动,HAL 和 Manager 只认配置,新增车型只加一份配置文件和对应的 sepolicy。另外,EVS 的 buffer 管理建议统一封装成一个BufferPool类,采集、处理、显示三段都从池里取还,这样排查泄漏时只看一个地方。
从那以后我每次改 EVS 配置,都强制走一遍「单路 → 多路 → 时序」三步验证,哪怕只改了一个分辨率字段——血泪经验是,配置错一个坐标,环视拼接就会整体偏移,上板才发现就得多烧一版。希望帮到你。
本文还有配套的精品资源,点击获取