编程知识:三角函数CORDIC算法(推导详解)

标签:
二分查找法减少乘法运算消除乘法运算storyofunifiedcordicc语言实现 |
分类: FA与PA:离散与过程自动化 |
【计算机如何计算三角函数值?】
三角函数的计算是个复杂的主题,有计算机之前,人们通常通过查找三角函数表来计算任意角度的三角函数的值。这种表格在人们刚刚产生三角函数的概念的时候就已经有了,它们通常是通过从已知值(比如sin(π/2)=1)开始并重复应用半角和和差公式而生成。现在有了计算机,三角函数表便推出了历史的舞台。但是像我这样的喜欢刨根问底的人,不禁要问计算机又是如何计算三角函数值的呢。最容易想到的办法就是利用级数展开,比如泰勒级数来逼近三角函数,只要项数取得足够多就能以任意的精度来逼近函数值。除了泰勒级数逼近之外,还有其他许多的逼近方法,比如切比雪夫逼近、最佳一致逼近和Padé逼近等。所有这些逼近方法本质上都是用多项式函数来近似我们要计算的三角函数,计算过程中必然要涉及到大量的浮点运算。在缺乏硬件乘法器的简单设备上(比如没有浮点运算单元的单片机),用这些方法来计算三角函数会非常的费时。
【CORDIC算法的诞生】
为了解决这个问题,J.
【第一步:从二分查找法说起】
先从一个例子说起,知道平面上一点在直角坐标系下的坐标(X,Y)=(100,200),如何求的在极坐标系下的坐标(ρ,θ)。用计算器计算一下可知答案是(223.61,63.435)。
图
为了突出重点,这里我们只讨论X和Y都为正数的情况。这时θ=atan(y/x)。求θ的过程也就是求atan函数的过程。Cordic算法采用的想法很直接,将(X,Y)旋转一定的度数,如果旋转完纵坐标变为了0,那么旋转的度数就是θ。坐标旋转的公式可能大家都忘了,这里把公式列出了。设(x,y)是原始坐标点,将其以原点为中心,顺时针旋转θ之后的坐标记为(x’,y’),则有如下公式:
也可以写为矩阵形式:
如何旋转呢,可以借鉴二分查找法的思想。我们知道θ的范围是0到90度。那么就先旋转45度试试。
旋转之后纵坐标为70.71,还是大于0,说明旋转的度数不够,接着再旋转22.5度(45度的一半)。
这时总共旋转了45+22.5=67.5度。结果纵坐标变为了负数,说明θ<67.5度,这时就要往回转,还是二分查找法的思想,这次转11.25度。
这时总共旋转了45+22.5-11.25=56.25度。又转过头了,接着旋转,这次顺时针转5.625度。
这时总共旋转了45+22.5-11.25+5.625=61.875度。这时纵坐标已经很接近0了。我们只是说明算法的思想,因此就不接着往下计算了。计算到这里我们给的答案是
可能有读者会问,计算中用到了
【以上方法的C语言实现】
#include
#include
double
int
{
double
printf("\n
return
}
double
{
const
0.04906767432742,0.02454122852291,0.01227153828572,0.006135884649154,0.003067956762966
,0.001533980186285,7.669903187427045e-4,3.834951875713956e-4,1.917475973107033e-4,
9.587379909597735e-5,4.793689960306688e-5,2.396844980841822e-5
};
const
0.9987954562052,0.9996988186962,0.9999247018391,0.9999811752826,0.9999952938096,
0.9999988234517,0.9999997058629,0.9999999264657,0.9999999816164,0.9999999954041,
0.999999998851,0.9999999997128
};
int
double
double
double
for(i
{
if(y
{
x_new
y_new
x
y
angleSum
}
else
{
x_new
y_new
x
y
angleSum
}
printf("Debug:
angle
}
return
}
程序运行的输出结果如下:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
z
【第二步:减少乘法运算】
现在已经有点
可以看出
省略cos(θ)后发生了什么呢,每次旋转后的新坐标点到原点的距离都变长了,放缩的系数是1/cos(θ)。不过没有关系,我们求的是θ,不关心ρ的改变。这样的变形非常的简单,但是每次循环的运算量一下就从4次乘法降到了2次乘法了。
【以上方法的C语言实现】
double
{
const
0.02454862210893,0.01227246237957,0.006136000157623,0.003067971201423,
0.001533981991089,7.669905443430926e-4,3.83495215771441e-4,1.917476008357089e-4,
9.587379953660303e-5,4.79368996581451e-5,2.3968449815303e-5
};
int
double
double
double
for(i
{
if(y
{
x_new
y_new
x
y
angleSum
}
else
{
x_new
y_new
x
y
angleSum
}
printf("Debug:
angle
}
return
}
计算的结果是:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
z
【第三步:消除乘法运算】
我们已经成功的将乘法的次数减少了一半,还有没有可能进一步降低运算量呢?还要从计算式入手。
第一次循环时,tan(45)=1,所以第一次循环实际上是不需要乘法运算的。第二次运算呢?
Tan(22.5)=0.4142135623731,很不幸,第二次循环乘数是个很不整的小数。是否能对其改造一下呢?答案是肯定的。第二次选择22.5度是因为二分查找法的查找效率最高。如果选用个在22.5到45度之间的值,查找的效率会降低一些。如果稍微降低一点查找的效率能让我们有效的减少乘法的次数,使最终的计算速度提高了,那么这种改进就是值得的。我们发现tan(26.565051177078)=0.5,如果我们第二次旋转采用26.565051177078度,那么乘数变为0.5,如果我们采用定点数运算的话(没有浮点协处理器时为了加速计算我们会大量的采用定点数算法)乘以0.5就相当于将乘数右移一位。右移运算是很快的,这样第二次循环中的乘法运算也被消除了。类似的方法,第三次循环中不用11.25度,而采用
tan(45)=
tan(26.565051177078)=
tan(14.0362434679265)=
tan(7.1250163489018)=
tan(3.57633437499735)=
tan(1.78991060824607)=
tan(0.8951737102111)=
tan(0.4476141708606)=
tan(0.2238105003685)=
【以上方法的C语言实现】
还是给出C语言的实现代码,我们采用循序渐进的方法,先给出浮点数的实现(因为用到了浮点数,所以并没有减少乘法运算量,查找的效率也比二分查找法要低,理论上说这个算法实现很低效。不过这个代码的目的在于给出算法实现的示意性说明,还是有意义的)。
double
{
const
1
1
};
const
1.78991060824607,
0.0559528918938,
};
int
double
double
for(i
{
if(y
{
x_new
y_new
x
y
angleSum
}
else
{
x_new
y_new
x
y
angleSum
}
printf("Debug:
}
return
}
程序运行的输出结果如下:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
z
有了上面的准备,我们可以来讨论定点数算法了。所谓定点数运算,其实就是整数运算。我们用256
序号 |
度数 |
×256 |
取整 |
1 |
45.0 |
11520 |
11520 |
2 |
26.565051177078 |
6800.65310133196 |
6801 |
3 |
14.0362434679265 |
3593.27832778918 |
3593 |
4 |
7.1250163489018 |
1824.00418531886 |
1824 |
5 |
3.57633437499735 |
915.541599999322 |
916 |
6 |
1.78991060824607 |
458.217115710994 |
458 |
7 |
0.8951737102111 |
229.164469814035 |
229 |
8 |
0.4476141708606 |
114.589227740302 |
115 |
9 |
0.2238105003685 |
57.2954880943458 |
57 |
10 |
0.1119056770662 |
28.647853328949 |
29 |
11 |
0.0559528918938 |
14.3239403248137 |
14 |
12 |
0.027976452617 |
7.16197186995294 |
7 |
13 |
0.01398822714227 |
3.58098614841984 |
4 |
14 |
0.006994113675353 |
1.79049310089035 |
2 |
15 |
0.003497056850704 |
0.8952465537802 |
1 |
C
int
{
const
int
int
int
x
y
for(i
{
if(y
{
x_new
y_new
x
y
angleSum
}
else
{
x_new
y_new
x
y
angleSum
}
printf("Debug:
}
return
}
计算结果如下:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
Debug:
z
16238/256=63.4296875度,精确的结果是63.4349499度,两个结果的差为0.00526,还是很精确的。
到这里CORDIC算法的最核心的思想就介绍完了。当然,这里介绍的只是CORDIC算法最基本的内容,实际上,利用CORDIC
想进一步深入学习的可以阅读