API GUIDE / I2D / OVERVIEW

G32S10 I2D API 概述

模块概述:本文说明 G32S10 SDK 中 I2D 驱动三层接口的设计与使用方法。

1. 文档范围

本文说明 G32S10 SDK 中 I2D 驱动三层接口的设计与使用方法。I2D 是一个独立的 DMA 引擎:读一块内存里的图像,按旋转/镜像/翻转的几何关系写回另一块内存。它不做缩放、 不做像素格式转换,源和目标共用同一种格式。

本文覆盖:

  • i2d_regs.h:寄存器布局、基地址、位域与格式/旋转编码

  • ll_i2d.h:寄存器级 static inline 读写接口

  • hal_i2d.h / hal_i2d.c:初始化、布局推导、参数校验、逐帧启动、中断分发与错误锁存

  • 从零开始的调用步骤:直接用 HAL 或 LL 完成「内存里的图 → 旋转/镜像 → 内存里的图」

  • 三个官方示例的逐段解读:code/examples/hal/i2d_blocking_transform_example.c、code/examples/hal/i2d_callback_transform_example.c、code/examples/ll/i2d_ll_example.c

  • 把 HAL/LL 接进自己工程的注意事项(与相机帧、与中间件的配合)

本文不覆盖中间件 im_i2d,见 docs/api/g32s10/middleware/i2d_mid_api.md。日常应用 优先使用中间件;本文面向需要直接操作 HAL/LL 的场景(bring-up、寄存器验证、自建 驱动/中间件、把 I2D 嵌进已有流水线)。

本文档基于以下实现编写:

文件内容
code/include/drivers/g32s10/i2d/i2d_regs.h寄存器结构体、基地址、位域宏、格式/旋转枚举
code/include/drivers/g32s10/i2d/ll_i2d.hLL 层 static inline 接口
code/include/drivers/g32s10/i2d/hal_i2d.hHAL 类型、宏与接口声明
code/drivers/g32s10/i2d/hal_i2d.cHAL 实现:校验、布局、寄存器编程、中断分发
code/drivers/g32s10/i2d/Kconfig配置开关 CONFIG_DRIVER_I2D
code/drivers/g32s10/i2d/CMakeLists.txt组件注册
code/examples/hal/i2d_blocking_transform_example.cHAL 示例:轮询等待
code/examples/hal/i2d_callback_transform_example.cHAL 示例:IRQ_I2D + 回调
code/examples/ll/i2d_ll_example.cLL 示例:直接操作寄存器
test/g32s10/module/i2d/寄存器/复位/功能/压力等测试用例(独立 test/ 仓库)

术语:

术语含义
一次变换(transform)一次 I2D 硬件作业:一张源图旋转/镜像/翻转后写入一块目标缓冲
源 / 目标(src / dst)读入的图像缓冲 / 写出的图像缓冲。地址由调用方提供
平面(plane)图像里一块连续的分量区。打包格式只有 1 个平面;NV12 有 Y、UV 两个
stride(行跨距)一行像素在内存里占多少字节(不是像素),可以大于图像宽度
shadow(影子寄存器)一类只有写 SHD_CTRL 后才生效的配置寄存器
安全复位(SAFE_RST)清内部流水线状态、但保留寄存器配置的复位
outstandingAXI 读请求的在途数量上限,用于限制突发带宽
完成事件FRAME_DONE / TIMEOUT / OUTSTANDING 三个硬件事件

2. 分层关系与依赖

2.1 分层与调用方向

APPLICATION  应用 / 示例 / 驱动  业务代码、板级支持与示例入口
↓
HAL  HAL I2D · hal_i2d_*  实例状态、参数校验、缓存维护、寄存器编程与中断分发
负责 SoC cache 维护和业务侧完成通知
↓
LL  LL I2D · ll_i2d_*(static inline)  寄存器字段读写、shadow 提交、安全复位、启停与中断使能
↓
HARDWARE REGISTERS  I2D Registers · 0x13330000  实例地址、寄存器布局与位域定义
IRQ  IRQ_I2D / PLIC  I2D 硬件中断源
DISPATCH  im_irq dispatcher  平台统一中断分发
SOFTWARE  Application ISR  由应用注册的完成或错误处理入口

2.2 各层职责

层负责不负责
应用 / 示例提供源/目标 buffer 与容量;填写宽高、格式、旋转;cache 维护;申请 SoC 中断;等待与超时;错误恢复寄存器细节、布局推导
HAL I2D参数校验;stride/平面地址/平面大小推导;寄存器编程与启动;中断状态读取/清除;状态机与错误位图;CPM 时钟门控cache 维护;SoC 中断申请;buffer 分配;等待
LL I2D寄存器字段读写;shadow 提交;安全复位;启停;中断掩码;硬件超时与内部时钟门控参数校验(只做字段截位);布局;状态机
i2d_regs.h寄存器布局、位域 mask/shift、格式/旋转编码、实例指针任何行为

关键边界:

  • HAL 不做 cache 维护,也不申请 SoC 中断。这两件事由调用方负责;走中间件im_i2d 时才自动具备。

  • HAL 不分配、不搬运 buffer,只把地址写进寄存器。

  • LL 不做语义校验。例如 ll_i2d_set_outstanding_limit() 只按 8 位截位,传 0 会原样写进硬件;ll_i2d_set_image_size() 只按 16 位截位。

  • 只有 I2D 一个实例,ll_i2d_is_instance() 与 HAL 都只接受它。

2.3 依赖的公共服务

服务用途谁用
hal_cpm_clk_enable() / hal_cpm_clk_disable()I2D 的 CPM 时钟门(HAL_CPM_ID_GATE_I2D)HAL
os_enter_critical() / os_exit_critical()hal_i2d_start() 与 IRQ 路径的启动竞态HAL
IM_HAL_PARAM_CHECK_RETURN统一参数检查(受 CONFIG_SDK_HAL_PARAM_CHECK 控制)HAL
im_cache_data_clean_by_addr() / im_cache_data_clean_invalidate_by_addr() / im_cache_data_invalidate_by_addr()buffer 的 cache 维护应用/示例
im_irq_request() / im_irq_enable() / im_irq_release()IRQ_I2D 的注册、使能与释放应用/示例
im_time_get_ms() / im_time_delay_us()软件超时与轮询间隔应用/示例
ll_aon_hpclkgr1_clear_stop_mask() / set_stop_mask()HP1 总线时钟 stop 位(仅 LL 路径)应用/示例

3. 构建配置与文件组织

3.1 文件职责

文件职责
i2d_regs.h寄存器结构体与偏移、I2D 实例、位域宏、I2D_MODE_DATA_TYPE_*、I2D_MODE_ROTATE_*、I2D_OUTSTANDING_LIMIT_DEFAULT
ll_i2d.h所有 static inline 寄存器操作,无状态、无锁、不做语义校验
hal_i2d.h枚举、结构体、宏、接口声明
hal_i2d.c校验、布局、寄存器编程、中断分发、状态机
code/examples/hal/*.c两个 HAL 用法示例(轮询 / 回调),不参与默认构建
code/examples/ll/*.c一个 LL 用法示例,不参与默认构建

4. 资源模型与公开类型

4.1 实例与硬件资源

项值来源
寄存器基地址 I2D_IOBASE0x13330000code/soc/g32s10/include/base.h
实例指针 I2D(i2d_regs_t *)(uintptr_t)I2D_IOBASEi2d_regs.h
SoC 中断IRQ_I2D = IRQ_PLIC0_START + 21irq_table.def
CPM 时钟门HAL_CPM_ID_GATE_I2D(HP1 组第 8 位)hal_cpm.h
总线时钟 stop 位AON hpclkgr1 的 AON_HPCLKGR1_I2D_BIT(= 8)cpm_regs.h
buffer 对齐HAL_I2D_BUFFER_ALIGN = 64 字节hal_i2d.h
实例数1ll_i2d_is_instance()
  • 时钟分两级:AON 的 HP1 总线门控(stop 位)与模块内部门控(clk_gate 寄存器)。 HAL 会两级都打开;LL 用户必须自己打开两级(见 §6.8)。

  • I2D 只用 IRQ_I2D 一根中断线;PLIC 侧的实际使能是应用的事。

4.2 寄存器映射

4.2.1 寄存器列表

寄存器访问偏移说明
versionR0x000RTL 版本号
shd_ctrlRW0x008Shadow 配置更新控制
ctrlRW0x010复位与启动控制
img_sizeRW0x014源图像宽高
modeRW0x018像素格式、旋转、镜像、翻转
outstandingRW0x01CAXI 读 outstanding 上限
src_addr_yRW0x020源 Y 平面地址
src_addr_uvRW0x024源 UV 平面地址
src_stride_yRW0x028源 Y 平面 stride
src_stride_uvRW0x02C源 UV 平面 stride
dst_addr_yRW0x030目标 Y 平面地址
dst_addr_uvRW0x034目标 UV 平面地址
dst_stride_yRW0x038目标 Y 平面 stride
dst_stride_uvRW0x03C目标 UV 平面 stride
irq_stateR0x080中断状态(锁存)
irq_clearW0x084中断状态清除(写 1 清对应位)
irq_maskRW0x088中断屏蔽(1 = 屏蔽)
timeout_valueRW0x090硬件超时上限
timeout_modeRW0x094硬件超时模式(自动重启)
clk_gateRW0x098模块内部时钟门控

0x004 / 0x00C / 0x040-0x07C / 0x08C 为保留区域。

4.2.2 SHD_CTRL(0x008)

位名称说明
[1]MODE_SHD提交 mode 类 shadow 配置
[0]SIZE_ADDR_SHD提交尺寸/地址/stride 类 shadow 配置

I2D_SHD_CTRL_UPDATE_ALL = 0x3。写完配置必须提交一次,否则硬件继续用上一次 (或复位值)的 mode/size/地址/stride。

4.2.3 CTRL(0x010)

位名称说明
[16]RST模块复位
[4]SAFE_RST安全复位(不清寄存器配置)
[0]START启动处理;写 1 启动,写 0 停止

HAL 只在每帧启动前用 SAFE_RST,从不使用 RST;RST 留给 bring-up 或整个模块需要重来时使用。

4.2.4 IMG_SIZE(0x014)

位名称说明
[31:16]WIDTH源图像宽度(16 bit)
[15:0]HEIGHT源图像高度(16 bit)

旋转 90°/270° 后输出宽高互换,但这里始终写源尺寸;输出的几何只体现在目标 stride 与目标 buffer 大小上。

4.2.5 MODE(0x018)

位名称说明
[6:4]DATA_TYPE像素格式
[3:2]ROTATE_MODE旋转角度
[1]MIRR水平镜像使能
[0]FLIP垂直翻转使能

DATA_TYPE 编码:

值宏格式
0I2D_MODE_DATA_TYPE_NV12NV12(Y + 交织 UV,4:2:0)
1I2D_MODE_DATA_TYPE_RAW8RAW8 / Y8 单平面
2I2D_MODE_DATA_TYPE_RGB565RGB565 packed
3I2D_MODE_DATA_TYPE_ARGB8888ARGB8888 packed
4I2D_MODE_DATA_TYPE_YUV422YUV422 packed

ROTATE_MODE 编码:

值宏角度
0I2D_MODE_ROTATE_00°
1I2D_MODE_ROTATE_90顺时针 90°
2I2D_MODE_ROTATE_180180°
3I2D_MODE_ROTATE_270顺时针 270°(= 逆时针 90°)

4.2.6 OUTSTANDING(0x01C)

位名称说明
[7:0]LIMITAXI 读 outstanding 上限

复位值 0x40(I2D_OUTSTANDING_LIMIT_DEFAULT)。字段 8 位,0 不是可配置值; HAL 要求 1..255。

4.2.7 地址寄存器(0x020/0x024/0x030/0x034)

位名称说明
[31:0]ADDRY/UV 平面字节地址

4.2.8 Stride 寄存器(0x028/0x02C/0x038/0x03C)

位名称说明
[15:0]STRIDE平面 stride(字节)

LL 写入时按 16 位截位;HAL 在使用前把超过 0xFFFF 的 stride 判为非法。

4.2.9 IRQ 状态/清除/屏蔽(0x080/0x084/0x088)

位名称说明
[2]OUTSTANDINGoutstanding 上限中断
[1]TIMEOUT硬件超时中断
[0]FRAME_DONE一帧处理完成
  • irq_state 只读,状态位锁存,读不清除。

  • irq_clear 只写,写 1 清对应位。

  • irq_mask 中 1 表示屏蔽、0 表示放行,与 VIC 等模块的方向相反。

  • 屏蔽只挡中断上报,irq_state 仍会锁存,可以轮询。

4.2.10 TIMEOUT_VALUE / TIMEOUT_MODE(0x090/0x094)

寄存器位名称说明
timeout_value[31:0]LIMIT硬件超时上限;HAL 约定 0 表示关闭硬件超时
timeout_mode[0]RST超时后自动重启

4.2.11 CLK_GATE(0x098)

位名称说明
[0]VALUE模块内部时钟门控;写 0 关闭内部时钟

4.2.12 复位/默认值

项值来源
OUTSTANDING.LIMIT 复位值0x40i2d_regs.h
hal_i2d_init_config_t::outstanding_limit 默认值0x40hal_i2d_init_config_default()
hal_i2d_init_config_t::timeout_value 默认值UINT32_MAX同上
timeout_restart 默认值true同上
frame_done_irq_enable / timeout_irq_enable 默认值true同上
outstanding_irq_enable 默认值false同上
hal_i2d_config_default()全 0 + NV12 + ROTATE_0hal_i2d_config_default()

4.3 状态机

STATE  RESET
→
STATE  READY
→
STATE  BUSY
→
STATE  TIMEOUT
→
STATE  ERROR

RESET → READY · hal_i2d_init() 成功

READY → BUSY · hal_i2d_start()

BUSY → READY · FRAME_DONE / IRQ

BUSY → TIMEOUT · TIMEOUT 事件

BUSY → ERROR · OUTSTANDING 事件

TIMEOUT / ERROR → READY · error_clear 或 stop 成功

READY → RESET · hal_i2d_deinit()

当前状态事件 / 调用新状态
RESEThal_i2d_init() 成功READY
READYhal_i2d_start()BUSY
BUSYFRAME_DONEREADY
BUSYTIMEOUTTIMEOUT
BUSYOUTSTANDINGERROR
TIMEOUT / ERRORhal_i2d_error_clear() 或 hal_i2d_stop()READY
任意(非 RESET)hal_i2d_deinit()RESET

注意两点:

  1. 同一拍多个事件时,FRAME_DONE 最后处理,会把 TIMEOUT/ERROR 覆盖成READY。是否出错必须看 hal_i2d_error_get(),不能只看状态。

  2. hal_i2d_start() 在非 READY 状态返回 IM_ERR_BUSY,不会打断正在进行的帧。

5. 接口规范

5.1 HAL 接口

所有可失败接口返回 error_code_t,参数检查统一使用 IM_HAL_PARAM_CHECK_RETURN。 HAL 只接受 I2D 一个实例。

5.1.1 hal_i2d_init_config_default

error_code_t hal_i2d_init_config_default(hal_i2d_init_config_t *init);

填默认值(见 §4.4),不访问硬件。init == NULL 返回 IM_ERR_INVALID_PARAM。

5.1.2 hal_i2d_config_default

error_code_t hal_i2d_config_default(hal_i2d_config_t *config);

清零整个结构体,并把 format 设为 HAL_I2D_FORMAT_NV12、rotate 设为HAL_I2D_ROTATE_0。宽高、地址与 stride 需要调用方填写。

5.1.3 hal_i2d_init

error_code_t hal_i2d_init(hal_i2d_handle_t *hi2d,
                          const hal_i2d_init_config_t *init);

校验顺序:

  1. hi2d、init 非空;

  2. init->instance 必须等于 I2D;

  3. outstanding_limit 必须非 0 且 <= 0xFF。

随后执行:

  1. 清零句柄,绑定 instance,保存 init 副本;

  2. hal_cpm_clk_enable(HAL_CPM_ID_GATE_I2D);

  3. ll_i2d_enable_clock_gate() 打开模块内部时钟;

  4. ll_i2d_safe_reset();

  5. 写 outstanding 上限、硬件超时值,并按 timeout_restart 置/清自动重启位;

  6. 按三个 *_irq_enable 组合使能掩码,向 irq_mask 写 ALL & ~enabled;

  7. 清除全部中断状态;

  8. error_code = NONE,state = READY。

第 2 步失败时原样返回 CPM 的错误码;此时句柄已被清零并写入 instance/init, 状态仍为 RESET。

HAL 不申请 SoC 中断:init 里的 *_irq_enable 只决定外设 irq_mask。

5.1.4 hal_i2d_deinit

error_code_t hal_i2d_deinit(hal_i2d_handle_t *hi2d);

依次执行:ll_i2d_stop()、屏蔽全部中断、清除全部中断状态、hal_cpm_clk_disable(HAL_CPM_ID_GATE_I2D)(返回值忽略)、清空callback/user_ctx、状态置 RESET。句柄无效时返回 IM_ERR_INVALID_PARAM。

5.1.5 hal_i2d_layout_get

error_code_t hal_i2d_layout_get(const hal_i2d_config_t *config,
                                hal_i2d_layout_t *layout);

纯软件计算,不访问硬件,也不需要先 hal_i2d_init()。它先按 §6.4 校验 config, 再按 §6.3 推导布局。config 非法返回 IM_ERR_INVALID_PARAM;config 合法但layout 为空也会返回该错误。

config.src_buf / dst_buf 必须非空,但函数不会解引用它们——两个示例在还不知道 大小时传的是 HAL_I2D_BUFFER_ALIGN 之类的占位地址。

5.1.6 hal_i2d_start

error_code_t hal_i2d_start(hal_i2d_handle_t *hi2d,
                           const hal_i2d_config_t *config);
  1. 校验句柄;要求 state == HAL_I2D_STATE_READY,否则返回 IM_ERR_BUSY;

  2. 内部调用 hal_i2d_layout_get() 完成校验与布局推导;失败时把HAL_I2D_ERROR_INVALID_PARAM 记入 error_code,返回 IM_ERR_INVALID_PARAM, 状态保持 READY;

  3. 进入临界区,依次:

    • 编程 MODE(格式/旋转/镜像/翻转)、IMG_SIZE、源/目标 Y/UV 地址与 stride;

    • 写 SHD_CTRL = UPDATE_ALL;

    • 清除全部中断状态;

    • 状态置 BUSY;

    • ll_i2d_safe_reset();

    • 置 CTRL.START 启动。

  4. 退出临界区返回 IM_OK。

HAL 不等待本帧完成;完成信号来自中断回调或轮询。源/目标 buffer 的 cache 维护 必须在调用前完成(见 §6.7)。

5.1.7 hal_i2d_stop

error_code_t hal_i2d_stop(hal_i2d_handle_t *hi2d);

先清 CTRL.START,再 safe_reset;只要句柄不是 RESET 状态,就把状态恢复为READY。它不关时钟,也不清中断状态。异常或超时后可先用它把引擎拉回可再次启动的 状态。

5.1.8 hal_i2d_irq_config

error_code_t hal_i2d_irq_config(hal_i2d_handle_t *hi2d,
                                uint32_t enable_mask);

enable_mask 只允许 HAL_I2D_IRQ_* 的组合,含其它位返回 IM_ERR_INVALID_PARAM。 函数把 irq_mask 写成 ALL & ~enable_mask:

  • 传 0 屏蔽全部外设中断源;

  • 只改外设门控,不申请、不使能、不释放 SoC 中断线;

  • 屏蔽不清 irq_state,被屏蔽的源仍锁存,可以轮询。

5.1.9 hal_i2d_irq_handler

void hal_i2d_irq_handler(hal_i2d_handle_t *hi2d);

由 SoC 中断入口调用;在没有注册 SoC 中断时,也可以由任务轮询调用(示例就是 这么做的)。

  1. 读 irq_state 并只保留三个有效位;

  2. 求 status = latched & enabled,其中 enabled = ALL & ~irq_mask;

  3. status == 0 直接返回;

  4. 只清除 status 中已使能且已置位的位;

  5. 按 TIMEOUT → OUTSTANDING → FRAME_DONE 的顺序处理并回调:

状态位错误标志状态迁移回调
TIMEOUTHAL_I2D_ERROR_TIMEOUTTIMEOUTHAL_I2D_EVENT_TIMEOUT
OUTSTANDINGHAL_I2D_ERROR_OUTSTANDINGERRORHAL_I2D_EVENT_OUTSTANDING
FRAME_DONE不改错误标志READYHAL_I2D_EVENT_FRAME_DONE

注意:默认 outstanding_irq_enable = false,所以 outstanding 事件不会进入状态机, 除非用 hal_i2d_init() 的字段或 hal_i2d_irq_config() 放行。

5.1.10 hal_i2d_callback_register

error_code_t hal_i2d_callback_register(hal_i2d_handle_t *hi2d,
                                       hal_i2d_callback_t callback,
                                       void *user_ctx);

注册/注销(传 NULL)底层事件回调。回调在 I2D IRQ 上下文运行,不得阻塞或做 cache 维护。一次 IRQ 处理可能连续调用多次(每个待处理状态位一次),需要记录事件的调用方 应使用位图或计数器,而不是只保存最后一个 event。

5.1.11 状态、错误

接口行为
hal_i2d_state_get(hi2d, &state)读句柄状态;无效句柄或空输出指针返回 IM_ERR_INVALID_PARAM
hal_i2d_error_get(hi2d)返回锁存的错误位图;无效句柄返回 HAL_I2D_ERROR_NONE
hal_i2d_error_clear(hi2d, error_mask)按 error_mask 清位;若状态为 ERROR/TIMEOUT,恢复为 READY

5.2 返回码与恢复策略

返回码场景恢复
IM_OK成功—
IM_ERR_INVALID_PARAM句柄/配置为空;instance 不是 I2D;outstanding 为 0 或 >255;格式/旋转非法;宽高越界或未按格式对齐;stride 超 0xFFFF 或小于最小行字节数;非 NV12 却给了 UV 指针/stride;enable_mask 含非法位修正参数;若错误来自 start,error_code 已锁存 INVALID_PARAM,用 hal_i2d_error_clear() 清掉
IM_ERR_BUSY上一帧未结束时再次 start等待完成(轮询/中断),或 hal_i2d_stop()
CPM 错误码hal_i2d_init() 打开 HAL_CPM_ID_GATE_I2D 失败检查 CPM 配置后重新 init
等待超时(应用侧返回)硬件没在预期时间内完成读 state/error_get;必要时 stop 后重试

硬件超时/outstanding 不通过返回码表达,而是通过 error_code 位图与状态反映。

6. 行为约束与实现说明

6.1 一次变换的控制流

APPLICATION  应用 / 示例 / 驱动  业务代码、板级支持与示例入口
↓
HAL  HAL I2D · hal_i2d_*  实例状态、参数校验、缓存维护、寄存器编程与中断分发
负责 SoC cache 维护和业务侧完成通知
↓
LL  LL I2D · ll_i2d_*(static inline)  寄存器字段读写、shadow 提交、安全复位、启停与中断使能
↓
HARDWARE REGISTERS  I2D Registers · 0x13330000  实例地址、寄存器布局与位域定义
IRQ  IRQ_I2D / PLIC  I2D 硬件中断源
DISPATCH  im_irq dispatcher  平台统一中断分发
SOFTWARE  Application ISR  由应用注册的完成或错误处理入口

6.2 寄存器编程顺序与 shadow / 安全复位

HAL 每帧的写序是固定的:

  1. MODE → 2. IMG_SIZE → 3. 源地址 → 4. 目标地址 → 5. 源 stride →

  2. 目标 stride → 7. SHD_CTRL = UPDATE_ALL → 8. 清中断状态 → 9. 状态置 BUSY→ 10. SAFE_RST → 11. START。

三处顺序有实际意义,自己写 LL 时不要调换:

  • UPDATE_ALL 必须在 START 之前。第 1~6 步写的是 shadow 寄存器,不提交就 不会生效;漏掉这一步,硬件会沿用上一次的格式/地址,现象往往是「图还是上一帧的」 或「整块乱码」。

  • SAFE_RST 在 UPDATE_ALL 之后。它清内部流水线残留状态,但不清寄存器配置; 放在提交之后可以保证两者的语义互不覆盖。

  • 启动前清一次中断状态。否则上一帧锁存的 FRAME_DONE 会被误当成本帧完成。

OUTSTANDING 与 TIMEOUT_VALUE 只在 hal_i2d_init() 写一次,不随每帧变化。

6.3 状态机与错误锁存

  • error_code 是或运算累积的,必须显式清除;清除不会自动清掉硬件状态位。

  • INVALID_PARAM 只会在 start 的布局校验失败时锁存,此时状态仍是 READY。

  • TIMEOUT/ERROR 状态下不能再 start(返回 IM_ERR_BUSY);先hal_i2d_error_clear() 或 hal_i2d_stop() 回到 READY。

  • FRAME_DONE 会把状态写回 READY,即使同一拍还有 TIMEOUT 位;所以判断成功与否 要同时看 state 与 error_get()。

6.4 Cache 与对齐

HAL 不做 cache 维护。调用方在每帧必须:

阶段地址操作
start 前源 Y(NV12 还有 UV)clean
start 前目标 Y(NV12 还有 UV)clean + invalidate
完成后目标 Y(NV12 还有 UV)invalidate

原因:

  • 源是 CPU(或上一个 DMA)写的,clean 把脏行写回内存,I2D 才能读到新数据。

  • 目标在写之前 clean+invalidate,避免 CPU cache 里的脏行在 I2D 写完之后被回写覆盖。

  • 完成后 invalidate,CPU 才会重新从内存读 I2D 写出的结果。

范围用 layout 的 *_y_size / *_uv_size 分平面维护,或按 *_buffer_size 一次 维护整块。buffer 按 64 字节(HAL_I2D_BUFFER_ALIGN,同时也是 cache 行大小)对齐, 否则边界那一行可能清不到或清多。

走中间件 im_i2d 时这三步由 manage_cache 自动完成;直接调 HAL 就必须自己做。

6.5 时钟与门控

I2D 有两级时钟:

级别寄存器打开方式
HP1 总线时钟(AON 管)AON hpclkgr1 的 I2D stop 位(bit 8)HAL:hal_cpm_clk_enable(HAL_CPM_ID_GATE_I2D);LL:ll_aon_hpclkgr1_clear_stop_mask(AON_CLKGR_STOP_MASK(AON_HPCLKGR1_I2D_BIT))
模块内部时钟clk_gatell_i2d_enable_clock_gate(I2D)
  • 总线时钟不开就访问寄存器,读回值没有意义,甚至可能挂总线。

  • HAL 的 init/deinit 能自动成对开关两级;LL 用户必须在 deinit 时把 AON stop 位 重新置回去(LL 示例的最后两行就是这件事)。

  • 顺序:先开 AON 总线门,再写模块寄存器(包括 clk_gate)。

6.6 旋转、镜像与翻转的几何模型

设源尺寸 W x H,90°/270° 时输出 out_w = H, out_h = W,否则out_w = W, out_h = H。输出坐标 (x, y) 对应的源坐标:

if horizontal_mirror: x = out_w - 1 - x
if vertical_flip:     y = out_h - 1 - y

ROTATE_0:   src = (x, y)
ROTATE_90:  src = (y, H - 1 - x)     /* 顺时针:源左上 → 输出右上 */
ROTATE_180: src = (W - 1 - x, H - 1 - y)
ROTATE_270: src = (W - 1 - y, x)

这个顺序(先镜像/翻转输出坐标,再做逆旋转)就是三个示例的校验函数example_i2d_map_dst_to_src() 使用的顺序,和硬件一致。写自己的校验时照抄即可。

组合语义:

配置等价于
ROTATE_0 + mirror左右镜像
ROTATE_0 + flip上下翻转
ROTATE_180 + mirror上下翻转
ROTATE_180 + flip左右镜像
ROTATE_90 + mirror转置(主对角线镜像)
ROTATE_270 + mirror副对角线镜像
ROTATE_90 + mirror + flipROTATE_270

6.7 已知限制

  • 只有 I2D 一个实例,没有多通道/多任务并行。

  • 源和目标格式相同,不做格式转换,也不缩放。

  • 宽度上限 3840、高度上限 2160;stride 是 16 位字段(<= 0xFFFF)。

  • 单帧只有一个完成事件,没有「半帧/行完成」中断。

  • HAL 不检查 buffer 容量,也不做 cache 维护。

  • 硬件超时默认 UINT32_MAX 且自动重启:实际等于「几乎不超时」,软件等待上限要 自己设。

7. 典型用法:从零开始

本节假设你手上有一块能跑 SDK 的板子,想把「内存里的图 → 旋转/镜像 → 内存里的图」 直接做在 HAL 或 LL 上。

7.1 HAL 五步模型

步骤做什么用到的接口
1初始化 I2D(开时钟、配中断门、清状态)hal_i2d_init_config_default() + hal_i2d_init()
2按旋转后的几何算出 stride 与 buffer 大小hal_i2d_layout_get()
3分配并对齐源/目标 buffer,填源图,做 cache 维护malloc / 静态数组 + im_cache_*
4启动一帧并等待完成hal_i2d_start() + 轮询或中断
5收尾:出错恢复、关闭hal_i2d_error_clear() / hal_i2d_stop() / hal_i2d_deinit()

关键认知:width / height 永远填源图尺寸。输出尺寸由旋转角度推出,你只需要 按 layout 给出的目标大小准备缓冲。

7.2 第一步:初始化

#include "hal_i2d.h"
#include "im_cache.h"
#include "im_time.h"

static hal_i2d_handle_t s_i2d;

static error_code_t i2d_open(void)
{
    hal_i2d_init_config_t init_cfg;
    error_code_t ret;

    ret = hal_i2d_init_config_default(&init_cfg);  /* instance 已是 I2D */
    if (ret != IM_OK) {
        return ret;
    }
    /* 可改:outstanding_limit / timeout_value / timeout_restart /
     * 三个 *_irq_enable。默认放行 FRAME_DONE 与 TIMEOUT。 */

    return hal_i2d_init(&s_i2d, &init_cfg);
}
  • HAL 会打开 CPM 门控与模块内部时钟,应用不需要再碰时钟。

  • HAL 只配置外设中断门;IRQ_I2D 的申请/使能由应用负责(回调模式才需要)。

7.3 第三步:准备 buffer 与 cache

static uint8_t *align64(uint8_t *p)
{
    return (uint8_t *)(((uintptr_t)p + (HAL_I2D_BUFFER_ALIGN - 1U)) &
                       ~((uintptr_t)HAL_I2D_BUFFER_ALIGN - 1U));
}

src_raw = malloc(layout.src_buffer_size + HAL_I2D_BUFFER_ALIGN);
dst_raw = malloc(layout.dst_buffer_size + HAL_I2D_BUFFER_ALIGN);
if ((src_raw == NULL) || (dst_raw == NULL)) {
    /* 释放已分配的那块,返回 IM_ERR_NO_MEMORY */
}
src = align64(src_raw);
dst = align64(dst_raw);

/* 填源图(相机帧 / 解码结果 / 素材)后: */
im_cache_data_clean_by_addr(src, layout.src_buffer_size);
im_cache_data_clean_invalidate_by_addr(dst, layout.dst_buffer_size);
  • 容量按 layout 给的值,不要按 width * height * bpp 自己算。

  • 目标 buffer 不必预填,但 clean+invalidate 不能省。

7.4 第四步:启动与等待

写法 A:轮询(不注册 SoC 中断)。irq_state 会锁存,直接反复调用hal_i2d_irq_handler() 即可,它会读状态、清状态、更新 HAL 状态机:

static error_code_t i2d_wait_done(hal_i2d_handle_t *hi2d, uint32_t timeout_ms)
{
    uint32_t start_ms = im_time_get_ms();

    for (;;) {
        hal_i2d_state_t state = HAL_I2D_STATE_RESET;

        hal_i2d_irq_handler(hi2d);          /* 读并确认当前挂起的事件 */
        (void)hal_i2d_state_get(hi2d, &state);

        if (state == HAL_I2D_STATE_READY) {
            return IM_OK;
        }
        if (state == HAL_I2D_STATE_TIMEOUT) {
            return IM_ERR_TIMEOUT;
        }
        if (state == HAL_I2D_STATE_ERROR) {
            return IM_ERR_HARDWARE;
        }
        if ((uint32_t)(im_time_get_ms() - start_ms) >= timeout_ms) {
            return IM_ERR_TIMEOUT;
        }
        im_time_delay_us(100U);
    }
}

启动:

hal_i2d_config_t cfg;
hal_i2d_layout_t layout;
error_code_t ret;

(void)hal_i2d_config_default(&cfg);
cfg.src_buf  = src;                  /* 真实地址 */
cfg.dst_buf  = dst;
cfg.width    = 640U;                 /* 源尺寸 */
cfg.height   = 480U;
cfg.format   = HAL_I2D_FORMAT_YUV422;
cfg.rotate   = HAL_I2D_ROTATE_90;

(void)hal_i2d_layout_get(&cfg, &layout);   /* 真实地址下再算一次,结果一致 */
im_cache_data_clean_by_addr(cfg.src_buf, layout.src_buffer_size);
im_cache_data_clean_invalidate_by_addr(cfg.dst_buf, layout.dst_buffer_size);

ret = hal_i2d_start(&s_i2d, &cfg);
if (ret == IM_OK) {
    ret = i2d_wait_done(&s_i2d, 500U);
}
if (ret == IM_OK) {
    im_cache_data_invalidate_by_addr(cfg.dst_buf, layout.dst_buffer_size);
}

写法 B:中断 + 回调(code/examples/hal/i2d_callback_transform_example.c 的 做法):

static volatile uint32_t s_events;
static volatile bool s_done;

static void on_i2d_event(hal_i2d_handle_t *hi2d,
                         hal_i2d_event_t event,
                         void *user_ctx)
{
    (void)hi2d;
    (void)user_ctx;
    s_events |= (uint32_t)event;    /* 一次入口可能回调多次 */
    s_done = true;
}

static void i2d_isr(uint32_t irq_id, void *arg)
{
    (void)irq_id;
    hal_i2d_irq_handler((hal_i2d_handle_t *)arg);
}

/* 初始化阶段: */
(void)hal_i2d_callback_register(&s_i2d, on_i2d_event, NULL);
(void)im_irq_request((uint32_t)IRQ_I2D, i2d_isr, "hal_i2d", &s_i2d);
(void)im_irq_enable((uint32_t)IRQ_I2D);

区别与注意:

项轮询中断
是否需要 SoC IRQ不需要需要 IRQ_I2D
谁调 hal_i2d_irq_handler()任务自己ISR
等待方式延时轮询状态等回调置的标志/信号量
适用bring-up、单帧、简单测试正式业务、任务需要睡眠

两者不要同时用:轮询与 ISR 都会读/清 IRQ_STATE,会互相吃掉事件。

7.5 第五步:收尾与错误恢复

/* 出错后把引擎拉回可再次启动的状态 */
(void)hal_i2d_stop(&s_i2d);
(void)hal_i2d_error_clear(&s_i2d,
        HAL_I2D_ERROR_INVALID_PARAM | HAL_I2D_ERROR_TIMEOUT |
        HAL_I2D_ERROR_OUTSTANDING);

/* 不再使用时 */
(void)hal_i2d_deinit(&s_i2d);

如果注册了 SoC 中断,收尾顺序是:先 im_irq_disable() + im_irq_release(), 再 hal_i2d_callback_register(&s_i2d, NULL, NULL),最后 deinit。先把中断摘掉, 避免回调访问已经失效的句柄。

7.6 把五步拼起来

error_code_t i2d_rotate_once(void *src, void *dst,
                             uint32_t width, uint32_t height,
                             hal_i2d_format_t format,
                             hal_i2d_rotate_t rotate,
                             uint32_t timeout_ms)
{
    hal_i2d_config_t cfg;
    hal_i2d_layout_t layout;
    error_code_t ret;

    if ((src == NULL) || (dst == NULL)) {
        return IM_ERR_INVALID_PARAM;
    }

    ret = i2d_open();                       /* 1. 打开 */
    if (ret != IM_OK) {
        return ret;
    }

    (void)hal_i2d_config_default(&cfg);     /* 2. 几何 */
    cfg.src_buf = src;
    cfg.dst_buf = dst;
    cfg.width   = width;
    cfg.height  = height;
    cfg.format  = format;
    cfg.rotate  = rotate;

    ret = hal_i2d_layout_get(&cfg, &layout);
    if (ret != IM_OK) {
        goto out;
    }

    /* 3. cache(调用方负责) */
    im_cache_data_clean_by_addr(src, layout.src_buffer_size);
    im_cache_data_clean_invalidate_by_addr(dst, layout.dst_buffer_size);

    /* 4. 启动并等待 */
    ret = hal_i2d_start(&s_i2d, &cfg);
    if (ret == IM_OK) {
        ret = i2d_wait_done(&s_i2d, timeout_ms);
    }
    if (ret == IM_OK) {
        im_cache_data_invalidate_by_addr(dst, layout.dst_buffer_size);
    } else {
        (void)hal_i2d_stop(&s_i2d);
        (void)hal_i2d_error_clear(&s_i2d,
                HAL_I2D_ERROR_INVALID_PARAM | HAL_I2D_ERROR_TIMEOUT |
                HAL_I2D_ERROR_OUTSTANDING);
    }

out:
    (void)hal_i2d_deinit(&s_i2d);           /* 5. 关闭 */
    return ret;
}

调用方按 §7.4 的 layout 准备两块足够大的 buffer(且 64 字节对齐)即可。如果要在 多帧之间复用句柄,把 i2d_open() / deinit() 提到循环外面。

7.7 换分辨率、格式、旋转时要改什么

改动需要同步改的
分辨率cfg.width/height;重新 layout_get;按新大小重分配 buffer
格式cfg.format;注意宽高对齐(§6.4)与 bpp 变化
旋转 0°↔180°只需 cfg.rotate;源/目标大小不变
旋转 0°↔90°/270°cfg.rotate;目标宽高与 stride 变化;按 layout 重新准备目标 buffer
镜像/翻转cfg.horizontal_mirror / vertical_flip;尺寸不变
NV12显式给 src_uv_buf/dst_uv_buf 或让 HAL 推导;UV stride 默认等于宽度

常见错误:改了旋转角度但目标 stride 还按源 stride 填。例如 640x480 转 90° 后目标 宽 480,stride 应为 960;若沿用源的 1280,输出在内存里变成「每行 960 字节数据 + 320 字节空隙」,下游按 960 读会看到斜切花屏,而且按 960×640 分配的缓冲会被写越界 (最后一行写到 639×1280+960 ≈ 819 KB,远超 614,400)。

7.8 连续处理多帧

HAL 支持「一帧完成后再次 start」:

for (;;) {
    src = 取下一帧();
    if (src == NULL) {
        break;
    }

    cfg.src_buf = src;                       /* 地址可以每帧变 */
    (void)hal_i2d_layout_get(&cfg, &layout);
    im_cache_data_clean_by_addr(cfg.src_buf, layout.src_buffer_size);
    im_cache_data_clean_invalidate_by_addr(cfg.dst_buf, layout.dst_buffer_size);

    ret = hal_i2d_start(&s_i2d, &cfg);
    if (ret != IM_OK) {
        break;
    }
    ret = i2d_wait_done(&s_i2d, 500U);
    if (ret != IM_OK) {
        /* 单帧失败不必重建句柄:stop + error_clear 后继续 */
        (void)hal_i2d_stop(&s_i2d);
        (void)hal_i2d_error_clear(&s_i2d,
                HAL_I2D_ERROR_TIMEOUT | HAL_I2D_ERROR_OUTSTANDING);
        continue;
    }
    im_cache_data_invalidate_by_addr(cfg.dst_buf, layout.dst_buffer_size);
    /* 下游消费 dst */
}

要点:

  • 每帧的源地址变化时只需改 cfg.src_buf;宽高/格式/旋转不变时 layout 每次结果相同。

  • 目标 buffer 被下游占用时,要么等下游用完再写,要么用双缓冲轮换。

  • 单帧失败可以恢复,不必 deinit/init;只有时钟或硬件彻底挂掉才需要重建。

7.9 LL 的等待与错误处理

/* 三个事件都放行;只关心帧完成与超时就删掉 OUTSTANDING(LL 示例就是这么做的) */
#define ENABLED_MASK (I2D_IRQ_FRAME_DONE_MASK | I2D_IRQ_TIMEOUT_MASK | \
                      I2D_IRQ_OUTSTANDING_MASK)

static error_code_t i2d_ll_wait_done(uint32_t timeout_ms, uint32_t *status_out)
{
    uint32_t start_ms = im_time_get_ms();

    for (;;) {
        uint32_t status = ll_i2d_get_irq_status(I2D) & ENABLED_MASK;

        if (status != 0U) {
            ll_i2d_clear_irq(I2D, status);       /* 读不清除,必须显式清 */
            if (status_out != NULL) {
                *status_out |= status;
            }
            if ((status & I2D_IRQ_TIMEOUT_MASK) != 0U) {
                return IM_ERR_TIMEOUT;
            }
            if ((status & I2D_IRQ_OUTSTANDING_MASK) != 0U) {
                return IM_ERR_HARDWARE;
            }
            return IM_OK;                        /* FRAME_DONE */
        }
        if ((uint32_t)(im_time_get_ms() - start_ms) >= timeout_ms) {
            return IM_ERR_TIMEOUT;
        }
        im_time_delay_us(100U);
    }
}
  • 轮询时用已使能掩码过滤,和 HAL 的做法一致;被屏蔽的位仍会锁存,但按约定不处理。

  • 失败时把原始 irq_state 和 ll_i2d_get_control(I2D) 打出来,能区分「没启动」与 「启动了但没完成」。

  • 收尾:ll_i2d_stop() → ll_i2d_disable_irq(I2D_IRQ_ALL) →ll_i2d_clear_irq(I2D_IRQ_ALL) → ll_i2d_disable_clock_gate() → 重新置上 AON stop 位。

7.10 自检:怎么确认真的转对了

三个示例都内置了输出校验:每行、每列各取 8 个点,按 §6.9 的映射把输出坐标反查回 源坐标,逐字节比较。

自己写校验时注意:

  • NV12 只比较 Y 平面(bpp 当 1 用),UV 平面不在抽样范围内。

  • YUV422 的色度由相邻像素共享,旋转/镜像后逐字节比较不可靠。示例的做法是填 YUYV(每个 Y 唯一、U/V 固定),只校验 Y。

  • 采样步长取 out_w / 8(out_w <= 8 时取 1),行、列共用;高很小而宽很大时, 纵向也会只抽到很少几行,必要的话给行单独算步长。

  • 校验函数按默认 stride(width * bpp / out_w * bpp)定位像素;用了自定义 stride 时要同步改成 layout 里的值。

  • 校验失败时打印首个不一致的 dst、src 坐标与字节号,比打印「mismatch」有用得多。

8. 示例程序解读

三个示例结构相同,只是「谁负责等待、谁负责时钟」不同:

文件层级等待方式时钟布局入口
i2d_blocking_transform_example.cHAL任务里轮询 hal_i2d_irq_handler()HALHALexample_i2d_blocking_transform_run()
i2d_callback_transform_example.cHALIRQ_I2D + HAL 回调HALHALexample_i2d_callback_transform_run()
i2d_ll_example.cLL轮询 irq_state自己开/关自己算example_i2d_ll_run()

8.1 共同结构

每个文件都按同样的顺序分段:

  1. Tunables:EXAMPLE_I2D_WIDTH/HEIGHT、_FORMAT/_DATA_TYPE、_ROTATE、_HORIZONTAL_MIRROR、_VERTICAL_FLIP、_TIMEOUT_MS。改参数只改这一段。

  2. Buffers:原始分配指针 + 对齐后的指针;alloc_buffers(size) 申请size + 64 再向上对齐。

  3. Geometry:输出宽高、每像素字节数、帧大小(HAL 两个示例用 layout,LL 示例自己算)。

  4. Pattern / verify:填渐变图案;map_dst_to_src();抽样比对。

  5. Run single:配置 → 分配 → 填充 → cache → 启动 → 等待 → invalidate → 校验。

  6. Entry:初始化 → run single → 清理 → 打印结果。

默认参数三个示例都是 1280x1280 ARGB8888、顺时针 90°:单帧 6,553,600 字节, 源/目标两帧共约 13.1 MB。

8.2 HAL 轮询示例

code/examples/hal/i2d_blocking_transform_example.c

入口:

error_code_t example_i2d_blocking_transform_run(void)
{
    hal_i2d_handle_t hi2d;
    hal_i2d_init_config_t init;
    error_code_t ret;
    bool i2d_inited = false;
    bool verify_failed = false;

    (void)memset(&hi2d, 0, sizeof(hi2d));

    ret = hal_i2d_init_config_default(&init);
    if (ret != IM_OK) {
        return ret;
    }
    init.instance = I2D;

    ret = hal_i2d_init(&hi2d, &init);
    if (ret != IM_OK) {
        return ret;                        /* 时钟/参数错误,无需收尾 */
    }
    i2d_inited = true;

    ret = example_i2d_run_single(&hi2d, &verify_failed);
    if ((ret == IM_OK) && verify_failed) {
        /* 硬件完成了,但抽样比对不一致:保留成功码,只告警 */
        IM_LOGW("I2D HAL example: output verify mismatch");
    }

    example_i2d_free_buffers();
    if (i2d_inited) {
        (void)hal_i2d_stop(&hi2d);
        (void)hal_i2d_deinit(&hi2d);
    }
    return ret;
}

单帧流程(example_i2d_run_single()):

步骤代码说明
1hal_i2d_config_default(&config)取默认值
2填宽高/格式/旋转/镜像/翻转来自 Tunables
3example_i2d_frame_size_get()用占位地址调 layout_get,取 max(src,dst)
4example_i2d_alloc_buffers(frame_size)两块 64 字节对齐缓冲
5example_i2d_fill_source() / example_i2d_fill_yuv422() + memset(dst, 0xA5)源图与目标初值
6im_cache_data_clean_by_addr(src) + clean_invalidate(dst)调用方的责任
7hal_i2d_start()启动
8example_i2d_wait_done()轮询
9im_cache_data_invalidate_by_addr(dst)读结果前
10example_i2d_verify()抽样校验

等待函数是本示例最值得看的点:

static error_code_t example_i2d_wait_done(hal_i2d_handle_t *hi2d, uint32_t timeout_ms)
{
    uint32_t start_ms = im_time_get_ms();

    for (;;) {
        hal_i2d_state_t state = HAL_I2D_STATE_RESET;

        hal_i2d_irq_handler(hi2d);       /* 读并确认外设状态 */
        (void)hal_i2d_state_get(hi2d, &state);

        if (state == HAL_I2D_STATE_READY) {
            return IM_OK;
        }
        if (state == HAL_I2D_STATE_TIMEOUT) {
            return IM_ERR_TIMEOUT;
        }
        if (state == HAL_I2D_STATE_ERROR) {
            return IM_ERR_HARDWARE;
        }
        if ((uint32_t)(im_time_get_ms() - start_ms) >= timeout_ms) {
            return IM_ERR_TIMEOUT;
        }
        im_time_delay_us(100U);
    }
}

为什么没注册中断也能工作:

  • irq_state 的锁存与 PLIC 是否使能无关;只要外设掩码放行,hal_i2d_irq_handler()就能读到并清除 FRAME_DONE。

  • hal_i2d_irq_handler() 本身不是 ISR 专用函数——它只是「读状态、清状态、更新 HAL 状态、调回调」,在任务里调用效果相同。

  • 默认只放行 FRAME_DONE 与 TIMEOUT;outstanding 被屏蔽,所以这个示例不会因为 outstanding 进入 ERROR,只会在真正超时后由软件超时退出。

8.3 HAL 回调示例

code/examples/hal/i2d_callback_transform_example.c

相比轮询示例,多了三块:

/* 任务与中断之间共享的标志 */
static volatile bool     s_example_i2d_done;
static volatile uint32_t s_example_i2d_events;

/* HAL 回调:中断上下文,只置标志 */
static void example_i2d_complete_callback(hal_i2d_handle_t *hi2d,
                                          hal_i2d_event_t event,
                                          void *user_ctx)
{
    (void)hi2d;
    (void)user_ctx;
    s_example_i2d_events |= (uint32_t)event;
    s_example_i2d_done = true;
}

/* SoC 中断入口:把 HAL 句柄当 user_ctx 传进来 */
static void example_i2d_irq_entry(uint32_t irq_id, void *user_ctx)
{
    (void)irq_id;
    hal_i2d_irq_handler((hal_i2d_handle_t *)user_ctx);
}

/* 注册并使能 IRQ_I2D */
ret = im_irq_request((uint32_t)IRQ_I2D, example_i2d_irq_entry, "hal_i2d", hi2d);
ret = im_irq_enable((uint32_t)IRQ_I2D);

等待改成等标志:

while (!s_example_i2d_done) {
    if ((uint32_t)(im_time_get_ms() - start_ms) >= timeout_ms) {
        return IM_ERR_TIMEOUT;
    }
}

if ((s_example_i2d_events & (HAL_I2D_EVENT_TIMEOUT |
                             HAL_I2D_EVENT_OUTSTANDING)) != 0U) {
    return IM_ERR_HARDWARE;
}
if ((s_example_i2d_events & HAL_I2D_EVENT_FRAME_DONE) == 0U) {
    return IM_ERR_NOT_READY;
}

要点:

  • 启动前先清 s_example_i2d_events / s_example_i2d_done,避免上一帧的事件被误认。

  • 回调可能一次入口触发多次,所以用 |= 位图,而不是只存最后一个 event。

  • 清理顺序:先关中断(im_irq_disable + im_irq_release),再注销回调, 最后 hal_i2d_stop() + hal_i2d_deinit(),防止回调访问已失效的句柄。

  • 这个示例的校验对所有格式都逐字节比较;如果把它改成 YUV422,请改用轮询示例的 luma 校验,否则色度共享会造成假失败。

  • 示例的等待是忙等。正式应用应在回调里释放信号量、任务里阻塞等待。

8.4 LL 示例

code/examples/ll/i2d_ll_example.c

它做了四件 HAL 本来会做的事:

  1. 时钟:AON HP1 stop 位 + 模块 clk_gate;

  2. 几何:example_i2d_layout_build() 自己算 stride、平面大小、UV 地址;

  3. 等待:直接读 irq_state;

  4. cache:调用方维护。

初始化:

static error_code_t example_i2d_ll_init(void)
{
    ll_aon_hpclkgr1_clear_stop_mask(AON_CLKGR_STOP_MASK(AON_HPCLKGR1_I2D_BIT));
    ll_i2d_enable_clock_gate(I2D);
    ll_i2d_safe_reset(I2D);
    ll_i2d_set_outstanding_limit(I2D, EXAMPLE_I2D_OUTSTANDING_LIMIT);
    ll_i2d_set_timeout_value(I2D, UINT32_MAX);
    ll_i2d_enable_timeout_restart(I2D);
    ll_i2d_set_irq_mask(I2D, EXAMPLE_I2D_IRQ_ALL_MASK & ~EXAMPLE_I2D_IRQ_ENABLED_MASK);
    ll_i2d_clear_irq(I2D, EXAMPLE_I2D_IRQ_ALL_MASK);
    return IM_OK;
}

EXAMPLE_I2D_IRQ_ALL_MASK = 三个事件位;EXAMPLE_I2D_IRQ_ENABLED_MASK =FRAME_DONE | TIMEOUT,所以写进掩码的只有 OUTSTANDING 位——outstanding 被屏蔽, 但读状态时仍按使能掩码过滤。

每帧启动:

static void example_i2d_ll_start(const example_i2d_layout_t *layout)
{
    ll_i2d_set_mode(I2D, EXAMPLE_I2D_DATA_TYPE, EXAMPLE_I2D_ROTATE,
                    EXAMPLE_I2D_HORIZONTAL_MIRROR, EXAMPLE_I2D_VERTICAL_FLIP);
    ll_i2d_set_image_size(I2D, EXAMPLE_I2D_WIDTH, EXAMPLE_I2D_HEIGHT);
    ll_i2d_set_source_addr(I2D, layout->src_y_addr, layout->src_uv_addr);
    ll_i2d_set_destination_addr(I2D, layout->dst_y_addr, layout->dst_uv_addr);
    ll_i2d_set_source_stride(I2D, layout->src_y_stride, layout->src_uv_stride);
    ll_i2d_set_destination_stride(I2D, layout->dst_y_stride, layout->dst_uv_stride);
    ll_i2d_update_shadow(I2D);

    ll_i2d_clear_irq(I2D, EXAMPLE_I2D_IRQ_ALL_MASK);
    ll_i2d_safe_reset(I2D);
    ll_i2d_start(I2D);
}

单帧流程里还有一个容易忽略的细节:分配 buffer 之后要用真实地址重建一次 layout, 因为第一次构建时地址还是 0:

ret = example_i2d_layout_build(&layout);       /* 只为算大小 */
frame_size = max(layout.src_buffer_size, layout.dst_buffer_size);
ret = example_i2d_alloc_buffers(frame_size);
ret = example_i2d_layout_build(&layout);       /* 真实地址,供寄存器使用 */

收尾:

static void example_i2d_ll_deinit(void)
{
    ll_i2d_stop(I2D);
    ll_i2d_disable_irq(I2D, EXAMPLE_I2D_IRQ_ALL_MASK);
    ll_i2d_clear_irq(I2D, EXAMPLE_I2D_IRQ_ALL_MASK);
    ll_i2d_disable_clock_gate(I2D);
    ll_aon_hpclkgr1_set_stop_mask(AON_CLKGR_STOP_MASK(AON_HPCLKGR1_I2D_BIT));
}

8.5 三者对照

维度HAL 轮询HAL 回调LL
需要 CONFIG_DRIVER_I2D是是头文件即可(运行时仍靠寄存器)
需要 CPM HAL是(HAL 内部用)是否(直接操作 AON)
需要 IRQ 框架否是否(示例轮询)
布局推导HALHAL自己算
参数校验HALHAL无
cache 维护自己做自己做自己做
单帧失败恢复stop + error_clear同左重新配置并启动
最适合bring-up、单帧验证正式业务寄存器调试、测试项

8.6 构建与运行

三个示例都不在默认构建里,按 §3.4 把它们接到一个 app 目录(SDK_APP_DIR)。

以 HAL 轮询示例为例,假设你的 app 目录是 apps/i2d_hal_demo/:

apps/i2d_hal_demo/
├── CMakeLists.txt
├── main.c
└── g32s10_v1_i2d_hal_demo_defconfig

CMakeLists.txt 见 §3.4;main.c 只管调用入口。defconfig 可从code/middleware/i2d/example/i2d_example_defconfig 复制(已包含CONFIG_DRIVER_I2D=y、CONFIG_DRIVER_CPM=y、CONFIG_DRIVER_IRQ=y、FreeRTOS 与 UART 控制台);不需要中间件的话可以把 CONFIG_MIDDLEWARE_I2D 关掉。

构建:

cd <SDK 根目录>

tools/imde/bin/imde use-defconfig apps/i2d_hal_demo/g32s10_v1_i2d_hal_demo_defconfig
tools/imde/bin/imde -D SDK_APP_DIR=apps/i2d_hal_demo configure
tools/imde/bin/imde build
  • SDK_APP_DIR 必须传,否则 configure 只编译 code/,示例根本不在构建里, 而且不会报错。判据:grep -c i2d_blocking_transform_example build/compile_commands.json非 0。

  • 切换 SDK_APP_DIR 前先 imde fullclean -y,否则可能残留上一个 app 的目标文件。

  • 烧录用 build/artifacts/spl_with_zero.bin;裸的 g32s10_v1.bin 没有 SPL, 板子起不来。

  • 串口 115200;示例不依赖任何外设,任何能跑这份 SDK 的板子都可以。

8.7 预期日志

HAL 轮询:

I2D HAL single case: 1280x1280 ARGB8888 ROTATE_90 mirror=0 flip=0 -> 1280x1280, frame=6553600 B x2
I2D HAL single case done: verify=OK
I2D HAL blocking transform example finished

HAL 回调:

I2D HAL callback case: 1280x1280 ARGB8888 ROTATE_90 mirror=0 flip=0 -> 1280x1280, frame=6553600 B x2
I2D HAL callback case done: verify=OK
I2D HAL callback transform example finished

LL:

I2D LL single case: 1280x1280 ARGB8888 ROTATE_90 mirror=0 flip=0 -> 1280x1280, src_stride=5120 dst_stride=5120 frame=6553600 B x2
I2D LL single case done: verify=OK
I2D LL transform example finished
日志含义
frame=6553600 B x2一帧 1280x1280 ARGB8888 = 6,553,600 字节,持有源/目标两帧
src_stride / dst_stride只有 LL 示例打印;90° 旋转后两者可能不同
verify=OK抽样比对通过
verify=MISMATCH比对失败,前一行会给出首个不一致的坐标
output verify mismatch入口看到 verify_failed 后的总结告警,与上一行成对出现
alloc ... failed, ret=内存不足
start failed, ret= / wait failed, ret=... state=... err=...启动或等待失败,按返回码与状态排查
init failed, ret=初始化失败(含 CPM 时钟错误)

verify=MISMATCH 不改退出码:硬件确实完成了,只是数据不对。自动化脚本要同时看 日志与返回值。

8.8 内存账与返回值

示例同时持有源、目标两帧,容量完全由 heap 余量决定(堆是 bss 之后到 DDR 顶端的 剩余空间,随板级/链接配置而变;示例文件头按「约 15.8 MB 堆」估算):

配置单帧两帧
640x480 YUV422614,4001.2 MB
1920x1080 NV123,125,7606.2 MB
1280x1280 ARGB8888(默认)6,553,60013.1 MB
1920x1080 ARGB88888,294,40016.6 MB
3840x2160 NV1212,441,60024.9 MB
3840x2160 ARGB888833,177,60066 MB

分配失败是 heap 的限制,不是 I2D 的限制;大分辨率时要先看链接脚本给堆留了多少。

三个示例的入口都返回 error_code_t:初始化、分配或变换失败时非 0;变换完成但抽样 不一致时仍为 0。

9. 集成到你自己的工程

9.1 不要和 im_i2d 同时驱动 I2D

中间件与直接调 HAL 都会管理同一份硬件资源:

资源im_i2d直接调 HAL
CPM 时钟门在 init/deinit 中开关同左(HAL 内部)
外设中断门每次 process 打开/收尾屏蔽由 init/irq_config 决定
IRQ_I2D中间件申请/释放应用自己申请/释放
句柄中间件内部静态单例应用自己的 hal_i2d_handle_t
  • 同一时刻只用一条路径。混用会导致一方关闭时钟/释放 IRQ,另一方读到无意义状态。

  • 需要中间件的调度又不满足需求时,改中间件,而不是在应用里再开一条 HAL 路径。

  • hal_i2d_layout_get() 是纯软件函数,可以和中间件共存,用来预先算 buffer 大小。

9.2 用 HAL 旋转相机帧

相机中间件与 I2D 是两条独立的硬件链路(VIC/ISP → WDMA 由 camera 管,I2D 归本驱动), 可以直接组合。几何来自相机状态而不是硬编码:

#include "im_camera_vic.h"
#include "hal_i2d.h"
#include "im_cache.h"
#include "im_time.h"

static hal_i2d_handle_t s_i2d;

static error_code_t rotate_camera_frame(void *src, uint32_t src_stride,
                                        uint32_t width, uint32_t height,
                                        void *dst, uint32_t dst_stride)
{
    hal_i2d_config_t cfg;
    hal_i2d_layout_t layout;
    error_code_t ret;

    (void)hal_i2d_config_default(&cfg);
    cfg.src_buf      = src;
    cfg.src_stride_y = src_stride;      /* 相机 status->line_length */
    cfg.dst_buf      = dst;
    cfg.dst_stride_y = dst_stride;      /* 旋转后的行字节数 */
    cfg.width        = width;           /* 源尺寸 */
    cfg.height       = height;
    cfg.format       = HAL_I2D_FORMAT_YUV422;
    cfg.rotate       = HAL_I2D_ROTATE_90;

    ret = hal_i2d_layout_get(&cfg, &layout);
    if (ret != IM_OK) {
        return ret;
    }

    im_cache_data_clean_by_addr(cfg.src_buf, layout.src_buffer_size);
    im_cache_data_clean_invalidate_by_addr(cfg.dst_buf, layout.dst_buffer_size);

    ret = hal_i2d_start(&s_i2d, &cfg);
    if (ret == IM_OK) {
        ret = i2d_wait_done(&s_i2d, 500U);
    }
    if (ret == IM_OK) {
        im_cache_data_invalidate_by_addr(cfg.dst_buf, layout.dst_buffer_size);
    }
    return ret;
}

调用侧从相机状态取几何(YUV422 转 90° 后目标宽 = 源高,stride = 源高 × 2):

camera_status_t status;
void *src;
void *dst = NULL;                               /* 按 layout 自行分配 */
error_code_t ret;

src = camera_wait_frame(cam, 1000U);            /* 取一帧 */
if (src == NULL) {
    /* 超时/未出流 */
}
(void)camera_get_status(cam, &status);

ret = rotate_camera_frame(src, status.line_length, status.width, status.height,
                          dst, status.height * 2U);   /* dst 由自己分配 */
(void)camera_put_frame(cam, src);               /* 转完即可归还 */

接线要点:

  • 源 stride 用相机状态里的 line_length,不要用 width * bpp 猜。

  • status.pixel_format 与 HAL_I2D_FORMAT_* 需要按需映射;相机侧也可能直接是 NV12(走 DMSC 时),此时还要填 src_uv_buf 并把 format 改成 NV12。

  • 源帧对齐:中间件分配的帧通常已经对齐;若自己分配,按 64 字节对齐。

  • 转完再 camera_put_frame(),且目标 buffer 是独立的一块(不要把旋转结果写回相机帧)。

  • 相机帧是 DMA 写的,I2D 读之前 clean 到内存。

  • 完整用例(含自测、每帧流程、下游 JPEGE/UVC)见test/g32s10/module/vic/slt_103/gc0308_i2d_jpeg_usb/;它的 I2D 部分走中间件, 但几何与 cache 的处理方式相同。

9.3 多帧流水与双缓冲

I2D 是单实例、单帧引擎,流水只能靠 buffer 轮换来提高吞吐:

源帧 A -> I2D -> 目标 0 -> 下游编码/显示
源帧 B -> I2D -> 目标 1 -> 下游

约束:

  • 同一块目标 buffer 在 I2D 写它期间不能被下游读。

  • 下游读之前必须 invalidate;下游读完、I2D 复用之前要做 clean+invalidate(如果 下游是 CPU)。

  • 源帧在 I2D 完成前不能归还给相机/上游;等 wait_done 返回后再 camera_put_frame()。

10. 调试与验证

10.1 可观测性

观察点手段说明
HAL 状态hal_i2d_state_get()区分 BUSY(没完成)/TIMEOUT/ERROR
错误位图hal_i2d_error_get()FRAME_DONE 覆盖状态时仍能看出错误
外设状态ll_i2d_get_irq_status(I2D)原始 IRQ_STATE,含被屏蔽的位
外设掩码ll_i2d_get_irq_mask(I2D)确认 FRAME_DONE 是否被放行
控制寄存器ll_i2d_get_control(I2D)START 是否真置上
配置回读ll_i2d_get_mode() / ll_i2d_get_image_size() 等确认 shadow 已提交
版本ll_i2d_get_version(I2D)确认寄存器窗口可访问

注意:CTRL.START 由软件置位、由 hal_i2d_stop() / ll_i2d_stop() 清除,读回它 只能确认启动请求确实写进了硬件;是否处理完成仍以 IRQ_STATE 或 HAL 状态为准。

10.2 仓库里的 I2D 测试用例

test/g32s10/module/i2d/(独立 test/ 仓库):

目录内容
register/寄存器读写遍历(i2d_reg_rw_test.c),经 regtest_adapter.h 上板
reset/复位与超时自动重启
function/五种排布(NV12/ARGB8888/RGB565/RAW8/YUV422)的功能测试
robustness/整个寄存器域遍历读,确认不会卡死
outstanding/outstanding 上限与总线带宽的关系
outstanding_irq/outstanding 中断的产生、清除与屏蔽
stress/1280x720 ARGB8888 旋转压测

这些用例是验证 HAL/LL 行为最直接的参考。

10.3 上板诊断顺序

  1. ll_i2d_get_version(I2D) 能否读到非全 0/全 F 的值——不行先查时钟与门控。

  2. hal_i2d_init() 的返回值——CPM 错误说明总线门没开成。

  3. hal_i2d_start() 的返回值——INVALID_PARAM 按 §6.4 对照参数;BUSY 说明上一 帧没结束。

  4. 启动后读 ll_i2d_get_control(),确认 START 置位。

  5. 完成不了时读 ll_i2d_get_irq_status():

    • 有 FRAME_DONE 但 HAL 没动:hal_i2d_irq_handler() 没被调用,或掩码被关。

    • 只有 TIMEOUT:硬件超时(通常是 timeout_value 配得太小或总线被卡)。

    • 只有 OUTSTANDING:总线拥塞 / outstanding 太小。

    • 什么都没有:根本没启动,或在等一个不会来的事件。

  6. 完成后数据不对:先按 §6.9 校验镜像/翻转顺序,再查 stride 与 NV12 UV 地址, 最后查 cache(§6.7)。

11. 常见问题排查

现象可能原因处理
hal_i2d_init 返回 CPM 错误时钟树/CPM 配置不对检查 HAL_CPM_ID_GATE_I2D 的时钟源与门控
hal_i2d_start 返回 IM_ERR_INVALID_PARAM宽高越界/未对齐、stride 太小或超 0xFFFF、非 NV12 却给了 UV 字段、格式/旋转非法按 §6.4 逐项对照;读 hal_i2d_error_get()
hal_i2d_start 返回 IM_ERR_BUSY上一帧未完成等待完成,或 hal_i2d_stop()
一直等不到完成,状态停在 BUSY没调 hal_i2d_irq_handler();ISR 没注册/没使能;掩码把 FRAME_DONE 关了轮询模式确认等待循环调了 handler;中断模式查 IRQ_I2D 与 irq_mask
状态变 TIMEOUT硬件超时检查 timeout_value、总线是否被其它主设备卡住
状态变 ERRORoutstanding 中断调大 outstanding_limit(1..255),或先查总线拥塞;注意默认掩码不放行它
图像整体错位/斜切stride 用了源行宽或像素数目标 stride 用 layout 的 dst_y_stride(字节)
90°/270° 输出被裁掉或越界目标 buffer 按源大小分配按 dst_buffer_size 分配;注意源/目标大小可能不同
右/下边缘出现上一帧残影目标 stride 大于实际行宽但没按布局写;或 buffer 太小核对 layout;不要手写 stride
输出是上一次的图/全乱漏写 SHD_CTRL = UPDATE_ALL(LL)在编程之后、START 之前提交 shadow
图像花、块状错乱、偶发正确cache 没维护或 buffer 未对齐按 §6.7 做 clean / clean+invalidate / invalidate;64 字节对齐
NV12 颜色错乱/上下半不对UV 地址或 UV stride 错用 layout.src_uv_addr / dst_uv_addr;注意 Y 平面 16 行对齐
旋转方向与预期相反把 ROTATE_90 当成逆时针编码是顺时针;按 §6.9 校验
镜像是上下而不是左右mirror 与 flip 弄反MIRR = 水平(左右),FLIP = 垂直(上下)
回调没被调用回调没注册;HAL 掩码关了;SoC IRQ 没使能;IRQ 号不对依次检查 callback_register、irq_mask、im_irq_enable、IRQ_I2D
回调里崩溃/日志乱在中断上下文打日志或阻塞回调只置标志,处理放任务里
每帧只成功一次,第二次 BUSY第一次完成后没清事件或没等状态回 READY确认 FRAME_DONE 被 hal_i2d_irq_handler() 处理
读寄存器全 0 或全 F时钟/门控没开,或实例指针不对先开 AON 门与 clk_gate;确认用 I2D 实例

12. 约束与注意事项

  • 只有 I2D 一个实例,没有多通道;同一时刻只能有一帧在跑。

  • 源/目标格式必须相同;不做缩放、不做色彩空间转换。

  • width/height 永远填源尺寸;输出宽高由旋转角度推出。

  • 宽高范围 8..3840 / 8..2160,且必须满足格式对齐(ARGB8888=2、RGB565/YUV422=4、 NV12/RAW8=8)。

  • stride 是 16 位字段,超过 0xFFFF 的配置 HAL 直接拒绝;LL 会静默截位。

  • HAL 每帧都先 safe_reset 再 START;不要在同一帧未完成时重复 start。

  • 超时/outstanding 后状态停在 TIMEOUT/ERROR,先 hal_i2d_error_clear() 或hal_i2d_stop() 才能再次启动。

  • 同一 IRQ 内 FRAME_DONE 最后处理,会把状态改写为 READY;判断是否出错必须读 错误位图。

  • hal_i2d_irq_config() 只改外设中断门,被屏蔽的源仍锁存;SoC IRQ 由应用负责。

  • 中断回调运行在 IRQ 上下文,可能一次入口多次触发;不要只保存最后一个 event。

  • HAL 不做 cache 维护、不检查 buffer 容量;按 layout 的 *_buffer_size 分配, 并按 §6.7 维护 cache。

  • NV12 的 Y 平面按 16 行对齐,UV 偏移必须用对齐后的行数计算。

  • 默认 timeout_value = UINT32_MAX、timeout_restart = true,硬件超时实际上很宽; 需要精确超时在应用侧做软件等待。

  • LL 只做字段截位,不做语义校验;参数错误会被原样写进硬件。

  • 直接使用 LL 时必须自己打开两级时钟,并在收尾时恢复 AON stop 位。

  • 不要同时用 im_i2d 与直接 HAL 驱动同一路 I2D。