4. CORDIC算法原理:从数学推导到FPGA实现
各位同学,今天我们来啃一块硬骨头——CORDIC算法。
说实话,我第一次接触CORDIC是在做电机控制项目的时候。当时需要实时计算sin和cos值,查表法太占资源,泰勒展开又太慢。后来老工程师甩给我一句话:「去学学CORDIC,这玩意儿只用移位和加法就能算三角函数。」
嗯,今天我就把这块硬骨头掰碎了喂给你们。
4.1 为什么需要CORDIC?
在伺服控制中,我们经常需要计算三角函数、反正切、平方根等。传统方法有几种:
- 查表法:精度越高,表越大。一个16位精度的sin表,要存65536个值,太费BRAM了。
- 泰勒展开:需要乘法器,迭代次数多,收敛慢。
- CORDIC:只需要移位和加法,没有乘法器,硬件开销极小。
说白了,CORDIC就是为FPGA量身定做的算法。它用迭代的方式,把复杂的数学运算变成了简单的加减和移位。
核心思想:通过旋转向量,逐步逼近目标角度。每次旋转的角度是预先算好的,旋转操作只用移位和加法实现。
4.2 CORDIC算法推导
我们先从几何角度理解。假设有一个向量 (x, y),我们想把它旋转 θ 角度。旋转后的坐标是:
x' = x·cosθ - y·sinθ
y' = x·sinθ + y·cosθ
提取公因子 cosθ:
x' = cosθ · (x - y·tanθ)
y' = cosθ · (y + x·tanθ)
关键来了。如果我们让每次旋转的角度 θ_i 满足 tanθ_i = 2^(-i),那么乘以 tanθ_i 就变成了右移 i 位的操作。这在硬件里就是一根线,不花任何资源。
我当年推导到这里的时候,拍了一下桌子——太巧妙了!
每次旋转的公式变成:
x_{i+1} = x_i - d_i · y_i · 2^(-i)
y_{i+1} = y_i + d_i · x_i · 2^(-i)
z_{i+1} = z_i - d_i · θ_i
其中 d_i 是旋转方向(+1 或 -1),θ_i = arctan(2^(-i)) 是预先算好的常数。
每次旋转后,向量长度会变化。所有迭代完成后,需要乘以一个缩放因子 K:
K = ∏ cos(θ_i) = ∏ 1/√(1 + 2^(-2i))
当迭代次数足够多时,K ≈ 1.64676。这个值也是常数,最后乘一次就行。
我的经验:实际项目中,我一般做16次迭代。精度够用,资源也省。如果你需要更高精度,做到20次也够了,再多收益不大。
4.3 旋转模式与向量模式
CORDIC有两种工作模式,就像一把刀的两个刃。
4.3.1 旋转模式
目标:给定角度 θ,计算 cosθ 和 sinθ。
做法:从向量 (1, 0) 开始,每次判断当前角度 z_i 与目标角度的差值。如果 z_i > 0,就逆时针旋转(d_i = +1);否则顺时针旋转(d_i = -1)。
迭代完成后:
x_n = K · cosθ
y_n = K · sinθ
最后乘上 1/K 就得到结果。
我在做永磁同步电机的位置观测器时,就用这个模式实时算角度。一个周期内算完,延迟只有几个时钟周期。
4.3.2 向量模式
目标:给定向量 (x, y),计算其角度和模长。
做法:每次旋转让 y 趋近于 0。如果 y_i > 0,就顺时针旋转(d_i = -1);否则逆时针旋转(d_i = +1)。
迭代完成后:
z_n = arctan(y/x)
x_n = K · √(x² + y²)
这个模式在伺服控制里太常用了。比如编码器信号处理,需要从正交信号里提取角度和幅值。
注意:向量模式要求输入向量在第一或第四象限。如果 x 为负,需要先做预处理。我曾经在这个坑里栽过跟头,仿真跑得好好的,上板子就出问题,查了半天才发现是象限没处理好。
4.4 在FPGA中的实现架构
好了,理论讲完了,咱们看看怎么在FPGA里搭出来。
4.4.1 总体架构
下面这张图是我自己画的CORDIC实现架构,你们感受一下:
架构分四块:
- 预处理:处理象限映射,把输入转换到第一或第四象限
- 流水线迭代单元:16级流水线,每级做一次旋转
- 角度查找表:存 arctan(2^(-i)) 的常数
- 后处理:乘上缩放因子 1/K
4.4.2 流水线设计细节
每一级流水线的结构很简单:
// 伪代码,每级流水线
always @(posedge clk) begin
if (z_reg >= 0) begin
x_next <= x_reg - (y_reg >> i);
y_next <= y_reg + (x_reg >> i);
z_next <= z_reg - atan_table[i];
end else begin
x_next <= x_reg + (y_reg >> i);
y_next <= y_reg - (x_reg >> i);
z_next <= z_reg + atan_table[i];
end
end
注意这里的移位操作。在Verilog里,右移就是直接连线,不占任何逻辑资源。这也是CORDIC能在FPGA上跑得飞快的原因。
数据位宽选择:我一般用16位定点数,1位符号位,1位整数位,14位小数位。这样角度精度能到0.005度左右,伺服控制完全够用。如果你做高精度应用,可以扩到24位。
4.4.3 资源与性能
以Xilinx Artix-7为例,16位CORDIC的资源消耗:
| 资源类型 | 消耗量 | 占比(以XC7A35T为例) |
|---|---|---|
| LUT | 约400个 | 2% |
| FF | 约500个 | 2.5% |
| DSP48 | 0个 | 0% |
| BRAM | 0个 | 0% |
看到了吗?一个DSP都不用,BRAM也不用。这就是CORDIC的魅力。
性能方面,16级流水线,时钟可以跑到200MHz以上。算一次sin/cos只需要16个时钟周期,延迟约80ns。对于伺服控制的电流环(通常10kHz-20kHz),绰绰有余。
4.5 避坑指南
最后,分享几个我踩过的坑:
- 数据溢出:迭代过程中,x和y的数值会变大。我建议位宽比输入多2-3位,防止溢出。
- 象限处理:输入角度范围是[-π, π],但CORDIC只能处理[-π/2, π/2]。需要做象限映射,算完后再映射回去。
- 缩放因子:别忘了最后乘1/K。我曾经忘了这步,结果算出来的sin值总是偏大,查了两天才发现。
- 流水线深度:不是越多越好。16级之后,精度提升很小,但资源消耗线性增长。性价比最高的就是16级。
好了,CORDIC的原理和实现就讲到这里。这个算法在伺服控制里无处不在——电流环的Park变换、位置环的角度计算、速度环的反正切...掌握了它,你就掌握了FPGA伺服控制的一把利器。
一句话总结:CORDIC用移位和加法,干掉了三角函数和反正切。硬件开销极小,速度极快。做伺服控制,必须得会。
公众号:蓝海资料掘金营,微信deep3321