CORDIC(坐标旋转数字算法)是一种计算三角、双曲和其他数学函数的有效方法。它是一种数字算法,每次运算均产生一次结果输出。这使我们能够根据应用需求调整算法精度;增加运算迭代次数可以得到更精确的结果。
CORDIC是只使用加法、减法、移位和查找表实现的简单算法,这种算法在FPGA中实现效率高,在硬件算法实现中经常用到。
CORDIC LV模式 原理
在这里CORDIC算法LV模式从输入的 ( x , y ) (x,y) (x,y)向量为起始点进行线性伪旋转,即横坐标x不变,纵坐标不断向x轴伪旋转角度 θ \theta θ,直到y变为0。如下所示从 ( x i , y i ) (x_i,y_i) (xi,yi)伪旋转到 ( x i + 1 , y i + 1 ) (x_{i+1},y_{i+1}) (xi+1,yi+1):
x i + 1 = x i = x 0 x_{i+1} = x_i = x_0 xi+1=xi=x0
y i + 1 = y i + x i ∗ t a n ( θ i ) y_{i+1} = y_i + x_i*tan(θ_i) yi+1=yi+xi∗tan(θi)
注意到由于 x x x是不变的,那么就有 x i ∗ t a n ( θ i ) = x i ∗ △ y x 0 = △ y x_i*tan(θ_i)= x_i * \frac {△y} {x_0} = △y xi∗tan(θi)=xi∗x0△y=△y,即下图伪旋转变化。
在CORDIC算法的LV模式下我们希望求除法计算,即对输入的点 ( x , y ) (x,y) (x,y)求除法 y x \frac y x xy,即 t a n θ tan\theta tanθ。注意到如上推导的 x i ∗ t a n ( θ i ) = x i ∗ △ y x 0 = △ y x_i*tan(θ_i) = x_i * \frac {△y} {x_0} = △y xi∗tan(θi)=xi∗x0△y=△y,当 y 0 y_0 y0伪旋转到0时,累计了一系列了的 θ i \theta_i θi旋转,他们的和如下推导:
∑ 0 < i < n tan θ i = ∑ 0 < i < n y i x 0 = y 0 x 0 \sum_{0<i<n} \tan \theta_{i}=\sum_{0<i<n} \frac{y_{i}}{x_{0}}=\frac{y_{0}}{x_{0}} 0<i<n∑tanθi=0<i<n∑x0yi=x0y0
注意伪旋转的角度和 θ \theta θ取指范围为
∑ 0 < i < n θ i = θ ∈ [ − π 2 , π 2 ] \sum_{0<i<n} \theta_{i}=\theta \in\left[-\frac{\pi}{2}, \frac{\pi}{2}\right] 0<i<n∑θi=θ∈[−2π,2π]
则可以总结推导的公式如下:
x = { x i + 1 = x 0 y i + 1 = y i + σ i x i t a n ( θ i ) z i + 1 = z i − σ i t a n ( θ i ) x = \begin{cases} x_{i+1} = x_0 \\ y_{i+1} = y_i + \sigma_i x_i tan(θ_i) \\ z_{i+1} = z_i - \sigma_i tan(θ_i) \end{cases} x=⎩⎪⎨⎪⎧xi+1=x0yi+1=yi+σixitan(θi)zi+1=zi−σitan(θi)
σ i = { + 1 , y i < 0 − 1 , y i > = 0 \sigma_i = \begin{cases} +1 ,y_i<0 \\ -1 , y_i>=0 \\ \end{cases} σi={+1,yi<0−1,yi>=0
最终 y n y_n yn趋近与0,可得到如下的结果:
x = { x n = x 0 y n = 0 z n = ∑ 0 < i < n − σ i tan ( θ i ) x=\left\{\begin{array}{l} x_{n}=x_{0} \\ y_{n}=0 \\ z_{n}=\sum_{0<i<n}-\sigma_{i} \tan \left(\theta_{i}\right) \end{array}\right. x=⎩⎨⎧xn=x0yn=0zn=∑0<i<n−σitan(θi)
则 z n z_n zn是输出的结果 y x \frac y x xy。
硬件计算优化
如上可以发现还是有两个乘法在其中,对于实现硬件代价较高。可以考虑**乘以2的任意幂可以转变为移位操作。**如果我们将旋转矩阵中的常量设置为2的幂,我们可以非常容易地执行旋转而不需要乘法。
如果我们限制 t a n ( θ i ) tan(θ_i) tan(θi) 的值是2的幂次,那么旋转运算可以简化为数据移位(乘法)和加法。具体为,我们设 t a n ( θ i ) = 2 − i tan(θ_i)=2^{−i} tan(θi)=2−i ,则变为
x = { x i + 1 = x 0 y i + 1 = y i + σ i x i 2 − i z i + 1 = z i − σ i 2 − i x = \begin{cases} x_{i+1} = x_0 \\ y_{i+1} = y_i + \sigma_i x_i 2^{-i}\\ z_{i+1} = z_i - \sigma_i 2^{-i} \end{cases} x=⎩⎪⎨⎪⎧xi+1=x0yi+1=yi+σixi2−izi+1=zi−σi2−i
在每次迭代中,我们需要知道刚刚执行的旋转角
θ
i
θ_i
θi,其中
θ
i
=
a
r
c
t
a
n
(
2
−
i
)
θ_i=arctan(2^{−i})
θi=arctan(2−i)。我们可以提前计算每一个i对应的
θ
i
θ_i
θi 值,然后把它们存储在片上内存中,之后我们可以像用查找表一样用这些值.
输入输出范围优化
由上述硬件计算优化小节分析,我们可以推导出 y n y_n yn到 y 0 y_0 y0的跌到的过程如下:
∑ 0 < i < n y i = ∑ 0 < i < n σ i x i 2 − i \sum_{0<i<n} y_{i}=\sum_{0<i<n} \sigma_{i} x_{i} 2^{-i} 0<i<n∑yi=0<i<n∑σixi2−i
取极限使得 σ i \sigma_i σi始终为正,即向量与x轴夹角为一个较大的正数度数;同时由于 x i = x 0 x_i=x_0 xi=x0;且线性推导最终 y i y_i yi趋近于0,其和为 y 0 y_0 y0,则有如下的式子:
y 0 = x 0 ∑ 0 < i < n 2 − i y_{0}=x_{0} \sum_{0<i<n} 2^{-i} y0=x00<i<n∑2−i
对式子中后者取极限,n趋近于正无穷,可得到
lim n → > ∞ ∑ 0 < i < n 2 − i = 2 \lim _{n \rightarrow>\infty} \sum_{0<i<n} 2^{-i}=2 n→>∞lim0<i<n∑2−i=2
所以可以得到输入输出的范围为:
−
2
<
=
y
x
<
=
2
−
2
<
=
z
<
=
2
\begin{aligned} &-2<=\frac{y}{x}<=2 \\ &-2<=z<=2 \end{aligned}
−2<=xy<=2−2<=z<=2
当然我们的除法肯定不能只实现如上范围的输入,为了扩充除法的范围到实数的限制,我们可以将
x
,
y
x,y
x,y都通过二进制位移(硬件成本较低)与提出正负号,将范围移动到
[
0.5
,
1
]
[0.5,1]
[0.5,1]得到
x
′
,
y
′
x',y'
x′,y′,将
x
′
,
y
′
x',y'
x′,y′的除法计算出来后,再二进制移位与添加正负号还原即可。
HLS代码思路
- 它将原坐标系下的向量表示 ( x 0 , y 0 ) (x_0,y_0) (x0,y0)作为输入,输出这除法 y x \frac y x xy结果 z z z
- 我们假设cordic.h文件定义不同的数据类型(例如,COS_SIN_TYPE和THETA_TYPE)并设置NUM_ITERATIONS为某个常数。
- 数据类型可以更改为不同的定点或浮点类型,设置NUM_ITERATIONS值要同时考虑我们期望的精度、资源和吞吐量。
- 单独定义移位到 [ 0.5 , 1 ] [0.5,1] [0.5,1]范围的模块,返回移位后的值、移位数量(左移为正,右移为负)和是否变换了符号。模块通过移位直到进入范围确定结果,移位的最高移位数一定大于等于可以表示的二进制位数。
#define THETA_TYPE float
#define COS_SIN_TYPE float
#define NUM_ITERATIONS 50
void shift_2_range(COS_SIN_TYPE x, COS_SIN_TYPE &out, int &cnt, int &flag){
// change signed number symbol
if(x<0){
x = -x;
flag = -1;
}else{
flag = 1;
}
if(x<0.5){
for(int i = 32;i>1;i++){ // 此左右位移数目可以依照定点数位数改变
if((x<<i) >= 0.5) out = x,cnt = i;
}
}else if(x>1){
for(int i = 1;i<32;i++){ // 此左右位移数目可以依照定点数位数改变
if((x>>i) <= 1) out = x,cnt = -i;
}
}else{
out = x;
cnt = 0;
}
}
void cordic_cv(COS_SIN_TYPE x_in, COS_SIN_TYPE y_in, THETA_TYPE &theta, THETA_TYPE &r)
{
// Handle the input range to [0.5,1]
COS_SIN_TYPE x, y;
int x_cnt=0, x_flag=0, y_cnt=0, y_flag=0;
shift_2_range(x_in,x,x_cnt,x_flag);
shift_2_range(y_in,y,y_cnt,y_flag);
// Set the initial vector that we wil rorate
COS_SIN_TYPE z = 0;
// This loop iteratively rotates the initial vector to find the
// sine and cosine vlaue corresponding to the input theta angle
for(int j = 0;j < NUM_ITERATIONS; j++){
// Determine if we are rotating by a positive or negative angle
int sigma = (y < 0) ? 1 : -1;
if(y<0){
y = (x >> j) + y;
z = z - 1>>j;
}else{
y = -(x >> j) + y;
z = z + 1>>j;
}
}
// shift z and sign z to real value
if((x_flag == -1 and y_flag == 1) or (x_flag == 1 and y_flag == -1)){
z = -z;
}
int shift_cnt = y_cnt-x_cnt;
if(shift_cnt <0) z = z << shift_cnt;
else z = z >> -shift_cnt;
}
Chisel 代码实现
定点数定义
首先定义定点数,注意在chisel.experimental包里有定点数的类
import chisel3._
import chisel3.util._
import chisel3.experimental._
import scala.collection.immutable
import scala.math._
/* 定点数的位宽定义 */
trait HasDataConfig {
val DataWidth = 24
val BinaryPoint = 10
}
源码实现
首先定义全局上使用的一些常量
/* Cordic 的全局常数定义 */
trait Cordic_Const_Data extends HasDataConfig{
/* 迭代次数配置 */
val NUM_ITERATIONS = 20
}
实现codic_lv算法,直接按照公式迭代流水线寄存器计算即可
class CORDIC_LV_ORIGIN extends Module with HasDataConfig with Cordic_Const_Data{
/*
* @x : 输入的向量横轴坐标 定点数类型 取值范围[0.5,1]
* @y : 输如的向量横轴坐标 定点数类型 取值范围[0.5,1]
* @z : 输出的除法结果
* details: 利用cordic圆坐标系的迭代得到除法的近似值,建议迭代次数不超过30,
在25次时,K值的变化已经超过了float的范围
**/
val io = IO(new Bundle {
val x: FixedPoint = Input(FixedPoint(DataWidth.W, BinaryPoint.BP))
val y: FixedPoint = Input(FixedPoint(DataWidth.W, BinaryPoint.BP))
val z: FixedPoint = Output(FixedPoint(DataWidth.W, BinaryPoint.BP))
/* Debug */
// val sin_o: Vec[FixedPoint] = Output(Vec(NUM_ITERATIONS, FixedPoint(DataWidth.W, BinaryPoint.BP)))
// val cos_o: Vec[FixedPoint] = Output(Vec(NUM_ITERATIONS, FixedPoint(DataWidth.W, BinaryPoint.BP)))
// val z: Vec[FixedPoint] = Output(Vec(NUM_ITERATIONS, FixedPoint(DataWidth.W, BinaryPoint.BP)))
})
/* 初始化计算的寄存器数组,形成NUM_ITERATIONS级流水 */
val current_x: Vec[FixedPoint] = RegInit(VecInit(Seq.fill(NUM_ITERATIONS)(FixedPoint.fromDouble(0.0, DataWidth.W, BinaryPoint.BP)))) // cos
val current_y: Vec[FixedPoint] = RegInit(VecInit(Seq.fill(NUM_ITERATIONS)(FixedPoint.fromDouble(0.0, DataWidth.W, BinaryPoint.BP)))) // sin
val current_z: Vec[FixedPoint] = RegInit(VecInit(Seq.fill(NUM_ITERATIONS)(FixedPoint.fromDouble(0.0, DataWidth.W, BinaryPoint.BP)))) // 目标角度
for (i <- 0 until NUM_ITERATIONS) {
/*
* x[i] = K(x[i-1] - sigma * 2^(-i) * y[i-1])
* y[i] = K(y[i-1] + sigma * 2^(-i) * x[i-1])
**/
if (i == 0) {
/* 流水线第一级直接对(io.x,io.y)点做运算,注意输入限制了范围 */
current_x(0) := io.x
current_y(i) := io.y - (io.x >> i)
current_z(i) := FixedPoint.fromDouble(1.0, DataWidth.W, BinaryPoint.BP)
} else {
current_x(i) := current_x(i - 1)
when(current_y(i - 1) < FixedPoint.fromDouble(0.0, DataWidth.W, BinaryPoint.BP)) {
current_y(i) := current_y(i - 1) + (current_x(i - 1) >> i)
current_z(i) := current_z(i - 1) - (FixedPoint.fromDouble(1.0, DataWidth.W, BinaryPoint.BP) >> i)
}.otherwise {
current_y(i) := current_y(i - 1) - (current_x(i - 1) >> i)
current_z(i) := current_z(i - 1) + (FixedPoint.fromDouble(1.0, DataWidth.W, BinaryPoint.BP) >> i)
}
}
/* Debug */
// io.cos_o(i) := current_x(i)
// io.sin_o(i) := current_y(i)
// io.z(i) := current_theta(i)
}
io.z := current_z(NUM_ITERATIONS - 1)
}
实现实数除法
按照如上讲解的二进制移位方法
首先单独定义移位到 [ 0.5 , 1 ] [0.5,1] [0.5,1]范围的模块,返回移位后的值、移位数量(左移为正,右移为负)和是否变换了符号。模块通过移位直到进入范围确定结果,移位的最高移位数一定大于等于可以表示的二进制位数。
其中使用的chisel自带的硬件优先编码器得到移位的数值。
class shift_2_range extends Module with HasDataConfig{
/*
* @x : 输入的待移位数 定点数类型 取值范围R
* @out : 输出移位后的结果 范围[0.5,1]
* @cnt : 输出的移位数量,左移为正,右移为负
* @flag : 输出x的正负符号分别为1,0
* details: 将x通过左右移位至范围[0.5,1],并输出移位数目和是否改变符号
**/
val io = IO(new Bundle {
val x: FixedPoint = Input(FixedPoint(DataWidth.W, BinaryPoint.BP))
val out: FixedPoint = Output(FixedPoint(DataWidth.W, BinaryPoint.BP))
val cnt: SInt = Output(SInt(log2Ceil(DataWidth).W))
val flag: UInt = Output(UInt(1.W))
})
val temp_x: FixedPoint = Wire(FixedPoint(DataWidth.W, BinaryPoint.BP))
when(io.x < FixedPoint.fromDouble(0.0, DataWidth.W, BinaryPoint.BP)){
temp_x := -io.x
io.flag := 0.U
}.otherwise{
temp_x := io.x
io.flag := 1.U
}
when(temp_x < FixedPoint.fromDouble(0.5, DataWidth.W, BinaryPoint.BP)){
/* 比0.5小,需要左移 */
val index = VecInit(Seq.fill(BinaryPoint)(0.B))
for(i <- 0 until BinaryPoint){
when((temp_x << i) < FixedPoint.fromDouble(0.5, DataWidth.W, BinaryPoint.BP)){
index(i) := 0.B
}.otherwise{
index(i) := 1.B
}
}
/* 优先编码器获得首先大于等于0.5的位置,即cnt的值 */
val temp_cnt = PriorityEncoder(index.asUInt())
io.cnt := temp_cnt.asSInt()
io.out := (temp_x << temp_cnt)
}.elsewhen(temp_x > FixedPoint.fromDouble(0.5, DataWidth.W, BinaryPoint.BP)){
/* 比1大,需要右移 */
val index = VecInit(Seq.fill(DataWidth)(0.B))
for(i <- 0 until DataWidth){
when((temp_x >> i) > FixedPoint.fromDouble(1, DataWidth.W, BinaryPoint.BP)){
index(i) := 0.B
}.otherwise{
index(i) := 1.B
}
}
/* 优先编码器获得首先小于等于1的位置,即cnt的值 */
val temp_cnt = PriorityEncoder(index.asUInt())
io.cnt := -(temp_cnt.asSInt())
io.out := (temp_x >> temp_cnt)
}.otherwise{
io.cnt := 0.S
io.out := temp_x
}
}
/*
* 定义这个类的伴生对象,并定义一个工厂方法来简化模块的例化和连线。
**/
object shift_2_range {
def apply(x: FixedPoint): (FixedPoint, SInt, UInt) = {
/*
* @x : 输入的待移位数 定点数类型 取值范围R
* @out : 输出移位后的结果 范围[0.5,1]
* @cnt : 输出的移位数量,左移为正,右移为负
* @flag : 输出x的正负符号分别为1,0
* details: 将x通过左右移位至范围[0.5,1],并输出移位数目和是否改变符号
**/
val unit = Module(new shift_2_range)
unit.io.x := x
(unit.io.out, unit.io.cnt, unit.io.flag)
}
}
最后调用各个模块组合成实数除法
class cordic_divide extends Module with HasDataConfig {
/*
* @x : 输入的向量横轴坐标 定点数类型 取值范围R
* @y : 输如的向量横轴坐标 定点数类型 取值范围R
* @z : 输出的除法结果
* details: 利用cordic圆坐标系的迭代得到除法的近似值,通过二进制移位将x、y
范围转移到[0.5,1],再记录符号,最后调用cordic_lv并处理输出值,
建议迭代次数不超过30,在25次时,K值的变化已经超过了float的范围
**/
val io = IO(new Bundle {
val x: FixedPoint = Input(FixedPoint(DataWidth.W, BinaryPoint.BP))
val y: FixedPoint = Input(FixedPoint(DataWidth.W, BinaryPoint.BP))
val z: FixedPoint = Output(FixedPoint(DataWidth.W, BinaryPoint.BP))
})
/* 使用二进制移位规范x,y的值到[0.5,1]区间 */
val (norm_x,x_cnt,x_flag) = shift_2_range(io.x)
val (norm_y,y_cnt,y_flag) = shift_2_range(io.y)
val cordic_lv: CORDIC_LV_ORIGIN = Module(new CORDIC_LV_ORIGIN)
cordic_lv.io.x := norm_x
cordic_lv.io.y := norm_y
val unnormed_z: FixedPoint = Wire(FixedPoint(DataWidth.W, BinaryPoint.BP))
/* 将z的值二进制移位回正确区间 */
when(((x_flag === 1.U) && (y_flag === 0.U)) || ((x_flag === 0.U) && (y_flag === 1.U))){
unnormed_z := -cordic_lv.io.z;
}.otherwise{
unnormed_z := cordic_lv.io.z;
}
val shift_cnt = y_cnt - x_cnt
when(shift_cnt < 0.S){
io.z := unnormed_z << (-shift_cnt).asUInt()
}.otherwise{
io.z := unnormed_z >> shift_cnt.asUInt()
}
}
测试
对实现的三个类都进行测试,包括二进制移位。
import org.scalatest._
import chisel3._
import chiseltest._
import chisel3.experimental._
class Cordic_LV_Tester extends FlatSpec with HasDataConfig with ChiselScalatestTester with Matchers {
behavior of "mytest2"
it should "do something" in{
val NUM_ITERATIONS = 30
test(new CORDIC_LV_ORIGIN){ c =>
c.io.x.poke(FixedPoint.fromDouble(0.75,32.W, 20.BP))
c.io.y.poke(FixedPoint.fromDouble(0.79,32.W, 20.BP))
for(i <- 0 until NUM_ITERATIONS*2){
println(s"*****************${i}******************")
println(s"theta ${c.io.z.peek}")
// for(j <- 0 until NUM_ITERATIONS){
// println(s"sin_o (${j}) ${c.io.sin_o(j).peek}")
// println(s"cos_o (${j}) ${c.io.cos_o(j).peek}")
// println(s"theta_o(${j}) ${c.io.theta_o(j).peek}")
// }
c.clock.step(1)
}
}
test(new shift_2_range){
c =>
c.io.x.poke(FixedPoint.fromDouble(-5,32.W, 20.BP))
println(s"out ${c.io.out.peek}")
println(s"cnt ${c.io.cnt.peek}")
println(s"flag ${c.io.flag.peek}")
c.io.x.poke(FixedPoint.fromDouble(-25,32.W, 20.BP))
println(s"out ${c.io.out.peek}")
println(s"cnt ${c.io.cnt.peek}")
println(s"flag ${c.io.flag.peek}")
c.io.x.poke(FixedPoint.fromDouble(-0.00631,32.W, 20.BP))
println(s"out ${c.io.out.peek}")
println(s"cnt ${c.io.cnt.peek}")
println(s"flag ${c.io.flag.peek}")
c.io.x.poke(FixedPoint.fromDouble(-0.00013545,32.W, 20.BP))
println(s"out ${FixedPoint.fromDouble(-0.00013545,32.W, 20.BP)}")
println(s"out ${c.io.out.peek}")
println(s"cnt ${c.io.cnt.peek}")
println(s"flag ${c.io.flag.peek}")
}
test(new cordic_divide){ c =>
c.io.x.poke(FixedPoint.fromDouble(-0.00631,32.W, 20.BP))
c.io.y.poke(FixedPoint.fromDouble(0.00013545,32.W, 20.BP))
for(i <- 0 until NUM_ITERATIONS*2){
println(s"*****************${i}******************")
println(s"theta ${c.io.z.peek}")
// for(j <- 0 until NUM_ITERATIONS){
// println(s"sin_o (${j}) ${c.io.sin_o(j).peek}")
// println(s"cos_o (${j}) ${c.io.cos_o(j).peek}")
// println(s"theta_o(${j}) ${c.io.theta_o(j).peek}")
// }
c.clock.step(1)
}
}
}
}
封装伴生对象
/*
* 定义这个类的伴生对象,并定义一个工厂方法来简化模块的例化和连线。
**/
object cordic_divide{
def apply(x: FixedPoint, y:FixedPoint): FixedPoint = {
/*
* @x : 输入的向量横轴坐标 定点数类型 取值范围R
* @y : 输如的向量横轴坐标 定点数类型 取值范围R
* @z : 输出的除法结果
* details: 利用cordic圆坐标系的迭代得到除法的近似值,通过二进制移位将x、y
范围转移到[0.5,1],再记录符号,最后调用cordic_lv并处理输出值,
建议迭代次数不超过30,在25次时,K值的变化已经超过了float的范围
**/
val divide = Module(new cordic_divide)
divide.io.x := x
divide.io.y := y
divide.io.z
}
}