
接手这个项目之前我对数字下变频的印象还停留在“从IP目录里拖一个NCO再拖一个CIC Compiler连几根线完事”。这次因为目标平台逻辑资源比较紧、DSP单元数量也有限而且不能依赖某一家的专用IP我不得不把整套FPGA数字下变频链路用Verilog从头写了一遍CORDIC做本振产生CIC做抽取滤波中间再补一级反Sinc补偿。整个过程走下来收获比想象中大得多。这篇文章就是把这次设计的指标拆解、CORDIC与CIC的Verilog实现、仿真排查和工程优化完整复盘一遍希望对正在做DDC、或者准备自己写RTL替代DDS/FIR IP的朋友有参考价值。1. 设计方案怎么定指标拆解与CORDIC/CIC组合的选用逻辑1.1 数字下变频在接收链路里的位置数字下变频在接收机里的作用本质上就三件事频谱搬移、抗混叠滤波、数据率抽取。ADC送进来的通常是一个中频采样信号比如80MHz采样率下的20MHz中频信号我们需要的却是2MHz左右的基带信号。DDC要做的事情就是把这段频谱整体搬到零中频然后通过抽取把80Msps的数据率降到2Msps级别减轻后续解调、同步模块的压力。这里的核心矛盾在于抽取前后都要保证信号不混叠而混频之后的高频镜像分量必须被滤掉。直接在高采样率侧上个FIR当然可以但80MHz采样率、2MHz带宽FIR要做的运算量会非常大而且高阶FIR对DSP资源的需求在中等规模FPGA上是吃不消的。这就是CORDIC加CIC这套组合能发挥价值的地方。1.2 本振方案选型查找表DDS与CORDIC的取舍本振产生方案我对比过三种。第一种是传统查找表DDS。相位累加器加上一块ROM查表输出正余弦。优点是实现简单、时序好控制IP成熟。缺点是ROM容量随相位位数指数增长如果要把无杂散动态范围做到70dBc以上ROM存储会占掉不少Block RAM这在资源受限的平台上很心疼。第二种是固定系数混频。如果本振频率恒定不变、且恰好是采样率的四分之一或八分之一I路和Q路可以直接用0、1、-1做乘法连乘法器都不用。但这个方案局限性太强只要频率控制字需要灵活变化立刻就失效。第三种就是CORDIC。核心思想是用移位加迭代替代乘法不占用DSP单元也不需要大容量ROM。相位累加器的频率控制字可以任意设置频率分辨率只取决于累加器位宽。对比查找表DDSCORDIC在18bit输出精度、70dBc以上SFDR的场景下逻辑资源消耗通常只有ROM方案的一半甚至更少而且完全不依赖平台厂商的IP。当时我仔细算过如果按DDS方案做18bit输出、16bit相位分辨率需要65536个深度、36bit位宽的ROM表约4.7Mbit存储这已经超出了我当时那颗FPGA的Block RAM预算。而CORDIC用16级流水线大概900个LUT加1100个FF就能跑下来还支持任意相位累加位宽。选型结论没有悬念。1.3 抽取滤波选型CIC的适用边界与指标反推抽取滤波我选了CIC最直接的动机是CIC不需要乘法器。CIC的本质是N级积分器加N级梳状器传递函数为H(z) [(1 - z^(-RM)) / (1 - z^(-1))]^N其中R是抽取因子M是差分延迟N是级数。因为分子分母的系数都是1或-1所以整个滤波器就是加减法没有任何乘法运算这在DSP资源紧张的场景下几乎是天生的FPGA结构。具体指标我是这么反推的。输入采样率fs80MHz信号带宽BW2MHz需要输出2Msps。抽取因子R20差分延迟M1CIC级数N选3。选N3是综合考虑阻带抑制和通带跌落后的结果N越大阻带抑制越好但通带跌落也越严重后续补偿滤波器的设计难度会上升。这里直接给三个关键计算结果CIC第一零点在fs/(R*M)4MHz处这套参数下2MHz信号边缘刚好落在第一零点的一半位置通带边缘衰减约11.7dB。3MHz处衰减约31dB4MHz附近才开始有较好的抑制效果。总增益为R^N8000折算成位宽增加约13bit也就是16bit输入需要至少29bit输出才不会溢出。CIC单独用不够带内那11.7dB跌落必须由后级补偿滤波器拉平同时在3MHz以上的阻带进一步压制混叠。这个组合拳打下来比直接用一个高性能FIR在80MHz采样率下滤波要省资源得多。2. CORDIC本振的FPGA落地旋转迭代、象限折叠与流水线实现2.1 旋转模式的核心迭代关系CORDIC旋转模式解决的核心问题是已知角度θ求cosθ和sinθ。它的原理是把目标角度拆成一组固定角度之和这一组角度满足一个非常特殊的性质都是arctan(2^(-i))。为什么要挑这个角度序列因为如果用角度θ去旋转向量(x,y)旋转公式里会出现cosθ和sinθ相乘但如果我们把每次旋转的角度选成arctan(2^(-i))那么cos(arctan(2^(-i))) 1/√(12^(-2i))sin(arctan(2^(-i))) 2^(-i)/√(12^(-2i))。把公因子√(12^(-2i))提出来之后每次迭代就只剩位移和加减法了x_next x - d_i * y * 2^(-i) y_next y d_i * x * 2^(-i) z_next z - d_i * arctan(2^(-i))其中d_i取1还是-1取决于当前残差角z的正负目标是让z一步步收敛到0。这个迭代每进行一级精度大约增加1bit。做16次迭代理论上可以得到16bit有效精度基本可以满足18bit输出的需求。2.2 象限折叠与增益补偿这里有个必须处理的细节CORDIC基本角度之和大约只能覆盖±99.88°无法直接覆盖整个圆周。处理办法是象限折叠。先把相位累加器的输出归一化到0到2π然后看最高两位0x: 第一、二象限1x: 第三、四象限。更常用的做法是先把角度折叠到第一和第四象限再用象限标志去决定输出符号和I/Q交换关系。我当时写了一个case语句把累加器高两位作为象限选择低16位作为CORDIC核心的输入角度。核心输入范围只有-π/2到π/2CORDIC迭代完全收敛。再就是增益补偿。CORDIC每一次迭代都会给向量引入√(12^(-2i))的模长增益迭代16次后总增益约等于1.64676。这个增益不会影响信号的相对幅度但如果后级链路有幅度预算就必须处理。两种做法一种是在初始向量上直接乘1/1.64676这样迭代完增益刚好抵消另一种是迭代完以后再补一个常数乘法器。前者不占额外乘法器我推荐用第一种把预缩放因子固定成常数综合器通常会优化成移位加不会真的生成乘法器。2.3 位宽规划和可综合RTL框架位宽规划方面我用的是32bit相位累加器频率分辨率做到fs/2^32对于80MHz采样率来说大概是0.02Hz级别完全够用。CORDIC内部的x、y、z数据路径用18bit有符号数其中1bit符号位、1bit整数位、16bit小数位可以表示-2到2的范围足够容纳迭代过程中的瞬态值。流水线实现的关键是每一级只做一次移位和一次加减然后用寄存器打拍。迭代i级的移位量就是i所以前几级移位量小后几级移位量大。需要注意的是Verilog对有符号数做移位时的写法右移要用算术右移才保证负数高位补1建议统一写。一个简化版的流水线CORDIC单级结构是这样的// CORDIC流水线第i级简化示意 wire signed [17:0] xs x_in i; wire signed [17:0] ys y_in i; wire signed [17:0] x_next dir ? (x_in - ys) : (x_in ys); wire signed [17:0] y_next dir ? (y_in xs) : (y_in - xs); wire signed [17:0] z_next dir ? (z_in - atan_rad[i]) : (z_in atan_rad[i]);角度表atan_rad[i]需要预先按定点格式量化格式是3.16定点弧度。实际跑下来16级流水线输出一个正交本振对时钟频率在150MHz以下都能稳定工作时序余量也比较乐观。3. CIC抽取滤波器从Sinc响应到Hogenauer结构的Verilog实现3.1 CIC频率响应的三个关键结论设计CIC前要建立三个直觉。第一CIC的幅频响应是一串高度的Sinc形状周期性地存在零点第一零点就是抽取率对应的频率位置fs/R。第二通带内的跌落不能忽略级数N越大跌落越严重这个跌落在系统预算里必须提前预留。第三CIC的阻带抑制在相邻零点附近是有起伏的不是单调递增所以设计抽取率时最好让信号带宽避开零点附近的过渡带。回到我的参数fs80MHzR20M1N3。在2MHz处通带边缘跌落约11.7dB在3MHz处衰减约31dB。后级补偿FIR的反Sinc响应要做的是把2MHz以内这一段抬平同时在3MHz以上继续压低保证抽取折叠进带内的噪声不会突破系统底噪要求。3.2 位宽逐级规划与两补码溢出特性CIC的位宽设计是最容易忽略的坑。三级积分器每级累加器的位宽如果和输入一样很快会溢出。但有趣的是两补码运算中积分器的溢出在CIC这种线性时不变系统里是可以被纠正的只要最终输出位宽足够中间各级的“错误”溢出不会损坏信号。这是Hogenauer论文里的经典结论工程上直接用就是了省去了很多中间位宽逐级计算的麻烦。当然前提是你清楚总增益。CIC总增益是(RM)^N也就是20^38000约13bit增益。16bit输入在最后一级至少需要29bit输出。我当时直接拉宽到32bit所有中间寄存器也统一32bit逻辑资源多花了一点点但换来的是不用逐级去推位宽调试时也不用担心溢出问题。如果后续想省资源再按Hogenauer收敛位宽表去逐级截断也不迟。3.3 积分器-抽取-梳状器RTL落地CIC在FPGA上落地时有一个非常实用的变形叫Hogenauer结构。利用Noble恒等变换把梳状器从抽取之前搬到抽取之后让梳状器工作在大约除以R的低速时钟域。好处很直观高速时钟域只剩下N级纯积分器低速时钟域才处理梳状器的差分运算整体功耗和时序压力都会小很多。积分器链的Verilog非常简单三级积分器就是三个带符号累加器级联reg signed [31:0] acc0, acc1, acc2; always (posedge clk) begin acc0 acc0 din; // 第一级积分 acc1 acc1 acc0; // 第二级积分 acc2 acc2 acc1; // 第三级积分 end抽取器用计数器产生抽取使能每20个时钟抽一个数据。梳状器在抽取使能有效的时钟域里做差分运算reg signed [31:0] diff0_r, diff0_d; always (posedge clk) begin if (clk_en) begin diff0_r data_in; diff0_d diff0_r; end end assign diff0_out diff0_r - diff0_d;这里有个特别容易踩的坑抽取使能刚开始有效的第一个周期梳状器内部寄存器还是复位值输出前两个点不是有效数据。所以CIC的输出valid信号不能直接当成“第一拍就有效”我通常会让valid信号比数据延迟两拍再输出同时用axis接口做打拍对齐避免后端模块采到垃圾数据。4. 系统级仿真与问题定位频谱没有按预期出现时怎么查4.1 搭建从激励到频谱分析的单音验证环境DDC这种信号链路仿真必须按“全链路闭环”的思路来搭。我的做法是先用MATLAB生成一个20MHz中频单音信号量化成16bit定点写入文本文件然后在Testbench里用$readmemh读入作为DDC的输入。CORDIC本振的频率控制字按20MHz计算混频后理想情况下应该输出一个0MHz附近的基带信号和一个40MHz的镜像分量其中镜像分量会被CIC滤除。仿真结束之后我会把CIC输出的数据用$fwrite写到另一个文本文件再丢回MATLAB做FFT。这样可以定量看三件事NCO输出频率是否正确、I/Q正交性好不好、CIC抽取后带内有没有混叠分量。4.2 一次镜像杂散排查的完整链路这里必须分享一次真实的排查过程。第一版RTL跑完仿真频谱里出现了一个明显的镜像杂散在-1MHz附近有个大约-35dBc的峰按DDC指标要求至少要到-60dBc以下差了整整一个数量级。排查链路是这样的。先怀疑CORDIC的迭代次数不够从14级加到16级镜像幅度没有明显变化。再怀疑输出位宽截断问题CORDIC的x/y原本是18bit截到16bit发送给混频器这里会有量化噪声但FFT谱线表明噪声底并没有整体抬高说明不是这个问题。最后把CORDIC内部的z角度数据路径位宽从16bit提到20bit基本角度表也换成对应位宽的定点格式镜像杂散直接掉到了-65dBc以下。根因就是角度残差z的数据路径精度不足导致CORDIC的相位收敛误差在最后一两bit上横跳这个误差直接表现为I/Q不平衡。大家以后遇到类似镜像杂散优先查CORDIC内部的角度数据路径位宽而不是盲目加迭代次数。4.3 浮点参考模型与定点RTL的对齐方法链路调试到后期光看频谱形状不够还得做定量比对。我的做法是MATLAB里用浮点模型把整个DDC链路完整搭建一遍输入同一个激励然后把RTL定点仿真输出和浮点模型输出做误差分析通常用EVM或者直接看误差谱密度。这里要提醒的是定点模型和浮点模型之间一定要保持同样的结构CIC中间参数不要省略CORDIC的增益补偿位置也要一致否则两者的差异会混入系统误差干扰判断。做完这个对齐之后系统级的SNR预算才能落到具体模块头上。5. 实测后的工程化优化与避坑清单5.1 资源与时序上的两个关键优化第一版设计跑通之后我开始做资源和时序优化。CORDIC资源优化主要是流水线结构和迭代复用两种方案的取舍。流水线结构一个时钟出一个点吞吐率高但寄存器消耗大。如果DDC的输出数据率可以远低于输入采样率比如80MHz进来、最后只要2MHz输出理论上可以用折叠迭代结构用状态机串行做16次迭代资源能砍掉一半以上。但前提是混频器必须仍然工作在80MHzNCO输出每个输入时钟都要更新折叠结构做不到因为一个样本点的迭代要占16个时钟。所以在这个场景里流水线是必须的资源优化只能从内部位宽压缩上想办法。时序优化方面最大的瓶颈是CIC三级积分器级联加法链。虽然每级积分器本身只有一次32bit加法但三级串起来组合路径就比较长。在低速时钟下没问题一旦采样率超过150MHz时序余量就开始吃紧。我最后用进位保存加法器对积分器链路做了改造把多级加法器拆成Carry Save形式关键路径缩短非常明显。如果不想引入CSA这种复杂结构也可以考虑在积分器链路中间插入额外的流水线寄存器CIC本质上是线性系统额外延迟只会整体推迟输出不会改变频谱形状只要后续valid信号配合打拍对齐就行。5.2 补偿滤波器的设计配合CIC后面我补了一级反Sinc补偿FIR。设计思路是用MATLAB的firls拟合目标幅频响应目标响应在通带内取CIC幅频响应的倒数阻带部分直接压低用firls的权重参数控制通带纹波。系数数量我试过32阶、48阶、64阶实际用48阶就能把通带纹波控制到0.2dB以内匹配2MHz带宽够用。FIR实现时占用的DSP数量其实很小因为48阶对称FIR可以做折叠结构先做系数对称的预加只需要24个乘法器在这个平台上完全能接受。如果连这24个乘法器也想省可以再改成分布式算法查表我这边因为有少量DSP余量所以没有继续折腾。5.3 我建议你保存的避坑清单这堆问题都是这次实操中踩过或差点踩进去的坑整理成一张清单放在这做同类设计时可以直接对照排查。问题现象根因解决办法输出信号整体偏小约4.3dBCORDIC增益1.64676没有补偿初始向量预除以增益或链尾补乘常数CORDIC角度不收敛波形异常输入相位超出±99.88°范围必须做象限折叠核心输入限制在-π/2到π/2频谱镜像杂散偏高z数据路径位宽不足或角度表量化精度不够把CORDIC内部z位宽和角度表同步加宽CIC输出前两拍数据乱跳梳状器差分寄存器没有对齐validvalid比数据延迟2拍并用axis协议对齐波形出现大量非信号谐波中间变量用了无符号数负值被错误处理I/Q和CIC所有累加、差分信号统一声明signed输出噪声底偏高1-2dB直接截断低有效位没有做舍入输出截取时做半值舍入而不是直接截断抽取后带内出现重复谱CIC通带跌落远超预算没有预加重提前算通带边缘衰减后级加反Sinc补偿时序跑不到目标频率三级积分器组合链路过长插入流水级或改进位保存加法器5.4 一点个人调试心得最后分享一个我调试这类链路时的经验不要一上来就仿真整个DDC链路。先把CORDIC单独拿出来给固定相位看I/Q输出是否落在理论值上再单独测CIC把输入设成一个阶跃或者单音看抽取后的波形和幅频响应每个模块各自验证通过后再级联做全链路测试。这样做的好处是一旦最后频谱有问题嫌疑范围马上就能缩小到接口对齐或者增益匹配上而不是在整条链路里大海捞针。数字下变频这条链路CORDIC和CIC是两颗很经典的“免乘法器”组件组合起来刚好覆盖了混频和抽取两大核心功能。如果只是调IP很多内部的位宽、溢出、时序问题都会被封装在黑盒里表面上能用但换个平台就抓瞎。自己动手写一遍把每个bit的来龙去脉搞清楚以后再遇到其他平台的移植、资源裁剪、任意频率规划心里就有底了。