API GUIDE / VIC / Middleware

G32S10 VIC Middleware 接口

Middleware 层接口:分层职责:

1. 分层关系

APPLICATION  Application / Example  业务代码、板级支持与示例入口
↓
CAPTURE  Camera capture middleware  im_camera_vic.c 及配置、帧与 sensor 子模块
AE  AE statistics middleware  im_camera_ae.c 负责曝光统计
↓
HAL  HAL VIC / TOP / IPC / DMSC / WDMA / RESP / AE  模块级配置、状态管理与硬件访问
HAL  HAL CSI / DPHY_RX / ISP clock / sensor MCLK  模块级配置、状态管理与硬件访问
↓
VIC HARDWARE  CIM0 / CIM1 + ISP0 / ISP1  并行采集与图像信号处理实例
MIPI INPUT  CSI / DPHY_RX  MIPI Camera 输入链路
↓
HARDWARE  Sensor · DVP / MIPI / BT.656  外部图像传感器与输入时序

分层职责:

  • Application / Example

    • 通过 camera_register_device() 登记 sensor 驱动与板级资源

    • 通过 camera_open() / camera_set_format() / camera_start() 建立采集链路

    • 在 camera_wait_frame() 与 camera_put_frame() 之间使用帧

    • 帧内容交给下游(JPEG、USB UVC、DPU/LCD 等)

  • Camera capture middleware

    • 维护 camera_device_t 状态机与两张帧队列

    • 把 sensor 属性翻译成 VIC/CSI/DPHY/TOP/IPC/WDMA/RESP 配置

    • 分配并管理帧缓冲,向 WDMA 提交帧、在完成中断里回收

    • 在中断里统计错误并通过 drop_next_completed_frame 丢弃坏帧

  • AE statistics middleware

    • 独立的单实例模块,固定使用 ISP0

    • 分配 AE 统计/直方图 DMA 缓冲并配置 HAL AE

    • 在每个 AE_STATIC_DONE 中断里解析 DMA 数据,向应用提供统计快照

    • 与采集中间件的生命周期没有耦合,需要应用单独初始化

  • HAL VIC / ISP / CSI

    • 负责寄存器级配置和硬件启动/停止

    • 不持有帧队列,不决定丢弃策略

1.1 硬件模块分工

一条采集通路会用到下面这些硬件块。中间件通过 HAL 访问它们,应用和 BSP 通常 不直接碰;但每一块都有自己的约束,出问题时按这张表定位。

模块在通路中的角色中间件做什么应用 / 板级要注意
VIC(含 CIMx 前端)相机数据的采集入口;CIM0 接 MIPI 还是 DVP 由板级设计决定按 sensor 属性配 DVP 时序/采样沿、MIPI 尺寸与裁剪;camera_start() 里启动、camera_stop() 里停止数据接在 CIM0 还是 CIM1,决定 index(§3.1、§9.5)
MIPI CSI把 CSI-2 包解析成像素流,做数据类型检查(单实例)按 lane_count / lane_rate_hz 初始化与反初始化;需要 CONFIG_DRIVER_CSI中间件只在 index = 0 上开放;未开 CSI 驱动时 MIPI 启动返回 IM_ERR_UNSUPPORTED
D-PHY RXMIPI 物理层按 sensor 报告的 lane 数/速率配置,等 lane 稳定后再启动 VIC片上集成,不需要板级 pinmux;停流/关闭时由中间件关断
TOPISP 前端:尺寸、输入数据类型、Bayer 相位、DMSC 旁路、AE 统计取数点camera_start() 里写入;statistics_position 按输入是否 RAW 选择Bayer 相位填错会偏色(§6.6)
IPCISP 处理链使用 HAL 默认配置不走 RAW→NV12 时基本不用动
DMSCRAW → YUV420 转换中间件从不配置 DMSC;RAW→NV12 依赖复位默认值,其余组合由 TOP 旁路想要彩色 NV12 只能「RAW 输入 + NV12 输出」(§6.4)
AE(硬件统计)分区统计与直方图,经 DMA 写出独立模块 im_camera_ae,固定 ISP0、单实例(§10)ISP1 没有 AE;AE 与采集生命周期无耦合,要单独初始化
WDMA把 ISP 输出写进帧缓冲,完成时发中断按格式组合选 mux_sel / dump_data_type / dump_data_order,提交帧、回收完成帧(§6.3、§7.4)应用只使用 camera_wait_frame() 返回的地址,不要按 paddr 自己切分
RESP中断汇总与错误状态使能 WDMA/AE 完成上报,统计错误,必要时丢弃下一帧(§8)帧异常时先看 frame_stats(§8.4)

一句话记忆:

  • DVP 走 VIC,MIPI 多一层 CSI + D-PHY;

  • 只有 RAW 进、NV12 出时 DMSC 才在通路上,其他组合都被旁路;

  • AE 只认 ISP0;

  • 帧最终都由 WDMA 写出来,所以「没帧」的问题先看 WDMA 完成计数。

2. 资源模型

2.1 固定配置宏

im_camera_device.h:

宏值说明
CAMERA_INSTANCE_COUNT2相机实例数
CAMERA_DEFAULT_FRAME_COUNT2应用侧可见帧数;实际帧池为 +1(内部流水线)
CAMERA_ALIGN_SIZE8行/帧尺寸对齐要求
CAMERA_FRAME_ALIGN_SIZE4096每块帧缓冲的对齐粒度,也是帧池分块大小
CAMERA_WDMA_INFLIGHT_COUNT2WDMA 同时在途帧数
CAMERA_WAIT_FRAME_TIMEOUT_MS3000预留宏,当前中间件未使用;等帧超时由调用者传入
CAMERA_MIPI_RX_HAL_ENABLEDCONFIG_DRIVER_CSI为 1 时才编译 CSI/D-PHY 启动路径

im_camera_frame.c 中实际帧池:

dev->frame_count = CAMERA_DEFAULT_FRAME_COUNT + 1U;   /* 3 */

im_camera_ae.h:

宏值说明
CAMERA_AE_MAX_ZONE_COUNT15AE 分区最大行/列数
CAMERA_AE_DMA_BUFFER_COUNT_MIN/MAX1 / 4统计与直方图 DMA 缓冲数量范围
CAMERA_AE_HISTOGRAM_BIN_COUNT256解包后的直方图 bin 数
CAMERA_AE_STATISTICS_BUFFER_SIZE4096单个统计 DMA 缓冲字节数
CAMERA_AE_HISTOGRAM_BUFFER_SIZE2048单个直方图 DMA 缓冲字节数
CAMERA_AE_DMA_BUFFER_ALIGNMENT64DMA 缓冲对齐要求

2.2 帧缓冲模型

camera_allocate_buffers() 一次分配两块内存:

dev->mem    : memalign(4096, frame_align_size * 3)
              +--------------------------+--------------------------+--------------------------+
              | frame 0                  | frame 1                  | frame 2                  |
              | frame_align_size 字节    | frame_align_size 字节    | frame_align_size 字节    |
              +--------------------------+--------------------------+--------------------------+
dev->frames : memalign(64, sizeof(camera_frame_t) * 3)   /* 帧描述符,与数据分开 */
  • 每帧物理地址由 im_mmio_virt_to_phys() 转换后交给 WDMA。

  • 帧描述符记录 status、addr、info(宽高、格式、vaddr/paddr、序号、时间戳)。

  • 三帧全部先挂在 free_list;camera_start() 取两帧交给 WDMA。

  • dev->mem 和 dev->frames 都由中间件释放,应用不负责分配/释放。

  • 应用通过 camera_status_t.paddr / mapped_mem 只能拿到帧池首地址,不要自行按块切分使用,应始终使用 camera_wait_frame() 返回的指针。

2.3 构建资源

项内容
KconfigCONFIG_MIDDLEWARE_CAMERA,depends on DRIVER_VIC,默认 n
CMake 组件im_camera,5 个 .c,INCLUDE_DIRS include,REQUIRES im_i2c
编译依赖CONFIG_DRIVER_VIC;MIPI 还需要 CONFIG_DRIVER_CSI;sensor 驱动与 BSP 另行选中
头文件依赖im_camera_sensor.h 直接包含 im_gpio.h、im_i2c_bus.h、im_i2c_dev.h

3. VIC MID 核心对象与状态

3.1 camera_device_t

camera_device_t 是静态设备表中的一项(s_camera_devices[2]),对应用不透明。 主要字段:

字段说明
index相机实例号
isp / csi / dphy_rx由 index 固定的硬件寄存器块
irq_id / irq_nameIRQ 号与注册名
lock / wait配置互斥锁、等帧条件变量
state设备状态机
format当前格式上下文(线长、帧长、UV 偏移等)
frames / mem / frame_count帧描述符、帧数据池、帧数(3)
free_list / usable_list空闲帧队列、已完成待取帧队列
transfer[2]当前交给 WDMA 的两帧
available_frame_countusable_list 中的帧数
dma_index下一次 WDMA 完成对应的 transfer[] 槽位
wait_timeout最近一次 camera_wait_frame() 是否超时
drop_next_completed_frame下一次 WDMA 完成时是否丢弃该帧
frame_statsframe_done_count / frame_error_count
sensor_attr驱动声明的静态属性
sensor_pathDVP/MIPI/BT.656 的 VIC 输入配置
isp_pipelineTOP/IPC/DMSC/WDMA 配置
sensor驱动操作表副本
is_registered是否已登记(跨 close() 保留)

3.2 设备状态机

STATE  UNINITIALIZED
→
STATE  INITIALIZED
→
STATE  READY
→
STATE  OPENED
→
STATE  STREAMING

UNINITIALIZED → INITIALIZED · camera_open() 初始化内部资源

INITIALIZED → READY · 帧池与 IRQ 就绪

READY → OPENED · sensor 上电

OPENED → STREAMING · camera_start()

STREAMING → OPENED · camera_stop()

READY / OPENED / STREAMING → UNINITIALIZED · camera_close()

状态含义何时出现
UNINITIALIZED未初始化上电后、camera_close() 后
INITIALIZED互斥锁/条件变量/队列已初始化camera_open() 内部瞬时状态
READY帧缓冲与 IRQ 已就绪camera_open() 内 power_on 之前
OPENEDsensor 已上电,可开流camera_open() 成功后、camera_stop() 后
STREAMING正在出流camera_start() 成功后

关键规则:

  • is_registered 与 state 独立。camera_close() 不清除 is_registered, 所以关闭后仍可用 camera_detect() 取回句柄并再次 camera_open()。

  • 再次 camera_open() 会重新读 sensor 属性、重新分配缓冲、重新上电;但camera_sensor_ops_t::init 不会被再次调用(登记时只调一次)。

  • camera_open() 在设备已 OPENED/STREAMING 时直接返回 IM_OK(幂等)。

  • camera_start() 在已 STREAMING 时直接返回 IM_OK;未 OPENED 返回IM_ERR_NOT_READY。

  • camera_stop() 在未出流时返回 IM_OK,不做任何事。

  • camera_close() 在未初始化时是空操作(void 返回)。

常用状态查询:

函数判据
camera_device_is_initialized()state >= INITIALIZED
camera_device_is_ready()state >= READY
camera_device_is_opened()state >= OPENED
camera_device_is_configured()state >= READY 且 frames/mem/frame_count 均非空
camera_device_is_streaming()state == STREAMING

3.3 帧状态与队列

FREE LIST  FREE  位于 free_list,可提交给 WDMA
→  camera_prime_wdma
HARDWARE  TRANS  WDMA 正在写入
→  done ISR
READY  USABLE  位于 usable_list,可由应用获取
→  camera_wait_frame
APPLICATION  USER  应用正在使用帧
↓  camera_put_frame() 归还后重新回到 FREE
CYCLE  FREE  帧重新进入 free_list
状态含义
FREE空闲,可提交给 WDMA
TRANS在途,WDMA 正在写
USABLE已完成、等待应用取走
USER已交给应用,尚未归还
  • free_list 与 usable_list 都由 list_head 维护,只在临界区或设备锁内操作。

  • available_frame_count 只统计 usable_list,不包含在途帧和 USER 帧。

  • transfer[2] 保存交给 WDMA 的两个帧指针;dma_index 在每次完成中断翻转。

  • 若两槽指向同一帧(transfer[0] == transfer[1]),完成中断不会发布帧—— 这是帧池被耗尽时的保护行为。

3.4 配置上下文

camera_format_context_t(由输出格式与 sensor 宽高算出):

字段说明
width / height来自 sensor_attr,与输出格式无关
input_pixel_formatsensor 总线格式
output_pixel_format写入帧缓冲的格式
output_yuv422_byte_order固定写 CAMERA_YUV422_BYTE_ORDER_YUYV(见 §6.7)
input_typeDVP / MIPI / BT656
line_length每行字节数
wdma_stride等于 line_length
frame_size有效帧字节数
frame_align_sizealign_up(frame_size, 4096),也是帧池分块大小
uv_data_offsetNV12 的 Y 平面字节数;其余格式为 0
enable_chromaWDMA 是否按双通道(含 UV/chroma 起始地址)提交

camera_sensor_path_config_t 保存按输入类型生成的 VIC 配置:

  • input_type

  • input.dvp / input.mipi / input.bt656(union)

camera_isp_pipeline_config_t 保存 TOP/IPC/DMSC/WDMA 静态配置:

  • top:宽高、输入数据类型、Bayer 相位、DMSC 旁路、统计取数点

  • ipc:HAL 默认值

  • dmsc:HAL 默认值(中间件不写 DMSC,见 §6.4)

  • wdma:mux_sel、dump_data_type、dump_data_order、stride、chroma_stride

3.5 对外结果结构

camera_status_t:

字段含义
width / height图像尺寸
pixel_format输出格式
yuv422_byte_order仅 YUV422 有意义;中间件固定为 YUYV
line_length每行字节数
frame_size有效帧字节数
frame_align_size帧池分块大小
uv_data_offsetNV12 的 Y 平面字节数(UV 平面起点)
frame_count帧池块数,固定 3
paddr帧池第一帧物理地址
mapped_mem帧池虚拟基地址

camera_frame_info_t(内部队列使用,camera_wait_frame() 只返回 vaddr):

字段含义
index帧池索引
width / height / pixel_format帧格式
yuv422_byte_order仅 YUV422 有意义
size对齐后的块大小(frame_align_size),不是有效帧长
paddr / vaddr物理/虚拟地址
sequence发布时的 frame_done_count
timestamp_us发布时的时间戳(中断里取)

注意:应用拿到的帧有效数据长度是 camera_status_t.frame_size,camera_frame_info_t.size 是块对齐大小,两者在 NV12 640x480 时分别是460800 和 462848。

4. 初始化与启停流程

4.1 camera_register_device()

error_code_t camera_register_device(uint32_t index,
                                    const camera_sensor_ops_t *sensor_ops,
                                    const camera_sensor_port_config_t *port_config);

流程:

  1. 校验 index < 2、sensor_ops != NULL、get_attr != NULL、stream_on != NULL、port_config != NULL;不满足返回 IM_ERR_INVALID_PARAM。

  2. 该 index 已登记则返回 IM_ERR_ALREADY_REGISTERED(不是致命错误, 示例把它当成功处理)。

  3. 若 sensor_ops->init != NULL,调用一次init(sensor_ops->user_ctx, port_config);失败则原样返回该错误。

  4. 把操作表按值拷贝到静态设备,绑定 index 对应的 ISP/CSI/DPHY/IRQ,置is_registered = true。

注意:

  • port_config 不会被中间件保存。它只在第 3 步传给驱动的 init; 驱动需要自己保存所需资源(I2C 设备句柄、GPIO、MCLK 参数)。

  • 同一 index 只能挂一颗 sensor,没有注销/替换接口。

  • 登记不访问硬件;真正的上电与芯片探测在 camera_open() 的 power_on 里。

4.2 camera_set_format()

error_code_t camera_set_format(camera_device_t *dev,
                               camera_pixel_format_t output_pixel_format);

前置条件:camera_open() 之后、camera_start() 之前;不能在出流时调用。

流程:

  1. 未初始化 → IM_ERR_NOT_READY;isp == NULL → IM_ERR_UNSUPPORTED。

  2. 出流中 → IM_ERR_BUSY。

  3. 请求格式与当前格式相同 → 直接 IM_OK。

  4. 若 IRQ 已注册则释放 IRQ;释放旧帧缓冲。

  5. 重新生成配置并分配帧缓冲。

  6. 重新注册 IRQ。

  7. 任一步失败则释放缓冲并返回错误;sensor 保持上电,设备状态不变。

约束:

  • 该函数会重新生成整条 VIC/ISP/WDMA 链路的配置并重新分配帧缓冲,帧池中的 旧数据全部失效。

  • 它不重新读 sensor 属性,也不重新上下电。

  • 输出格式与输入格式的合法组合见 §6.2。

4.3 camera_detect()

camera_device_t *camera_detect(uint32_t index);
  • 不探测硬件,只按 index 取静态设备指针。
  • 只有已登记的设备才返回非 NULL;因此顺序必须是先 register 再 detect。
  • 返回的指针是后续所有 camera_*() API 的入口。

4.4 camera_open()

error_code_t camera_open(camera_device_t *dev);

前置条件:设备已登记。

流程:

  1. 已 OPENED/STREAMING → 返回 IM_OK。
  2. 处于 INITIALIZED 但尚未打开 → 返回 IM_ERR_BUSY(只在内部瞬时出现)。
  3. 调用 hal_isp_clock_enable():
    • 选父时钟 SCLK_A,频率设为 100 MHz,再使能 CGU_ISPM。
    • 访问 ISP 寄存器前必须先使能该时钟;关闭时 ISP 寄存器读回常数、写全部丢弃。
  4. camera_init():初始化互斥锁、条件变量、两条帧队列,状态置 INITIALIZED。
  5. camera_prepare_sensor_attr():调用驱动的 get_attr() 并校验(见 §9.4)。
  6. camera_configure(dev, sensor_attr.output_pixel_format):
    • 释放旧缓冲;
    • camera_prepare_config() 生成格式上下文、VIC 输入配置、TOP/IPC/DMSC/WDMA 配置;
    • camera_allocate_buffers() 分配 3 帧;
    • sys_irq_request() 注册 IRQ;
    • 状态置 READY。
  7. 再次校验属性与缓冲配置。
  8. 调用驱动 power_on();成功后状态置 OPENED。

进入配置阶段后的任何一步失败都会在内部调用 camera_close() 收尾;更早的 时钟使能失败直接返回、camera_init() 失败则直接释放时钟。因此 应用在 camera_open() 失败后不需要、也不应该再调 camera_close()。

camera_open() 使用的是驱动在 sensor_attr.output_pixel_format 中声明的 默认输出格式;要换格式必须再调 camera_set_format()。

4.5 camera_start()

error_code_t camera_start(camera_device_t *dev);

前置条件:camera_device_is_configured(dev) 为真,且状态为 OPENED。

流程(持设备锁):

  1. 已在出流 → IM_OK;未打开 → IM_ERR_NOT_READY。
  2. camera_reset_lists():三帧全部回到空闲队列,计数器/统计清零。
  3. camera_configure_isp_pipeline():
    • TOP:宽高、输入数据类型、Bayer 相位、bypass_dmsc、statistics_position;
    • IPC:HAL 默认配置。
    • 不调用 hal_isp_dmsc_configure()。
  4. camera_configure_resp():使能并向 CPU 上报 GPL_WDMA_DONE 与 AE_STATIC_DONE,使能前先清全部。
  5. camera_configure_vic_input():按 DVP/MIPI/BT656 配置 VIC 输入。
    • MIPI 的 total_width 小于输出宽时抬到输出宽。
  6. hal_isp_wdma_reset()。
  7. camera_apply_saved_wdma_config():把静态 WDMA 配置写入 HAL。
  8. camera_prime_wdma():从空闲队列取两帧提交给 WDMA。
  9. 按输入类型启动:
    • DVP:先 sensor.stream_on(),再 hal_vic_start()。 若 hal_vic_start() 失败,回滚 sensor.stream_off()。
    • MIPI:先 hal_csi_init() + hal_dphy_rx_configure_mipi(), 再等 lanes 停止(失败只告警),然后 hal_vic_start(),最后 sensor.stream_on()。若 stream_on() 失败:停止 VIC、延时 1 ms、 关闭 D-PHY 并反初始化 CSI。
    • BT656:没有额外的启动动作;sensor.stream_on() 不会被调用。
  10. 状态置 STREAMING,然后 sys_irq_enable()。

失败处理:

  • 已启动的输入会按上述顺序回滚;
  • 若状态已置 STREAMING 则退回 OPENED;
  • WDMA 复位、帧队列清空;
  • 返回错误码。

注意:每次 camera_start() 都会清零 frame_stats,所以统计是「本次开流以来」 的计数。

4.6 camera_stop()

error_code_t camera_stop(camera_device_t *dev);

流程(持设备锁):

  1. 未初始化 → IM_ERR_NOT_READY。
  2. 若在出流:
    • sys_irq_disable();
    • hal_isp_wdma_reset();
    • 调用 DVP 停止路径(仅 DVP 生效):hal_vic_stop() → sensor.stream_off();
    • 调用 MIPI 停止路径(仅 MIPI 生效):hal_vic_stop() → 延时 1 ms → hal_dphy_rx_shutdown() + hal_csi_deinit() → sensor.stream_off();
    • 状态置 OPENED;
    • camera_reset_lists():帧全部回空闲队列,统计清零;
    • 广播条件变量,唤醒正在等帧的线程。
  3. 帧缓冲保留,供下次 camera_start() 复用;IRQ 保持注册但已禁用。

4.7 camera_close()

void camera_close(camera_device_t *dev);

流程:

  1. dev == NULL 或未初始化 → 直接返回。
  2. 若在出流:禁用 IRQ、复位 WDMA、执行 DVP/MIPI 停止路径,状态退回 OPENED。
  3. 若 state >= READY:释放 IRQ。
  4. 若 state >= OPENED 且驱动有 power_off:调用 power_off()。
  5. 释放帧数据与帧描述符。
  6. hal_isp_clock_disable()。
  7. 清零 sensor_attr,状态置 UNINITIALIZED,销毁互斥锁。

注意:

  • 不调用驱动的 deinit()(当前没有注销流程)。
  • is_registered 保留,可以再次 camera_open();但驱动的 init() 不会重跑。
  • 若 power_on() 失败,状态不会进入 OPENED,因此 camera_close() 不会 调用 power_off()。

5. VIC MID 数据通路与格式矩阵

5.1 通路总览

DVP

INPUT  Sensor  DVP 8/10/12 bit
VIC  CIMx  采集输入
VIC  VICx → TOP  格式与顶层处理
PATH  DMSCx 旁路
OUTPUT  WDMAx  写入帧缓冲

MIPI

INPUT  Sensor  MIPI CSI
MIPI  CIM0 → D-PHY RX → CSI  串行接收链路
VIC  VIC0 → TOP  图像采集与顶层处理
PATH  DMSC0(可选)  色彩空间处理
OUTPUT  WDMA0 → 帧缓冲  输出到内存

BT.656

INPUT  Sensor  BT.656
VIC  VICx → TOP  采集与顶层处理
PATH  DMSCx 旁路
OUTPUT  WDMAx → 帧缓冲  输出到内存
  • CIM0/CIM1 是芯片对外的两路相机接口,具体接 MIPI 还是 DVP 由板级设计 决定:参考板上 DVP 接口接 CIM1,MIPI 接口接 CIM0。

  • CSI 只有一份,不区分实例;中间件把它归在 index = 0 上。

  • sensor 的 MCLK 由哪个 CIM 通道输出,也由板级决定(mclk_index):参考板上 SC035 的 24 MHz 就来自 CIM0(§9.3、§9.5)。

  • DVP/BT656 只使用 VIC 前端,不涉及 CSI/D-PHY。

  • MIPI 的 CSI/D-PHY 配置由中间件在 camera_start() 里完成;需要CONFIG_DRIVER_CSI。

  • WDMA 的取数点由输入/输出格式组合决定(见 §6.3)。

  • DMSC 只有 RAW 输入 → NV12 输出时在通路上;其余组合由 TOP 旁路。

5.2 输入/输出组合矩阵

sensor_attr.input_pixel_format 只能是 RAW8/RAW10/RAW12/GREY/YUV422; NV12 不能作为输入格式。

输入格式允许的输出格式结果
YUV422YUV422原样搬运,DMSC 旁路
YUV422NV12取 DMSC 旁路后的 YUV 输出,按 YUV420 双通道写入
RAW8RAW8原样搬运,DMSC 旁路
RAW8NV12DMSC 在通路上做 RAW→YUV420
RAW10RAW10原样搬运,DMSC 旁路
RAW10NV12DMSC 在通路上做 RAW→YUV420
RAW12RAW12原样搬运,DMSC 旁路
RAW12NV12DMSC 在通路上做 RAW→YUV420
GREYGREY原样搬运,DMSC 旁路
其他组合—IM_ERR_UNSUPPORTED

组合在 camera_set_format()(以及 camera_open() 的默认格式路径)里检查; 不合法时不会分配新缓冲。

5.3 WDMA 选择

camera_prepare_wdma_config() 根据「输入格式 + 输出格式」选择:

输入 → 输出mux_seldump_data_typedump_data_orderenable_chroma
YUV422 → YUV422YUV422_PATHYUV422_DUAL1true
YUV422 → NV12DMSC_YUV_PATHYUV420_DUAL0true
RAW8 → RAW8RAW_PATHRAW8_OR_Y0false
RAW8 → NV12DMSC_YUV_PATHYUV420_DUAL1true
RAW10 → RAW10RAW_PATHRAW10_12_16_OR_UV0false
RAW10 → NV12DMSC_YUV_PATHYUV420_DUAL1true
RAW12 → RAW12RAW_PATHRAW10_12_16_OR_UV0false
RAW12 → NV12DMSC_YUV_PATHYUV420_DUAL1true
GREY → GREYRAW_PATHRAW8_OR_Y0false

补充:

  • stride 恒等于 line_length。

  • enable_chroma 为 true 时,chroma_stride = line_length,提交帧时chroma_start_address = start_address + uv_data_offset。

  • YUV422 的 uv_data_offset = 0,因此 chroma 起始地址与帧起始地址相同。

  • dump_data_order 对 RAW→NV12 为 1,对 YUV422→NV12 为 0;这是两条不同的 硬件通路,不要混用。

5.4 DMSC 旁路规则

TOP 配置由 camera_prepare_isp_configs() 生成:

top_config->width  = sensor_attr->width;
top_config->height = sensor_attr->height;
top_config->data_type = <由输入格式映射>;
top_config->bayer_type = (uint32_t)sensor_attr->bayer_pattern;
top_config->bypass_dmsc =
    (output_pixel_format != CAMERA_PIXEL_FORMAT_NV12) ||
    !camera_input_format_is_raw(sensor_attr->input_pixel_format);
top_config->statistics_position =
    camera_input_format_is_raw(sensor_attr->input_pixel_format) ? 1U : 0U;
输入 → 输出bypass_dmscstatistics_position
YUV422 → YUV42210
YUV422 → NV1210
RAW → RAW11
RAW → NV1201
GREY → GREY10

关键点:

HAL 默认值。RAW→NV12 路径依赖 DMSC 的复位默认配置。

  • YUV422→NV12 的 mux_sel 虽然是 DMSC_YUV_PATH,但 bypass_dmsc = 1: WDMA 取的是 DMSC 模块旁路后的 YUV 输出,不是 DMSC 转换结果。

  • statistics_position 对 RAW 输入为 1,对 YUV422/GREY 为 0,供 TOP 的 AE 统计取数点使用。

5.5 行/帧几何

width/height 来自 sensor_attr,不做裁剪。输出格式决定:

输出格式line_lengthframe_sizeuv_data_offset
RAW8 / GREYalign_up(width, 8)line * height0
RAW10 / RAW12align_up(width, 8) * 2line * height0
YUV422align_up(width, 8) * 2line * height0
NV12align_up(width, 16)line * height * 3 / 2line * height

对任意格式:

frame_align_size = align_up(frame_size, 4096)

校验条件(任一不满足返回 IM_ERR_INVALID_PARAM):

  • line_length 按 8 字节对齐;

  • line_length >= width(RAW8/GREY/NV12)或 >= width * 2(YUV422/RAW10/RAW12);

  • frame_size 按 8 字节对齐;

  • frame_size 不小于按最小行长得出的帧长;

  • frame_align_size >= frame_size;

  • uv_data_offset == 0,NV12 时为 line_length * height。

640x480 实例:

输出格式line_lengthframe_sizeframe_align_sizeuv_data_offset
YUV42212806144006144000
NV12640460800462848307200
RAW1012806144006144000

5.6 Sensor 属性到 HAL 配置的映射

5.6.1 DVP

camera_dvp_input_config_t去向说明
timingtiming_modeHREF / HSYNC / SONY
hsync_polarity / vsync_polarity同名极性字段高有效/低有效
sample_edgesample_edge上升沿/下降沿

宽高取 format.width/height;data_format 由输入格式映射: RAW10/RAW12 原样,YUV422/NV12→YUV422_8BIT,其余→RAW8。

5.6.2 MIPI

camera_mipi_input_config_t去向
lane_countCSI lane_count
lane_rate_hzD-PHY lane_rate_hz
total_widthVIC total_width(0 时取输出宽;小于输出宽时抬到输出宽)
hcrop_start_pixelVIC hcrop_start_pixel
vcrop_start_lineVIC vcrop_start_line
start_delayVIC start_delay
start_channelVIC start_channel(直接按枚举强转)
enable_data_type_checkVIC 同名
enable_horizontal_compare / enable_vertical_compareVIC 同名

中间件固定的 MIPI RX 配置:

  • CSI:check_version = false、unmask_errors = false;

  • D-PHY:use_explicit_settle_range = false、continuous_clock = false、enable_calibration = false、enable_data0_msb = false;

  • VIC:sync_mode = HEADER、sensor_frame_mode = LINEAR;

5.6.3 BT.656

5.6.4 TOP / IPC / DMSC / WDMA

  • TOP:见 §6.4;data_type 由输入格式映射(RAW10/12、YUV、其余 RAW8)。

  • WDMA:见 §6.3。

5.7 YUV422 字节序

camera_format_context_t.output_yuv422_byte_order 固定写成CAMERA_YUV422_BYTE_ORDER_YUYV,camera_status_t.yuv422_byte_order 读回 也是它,帧缓冲里的实际字节顺序同样是 YUYV。

WDMA 的 dump_data_order 复位值为 0,双通道 YUV422 下写进 DDR 的是 YVYU;camera_prepare_wdma_config() 在 YUV422 → YUV422 分支把它置 1(§6.3), 写入顺序才与声明一致,可以直接把帧按 YUYV 交给编码器或显示。若拿到的帧 红蓝对调而中性灰仍然中性(色度轴对调的特征),先确认camera_prepare_wdma_config() 在 YUV422 分支里把 dump_data_order 置了 1; 判据是比较 U/V 均值是否对调。

走 NV12 输出的路径同理:RAW → NV12 真过 DMSC 的那一支也置dump_data_order = 1,UV 平面按 Cb 先写(§6.3)。

6. 帧管理与缓存

6.1 分配与物理地址

  • 帧数据池:memalign(4096, frame_align_size * frame_count),实际长度为 缓存行对齐后的长度。

  • 帧描述符:memalign(64, sizeof(camera_frame_t) * frame_count)。

  • 每帧 info.paddr = im_mmio_virt_to_phys(addr);info.size 写的是frame_align_size。

  • 提交给 WDMA 的起始地址会做 & 0x1FFFFFF8 掩码(低 3 位清零、限制在 32 位物理地址窗口内)。

6.2 camera_wait_frame()

void *camera_wait_frame(camera_device_t *dev, uint32_t timeout_ms);

流程:

  1. dev == NULL 或未在出流 → 返回 NULL。

  2. camera_dequeue_frame():

    • 先从 usable_list 取一帧;

    • 取不到且 timeout_ms != 0 时在条件变量上等待;

    • 超时:置 wait_timeout = true,返回 IM_ERR_TIMEOUT;

    • 超时后仍无帧,或非阻塞轮询无帧:返回 IM_ERR_NOT_READY。

  3. 成功:帧状态置 USER,对整块缓冲做数据 cache invalidate,拷贝帧信息, 清 wait_timeout,返回虚拟地址。

camera_wait_frame() 把所有失败都折叠成 NULL。要区分「超时」与「未开流」, 用 camera_get_frame_error():

返回值含义
IM_ERR_TIMEOUT最近一次等帧超时
IM_ERR_NOT_READY未配置或未出流
IM_OK正常

空取不一定代表链路坏了:帧池被占满、消费者太慢、或上一轮排队帧刚被计数掉, 都会出现若干次空取,随后又能取到。判断链路是否正常,应看连续空取次数与frame_done_count 是否继续增长。

6.3 camera_put_frame()

error_code_t camera_put_frame(camera_device_t *dev, void *buffer);
  • 只接受 camera_wait_frame() 返回的指针。

  • 校验:缓冲必须落在 dev->mem 内,索引由 (buffer - mem) / frame_align_size算出,且与描述符的 vaddr 完全一致。

  • 帧状态必须是 USER;否则返回 IM_ERR_INVALID_PARAM。

  • 归还时对整块缓冲做 cache invalidate,然后挂回 free_list。

  • 同一帧不能归还两次;不要在未取帧时归还任意地址。

6.4 WDMA 完成路径

camera_handle_wdma_done(dev, drop_completed_frame) 在中断上下文执行:

if (dma_index 越界)            -> error_count++,返回
if (不在 STREAMING)            -> error_count++,返回
dma_index 翻转;done_count++
completed = (transfer[0] != transfer[1]) ? transfer[old_index] : NULL

if (drop_completed_frame) error_count++

if (需要丢弃 && completed != NULL):
    next = completed                  /* 原帧直接重新提交 */
    completed = NULL
else:
    next = 空闲帧
    if (无空闲帧) next = 最早的 usable 帧(从应用队列里偷)
    if (仍无)     next = 另一个在途槽位(两槽指向同一帧)

transfer[old_index] = next
提交 next 给 WDMA(失败则 error_count++)

if (completed != NULL):
    sequence = done_count
    timestamp_us = im_time_get_us()
    挂入 usable_list;signal 条件变量

由此产生的行为:

  • 每次有效完成事件(处于出流且 dma_index 合法)都会让 frame_done_count加一,包括被丢弃的帧。

  • frame_error_count 统计:VIC/CSI 错误导致的丢帧、dma_index 越界、 非出流状态下的完成中断、WDMA 提交失败。

  • 应用长期不归还帧时,中间件会从 usable_list 偷帧,甚至让两个在途槽位 指向同一帧;后者会导致某些完成事件不发布帧,直到应用归还缓冲。

  • 发布顺序由 usable_list 尾部追加,camera_wait_frame() 从头部取,因此是 FIFO(在未发生偷帧的正常情况下)。

7. Sensor 驱动接口

7.1 操作表

camera_sensor_ops_t:

回调必需调用时机
init(user_ctx, port_config)否camera_register_device() 调用一次
deinit(user_ctx)否当前从不调用(没有注销流程)
get_attr(user_ctx, attr_out)是camera_open()
power_on(user_ctx)否camera_open() 成功路径
power_off(user_ctx)否camera_close()(且状态 >= OPENED)
stream_on(user_ctx)是DVP/MIPI 的 camera_start();BT656 不调用
stream_off(user_ctx)否camera_stop() / camera_close() 的 DVP/MIPI 路径
set_fps / get_fps否camera_set_fps() / camera_get_fps()
set_gain / get_gain否camera_set_gain() / camera_get_gain()
set_exposure / get_exposure否camera_set_exposure() / camera_get_exposure()
read_register / write_register否中间件不调用,仅供调试/应用使用
user_ctx—中间件原样回传

未实现的可选回调返回 IM_ERR_NOT_READY(FPS/增益/曝光)。

驱动职责:

  • 在 init() 中保存所需端口资源;port_config 不会被中间件保留。

  • get_attr() 返回实际总线格式,必须与硬件初始化表一致。

  • power_on() 负责供电/复位/MCLK/初始化表;stream_on() 只负责开流。

  • 操作表按值拷贝到设备,user_ctx 指向的上下文生命周期必须覆盖整个 打开/关闭周期。

  • 同一 index 不要注册两颗 sensor。

7.2 Sensor 属性校验

camera_open() 通过 camera_prepare_sensor_attr() 校验:

条件不满足的后果
width != 0 && height != 0IM_ERR_INVALID_PARAM
input_pixel_format ∈ {RAW8, RAW10, RAW12, GREY, YUV422}IM_ERR_INVALID_PARAM
bayer_pattern <= GBRGIM_ERR_INVALID_PARAM
input.type ∈ {DVP, MIPI, BT656}IM_ERR_INVALID_PARAM
MIPI:lane_count != 0 && lane_rate_hz != 0IM_ERR_INVALID_PARAM
YUV422 输入:input_yuv422_byte_order <= VYUYIM_ERR_INVALID_PARAM

output_pixel_format 的合法性(必须是已知枚举)在 camera_prepare_format_context()里检查;与输入格式的兼容性在 §6.2 的组合矩阵里检查。

7.3 板级接入(BSP)

中间件不认识「板子」,它只认识一个 camera_sensor_port_config_t。BSP 的职责就 是把板上的 I2C、GPIO、MCLK 整理成这个结构体,再把 sensor 驱动登记进来。这一节 以参考板 code/bsp/g32s10/g32s103_sk_slt/ 为例说明做法,自己的板子照这个 结构改。

7.3.1 参考板接线

参考板有两个相机接口:DVP 接口接 CIM1,MIPI 接口接 CIM0。 两者共用一条 I2C1 控制总线和一组常通的电源轨。

信号引脚功能 / 归属说明
CIM1_D0–D7PA2–PA9功能 3DVP 数据
CIM1_PCLKPA11功能 3DVP 像素时钟
CIM1_HSYNC / VSYNCPA12 / PA13功能 3DVP 行/场同步
CIM1_MCLKPA10功能 3CIM1 的 MCLK 输出,24 MHz 给 DVP 接口的 sensor
CIM1_RSTPA0GPIO 输出DVP 接口复位,低有效
CIM1_PWDNPA1GPIO 输出DVP 接口掉电/待机,高有效
CIM0_MCLKPB20功能 0CIM0 的 MCLK 输出,24 MHz 给 MIPI 接口的 sensor
CIM0_PWDNPB19GPIO 输出MIPI 接口的 XSHUTDN,高有效
I2C1_SDA / SCKPB24 / PB25功能 0两路接口共用的控制总线

对应到两条例程:


DVP / GC0308MIPI / SC035HGS
采集入口CIM1 = index 1CIM0(MIPI → CSI)= index 0
sensor 总线DVP,YUV422MIPI CSI-2,1 lane / 170 MHz
I2C 地址0x210x30
复位 / 待机PA0 / PA1同一个 PB19(sensor 只有 XSHUTDN 一个控制脚)
供电电阻常通,无使能脚电阻常通,无使能脚
MCLKCIM1,24 MHzCIM0,24 MHz

几个容易踩的点:

  • index 由 sensor 接到哪一路 CIM 决定。 sensor 接在 CIM1 上就用 index 1, 接在 CIM0 上就用 index 0;MIPI 只能走 index 0,因为 CSI/D-PHY 只有一份 (§3.1)。

  • CIM0 接 MIPI 还是 DVP 由板级决定。 芯片对外暴露的是 CIM0、CIM1 两路相机 接口,参考板把它们分别接成了 MIPI 和 DVP。MIPI 数据经 CSI 进入对应实例的 VIC。

  • MCLK 由 SoC 提供时,mclk_index 按实际走线填。 参考板上两块 sensor 的 24 MHz 都来自 CIM 的 MCLK 输出,不是板上的晶振。

  • I2C 的 SCL 是 input_clock_hz 分频出来的。 input_clock_hz 要填 I2C 控制器 实际的输入时钟,填错会让 SCL 偏离目标波特率,轻则偶发 NACK,重则完全读不到 chip id。

  • 总线只能注册一次。 两路接口共用 I2C1,BSP 用「已注册就直接返回」来防止重复 注册(board_camera_i2c_register())。

  • 没有电源使能脚时不要填一个假的。 参考板的 power_enable_signal 填的是IM_GPIO_SIGNAL(IM_GPIO_PORT_INVALID, 0U, false),驱动据此跳过电源脚操作, 只等电源轨稳定。

  • CSI 和 D-PHY 不需要板级配置。 中间件开流时按 sensor 上报的 lane 数和速率 自己配置,BSP 里没有对应代码。

7.3.2 BSP 需要提供的三样东西

  1. pinmux:在 board_pinmux.c 的信号表里把 CIM 数据脚/时钟、I2C 引脚、GPIO 控制脚配好。表在板级初始化时统一应用,驱动里不要再改 pinmux。

  2. I2C 总线与设备:注册总线和每颗 sensor 的从设备句柄。总线是全局资源, 用「是否已注册」保护;设备句柄缓存起来复用。

  3. 端口配置函数:board_<sensor>_configure(camera_sensor_port_config_t *), 把上面的资源填进去。参考板上的写法:

error_code_t board_gc0308_configure(camera_sensor_port_config_t *config_out)
{
    error_code_t ret;

    if (config_out == NULL) {
        return IM_ERR_INVALID_PARAM;
    }
    ret = board_camera_power_enable();          /* 等电源轨稳定,只做一次 */
    if (ret != IM_OK) {
        return ret;
    }
    ret = board_gc0308_get_i2c_device(&s_gc0308_i2c_device);
    if (ret != IM_OK) {
        return ret;
    }
    *config_out = (camera_sensor_port_config_t){
        .i2c_device = s_gc0308_i2c_device,
        .power_enable_signal = s_board_gc0308_resource.power_enable_signal,
        .reset_signal = s_board_gc0308_resource.reset_signal,
        .standby_signal = s_board_gc0308_resource.standby_signal,
        .mclk_hz = s_board_gc0308_resource.mclk_hz,
        .mclk_index = s_board_gc0308_resource.mclk_index,
    };
    return IM_OK;
}

板上参数集中放在文件顶部的 s_board_*_resource 静态表里(地址、GPIO、MCLK),configure() 只做搬运,便于换板时逐条核对。应用侧只需要两行:

board_gc0308_configure(&sensor_port);        /* 或 board_sc035_mipi_configure() */
camera_register_device(1U, &sensor_ops, &sensor_port);

camera_register_device() 会立刻用这份配置调用驱动的 init(),所以configure() 失败就不要登记。

7.3.3 换一块板子时的检查清单

  1. sensor 接在 CIM0 还是 CIM1?走 MIPI 还是 DVP?这两点一起确定 index。

  2. 数据脚、PCLK、HSYNC/VSYNC 的 pinmux 是否配成了 CIM 功能?

  3. I2C 用哪一组?input_clock_hz、bitrate_hz 是否与实际时钟一致?地址是 7 位还是 8 位(中间件按 7 位)?

  4. 复位、待机脚是哪个 GPIO、有效电平是什么?没有的脚填 IM_GPIO_PORT_INVALID。

  5. MCLK 是 SoC 给还是板上晶振给?SoC 给的话是哪一路 CIM、多少 Hz?

  6. 电源是否常通?需要上电时序的话,把延时/拉脚放在 configure() 或驱动的power_on() 里,不要放在应用里。

  7. camera_open() 能否成功、能不能读到 chip id —— 这是板级接线是否正确的第一 个判据(§13)。

7.4 接入一颗新 sensor

换一颗 sensor 要动三层,中间件本身不用改:

步骤做什么改哪里
1. 驱动提供操作表:属性、上电时序、初始化表、开流新增 code/devices/camera/<sensor>/,并在 code/devices/camera/{Kconfig,CMakeLists.txt} 登记
2. BSP把板上的 I2C、GPIO、MCLK 整理成端口配置code/bsp/g32s10/<board>/ 下的 board_camera.c、include/bsp/board_camera.h、board_pinmux.c、include/board_pins.h、CMakeLists.txt
3. 应用登记驱动、选格式、出流你的 SDK_APP_DIR;以 code/middleware/camera/example/ 为模板

参考实现:DVP 看 code/devices/camera/gc0308/,MIPI 看code/devices/camera/sc035_mipi/。

7.4.1 第一步:写驱动

放哪儿:新建 code/devices/camera/<sensor>/(建议带上总线类型,如xxx_dvp/、xxx_mipi/,同一颗 sensor 两种接法可以各成一份),目录里放四类文件:

文件写什么
sensor_<sensor>.c驱动本体:寄存器表、I2C 小工具、全部回调、操作表
sensor_<sensor>.h只对外导出一个取操作表的函数
CMakeLists.txt组件注册,受 CONFIG_DEVICE_SENSOR_<SENSOR> 控制
Kconfig新增 CONFIG_DEVICE_SENSOR_<SENSOR>(需要多档位时再加子符号)

再在 code/devices/camera/Kconfig 里 source 新目录的 Kconfig,在code/devices/camera/CMakeLists.txt 里 include 新目录的 CMakeLists.txt, 两处各一行。

sensor_<sensor>.h:只做声明,不暴露寄存器细节。

#ifndef SENSOR_XXX_H
#define SENSOR_XXX_H

#include "im_camera_sensor.h"

#ifdef __cplusplus
extern "C" {
#endif

/**
 * @brief Fill the sensor callback table of one driver instance.
 * @param instance Driver context index (0 for a single camera).
 * @param ops_out Receives the callbacks; user_ctx is set to the context.
 */
error_code_t xxx_get_sensor_ops(uint32_t instance,
                                camera_sensor_ops_t *ops_out);

#ifdef __cplusplus
}
#endif
#endif /* SENSOR_XXX_H */

只有一颗同型号 sensor 时,也可以照 sc035_mipi 那样用void xxx_get_sensor_ops(camera_sensor_ops_t *) + 一个静态实例;板上可能同时 接两颗同型号时用上面的带 instance 形式(参考 gc0308,最多两路)。

sensor_<sensor>.c:从上到下按这个顺序写,对照 sensor_sc035_mipi.c:

  1. 常量与寄存器表:表元素用 { uint16_t reg; uint8_t value; },自己定两个 哨兵值(参考实现用 0xFFFF 结束、0xFFFE 表示延时多少毫秒);I2C 7 位地址、 单次访问超时;分辨率、默认帧率、MIPI 的 lane 数/速率或 DVP 的 H/V 时序。 初始化表按输出档位分档(如 RAW10/RAW8 各一张),用#ifdef CONFIG_DEVICE_SENSOR_XXX_RAW10 二选一,未被选中的表加__attribute__((unused))。

  2. 驱动上下文:保存 camera_sensor_port_config_t,再加is_port_initialized / is_configured / is_powered / is_streaming 四个状态位; 静态实例的地址就是 user_ctx,生命周期覆盖整个打开/关闭周期。init() 在 camera_register_device() 里被调用,负责从 config.i2c_device取句柄并置 is_port_initialized;deinit() 中间件从不调用,留 NULL或空实现即可。

  3. I2C 小工具:write_reg() / read_reg() / write_array() /read_array(),全部走 config.i2c_device,不要直接碰总线句柄。chip id 也 做成一张 read_array() 表。

  4. get_attr():填 camera_sensor_attr_t,字段见下表。

  5. power_on() / power_off():按camera_sensor_mclk_enable() → 电源使能 → 复位/待机时序 → 必要延时 → 写初始化表 的顺序;power_off() 反过来,最后camera_sensor_mclk_disable()。MCLK 没起来之前不要访问寄存器(§9.3)。

  6. stream_on() / stream_off():只写开流/停流那一两笔寄存器;初始化表 不要放在这里,每次 power_on() 都会重写一遍。

  7. 可选接口:set_fps/get_fps、set_gain/get_gain、set_exposure/get_exposure、read_register/write_register。不实现就留NULL,中间件返回 IM_ERR_NOT_READY(后两个中间件不调用,只供应用调试)。

  8. 操作表与导出:static const camera_sensor_ops_t s_xxx_ops = {...},在xxx_get_sensor_ops() 里整表拷贝出去并把 user_ctx 指向静态实例。

最小可用回调是 get_attr() 和 stream_on();实际驱动几乎都需要 init()(拿到 I2C 句柄)和 power_on()(MCLK、复位、初始化表)。

get_attr() 填什么:中间件后续所有硬件配置都由这份属性推出来,填错的 代价是「能开不能出帧」或「出帧但颜色不对」。字段含义:

字段填什么常见错误
width / heightsensor 实际输出的分辨率填了 binning 之前的分辨率
input_pixel_formatsensor 在本板上实际出的格式初始化表配成 RAW8,属性写 RAW10
output_pixel_formatcamera_open() 的默认输出格式与驱动默认值不一致
input_yuv422_byte_orderYUV422 输入的字节序(仅 YUV422 时有效)输出偏色、红蓝对调
input.typeDVP / MIPI / BT656板上是 MIPI 却写 DVP
input.bus.dvp.timing 等DVP 的 H/V 时序、采样沿、同步极性画面错位、行首撕裂
input.bus.mipi.lane_count/lane_rate_hzMIPI lane 数与每条 lane 的速率与 sensor 寄存器里的 lane 配置不一致
bayer_pattern实际 Bayer 相位颜色整体偏绿/偏紫

DVP 的 H 时序单位是像素,不是字节;只有 YUV422 8 bit 输出时一个像素才占两个 字节(§6.5)。

Kconfig:一颗 sensor 一个开关;MIPI 的 depends on 里再加 && DRIVER_CSI(参考 code/devices/camera/sc035_mipi/Kconfig)。RAW8/RAW10 这类输出档位做成子 开关,depends on 主开关。

config DEVICE_SENSOR_XXX_DVP
    bool "XXX DVP camera sensor"
    depends on MIDDLEWARE_CAMERA && DRIVER_I2C
    default n
    help
      Build the XXX DVP sensor core.

config DEVICE_SENSOR_XXX_DVP_RAW8
    bool "XXX DVP RAW8 output"
    depends on DEVICE_SENSOR_XXX_DVP
    default n

CMakeLists.txt:把 .c 注册成组件,REQUIRES im_camera。

if(CONFIG_DEVICE_SENSOR_XXX_DVP)
    imde_component_register(NAME dev_xxx_dvp
        SRCS "${CMAKE_CURRENT_LIST_DIR}/sensor_xxx_dvp.c"
        INCLUDE_DIRS "${CMAKE_CURRENT_LIST_DIR}"
        REQUIRES im_camera)
endif()

做到这里驱动就齐了,code/middleware/camera/ 一行都不用改。

7.4.2 第二步:写 BSP

放哪儿:code/bsp/g32s10/<board>/。要改的是下面五个文件;板级 Kconfig 不用动(sensor 开关在 code/devices/camera/Kconfig 里):

文件写什么
include/board_pins.h新增复位/待机 GPIO 宏,如 BOARD_XXX_RST / BOARD_XXX_PWDN
board_pinmux.c信号表里加引脚:DVP 加 D0–D7、PCLK、HSYNC、VSYNC、MCLK;MIPI 加 MCLK 和控制脚
include/bsp/board_camera.hboard_xxx_resource_t 结构体 + 三个函数声明
board_camera.c静态资源表、I2C 总线/设备注册、board_xxx_configure()
CMakeLists.txt把 CONFIG_DEVICE_SENSOR_XXX 加进编译 board_camera.c 的条件

include/board_pins.h:宏里带有效电平。

#define BOARD_XXX_RST   IM_GPIO_SIGNAL(IM_GPIO_PORT_A, 0U, false)
#define BOARD_XXX_PWDN  IM_GPIO_SIGNAL(IM_GPIO_PORT_A, 1U, true)

board_pinmux.c:功能号和上下拉按原理图与引脚复用表填。

/* 数据/时钟脚:功能号 */
IM_GPIO_PINMUX_CONFIG("XXX_D0", IM_GPIO_PORT_A, 2U,
                      IM_GPIO_FUNCTION_3, IM_GPIO_PULL_NONE,
                      IM_GPIO_SLEW_LOW, IM_GPIO_DRIVE_STRENGTH_10MA, false),
/* 复位/待机脚:GPIO 输出,带上电默认电平 */
IM_GPIO_PINMUX_CONFIG("XXX_RST", IM_GPIO_PORT_A, 0U,
                      IM_GPIO_FUNCTION_OUTPUT_HIGH, IM_GPIO_PULL_NONE,
                      IM_GPIO_SLEW_LOW, IM_GPIO_DRIVE_STRENGTH_10MA, false),

MIPI 的 data/clock lane 不需要 pinmux(D-PHY RX 在片内),只配 MCLK 输出和 XSHUTDN 这类控制脚。board_pins.h 里定义的每个信号都要在这里有对应的条目, 否则该引脚不会被配置成 GPIO/复用功能。

include/bsp/board_camera.h:资源结构体加三个函数。

typedef struct {
    uint8_t i2c_address;
    im_gpio_signal_t power_enable_signal;
    im_gpio_signal_t reset_signal;
    im_gpio_signal_t standby_signal;
    uint32_t mclk_hz;      /* 0 = 板级外部供时钟 */
    uint32_t mclk_index;   /* 0 = CIM0, 1 = CIM1 */
} board_xxx_resource_t;

const board_xxx_resource_t *board_xxx_get_resource(void);
error_code_t board_xxx_get_i2c_device(im_i2c_device_t **device_out);
error_code_t board_xxx_configure(camera_sensor_port_config_t *config_out);

board_camera.c:

  1. 文件顶部加静态资源表,地址、GPIO、MCLK 都集中在这张表里,换板只改这一处:

    static const board_xxx_resource_t s_board_xxx_resource = {
        .i2c_address = 0x30U,
        .power_enable_signal = IM_GPIO_SIGNAL(IM_GPIO_PORT_INVALID, 0U, false),
        .reset_signal = BOARD_XXX_RST,
        .standby_signal = BOARD_XXX_PWDN,
        .mclk_hz = 24000000U,
        .mclk_index = 0U,
    };
  2. I2C 沿用文件里的 board_camera_i2c_register() /board_camera_i2c_device_register():总线是全局资源,注册一次、 「已注册就直接返回」;每颗 sensor 在总线上挂一个从设备句柄并缓存起来复用。input_clock_hz 填控制器实际输入时钟,bitrate_hz 按 sensor 能接受的速率填。

  3. board_xxx_configure() 只做搬运:等电源轨稳定(没有使能脚时就是一次延时)→ 取 I2C 设备句柄 → 填 camera_sensor_port_config_t,模板见 §9.5.2。

板上没有的 GPIO 一律填 IM_GPIO_SIGNAL(IM_GPIO_PORT_INVALID, 0U, false),驱动会 跳过;不要为了「把结构体填满」造一个假引脚。

BSP CMakeLists.txt:新符号要加进条件,否则 board_camera.c 不会参与编译。

if(CONFIG_DEVICE_SENSOR_GC0308_DVP OR CONFIG_DEVICE_SENSOR_GC0328_DVP
   OR CONFIG_DEVICE_SENSOR_SC035_MIPI OR CONFIG_DEVICE_SENSOR_XXX_DVP)
    list(APPEND _board_sources
        "${CMAKE_CURRENT_LIST_DIR}/board_camera.c")
    list(APPEND _board_requires im_i2c im_camera)
endif()

接线核对清单见 §9.5.3。

7.4.3 第三步:写应用

放哪儿:你的 SDK_APP_DIR。最省事的做法是把code/middleware/camera/example/ 复制一份当模板:DVP sensor 抄gc0308_YUV422_640_480_DVP_ISP1.c,MIPI sensor 抄sc035_RAW10_640_480_MIPI_ISP0_dmsc_Y.c,然后改下面几处:

文件改什么
main/<新例程>.c换 xxx_get_sensor_ops()、board_xxx_configure()、EXAMPLE_CAMERA_INDEX、camera_set_format();文件头的数据通路和日志前缀跟着改
main/main.c加一个 #if defined(CONFIG_DEVICE_SENSOR_XXX) 分支选入口
main/CMakeLists.txt同一个配置符号下挑新源文件
CMakeLists.txt配置自检:SDK_APP_DIR 非空、MIDDLEWARE_CAMERA、板型、有且只有一个 sensor 符号、档位符号
camera_example_<sensor>_defconfig从最接近的 defconfig 复制,改 sensor 与档位符号
README.md可选:加一行说明

main/CMakeLists.txt 的选择逻辑:

if(CONFIG_DEVICE_SENSOR_XXX_DVP)
    set(_camera_example_body "${CMAKE_CURRENT_LIST_DIR}/xxx_640_480_dvp.c")
else()
    set(_camera_example_body "${CMAKE_CURRENT_LIST_DIR}/gc0308_..._ISP1.c")
endif()

main.c 的入口选择:

#if defined(CONFIG_DEVICE_SENSOR_XXX_DVP)
int xxx_640_480_dvp_run(void);
#else
int gc0308_YUV422_640_480_DVP_ISP1_run(void);
#endif

index 在应用里确定:sensor 接在 CIM1 就登记到 index 1,接在 CIM0 就登记到index 0;MIPI 和 RAW→NV12 只能走 index 0(CSI/D-PHY 只有一份,DMSC 只在 ISP0 那一侧,§3.1)。index 同时决定 CIM/ISP/VIC 实例。

defconfig 里和相机相关的符号至少有这些(MIPI 才需要 DRIVER_CSI; defconfig 本身是整份 SDK 配置,其余行照抄最接近的那份即可):

CONFIG_BSP_BOARD=y
CONFIG_BSP_PINMUX=y          # 引脚配置表生效
CONFIG_BOARD_<你的板子>=y
CONFIG_DRIVER_GPIO=y
CONFIG_DRIVER_I2C=y
CONFIG_DRIVER_VIC=y
CONFIG_DRIVER_CSI=y                   # MIPI 才需要
CONFIG_MIDDLEWARE_CAMERA=y
CONFIG_DEVICE_SENSOR_XXX_DVP=y
CONFIG_DEVICE_SENSOR_XXX_DVP_RAW8=y   # 按实际输出档位

构建与烧录:

tools/imde/bin/imde use-defconfig camera_example_xxx_defconfig
tools/imde/bin/imde -D SDK_APP_DIR=<你的 app 目录> configure
tools/imde/bin/imde build

烧 build/artifacts/spl_with_zero.bin。应用里的调用顺序见 §12.7。

7.4.4 调试顺序

出不了图时按这个顺序查,能少走弯路:

  1. camera_open() 返回 0 吗?失败先看 §13 的错误码表。

  2. 读 chip id 对不对?不对说明 I2C 地址/总线/MCLK 有问题,与 ISP 无关;可用 驱动的 read_register() 或 chip id 表核对。

  3. stream_on() 后 frame_done_count 增长吗?不增长说明 VIC 没收到数据: 先确认 sensor 真的在输出(示波器看 PCLK/HSYNC/VSYNC 或 MIPI 时钟),再查 DVP 时序参数、MIPI lane 配置。

  4. 有帧但全黑/颜色不对:查 input_pixel_format、Bayer 相位和输出格式组合 (§6.2、§6.6)。

8. AE 统计中间件

8.1 定位与约束

  • 单实例:s_im_camera_ae_dev,只支持 IM_CAMERA_AE_INSTANCE_0。

  • 固定使用 ISP0,与 camera index 无关。相机在 ISP1 上出流时,AE 仍然读 ISP0;ISP1 没有 AE。

  • 与采集中间件独立:camera_open()/camera_start() 不会初始化 AE,AE 也不会 自动使能/关闭相机。

  • 只提供统计,不运行 AEC 算法;apply_result() 只是把给定值下发给 sensor。

  • 设备指针是中间件内部静态对象,应用不能分配或解引用。

  • 配置接口不应在出流期间调用;deinit() 必须在停流后调用。

8.2 初始化

void im_camera_ae_config_struct_init(im_camera_ae_config_t *config);
error_code_t im_camera_ae_init(im_camera_ae_t **dev_out,
                               const im_camera_ae_config_t *config);

im_camera_ae_config_struct_init() 的默认值:

字段默认值
instanceIM_CAMERA_AE_INSTANCE_0
sensor全 0(无回调)
statistics_buffer_count4
histogram_buffer_count4

im_camera_ae_init() 校验:

  • instance == INSTANCE_0;

  • 两个 buffer count 都在 1..4;

  • 模块尚未初始化(否则 IM_ERR_BUSY)。

初始化后的硬件默认参数:

参数默认值
分区网格15 × 15
H/V 分区尺寸20 / 16
暗区阈值30
饱和阈值230
统计频率 / 直方图频率1 / 1
特定区域关闭
影子更新HAL 默认(使能)
特定区域输入帧号8

初始化步骤:

  1. 清零模块并保存配置;

  2. 分配统计/直方图 DMA 缓冲(任一失败时释放已分配部分);

  3. 写物理地址表(未使用的槽位写 0);

  4. 状态置 READY。

注意:AE 配置写的是 ISP0 寄存器。调用前应确保 ISP 时钟已打开 (通常先 camera_open() 同一 ISP0 的相机)。

8.3 DMA 缓冲区

统计缓冲(每个 4 KiB):

+------------------+-----------------------------+-----------------------------+
| 8 字节头(跳过) | zone 0: 4 words             | zone 1: 4 words ...         |
+------------------+-----------------------------+-----------------------------+
每个 zone:
  word0: 暗区亮度和(低 22 bit)
  word1: 中间区亮度和(低 22 bit)
  word2: 饱和区亮度和(低 22 bit)
  word3: [31:16] 饱和像素计数,[15:0] 暗像素计数

zone 数量 = zone_v_number * zone_h_number,行主序:index = v * zone_h_number + h。

直方图缓冲(每个 2 KiB):

+------------------+-----------------------------------------------+
| 8 字节头(跳过) | 128 组 × 2 words → 256 个 bin                 |
+------------------+-----------------------------------------------+
每个 word 低 22 bit 为一个 bin 的计数。

15×15 分区时统计区实际使用 8 + 225 * 16 = 3608 字节,仍小于 4 KiB。

8.4 逐帧处理

im_camera_ae_process():

  1. 读 HAL DMA 状态;

  2. 若 ae_frame_number 与上次相同,直接返回 IM_OK;

  3. 按缓冲索引 ae_current_base 定位统计缓冲,invalidate 后解析分区统计;

  4. 按缓冲索引 hist_current_base 定位直方图缓冲,invalidate 后解析 256 bin;

  5. 读特定区域寄存器结果;

  6. 发布 ae_statistics.ae_frame_number,更新 last_ae_frame_number;

  7. 若注册了统计回调,在中断上下文调用回调。

整份 camera_ae_statistics_t 是一个快照,但两个帧号标识不同的硬件路径:

  • ae_frame_number:AE 统计 DMA 的帧号(0..255 回绕);

  • spec_frame_number_out:特定区域的输出帧号,可能因流水线延迟与之不同。

im_camera_ae_isr() 是中断入口:

void im_camera_ae_isr(void);
  • 每次进入都 isr_count++,即使模块未就绪;

  • 模块就绪时调用 im_camera_ae_process();

  • 不做分配、加锁或日志,可在中断上下文安全调用。

相机中断处理里,AE_STATIC_DONE 事件会无条件调用它(见 §8.2)。

8.5 配置接口

API作用生效方式
im_camera_ae_set_zone()设置分区起点、行列数、每格像素宽/高重新写 HAL,影子寄存器下一帧生效
im_camera_ae_set_thd()设置暗区/饱和阈值(0..255)同上
im_camera_ae_set_fps()设置统计/直方图频率(0..15)同上
im_camera_ae_set_spec_area()设置特定区域窗口立即写入 HAL;坐标仅在 enable=true 时校验

set_zone() 细节:

  • h_start/v_start 范围 0..4095;行列数 1..15;

  • 整张尺寸表可以为 NULL,此时全部使用默认值(H=20,V=16);

  • 非 NULL 时只取前 h_number/v_number 项,值必须非 0 且 ≤255;

  • 未使用到的表项仍被填成默认值。

set_spec_area() 细节:

  • HAL 配置结构里没有独立的 enable 位;enable 只记录在中间件快照中并决定 是否校验坐标;

  • 无论 enable 与否,坐标都会写进 HAL;

  • spec_pixel_count = (h_end - h_start) * (v_end - v_start) / 4(enable 时), 单位是 4 像素组。

所有配置 API 要求模块为 READY,否则 IM_ERR_INVALID_PARAM(IM_PARAM_CHECK 的返回码)。

8.6 取数接口

API内容
im_camera_ae_get_stats()完整快照:分区、直方图、特定区域、帧号
im_camera_ae_get_spec()只取特定区域结果与 frame_number_out
im_camera_ae_get_hist()只取 256 个直方图 bin
im_camera_ae_get_dma_info()当前 DMA 缓冲索引、帧号、mismatch/ack 状态
im_camera_ae_get_isr_count()进入 im_camera_ae_isr() 的次数(无状态检查)

get_stats() 返回的是最近一次解析完成的快照,不是阻塞采集;应用应结合ae_frame_number 判断是否是新帧。

8.7 下发 AE 结果

error_code_t im_camera_ae_apply_result(im_camera_ae_t *dev,
                                       const unsigned int ae_result[4]);

前置条件:模块为 READY。数组顺序:

下标内容处理
0曝光调用 sensor.set_exposure()
1增益调用 sensor.set_gain()
2数字增益只保存,不下发
3稳定标志只保存,不下发
  • 回调为 NULL 时跳过该项。

  • set_exposure() 失败会立即返回,后续 set_gain() 不再调用。

  • 本函数只负责下发,不校验数值范围。

8.8 使用骨架

im_camera_ae_t *ae;
im_camera_ae_config_t ae_config;
camera_ae_statistics_t stats;

im_camera_ae_config_struct_init(&ae_config);
ae_config.sensor = sensor_ops;          /* 需要曝光/增益回调用 */

if (im_camera_ae_init(&ae, &ae_config) == IM_OK) {
    /* 相机已在 ISP0 上开流后,每个 AE_STATIC_DONE 由相机中断驱动解析 */
    if (im_camera_ae_get_stats(ae, &stats) == IM_OK) {
        /* 用 stats.zone_sum/... 计算曝光与增益 */
    }
}

停流后再 im_camera_ae_deinit(ae);deinit 会先关闭 TOP 的 AE/直方图使能, 再释放 DMA 缓冲。

9. 对外接口速查

9.1 采集接口(im_camera_vic.h)

API说明
camera_register_device(index, ops, port)登记 sensor 与板级资源
camera_detect(index)按号取设备句柄,不探测硬件
camera_open(dev)开时钟、读属性、配链路、分配缓冲、上电
camera_set_format(dev, fmt)打开后、开流前切换输出格式
camera_start(dev)配 VIC/ISP/WDMA、提交两帧、启动输入
camera_stop(dev)停输入、复位 WDMA、保留缓冲
camera_close(dev)停流、释放 IRQ/缓冲、下电、关时钟
camera_set_fps/get_fps透传 sensor 回调
camera_set_gain/get_gain透传 sensor 回调
camera_set_exposure/get_exposure透传 sensor 回调
camera_wait_frame(dev, timeout_ms)取一帧,失败返回 NULL
camera_put_frame(dev, buf)归还一帧
camera_get_available_frame_count(dev)待取帧数量
camera_is_streaming(dev)是否出流
camera_get_frame_error(dev)最近一次等帧错误
camera_get_status(dev, &status)当前格式与帧池信息
camera_get_frame_stats(dev, &stats)完成/错误计数

9.2 Sensor 辅助接口(im_camera_sensor.h)

API说明
camera_sensor_mclk_enable(config)按端口配置打开 sensor MCLK
camera_sensor_mclk_disable(config)关闭 sensor MCLK

9.3 AE 接口(im_camera_ae.h)

API说明
im_camera_ae_config_struct_init(&config)填默认配置
im_camera_ae_init(&dev, &config)初始化 AE 模块与 DMA
im_camera_ae_deinit(dev)停采集、释放缓冲
im_camera_ae_apply_result(dev, result)下发曝光/增益
im_camera_ae_set_zone(...)配置分区
im_camera_ae_set_thd(...)配置阈值
im_camera_ae_set_fps(...)配置统计/直方图频率
im_camera_ae_set_spec_area(...)配置特定区域
im_camera_ae_get_spec(dev, &spec)特定区域结果
im_camera_ae_get_stats(dev, &stats)完整快照
im_camera_ae_get_hist(dev, hist)直方图
im_camera_ae_process(dev)处理一帧统计(仅 ISR 调用)
im_camera_ae_isr(void)AE 中断入口
im_camera_ae_get_isr_count(dev, &count)ISR 进入次数
im_camera_ae_get_dma_info(dev, &info)DMA 状态
im_camera_ae_set_stats_callback(dev, cb, ctx)统计帧完成回调
im_camera_ae_dump_regs(dev)打印 AE 寄存器(需调试辅助开关)

10. 启用与示例

10.1 CMake

code/middleware/camera/CMakeLists.txt:

if(CONFIG_MIDDLEWARE_CAMERA)
    imde_component_register(NAME im_camera
        SRCS
        "${CMAKE_CURRENT_LIST_DIR}/src/im_camera_vic.c"
        "${CMAKE_CURRENT_LIST_DIR}/src/im_camera_sensor.c"
        "${CMAKE_CURRENT_LIST_DIR}/src/im_camera_frame.c"
        "${CMAKE_CURRENT_LIST_DIR}/src/im_camera_config.c"
        "${CMAKE_CURRENT_LIST_DIR}/src/im_camera_ae.c"
        INCLUDE_DIRS "${CMAKE_CURRENT_LIST_DIR}/include"
        REQUIRES im_i2c)
endif()

10.2 代码走读:从上电到第一帧

示例代码只做七步,每一步为什么放在那里、调用后硬件处于什么状态,见下表。代码 目录:code/middleware/camera/example/main/。

步骤调用硬件状态 / 说明详见
1gc0308_dvp_get_sensor_ops() / board_gc0308_configure()拿到驱动操作表和板级接线(I2C 句柄、GPIO、MCLK)。不碰硬件§9.1、§9.5
2camera_register_device(index, &ops, &port)只登记:驱动的 init() 把接线存起来。已登记过返回 IM_ERR_ALREADY_REGISTERED,可视为成功§5.1
3camera_detect(index)按号取设备句柄,不探测硬件。所以必须先 register 再 detect§5.2
4camera_open(camera)打开 ISP 模块时钟;sensor 上电、复位、发初始化表;按默认输出格式配链路并分配帧缓冲。真正的硬件探测在这里§5.3
5camera_set_format(camera, fmt)改输出格式并重新分配帧缓冲;必须在 open 之后、start 之前§5.4、§6.2
6camera_get_status()打印实际生效的几何与帧池大小,对上表(§12.4)说明配置无误§4.5
7camera_start(camera) → 取帧循环 → camera_stop() / camera_close()从 start 返回开始,帧就在往内存里落§5.5、§7.2、§5.6、§5.7

取帧循环里只有三条规则,其余都是这段循环的变体:

  1. camera_wait_frame() 返回的指针属于中间件。 用完必须camera_put_frame(),否则帧池(3 帧)很快被占满,之后只能空取。

  2. 返回 NULL 不等于链路坏了。 帧池占满、上一轮排队的帧刚被计数掉,都会 导致若干次空取,接着又能取到。用「连续空取次数」判断,并给它一个上限 (示例是 10 次 × 1 s);单次空取只累计、不退出。

  3. 对帧的所有处理都必须在 camera_put_frame() 之前做完。 归还之后 WDMA 随时可能重写这块内存。

失败路径上不要调用 camera_close():ISP 模块时钟是两颗 ISP 共用的,开关没有 引用计数,camera_open() 失败时它已经自行收尾,再 close 会把另一路也关掉 (§5.3)。