跳转至

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. 硬件连接与运行前检查

  1. 使用支持 UVC 和 MJPEG 输出的 USB 摄像头。
  2. 将开发板 USB 选择开关的第 2 位拨到 ON,使接口工作在 USB Host 模式。
  3. 把摄像头连接到 Hi3322 USB Host 接口,再给开发板上电。
  4. USB 烧录前应把第 2 位拨回 OFF,切回 USB Device 模式。
  5. 摄像头功耗必须在开发板 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 的目标配置中增加:

'CONFIG_ENABLE_USB_CAMERA',

并把组件加入对应目标的 ram_component:

'usb_camera_sample',

samples/native_samples/CMakeLists.txt 中需要存在条件入口:

if("CONFIG_ENABLE_USB_CAMERA" IN_LIST DEFINES)
    add_subdirectory_if_exist(usb_camera)
endif()

示例自己的 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 工程根目录执行:

fbb set-target pack_diting_community
fbb build --clean diting-community

修改组件列表、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 再注册实际页面:

REGIST_SLICE_PAGE(VIEW_USB_CAMERA, USB_CAMERA_MAIN_PAGE,
    UsbCameraMainPage, true);

这两个注册都依赖静态对象初始化,所以 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* 设备后,先枚举摄像头支持的格式和尺寸,再执行:

VIDIOC_S_FMT -> VIDIOC_G_FMT
VIDIOC_S_PARM -> VIDIOC_G_PARM

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 资源。

推荐严格遵守:

停止业务请求 -> 停止取帧线程 -> STREAMOFF -> 释放映射与解码资源 -> 关闭设备

拔出摄像头前应先停止预览或录像。运行中直接拔出设备会让 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 起止标记:

SOI:0xFF 0xD8
EOI:0xFF 0xD9

播放器先统计完整帧数量,再结合 .meta 中的录像时长计算回放节奏。每个完整 JPEG 帧经硬件 JPEG 解码后写入 Surface。回放在独立线程中执行,停止时需要设置退出状态、等待线程结束,再释放文件、解码器和 Surface 资源。

若录像文件在写入过程中断电,最后一帧可能缺少 EOI;播放器应忽略不完整尾帧,而不是把剩余文件内容交给解码器。

9. DSP 扩展边界

本示例的默认链路是 UVC + MJPEG + JPEG/VDEC,没有直接调用 DSP API。SDK 具备 DSP 能力不等于 USB Camera 帧会自动经过 DSP。

需要增加图像算法时,建议在 HAL 已经生成目标格式的 framework buffer 后增加异步处理队列:

UVC 取帧线程 -> 格式转换/解码 -> 有界帧队列 -> DSP 任务 -> 显示或编码

设计时至少确认:

  • DSP 输入是压缩 JPEG、RGB 还是 YUV。
  • buffer 的物理地址、虚拟地址、stride 和 cache 一致性。
  • buffer 所有权何时从 Camera HAL 移交、何时归还。
  • DSP 处理超过一帧周期时是丢旧帧、丢新帧还是降低帧率。
  • 算法线程不能阻塞 DQBUF/QBUF 循环和 UI 线程。

10. 操作步骤

  1. 烧录包含 USB Camera sample、UVC Host 和 usb_camera_lite HAL 的固件。
  2. 断电,将 USB 选择开关第 2 位拨到 ON,连接 UVC 摄像头后重新上电。
  3. 打开 Native Launcher 中的 USB Camera。
  4. 先点击 USB Host,等待 USB 枚举和 UVC 初始化完成。
  5. 点击 Preview 验证实时预览。
  6. 点击 Capture,再点击 Photo 检查照片。
  7. 点击 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 生命周期,并降低算法负载或输入帧率

12. 参考源码