eXpressDSP算法标准与API Wrapper:构建可复用DSP图像处理模块
1. 项目概述与核心价值
在嵌入式数字信号处理器(DSP)上开发图像处理应用,比如视频编解码、实时滤镜或者机器视觉,我们常常面临一个核心矛盾:一方面,算法本身(比如一个高效的小波变换或边缘检测)需要极致的性能优化,往往要手写汇编来压榨硬件潜力;另一方面,整个应用系统又需要良好的模块化、可维护性和可移植性,方便不同算法组合、调试和升级。如果每个算法都跟具体的硬件内存布局、DMA传输耦合在一起,那代码就会变成一坨难以维护的“意大利面条”,换块DSP芯片或者加个新功能都得伤筋动骨。
eXpressDSP算法标准连同其API Wrapper,就是TI(德州仪器)给出的一套非常漂亮的解决方案。它不是什么高深的理论,而是一套工程上的“最佳实践”框架。简单说,它定义了一套“插座”标准(IALG接口),你的算法(无论多底层)只要做成符合这个标准的“插头”(API Wrapper),就能即插即用地接入到更大的系统框架里。本文将以一个具体的二维小波变换(2D Wavelet Transform)实现为例,彻底拆解这个“插座”和“插头”是怎么工作的。你会看到,从最顶层的算法调用,到中间层的数据搬运管理(Image Data Manager),再到最底层手写汇编的ImageLIB内核,整个链条是如何被清晰解耦并高效协同的。对于需要在C6000系列或其他DSP平台上进行算法开发的工程师来说,理解这套模式,是摆脱“一次性代码”、构建可复用算法库的关键一步。
2. eXpressDSP算法标准与API Wrapper深度解析
2.1 算法标准:定义通用的“插座”
在深入代码之前,必须理解eXpressDSP算法标准(Algorithm Standard)的核心思想。它不是一个具体的函数库,而是一套接口规范,其目标是为所有DSP算法建立一个统一的、基于对象(虽然C语言)的模型。这个模型主要定义了两个最基础的接口:
IALG(Algorithm Interface):这是所有算法对象的“根接口”。它定义了算法对象的生命周期管理方法,比如:
algAlloc:为算法实例分配内存(包括实例对象本身和其所需的工作缓冲区)。algInit:初始化算法实例,将参数应用到实例中。algFree:释放算法实例占用的内存。algMoved:当算法实例对象在内存中被移动时(由框架管理),通知算法进行内部指针调整。
IALG接口确保了算法实例的创建、初始化和销毁能够被系统框架统一管理,而不是散落在应用代码的各个角落。
算法特定接口(例如 IWavelet):在IALG的基础上,每个特定类型的算法会定义自己的接口。这个小波变换的
IWavelet接口就扩展了IALG,并增加了算法特有的方法:apply:执行小波变换计算。control:在算法运行时动态获取或设置状态参数(如图像尺寸、滤波器系数)。
为什么这么做?想象一下,你的系统里有一个视频处理管道,需要依次调用色彩空间转换、JPEG编码、小波滤波等多个算法。如果没有标准接口,每个算法的调用方式、参数传递、内存申请都各不相同,集成起来就是噩梦。有了IALG和IWavelet这样的接口,框架就可以用完全相同的方式(通过函数表指针)来创建、配置和运行任何一个符合标准的算法,极大降低了集成复杂度。
2.2 API Wrapper:为你的算法制作“插头”
API Wrapper,顾名思义,就是一层“包装纸”。它的任务是把一个原始的、可能非常底层和特化的算法函数(比如直接操作寄存器的汇编函数),包装成符合上述eXpressDSP算法标准接口的模块。
我们来看例子中的两个关键头文件:iwavelet.h和wavelet_ti.h。
iwavelet.h:定义接口(合同)这个文件定义了IWavelet接口的“样子”,也就是一份“合同”。所有实现该接口的模块都必须遵守。
// iwavelet.h 节选 typedef struct IWavelet_Obj *IWavelet_Handle; // 算法实例的不透明句柄 typedef struct IWavelet_Status { Int size; // 状态结构体大小,必须为首字段 int img_cols; int img_rows; short* qmf_ext; // 外部存储的低通滤波器组指针 short* mqmf_ext; // 外部存储的高通滤波器组指针 int scale; IMG_TYPE img_val; // 图像类型:场(FLDS)或逐行(PROG) } IWavelet_Status; typedef struct IWavelet_Params { Int size; // 参数结构体大小,必须为首字段 int img_cols; int img_rows; const short* qmf_ext; const short* mqmf_ext; int scale; IMG_TYPE img_val; } IWavelet_Params; typedef struct IWavelet_Fxns { IALG_Fxns ialg; // 内嵌IALG函数表,这是继承关系的关键 XDAS_Bool (*control)(IWavelet_Handle handle, IWavelet_Cmd cmd, IWavelet_Status *status); XDAS_Int32 (*apply)(IWavelet_Handle handle, XDAS_Int8** in, XDAS_Int8* out); } IWavelet_Fxns;关键点解析:
- 句柄(Handle):
IWavelet_Handle是一个指向不完整结构体的指针,这是C语言实现封装和信息隐藏的经典手法。框架和用户通过句柄来操作算法对象,而无需知晓其内部数据结构细节。 - Params与Status:
Params用于创建实例时传递初始参数(通常是const,表示创建后不应改变);Status用于运行时查询或修改状态。将它们分离符合软件设计的最佳实践。 - 函数表(Fxns):这是面向对象中“虚函数表”的C语言实现。
IWavelet_Fxns结构体包含了指向该算法所有操作函数的指针。第一个成员是IALG_Fxns,这意味着IWavelet接口“继承”了IALG接口。框架可以通过ialg成员调用生命周期管理函数,再通过control和apply调用算法特有函数。
wavelet_ti.h:公布实现(提供插头)这个文件非常简单,就是声明TI公司提供的小波变换算法的具体实现,即那个符合IWavelet接口的“插头”。
// wavelet_ti.h extern IALG_Fxns Wavelet_TI_IALG; // TI实现的IALG方法 extern IWavelet_Fxns Wavelet_TI_IWavelet; // TI实现的IWavelet方法在另一个.c文件(例如wavelet_ti.c)中,会定义Wavelet_TI_IWavelet这个全局结构体变量,其中的函数指针都指向TI实现的具体函数。当你的应用程序想要使用这个小波算法时,只需要在链接阶段包含这个模块,然后在代码中通过&Wavelet_TI_IWavelet就能获取到整个函数表,进而创建和使用算法实例。
实操心得:编写你自己的API Wrapper时,最需要小心的是内存对齐和
size字段。IALG接口的algAlloc和algInit等函数,严重依赖于Params和Status结构体第一个size字段的正确性,以便进行正确的内存拷贝和边界检查。务必确保你的结构体定义与框架期望的完全一致,通常需要使用sizeof()操作符来设置这个字段。
3. 从算法函数到ImageLIB内核的完整调用链
理解了接口规范,我们来看一个具体的算法是如何被组织起来的。eXpressDSP推荐的分层结构非常清晰:应用层 -> 算法模块(API Wrapper) -> 图像处理函数 -> ImageLIB/自定义内核。
3.1 顶层算法函数:wavelet_codec
这是暴露给用户的、最上层的算法入口。它已经是一个被部分包装的函数,但尚未完全符合eXpressDSP标准。它负责协调整个小波变换的流程。
void wavelet_codec(IMAGE *in_image_ev, IMAGE *in_image_od, IMAGE *out_image, SCRATCH_PAD *scratch_pad, WAVE_PARAMS *wave_params, img_type img_val);参数解析:
in_image_ev,in_image_od: 分别指向偶场和奇场图像数据的指针。对于逐行(PROG)图像,in_image_od被忽略。scratch_pad: 指向临时内存(暂存区)的指针。在DSP编程中,经常需要将片外大容量、低速内存(如SDRAM)中的数据,分批搬运到片内小容量、高速内存(如SRAM)中进行计算。scratch_pad就是用于片内计算的临时缓冲区。wave_params: 小波变换的参数,如滤波器系数。img_val: 图像类型,决定是按场处理还是按逐行处理。
这个函数的内部逻辑,就是经典的小波变换二维分解流程:
- 水平变换:对图像的每一行进行一维小波滤波。
- 垂直变换:对经过水平变换后的图像的每一列进行一维小波滤波。
- 结果显示处理:将变换后的子带图像(LL, LH, HL, HH)的数值范围重新归一化到0-255,以便显示。
这个函数本身不处理具体的数据搬运和行列滤波计算,它只是一个调度器。真正的脏活累活,交给了下一层的图像处理函数(Image Processing Functions)。
3.2 图像处理函数:数据搬运的管理者
以wave_horz_image这个水平变换函数为例。它的核心职责不是做数学运算,而是管理数据流。它需要把一大张图像(存储在片外慢速内存)切成一条条“带”(strip),通过DMA搬运到片内快速内存,调用核心计算内核处理,再把结果搬回片外。
void wave_horz_image(IMAGE *in_image_ev, IMAGE *in_image_od, short *qmf, short *mqmf, SCRATCH_PAD *scratch_pad, int scale, img_type img_type_val) { // ... 初始化代码,计算指针、偏移量等 ... // 关键步骤1:初始化数据流(Data Stream) err_code = dstr_init(&i_dstr, ... , DSTR_INPUT); // 初始化输入流 err_code = dstr_init(&o_dstr, ... , DSTR_OUTPUT); // 初始化输出流 // 关键步骤2:循环处理每条“带” for ( i = 0; i < (rows / num_lines); i++) { // 获取一个空闲的输出缓冲区指针 out_data = (short *) dstr_put_2D(&o_dstr); if (!scale) // 如果是第一级分解 { // 获取一个已填充的输入缓冲区指针 in_ch_data = (unsigned char *) dstr_get_2D(&i_dstr); // 调用内核1:将8-bit像素扩展为16-bit pix_expand_asm(cols * num_lines, in_ch_data, ptr_pix_expand); // 对这条“带”的每一行,调用核心小波滤波内核 for ( j = 0; j < num_lines; j++) { ptr_wave = ptr_pix_expand + ( j * cols); ptr_out = out_data + ( j * cols); wave_horz_asm(ptr_wave, qmf, mqmf, ptr_out, cols); // 核心计算! } } // 关键步骤3:处理到一半时(例如场处理),重置数据流起始地址 if ( i == ((rows / num_lines) >> 1 ) – 1) { dstr_put_2D(&o_dstr); dstr_rewind(&i_dstr, in_rewind, DSTR_INPUT, 1); // 重绕输入流 dstr_rewind(&o_dstr, out_rewind, DSTR_OUTPUT, 1);// 重绕输出流 } } // 关键步骤4:收尾工作 dstr_put_2D(&o_dstr); dstr_close(&o_dstr); }这里有几个非常重要的设计模式:
- 双缓冲(Double Buffering):
dstr_init中的w_size参数设为1,就表示使用双缓冲。当内核正在处理缓冲区A的数据时,DMA可以同时将下一块数据搬运到缓冲区B,实现计算与I/O的重叠,隐藏数据搬运延迟,这是DSP高性能编程的基石。 - 流抽象(Stream Abstraction):
dstr_get_2D和dstr_put_2D这两个函数抽象了底层DMA的细节。开发者不用关心DMA通道配置、传输完成中断(TCINT)等,只需关心“给我数据”和“把数据送走”。这极大简化了代码。 - 重绕(Rewind)操作:在处理隔行扫描视频的场数据时,处理完偶场(Even Field)后,需要跳转到奇场(Odd Field)的起始地址继续处理。
dstr_rewind函数优雅地处理了这种非连续内存访问的需求。
3.3 ImageLIB内核:极致的性能优化
最后,我们抵达了最底层:pix_expand_asm和wave_horz_asm。这些函数通常由汇编语言编写,深度优化以充分利用DSP的硬件特性,如并行指令(如C6000系列的.S单元和.L单元)、软件流水线、循环展开等。
wave_horz_asm实现了一维离散小波变换。其算法核心是滤波器组卷积后下采样。代码中展示的虽然是行为级C代码,但揭示了关键优化点:
- 定点数运算(Q格式):DSP擅长整数运算。代码中的
Qpt=15表示采用Q15定点数格式,即小数点在最高位(符号位)之后。Qr=16384是四舍五入的偏移量(1 << (Qpt-1))。 - 循环卷积(Circular Convolution):由于小波变换通常假设信号是周期性的,当指针
xptr超过数组末尾时,会通过if (xptr > x_end) xptr = in_data;回绕到开头。在汇编实现中,这可能会通过特殊的寻址模式(如循环寻址)来高效实现。 - 内联函数与手工汇编:真正的
wave_horz_asm很可能是用线性汇编或直接汇编写的,通过精细的指令调度,让乘加(MAC)操作、数据加载和指针更新在多个功能单元上并行执行,实现单个时钟周期处理多个数据。
注意事项:当你需要自己编写或调用这类底层内核时,必须严格遵守其调用约定(Calling Convention),包括寄存器使用、栈帧结构、参数传递顺序等。同时,要清楚内核函数对输入/输出数据的对齐要求(例如是否要求8字节对齐),不满足要求可能会导致性能下降甚至错误。
4. Image Data Manager (IDM):数据搬运的瑞士军刀
上面反复提到的dstr_init,dstr_get_2D,dstr_put_2D等函数,都属于Image Data Manager (IDM)库。它是连接慢速外部存储和快速内部计算单元的桥梁,其设计哲学是声明式配置,自动化管理。
4.1 IDM核心工作流程
打开流(dstr_open / dstr_init):这是最关键的配置步骤。你需要告诉IDM:
x_data,x_size: 外部内存缓冲区(源或目标)的起始地址和总大小。i_data,i_size: 内部内存缓冲区(用于双缓冲)的起始地址和大小。quantum: 单次get或put操作传输的基本数据单元大小(例如,一行图像的字节数)。multiple: 每次传输多少个quantum(例如,一次传输4行图像)。stride: 外部内存中,连续两个quantum之间的偏移量。这对于处理非连续存储的图像数据(如仅处理Y分量)至关重要。w_size: 窗口大小,1代表双缓冲,2代表三缓冲,以此类推。dir: 数据流方向,DSTR_INPUT(外->内)或DSTR_OUTPUT(内->外)。
配置完成后,IDM内部会初始化好DMA传输描述符,并管理好读写指针。
获取/提交数据(dstr_get_2D / dstr_put_2D):
dstr_get_2D(&i_dstr): 对于输入流,调用此函数会返回一个指向内部缓冲区的指针,该缓冲区内的数据已经由DMA在后台填充完毕(对于第一次调用,会立即启动第一次DMA)。同时,它会自动为下一次get操作排队一个新的DMA请求(如果使用了双缓冲)。dstr_put_2D(&o_dstr): 对于输出流,调用此函数会返回一个指向内部缓冲区的指针,你可以向其中写入处理结果。写入完成后,再次调用dstr_put_2D(或循环结束时的提交)会触发DMA将该缓冲区内容搬移到外部内存。
重绕与关闭(dstr_rewind / dstr_close):
dstr_rewind: 用于非连续访问,如之前提到的场数据处理。它重置外部内存指针到一个新地址,但保持内部缓冲区和状态不变。dstr_close: 关闭数据流,等待所有未完成的DMA传输完成,并清理资源。
4.2 IDM的优势与使用陷阱
优势:
- 简化编程模型:开发者从繁琐的DMA配置、中断服务程序(ISR)编写中解放出来,只需关注“数据块”的消费和生产。
- 提升性能:自动化的双缓冲机制最大化了DMA与CPU的并行性。
- 增强可移植性:IDM底层可能调用CSL(Chip Support Library)的DAT(DMA传输)API或更底层的EDMA API。使用IDM的代码在不同DSP型号间移植时,数据搬运部分通常无需改动。
常见陷阱与排查技巧:
- 缓冲区大小计算错误:
i_size必须至少能容纳multiple * quantum的数据。如果multiple是4行,quantum是一行字节数,那么i_size必须 >= 4 * 一行字节数。否则会导致缓冲区溢出,数据错乱。 stride设置不当:如果外部图像数据在内存中是连续存储的(例如RGBRGBRGB...),那么stride应该等于quantum。如果数据是平面格式(例如所有Y分量连续存,然后是所有U分量),那么stride可能为0(仅第一次有效)或一个很大的值。设置错误会导致DMA读取到错误的内存区域。- 未检查返回值:
dstr_init、dstr_rewind等函数都有返回值,必须检查。返回非零值通常意味着参数配置有误(如地址未对齐、大小不匹配)。 dstr_close调用时机:必须在确保所有对该流的put/get操作都完成后才能调用dstr_close。提前关闭会导致DMA传输未完成,数据丢失。
实操心得:在调试IDM相关问题时,一个非常有效的方法是可视化内存。在CCS(Code Composer Studio)的Memory Browser中,查看你配置的
x_data和i_data指向的内存区域。在算法运行前后,手动检查这些区域的数据是否如预期般发生了变化。这能快速定位是DMA没搬数据,还是搬错了数据,或者是内核写错了数据。
5. 系统集成与演示场景分析
理解了单个算法模块的构成,我们就能看懂eXpressDSP框架如何将它们组装成完整的应用。文档中提到的几个演示场景(JPEG环回、H.263多通道解码、图像处理、小波变换)都是这种集成的典范。
5.1 以JPEG环回演示为例
这个演示包含两个任务(Task):
- Task 1:对采集的视频数据进行色彩空间转换(例如YUV到RGB),然后直接送显示。这路是“直通”画面,用于参考。
- Task 2:对同一份采集数据先进行色彩空间转换(YUV到YUV4:2:0),然后进行JPEG编码,紧接着对编码后的码流进行JPEG解码,最后再将解码后的YUV数据转换回RGB送显示。这路是“编解码后”的画面。
关键集成技术:
- 通道管理器(Channel Manager):它负责管理数据在不同算法模块间的流动。一个通道(Channel)可以看作一个预定义的数据处理管道。在JPEG演示中,Task 2的管道就是:
采集 -> 色彩空间转换 -> JPEG编码 -> JPEG解码 -> 色彩空间转换 -> 显示。通道管理器确保了数据缓冲区在各个算法间正确传递。 - 算法实例创建与链接:通过eXpressDSP的通用API(如
ALG_create),框架可以动态创建色彩空间转换、JPEG编码、JPEG解码等算法实例。这些实例通过标准的IALG接口被创建和初始化,然后通过它们各自的apply函数被通道管理器依次调用。 - 数据缓冲区管理:框架负责在算法之间传递数据缓冲区。例如,色彩空间转换算法的
apply函数输出一个缓冲区,这个缓冲区会被自动作为JPEG编码算法apply函数的输入。这一切对用户代码是透明的,用户只需配置好通道,然后启动它。
5.2 多通道解码的资源配置考量
H.263多通道解码演示展示了在有限资源(16MB板载内存)下如何进行预算分配。这是一个非常实际的工程问题。
| 内存用途 | 估算大小 | 说明 |
|---|---|---|
| H.263解码器(数据+程序) | 400 KB | 每个解码通道都需要一份代码和静态数据。 |
| 多通道框架开销 | 100 KB | 通道管理器、任务调度等系统开销。 |
| 解码与显示间缓冲区 | ~304 KB | 以CIF(352x288)分辨率,YUV4:2:0格式(1.5字节/像素),双缓冲计算:3522881.5*2。 |
| 显示缓冲区(16位,三缓冲) | 1.85 MB | 6404802字节/像素 * 3。 |
| H.263码流存储(3路,各10秒,512kbps) | 1.92 MB | 3 * 10秒 * 512kbps / 8 = 1.92MB。 |
| 总计 | ~4.58 MB | 仍在16MB容量内,可行。 |
这里的启示是:在嵌入式系统设计中,内存是首要约束。在编写算法和设计数据流时,必须精确计算每一块缓冲区的大小,并考虑其生命周期。eXpressDSP框架通过清晰的接口和IDM这样的工具,帮助开发者管理这些内存资源,但最终的内存预算和分配策略,仍需开发者自己精心规划。
6. 实战:基于eXpressDSP标准实现自定义图像滤波器
假设我们现在要在C6711 DSK上实现一个自定义的3x3中值滤波器,并希望它能够像TI的ImageLIB一样,被集成到上述演示框架中。我们应该怎么做?
6.1 第一步:定义算法接口imedianfilter.h
模仿iwavelet.h,我们定义自己的算法接口。
#ifndef IMEDIANFILTER_ #define IMEDIANFILTER_ #include <std.h> #include <xdas.h> #include <ialg.h> typedef struct IMedianFilter_Obj *IMedianFilter_Handle; typedef struct IMedianFilter_Params { Int size; int width; int height; int borderType; // 边界处理类型,如0填充、镜像等 } IMedianFilter_Params; typedef struct IMedianFilter_Fxns { IALG_Fxns ialg; XDAS_Bool (*control)(IMedianFilter_Handle handle, int cmd, void *status); XDAS_Int32 (*apply)(IMedianFilter_Handle handle, XDAS_Int8* in, XDAS_Int8* out); } IMedianFilter_Fxns; #endif /* IMEDIANFILTER_ */6.2 第二步:实现API Wrappermedianfilter_ti.c
创建实现文件,填充函数表,并实现IALG和IMedianFilter接口要求的所有函数。
#include <imedianfilter.h> #include <stdlib.h> // 1. 算法实例对象结构体(对用户隐藏) typedef struct MedianFilter_TI_Obj { struct IMedianFilter_Fxns *fxns; // 必须为首成员 int width; int height; int borderType; short* lineBuffer[3]; // 用于存储三行输入,用于3x3滤波 } MedianFilter_TI_Obj; // 2. 实现IALG接口函数 static Void *MedianFilter_TI_algAlloc(const IALG_Params *params, IALG_Fxns **parentFxns); static Int MedianFilter_TI_algInit(IALG_Handle handle, const IALG_Params *params, IALG_MemRec memTab[]); static Void MedianFilter_TI_algFree(IALG_Handle handle, IALG_MemRec memRec[]); static Int MedianFilter_TI_algMoved(IALG_Handle handle, IALG_MemRec memTab[], IALG_Handle newHandle); // 3. 实现IMedianFilter接口函数 static XDAS_Bool MedianFilter_TI_control(IMedianFilter_Handle handle, int cmd, void *status); static XDAS_Int32 MedianFilter_TI_apply(IMedianFilter_Handle handle, XDAS_Int8* in, XDAS_Int8* out); // 4. 底层核心处理函数(假设已用汇编优化) extern void median3x3_asm(const short* inLines[3], short* outLine, int width, int borderType); // 5. 定义并导出函数表 IMedianFilter_Fxns MedianFilter_TI_IMEDIANFILTER = { { /* IALG_Fxns */ MedianFilter_TI_algAlloc, MedianFilter_TI_algInit, NULL, // algActivate (可选) NULL, // algDeactivate (可选) MedianFilter_TI_algFree, MedianFilter_TI_algMoved, NULL, // algNumAlloc (可选) }, MedianFilter_TI_control, MedianFilter_TI_apply }; // 6. 具体函数实现(以algInit和apply为例) static Int MedianFilter_TI_algInit(IALG_Handle handle, const IALG_Params *params, IALG_MemRec memTab[]) { MedianFilter_TI_Obj *obj = (MedianFilter_TI_Obj *)handle; const IMedianFilter_Params *p = (const IMedianFilter_Params *)params; if (p == NULL) { p = &IMedianFilter_PARAMS; // 使用默认参数 } obj->width = p->width; obj->height = p->height; obj->borderType = p->borderType; // 为行缓冲区分配内部内存(这里简化处理,实际应在algAlloc中申请) for(int i=0; i<3; ++i) { obj->lineBuffer[i] = (short*)malloc(sizeof(short) * obj->width); } return IALG_EOK; } static XDAS_Int32 MedianFilter_TI_apply(IMedianFilter_Handle handle, XDAS_Int8* in, XDAS_Int8* out) { MedianFilter_TI_Obj *obj = (MedianFilter_TI_Obj *)handle; // 这里应该是调用一个类似wave_horz_image的包装函数, // 该函数内部使用IDM管理数据流,并循环调用median3x3_asm // 为了示例,我们简化成一个伪实现 process_image_with_idm(obj, (short*)in, (short*)out); return 0; // 成功 }6.3 第三步:实现图像处理函数与IDM集成
在process_image_with_idm函数中,我们需要像wave_horz_image那样,使用IDM来分块处理图像。
void process_image_with_idm(MedianFilter_TI_Obj *obj, short *in, short *out) { dstr_t i_dstr, o_dstr; int rows = obj->height; int cols = obj->width; int num_lines = 4; // 每次处理4行 short *internal_buf_in, *internal_buf_out; // 初始化输入流:从外部内存(in)搬运到内部缓冲区 dstr_init(&i_dstr, in, rows*cols*sizeof(short), internal_buf_in, num_lines*cols*sizeof(short), cols*sizeof(short), num_lines, cols*sizeof(short), 1, DSTR_INPUT); // 初始化输出流:从内部缓冲区搬运到外部内存(out) dstr_init(&o_dstr, out, rows*cols*sizeof(short), internal_buf_out, num_lines*cols*sizeof(short), cols*sizeof(short), num_lines, cols*sizeof(short), 1, DSTR_OUTPUT); for (int i = 0; i < rows; i += num_lines) { short *in_buf = (short*)dstr_get_2D(&i_dstr); short *out_buf = (short*)dstr_put_2D(&o_dstr); // 调用一个负责处理num_lines行的函数 // 该函数需要处理3x3滤波所需的行间依赖,可能涉及边界行缓存 median_filter_lines(obj, in_buf, out_buf, num_lines, cols); } dstr_close(&o_dstr); // i_dstr 会在所有get完成后自动结束 }6.4 第四步:集成到演示框架
- 编译:将
medianfilter_ti.c和你的汇编内核median3x3.asm编译成库文件(.lib)。 - 链接:在你的演示程序工程中,链接这个库。
- 创建实例:在应用代码中,通过
ALG_create((IALG_Fxns*)&MedianFilter_TI_IMEDIANFILTER, NULL, (IALG_Params*)¶ms)来创建滤波器算法实例。 - 加入通道:在通道管理器的配置中,将这个实例的
apply函数插入到处理链的合适位置(例如在色彩空间转换之后,显示之前)。
通过以上四步,你就得到了一个符合eXpressDSP标准的、可被系统框架管理、能高效利用DMA进行数据搬运的自定义图像滤波算法模块。这套方法论,可以扩展到任何你需要在DSP上实现的信号处理算法中。
