USB Camera 开发指南
1. 概述
USB Camera 示例运行在 HiDiTing V100 社区开发板上,通过 USB Host 连接标准 UVC(USB Video Class)摄像头,并在 Native Launcher 中提供实时预览、拍照、MJPEG 录像和录像回放功能。
默认数据链路是:
UVC 摄像头
-> USB Host/UVC 驱动
-> V4L2 设备节点和缓冲队列
-> usb_camera_lite Camera HAL
-> Camera Lite framework/sample
-> USB Camera NativeUI 页面
-> 屏幕预览、JPEG 照片或 MJPEG 文件
SDK 还提供 DSP 能力,但本示例默认不调用 DSP 接口。需要图像算法时,应在获取到稳定帧数据后增加独立处理模块,并明确像素格式、缓存所有权和实时性要求。
2. 功能与默认参数
| 项目 | 默认值 |
|---|---|
| 摄像头数量 | 1 |
| UVC 输入格式 | MJPEG |
| 分辨率 | 640×480 |
| 帧率 | 30 fps |
| framework 输出格式 | JPEG、PRIVATE、YVU420 |
| 最大并行流数量 | 3 |
| 拍照文件 | /user/cam_capture.jpg |
| 录像文件 | /user/cam_record.mjpg |
| 录像时长信息 | /user/cam_record.meta |
以上约束定义在 hal_camera_config.h。摄像头至少要支持 HAL 请求的 MJPEG 分辨率和帧率,否则格式协商或开流会失败。
3. 代码结构
3.1 示例页面
| 文件 | 作用 |
|---|---|
UsbCameraPresenter.cpp |
将 USB Camera 注册到 Native Launcher 菜单 |
UsbCameraMainPage.cpp |
页面布局、按钮事件、USB Host 初始化和 Camera sample 命令分发 |
UsbCameraMjpegPlayer.cpp |
MJPEG 文件扫描、JPEG 拆帧、硬件解码和 Surface 回放 |
CMakeLists.txt |
定义 usb_camera_sample 组件并启用整库链接 |
3.2 Camera sample 与 HAL
| 文件 | 作用 |
|---|---|
camera_sample.cpp |
把文本命令映射为预览、拍照和录像操作 |
camera_test.cpp |
Camera framework 会话、流和 Surface 管理 |
hal_camera.c |
UVC 格式协商、缓冲创建、取帧、JPEG/VDEC 处理和 stream 生命周期 |
hal_camera_pipe.c |
封装 V4L2 STREAMON/OFF、DQBUF/QBUF 操作 |
hal_camera_config.h |
分辨率、帧率、格式、队列数量和超时配置 |
4. 硬件连接与运行前检查
- 使用支持 UVC 和 MJPEG 输出的 USB 摄像头。
- 将开发板 USB 选择开关的第 2 位拨到 ON,使接口工作在 USB Host 模式。
- 把摄像头连接到 Hi3322 USB Host 接口,再给开发板上电。
- USB 烧录前应把第 2 位拨回 OFF,切回 USB Device 模式。
- 摄像头功耗必须在开发板 USB 供电能力范围内;出现反复枚举或异常发热时立即断电检查。
USB Host 与 USB 烧录接线可参考社区开发板使用指南。
5. 构建配置
USB Camera 页面不是独立进程,必须与 Native Launcher、Camera Lite framework、USB Camera HAL 和 UVC Host 驱动一起构建。
5.1 加入示例组件
在 src/build/config/target_config/3322/config.py 的目标配置中增加:
并把组件加入对应目标的 ram_component:
samples/native_samples/CMakeLists.txt 中需要存在条件入口:
示例自己的 CMakeLists.txt 将三个 C++ 源文件组成 usb_camera_sample,并设置 WHOLE_LINK true。整库链接不能省略,否则仅依赖注册宏的页面可能被链接器裁剪。
5.2 选择 USB Camera HAL
usb_camera_lite 和板载 Camera HAL 都生成 camera_hal 组件,实现相同的 HalCamera* 接口。因此目标只能选择其中一个实现,不能同时链接。
在 src/middleware/services/media/hal/CMakeLists.txt 中,可按目标宏切换:
if("CONFIG_ENABLE_USB_CAMERA" IN_LIST DEFINES)
add_subdirectory_if_exist(usb_camera_lite)
else()
add_subdirectory_if_exist(camera_lite)
endif()
如果最终仍编入 camera_lite,页面能够出现,但 Camera framework 不会走 UVC HAL。
5.3 开启 USB Host 和 UVC
在目标使用的 LiteOS 配置中开启 USB Host、xHCI、Host 独立内存池、FIFO 和 UVC class driver。至少应确认:
LOSCFG_DRIVERS_USB=y
LOSCFG_DRIVERS_USB_HOST_DRIVER=y
LOSCFG_DRIVERS_USB_HOST_XHCI=y
LOSCFG_DRIVERS_USB_HOST_XHCI_IOSC_SIA=y
LOSCFG_DRIVERS_USB_HOST_PARAMS_CONFIG=y
LOSCFG_DRIVERS_USB_XHCI_CONFIG=y
LOSCFG_DRIVERS_USB_HOST_STANDALONE_MEM_POOL=y
LOSCFG_DRIVERS_USB_FIFO=y
LOSCFG_DRIVERS_USB_HOST_UVC_CLASS=y
同时检查当前目标是否还启用了互斥的 USB Device 功能。Host/Device 模式除了软件配置,还受开发板 USB 选择开关控制。
5.4 编译
在 SDK 工程根目录执行:
修改组件列表、HAL 选择或 LiteOS USB 配置后必须执行干净构建。
6. 页面注册与命令分发代码走读
6.1 菜单和页面注册
UsbCameraPresenter.cpp 使用菜单注册宏,把入口加入 Native Launcher:
REGIST_MENU(VIEW_USB_CAMERA, UsbCameraView, UsbCameraPresenter,
EMPTY_ICON, EMPTY_ICON, "USB Camera");
UsbCameraMainPage.cpp 再注册实际页面:
这两个注册都依赖静态对象初始化,所以 usb_camera_sample 使用 WHOLE_LINK true。如果固件中没有 USB Camera 菜单,应先检查组件是否整库链接,再检查 View/Page ID 是否冲突。
6.2 USB Host 初始化
用户点击 USB Host 后,页面先通过 EnableUsbHostPower() 打开 USB Host 供电,再调用 at_usb_host_init() 初始化 Host 协议栈。这个操作必须早于预览、拍照和录像。
UI 层只负责触发初始化,不负责 UVC 端点枚举。枚举失败时应从 USB Host/UVC 日志定位,不能通过反复点击预览规避。
6.3 按钮到 Camera sample
页面把按钮动作转换为五个字符串命令:
| 页面动作 | 命令 |
|---|---|
| 开始预览 | preview_start |
| 停止预览 | preview_stop |
| 拍照 | capture |
| 开始录像 | video_start |
| 停止录像 | video_stop |
所有命令最终经过:
int32_t UsbCameraMainPage::RunCameraTestCommand(const char *cmd)
{
char *argv[] = {const_cast<char *>(cmd)};
return ::camera_sample(1, argv);
}
camera_sample.cpp 中的命令表再把字符串映射为 CameraTestStart、CameraTestStop、CameraTestCapture、CameraTestRecordStart 和 CameraTestRecordStop。这层复用 Camera Lite 的 sample 控制逻辑,使 UI 不需要直接管理 CameraDevice、CameraConfig 和 FrameStateCallback。
7. UVC HAL 核心代码走读
7.1 设备与格式协商
HAL 打开 UVC 对应的 /dev/video* 设备后,先枚举摄像头支持的格式和尺寸,再执行:
uvc_set_format() 把 framework 请求转换为 UVC MJPEG,并用 VIDIOC_G_FMT 回读实际宽高和像素格式。只检查 VIDIOC_S_FMT 返回成功是不够的,因为驱动可能自动回退到其他格式。
uvc_set_fps() 用 timeperframe = 1/30 请求 30 fps,再通过 VIDIOC_G_PARM 读取最终帧率。摄像头不支持目标组合时,应调整 hal_camera_config.h,而不是假设所有 UVC 设备都支持 640×480@30 fps MJPEG。
7.2 Stream 创建
HalCameraStreamCreate() 完成以下工作:
初始化 UVC 设备
-> 配置 MJPEG 输入格式和帧率
-> 创建并映射 V4L2 缓冲
-> 根据 framework 输出格式选择处理路径
JPEG/PRIVATE:硬件 JPEG 解码或直接复制
YVU420:创建 MJPEG VDEC 通道
-> 将 stream 状态置为 CREATED
UVC 线缆上传输的仍是 MJPEG。framework 请求的输出格式决定 HAL 是保留压缩数据、使用 JPEG 硬件单帧解码,还是通过 VDEC 生成 YVU420。
7.3 开流和取帧线程
HalCameraStreamOn() 启动 V4L2 pipe,并创建名为 cam-hal 的取帧线程。HAL 会跳过开流后的前 3 帧,以减少摄像头刚启动时曝光和数据不稳定造成的异常画面。
稳定运行后的循环是:
VIDIOC_DQBUF
-> packet_to_streams()
-> 遍历所有活动 stream
-> packet_to_stream()
-> JPEG/复制路径,或 vdec_input_stream()
-> 将 framework buffer 标记为 FILLED
-> VIDIOC_QBUF
DQBUF 取得的缓冲属于 V4L2 队列,处理完所有活动 stream 后必须及时 QBUF 归还。耗时算法若直接插入该线程,会延迟归还缓冲并最终造成 UVC 丢帧或队列耗尽。
7.4 JPEG 与 VDEC 两条处理路径
packet_to_stream() 根据 stream 是否创建了 vdec_info 选择路径:
- 无 VDEC:对 JPEG 输出执行拷贝;需要像素输出时调用
uapi_jpeg_decomress_create()和uapi_jpeg_decompress_start()完成硬件 JPEG 解码。 - 有 VDEC:调用
vdec_input_stream()把 MJPEG 帧送入视频解码通道,再等待输出 YVU420 buffer。
VDEC 输出 stride 按 256 字节对齐,缓冲下限按 stride × height × 3 / 2 计算。二次开发不能简单按 width × height × 3 / 2 分配,否则在带 stride 的行尾会越界。
7.5 停流与资源释放
HalCameraStreamOff() 先将目标 stream 标记为停止;当没有活动 stream 后,等待 cam-hal 线程退出并执行 VIDIOC_STREAMOFF。随后才能解除缓冲映射和销毁 VDEC/JPEG 资源。
推荐严格遵守:
拔出摄像头前应先停止预览或录像。运行中直接拔出设备会让 DQBUF 返回错误,业务层还要避免在错误路径重复释放同一 Surface 或文件句柄。
8. 拍照、录像与回放代码走读
8.1 拍照
点击 Capture 后,页面发送 capture。Camera sample 将一帧写入 /user/cam_capture.jpg。点击 Photo 时,页面从同一路径加载 JPEG 并展示。
拍照失败时依次检查:Camera stream 是否已建立、JPEG 帧是否完整、/user 是否已挂载且可写、剩余空间是否足够。
8.2 录像
video_start 开始把连续 MJPEG 帧写入 /user/cam_record.mjpg;video_stop 停止写入,页面同时把录像时长保存到 /user/cam_record.meta。MJPEG 文件本质上是连续 JPEG 帧,没有复杂容器索引。
8.3 MJPEG 回放
UsbCameraMjpegPlayer 扫描录像文件中的 JPEG 起止标记:
播放器先统计完整帧数量,再结合 .meta 中的录像时长计算回放节奏。每个完整 JPEG 帧经硬件 JPEG 解码后写入 Surface。回放在独立线程中执行,停止时需要设置退出状态、等待线程结束,再释放文件、解码器和 Surface 资源。
若录像文件在写入过程中断电,最后一帧可能缺少 EOI;播放器应忽略不完整尾帧,而不是把剩余文件内容交给解码器。
9. DSP 扩展边界
本示例的默认链路是 UVC + MJPEG + JPEG/VDEC,没有直接调用 DSP API。SDK 具备 DSP 能力不等于 USB Camera 帧会自动经过 DSP。
需要增加图像算法时,建议在 HAL 已经生成目标格式的 framework buffer 后增加异步处理队列:
设计时至少确认:
- DSP 输入是压缩 JPEG、RGB 还是 YUV。
- buffer 的物理地址、虚拟地址、stride 和 cache 一致性。
- buffer 所有权何时从 Camera HAL 移交、何时归还。
- DSP 处理超过一帧周期时是丢旧帧、丢新帧还是降低帧率。
- 算法线程不能阻塞
DQBUF/QBUF循环和 UI 线程。
10. 操作步骤
- 烧录包含 USB Camera sample、UVC Host 和
usb_camera_liteHAL 的固件。 - 断电,将 USB 选择开关第 2 位拨到 ON,连接 UVC 摄像头后重新上电。
- 打开 Native Launcher 中的 USB Camera。
- 先点击 USB Host,等待 USB 枚举和 UVC 初始化完成。
- 点击 Preview 验证实时预览。
- 点击 Capture,再点击 Photo 检查照片。
- 点击 Record 开始录像,点击 StopRec 停止,再点击 Play 回放。
11. 常见问题
| 现象 | 排查建议 |
|---|---|
| 菜单中没有 USB Camera | 检查 CONFIG_ENABLE_USB_CAMERA、usb_camera_sample 和 WHOLE_LINK true |
| 点击后没有摄像头 | 先执行 USB Host 初始化,检查开关第 2 位、供电、Host/UVC 配置和枚举日志 |
| 格式设置失败 | 确认摄像头支持 MJPEG、640×480 和 30 fps,查看 G_FMT/G_PARM 回读结果 |
| 开流后黑屏 | 检查 STREAMON、DQBUF、首帧跳过计数以及 Surface 是否正常入队 |
| 预览一段时间后卡住 | 检查每个 DQBUF 是否都能走到 QBUF,算法或文件写入是否阻塞取帧线程 |
| 拍照/录像失败 | 检查 /user 挂载、可写空间、文件权限和 JPEG 帧完整性 |
| 回放失败 | 检查 MJPEG 是否包含完整 SOI/EOI、.meta 是否存在、硬件 JPEG 组件是否构建 |
| 画面错行或越界 | 检查 YVU420 stride 256 字节对齐和目标 buffer 大小 |
| 加入 DSP 后丢帧 | 使用有界异步队列,明确 buffer 生命周期,并降低算法负载或输入帧率 |