
简介一套面向TI C6678浮点DSP的FFT源代码工程基于CCS开发环境组织适合数字信号处理学习者、嵌入式工程师及需要将算法移植到多核DSP的开发人员参考。工程中核心源码包含myfft.h/myfft.cpp、fft_example_sp.cpp还提供汇编文件.asm、链接命令.cmd与映射文件.map配合makefile、编译选项及调试启动配置可直接导入IDE观察完整编译链接过程。通过对照C/C源文件与汇编码可逐行理解基2迭代FFT的蝶形运算、位反转、数据预处理等实现细节同时体会C6000编译器对代码的映射与优化。资源包共31个文件整体141KB结构紧凑目前已有364人学习浏览。代码中还涵盖了数据填充、复数运算、多核并行化与性能调优等关键环节并附带可直接运行的.out文件适合作为课程设计或项目预研的起点能帮助读者快速验证算法效果并进一步展开DSP底层优化。 在C6678这颗芯片上调FFT我是真踩过不少坑才把性能拉起来的。最早接手项目时我以为把PC上验证过的FFT代码用CCS编译一下扔进DSP就能跑结果一测帧处理时间直接超了实时预算三倍数据还时不时出现“灵异错位”。后来才明白C6678这种多核DSP上的FFT真正的技术含量不在于“怎么算”而在于“数据怎么流动、核怎么协同、内存怎么布局”。这篇东西就是把我从选型、单核验证、多核拆分到Cache一致性排查的完整过程梳理一遍给后面要在这颗芯片上做FFT或者类似信号处理的朋友做个参考。1. 在C6678上写FFT最大的坑不是算法本身1.1 表面看是FFT实际是存储体系之争C6678是TI KeyStone架构的8核DSP每核主频最高1.25GHz内部是C66x内核单周期能做两次复数乘加浮点能力比上一代C64x强了一大截。单从算力看做1024点FFT简直小菜一碟但实际跑起来发现瓶颈全在存储器。芯片内部的内存层次是这样的每个核有32KB L1P和32KB L1DL2一共1MB可以配成SRAM或者Cache或者混合模式另外还有4MB MSMC多核共享内存再往外才是DDR3。PC上写FFT数据在内存里连续访问Cache miss的频率不高你基本可以当内存是均匀的。但C6678不一样DSP访问L1D的速度和访问DDR3的速度差了十倍不止如果旋转因子表放在DDR3里每次蝶形运算都去外部取一次性能直接崩掉。我习惯把内存体系想成一个小型工厂L2 SRAM是工作台DDR3是库房EDMA是搬运工。FFT算法本身是流水线上的工人在工作台上干活关键不是工人手多快而是原材料能不能及时送到工作台、成品能不能及时搬走。很多人在C6678上跑FFT慢不是算法写得差而是让工人反复跑去库房拿东西。1.2 确定方案前先想清楚的三个问题拿到需求别急着写代码先问自己三件事。第一数据规模和格式是什么单精度浮点还是双精度点数是2的幂吗C6678的C66x内核支持单精度和双精度浮点但双精度FFT的周期数是单精度的数倍如果系统精度要求没那么高尽量用单精度。点数如果不是2的幂TI的DSPLIB里也有混合基FFT支持3、4、5这些因子但性能和代码复杂度都会变化。第二实时性指标是多少帧周期多长每帧多少个通道这就是在决定要不要上多核。我见过有人明明单核DSPLIB跑1024点FFT只需要几十微秒实时绰绰有余却非要折腾多核并行最后核间通信的开销比节省的计算时间还多得不偿失。第三FFT之后还要做什么处理是简单的频谱分析还是后续还要滤波、解调、目标检测这决定了数据是留在核内L2还是搬回共享内存。如果后续处理还在这颗核上做那就别把输出写到DDR再读回来直接在L2里交接最划算。2. 单核FFT选型信TI库还是信自己2.1 TI官方DSPLIB库的边界与正确用法很多从FPGA转过来的人习惯问FPGA有FFT IP核DSP这边有没有现成的东西有就是TI的DSPLIB全称是TI DSP Library在TI的处理器SDKProcessor SDK里可以直接找到源码和文档。C6678对应的主要函数是DSPF_sp_fftSPxSP和DSPF_dp_fftDPxDP分别处理单精度和双精度复数FFT。这里有个关键点DSPLIB里的函数并不是简单“调用就完事”它要求你提前准备好旋转因子表twiddle table和缓冲区还得注意对齐。比如DSPF_sp_fftSPxSP的典型签名里有几个容易忽略的输入ptr_x输入数组指针复数按实部、虚部交替存放。ptr_w旋转因子表可以通过DSPF_sp_fftSPxSP_ twiddles预生成。n_cn点数。ptr_y输出数组指针。radex基的选择通常用4。brev位反序标志某些实现里已经做了位反序优化你按文档传0或传特定值即可。n_min最小蝶形基数优化参数默认1。我自己最常踩的坑是旋转因子表和输入数据的字节对齐。C66x的lddw单指令可以加载64位数据如果指针是8字节对齐的性能会明显好一截。TI的文档建议把缓冲区都用memalign(8, size)或者编译器的#pragma DATA_ALIGN声明对齐。一个最简单的调用流程是#include ti/dsplib/dsplib.h #pragma DATA_ALIGN(input, 8) #pragma DATA_ALIGN(output, 8) #pragma DATA_ALIGN(twiddle, 8) float input[2 * 1024]; // 实部虚部交替 float output[2 * 1024]; float twiddle[2 * 1024]; // 实际可按说明分配 int n 1024; DSPF_sp_fftSPxSP_twiddles(twiddle, n); DSPF_sp_fftSPxSP(n, input, twiddle, output, brev, radix, 0, n);如果你要在循环里连续处理多帧把旋转因子表只生成一次放在L2里常驻不要每帧都重新生成。2.2 为什么我建议先跑库函数再考虑手写我在好几个项目里见过同事撸起袖子手写FFT理由是“TI的库不够透明想按自己的思路优化”。但实际上TI的DSPLIB从C64x时代就一直在为每颗DSP内核做汇编级优化C66x上的FFT已经用上了软件流水线software pipelining和双精度浮点流水你纯用C语言手写编译器生成的汇编根本达不到那个状态除非你愿意花几周时间手写汇编并对着profile一点点调。更好的路径是先直接用DSPLIB跑通功能拿到正确的频谱结果再花时间读DSPLIB的源码学它的数据排布和循环展开方式。TI的库里源码是开放的里面注释很详细我第一次读DSPF_sp_fftSPxSP源码时最大的收获不是学会了FFT蝶形公式而是看到了它怎么设计数据缓冲区来避免bank conflict——把相邻蝶形运算的数据安排在L1D不同的bank里这样并行访问时才不会因为bank冲突白白多等一个周期。我做了一个小对比给同样1024点单精度复数FFT分别在PC仿真、C6678上用纯C手写、C6678上用DSPLIB跑三种情况做耗时参考方案开发量性能可维护性PC纯C移植到DSP小差一般DSPLIB官方库很小优最好参考DSPLIB源码手写优化大优或略好差结论很直接除非你确定TI库满足不了特殊需求比如要自己控制内存布局到极致的系统否则先信官方库。尤其是做工程交付代码是要维护的后来接手的人看到TI标准API比看你写的几百行汇编要轻松太多。3. 多核拆解FFT的两种姿势与核间协作细节3.1 按通道拆分最简单也最稳C6678有8个核最自然的想法就是把8路互相独立的信号分别扔给8个核每个核做自己那一路FFT。这个方案几乎不需要额外设计核间通信每个核从共享内存读自己的输入数据算完写回自己的输出区域然后通知主核完成任务就行。这种模式里最容易翻车的是“伪共享”false sharing。C66x的Cache line通常是64字节如果两个核各自要写的数据恰好落在同一个cache line里比如核0写地址0x80001000附近核1写地址0x80001020附近虽然物理上不是同一块数据但缓存同步会把整条line来回搬运性能急剧下降。解决办法就是把每个核的数据区域按cache line边界对齐并且每个核的输入区域、输出区域之间留出填充padding宁可浪费一点内存也别让两个核共享同一行。3.2 单一大FFT的Cooley-Tukey分解与旋转因子乘法如果你只有一路很长的FFT比如16K点或者64K点单核算不完必须多核协作就需要用Cooley-Tukey分解了。思路是把N点FFT拆成N1乘以N2的两级先把数据看作N1行N2列的矩阵对每一列做N2点FFT然后给每个结果乘旋转因子再对每一行做N1点FFT。整个流程分到多个核上就是每个核负责其中若干列或者若干行的FFT。在我做的项目里128K点FFT拆成N11024、N21288个核分三阶段干阶段一每个核处理若干列的小FFT比如128列分给8个核每核16列。阶段二全核同步对中间结果乘旋转因子这一步是纯数据操作开销小但需要所有核都完成阶段一才能开始。阶段三每个核处理若干行的FFT这阶段的数据访问变成按行跳跃要注意内存访问的局部性最好先在核内做一次数据重排把要处理的行搬运到L2里再计算。你可能要问中间那段旋转因子乘法是必须的吗是的少了这一步整个结果全错。我最初实现时为了省事把它放在阶段一刚结束后由核0单独做结果发现其他核在空转等待整体的并行效率反而降低。后来改成每个核在自己的L2里先算完自己负责区域的旋转因子再去参与下一阶段同步次数从两次变成一次整体加速比涨了不少。3.3 核间同步与数据搬移的实测提醒多核FFT最容易被低估的就是同步和搬移的开销。C6678上核间通信可以用多种方式MessageQ基于IPC、共享内存标志位、OpenMP。如果你只是做FFT这种计算密集型任务我建议用最轻量的方式——共享内存里的标志位加内存屏障。TI的OpenMP工具链在C6678上虽然能用但每次parallel region的启动和回收都有固定开销对几百上千周期的FFT来说占比不小。我实测过一个4核拆分的1024点FFT用OpenMP甚至比单核DSPLIB还慢原因就是overhead太大。反倒是如果用共享flag做手工同步核心计算消耗在上面控制开销可以压到几十个周期以内。数据搬移方面多核场景下尽量走MSMC。MSMC是芯片内部的共享内存访问速度比DDR3快很多而且对核间共享的一致性处理比DDR3更好。我习惯把输入帧先由主核通过EDMA从DDR搬到MSMC的共享Buffer各核从MSMC取数处理完再通过EDMA把结果搬回DDR。DDR3上的共享内存要小心Cache一致性问题下面细说。4. Cache一致性、EDMA与一次线上事故的完整排查4.1 从“偶发错数”到定位Cache一致性有一次多核程序跑到现场之后FFT结果偶尔会出错频率不高隔几分钟出现一次。一开始怀疑算法写错了回放数据又没问题后来发现是Cache一致性导致。具体场景是核0从外设采集数据写入DDR3的共享bufferDDR3这段内存被配置成了Cacheable核0写完后核1通过共享flag收到通知直接从DDR3读数据。问题在于核0写入时数据还在它自己的L1D或L2 Cache里没有真正落回DDR核1读数据时从DDR读到的其实是旧数据。偶发性来自Cache行什么时候被替换、什么时候写回时间点不确定所以症状就是“偶发错数”。排查链路是这样的第一步打印所有核看到的数据发现核1读到的和核0写入的不一致。第二步把共享buffer改用MSMC内存因为MSMC对多核访问一致性的支持比DDR3 Cacheable场景强发现稳定复现不再出错。第三步回头查DDR3场景意识到问题根源是Cache没有主动维护。修复办法是在核0写完数据后手动写回Cache在核1读数据前手动失效对应Cache行// 核0写完数据后 __cache_wb((void *)shared_buf, buf_size); // 核1读数据前 __cache_inv((void *)shared_buf, buf_size);__cache_wb和__cache_inv是TI编译器提供的内建函数分别对应写回writeback和失效invalidate。从此之后凡是跨核共享的数据写入方一律writeback读取方一律invalidate偶发错数再也没出现过。多核DSP上碰见“偶尔不对”的数据问题第一反感应是算法bug第二反应就应该是Cache一致性。4.2 EDMA乒乓缓冲与计算重叠的关键点单核FFT计算再快如果数据是从DDR3边算边搬CPU还是会被存储延迟拖累。解决思路是EDMA乒乓缓冲把L2 SRAM里开两块bufferEDMA把下一帧数据从DDR搬到Buffer A的同时CPU正对Buffer B里的上一帧做FFT交替使用把数据搬运和计算重叠起来。乒乓缓冲流程不复杂核心是三步申请两块相同大小的L2 buffer满足8字节对齐。配置EDMA的ping-pong传输让EDMA完成Buffer A传输后自动切到Buffer B同时触发CPU中断或设置标志位。CPU在中断或轮询标志位里判断当前哪块缓冲区是“新数据”对这块数据调用DSPLIB做FFT同时启动下一轮EDMA。我实际工程里最常出的问题是EDMA的“传输完成”标志没有及时同步。比如CPU在EDMA还没写完最后一个字节时就开始读数据导致FFT输入的前几个点还是旧值。解决办法是不要用“大概等了一会儿”这种策略而是严格检查EDMA的IPR中断标志寄存器或者用while (!(edmaReg-IPR (1 eventNum)));这种标准轮询直到标志位置位再开算。还有一个细节乒乓buffer和L1D的关系。如果L2这部分被配置成SRAM那么CPU访问它和访问L1D不是一回事能拿到确定性延迟DSPLIB算FFT时最怕的就是数据在Cache里反复驱逐。我当时把L2配成128KB Cache 896KB SRAMFFT的输入输出buffer和旋转因子表全放在SRAM区只有代码和栈用Cache部分性能释放得非常彻底。5. 从Matlab仿真到板上验证的流程闭环5.1 用Matlab生成激励并对比结果写FFT代码最怕的就是“看着对实际上错”。我习惯先做闭环验证用Matlab生成测试序列跑一遍fft()得到基准结果然后把同一份序列导出成二进制文件烧给板卡板卡上DSP算完再传回PC在Matlab里对比。有一次需求是从Excel读一列传感器信号做FFT分析我就在Matlab里用xlsread或者readmatrix把数据读进来先做去均值和可能的窗函数处理再把预处理后的数据保存成single类型的二进制文件。DSP端的测试程序负责读这个文件做1024点FFT把结果以二进制格式输出到另一个文件。回到PC端对比时注意幅值是否对得上频率分量是否在预期位置相位是否存在整体偏移。建议用相对误差比如每个频点的幅值误差控制在1e-4以内单精度浮点的合理量级相位误差在0.01弧度以内。这套流程我用了很久好处是能快速定位问题到底出在数据预处理、FFT实现还是核间数据搬移。如果Matlab基准和DSP输出对不上先把DSP的输入原样打印出来和Matlab比对如果输入都一样但输出不同就缩小范围去检查旋转因子表或者位反序参数。5.2 实测中得到的数据与内存布局再调整做完基础验证之后我开始把工程里的实际数据规模跑一轮性能测试。我当时的工程环境是CCS 6.2Compiler优化等级-O3C6678主频1.0GHz降频使用为了控制功耗。单核用DSPLIB跑1024点单精度复数FFT实测大约四千个周期左右如果完全不用DSPLIB用普通C代码写大概要两万周期以上。128K点大FFT用8核Cooley-Tukey拆分总耗时大约是单核版本的5倍加速而不是8倍剩下的差距主要来自同步和数据搬移。这个数字不绝对但很能说明问题多核并行不是线性的你一定要放低预期。内存布局经过反复调整后我最终固定下来的方案是旋转因子表放L1D SRAM如果放不下就放L2 SRAM输入输出buffer放L2 SRAM外部DDR只存原始采集数据和最终频谱输出EDMA搬运由DMA控制器独立完成。这样配置之后FFT的CPU时间占比降下来了系统整体实时余量大了不少。最后聊一个我自己的习惯每次拿到新板卡我不会直接跑完整应用而是写一个最小化的测试工程——先让单核跑通DSPLIB基础FFT确认内存布局、缓存配置和EDMA这几个基础组件没问题再加多核同步逻辑最后才接真实数据。这套流程帮我避开了很多“后来才发现基础配置就错了”的麻烦。你如果也要在C6678上做FFT或者相关的信号处理建议也照着这个顺序来一遍比一上来就对着大工程调试要省心得多。本文还有配套的精品资源点击获取