API GUIDE / VIC / OVERVIEW
G32S10 VIC API 概述
模块概述:本文档说明 G32S10 视频输入控制器(VIC)与配套 ISP 前端的 HAL/LL 接口,以及如何从零 把一路相机接到 DDR。
1. 文档范围
本文档说明 G32S10 视频输入控制器(VIC)与配套 ISP 前端的 HAL/LL 接口,以及如何从零
把一路相机接到 DDR。读者对象是需要在 camera_*() 中间件之下直接操作硬件、或者需要
先做寄存器级验证的驱动/应用工程师。
文档覆盖:
分层关系、实例与资源模型、编译开关
HAL 接口:默认值填充、参数检查、多步寄存器编程顺序、错误码
LL 接口:单字段读写与单比特操作
寄存器映射与位域约定
典型用法:从时钟、复位、TOP/IPC、VIC 输入、DMSC、WDMA、RESP 到 AE 的完整顺序
真实代码走读:中间件与测试用例里现有的 HAL/LL 用法
集成、调试与常见问题
本文档基于以下实现编写:
| 类别 | 文件 | 说明 |
|---|---|---|
| HAL 头文件 | hal_vic.h | VIC 输入前端(DVP / BT656 / MIPI CSI)与 sensor MCLK |
hal_isp_clock.h | ISP 模块时钟 | |
hal_isp_top.h | ISP TOP(帧尺寸、数据类型、DMSC 旁路、统计开关) | |
hal_isp_ipc.h | IPC 帧输出控制与输出偏移 | |
hal_isp_resp.h | RESP 事件、中断与断点位置 | |
hal_isp_wdma.h | GPL_WDMA 写帧、地址 FIFO 与完成状态 | |
hal_isp_dmsc.h | DMSC 去马赛克与颜色矩阵 | |
hal_isp_ae.h | AE 分区统计、直方图与 DMA | |
| LL 头文件 | ll_vic.h、ll_isp_top.h、ll_isp_ipc.h、ll_isp_resp.h、ll_isp_wdma.h、ll_isp_dmsc.h、ll_isp_ae.h | 每个子模块一份,全部为 static inline |
| 寄存器头文件 | isp_regs.h、vic_regs.h、isp_top_regs.h、isp_ipc_regs.h、isp_resp_regs.h、isp_wdma_regs.h、isp_dmsc_regs.h、isp_ae_regs.h | 结构体布局、字段掩码与实例宏 |
| 实现文件 | hal_vic.c、hal_isp_clock.c、hal_isp_top.c、hal_isp_ipc.c、hal_isp_resp.c、hal_isp_wdma.c、hal_isp_dmsc.c、hal_isp_ae.c | code/drivers/g32s10/vic/ |
| 构建 | code/drivers/g32s10/vic/Kconfig、CMakeLists.txt | CONFIG_DRIVER_VIC |
补充说明:
一个 VIC 实例对应一整个 ISP 寄存器块。VIC 前端与 TOP/IPC/RESP/GPL_WDMA/DMSC/AE 是同一基地址下的不同子模块,不能把 VIC 当作独立外设单独寻址。
本文只描述软件接口,不替代芯片寄存器手册。寄存器字段与手册冲突时,以芯片寄存器 手册与 SDK 寄存器头文件的字段定义为准;两者的说明不一致时,以实际读回的行为为准。
上一层的
camera_*()接口、sensor 驱动接入与帧管理见middleware/vic_mid_api.md;本文只讲它下面这一层。仓库里没有 VIC/ISP 的 HAL/LL 示例程序(
code/examples/下没有对应目录)。 HAL/LL 目前只被相机中间件code/middleware/camera/和test/下的用例调用。 因此第 9 章的“示例解读”选的是三份真实用例与中间件源码,而不是仓库里的 example。文中代码片段的注释统一使用英文;为什么这样调用、哪些值不能照抄,写在各章正文里。
术语:
| 术语 | 含义 |
|---|---|
| 实例 | 一套完整的 ISP 寄存器块:ISP0(0x13350000)或 ISP1(0x13360000) |
| VIC | 视频输入前端,负责 DVP/BT656/MIPI CSI 的时序解码、裁剪与格式转换 |
| TOP | ISP 顶层,决定帧尺寸、数据类型、Bayer、统计位置与 DMSC 旁路 |
| IPC | 帧输出控制器,把 TOP 处理后的数据按通道送出 |
| DMSC | 去马赛克与颜色空间转换(RAW → YUV) |
| GPL_WDMA | 写 DMA,把 IPC/DMSC 的输出写进 DDR |
| RESP | 事件与中断汇总,记录 AE/DMSC/WDMA 的完成与错误 |
| AE | 自动曝光统计块,输出 15×15 分区统计与直方图(只有 ISP0 有) |
| CIM / CSI | DVP 的时钟/输入域(CIM0、CIM1)与 MIPI 接收控制器(CSI0) |
| MCLK | 送给 sensor 的主时钟,由 CIM0/CIM1 提供 |
| RAW | sensor 原始 Bayer 数据;G32S10 上常见 RAW8/RAW10/RAW12 |
| NV12 | Y 平面 + 交织 UV 平面(YUV420),WDMA 双平面输出格式 |
建议阅读顺序:
第一次使用:第 2 章 → 第 3 章 → 第 8 章,按第 8 章的顺序一步一步写代码。
查某个函数:第 5 章(HAL)或第 6 章(LL)。
查寄存器:第 7 章。
出问题时:第 11 章 → 第 12 章 → 第 13 章。
2. 分层关系与依赖
2.1 软件层次
wdma / dmsc / ae
| 层 | 职责 | 典型使用者 |
|---|---|---|
| 应用 / BSP / 中间件 | 选实例、准备帧缓冲、处理中断、决定格式 | 产品代码、im_camera_vic.c |
| HAL | 填默认值、校验参数、按顺序编程、超时与错误码 | 中间件、测试用例、产品代码 |
| LL | 单字段读写、单比特使能,不校验、不排序 | 调试探针、寄存器级用例、HAL 自身 |
| 寄存器 | 一个实例内按子模块偏移组织,VIC 在 0xD000 | — |
选择建议:
正常采集路径一律走 HAL。HAL 已经处理了“先复位、再写字段、最后
REG_EN”这类顺序, 以及参数范围检查。只在两种情况下用 LL:① 需要读/写 HAL 没有暴露的字段;② 调试时故意绕过 HAL 的 守卫,构造异常现场(例如把地址 FIFO 写满)。第 9 章的
vic_int与wdma_addr_fifo_test都是后一种。不要混用同一寄存器的 HAL 与 LL 写操作而不清楚后果:LL 不做
REG_EN提交, VIC 的配置要等hal_vic_apply_register_update()或下一次hal_vic_configure_*()。
2.2 头文件与包含关系
#include "drivers/g32s10/vic/hal_vic.h" #include "drivers/g32s10/vic/hal_isp_clock.h" #include "drivers/g32s10/vic/hal_isp_top.h" #include "drivers/g32s10/vic/hal_isp_ipc.h" #include "drivers/g32s10/vic/hal_isp_resp.h" #include "drivers/g32s10/vic/hal_isp_wdma.h" #include "drivers/g32s10/vic/hal_isp_dmsc.h" #include "drivers/g32s10/vic/hal_isp_ae.h"
每个 HAL 头文件都包含了自己的 LL 头文件和
isp_regs.h,因此ISP0/ISP1宏随 任意一个 HAL 头文件可见。MIPI 输入还需要 CSI/D-PHY 驱动(
hal_csi.h、hal_dphy_rx.h,或中间件的im_camera_vic封装)。hal_vic_configure_mipi()只配置 VIC 一侧, 不会启动 CSI 接收、也不会配置 D-PHY。调试用例里常见的
ll_*调用需要单独包含对应 LL 头文件。
3. 资源模型
3.1 子模块偏移
| 子模块 | 偏移 | 头文件 |
|---|---|---|
| TOP | 0x0000 | isp_top_regs.h |
| AE | 0x2000 | isp_ae_regs.h |
| IPC | 0x3000 | isp_ipc_regs.h |
| DMSC | 0x4000 | isp_dmsc_regs.h |
| GPL_WDMA | 0x9000 | isp_wdma_regs.h |
| RESP | 0xA000 | isp_resp_regs.h |
| VIC | 0xD000 | vic_regs.h |
isp_regs_t 就是按这些偏移拼起来的结构体,可以直接 ispx->vic.ctrl 这样访问;
偏移正确性由编译期断言保证(isp_regs.h)。
3.2 能力与约束
| 项目 | 说明 |
|---|---|
| 输入接口 | DVP(硬件值 3)、BT656(0)、MIPI CSI(2) |
| 扫描方式 | 仅逐行(progressive);BT656 起始场可配,DVP 固定奇数场 |
| MIPI 通道 | 硬件支持 0–3,G32S10 正常使用通道 0 |
| MIPI 帧模式 | 线性(LINEAR),其余值仅为字段透传 |
| MIPI 像素格式 | RAW8/RAW10/RAW12/YUV422 |
| DVP 数据格式 | RAW8/RAW10/RAW12/YUV422_16BIT/YUV422_8BIT |
| TOP 帧尺寸 | 640×360 ~ 1920×1080,宽高均 4 对齐 |
| 统计位置 | 0 = IPC,1 = DMSC,2~7 保留 |
| WDMA 数据源 | mux_sel 1 = RAW 通路,2 = YUV422 通路,3 = DMSC YUV 通路 |
| WDMA 地址 FIFO | 深度 4,满时 HAL 自动复位并重配静态参数 |
| DMSC 输入 | 当前实现按 RAW10 输入使用;RAW8 输入没有完整软件通路 |
| AE 分区 | 最多 15×15,每方向 15 个尺寸表项 |
| ISP 时钟 | 两个 ISP 实例共用一个 CGU_ISPM,无独立门控位 |
| AE 实例 | 只有 ISP0 有 AE;ISP1 没有 |
| 中断线 | VIC 与 RESP 共用 IRQ_ISP0/IRQ_ISP1,一个 ISR 服务两者 |
4. 典型用法:从零开始
本章按“从一块刚上电的板子到第一帧数据落进 DDR”的顺序,把 HAL 调用串起来。每一步都 给出目标、代码和容易踩的坑。如果你只是想改中间件的行为,可以只读 8.1 和 8.12。
4.1 第二步:复位到已知状态
目标:清掉上一次运行留下的配置,尤其是 REG_EN。
#include "drivers/g32s10/vic/hal_vic.h"
error_code_t vic_reset_all(void)
{
/* 0 means the HAL default timeout (1000 ms). */
return hal_vic_reset(ISP0, 0U);
}补充说明:
hal_vic_reset()写CTRL.RST(bit2)并轮询CTRL == 0,复位完成后REG_EN与START都是 0。CTRL.RST自清,不需要手动写 0。timeout_ms = 0表示使用默认 1000 ms,不是“无限等待”。超时返回
IM_ERR_TIMEOUT。常见原因是 ISP 时钟没开、CIM 门控没开,或者 上一次复位还卡着。TOP 还有自己的复位:
hal_isp_top_reset()触发FUN_RST(bit2),只复位各模块的计算部分,不复位配置参数。FUN_RST不会清STATIC_EN的回读值,所以不要用 “STATIC_EN变 0”判断它发生过;要判断 TOP 是否被复位,看流是否中断 (frame_done_count停住)。尽管
FUN_RST不清STATIC_EN,仍建议把 AE/直方图使能放在启动之后——这是中间件 采用的顺序,见 8.4。顺序上建议:先
hal_isp_top_reset(),再hal_vic_reset(),最后配置各模块。hal_isp_top_configure()内部还会再触发一次FUN_RST,重复调用是安全的。
4.2 第三步:配置 TOP
目标:告诉 ISP 这一帧有多大、什么格式、要不要走 DMSC、统计从哪里取。
#include "drivers/g32s10/vic/hal_isp_top.h"
error_code_t top_setup(bool output_nv12)
{
hal_isp_top_config_t top;
hal_isp_top_struct_init(&top);
top.ispx = ISP0;
top.width = 640U;
top.height = 480U;
top.data_type = HAL_ISP_TOP_DATA_TYPE_RAW10;
top.bayer_type = 1U;
/* NV12 output requires DMSC; RAW/YUV passthrough must bypass it. */
top.bypass_dmsc = !output_nv12;
/* Statistics are taken after DMSC for RAW input. */
top.statistics_position = output_nv12 ? 1U : 0U;
return hal_isp_top_configure(&top);
}补充说明:
帧尺寸必须在
640×360~1920×1080之间且宽高 4 对齐;否则返回IM_ERR_INVALID_PARAM,不会写寄存器。小分辨率(例如 512×256)在这版硬件上 无法通过 TOP,见第 13 章。bypass_dmsc的判据:输出不是 NV12,或输入不是 RAW,就要旁路。旁路之后 WDMA 不能再选mux_sel = 3。statistics_position:中间件对 RAW 输入用 1(DMSC 之后),对 YUV 输入用 0。 填错统计会取错位置,AE 数值看起来“不对但也不报错”。测试图使能位在硬件上是反相的,HAL 已经处理;不要用 LL 直接写这个位。
hal_isp_top_configure()会先触发FUN_RST(只复位计算部分)。推荐把 AE/直方图 使能放在启动之后:TOP → VIC → 启动 →hal_isp_top_enable_ae_statistics()。FUN_RST并不清STATIC_EN,所以这不是硬性依赖,但这是中间件采用的顺序。ipc_line_space/normal_line_space保持 0 即可;只有多路复用同一套行缓冲时 才需要改。
4.3 第五步:配置 VIC 输入
目标:解码 sensor 的时序,把有效像素交给 TOP。
4.3.1 DVP
#include "drivers/g32s10/vic/hal_vic.h"
error_code_t dvp_setup(uint32_t mclk_hz)
{
hal_vic_dvp_config_t dvp;
/* CIM1 drives the MCLK of the connector we are using. */
(void)hal_vic_camera_enable_sensor_mclk(1U, mclk_hz);
hal_vic_dvp_struct_init(&dvp);
dvp.ispx = ISP1;
dvp.width = 640U;
dvp.height = 480U;
dvp.data_format = HAL_VIC_DVP_DATA_FORMAT_RAW8;
dvp.timing_mode = HAL_VIC_DVP_TIMING_MODE_HREF;
dvp.sample_edge = HAL_VIC_DVP_SAMPLE_RISING_EDGE;
/* Keep LOW for DVP RAW: HIGH scales the image brightness by 16x down. */
dvp.raw_align = HAL_VIC_RAW_ALIGN_LOW;
dvp.enable_image_check = true;
/* Mask everything except FRAME_DONE (hardware: 1 = masked). */
dvp.common.interrupt_mask =
(uint32_t)HAL_VIC_IRQ_ALL & ~(uint32_t)HAL_VIC_IRQ_FRAME_DONE;
return hal_vic_configure_dvp(&dvp);
}补充说明:
width/height是输出尺寸,不是 sensor 的 HTS/VTS。DVP 的 HACT 由 HAL 计算,单位是像素时钟,不是字节:RAW8/RAW10/RAW12 用
width,YUV422 8-bit 用width × 2(一个像素时钟两个字节),YUV422 16-bit 用width。如果把 RAW 也按字节算成width × 2,VIC 会等 2 倍于实际像素的时钟, 每帧都报错并被上层丢弃,日志表现为done == err、画面整体偏绿。raw_align用LOW。HIGH在 RAW8 输入下按高 4 位对齐截取,画面会明显变暗。timing_mode选错表现为整帧错位或完全无帧;HSYNC/SONY 模式才需要填消隐字段。interrupt_mask的语义是1 = 屏蔽,所以“只放行帧完成”要写成HAL_VIC_IRQ_ALL & ~HAL_VIC_IRQ_FRAME_DONE,而不是HAL_VIC_IRQ_FRAME_DONE。 写反是本模块最常见的错误:写HAL_VIC_IRQ_ALL等于全部屏蔽,一个中断都收不到。默认掩码是
FRAME_START | FRAME_RESET,含义同样是“只屏蔽这两位”,其余放行。
4.3.2 MIPI
#include "drivers/g32s10/vic/hal_vic.h"
error_code_t mipi_setup(uint32_t mclk_hz)
{
hal_vic_mipi_config_t mipi;
(void)hal_vic_camera_enable_sensor_mclk(0U, mclk_hz);
hal_vic_mipi_struct_init(&mipi);
mipi.ispx = ISP0;
mipi.output_width = 640U;
mipi.output_height = 480U;
mipi.total_width = 640U;
mipi.pixel_format = HAL_VIC_MIPI_PIXEL_FORMAT_RAW10;
mipi.data_type = HAL_VIC_MIPI_DATA_TYPE_RAW10;
mipi.start_channel = HAL_VIC_MIPI_START_CHANNEL_0;
mipi.sensor_frame_mode = HAL_VIC_MIPI_SENSOR_FRAME_MODE_LINEAR;
mipi.hcrop_start_pixel = 0U;
mipi.vcrop_start_line = 0U;
return hal_vic_configure_mipi(&mipi);
}补充说明:
hal_vic_configure_mipi()只配置 VIC 一侧。MIPI 的 CSI 接收控制器和 D-PHY 要另外启动(中间件里是camera_mipi_rx_start()),否则 VIC 永远等不到 数据包。total_width填 0 时 HAL 自动用output_width。裁剪必须满足hcrop_start_pixel ≤ total_width且total_width - hcrop_start_pixel ≥ output_width, 否则返回IM_ERR_INVALID_PARAM。硬件行宽字段是 4 字节字数:
WIDTH4 = ceil(total_width × bpp / 32),bpp 由pixel_format决定(RAW8=8、RAW10=10、RAW12=12、YUV422=16)。HAL 负责换算, 用 LL 直接写时要自己算。data_type必须和 sensor 实际发出的 CSI-2 DT 一致;enable_data_type_check打开后不一致会报错,但不会自动纠正。MIPI 的默认中断掩码与 DVP 不同:帧完成和错误位是放行的,帧开始/帧复位反而被 屏蔽。需要帧开始通知时要显式改掩码。
4.3.3 BT656
BT656 的调用形式与 DVP 相同,只是用 hal_vic_bt656_struct_init() /hal_vic_configure_bt656(),并且要指定总线宽度(8/16 bit)与 SAV/EAV 顺序:
hal_vic_bt656_config_t bt; hal_vic_bt656_struct_init(&bt); bt.ispx = ISP0; bt.width = 720U; bt.height = 576U; bt.bus_width = HAL_VIC_BT656_BUS_WIDTH_8BIT; bt.sync_order = HAL_VIC_BT656_SYNC_EAV_BEFORE_SAV; bt.start_field = HAL_VIC_FIELD_ODD; (void)hal_vic_configure_bt656(&bt);
HACT 同样是按总线宽度换算的:16-bit 总线是 width,8-bit 总线是 width × 2。
4.4 第七步:配置 WDMA 并逐帧提交
目标:把输出写进 DDR,并保持地址 FIFO 里始终有下一帧的地址。
4.4.1 )计算步长
先按输出格式确定每行字节数(与中间件 camera_default_line_length() 一致):
| 输出格式 | 步长 |
|---|---|
| NV12(YUV420 双平面) | ALIGN_UP(width, 16),UV 步长相同 |
| YUV422 | ALIGN_UP(width, 8) × 2 |
| RAW8 / 灰度 | ALIGN_UP(width, 8) |
| RAW10 / RAW12 | ALIGN_UP(width, 8) × 2 |
NV12 一帧大小 = stride × height × 3 / 2(Y 平面 + 半个 Y 平面)。
4.4.2 )配置静态参数
#include "drivers/g32s10/vic/hal_isp_wdma.h"
static hal_isp_wdma_static_config_t s_wdma_static;
error_code_t wdma_setup(uint32_t width, uint32_t height, bool nv12)
{
hal_isp_wdma_config_t wdma;
uint32_t stride;
hal_isp_wdma_struct_init(&wdma);
wdma.ispx = ISP0;
wdma.static_config.mux_sel = nv12 ? HAL_ISP_WDMA_MUX_SEL_DMSC_YUV_PATH
: HAL_ISP_WDMA_MUX_SEL_RAW_PATH;
/* Pick the layout that matches the data: RAW8 uses RAW8_OR_Y,
* RAW10/RAW12 use RAW10_12_16_OR_UV, YUV422 uses *_DUAL. */
wdma.static_config.dump_data_type =
nv12 ? HAL_ISP_WDMA_DUMP_DATA_TYPE_YUV420_DUAL
: HAL_ISP_WDMA_DUMP_DATA_TYPE_RAW8_OR_Y;
stride = nv12 ? ((width + 15U) & ~15U) : ((width + 7U) & ~7U);
wdma.static_config.stride = stride;
wdma.static_config.chroma_stride = nv12 ? stride : 0U;
/* Save it: the HAL needs it again when it recovers a full address FIFO. */
s_wdma_static = wdma.static_config;
return hal_isp_wdma_configure(&wdma);
}补充说明:
mux_sel必须与 TOP 的旁路选择一致:旁路 DMSC 时选 1(RAW)或 2(YUV422), 不旁路时选 3。dump_data_type必须与数据本身一致:RAW8 用RAW8_OR_Y,RAW10/RAW12 用RAW10_12_16_OR_UV,NV12 用YUV420_DUAL,YUV422 用YUV422_DUAL。 “RAW 通路”不等于“用RAW8_OR_Y”。步长和地址都必须 8 字节对齐,地址上限
0x1FFFFFF8,步长上限0xFFF8。静态配置被
hal_isp_wdma_reset()清掉(软复位清静态参数),所以每次 reset 之后 都要重新hal_isp_wdma_configure()。HAL 在地址 FIFO 满时会自动 reset 并重配, 重配用的就是你传进来的static_config。地址 FIFO 深度是 4。启动前要把 4 个槽位都填上地址,否则 WDMA 写完一帧就停。
4.4.3 )逐帧提交
static hal_isp_wdma_frame_config_t s_wdma_frame;
void wdma_frame_init(void)
{
hal_isp_wdma_frame_struct_init(&s_wdma_frame);
s_wdma_frame.ispx = ISP0;
}
error_code_t wdma_submit(uint32_t y_addr, uint32_t uv_addr)
{
s_wdma_frame.start_address = y_addr;
s_wdma_frame.chroma_start_address = uv_addr;
s_wdma_frame.enable_chroma = (uv_addr != 0U);
return hal_isp_wdma_start_frame(&s_wdma_frame, &s_wdma_static);
}补充说明:
HAL 先写
C_START_ADDR(若enable_chroma),再写START_ADDR。顺序反了会把 UV 地址当成下一帧的 Y 地址。每收到一次
GPL_WDMA_DONE,就提交一块新缓冲,保持 FIFO 不空。FIFO 满时
hal_isp_wdma_start_frame()会打印WDMA address FIFO full警告、 自动复位并重配静态参数。返回IM_OK不代表“这次地址被接受了”,要结合警告看。hal_isp_wdma_read_done()返回的done_address在两次运行之间不会复位, 非 0 不代表这帧真的写完了;判断“这帧有没有真正写进内存”要看地址是否落在自己的帧环里。YUV422 输出时 DDR 里的实际字节序是 YVYU,不是 YUYV;软件解码要交换 U/V。
4.5 第十步:启动、取帧与停流
启动顺序按输入接口不同:
DVP
sensor_stream_on(); /* vendor sensor driver */ (void)hal_vic_start(ISP1); /* VIC starts after the sensor is sending data */
MIPI
camera_mipi_rx_start(); /* CSI + D-PHY, not part of hal_vic */ (void)hal_vic_start(ISP0); sensor_stream_on(); irq_enable(IRQ_ISP0);
取帧循环(中断版):
void isp_irq_handler(void)
{
uint32_t vic_status = 0U;
hal_isp_resp_pending_t pending;
(void)hal_vic_read_interrupt_status(ISP0, &vic_status);
if (vic_status != 0U) {
(void)hal_vic_clear_interrupt_status(ISP0, vic_status);
}
if (hal_isp_resp_read_pending(ISP0, &pending) != IM_OK) {
return;
}
if (pending.common0.gpl_wdma_done) {
/* A frame is in DDR: hand it to the consumer and submit a new address. */
wdma_submit(next_y_addr(), next_uv_addr());
}
if (pending.has_pending) {
(void)hal_isp_resp_clear_pending(ISP0, &pending);
}
}停流顺序与启动相反:
irq_disable(IRQ_ISP0); (void)hal_vic_stop(ISP0); /* Stop the input first. */ sensor_stream_off(); (void)hal_isp_wdma_reset(ISP0); /* Clears static config, re-configure before restart */ (void)hal_isp_clock_disable();
补充说明:
重启必须走 stop/start。只写一次
START当“重启”会破坏 VIC 的几何配置, 表现为画面错位或尺寸错误;hal_vic_stop()后再hal_vic_start()才是干净路径。溢出之后总线放开也救不回出流。单路层面可以按 8.5 的对照表用
TRIGE或func_reset抢救;不能确定属于哪一档时,停流/启动是通用路径,并且要重新hal_isp_wdma_configure()。停流顺序里先关中断,避免在拆缓冲的时候 ISR 还在提交地址。
hal_isp_wdma_reset()清静态配置;如果后面还要继续用,重启前重配。
4.6 完整骨架
把前面十步按顺序放在一起,就是一条最小可用的采集链路:
error_code_t isp_capture_start(uint32_t mclk_hz)
{
error_code_t ret;
ret = hal_isp_clock_enable();
if (ret != IM_OK) {
return ret;
}
(void)hal_isp_top_reset(ISP0);
(void)hal_vic_reset(ISP0, 0U);
/* TOP: 640x480 RAW10 -> NV12 through DMSC. */
hal_isp_top_config_t top;
hal_isp_top_struct_init(&top);
top.ispx = ISP0;
top.width = 640U;
top.height = 480U;
top.data_type = HAL_ISP_TOP_DATA_TYPE_RAW10;
top.bypass_dmsc = false;
top.statistics_position = 1U;
if ((ret = hal_isp_top_configure(&top)) != IM_OK) {
goto err_clock;
}
hal_isp_ipc_config_t ipc;
hal_isp_ipc_struct_init(&ipc);
ipc.ispx = ISP0;
if ((ret = hal_isp_ipc_configure(&ipc)) != IM_OK) {
goto err_clock;
}
if ((ret = mipi_setup(mclk_hz)) != IM_OK) { /* VIC input */
goto err_clock;
}
if ((ret = wdma_setup(640U, 480U, true)) != IM_OK) {
goto err_clock;
}
if ((ret = resp_setup(false)) != IM_OK) {
goto err_clock;
}
/* Prime the 4-deep address FIFO before starting. */
for (uint32_t i = 0U; i < 4U; i++) {
if ((ret = wdma_submit(frame_y(i), frame_uv(i))) != IM_OK) {
goto err_clock;
}
}
camera_mipi_rx_start();
(void)hal_vic_start(ISP0);
sensor_stream_on();
irq_enable(IRQ_ISP0);
return IM_OK;
err_clock:
(void)hal_isp_clock_disable();
return ret;
}这里省略了 DMSC 配置(示例用的是复位默认值自带的 BT.601 通路)、AE 和错误回滚的细节。
真实产品代码要补上:DMSC 矩阵(如果不用复位默认值)、帧缓冲管理、错误路径上的hal_vic_stop() 与 hal_isp_clock_disable()。
5. 示例程序解读
5.1 用例一:wdma_addr_fifo_test
它回答什么问题:WDMA 的 4 深地址 FIFO 能不能被正确填满、软复位能不能清空、 驱动在 FIFO 满时能不能自愈,以及搬进 DDR 的字节是不是一张真图。
用例分成 A0~A3 与 A2b 几段,逐层收窄:
| 段 | 做什么 | 判据 |
|---|---|---|
| A0 | 确认调试寄存器地址(两个候选地址二选一) | 非空状态下两组读数能区分 |
| A3 | 写 2 个地址 → 软复位 → 回读 | 四套 FIFO 全部 empty=1 / data=0 / free=4 |
| A2 | 用 LL 直写把 FIFO 填满(4 个地址) | 第 4 次写后 full=1 / free=0 / data=4 |
| A2b | 在“满”的现场调用 HAL 提交地址 | HAL 自动复位 + 重配 + 写入,FIFO 只剩 1 个地址 |
| A1 | 真实出流,逐帧观察 FIFO 与帧内容 | FIFO 不溢出、DONE_ADDR 不跑出帧环、Y 不是常数场 |
A2:为什么要绕过 HAL 才能造出“满”
/* Fill the FIFO through LL: hal_isp_wdma_start_frame() would refuse or heal. */
static void push_address_via_ll(isp_regs_t *ispx, uint32_t y_addr, uint32_t c_addr)
{
ll_isp_wdma_set_chroma_start_address(ispx, c_addr);
ll_isp_wdma_set_start_address(ispx, y_addr);
}hal_isp_wdma_start_frame() 在 FIFO 满时会自动复位,所以正常调用永远造不出“满”的
现场。用例只能走 LL 直写。这也是“什么时候该用 LL”的标准答案:要观测硬件异常
行为,就必须绕过驱动的保护。
读 FIFO 状态用的是调试寄存器解码:
state->data_num = ll_isp_wdma_get_address_fifo_data_count_from_raw(raw); state->free_num = ll_isp_wdma_get_address_fifo_free_count_from_raw(raw); state->full = ll_isp_wdma_is_address_fifo_full_from_raw(raw); state->empty = ll_isp_wdma_is_address_fifo_empty_from_raw(raw); /* Keep the same three identities as the HAL decoder. */ state->valid = (state->data_num + state->free_num == 4U) && (state->full == (state->free_num == 0U)) && (state->empty == (state->data_num == 0U));
注意:HAL 里对应的解码函数是 static,不对外暴露;外部只能用这几个 LL 原语
自己解码。raw 编码见 7.5 节。
A2b:HAL 自愈路径
/* FIFO is full here. Expect a "WDMA address FIFO full" warning and IM_OK. */ ret = hal_isp_wdma_start_frame(&frame_config, &static_config);
三步判据缺一不可:
返回
IM_OK,日志里出现WDMA address FIFO full(证明真的判到了满);自愈后 Y/C 两套 FIFO 各只剩本次写入的 1 个地址(
data=1、full=0);回读
MUX_SEL/DUMP_DATA_TYPE/STRIDE/CHROMA_STRIDE,必须是刚才下发的那组 非默认值——SOFT_RESET会把静态配置一起清掉,重配漏了这里就会拿复位默认值 去写 DDR。
A1:三层判据
A1 是真实出流。它同时看三样东西:
地址 FIFO:
y_free_min、y_data_max、full事件次数——FIFO 不能被写满;完成地址:
done_out_of_ring == 0,即DONE_ADDR始终落在自己的帧环内;帧内容:Y 的 min/max/mean、U/V 均值、UV 极差,以及帧间签名——排除“DONE 每帧 都在走,但搬进 DDR 的是常量场”这种“检查全过、画面是假的”情况。
不要把“DONE_ADDR 每帧 +1”当作判据:中间件在没有空闲帧时会把同一块缓冲重复
提交给 WDMA,于是 DONE 事件在加、地址不动。只能判“地址是否跑出帧环”,不能判步进。
这条经验对任何基于 DONE_ADDR 的监控都适用。
可以直接抄走的三条做法:
起流前必须把地址 FIFO 清空(
hal_isp_wdma_reset()),否则上一轮残留的地址会在 起流瞬间被写进 DDR——这是内存被踩的经典成因;任何 FIFO 状态读数都要先检查自洽(
valid),不自洽时后续判据全部作废;寄存器检查全部通过不等于图像正确,至少加一条帧内容判据。
5.2 中间件 im_camera_vic.c 的 HAL 用法
中间件是“把这些接口串成产品”的参考答案。看三个函数就够:
camera_open():资源与窗口
hal_isp_clock_enable() -> hal_isp_top_configure()/ipc_configure()/vic_configure_*() -> sensor power_on() /* opens the shared CIM/MCLK gate */
ISP 时钟先开,寄存器窗口要等 sensor power_on() 打开 CIM 门控之后才可靠。这也是
为什么寄存器级的调试代码必须放在 camera_open() 之后。
camera_start():启动顺序
reset frame lists -> configure ISP pipeline (TOP + IPC) -> configure RESP -> configure VIC input -> hal_isp_wdma_reset() + hal_isp_wdma_configure() -> prime WDMA (submit CAMERA_WDMA_INFLIGHT_COUNT frames) -> DVP : sensor stream_on -> hal_vic_start() MIPI : camera_mipi_rx_start() -> hal_vic_start() -> sensor stream_on() -> irq_enable()
注意 MIPI 的 hal_vic_start() 在 sensor stream_on() 之前,DVP 在之后。
不是笔误:MIPI 需要先让 VIC 准备好收包,DVP 则需要 sensor 已经在发数据。
camera_irq_handler():先 VIC 后 RESP
read VIC INT_STA -> clear VIC INT_STA -> hal_isp_resp_read_pending() -> dispatch gpl_wdma_done / ae_static_done -> hal_isp_resp_clear_pending()
顺序不能反。VIC 是电平型,先清它才不会在处理 RESP 期间被反复打断。
配置侧的三个推导规则(im_camera_config.c):
| 规则 | 代码 |
|---|---|
| 输出 NV12 且输入是 RAW 才走 DMSC | bypass_dmsc = (output != NV12) || (input != RAW) |
| 统计位置 | RAW 输入取 1,YUV 输入取 0 |
| 行长度 | NV12 ALIGN_UP(w,16);YUV422 ALIGN_UP(w,8)*2;RAW8 ALIGN_UP(w,8);RAW10/12 ALIGN_UP(w,8)*2 |
另外,中间件从不调用 hal_isp_dmsc_configure(),DMSC 依赖复位默认值自带的 BT.601
通路。这意味着一旦你在自己的代码里写 DMSC 配置,就等于接管了颜色矩阵,必须填全。
6. 集成到你自己的工程
6.1 Kconfig
CONFIG_DRIVER_VIC=y # CONFIG_VIC_ENABLE_DEBUG_HELPERS is not set
CONFIG_DRIVER_VIC会select DRIVER_CPM,不需要另外打开 CPM。调试阶段可以打开
CONFIG_VIC_ENABLE_DEBUG_HELPERS,但它同时还要求CONFIG_SDK_ENABLE_DEBUG_HELPERS=y,两个都开*_regs_dump()才有实现。产品配置保持
CONFIG_SDK_HAL_PARAM_CHECK=y(默认),否则所有参数检查都会消失。
6.2 defconfig 与构建
以仓库里现成的配置为起点:
code/examples/middleware/usb/device/defconfigs/g32s10_v1_usb_gadget_uvc_camera_jpeg_defconfig
它已经打开了 CONFIG_DRIVER_VIC=y、CONFIG_DRIVER_CSI=y,调试辅助关闭。
构建三步(SDK_APP_DIR 必须传,否则 configure 只会编 code/,你的应用源文件
根本不在 compile_commands.json 里,构建会“静默成功”):
tools/imde/bin/imde use-defconfig <你的 app>/xxx_defconfig tools/imde/bin/imde -D SDK_APP_DIR=<你的 app> configure tools/imde/bin/imde build
注意 build/ 是共享目录:如果它被其他人或别的工程改过配置,烧录前把这三步重跑一遍。
另外 imde use-defconfig 会静默丢弃它不认识的 CONFIG_ 符号,新增 Kconfig
符号时先确认它真的出现在生成的 .config 里。
6.3 烧录与最小验证
烧录产物是:
build/artifacts/spl_with_zero.bin
上电后按这个顺序验证:
串口有启动日志;
hal_isp_clock_enable()返回IM_OK;读 TOP
version(ISPx + 0x0000)不是全 0/全 1;调
hal_vic_reset()返回IM_OK,回读VIC.CTRL为 0;打开调试辅助时,
hal_vic_regs_dump()/hal_isp_top_regs_dump()能打出 合理的值(见 11.2);起流后
hal_isp_wdma_read_done()的done_address落进你的帧环;抽一帧看 Y 的 min/max——不是常数场,才算真的出图。
7. 调试与验证
7.1 先确认寄存器窗口是活的
所有“寄存器读回一个怪值”的排查,第一步都是确认窗口可访问:
| 检查 | 方法 | 正常表现 |
|---|---|---|
| ISP 时钟 | hal_isp_clock_get_rate_hz() | 返回 100 MHz |
| 顶层版本 | 读 ISPx + 0x0000(TOP version) | 非 0、非 0xFFFFFFFF |
| VIC 版本 | 读 ISPx + 0xD000、+0xD004 | 能区分出两个不同的值 |
| 写读 | 写 VIC.INT_MASK 一个图案再读回 | 读回等于写入值 |
窗口没打开时,这些读回会全部相同(常见是全 0 或 0x1),而且写不进去。
原因通常是 ISP 时钟没开,或 CIM/MCLK 门控没开(camera_open() 之前)。
7.2 寄存器 dump
(void)hal_vic_regs_dump(ISP0); (void)hal_isp_top_regs_dump(ISP0); (void)hal_isp_ipc_regs_dump(ISP0); (void)hal_isp_resp_regs_dump(ISP0); (void)hal_isp_wdma_regs_dump(ISP0); (void)hal_isp_dmsc_regs_dump(ISP0); (void)hal_isp_ae_regs_dump(ISP0);
前提:CONFIG_VIC_ENABLE_DEBUG_HELPERS=y 且 CONFIG_SDK_ENABLE_DEBUG_HELPERS=y;
否则返回 IM_ERR_UNSUPPORTED。
输出格式:
VIC0[0x0d010] CTRL = 0x00000002 VIC0[0x0d1e0] INT_STA = 0x00000000
VIC0/VIC1 对应 ISP0/ISP1,方括号里是实例内偏移。带索引的寄存器
(分区尺寸、颜色表等)用 %s[0x%04x] %-20s[%u] = 0x%08x 的形式。
7.3 RESP 事件与断点位置
hal_isp_resp_pending_t pending;
if (hal_isp_resp_read_pending(ISP0, &pending) == IM_OK) {
/* Inspect pending.common0 / unusual0 / backup0. */
(void)hal_isp_resp_clear_pending(ISP0, &pending);
}
for (uint32_t i = 0; i < HAL_ISP_RESP_POS_COUNT; i++) {
hal_isp_resp_position_t pos;
(void)hal_isp_resp_read_position(ISP0, i, &pos);
}UNUSUAL0有事件(IPC_OVERFLOW/IPC_SIZE_ERROR/AE_ERROR)说明流水线已经异常, 不要只看WDMA_DONE在不在。BACKUP0.DMSC_FRAME_DONE可以用来确认 DMSC 在出帧。读清模式默认关闭,读完必须显式清。
7.4 一组可对照的读数
一次正常的 640×480 NV12 出流,静态配置大致是:
| 寄存器 | 期望 |
|---|---|
TOP.FRM_SIZE | 宽 640、高 480 |
TOP.BYPASS | DMSC 未旁路(走 NV12 时) |
VIC.INT_MASK | 只放行需要的位(如 FRAME_DONE) |
WDMA.MUX_SEL | 3(DMSC YUV 通路) |
WDMA.STRIDE | 640(ALIGN_UP(640,16)) |
WDMA.CHR_STRIDE | 640 |
AE.ZONE_NUM_START | 0xf000f000(15×15,起点 0) |
这些值任何一项对不上,先按第 12 章的表排查,再怀疑硬件。
8. 常见问题排查
| 现象 | 最可能的原因 | 怎么确认 / 怎么修 |
|---|---|---|
| 所有寄存器读回同一个常数 | ISP 时钟没开,或 CIM/MCLK 门控没开 | 见 11.1;把寄存器操作放到 camera_open() 之后 |
写 INT_MASK 后一个中断都没有 | 极值写反:写成了 HAL_VIC_IRQ_ALL | 要放行的位必须清 0:ALL & ~bit |
| MIPI 没有帧开始中断 | MIPI 默认掩码把 FRAME_START/FRAME_RESET 屏蔽了 | 显式改掩码 |
| 中断风暴 / CPU 反复进 ISR | INT_STA 没清,或 RESP 事件没清,或 MIPI bit8 常置 | ISR 先清 VIC 再处理 RESP;不要全放行 |
hal_vic_reset() 超时 | 时钟/门控没开,或 VIC 还卡着 | 先开时钟;确认没有残留的 START |
hal_isp_top_configure() 返回 INVALID_PARAM | 帧尺寸不在 640×360~1920×1080 或未 4 对齐 | 用 640×480 / 1280×720 / 1920×1080 这类值 |
| 起流后一帧都没有 | 地址 FIFO 没预填满 / stride 为 0 / WDMA 被 reset 后没重配 | 预填 4 个地址;hal_isp_wdma_configure() 在 reset 之后 |
| 帧写到了错误的缓冲 | FIFO 里有上一轮残留的地址 | 起流前 hal_isp_wdma_reset() 并确认全空 |
DONE_ADDR 不动但 DONE 在涨 | 中间件把同一块缓冲重复提交 | 这是已知行为,改用“地址是否在帧环内”判 |
| 画面全黑但寄存器检查全部通过 | DMSC 全 0 矩阵把 Y 打成常数 0 | 检查 hal_isp_dmsc_configure() 是否被调用且矩阵非 0 |
| 画面明显变暗 | DVP raw_align 用了 HIGH | 改成 HAL_VIC_RAW_ALIGN_LOW |
| 彩色画面偏色 | uv_offset 非 0,或 YUV422 字节序 U/V 未交换 | 检查 IPC 偏移;YUV422 dump 是 YVYU |
| 画面错位 / 尺寸错误 | 用“只写 START”的方式重启了 VIC | 必须 hal_vic_stop() → hal_vic_start() |
| MIPI 每行错位 | total_width/WIDTH4/裁剪与 sensor 实际行宽不一致 | 对齐 total_width 与 sensor HTS;检查裁剪合法性 |
| AE 统计头全 0 | 网格比帧大 / STATIC_EN 没打开 / DMA 基址为 0 | 用默认网格整套配置;确认 STATIC_EN 的 AE 位为 1;基址 8 对齐 |
| AE 帧号在涨但没统计 | 帧边界到了,但网格越界或 DMA 没写入数据 | 看 hdr;检查网格尺寸与 DMA_INFO |
SHD_CTRL 读回 0 | 一次性触发已经生效并自清 | 正常现象,不是配置没生效 |
*_regs_dump() 返回 UNSUPPORTED | 调试辅助没编译 | 同时打开两个 Kconfig |
| 非法参数没被拦住 | CONFIG_SDK_HAL_PARAM_CHECK=n | 打开参数检查,或自己校验 |
9. 约束与注意事项
9.1 中断极性
VIC:
INT_MASK1 = 屏蔽、0 = 放行。RESP:事件源
INT_EN1 = 使能;CPUINT_MASK1 = 允许上报、0 = 阻止。两套语义相反,不能共用同一个掩码辅助函数。
RESP
INT_CTRL.ALL_CLR位于 bit16,写入后清除所有挂起事件。
9.2 VIC 几何与格式
DVP
HACT:YUV422_8BIT 为width×2,其余为width。BT656
HACT:16-bit 总线为width,8-bit 总线为width×2。MIPI
HACT:固定为output_width×2。MIPI 4 字节字宽按
ceil(total_width×bpp/32)计算,total_width=0时取输出宽度。裁剪必须满足
hcrop_start_pixel ≤ total_width且剩余宽度不小于输出宽度。帧配置固定写
PCK_MASK = 0xFFFF、VLD_FRM_SAV = height、VLD_FRM_EAV = width。DVP 只支持逐行,HAL 固定写奇数场;BT656 起始场可配。
RAW_ALIGN低位模式截取 RAW8[7:0]/RAW10[9:0]/RAW12[11:0],高位模式截取 RAW8[11:4]/RAW10[11:2]/RAW12[11:0]。
9.3 IPC 约束
OUT_NUM是输出映射计数上限(0~0x1F),不是通道数;0表示只用映射项 0, 即单路线性输出。触发模式 0 = 正常,1 = 一帧后停止。
通道 0 触发源 0 表示“不启动”,默认值 4 才是正常启动配置。
Y/UV 偏移上限
0x1FF,硬件加法后饱和到 0~255。RESTART(0x3040)的 bit4OVERFLOW、bit0SIZE_ERR只决定异常后是否进入停流 序列,没有自动恢复能力;两档的恢复动作不同且不可互换,见 8.5。TRIGE与func_reset都是每路独立的:ISP0 写错救不了 ISP1。
9.4 GPL_WDMA 约束
地址与步长必须 8 字节对齐;地址上限
0x1FFFFFF8,步长上限0xFFF8。地址 FIFO 深度为 4;只有解码自洽(计数相加为 4、满/空标志匹配)时才参与满判断。
FIFO 满时 HAL 自动复位并重配静态参数,调用方无需手工恢复,但应预期本次写地址耗时增加。
C_START_ADDR必须在START_ADDR之前写;START_ADDR可能立即启动搬运。DONE_ADDR/C_DONE_ADDR不复位,可能保留上次运行的值;不能把“非 0”当作本帧完成。双平面模式下只有
enable_chroma为真时才写 UV 地址。mux_sel = 3(DMSC YUV 通路)是默认值;mux_sel与数据排布必须匹配。
9.5 DMSC 约束
只有 ISP0 有 DMSC;ISP1 上没有这个寄存器块。
当前软件通路按 RAW10 输入使用;RAW8 输入没有完整验证过的通路。
全零 3×3 矩阵会让 Y 输出恒为 0,与输入无关;配置矩阵前先确认系数表。
复位默认值本身对应一条 BT.601 Q10 彩色通路,不能把“未配置”理解为“直通”。
矩阵系数位宽不一致,写越界值会被 HAL 拒绝。
DMSC 输出 FIFO 只有 4 项,
DEBUG_0/1适合观察瞬时反压,不适合作为长期统计。
9.6 AE 约束
每方向分区数 1~15;尺寸表固定 15 项,只有前
zone_h_number/zone_v_number项被拷贝,其余写 0。DMA 缓冲数 1~4,写入硬件时减 1;未使用的基址写 0。
DMA 基址必须 8 字节对齐,0 表示该表项不使用。
特定窗口坐标上限
0xFFF,帧号上限0xFF。默认 15×15 分区、水平尺寸 20、垂直尺寸 16(对应
帧宽/2/15与帧高/2/15), 是中间件验证过的配置;分区尺寸的复位值是 64×36,15×15 格覆盖 960×540, 在大帧上可能越界,所以不能只改 DMA 基址。SHD_CTRL是一次性影子更新触发,帧边界自清;每次改配置都要重新写。只有 ISP0 有 AE;ISP1 没有这个寄存器块。
AE 统计使能和直方图使能是两个独立位,需要分别打开。