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.hVIC 输入前端(DVP / BT656 / MIPI CSI)与 sensor MCLK

hal_isp_clock.hISP 模块时钟

hal_isp_top.hISP TOP(帧尺寸、数据类型、DMSC 旁路、统计开关)

hal_isp_ipc.hIPC 帧输出控制与输出偏移

hal_isp_resp.hRESP 事件、中断与断点位置

hal_isp_wdma.hGPL_WDMA 写帧、地址 FIFO 与完成状态

hal_isp_dmsc.hDMSC 去马赛克与颜色矩阵

hal_isp_ae.hAE 分区统计、直方图与 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.ccode/drivers/g32s10/vic/
构建code/drivers/g32s10/vic/Kconfig、CMakeLists.txtCONFIG_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 的时序解码、裁剪与格式转换
TOPISP 顶层,决定帧尺寸、数据类型、Bayer、统计位置与 DMSC 旁路
IPC帧输出控制器,把 TOP 处理后的数据按通道送出
DMSC去马赛克与颜色空间转换(RAW → YUV)
GPL_WDMA写 DMA,把 IPC/DMSC 的输出写进 DDR
RESP事件与中断汇总,记录 AE/DMSC/WDMA 的完成与错误
AE自动曝光统计块,输出 15×15 分区统计与直方图(只有 ISP0 有)
CIM / CSIDVP 的时钟/输入域(CIM0、CIM1)与 MIPI 接收控制器(CSI0)
MCLK送给 sensor 的主时钟,由 CIM0/CIM1 提供
RAWsensor 原始 Bayer 数据;G32S10 上常见 RAW8/RAW10/RAW12
NV12Y 平面 + 交织 UV 平面(YUV420),WDMA 双平面输出格式

建议阅读顺序:

  • 第一次使用:第 2 章 → 第 3 章 → 第 8 章,按第 8 章的顺序一步一步写代码。

  • 查某个函数:第 5 章(HAL)或第 6 章(LL)。

  • 查寄存器:第 7 章。

  • 出问题时:第 11 章 → 第 12 章 → 第 13 章。

2. 分层关系与依赖

2.1 软件层次

APPLICATION  Application / BSP / Camera middleware  业务代码、板级支持与示例入口
↓  camera_*() / board_*() 接口进入图像采集链路
HAL  HAL VIC + HAL ISP sub-modules  clock / vic / top / ipc / resp
wdma / dmsc / ae
↓  按字段写入和读取硬件状态
LL  LL VIC / ISP  寄存器访问与字段级控制
↓
HARDWARE REGISTERS  VIC + ISP register file  实例地址、寄存器布局与位域定义
层职责典型使用者
应用 / 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 子模块偏移

子模块偏移头文件
TOP0x0000isp_top_regs.h
AE0x2000isp_ae_regs.h
IPC0x3000isp_ipc_regs.h
DMSC0x4000isp_dmsc_regs.h
GPL_WDMA0x9000isp_wdma_regs.h
RESP0xA000isp_resp_regs.h
VIC0xD000vic_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 步长相同
YUV422ALIGN_UP(width, 8) × 2
RAW8 / 灰度ALIGN_UP(width, 8)
RAW10 / RAW12ALIGN_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);

三步判据缺一不可:

  1. 返回 IM_OK,日志里出现 WDMA address FIFO full(证明真的判到了满);

  2. 自愈后 Y/C 两套 FIFO 各只剩本次写入的 1 个地址(data=1、full=0);

  3. 回读 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 的监控都适用。

可以直接抄走的三条做法:

  1. 起流前必须把地址 FIFO 清空(hal_isp_wdma_reset()),否则上一轮残留的地址会在 起流瞬间被写进 DDR——这是内存被踩的经典成因;

  2. 任何 FIFO 状态读数都要先检查自洽(valid),不自洽时后续判据全部作废;

  3. 寄存器检查全部通过不等于图像正确,至少加一条帧内容判据。

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 才走 DMSCbypass_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

上电后按这个顺序验证:

  1. 串口有启动日志;

  2. hal_isp_clock_enable() 返回 IM_OK;

  3. 读 TOP version(ISPx + 0x0000)不是全 0/全 1;

  4. 调 hal_vic_reset() 返回 IM_OK,回读 VIC.CTRL 为 0;

  5. 打开调试辅助时,hal_vic_regs_dump() / hal_isp_top_regs_dump() 能打出 合理的值(见 11.2);

  6. 起流后 hal_isp_wdma_read_done() 的 done_address 落进你的帧环;

  7. 抽一帧看 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.BYPASSDMSC 未旁路(走 NV12 时)
VIC.INT_MASK只放行需要的位(如 FRAME_DONE)
WDMA.MUX_SEL3(DMSC YUV 通路)
WDMA.STRIDE640(ALIGN_UP(640,16))
WDMA.CHR_STRIDE640
AE.ZONE_NUM_START0xf000f000(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 反复进 ISRINT_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_MASK 1 = 屏蔽、0 = 放行。

  • RESP:事件源 INT_EN 1 = 使能;CPU INT_MASK 1 = 允许上报、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)的 bit4 OVERFLOW、bit0 SIZE_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 统计使能和直方图使能是两个独立位,需要分别打开。