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.h | LL 层 static inline 接口 |
code/include/drivers/g32s10/i2d/hal_i2d.h | HAL 类型、宏与接口声明 |
code/drivers/g32s10/i2d/hal_i2d.c | HAL 实现:校验、布局、寄存器编程、中断分发 |
code/drivers/g32s10/i2d/Kconfig | 配置开关 CONFIG_DRIVER_I2D |
code/drivers/g32s10/i2d/CMakeLists.txt | 组件注册 |
code/examples/hal/i2d_blocking_transform_example.c | HAL 示例:轮询等待 |
code/examples/hal/i2d_callback_transform_example.c | HAL 示例:IRQ_I2D + 回调 |
code/examples/ll/i2d_ll_example.c | LL 示例:直接操作寄存器 |
test/g32s10/module/i2d/ | 寄存器/复位/功能/压力等测试用例(独立 test/ 仓库) |
术语:
| 术语 | 含义 |
|---|---|
| 一次变换(transform) | 一次 I2D 硬件作业:一张源图旋转/镜像/翻转后写入一块目标缓冲 |
| 源 / 目标(src / dst) | 读入的图像缓冲 / 写出的图像缓冲。地址由调用方提供 |
| 平面(plane) | 图像里一块连续的分量区。打包格式只有 1 个平面;NV12 有 Y、UV 两个 |
| stride(行跨距) | 一行像素在内存里占多少字节(不是像素),可以大于图像宽度 |
| shadow(影子寄存器) | 一类只有写 SHD_CTRL 后才生效的配置寄存器 |
| 安全复位(SAFE_RST) | 清内部流水线状态、但保留寄存器配置的复位 |
| outstanding | AXI 读请求的在途数量上限,用于限制突发带宽 |
| 完成事件 | FRAME_DONE / TIMEOUT / OUTSTANDING 三个硬件事件 |
2. 分层关系与依赖
2.1 分层与调用方向
负责 SoC cache 维护和业务侧完成通知
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_IOBASE | 0x13330000 | code/soc/g32s10/include/base.h |
实例指针 I2D | (i2d_regs_t *)(uintptr_t)I2D_IOBASE | i2d_regs.h |
| SoC 中断 | IRQ_I2D = IRQ_PLIC0_START + 21 | irq_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 |
| 实例数 | 1 | ll_i2d_is_instance() |
时钟分两级:AON 的 HP1 总线门控(stop 位)与模块内部门控(
clk_gate寄存器)。 HAL 会两级都打开;LL 用户必须自己打开两级(见 §6.8)。I2D 只用
IRQ_I2D一根中断线;PLIC 侧的实际使能是应用的事。
4.2 寄存器映射
4.2.1 寄存器列表
| 寄存器 | 访问 | 偏移 | 说明 |
|---|---|---|---|
version | R | 0x000 | RTL 版本号 |
shd_ctrl | RW | 0x008 | Shadow 配置更新控制 |
ctrl | RW | 0x010 | 复位与启动控制 |
img_size | RW | 0x014 | 源图像宽高 |
mode | RW | 0x018 | 像素格式、旋转、镜像、翻转 |
outstanding | RW | 0x01C | AXI 读 outstanding 上限 |
src_addr_y | RW | 0x020 | 源 Y 平面地址 |
src_addr_uv | RW | 0x024 | 源 UV 平面地址 |
src_stride_y | RW | 0x028 | 源 Y 平面 stride |
src_stride_uv | RW | 0x02C | 源 UV 平面 stride |
dst_addr_y | RW | 0x030 | 目标 Y 平面地址 |
dst_addr_uv | RW | 0x034 | 目标 UV 平面地址 |
dst_stride_y | RW | 0x038 | 目标 Y 平面 stride |
dst_stride_uv | RW | 0x03C | 目标 UV 平面 stride |
irq_state | R | 0x080 | 中断状态(锁存) |
irq_clear | W | 0x084 | 中断状态清除(写 1 清对应位) |
irq_mask | RW | 0x088 | 中断屏蔽(1 = 屏蔽) |
timeout_value | RW | 0x090 | 硬件超时上限 |
timeout_mode | RW | 0x094 | 硬件超时模式(自动重启) |
clk_gate | RW | 0x098 | 模块内部时钟门控 |
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 编码:
| 值 | 宏 | 格式 |
|---|---|---|
0 | I2D_MODE_DATA_TYPE_NV12 | NV12(Y + 交织 UV,4:2:0) |
1 | I2D_MODE_DATA_TYPE_RAW8 | RAW8 / Y8 单平面 |
2 | I2D_MODE_DATA_TYPE_RGB565 | RGB565 packed |
3 | I2D_MODE_DATA_TYPE_ARGB8888 | ARGB8888 packed |
4 | I2D_MODE_DATA_TYPE_YUV422 | YUV422 packed |
ROTATE_MODE 编码:
| 值 | 宏 | 角度 |
|---|---|---|
0 | I2D_MODE_ROTATE_0 | 0° |
1 | I2D_MODE_ROTATE_90 | 顺时针 90° |
2 | I2D_MODE_ROTATE_180 | 180° |
3 | I2D_MODE_ROTATE_270 | 顺时针 270°(= 逆时针 90°) |
4.2.6 OUTSTANDING(0x01C)
| 位 | 名称 | 说明 |
|---|---|---|
[7:0] | LIMIT | AXI 读 outstanding 上限 |
复位值 0x40(I2D_OUTSTANDING_LIMIT_DEFAULT)。字段 8 位,0 不是可配置值;
HAL 要求 1..255。
4.2.7 地址寄存器(0x020/0x024/0x030/0x034)
| 位 | 名称 | 说明 |
|---|---|---|
[31:0] | ADDR | Y/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] | OUTSTANDING | outstanding 上限中断 |
[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 复位值 | 0x40 | i2d_regs.h |
hal_i2d_init_config_t::outstanding_limit 默认值 | 0x40 | hal_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_0 | hal_i2d_config_default() |
4.3 状态机
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()
| 当前状态 | 事件 / 调用 | 新状态 |
|---|---|---|
RESET | hal_i2d_init() 成功 | READY |
READY | hal_i2d_start() | BUSY |
BUSY | FRAME_DONE | READY |
BUSY | TIMEOUT | TIMEOUT |
BUSY | OUTSTANDING | ERROR |
TIMEOUT / ERROR | hal_i2d_error_clear() 或 hal_i2d_stop() | READY |
任意(非 RESET) | hal_i2d_deinit() | RESET |
注意两点:
同一拍多个事件时,
FRAME_DONE最后处理,会把TIMEOUT/ERROR覆盖成READY。是否出错必须看hal_i2d_error_get(),不能只看状态。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);
校验顺序:
hi2d、init非空;init->instance必须等于I2D;outstanding_limit必须非 0 且<= 0xFF。
随后执行:
清零句柄,绑定
instance,保存init副本;hal_cpm_clk_enable(HAL_CPM_ID_GATE_I2D);ll_i2d_enable_clock_gate()打开模块内部时钟;ll_i2d_safe_reset();写 outstanding 上限、硬件超时值,并按
timeout_restart置/清自动重启位;按三个
*_irq_enable组合使能掩码,向irq_mask写ALL & ~enabled;清除全部中断状态;
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);
校验句柄;要求
state == HAL_I2D_STATE_READY,否则返回IM_ERR_BUSY;内部调用
hal_i2d_layout_get()完成校验与布局推导;失败时把HAL_I2D_ERROR_INVALID_PARAM记入error_code,返回IM_ERR_INVALID_PARAM, 状态保持READY;进入临界区,依次:
编程
MODE(格式/旋转/镜像/翻转)、IMG_SIZE、源/目标 Y/UV 地址与 stride;写
SHD_CTRL = UPDATE_ALL;清除全部中断状态;
状态置
BUSY;ll_i2d_safe_reset();置
CTRL.START启动。退出临界区返回
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 中断时,也可以由任务轮询调用(示例就是 这么做的)。
读
irq_state并只保留三个有效位;求
status = latched & enabled,其中enabled = ALL & ~irq_mask;status == 0直接返回;只清除
status中已使能且已置位的位;按 TIMEOUT → OUTSTANDING → FRAME_DONE 的顺序处理并回调:
| 状态位 | 错误标志 | 状态迁移 | 回调 |
|---|---|---|---|
TIMEOUT | HAL_I2D_ERROR_TIMEOUT | TIMEOUT | HAL_I2D_EVENT_TIMEOUT |
OUTSTANDING | HAL_I2D_ERROR_OUTSTANDING | ERROR | HAL_I2D_EVENT_OUTSTANDING |
FRAME_DONE | 不改错误标志 | READY | HAL_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 一次变换的控制流
负责 SoC cache 维护和业务侧完成通知
6.2 寄存器编程顺序与 shadow / 安全复位
HAL 每帧的写序是固定的:
MODE→ 2.IMG_SIZE→ 3. 源地址 → 4. 目标地址 → 5. 源 stride →目标 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_gate | ll_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 + flip | ROTATE_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.c | HAL | 任务里轮询 hal_i2d_irq_handler() | HAL | HAL | example_i2d_blocking_transform_run() |
i2d_callback_transform_example.c | HAL | IRQ_I2D + HAL 回调 | HAL | HAL | example_i2d_callback_transform_run() |
i2d_ll_example.c | LL | 轮询 irq_state | 自己开/关 | 自己算 | example_i2d_ll_run() |
8.1 共同结构
每个文件都按同样的顺序分段:
Tunables:
EXAMPLE_I2D_WIDTH/HEIGHT、_FORMAT/_DATA_TYPE、_ROTATE、_HORIZONTAL_MIRROR、_VERTICAL_FLIP、_TIMEOUT_MS。改参数只改这一段。Buffers:原始分配指针 + 对齐后的指针;
alloc_buffers(size)申请size + 64再向上对齐。Geometry:输出宽高、每像素字节数、帧大小(HAL 两个示例用 layout,LL 示例自己算)。
Pattern / verify:填渐变图案;
map_dst_to_src();抽样比对。Run single:配置 → 分配 → 填充 → cache → 启动 → 等待 → invalidate → 校验。
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()):
| 步骤 | 代码 | 说明 |
|---|---|---|
| 1 | hal_i2d_config_default(&config) | 取默认值 |
| 2 | 填宽高/格式/旋转/镜像/翻转 | 来自 Tunables |
| 3 | example_i2d_frame_size_get() | 用占位地址调 layout_get,取 max(src,dst) |
| 4 | example_i2d_alloc_buffers(frame_size) | 两块 64 字节对齐缓冲 |
| 5 | example_i2d_fill_source() / example_i2d_fill_yuv422() + memset(dst, 0xA5) | 源图与目标初值 |
| 6 | im_cache_data_clean_by_addr(src) + clean_invalidate(dst) | 调用方的责任 |
| 7 | hal_i2d_start() | 启动 |
| 8 | example_i2d_wait_done() | 轮询 |
| 9 | im_cache_data_invalidate_by_addr(dst) | 读结果前 |
| 10 | example_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 本来会做的事:
时钟:AON HP1 stop 位 + 模块
clk_gate;几何:
example_i2d_layout_build()自己算 stride、平面大小、UV 地址;等待:直接读
irq_state;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 框架 | 否 | 是 | 否(示例轮询) |
| 布局推导 | HAL | HAL | 自己算 |
| 参数校验 | HAL | HAL | 无 |
| 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 YUV422 | 614,400 | 1.2 MB |
| 1920x1080 NV12 | 3,125,760 | 6.2 MB |
| 1280x1280 ARGB8888(默认) | 6,553,600 | 13.1 MB |
| 1920x1080 ARGB8888 | 8,294,400 | 16.6 MB |
| 3840x2160 NV12 | 12,441,600 | 24.9 MB |
| 3840x2160 ARGB8888 | 33,177,600 | 66 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 上板诊断顺序
ll_i2d_get_version(I2D)能否读到非全 0/全 F 的值——不行先查时钟与门控。hal_i2d_init()的返回值——CPM 错误说明总线门没开成。hal_i2d_start()的返回值——INVALID_PARAM按 §6.4 对照参数;BUSY说明上一 帧没结束。启动后读
ll_i2d_get_control(),确认START置位。完成不了时读
ll_i2d_get_irq_status():有
FRAME_DONE但 HAL 没动:hal_i2d_irq_handler()没被调用,或掩码被关。只有
TIMEOUT:硬件超时(通常是timeout_value配得太小或总线被卡)。只有
OUTSTANDING:总线拥塞 / outstanding 太小。什么都没有:根本没启动,或在等一个不会来的事件。
完成后数据不对:先按 §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、总线是否被其它主设备卡住 |
状态变 ERROR | outstanding 中断 | 调大 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。