尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

FPGA中CORDIC IP核实现sin/cos计算:配置、仿真与避坑指南

FPGA中CORDIC IP核实现sin/cos计算:配置、仿真与避坑指南 1. 为什么要在FPGA里算三角函数做FPGA信号处理的朋友几乎都绕不开一个需求把角度变成正弦或余弦值。比如做数字下变频DDC、电机矢量控制里的Park变换、或者通信里的载波恢复都需要实时算sin/cos。用CPU跑math.h里的sinf()当然简单但延迟大、吞吐低几百纳秒起步根本喂不饱高速ADC后面的流水线。这时候CORDIC算法就派上用场了。它的全称是Coordinate Rotation Digital Computer核心思想特别朴素用一系列固定角度的旋转去逼近目标角度每次只做移位和加减法完全不需要乘法器。在FPGA里一个乘法器DSP48要占宝贵资源而移位和加法几乎不花钱。所以CORDIC天生适合FPGA实现。Xilinx现在叫AMD的Vivado里自带一个CORDIC IP核官方叫“CORDIC v6.0”。它支持旋转、向量、sin/cos、arctan、平方根等多种功能。很多人第一次用会觉得配置项太多、接口看不懂其实只要抓住几个关键参数5分钟就能跑通一个sin/cos计算模块。这篇文章我就把从IP配置到仿真验证的完整流程拆开讲顺带把那些官方文档里不写、但实际会卡住你的坑一个个填上。适合谁看如果你刚接触FPGA想找个能跑通的实战例子或者你已经在用CORDIC但结果总是不对、时序过不了那这篇应该能帮到你。我默认你会基本的Vivado操作比如建工程、加IP、写testbench这些不展开。2. CORDIC IP核配置项逐个拆解打开Vivado的IP Catalog搜“CORDIC”双击进去你会看到一个配置界面。别急着点OK下面这些参数决定了你的模块能不能用、好不好用。2.1 Functional Selection先想清楚要算什么第一个下拉框是Functional Selection选项有Rotate、Translate、Sin and Cos、Arc Tan、Square Root等。我们要算sin/cos直接选Sin and Cos。这里有个细节选Sin and Cos之后IP会自动把输入当作角度输出cos和sin两路。注意输出顺序是cos在前、sin在后别接反了。如果你选Rotate输入是(x, y)坐标和角度输出旋转后的坐标那是另一种用法。很多人误选Rotate然后发现输出不对就是这里搞混了。2.2 Architectural Configuration并行还是串行第二个关键选项是Architectural Configuration有Parallel和Word Serial两种。Parallel全流水线每个时钟出一个结果吞吐率最高但消耗资源多。适合高速数据流。Word Serial迭代式一个结果要多个时钟周期资源省但速度慢。适合低速、资源紧张的场景。我一般做通信或电机控制都选Parallel因为后面数据率摆在那。如果你只是做个低速传感器解算Word Serial也够用。选Parallel之后下面会多出“Pipelining Mode”选项有Optimal、Maximum、No Pipelining。Optimal是官方推荐的平衡点Maximum会插更多寄存器提高时序No Pipelining组合逻辑会很长时序容易挂。我实测Optimal在Artix-7上跑200MHz没问题。2.3 Data Format定点数的坑从这里开始Data Format选Signed Fraction。CORDIC IP只支持定点不支持浮点。这里要理解它的定点表示法输入输出都是二进制补码但小数点的位置由你通过位宽和整数位宽来定。具体来说Input Width和Input Integer Width两个参数决定格式。比如Input Width16Input Integer Width2那么表示范围是[-2, 2)精度是2^(2-16)2^-14。角度输入通常用弧度制范围[-π, π]π约等于3.14159所以整数位宽至少要3位才能覆盖2^24 π。但官方例程里常用Input Integer Width3Input Width16这样范围是[-4, 4)精度2^-13。输出这边Output Width和Output Integer Width。sin/cos的值域是[-1, 1]所以整数位宽1位就够表示-1到1。但为了精度Output Width一般设16或18。我习惯Output Width16Output Integer Width1这样输出范围[-1, 1)精度2^-15。注意输入角度的单位是弧度不是度。如果你手头是0到360度先转成弧度再送进来。转换公式rad deg * π / 180。在FPGA里可以预先算好系数或者用定点乘法。2.4 Phase FormatRadians还是Scaled RadiansPhase Format有两个选项Radians和Scaled Radians。Radians就是标准弧度输入π对应180度。Scaled Radians是把π映射到2^(Input Width-1)也就是用整数表示角度方便定点运算。我建议选Scaled Radians因为这样输入就是一个整数范围[-2^(N-1), 2^(N-1)-1]对应[-π, π]。比如Input Width16那么输入范围是[-32768, 32767]对应[-π, π]。这样你不需要在FPGA里做浮点乘法直接送整数就行。官方例程也多用这个。选Scaled Radians后输入值x对应的实际角度是 x * π / 2^(N-1)。比如x16384N16角度16384*π/32768π/2正好90度。2.5 Rounding Mode截断还是四舍五入Rounding Mode有Truncate、Round Positive、Round Nearest等。Round Nearest是四舍五入精度最好但多一点点逻辑。Truncate直接砍掉低位简单但会引入偏差。我一般选Round Nearest资源差别可以忽略。2.6 其他选项补偿缩放和输出范围Compensation Scaling是补偿CORDIC的增益。CORDIC旋转会有一个固有增益K≈1.64676IP内部会自动补偿你不需要管。但如果你选No Compensation输出会放大1.64676倍那就得自己除。默认选Compensation Scaling就行。Output Range有Full Range和Positive Range。Full Range输出[-1,1]Positive Range输出[0,1]但会损失负半周。算sin/cos当然选Full Range。配置完点OKIP就加进来了。接下来看接口。3. 接口时序与握手信号实战CORDIC IP的接口不复杂但握手信号如果没接对数据会丢或者重复。我们逐个看。3.1 主要端口说明以Parallel模式为例端口大概这些aclk时钟。s_axis_phase_tvalid输入角度有效。s_axis_phase_tdata输入角度数据。s_axis_phase_treadyIP准备好接收。m_axis_dout_tvalid输出数据有效。m_axis_dout_tdata输出数据cos和sin拼接在一起。m_axis_dout_tready下游准备好接收。注意输入通道叫phase输出叫dout。tdata的位宽是Output Width*2因为cos和sin各占一半。比如Output Width16tdata就是32位低16位是cos高16位是sin具体顺序看IP配置一般低半是cos。3.2 握手规则valid和ready要成对AXI-Stream的握手规则是valid和ready同时为高时数据才传输。对于输入你拉高tvalid等IP拉高tready那个时钟数据就被接收。对于输出IP拉高tvalid你拉高tready数据才有效。很多人图省事把tready直接接1tvalid也一直拉高这样数据会连续输入IP也会连续输出。这在数据流连续的场景没问题。但如果你输入是间歇的就要用状态机控制tvalid否则会算错。我一般这样处理输入侧用一个计数器产生角度每N个时钟送一个有效数据输出侧tready常1因为下游通常能接。如果你下游是FIFO注意FIFO满时拉低tready反压IP。3.3 延迟与吞吐率Parallel模式下从输入到输出有固定延迟叫Latency。这个延迟取决于配置一般在10到20个时钟。你可以在IP的“Latency”标签页看到具体数值。仿真时如果发现输出比输入晚很多拍别慌这是正常的。吞吐率是每时钟一个结果只要tvalid连续输出就连续。Word Serial模式则是一个结果要N个时钟N等于迭代次数一般是Input Width1左右。实操心得如果你做闭环控制这个固定延迟要算进环路补偿里。比如电流环里用CORDIC算角度延迟20个时钟在100MHz下就是200ns对高带宽环路有影响。4. 手把手搭建一个sin/cos计算工程光说不练假把式。下面我从建工程开始一步步做一个能跑仿真的sin/cos模块。4.1 新建工程与添加IP打开VivadoCreate Project选一个你手头的器件型号比如xc7a35tftg256-1Artix-7。工程建好后在Flow Navigator里点IP Catalog搜CORDIC双击。按第2节的配置Functional Selection选Sin and CosArchitectural Configuration选ParallelPipelining Mode选OptimalData Format选Signed FractionPhase Format选Scaled RadiansInput Width16Input Integer Width3Output Width16Output Integer Width1Rounding Mode选Round NearestCompensation Scaling选Compensation ScalingOutput Range选Full Range。点OKGenerate Output Products选Global。等生成完IP就出现在Sources里。4.2 写一个顶层模块包装IP的端口名比较长我习惯写一个wrapper把它简化。新建一个Verilog文件cordic_wrapper.vmodule cordic_wrapper( input wire clk, input wire rst_n, input wire phase_valid, input wire [15:0] phase_in, output wire dout_valid, output wire [15:0] cos_out, output wire [15:0] sin_out ); wire [31:0] dout_tdata; wire dout_tvalid; wire phase_tready; cordic_0 u_cordic ( .aclk (clk), .s_axis_phase_tvalid (phase_valid), .s_axis_phase_tdata (phase_in), .s_axis_phase_tready (phase_tready), .m_axis_dout_tvalid (dout_tvalid), .m_axis_dout_tdata (dout_tdata) ); assign dout_valid dout_tvalid; assign cos_out dout_tdata[15:0]; assign sin_out dout_tdata[31:16]; endmodule注意tdata的拼接顺序低16位是cos高16位是sin。这个顺序在IP的“Output”标签页能看到不同版本可能略有差异生成后确认一下。4.3 写testbench验证testbench里产生几个关键角度0、π/2、π、-π/2看输出对不对。因为用Scaled Radians输入整数对应角度0度 - 090度 - 16384180度 - 32767接近π因为32768会溢出到-32768-90度 - -16384testbench代码timescale 1ns/1ps module tb_cordic; reg clk 0; reg rst_n 0; reg phase_valid 0; reg [15:0] phase_in 0; wire dout_valid; wire [15:0] cos_out, sin_out; always #5 clk ~clk; // 100MHz cordic_wrapper u_dut ( .clk(clk), .rst_n(rst_n), .phase_valid(phase_valid), .phase_in(phase_in), .dout_valid(dout_valid), .cos_out(cos_out), .sin_out(sin_out) ); initial begin #100 rst_n 1; #20; // 0度 phase_in 16d0; phase_valid 1; #10 phase_valid 0; #200; // 90度 phase_in 16d16384; phase_valid 1; #10 phase_valid 0; #200; // 180度 phase_in 16d32767; phase_valid 1; #10 phase_valid 0; #200; // -90度 phase_in -16sd16384; phase_valid 1; #10 phase_valid 0; #200; $finish; end endmodule跑仿真看波形。0度时cos应该接近32767即1.0sin接近0。90度时cos接近0sin接近32767。180度时cos接近-32768sin接近0。-90度时cos接近0sin接近-32768。如果结果差很多先检查Phase Format是不是Scaled Radians再检查输入位宽和整数位宽。4.4 定点数结果换算成实际值仿真里看到的是整数怎么知道实际sin值公式实际值 整数 / 2^(Output Width - Output Integer Width)。比如Output Width16Output Integer Width1那么除数2^1532768。整数32767对应32767/32768≈0.99997接近1。整数-32768对应-1。如果你要把结果送给DAC或后续处理记得按这个比例缩放。5. 那些年我踩过的坑与排查技巧CORDIC IP看着简单实际用起来坑不少。下面这些是我和身边同事真实遇到过的整理成速查表。5.1 常见问题速查表现象可能原因排查方法输出一直是0tvalid没拉高或tready没接用ILA抓s_axis_phase_tvalid和tready输出值不对差1.64676倍Compensation Scaling选了No Compensation改回Compensation Scaling角度对但sin/cos反了tdata高低位接反交换cos_out和sin_out的赋值180度附近输出跳变输入整数溢出180度用32767而不是32768时序不收敛Pipelining Mode选了No Pipelining改Optimal或Maximum仿真输出比输入晚很多拍正常Latency查IP Latency标签别误判为bugWord Serial模式输出断续迭代需要多周期正常等tvalid连续拉高5.2 坑一tready没接导致数据丢失我第一次用的时候只接了tvalid和tdatatready悬空。结果仿真里输出偶尔有、偶尔没有。后来查AXI-Stream协议才知道tready悬空默认是未知IP可能认为下游没准备好就不发数据。解决办法如果下游总能接收tready直接接1如果不能用FIFO的满信号反压。5.3 坑二角度单位搞错有次同事用CORDIC算电机角度输入是0到360的度数直接送进去结果输出完全乱套。CORDIC要的是弧度或Scaled Radians整数。度数转弧度rad deg * π / 180。如果嫌麻烦就在外面先做定点乘法把度数转成Scaled Radians整数。比如360度对应2πScaled Radians下2π对应65536但16位只能表示到32767所以360度会溢出。正确做法是把角度归一化到[-180, 180]再转。5.4 坑三输出位宽不够导致精度差有人为了省资源Output Width设成8。结果sin值只有256个台阶做控制时抖动明显。建议至少12位一般16位够用。如果做高精度测量18位或20位。资源增加不多但精度提升明显。5.5 坑四忘记复位导致X态传播CORDIC IP内部有寄存器如果复位没做好仿真一开始输出是X。虽然实际硬件上电会复位但仿真里要手动给rst_n。另外IP的复位是高有效还是低有效看IP配置。一般aclk配低有效复位但有些版本是高有效。生成后看端口名带_n的是低有效。5.6 坑五多通道时分复用时的握手冲突如果你用一个CORDIC核分时算多路角度输入tvalid要按通道切换输出也要对应标记通道号。这时候不能简单tready常1否则输出混在一起分不清。我一般加一个FIFO输入侧按通道写输出侧按通道读用FIFO的empty/full做反压。独家技巧在ILA里抓CORDIC接口时把tvalid和tready都加上触发条件设成tvalid为高但tready为低这样能抓到反压时刻快速定位握手问题。6. 资源占用与性能实测数据光说理论不够我拿一个实际工程的数据给你参考。器件是Artix-7 xc7a35t速度等级-1Vivado 2022.2CORDIC配置为Parallel、Optimal、Input Width16、Output Width16。综合实现后的资源占用资源数量LUT约320FF约450DSP480BRAM0时序方面在100MHz时钟下WNS为正能跑。试着提到200MHzWNS约-0.3ns差一点。如果选Maximum Pipelining200MHz能过但FF增加到约600。所以如果你时序紧就选Maximum。对比Word Serial模式LUT约120FF约180但一个结果要18个时钟。吞吐率差18倍。所以高速场景别省这点资源。另外CORDIC不占DSP这是它相比查找表法或多项式近似法的最大优势。查找表法要BRAM多项式法要DSP。在DSP资源紧张的FPGA里CORDIC是首选。7. 进阶用法从sin/cos到完整信号链算出一个sin/cos只是开始。实际项目里你往往需要把CORDIC嵌入更大的信号链。举两个我做过的例子。7.1 数字下变频中的混频在DDC里ADC采样中频信号需要乘以本地载波的cos和sin得到I/Q两路。本地载波就是CORDIC输出的cos/sin。相位累加器每个时钟加一个频率控制字输出角度送CORDICCORDIC输出cos/sin再和ADC数据做复数乘法。整个链路全流水能跑到200MHz以上。这里要注意相位累加器的位宽和CORDIC输入位宽要匹配。比如累加器32位取高16位送CORDIC这样频率分辨率是fs/2^32足够细。7.2 电机控制中的Park变换Park变换需要sin和cos把静止坐标系的电流转到旋转坐标系。角度来自编码器或观测器。CORDIC算sin/cos然后做矩阵乘法。这里对延迟敏感因为环路带宽可能几百Hz到几kHz。CORDIC的固定延迟要算进环路模型否则相位裕度不够。我一般把CORDIC配置成Parallel、Optimal延迟约15个时钟。在100MHz下150ns对1kHz环路来说相位滞后0.054度可以忽略。但如果环路带宽10kHz滞后0.54度就要留意了。7.3 和DDS IP的对比有人问Xilinx也有DDS Compiler IP直接输出sin/cos为什么用CORDIC区别在于DDS用查找表相位累加器输出频谱纯度高但占BRAMCORDIC用迭代占LUT无BRAM。如果你BRAM紧张用CORDIC如果对SFDR要求极高用DDS。另外DDS能直接调频率和相位CORDIC只算角度到值频率控制要自己做累加器。我一般做混频用DDS因为要频率可调做坐标变换用CORDIC因为角度是外部给的。8. 仿真加速与验证技巧CORDIC仿真如果跑几万个点Vivado自带的仿真器会慢。几个提速办法用$random产生随机角度但别每个时钟都送隔几个时钟送一个减少仿真时间。把仿真时间设短只看关键角度。用Vivado的XSIM编译优化或者换第三方仿真器。如果做回归测试把CORDIC输出和MATLAB的sin/cos对比用脚本自动跑。我习惯在testbench里加一个自动检查把CORDIC输出和预期值比较误差超过阈值就报错。阈值设2个LSB因为定点有量化误差。这样不用人工看波形跑完看log就行。注意MATLAB的sin是双精度CORDIC是定点对比时要先把MATLAB结果量化到同样位宽再比。否则会误判。9. 版本差异与IP升级注意事项Vivado不同版本CORDIC IP略有差异。比如2018.3和2022.2的配置界面基本一样但端口名可能从s_axis_phase_tdata变成s_axis_phase_tdata没变主要是Latency数值可能不同。升级IP时如果原来工程时序刚好过升级后可能变差。建议升级后重新跑实现看WNS。另外Vivado 2020.2之后CORDIC IP支持更多器件但老器件如Spartan-6不支持。如果你用老器件得用ISE的CORDIC或者自己写。License方面CORDIC IP是免费包含在Vivado所有版本里的不需要额外license。WebPACK也能用。这点很良心。10. 自己写CORDIC还是用IP最后聊个实际问题既然CORDIC原理不复杂要不要自己写我的经验是如果只是算sin/cos用IP最省事配置一下就行时序和资源都优化过。但如果你的需求特殊比如要算双曲函数、或者要自定义迭代次数、或者要和其他逻辑深度融合自己写更灵活。自己写CORDIC的核心是一个迭代状态机每次根据当前角度符号决定旋转方向累加角度和坐标。代码大概几十行。但要注意增益补偿和定点溢出。我早期自己写过仿真对了上板偶尔出错查了半天是某个中间结果溢出。后来改用IP省心。所以我的建议新手直接用IP把精力放在系统集成上老手如果IP满足不了再自己写。IP的文档在Xilinx官网搜PG105里面公式和时序图都有值得翻一遍。实操心得用IP时把PG105的“Latency”和“Interface”两节打印出来放旁边配置和调试时对照看比在网上搜零散答案快得多。这个模块我后来复用到好几个项目里从电机控制到通信混频基本没改过配置。唯一要注意的是每次换器件或换Vivado版本重新跑一遍时序确认WNS。CORDIC IP的稳定性很好只要握手接对、定点格式匹配结果就靠谱。如果你在调试中遇到奇怪现象先抓接口波形再看定点格式九成问题出在这两处。
返回列表