API GUIDE / VIC / Middleware
G32S10 VIC Middleware 接口
Middleware 层接口:分层职责:
1. 分层关系
分层职责:
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 RX | MIPI 物理层 | 按 sensor 报告的 lane 数/速率配置,等 lane 稳定后再启动 VIC | 片上集成,不需要板级 pinmux;停流/关闭时由中间件关断 |
| TOP | ISP 前端:尺寸、输入数据类型、Bayer 相位、DMSC 旁路、AE 统计取数点 | camera_start() 里写入;statistics_position 按输入是否 RAW 选择 | Bayer 相位填错会偏色(§6.6) |
| IPC | ISP 处理链 | 使用 HAL 默认配置 | 不走 RAW→NV12 时基本不用动 |
| DMSC | RAW → 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_COUNT | 2 | 相机实例数 |
CAMERA_DEFAULT_FRAME_COUNT | 2 | 应用侧可见帧数;实际帧池为 +1(内部流水线) |
CAMERA_ALIGN_SIZE | 8 | 行/帧尺寸对齐要求 |
CAMERA_FRAME_ALIGN_SIZE | 4096 | 每块帧缓冲的对齐粒度,也是帧池分块大小 |
CAMERA_WDMA_INFLIGHT_COUNT | 2 | WDMA 同时在途帧数 |
CAMERA_WAIT_FRAME_TIMEOUT_MS | 3000 | 预留宏,当前中间件未使用;等帧超时由调用者传入 |
CAMERA_MIPI_RX_HAL_ENABLED | CONFIG_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_COUNT | 15 | AE 分区最大行/列数 |
CAMERA_AE_DMA_BUFFER_COUNT_MIN/MAX | 1 / 4 | 统计与直方图 DMA 缓冲数量范围 |
CAMERA_AE_HISTOGRAM_BIN_COUNT | 256 | 解包后的直方图 bin 数 |
CAMERA_AE_STATISTICS_BUFFER_SIZE | 4096 | 单个统计 DMA 缓冲字节数 |
CAMERA_AE_HISTOGRAM_BUFFER_SIZE | 2048 | 单个直方图 DMA 缓冲字节数 |
CAMERA_AE_DMA_BUFFER_ALIGNMENT | 64 | DMA 缓冲对齐要求 |
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 构建资源
| 项 | 内容 |
|---|---|
| Kconfig | CONFIG_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_name | IRQ 号与注册名 |
lock / wait | 配置互斥锁、等帧条件变量 |
state | 设备状态机 |
format | 当前格式上下文(线长、帧长、UV 偏移等) |
frames / mem / frame_count | 帧描述符、帧数据池、帧数(3) |
free_list / usable_list | 空闲帧队列、已完成待取帧队列 |
transfer[2] | 当前交给 WDMA 的两帧 |
available_frame_count | usable_list 中的帧数 |
dma_index | 下一次 WDMA 完成对应的 transfer[] 槽位 |
wait_timeout | 最近一次 camera_wait_frame() 是否超时 |
drop_next_completed_frame | 下一次 WDMA 完成时是否丢弃该帧 |
frame_stats | frame_done_count / frame_error_count |
sensor_attr | 驱动声明的静态属性 |
sensor_path | DVP/MIPI/BT.656 的 VIC 输入配置 |
isp_pipeline | TOP/IPC/DMSC/WDMA 配置 |
sensor | 驱动操作表副本 |
is_registered | 是否已登记(跨 close() 保留) |
3.2 设备状态机
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 之前 |
OPENED | sensor 已上电,可开流 | 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 | 空闲,可提交给 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_format | sensor 总线格式 |
output_pixel_format | 写入帧缓冲的格式 |
output_yuv422_byte_order | 固定写 CAMERA_YUV422_BYTE_ORDER_YUYV(见 §6.7) |
input_type | DVP / MIPI / BT656 |
line_length | 每行字节数 |
wdma_stride | 等于 line_length |
frame_size | 有效帧字节数 |
frame_align_size | align_up(frame_size, 4096),也是帧池分块大小 |
uv_data_offset | NV12 的 Y 平面字节数;其余格式为 0 |
enable_chroma | WDMA 是否按双通道(含 UV/chroma 起始地址)提交 |
camera_sensor_path_config_t 保存按输入类型生成的 VIC 配置:
input_typeinput.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_offset | NV12 的 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);
流程:
校验
index < 2、sensor_ops != NULL、get_attr != NULL、stream_on != NULL、port_config != NULL;不满足返回IM_ERR_INVALID_PARAM。该
index已登记则返回IM_ERR_ALREADY_REGISTERED(不是致命错误, 示例把它当成功处理)。若
sensor_ops->init != NULL,调用一次init(sensor_ops->user_ctx, port_config);失败则原样返回该错误。把操作表按值拷贝到静态设备,绑定
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() 之前;不能在出流时调用。
流程:
未初始化 →
IM_ERR_NOT_READY;isp == NULL→IM_ERR_UNSUPPORTED。出流中 →
IM_ERR_BUSY。请求格式与当前格式相同 → 直接
IM_OK。若 IRQ 已注册则释放 IRQ;释放旧帧缓冲。
重新生成配置并分配帧缓冲。
重新注册 IRQ。
任一步失败则释放缓冲并返回错误;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);
前置条件:设备已登记。
流程:
- 已
OPENED/STREAMING→ 返回IM_OK。 - 处于
INITIALIZED但尚未打开 → 返回IM_ERR_BUSY(只在内部瞬时出现)。 - 调用
hal_isp_clock_enable():- 选父时钟
SCLK_A,频率设为100 MHz,再使能CGU_ISPM。 - 访问 ISP 寄存器前必须先使能该时钟;关闭时 ISP 寄存器读回常数、写全部丢弃。
- 选父时钟
camera_init():初始化互斥锁、条件变量、两条帧队列,状态置INITIALIZED。camera_prepare_sensor_attr():调用驱动的get_attr()并校验(见 §9.4)。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。
- 再次校验属性与缓冲配置。
- 调用驱动
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。
流程(持设备锁):
- 已在出流 →
IM_OK;未打开 →IM_ERR_NOT_READY。 camera_reset_lists():三帧全部回到空闲队列,计数器/统计清零。camera_configure_isp_pipeline():- TOP:宽高、输入数据类型、Bayer 相位、
bypass_dmsc、statistics_position; - IPC:HAL 默认配置。
- 不调用
hal_isp_dmsc_configure()。
- TOP:宽高、输入数据类型、Bayer 相位、
camera_configure_resp():使能并向 CPU 上报GPL_WDMA_DONE与AE_STATIC_DONE,使能前先清全部。camera_configure_vic_input():按 DVP/MIPI/BT656 配置 VIC 输入。- MIPI 的
total_width小于输出宽时抬到输出宽。
- MIPI 的
hal_isp_wdma_reset()。camera_apply_saved_wdma_config():把静态 WDMA 配置写入 HAL。camera_prime_wdma():从空闲队列取两帧提交给 WDMA。- 按输入类型启动:
- 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()不会被调用。
- DVP:先
- 状态置
STREAMING,然后sys_irq_enable()。
失败处理:
- 已启动的输入会按上述顺序回滚;
- 若状态已置
STREAMING则退回OPENED; - WDMA 复位、帧队列清空;
- 返回错误码。
注意:每次 camera_start() 都会清零 frame_stats,所以统计是「本次开流以来」
的计数。
4.6 camera_stop()
error_code_t camera_stop(camera_device_t *dev);
流程(持设备锁):
- 未初始化 →
IM_ERR_NOT_READY。 - 若在出流:
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():帧全部回空闲队列,统计清零;- 广播条件变量,唤醒正在等帧的线程。
- 帧缓冲保留,供下次
camera_start()复用;IRQ 保持注册但已禁用。
4.7 camera_close()
void camera_close(camera_device_t *dev);
流程:
dev == NULL或未初始化 → 直接返回。- 若在出流:禁用 IRQ、复位 WDMA、执行 DVP/MIPI 停止路径,状态退回
OPENED。 - 若
state >= READY:释放 IRQ。 - 若
state >= OPENED且驱动有power_off:调用power_off()。 - 释放帧数据与帧描述符。
hal_isp_clock_disable()。- 清零
sensor_attr,状态置UNINITIALIZED,销毁互斥锁。
注意:
- 不调用驱动的
deinit()(当前没有注销流程)。 is_registered保留,可以再次camera_open();但驱动的init()不会重跑。- 若
power_on()失败,状态不会进入OPENED,因此camera_close()不会 调用power_off()。
5. VIC MID 数据通路与格式矩阵
5.1 通路总览
DVP
MIPI
BT.656
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 不能作为输入格式。
| 输入格式 | 允许的输出格式 | 结果 |
|---|---|---|
YUV422 | YUV422 | 原样搬运,DMSC 旁路 |
YUV422 | NV12 | 取 DMSC 旁路后的 YUV 输出,按 YUV420 双通道写入 |
RAW8 | RAW8 | 原样搬运,DMSC 旁路 |
RAW8 | NV12 | DMSC 在通路上做 RAW→YUV420 |
RAW10 | RAW10 | 原样搬运,DMSC 旁路 |
RAW10 | NV12 | DMSC 在通路上做 RAW→YUV420 |
RAW12 | RAW12 | 原样搬运,DMSC 旁路 |
RAW12 | NV12 | DMSC 在通路上做 RAW→YUV420 |
GREY | GREY | 原样搬运,DMSC 旁路 |
| 其他组合 | — | IM_ERR_UNSUPPORTED |
组合在 camera_set_format()(以及 camera_open() 的默认格式路径)里检查;
不合法时不会分配新缓冲。
5.3 WDMA 选择
camera_prepare_wdma_config() 根据「输入格式 + 输出格式」选择:
| 输入 → 输出 | mux_sel | dump_data_type | dump_data_order | enable_chroma |
|---|---|---|---|---|
| YUV422 → YUV422 | YUV422_PATH | YUV422_DUAL | 1 | true |
| YUV422 → NV12 | DMSC_YUV_PATH | YUV420_DUAL | 0 | true |
| RAW8 → RAW8 | RAW_PATH | RAW8_OR_Y | 0 | false |
| RAW8 → NV12 | DMSC_YUV_PATH | YUV420_DUAL | 1 | true |
| RAW10 → RAW10 | RAW_PATH | RAW10_12_16_OR_UV | 0 | false |
| RAW10 → NV12 | DMSC_YUV_PATH | YUV420_DUAL | 1 | true |
| RAW12 → RAW12 | RAW_PATH | RAW10_12_16_OR_UV | 0 | false |
| RAW12 → NV12 | DMSC_YUV_PATH | YUV420_DUAL | 1 | true |
| GREY → GREY | RAW_PATH | RAW8_OR_Y | 0 | false |
补充:
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_dmsc | statistics_position |
|---|---|---|
| YUV422 → YUV422 | 1 | 0 |
| YUV422 → NV12 | 1 | 0 |
| RAW → RAW | 1 | 1 |
| RAW → NV12 | 0 | 1 |
| GREY → GREY | 1 | 0 |
关键点:
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_length | frame_size | uv_data_offset |
|---|---|---|---|
| RAW8 / GREY | align_up(width, 8) | line * height | 0 |
| RAW10 / RAW12 | align_up(width, 8) * 2 | line * height | 0 |
| YUV422 | align_up(width, 8) * 2 | line * height | 0 |
| NV12 | align_up(width, 16) | line * height * 3 / 2 | line * 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_length | frame_size | frame_align_size | uv_data_offset |
|---|---|---|---|---|
| YUV422 | 1280 | 614400 | 614400 | 0 |
| NV12 | 640 | 460800 | 462848 | 307200 |
| RAW10 | 1280 | 614400 | 614400 | 0 |
5.6 Sensor 属性到 HAL 配置的映射
5.6.1 DVP
camera_dvp_input_config_t | 去向 | 说明 |
|---|---|---|
timing | timing_mode | HREF / HSYNC / SONY |
hsync_polarity / vsync_polarity | 同名极性字段 | 高有效/低有效 |
sample_edge | sample_edge | 上升沿/下降沿 |
宽高取 format.width/height;data_format 由输入格式映射:
RAW10/RAW12 原样,YUV422/NV12→YUV422_8BIT,其余→RAW8。
5.6.2 MIPI
camera_mipi_input_config_t | 去向 |
|---|---|
lane_count | CSI lane_count |
lane_rate_hz | D-PHY lane_rate_hz |
total_width | VIC total_width(0 时取输出宽;小于输出宽时抬到输出宽) |
hcrop_start_pixel | VIC hcrop_start_pixel |
vcrop_start_line | VIC vcrop_start_line |
start_delay | VIC start_delay |
start_channel | VIC start_channel(直接按枚举强转) |
enable_data_type_check | VIC 同名 |
enable_horizontal_compare / enable_vertical_compare | VIC 同名 |
中间件固定的 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);
流程:
dev == NULL或未在出流 → 返回NULL。camera_dequeue_frame():先从
usable_list取一帧;取不到且
timeout_ms != 0时在条件变量上等待;超时:置
wait_timeout = true,返回IM_ERR_TIMEOUT;超时后仍无帧,或非阻塞轮询无帧:返回
IM_ERR_NOT_READY。成功:帧状态置
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 != 0 | IM_ERR_INVALID_PARAM |
input_pixel_format ∈ {RAW8, RAW10, RAW12, GREY, YUV422} | IM_ERR_INVALID_PARAM |
bayer_pattern <= GBRG | IM_ERR_INVALID_PARAM |
input.type ∈ {DVP, MIPI, BT656} | IM_ERR_INVALID_PARAM |
MIPI:lane_count != 0 && lane_rate_hz != 0 | IM_ERR_INVALID_PARAM |
YUV422 输入:input_yuv422_byte_order <= VYUY | IM_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–D7 | PA2–PA9 | 功能 3 | DVP 数据 |
| CIM1_PCLK | PA11 | 功能 3 | DVP 像素时钟 |
| CIM1_HSYNC / VSYNC | PA12 / PA13 | 功能 3 | DVP 行/场同步 |
| CIM1_MCLK | PA10 | 功能 3 | CIM1 的 MCLK 输出,24 MHz 给 DVP 接口的 sensor |
| CIM1_RST | PA0 | GPIO 输出 | DVP 接口复位,低有效 |
| CIM1_PWDN | PA1 | GPIO 输出 | DVP 接口掉电/待机,高有效 |
| CIM0_MCLK | PB20 | 功能 0 | CIM0 的 MCLK 输出,24 MHz 给 MIPI 接口的 sensor |
| CIM0_PWDN | PB19 | GPIO 输出 | MIPI 接口的 XSHUTDN,高有效 |
| I2C1_SDA / SCK | PB24 / PB25 | 功能 0 | 两路接口共用的控制总线 |
对应到两条例程:
| DVP / GC0308 | MIPI / SC035HGS | |
|---|---|---|
| 采集入口 | CIM1 = index 1 | CIM0(MIPI → CSI)= index 0 |
| sensor 总线 | DVP,YUV422 | MIPI CSI-2,1 lane / 170 MHz |
| I2C 地址 | 0x21 | 0x30 |
| 复位 / 待机 | PA0 / PA1 | 同一个 PB19(sensor 只有 XSHUTDN 一个控制脚) |
| 供电 | 电阻常通,无使能脚 | 电阻常通,无使能脚 |
| MCLK | CIM1,24 MHz | CIM0,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 需要提供的三样东西
pinmux:在
board_pinmux.c的信号表里把 CIM 数据脚/时钟、I2C 引脚、GPIO 控制脚配好。表在板级初始化时统一应用,驱动里不要再改 pinmux。I2C 总线与设备:注册总线和每颗 sensor 的从设备句柄。总线是全局资源, 用「是否已注册」保护;设备句柄缓存起来复用。
端口配置函数:
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 换一块板子时的检查清单
sensor 接在 CIM0 还是 CIM1?走 MIPI 还是 DVP?这两点一起确定
index。数据脚、PCLK、HSYNC/VSYNC 的 pinmux 是否配成了 CIM 功能?
I2C 用哪一组?
input_clock_hz、bitrate_hz是否与实际时钟一致?地址是 7 位还是 8 位(中间件按 7 位)?复位、待机脚是哪个 GPIO、有效电平是什么?没有的脚填
IM_GPIO_PORT_INVALID。MCLK 是 SoC 给还是板上晶振给?SoC 给的话是哪一路 CIM、多少 Hz?
电源是否常通?需要上电时序的话,把延时/拉脚放在
configure()或驱动的power_on()里,不要放在应用里。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:
常量与寄存器表:表元素用
{ 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))。驱动上下文:保存
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或空实现即可。I2C 小工具:
write_reg()/read_reg()/write_array()/read_array(),全部走config.i2c_device,不要直接碰总线句柄。chip id 也 做成一张read_array()表。get_attr():填camera_sensor_attr_t,字段见下表。power_on()/power_off():按camera_sensor_mclk_enable()→ 电源使能 → 复位/待机时序 → 必要延时 → 写初始化表 的顺序;power_off()反过来,最后camera_sensor_mclk_disable()。MCLK 没起来之前不要访问寄存器(§9.3)。stream_on()/stream_off():只写开流/停流那一两笔寄存器;初始化表 不要放在这里,每次power_on()都会重写一遍。可选接口:
set_fps/get_fps、set_gain/get_gain、set_exposure/get_exposure、read_register/write_register。不实现就留NULL,中间件返回IM_ERR_NOT_READY(后两个中间件不调用,只供应用调试)。操作表与导出:
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 / height | sensor 实际输出的分辨率 | 填了 binning 之前的分辨率 |
input_pixel_format | sensor 在本板上实际出的格式 | 初始化表配成 RAW8,属性写 RAW10 |
output_pixel_format | camera_open() 的默认输出格式 | 与驱动默认值不一致 |
input_yuv422_byte_order | YUV422 输入的字节序(仅 YUV422 时有效) | 输出偏色、红蓝对调 |
input.type | DVP / MIPI / BT656 | 板上是 MIPI 却写 DVP |
input.bus.dvp.timing 等 | DVP 的 H/V 时序、采样沿、同步极性 | 画面错位、行首撕裂 |
input.bus.mipi.lane_count/lane_rate_hz | MIPI 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.h | board_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:
文件顶部加静态资源表,地址、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, };I2C 沿用文件里的
board_camera_i2c_register()/board_camera_i2c_device_register():总线是全局资源,注册一次、 「已注册就直接返回」;每颗 sensor 在总线上挂一个从设备句柄并缓存起来复用。input_clock_hz填控制器实际输入时钟,bitrate_hz按 sensor 能接受的速率填。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 调试顺序
出不了图时按这个顺序查,能少走弯路:
camera_open()返回 0 吗?失败先看 §13 的错误码表。读 chip id 对不对?不对说明 I2C 地址/总线/MCLK 有问题,与 ISP 无关;可用 驱动的
read_register()或 chip id 表核对。stream_on()后frame_done_count增长吗?不增长说明 VIC 没收到数据: 先确认 sensor 真的在输出(示波器看 PCLK/HSYNC/VSYNC 或 MIPI 时钟),再查 DVP 时序参数、MIPI lane 配置。有帧但全黑/颜色不对:查
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() 的默认值:
| 字段 | 默认值 |
|---|---|
instance | IM_CAMERA_AE_INSTANCE_0 |
sensor | 全 0(无回调) |
statistics_buffer_count | 4 |
histogram_buffer_count | 4 |
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 |
初始化步骤:
清零模块并保存配置;
分配统计/直方图 DMA 缓冲(任一失败时释放已分配部分);
写物理地址表(未使用的槽位写 0);
状态置
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():
读 HAL DMA 状态;
若
ae_frame_number与上次相同,直接返回IM_OK;按缓冲索引
ae_current_base定位统计缓冲,invalidate 后解析分区统计;按缓冲索引
hist_current_base定位直方图缓冲,invalidate 后解析 256 bin;读特定区域寄存器结果;
发布
ae_statistics.ae_frame_number,更新last_ae_frame_number;若注册了统计回调,在中断上下文调用回调。
整份 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/。
| 步骤 | 调用 | 硬件状态 / 说明 | 详见 |
|---|---|---|---|
| 1 | gc0308_dvp_get_sensor_ops() / board_gc0308_configure() | 拿到驱动操作表和板级接线(I2C 句柄、GPIO、MCLK)。不碰硬件 | §9.1、§9.5 |
| 2 | camera_register_device(index, &ops, &port) | 只登记:驱动的 init() 把接线存起来。已登记过返回 IM_ERR_ALREADY_REGISTERED,可视为成功 | §5.1 |
| 3 | camera_detect(index) | 按号取设备句柄,不探测硬件。所以必须先 register 再 detect | §5.2 |
| 4 | camera_open(camera) | 打开 ISP 模块时钟;sensor 上电、复位、发初始化表;按默认输出格式配链路并分配帧缓冲。真正的硬件探测在这里 | §5.3 |
| 5 | camera_set_format(camera, fmt) | 改输出格式并重新分配帧缓冲;必须在 open 之后、start 之前 | §5.4、§6.2 |
| 6 | camera_get_status() | 打印实际生效的几何与帧池大小,对上表(§12.4)说明配置无误 | §4.5 |
| 7 | camera_start(camera) → 取帧循环 → camera_stop() / camera_close() | 从 start 返回开始,帧就在往内存里落 | §5.5、§7.2、§5.6、§5.7 |
取帧循环里只有三条规则,其余都是这段循环的变体:
camera_wait_frame()返回的指针属于中间件。 用完必须camera_put_frame(),否则帧池(3 帧)很快被占满,之后只能空取。返回
NULL不等于链路坏了。 帧池占满、上一轮排队的帧刚被计数掉,都会 导致若干次空取,接着又能取到。用「连续空取次数」判断,并给它一个上限 (示例是 10 次 × 1 s);单次空取只累计、不退出。对帧的所有处理都必须在
camera_put_frame()之前做完。 归还之后 WDMA 随时可能重写这块内存。
失败路径上不要调用 camera_close():ISP 模块时钟是两颗 ISP 共用的,开关没有
引用计数,camera_open() 失败时它已经自行收尾,再 close 会把另一路也关掉
(§5.3)。