Openvx & Tiovx技术杂谈
Openvx & Tiovx技术杂谈
Openvx & Tiovx (六) Host & Target
https://zhuanlan.zhihu.com/p/474701695
https://dev.ti.com/tirex/explore/node?node=ANd.gAKGXC97FboluTIMhw
Openvx 保留了代码移植到多核平台的可能性。可能是因为多核平台的种类繁多,Openvx 在定义时并没有引入太多与多核平台相关的概念,也没有多少与多核平台相关的 API。在我印象中与多核平台相关的 API 只有 vxSetNodeTarget() 和 vxSetImmediateModeTarget()。以 vxSetNodeTarget() 为例,该 API 用于设定节点在哪一个核上运行。
由于 Ti 公司的主流产品为多核平台,Tiovx 扩展 Openvx 的重点之一是实现图对多核资源的高效利用,于是 Host 和 Target 是 Tiovx 中非常重要的概念。
Host & Target 分别指什么
说实话,我觉得 Tiovx 的官方文档并没有很好地解释什么是 Host、什么是Target。根据 Tiovx 的源码以及我的实践经验,Host 和 Target 的概念可以如下概括:
- Host 和 Target 是针对具体的一个图而言的。
- 负责创建图的核为 Host,负责运行节点的核为 Target。
- 如果 Host 也会负责运行部分节点,那么该核同时也为 Target。
例如有核 A 和核 B,在核 A 上创建一个包含两个节点 N1 和 N2 的图 G,则针对该图 G 而言,核 A 为 Host。如果 N1 和 N2 均在 核 B 上运行,则针对该图 G 而言核 B 为 Target。如果 N1 在核 B 上运行,N2 在核 A 上运行,则针对该图 G 而言核 A 是 Host 的同时也是 Target。
核间通信及资源共享
在谈及多核平台时,须有如下概念:
- 每个核都会独立运行一个操作系统,如 Linux、QNX、RTOS。每个操作系统都可能运行多个进程/线程/任务。
- 保持系统之间的协作必须实现一个合适的通信协议。
Tiovx 并没有指定特定的通信协议,在代码中以 IPC (Inter-Processor Communication,处理器间通信) 代指。这很好理解,因为实现通信协议是其它模块的事,Tiovx 只负责调用相关接口。从源码中可以看出,Tiovx 模块发送的消息的载荷很短,仅包含 Target id 以及命令码两部分。命令主要包含:
- 图节点的创建。
- 图节点的析构。
- 图节点的控制。
- 图节点的执行。
- 图节点执行完毕后的回调处理。
此外,Tiovx 还专门创建一个线程或任务来处理通信消息。
以上内容可在源码 (下载地址见参考资料) 中的 vx_target.c 文件查找到。
Tiovx 采用共享内存的方式实现资源共享,该共享内存是外部物理内存的一块指定区域。Tiovx 会将该段共享内存的首地址保存在变量 gTivxObjDescShmEntry 中,该变量用于存储对象的描述符 —— 每个对象 (节点对象、图像对象等) 都有对应的一个描述符 (本质是一个结构体),描述符包含对象类型、对象数据存储空间的首地址、对象数据存储空间的大小等重要信息。即 Tiovx 共享的资源是对象的描述符,通过对象的描述符就可以访问对象的所有信息。
注意,不同操作系统使用不同的虚拟地址空间,即同样的一个地址,在不同虚拟地址空间中对应不同的物理地址。Tiovx 引入 shared_ptr 作为中间载体,这里假设 shared_ptr 为物理地址 (根据实际应用,也可以采用其它形式的地址)。在 Host 端,Tiovx 为对象分配数据存储空间之后,会自动调用 tivxMemHost2SharedPtr() 函数将存储空间的首地址 (Host 的虚拟地址) 转为物理地址。在 Target 端,如要访问对象数据存储空间,要主动调用 tivxMemShared2TargetPtr() 函数将物理地址转为 Target 端的虚拟地址。
核间通信及资源共享示意
参考文献链接
https://zhuanlan.zhihu.com/p/474701695
https://dev.ti.com/tirex/explore/node?node=ANd.gAKGXC97FboluTIMhw
Openvx & Tiovx (七) 自定义 Kernel (Tiovx 拓展)
https://zhuanlan.zhihu.com/p/476213104
https://software-dl.ti.com/jacinto7/esd/processor-sdk-rtos-jacinto7/09_02_00_05/exports/docs/psdk_rtos/docs/user_guide/index.html
https://link.zhihu.com/?target=https%3A//software-dl.ti.com/jacinto7/esd/processor-sdk-rtos-jacinto7/latest/exports/docs/tiovx/docs/user_guide/index.html
Target Kernel & Target Kernel Instance
Tiovx 在自定义 Kernel 上的拓展与如何在 Target 上执行节点是同源的,Tiovx 给出的解决方案如下:
- 引入 Target Kernel 对象以及 Target Kernel Instance 对象。
- Target Kernel 对象与节点的执行函数绑定。
- 原则上相关代码仅存在于 Target 上,除非 Host 同时也作为 Target。
- Target Kernel 对象以及 Target Kernel Instance 对象与 Context 解绑,目的是节省资源。
- Target Kernel 对象与 Target Kernel Instance 对象的关系正如 Kernel 对象与节点对象的关系,后者关系见我在 Openvx & Tiovx (三) 自定义 Kernel 文章中的描述。
- 采用独立的列表维护注册的 Target Kernel 对象,该列表在源码中为 g_target_kernel_table。
- 采用独立的列表维护创建的 Target Kernel Instance 对象,该列表在源码中为 g_target_kernel_instance_table。
Kernel 对象与 Target Kernel 对象的关系
Kernel 对象存在于 Host 上,回忆我在 Openvx & Tiovx (三) 自定义 Kernel 文章中的描述,在 Context 上注册的 Kernel 对象必须有唯一的名称及枚举值。
Target Kernel 对象存在于 Target 上,与 Kernel 类似,在 g_target_kernel_table 上注册的 Target Kernel 对象都必须有唯一的名称及枚举值。
不强制 Host 上的 Kernel 对象与 Target 上的 Target Kernel 对象一一对应。如果 Target 上某个 Target Kernel 对象正好与 Host 上某个 Kernel 对象有相同的名称及枚举值,则该 Kernel 对象生成的节点对象可以在 Target 上运行。
节点对象与 Target Kernel Instance 对象的关系
在 Tiovx 创建图之后 (调用 vxVerifyGraph() 函数之后), 节点对象的对象描述符会保存对应的 Target Kernel Instance 对象信息。Target 通过节点对象描述符获得 Target Kernel Instance 对象,再通过 Target Kernel Instance 对象间接调用对应的 Target Kernel 对象的执行函数,从而实现节点的执行。
(节点描述符是什么、Target 为何能获取 Host 上创建的节点描述符见 Openvx & Tiovx (六) Host & Target。)
Kernel 对象、节点对象、Target Kernel 对象、Target Kernel Instance 对象之间的关系
注册自定义 Kernel
Host 端
在 Host 端的注册方式与 Openvx 原生的注册方式几乎一致 (详见 Openvx & Tiovx (三) 自定义 Kernel) 除了:
- 调用 vxAddUserKernel() 函数时参数 func_ptr 为 NULL。Tiovx 中负责执行节点的是 Target,故在 Host 上注册时不需要提供执行函数。(注:实际上也可以提供执行函数,此时节点会在 Host 上执行。)
- 调用 vxFinalizeKernel() 函数完成注册前,需要调用 tivxAddKernelTarget() 函数设定能执行节点的 Target。可以多次调用该函数设定多个 Target。节点默认在第一个 Target 上执行,可以调用 vxSetNodeTarget() 函数修改执行节点的 Target。tivxAddKernelTarget() 函数的声明如下:
vx_status tivxAddKernelTarget(
vx_kernel kernel, /* 调用 vxAddUserKernel() 函数返回的 Kernel 对象 */
const char *target_name /* 支持执行对应节点的 Target 名称 */
);
此外,Tiovx 提供 tivxCreateNodeByKernelEnum() 函数及 tivxCreateNodeByKernelName() 函数,以快速通过 Kernel 对象的枚举值或名称生成对应的节点对象并与数据对象进行绑定。例如 Tiovx 封装 vxAndNode() 函数的代码只有如下几行:
vx_node vxAndNode(vx_graph graph, vx_image in1, vx_image in2, vx_image out)
{
/* 参数的顺序要与期望的一致 */
vx_reference params[] = {
(vx_reference)in1,
(vx_reference)in2,
(vx_reference)out,
};
/* VX_KERNEL_AND 是 Kernel 对象的枚举值 */
/* dimof 是 Tiovx 提供的宏函数,用于获取数组的元素个数 */
return tivxCreateNodeByKernelEnum(graph, (vx_enum)VX_KERNEL_AND, params, dimof(params));
}
Target 端
在 Target 端注册的流程为
- 实现 Target Kernel Instance 对象的初始化函数。初始化函数主要负责申请必要的动态内存。
- 实现 Target Kernel Instance 对象的析构函数。析构函数主要负责释放初始化函数申请的动态内存。
- 实现 Target Kernel Instance 对象的执行函数。
- 调用 tivxAddTargetKernel() 函数或 tivxAddTargetKernelByName() 函数完成注册。(二选一,建议选前者。)
Target Kernel Instance 对象的初始化函数、析构函数、执行函数的原型均为:
/*
* @param[in] kernel Target Kernel Instance 对象
* @param[in] obj_desc 参数的对象描述符列表,与节点对象绑定的数据对象一一对应
* @param[in] num_params 参数个数
* @param[in] priv_arg 可选参数
*/
typedef vx_status(*tivx_target_kernel_f)(tivx_target_kernel_instance kernel, tivx_obj_desc_t *obj_desc[], uint16_t num_params, void *priv_arg);
注:参数 obj_desc 是对象描述符列表,执行函数可以通过列表中的对象描述符直接访问数据,当然需要通过 tivxMemShared2TargetPtr() 函数进行地址空间的转换。
tivxAddTargetKernel() 函数及 tivxAddTargetKernelByName() 函数的声明如下
/*
* @param[in] kernel_id 枚举值,需要与对应的 Kernel 对象的枚举值相同
* @param[in] target_name 必须为注册所在的 Target 的名称
* @param[in] process_func 执行函数
* @param[in] create_func 初始化函数
* @param[in] delete_func 析构函数
* @param[in] control_func 控制函数,通常为 NULL,即不需要提供
* @param[in] priv_arg 额外参数,通常为 NULL,即不需要提供
*/
tivx_target_kernel tivxAddTargetKernel(
vx_enum kernel_id,
const char *target_name,
tivx_target_kernel_f process_func,
tivx_target_kernel_f create_func,
tivx_target_kernel_f delete_func,
tivx_target_kernel_control_f control_func,
void *priv_arg
);
/*
* @param[in] kernel_name 名称,需要与对应的 Kernel 对象的名称相同
* @param[in] target_name 必须为注册所在的 Target 的名称
* @param[in] process_func 执行函数
* @param[in] create_func 初始化函数
* @param[in] delete_func 析构函数
* @param[in] control_func 控制函数,通常为 NULL,即不需要提供
* @param[in] priv_arg 额外参数,通常为 NULL,即不需要提供
*/
tivx_target_kernel tivxAddTargetKernelByName(
const char *kernel_name,
const char *target_name,
tivx_target_kernel_f process_func,
tivx_target_kernel_f create_func,
tivx_target_kernel_f delete_func,
tivx_target_kernel_control_f control_func,
void *priv_arg
);
调用 tivxRemoveTargetKernel() 函数注销已注册的 Target Kernel 对象。
不用考虑 Target Kernel Instance 对象如何创建或执行,Tiovx 框架会自动完成这些工作。
参考文献链接
https://zhuanlan.zhihu.com/p/476213104
https://software-dl.ti.com/jacinto7/esd/processor-sdk-rtos-jacinto7/09_02_00_05/exports/docs/psdk_rtos/docs/user_guide/index.html
Openvx & Tiovx (八) 如何基于 Tiovx 编写应用程序
https://zhuanlan.zhihu.com/p/476569209
https://www.ti.com/tool/download/PROCESSOR-SDK-RTOS-J721E
Ti 提供的 PROCESSOR-SDK-RTOS-J721E_08.01.00.13) 中包含了非常丰富的例子,详见 vision_apps 文件夹。毫无疑问,这些例子能帮助我们更好地理解如何在 Ti 平台上部署程序。但如果你只想知道如何基于 Tiovx 编写应用程序,这些例子毫无疑问会带来额外的学习成本:
- 包含大量其它模块的初始化代码,如日志模块的初始化。
- 如果程序运行在 RTOS 系统上,则会包含大量 RTOS 系统的初始化代码,如中断的初始化。
本文结合我个人的实践经验,从 Ti 的例子中抽丝剥茧,得出基于 Tiovx 编写应用程序的代码示例。这里要强调一下,如果需要在 Ti 平台上部署应用,还是要多多参考 Ti 提供的例子。
Host 端
/* vx.h 及 tivx.h 这两个头文件是必须要包含的,其它头文件按需包含 */
#include <VX/vx.h>
#include <TI/tivx.h>
/* 从本地文件读取图片并保存到图像对象中。 */
/* 这里不提供实现。可以通过 Opencv 实现。 */
extern vx_status read_img_from_file(const char *file_name, vx_image img);
/* 将图像对象中的数据保存到本地文件中。 */
/* 这里不提供实现。可以通过 Opencv 实现。 */
extern vx_status write_img_to_file(const char *file_name, vx_image img);
/* 本示例使用的图,作用是将 RGB 图片转为灰度图 */
static vx_status add_rgb2gray_to_graph(vx_graph graph, vx_image src, vx_image dst);
int main(int argc, char *argv[])
{
/* 初始化 Tiovx 框架,包括共享内存初始化、内部日志初始化、创建 IPC 处理线程(或任务)等 */
/* 根据 Tiovx 库的编译条件,可能还会注册 Openvx 原生 Kernel 在 Target 端的部分。 */
/* 注:由 Ti 的 RTOS SDK 源码得知,Tiovx 框架下只希望由 DSP 执行节点,不希望 ARM 执行节点。 */
tivxInit();
/* 加载 Openvx 原生 Kernel 模块,加载后才能在接下来的代码中注册 Openvx 原生 Kernel 在 Host 端的部分。*/
tivxHostInit();
vx_status status = VX_SUCCESS;
vx_uint32 width = 1024, height = 720;
/* 创建 Context 对象并初始化、注册 Openvx 原生 Kernel 在 Host 端的部分。 */
vx_context context = vxCreateContext();
/* 如有必要,在这里注册自定义 Kernel 在 Host 端的部分。 */
vx_graph graph = vxCreateGraph(context);
vx_image src = vxCreateImage(context, width, height, VX_DF_IMAGE_RGB);
vx_image dst = vxCreateImage(context, width, height, VX_DF_IMAGE_U8);
/* ====== 图像处理逻辑开始 ====== */
status = add_rgb2gray_to_graph(graph, src, dst);
if (VX_SUCCESS == status) {
status = read_img_from_file("input.png", src);
}
if (VX_SUCCESS == status) {
status = vxVerifyGraph(graph);
}
if (VX_SUCCESS == status) {
status = vxScheduleGraph(graph);
}
if (VX_SUCCESS == status) {
status = vxWaitGraph(graph);
}
if (VX_SUCCESS == status) {
status = write_img_to_file("output.img", dst);
}
/* ====== 图像处理逻辑结束 ====== */
vxReleaseImage(&dst);
vxReleaseImage(&src);
vxReleaseGraph(&graph);
vxReleaseContext(&context);
tivxHostDeInit();
tivxDeInit();
return (int)status ;
}
static vx_status add_rgb2gray_to_graph(vx_graph graph, vx_image src, vx_image dst)
{
/* 这里没有检测可能存在的错误。 */
/* 实际上错误会传导的,可以在 vxVerifyGraph() 函数中一并检测。 */
vx_status status = VX_SUCCESS;
vx_image nv12 = vxCreateVirtualImage(graph, 0, 0, VX_DF_IMAGE_NV12);
vx_node n0 = vxColorConvertNode(graph, src, nv12);
vx_node n1 = vxChannelExtractNode(graph, nv12, 0, dst);
vxReleaseImage(&nv12);
vxReleaseNode(&n0);
vxReleaseNode(&n1);
return status;
}
Target 端
/* vx.h 及 tivx.h 这两个头文件是必须要包含的,其它头文件按需包含 */
#include <VX/vx.h>
#include <TI/tivx.h>
extern int is_exit;
/* 闲暇任务 */
extern void idle_task(void);
int main(int argc, char *argv[])
{
/* 初始化 Tiovx 框架,包括共享内存初始化、内部日志初始化、创建 IPC 处理线程(或任务)等 */
/* 根据 Tiovx 库的编译条件,可能还会注册 Openvx 原生 Kernel 在 Target 端的部分。 */
/* 注:由 Ti 的 RTOS SDK 源码得知,Tiovx 框架下只希望由 DSP 执行节点,不希望 ARM 执行节点。 */
tivxInit();
/* 由于这里是 Target 端,故不需要调用 tivxHostInit() 函数*/
/* 如有必要,在这里注册自定义 Kernel 在 Target 端的部分。 */
/* 防止主线程(或主任务)退出。 */
/* 如果主线程(或主任务)退出,IPC 处理线程(或任务)也会退出, */
/* 从而无法处理 Host 端发送的节点创建、执行等命令。 */
while(is_exit != 1)
{
idle_task();
}
return 0;
}
参考文献链接
https://zhuanlan.zhihu.com/p/476569209
https://www.ti.com/tool/download/PROCESSOR-SDK-RTOS-J721E