公司动态

DM642 EVM视频处理系统迁移:FVID驱动与内存优化实战

📅 2026/7/22 16:41:56
DM642 EVM视频处理系统迁移:FVID驱动与内存优化实战
1. 项目概述从NVDK到DM642 EVM的迁移挑战几年前我接手了一个老项目的移植任务要把一个在TI C6416 NVDK开发板上跑得挺稳的多通道运动检测系统搬到当时新出的DM642 EVM评估板上。听起来像是换个硬件平台改改引脚定义就完事了真干起来才发现这活儿是个典型的“嵌入式系统外科手术”核心矛盾就两点视频端口Video Ports的驱动架构彻底变了以及内部SRAM从1MB骤减到256KB。原来的系统在NVDK上资源充裕设计上有些“大手大脚”到了DM642上就得精打细算每一KB内存都得用在刀刃上。这个运动检测系统的核心是实时处理多路视频流进行帧间差分计算来检测运动物体。在NVDK上视频采集和显示依赖一块FPGA和Ateme提供的专用图形库来桥接数据走的是外部存储器接口EMIF。而DM642的亮点在于集成了三个可配置的视频端口能直接对接像SAA7115解码器和SAA7105编码器这类芯片相当于把视频数据通路“硬化”了效率更高也解放了EMIF的带宽。但好处不是白来的你得重新学会跟这些视频端口打交道用TI新推出的FVID驱动框架去配置它们。同时内存的“豪宅”变“公寓”迫使你必须重新审视每一个缓冲区、每一个BIOS对象的安家之处从粗放管理转向精细优化。这次迁移本质上是一次针对特定硬件DM642 EVM的深度适配与性能压榨过程对于从事TI C6000系列DSP特别是涉及视频处理的嵌入式开发来说里面的坑和技巧都非常有代表性。2. 核心思路与方案选型解析2.1 视频通路的重构从FPGA桥接到直连视频端口在NVDK平台上视频流路径是视频源 - 视频解码芯片 - FPGA - EMIF - DSP。DSP通过Ateme的图形库API与FPGA交互完成视频帧的抓取和回显。这个方案通用性强但引入了FPGA这个中间层增加了延迟和潜在的带宽瓶颈。DM642的视频端口方案则是视频源 - 视频解码芯片如SAA7115- VP0视频端口捕获- DSP内部处理 - VP2视频端口显示- 视频编码芯片如SAA7105。这是一个点对点的直连架构。为什么选择这个方案核心优势在于“专用”和“直接”。视频端口是DSP片上的外设有专用的DMA控制器能够以行/场中断的方式自动将视频数据搬运到指定的内存缓冲区完全不占用CPU核心对EMIF带宽的占用也极低。这对于需要处理多路高清当时是D1分辨率视频的运动检测应用来说是保证实时性的基石。因此迁移的首要任务就是彻底剥离旧的Ateme图形库依赖全面转向TI的Driver Development Kit (DDK) 1.1中提供的FVID驱动。FVIDFrame Video Driver提供了一套统一的、基于IOM模型的API来管理视频端口抽象了底层硬件细节让开发者可以更关注业务逻辑。这个选择是必然的因为它是TI为DM642视频子系统提供的官方、唯一的标准驱动方案。2.2 内存架构的重新规划256KB内部SRAM的生存法则C6416 NVDK拥有1MB的片上SRAM这让初代系统设计可以很“任性”几乎所有的代码段、数据段、BIOS对象如任务堆栈、信号量、队列以及视频处理中间缓冲区都可以一股脑地放在内部内存ISRAM里以获得最快的访问速度。但DM642只有256KB ISRAM直接照搬必然爆掉。这里的设计思路必须转变ISRAM是珍贵的战略资源必须留给最需要、最频繁访问的数据和代码。我们的优化原则是速度优先将计算最密集的算法所操作的中间处理缓冲区放在ISRAM中。在运动检测中就是当前帧与参考帧做差分的那些Y、Cr、Cb分量缓冲区。缓存友好开辟一大块ISRAM如128KB配置为L2 Cache。这样即使代码和部分数据放在外部慢速SDRAM中也能通过缓存机制获得接近内部内存的访问性能。外部化非核心资源将操作系统DSP/BIOS的数据对象、大部分代码段、以及非实时的、大的数据缓冲区如完整的视频帧缓冲区全部移到32MB的外部SDRAM中。精细化管理利用链接器命令文件.cmd和DSP/BIOS的配置数据库CDB精确控制每一个内存段的归属。这种“核心数据进ISRAM代码与大缓冲进SDRAMCACHE”的混合架构是在有限资源下追求极致性能的经典做法。3. 视频端口配置与FVID驱动实战3.1 清理旧驱动与引入FVID第一步是“拆旧”。在thrCapture.c、thrDisplay.c和thrProcess.c这几个核心线程文件中移除所有对agl.h和iekc64.h头文件的引用。在链接器命令文件中移除agl_c64.lib和iekc64_d.lib这两个Ateme图形库文件。同时删除thrCapture.c中的openInput()、getInputFrame()以及thrDisplay.c中的openDisplay()、putOutputFrame()等旧驱动函数。这相当于为新的视频通路清空了场地。接下来是“建新”。FVID驱动的基本使用模式是创建FVID_create、控制FVID_control、数据交换FVID_exchange、删除FVID_delete。捕获线程thrCapture.c初始化示例// 包含必要的DDK头文件 #include fvid.h #include evmdm642.h #include evmdm642_vport.h // 在初始化函数中 FVID_Handle capChan; Int status; // 配置捕获通道参数关键是指定内存段为外部堆EXTERNALHEAP因为帧缓冲区很大 EVMDM642_vCapParamsChan.segId EXTERNALHEAP; // 配置SAA7115解码器参数通过I2C控制 EVMDM642_vCapParamsSAA7115.hI2C EVMDM642_I2C_hI2C; // 创建视频捕获通道。/VP0CAPTURE/A/0表示使用VP0口的A场或通道0进行捕获 capChan FVID_create(/VP0CAPTURE/A/0, IOM_INPUT, status, (Ptr)EVMDM642_vCapParamsChan, NULL); if (capChan NULL) { // 错误处理... } // 发送控制命令配置前端解码器EDC: External Device Control FVID_control(capChan, VPORT_CMD_EDC_BASEEDC_CONFIG, (Ptr)EVMDM642_vCapParamsSAA7115); // 启动捕获通道 FVID_control(capChan, VPORT_CMD_START, NULL);这里的关键是EVMDM642_vCapParamsChan和EVMDM642_vCapParamsSAA7115这两个结构体。它们的详细定义在DDK的示例文件中通常位于CCS安装目录\boards\evmdm642\examples\video\driver\settings下。你需要根据你的视频格式如NTSC 720x480选择合适的源文件如evmdm642_vcapparamsNTSC.c添加到你的工程中。一个常见的坑是忘记将这些配置文件加入工程导致链接时找不到结构体定义。3.2 视频数据流转与格式转换在捕获线程的运行函数thrCaptureRun中核心操作是FVID_exchange。这个函数是非阻塞的它提交一个空闲缓冲区给驱动用于填充下一帧数据同时返回一个已经填满数据的缓冲区给你处理。// 假设 capFrameBuf 是包含帧数据的缓冲区指针 FVID_exchange(capChan, capFrameBuf); // 此时capFrameBuf 指向新捕获的帧数据从视频端口捕获的原始数据通常是YCrCb 4:2:2隔行格式。但许多图像处理算法包括我们运动检测中的一些操作在4:2:0格式上效率更高。因此我们需要在捕获后立即进行一次转换// 定义输入输出缓冲区指针数组 Char *inBuf[3], *outBuf[3]; // 指向捕获的422数据 inBuf[Y] capFrameBuf-frame.iFrm.y1; inBuf[CR] capFrameBuf-frame.iFrm.cr1; inBuf[CB] capFrameBuf-frame.iFrm.cb1; // 指向我们为处理分配的420缓冲区 outBuf[Y] scombufCap-bufYCRCB[Y]; outBuf[CR] scombufCap-bufYCRCB[CR]; outBuf[CB] scombufCap-bufYCRCB[CB]; // 调用422到420的转换函数 yuv422to420(inBuf, outBuf, PROCF_WIDTH, CAPF_HEIGHT, CAPF_WIDTH);这里有个重要细节PROCF_WIDTH处理宽度和CAPF_WIDTH捕获宽度可能不同。例如我们可能捕获D1分辨率720x480但只处理其中的CIF区域352x240。转换函数需要知道源和目的的行跨度line pitch即每行数据的字节数才能正确拷贝。CAPF_WIDTH就是捕获数据的行跨度。显示线程thrDisplay.c是相反的过程。处理完的数据是YCrCb 4:2:0格式需要转换回4:2:2格式然后通过FVID_exchange提交给显示视频端口VP2。// 将处理后的420数据转换回422以便显示 inBuf[Y] scombufDisp-bufYCRCB[Y]; inBuf[CR] scombufDisp-bufYCRCB[CR]; inBuf[CB] scombufDisp-bufYCRCB[CB]; outBuf[Y] disFrameBuf-frame.iFrm.y1; outBuf[CR] disFrameBuf-frame.iFrm.cr1; outBuf[CB] disFrameBuf-frame.iFrm.cb1; yuv420to422(inBuf, outBuf, PROCF_WIDTH, CAPF_HEIGHT, PROCF_WIDTH); // 将填充好的显示缓冲区提交出去 FVID_exchange(disChan, disFrameBuf);3.3 输出格式切换与多通道布局原NVDK系统输出是VGA RGB格式。DM642视频端口更自然地支持BT.656标准的YCrCb输出这省去了额外的RGB转换步骤也更符合视频领域通用规范。切换输出格式很简单只需修改显示参数结构体EVMDM642_vDisParamsSAA7105中的一个字段SAA7105_ConfParams EVMDM642_vDisParamsSAA7105 { SAA7105_AFMT_SVIDEO, // 使用S-Video输出 // SAA7105_AFMT_COMPOSITE, // 或者使用复合视频输出 SAA7105_MODE_NTSC720, SAA7105_IFMT_YCBCR422_INTERLACED, TRUE, FALSE, INV };选择SAA7105_AFMT_SVIDEO或SAA7105_AFMT_COMPOSITE即可在S端子和复合视频之间切换。对于多通道运动检测我们需要在同一个显示画面上排列多个处理通道的结果。这涉及到复杂的缓冲区偏移计算。原来RGB模式下只有一个连续的缓冲区现在Y、Cr、Cb三个分量需要分别计算偏移。我们定义了一个结构体和偏移量宏typedef struct placementBuff { Char *y; Char *cr; Char *cb; } placementBuff; // 假设将画面分为4象限计算每个象限Y分量的起始偏移 #define Q1_Y_OFFSET 0 #define Q2_Y_OFFSET (OPF_WIDTH 1) // 半屏宽度 #define Q3_Y_OFFSET ((OPF_WIDTH * OPF_HEIGHT) 1) // 半屏面积 #define Q4_Y_OFFSET ((OPF_WIDTH * OPF_HEIGHT 1) (OPF_WIDTH 1)) // Cr/Cb分量是Y分量大小的一半4:2:0采样偏移量也相应减半 #define Q1_CR_OFFSET 0 #define Q2_CR_OFFSET (Q2_Y_OFFSET 1) // ... 其他偏移类似在将缓冲区传递给处理单元Cell时需要设置正确的起始指针placementBuff outputBuff; outputBuff.y scombufDisp-bufYCRCB[Y] Q2_Y_OFFSET; // 第二象限 outputBuff.cr scombufDisp-bufYCRCB[CR] Q2_CR_OFFSET; outputBuff.cb scombufDisp-bufYCRCB[CB] Q2_CB_OFFSET; // 通过ICCInter-Cell Communication设置输出缓冲区 ICC_setBuf(chan-cellSet[CHDIFFCELLDIFF].outputIcc[0], outputBuff, 0);这里的关键点是“行跨度”linePitch的处理。在处理单元内部算法可能按一维数组方式处理数据。但在最终用DAT_copy2d拷贝到显示缓冲区时必须考虑目标缓冲区实际的二维布局。行跨度就是二维缓冲区中一行的字节数。对于处理单元其输出行跨度通常是处理宽度PROCF_WIDTH而对于最终拷贝到显示缓冲区的操作行跨度是输出帧的宽度OPF_WIDTH。这个值需要作为环境变量传递给处理单元。// 在执行通道前设置行跨度 thrProcess.diffEnv-linePitch OPF_WIDTH; // 传递给差分处理单元 // 在处理单元内部使用DAT_copy2d进行拷贝指定正确的行跨度 DAT_copy2d(DAT_1D2D, yOutBuff, outData[Y], PROCF_WIDTH, PROCF_HEIGHT, linePitch); DAT_copy2d(DAT_1D2D, crOutBuff, outData[CR], PROCF_WIDTH1, PROCF_HEIGHT1, linePitch1); // ... 等待拷贝完成如果行跨度设置错误会导致图像在屏幕上倾斜、错位或者出现奇怪的条纹调试时需要重点检查。4. 内存优化与缓存策略深度解析4.1 内部SRAMISRAM的精细分区面对256KB的ISRAM我们必须像规划市中心地块一样精打细算。通过修改链接器命令文件.cmd和DSP/BIOS配置我们可以手动划分这片区域。一个典型的分区方案如下L2 Cache128KB这是性能的倍增器。将ISRAM的一部分配置为缓存可以显著加速对外部SDRAM中代码和数据的访问。在DM642上L2 SRAM可以灵活配置为全映射缓存、部分缓存部分SRAM或全SRAM。我们选择将其配置为128KB的缓存。这通常在BIOS配置工具中设置或者在gel文件初始化时通过写缓存配置寄存器CACHE_CFG完成。关键处理缓冲区约数十KB剩下的128KB SRAM中我们划出大部分留给最核心的中间处理缓冲区。在运动检测中就是intYBufintCrBufintCbBuf这些用于帧间差分、滤波等实时计算的缓冲区。将它们放在ISRAM可以确保算法核心循环的访问是零等待的这对维持高帧率至关重要。内部堆Internal Heap 剩余部分DSP/BIOS运行时需要一些内部堆空间来创建对象如信号量、邮箱。这部分不需要太大几KB到十几KB即可用于分配那些访问频繁的小型RTOS对象。具体在.cmd文件中的体现可能是这样的MEMORY { ISRAM: origin 0x00000000, length 0x00040000 /* 256KB */ SDRAM: origin 0x80000000, length 0x02000000 /* 32MB */ ... } SECTIONS { .intYBuffer ISRAM .intCrBuffer ISRAM .intCbBuffer ISRAM .internalHeap ISRAM .text SDRAM /* 代码段全部放到外部 */ .bss SDRAM /* 未初始化数据段 */ .far SDRAM /* 远数据段 */ .stack SDRAM /* 系统堆栈 */ .data SDRAM /* 初始化数据段 */ .const SDRAM /* 常量段 */ .cio SDRAM /* C I/O 缓冲区 */ ... }注意事项.internalHeap段的大小需要在DSP/BIOS配置中明确设置。确保其足够分配你需要的BIOS对象但又不至于挤占处理缓冲区的空间。一个技巧是先在宽松配置下运行通过BIOS的统计工具查看堆的使用峰值然后再回来调整大小。4.2 缓存一致性问题与CACHE API的运用当ISRAM一部分用作缓存后一个经典的嵌入式问题就出现了缓存一致性问题。DMA如视频端口、EDMA会直接与内存交互而不经过缓存。如果CPU修改了缓存中的数据但未写回内存DMA读到的是旧数据反之如果DMA向内存写了新数据但CPU缓存中还是旧数据CPU就会读到脏数据。在我们的运动检测系统中有一个通过GEL通用仿真器接口脚本更新参考帧的功能。参考帧数据由外部工具通过仿真器直接写入SDRAM。由于这部分SDRAM区域可能已被缓存CPU看到的可能还是旧的缓存内容。为了解决这个问题必须使用CACHE API来手动维护一致性/* 假设 scombufCap 是捕获缓冲区prevY, prevCr, prevCb 是存储在SDRAM中的参考帧缓冲区 */ /* 步骤1将捕获缓冲区可能在缓存中被修改过写回并无效化其在L2中的缓存行 */ CACHE_wbInvL2(scombufCap-bufYCRCB[Y], CAPF_SIZE_IN_PIXELS, CACHE_WAIT); CACHE_wbInvL2(scombufCap-bufYCRCB[CR], CAPF_SIZE_IN_PIXELS2, CACHE_WAIT); CACHE_wbInvL2(scombufCap-bufYCRCB[CB], CAPF_SIZE_IN_PIXELS2, CACHE_WAIT); /* 步骤2将数据从捕获缓冲区拷贝到参考帧缓冲区使用DMA加速的DAT_copy */ prevYId DAT_copy2d(DAT_2D1D, (Void *) scombufCap-bufYCRCB[Y], (Void *) prevY, PROCF_WIDTH, PROCF_HEIGHT, PROCF_WIDTH); prevCrId DAT_copy2d(DAT_2D1D, (Void *) scombufCap-bufYCRCB[CR], (Void *) prevCr, PROCF_WIDTH1, PROCF_HEIGHT1, PROCF_WIDTH1); prevCbId DAT_copy2d(DAT_2D1D, (Void *) scombufCap-bufYCRCB[CB], (Void *) prevCb, PROCF_WIDTH1, PROCF_HEIGHT1, PROCF_WIDTH1); /* 步骤3等待DMA拷贝完成 */ DAT_wait(prevYId); DAT_wait(prevCrId); DAT_wait(prevCbId); /* 步骤4无效化参考帧缓冲区在L2缓存中的内容确保CPU后续读取时从SDRAM获取最新数据 */ CACHE_invL2(prevY, CAPF_SIZE_IN_PIXELS, CACHE_WAIT); CACHE_invL2(prevCr, CAPF_SIZE_IN_PIXELS2, CACHE_WAIT); CACHE_invL2(prevCb, CAPF_SIZE_IN_PIXELS2, CACHE_WAIT);这里CACHE_wbInvL2和CACHE_invL2的区别至关重要wbInvWrite-Back and Invalidate先将缓存中已修改的数据写回内存然后标记该缓存行无效。invInvalidate则直接丢弃缓存行中的数据下次访问时从内存重新加载。在DMA传输前后正确使用这些API是保证系统稳定性的关键。4.3 SDRAM的配置与代码搬迁DM642 EVM板载32MB SDRAM地址从0x8000 0000开始。我们需要将所有非核心的代码和数据都安排到这里。这包括所有编译器生成的段.text代码.data初始化数据.bss未初始化全局/静态变量.const.far等。几乎所有的DSP/BIOS对象段.trcdata.sysdata.objdata以及各种任务堆栈.taskstack。操作是在DSP/BIOS的图形化配置工具CDB Editor中完成的。你需要逐一检查每个“属性”Property页签将其中“Memory Segment”下拉框从“Internal Memory”改为“External Memory”或你命名的SDRAM段如SDRAM。一个极易遗漏的坑是改变内存段后相关的内存池Memory Pool或堆Heap的大小和位置也需要相应调整。例如如果你将.text段移到了SDRAM那么用于动态分配代码或数据的堆如EXTERNALHEAP也必须确保其基地址和大小在SDRAM的有效范围内并且在链接命令文件中被正确定义和分配。迁移完成后由于代码在外部慢速内存中运行初始性能会下降。这正是我们配置128KB L2 Cache的意义所在。CPU取指和访问数据时会首先在缓存中查找命中则全速访问未命中才去访问SDRAM。对于循环密集的视频处理代码缓存命中率通常很高从而能将平均访问速度提升到接近ISRAM的水平。5. 调试技巧与常见问题排查5.1 视频端口无信号或花屏这是移植初期最常见的问题。检查物理连接与电源确保视频解码器SAA7115、编码器SAA7105供电正常视频输入/输出线缆连接正确。DM642 EVM板上的跳线设置如果有是否符合视频端口配置如复合视频 vs. S-Video。验证I2C配置视频编解码器的初始化是通过I2C总线配置其内部寄存器完成的。确保I2C驱动已正确初始化并且EVMDM642_I2C_hI2C句柄有效。可以使用I2C扫描工具或读取编解码器ID寄存器来验证通信是否成功。核对FVID创建参数检查FVID_create调用中使用的设备名称字符串如/VP0CAPTURE/A/0是否与硬件连接匹配。VP0通常用于捕获VP2用于显示。A/0表示场A或通道0。确认缓冲区尺寸与对齐视频端口DMA对缓冲区地址和大小可能有对齐要求如128字节对齐。确保通过FVID_exchange提交的缓冲区地址和大小符合驱动要求。使用MEM_align()函数来分配对齐的内存。检查时钟与同步信号使用示波器或逻辑分析仪测量视频编解码器输出的像素时钟PCLK、行同步HSYNC和场同步VSYNC信号是否正常并确保DM642视频端口的配置如时钟极性、同步方式与之匹配。这些配置都封装在EVMDM642_vCapParamsSAA7115等结构体中务必从TI官方示例中拷贝正确的配置值。5.2 内存错误与数据损坏链接错误Section placement fails检查.cmd文件确保所有定义的段section都被分配到了已定义的内存区域且没有重叠。特别注意.internalHeap这类在BIOS中配置大小的段其长度必须与.cmd文件中分配给它的空间一致。运行时内存越界这是最难查的问题之一。症状可能是随机死机、图像出现随机噪点、或变量值被莫名修改。工具辅助使用CCSCode Composer Studio的内存查看器Memory Browser在疑似越界的缓冲区前后设置“内存访问断点”。如果某个函数写到了缓冲区之外会触发断点。代码审查重点检查所有数组访问、指针运算特别是涉及行跨度linePitch和图像宽高的计算。一个像素偏移计算错误在循环中就会被放大成千上万次越界写。使用安全函数尽量使用DAT_copy这类经过优化的库函数进行内存拷贝它们比手写的循环更安全、更快。缓存一致性问题导致的数据“神游”表现为处理后的图像时好时坏或者GEL更新参考帧后算法不生效。确认数据流画一张数据流图标明哪些数据是CPU产生/消费的哪些是DMA视频端口、EDMA产生/消费的。添加CACHE维护在所有CPU与DMA共享的数据缓冲区进行数据交换的前后严格按照“DMA写 -CACHE_inv”“CPU写 -CACHE_wb或CACHE_wbInv”的原则添加缓存维护操作。简化调试可以尝试暂时将L2配置为全SRAM关闭缓存如果问题消失那基本可以断定是缓存一致性问题。5.3 性能不达标Profile是关键使用CCS的Profiler工具或DSP/BIOS的实时分析工具如STS模块、LOG模块来测量关键线程如thrProcess的执行周期。找出最耗时的函数。检查内存访问瓶颈如果性能分析显示大量时间花在内存访问上检查你的核心处理循环。确保核心循环数据在ISRAM使用#pragma DATA_SECTION指令将核心循环用到的数组强制放到ISRAM段如.intYBuffer。优化循环结构使用编译器支持的#pragma MUST_ITERATE提供循环次数信息帮助编译器做更好的软件流水优化。展开内层循环。使用内联函数和 intrinsics对于像_abs()、_sadd()、_ssub()等常用操作使用TI提供的编译器内联函数intrinsics可以生成非常高效的汇编指令。DMA使用是否充分图像数据的搬运如格式转换、通道间传递应尽量使用DAT_copy2d背后是EDMA而不是用CPU的memcpy或循环拷贝。确保DAT_copy调用是异步的并通过DAT_wait等待完成这样CPU可以在DMA搬运数据时并行做其他计算。5.4 从NVDK迁移的特定陷阱宏定义和常量NVDK和DM642 EVM的板级支持包BSP可能使用了不同的宏来定义视频尺寸、内存地址等。例如CAPF_WIDTH、OPF_HEIGHT这些值需要根据DM642 EVM支持的视频模式如NTSC 720x480重新检查定义。中断向量表IVTDM642的中断映射可能与C6416不同。确保中断向量表正确配置特别是视频端口捕获完成中断、显示完成中断等它们是否正确连接到了DSP/BIOS的HWI硬件中断对象上。外设时钟初始化DM642的PLL锁相环配置、外设时钟分频可能与C6416不同。参考DM642 EVM的GEL文件或示例工程确保系统时钟、EMIF时钟、视频端口时钟等初始化正确。错误的时钟会导致视频端口无法以正确速率采集数据或者EDMA传输速度不匹配。整个迁移过程就像是在一台更精密但也更紧凑的新机器上重新部署一套复杂的实时流水线。每一个环节——从视频数据的流入、处理到流出再到内存中每一字节的摆放——都需要仔细考量。最终当多路视频稳定地在DM642 EVM上实现实时运动检测并显示时那种对系统资源完全掌控的感觉是嵌入式开发独有的乐趣。这种针对特定硬件进行深度优化、在资源限制下榨取每一分性能的经验对于处理其他嵌入式视觉项目尤其是基于TI C6000系列DSP的平台具有很高的复用价值。