CORDIC(坐标旋转数字算法)是一种计算三角、双曲和其他数学函数的数字算法,每次运算均产生一次结果输出。这使我们能够根据应用需求调整算法精度;增加运算迭代次数可以得到更精确的结果。CORDIC 是只使用加法、减法、移位和查找表实现的简单算法,这种算法在FPGA中实现效率高,在硬件算法实现中经常用到。
论文[1]介绍了一种通过CORDIC算法中CR、CV、LV三种模式拆分高性能计算复数除法的方法,本文将使用Chisel 实现该方法,这里默认在以下几篇文章的CORDIC原理基础之上完成。
HLS / Chisel 实现CORDIC算法计算三角函数(圆周系统之旋转模式)
HLS / Chisel 实现CORDIC算法计算反三角函数(圆坐标系向量模式)
HLS / Chisel 实现CORDIC算法计算除法(线性坐标系向量模式LV)
原理
CORDIC背景
CORDIC算法有旋转和向量两个模式,分别可以在圆坐标系、线性坐标系和双曲线坐标系使用,从而可以演算出8种运算,而结合这8种运算也可以衍生出其他许多运算。下表展示了8种运算在CORDIC算法中计算的核心公式推导:
接下来我们将利用如上的CR、CV、LV三种模式实现复数除法
复数除法高性能计算
首先有如下的复数除法公式,我们一般会做如下的拆分:
Z = a + i b p + i q = a p + b q p 2 + q 2 + i b p − a q p 2 + q 2 \mathrm{Z}=\frac{a+i b}{p+i q}=\frac{a p+b q}{p^{2}+q^{2}}+i \frac{b p-a q}{p^{2}+q^{2}} Z=p+iqa+ib=p2+q2ap+bq+ip2+q2bp−aq
- CV模式转化为极坐标形式
首先输入如下的数值
X 0 = p , Y 0 = q , Z 0 = 0 \mathrm{X}_{0}=p, \quad Y_{0}=q, \quad Z_{0}=0 X0=p,Y0=q,Z0=0
在此反三角函数计算模块可以得到如下的结果:
X = K p 2 + q 2 = r , Y = 0 , Z = tan ( q / p ) − 1 = θ X=K \sqrt{p^{2}+q^{2}} = r, \quad \mathrm{Y}=0, \quad Z=\tan (q / p)^{-1}=\theta X=Kp2+q2=r,Y=0,Z=tan(q/p)−1=θ
其中K在CV模式的推导文章中有介绍,是一个固定的常数。接下来的步骤我们需要极坐标系的结果 ( r , θ ) (r,\theta) (r,θ)。
- LV模式计算实数除法
将1中的结果 X = r X=r X=r作为输入此模块,输入初始化为如下:
X 0 = K p 2 + q 2 , Y 0 = a , Z 0 = 0 \mathrm{X}_{0}=K \sqrt{p^{2}+q^{2}}, \quad Y_{0}=a, \quad Z_{0}=0 X0=Kp2+q2,Y0=a,Z0=0
最后通过实数除法得到如下的结果:
X = K p 2 + q 2 , Y = 0 , Z = a K p 2 + q 2 X=K \sqrt{p^{2}+q^{2}}, \quad \mathrm{Y}=0, \quad Z=\frac{a}{K \sqrt{p^{2}+q^{2}}} X=Kp2+q2,Y=0,Z=Kp2+q2a
同理再输入
X 0 = K p 2 + q 2 , Y 0 = b , Z 0 = 0 \mathrm{X}_{0}=K \sqrt{p^{2}+q^{2}}, \quad Y_{0}=b, \quad Z_{0}=0 X0=Kp2+q2,Y0=b,Z0=0
获得如下结果:
X = K p 2 + q 2 , Y = 0 , Z = b K p 2 + q 2 X=K \sqrt{p^{2}+q^{2}}, \quad \mathrm{Y}=0, \quad Z=\frac{b}{K \sqrt{p^{2}+q^{2}}} X=Kp2+q2,Y=0,Z=Kp2+q2b
- CR模式计算旋转推导出复数除法
将1的结果 Z = tan ( q / p ) − 1 = θ Z=\tan (q / p)^{-1}=\theta Z=tan(q/p)−1=θ,2中的结果 Z = a K p 2 + q 2 , Z = b K p 2 + q 2 Z=\frac{a}{K \sqrt{p^{2}+q^{2}}},Z=\frac{b}{K \sqrt{p^{2}+q^{2}}} Z=Kp2+q2a,Z=Kp2+q2b输入CR模块,输入如下:
X 0 = a K p 2 + q 2 , Y 0 = b K p 2 + q 2 , Z 0 = − θ \mathrm{X}_{0}=\frac{a}{K \sqrt{p^{2}+q^{2}}}, \quad Y_{0}=\frac{b}{K \sqrt{p^{2}+q^{2}}}, \quad Z_{0}=-\theta X0=Kp2+q2a,Y0=Kp2+q2b,Z0=−θ
已知CR模式的推导公式如下:
K ( x cos z − y sin z ) K ( y cos z + x sin z ) \begin{aligned} &K(x \cos z-y \sin z) \\ &K(y \cos z+x \sin z) \end{aligned} K(xcosz−ysinz)K(ycosz+xsinz)
已知
tan ( q / p ) − 1 = θ \tan (q / p)^{-1}=\theta tan(q/p)−1=θ
那么可以得到:
sin θ = q q 2 + p 2 cos θ = p q 2 + p 2 \begin{aligned} &\sin\theta = \frac q {\sqrt{q^2+p^2}} \\ &\cos\theta = \frac p {\sqrt{q^2+p^2}} \end{aligned} sinθ=q2+p2qcosθ=q2+p2p
将x,y,z带入CR模式的推导模式则可以得到:
X = a p + b q p 2 + q 2 Y = b p − a q p 2 + q 2 X=\frac{a p+b q}{p^{2}+q^{2}} \quad Y=\frac{b p-a q}{p^{2}+q^{2}} X=p2+q2ap+bqY=p2+q2bp−aq
即复数的实部和虚部计算完成。
输入输出范围限定
整个CORDIC计算复数除法的输入输出如上图。由于CR、CV、LV模块有输入输出的范围限制,则对于整个复数除法计算大模块的输入输出也有范围限制,首先我们单独讨论子模块:
- CR模块执行旋转功能,将输入的点(x,y)∈R旋转 θ ∈ [ − 90 ° , 90 ° ] \theta∈[-90°,90°] θ∈[−90°,90°]角度得到点(X,Y)
- CV模块执行极坐标转化(arctan计算),将输入的点(x>0,y∈R)即一四象限的点转化为极坐标(r∈R, θ ∈ [ − 90 ° , 90 ° ] \theta∈[-90°,90°] θ∈[−90°,90°])
- LV模块执行除法运算,在我们设计的模块中,我们使用二进制移位处理使得可以实现实数除法,所以 输入(x,y)∈R,输出z∈R
基于如上分析,我们分别对复数除法模块的a、b、q、p进行取值范围分析:
a、b参与的运算从LV模块开始,然后LV模块输出影响CR模块的输入。由于LV和CR的输入都是实数域R,所以输入a、b∈R
p、q参与了CV、CR、LV三个模块的运算,有以下分析。
a. 对于q<0,q>0
此时在首先CV模块的输入,(q,p)是在第二象限,由于CV模块的输入范围限制,我们可以将 ( q , p ) (q,p) (q,p)作原点对称到 ( q ‘ , p ‘ ) = ( − p , − q ) (q`,p`) = (-p,-q) (q‘,p‘)=(−p,−q),则计算出来的结果 θ \theta θ为(q`,p`) 对应的结果,但极坐标的 K p 2 + q 2 = r K \sqrt{p^{2}+q^{2}} = r Kp2+q2=r是没有任何影响的
然后在LV模块中计算由于r是不变化的,所以LV模块没有任何影响。
最后在CR模块中将 − θ -\theta −θ带入,得到的应该是
X = a p ‘ + b q ‘ p ‘ 2 + q ‘ 2 Y = b p ‘ − a q ‘ p ‘ 2 + q ‘ 2 X=\frac{a p`+b q`}{p`^{2}+q`^{2}} \quad Y=\frac{b p`-a q`}{p`^{2}+q`^{2}} X=p‘2+q‘2ap‘+bq‘Y=p‘2+q‘2bp‘−aq‘
带入 ( q ‘ , p ‘ ) = ( − p , − q ) (q`,p`) = (-p,-q) (q‘,p‘)=(−p,−q)即可以将负号加上就可以得到复数除法结果
X = − a p − b q p 2 + q 2 Y = − b p + a q p 2 + q 2 X=\frac{-a p-b q}{p^{2}+q^{2}} \quad Y=\frac{-b p+a q}{p^{2}+q^{2}} X=p2+q2−ap−bqY=p2+q2−bp+aq
Z = a + i b p + i q = a p + b q p 2 + q 2 + i b p − a q p 2 + q 2 = − a p ‘ + b q ‘ p ‘ 2 + q ‘ 2 − i b p ‘ − a q ‘ p ‘ 2 + q ‘ 2 \mathrm{Z}=\frac{a+i b}{p+i q}=\frac{a p+b q}{p^{2}+q^{2}}+i \frac{b p-a q}{p^{2}+q^{2}} = -\frac{a p`+b q`}{p`^{2}+q`^{2}}-i \frac{b p`-a q`}{p`^{2}+q`^{2}} Z=p+iqa+ib=p2+q2ap+bq+ip2+q2bp−aq=−p‘2+q‘2ap‘+bq‘−ip‘2+q‘2bp‘−aq‘
b.对于q<0,q<0
此时在首先CV模块的输入,(q,p)是在第三象限,同理如上小节方法也可以转换到第一象限,最后在第三步CR模块得到的处理也一样,即如下:
X = − a p − b q p 2 + q 2 Y = − b p + a q p 2 + q 2 X=\frac{-a p-b q}{p^{2}+q^{2}} \quad Y=\frac{-b p+a q}{p^{2}+q^{2}} X=p2+q2−ap−bqY=p2+q2−bp+aq
c.对于q>0,q>0 或 q>0,q<0
此时首先CV模块的输入是在允许的一四象限处理范围内,可以直接计算得到正确的结果
X = a p + b q p 2 + q 2 Y = b p − a q p 2 + q 2 X=\frac{a p+b q}{p^{2}+q^{2}} \quad Y=\frac{b p-a q}{p^{2}+q^{2}} X=p2+q2ap+bqY=p2+q2bp−aq
HLS实现
- 定义复数
- 对输入的数做处理转移到输入合适的区间
#define COS_SIN_TYPE float
#define NUM_ITERATIONS 50
typedef struct { COS_SIN_TYPE re, im; } Complex;
void cordic_complex_divide(Complex dividend, Complex divisor, Complex &z)
{
COS_SIN_TYPE a,b,p,q;
a = dividend.re;
b = dividend.im;
p = divisor.re;
q = divisor.im;
bool flag = 1; // 默认不改变最后输出实部虚部的符号
if((p < 0 and q > 0) or (p < 0 and q < 0)){
// 将2,3象限的点旋转到1,4象限,方便CV模块操作
p = -p;
q = -q;
flag = 0;
}
COS_SIN_TYPE theta, r, arctan; // arctan为舍弃的
cordic_cv(p, q, r, theta, arctan);
COS_SIN_TYPE temp_divide_a, temp_divide_b;
cordic_lv(r, a, temp_divide_a);
cordic_lv(r, b, temp_divide_b);
cordic_cr(x = temp_divide_a, y = temp_divide_b, theta = -theta, z.re, z.im);
if(flag) {
z.re = -z.re;
z.im = -z.im;
}
}
Chisel实现
定义数据类型
import chisel3._
import chisel3.experimental._
trait HasDataConfig {
/* 定点数的位宽定义 */
val DataWidth = 32
val BinaryPoint = 20
}
class Complex extends Bundle with HasDataConfig{
/* 复数的 实部,虚部*/
val re: FixedPoint = FixedPoint(DataWidth.W, BinaryPoint.BP)
val im: FixedPoint = FixedPoint(DataWidth.W, BinaryPoint.BP)
}
class ComplexOperationIO extends Bundle with HasDataConfig{
val op1: Complex = Input(new Complex())
val op2: Complex = Input(new Complex())
val res: Complex = Output(new Complex())
}
源码
import chisel3._
import chisel3.experimental._
class cordic_complex_divide extends Module with HasDataConfig with Cordic_Const_Data {
/*
* @io.op1 : 输入的被除数 定点数表示的复数Complex 输入范围R
* @io.op2 : 输入的除数 定点数表示的复数Complex 输入范围R
* @io.res : 输出的复数除法结果 定点数表示的复数Complex 范围R
* details: 利用cordic实现复数除法,周期为3*迭代次数NUM_ITERATIONS
**/
val io = IO(new ComplexOperationIO)
val a = WireInit(io.op1.re)
val b = WireInit(io.op1.im)
val p = Wire(FixedPoint(DataWidth.W,BinaryPoint.BP))
val q = Wire(FixedPoint(DataWidth.W,BinaryPoint.BP))
/* (p,q)所在象限必须调整到一四象限才能在CV阶段输入,最后通过flag再回调即可,flag 需要存储NUM_ITERATIONS个流水层级到最后输出时使用 */
val flag_reg: Vec[Bool] = RegInit(VecInit(Seq.fill(NUM_ITERATIONS*3)(1.B)))
when(io.op2.re < FixedPoint.fromDouble(0,DataWidth.W,BinaryPoint.BP)){
p := -io.op2.re
q := -io.op2.im
flag_reg(0) := 0.B
}.otherwise{
p := io.op2.re
q := io.op2.im
flag_reg(0) := 1.B
}
for(i <- 1 until NUM_ITERATIONS*3){
flag_reg(i) := flag_reg(i-1)
}
/* a,b为LV模块的输入,但是需要存储保存NUM_ITERATIONS个流水层级到LV模块使用 */
val a_reg: Vec[FixedPoint] = RegInit(VecInit(Seq.fill(NUM_ITERATIONS)(FixedPoint.fromDouble(0.0, DataWidth.W, BinaryPoint.BP))))
val b_reg: Vec[FixedPoint] = RegInit(VecInit(Seq.fill(NUM_ITERATIONS)(FixedPoint.fromDouble(0.0, DataWidth.W, BinaryPoint.BP))))
for(i <- 0 until NUM_ITERATIONS){
if(i == 0){
a_reg(0) := a
b_reg(0) := b
}else{
a_reg(i) := a_reg(i-1)
b_reg(i) := b_reg(i-1)
}
}
/* CV模块得到极坐标坐标系结果 */
val (theta, r, arctan) = cordic_cv(p,q)
/* theta为CV模块的输出,但是需要存储保存NUM_ITERATIONS个流水层级到CR模块使用 */
val theta_reg: Vec[FixedPoint] = RegInit(VecInit(Seq.fill(NUM_ITERATIONS)(FixedPoint.fromDouble(0.0, DataWidth.W, BinaryPoint.BP))))
for(i <- 0 until NUM_ITERATIONS){
if(i == 0){
theta_reg(0) := theta
}else{
theta_reg(i) := theta_reg(i-1)
}
}
/* LV模块并行计算得到两个除法结果 */
val z_a = cordic_divide(r, a_reg(NUM_ITERATIONS-1))
val z_b = cordic_divide(r, b_reg(NUM_ITERATIONS-1))
/* CR旋转点计算复数除法 */
val (re, im) = cordic_cr(z_a, z_b, -theta_reg(NUM_ITERATIONS-1))
when(flag_reg(NUM_ITERATIONS*3-1)){
io.res.re := re
io.res.im := im
}.otherwise{
io.res.re := -re
io.res.im := -im
}
}
object cordic_complex_divide_APP extends App {
println(getVerilogString(new cordic_complex_divide))
(new chisel3.stage.ChiselStage).emitVerilog(new cordic_complex_divide)
}
定义伴生对象工厂方法
最后我们定义两个个伴生对象工厂方法来方便直接调用函数,无需多加连线
/*
* 定义这个类的伴生对象,并定义一个工厂方法来简化模块的例化和连线。
**/
object cordic_complex_divide {
def apply(divided: Complex, divisor: Complex): Complex = {
/*
* @divided : 输入的被除数 定点数表示的复数Complex 输入范围R
* @divisor : 输入的除数 定点数表示的复数Complex 输入范围R
* @res : 输出的复数除法结果 定点数表示的复数Complex 范围R
* details: 利用cordic实现复数除法,周期为3*迭代次数NUM_ITERATIONS
**/
val unit = Module(new cordic_complex_divide)
unit.io.op1 := divided
unit.io.op2 := divisor
unit.io.res
}
}
参考论文
[1] Yang B , Dong W , Liu L . Complex division and square-root using CORDIC. IEEE, 2012.