2019独角兽企业重金招聘Python工程师标准>>>
之前搞过一段时间的单片机,当时用的芯片只有16M的空间用于写代码。用到了asin、acos、atan这些反三角函数,但是引用C标准库的时候发现,每多用一个这样的标准函数,空间多占用1M左右,也就是用到这3个函数的话就要占用3M,如果用到sin、cos这些,那么没有什么空间写其他代码了。所以就需要自己实现这些函数。
标准库的函数八成是用的查表法,而且表可能过大,我没有去看标准库的实现代码。那么我自己实现的函数和标准库的比起来有什么差别呢?其实就是容量小,但是误差大些,不过在我的应用里,就小数点好几位后的误差基本可以忽略。
0.实现Sqrt(平方根)和RSqrt函数(平方根倒数)
#include
typedef union{
float f;
int32_t i;
}FIUnion;
float RSqrt(float value){
size_t i;
FIUnion buffer;
buffer.f = value;
buffer.i = 0x5f3759df - (buffer.i >> 1);
for(i = 0;i != 4;++i)
buffer.f *= (3.0f - value*buffer.f*buffer.f)*0.5f;
return buffer.f;
}
float Sqrt(float value){
return 1.0f / RSqrt(value);
}
这个算法在《算法心得》(Hacker's Delight)里“17.4 估算平方根”有提及,网上关于卡马克的传说里也有,大体上可以理解为从牛顿法演变过来的算法。这个例子做了4次迭代,精度还行,过多的迭代,单片机受不了。
1.通过CORDIC(坐标旋转数字计算方法)求反三角函数
#include
#define MPI 3141593//π*1000000取整
#define MUL 0.000001f//
#define NUM 21
float ATan2(float y,float x){
static const int radin[NUM] = {
785398, 463648, 244979, 124355,
62419, 31240, 15624, 7812,
3906, 1953, 977, 488,
244, 122, 61, 31,
15, 8, 4, 2, 1};
size_t i;
int tx,ty;
int ix = x*1024.0f,iy = y*1024.0f;
int result = 0;
if(ix < 0){
result = iy >= 0 ? MPI : -MPI;
ix = -ix;
iy = -iy;
}
for(i = 0;i != NUM;++i){
if(iy == 0)break;
if(iy > 0){
tx = ix + (iy >> i);
ty = iy - (ix >> i);
result += radin[i];
}else{
tx = ix - (iy >> i);
ty = iy + (ix >> i);
result -= radin[i];
}
ix = tx;
iy = ty;
}
return result * MUL;
}
float ASin(float value){
float y = value;
float x = 1.0f / RSqrt(1.0f - y*y);
return ATan2(y,x);
}
float ACos(float value){
float x = value;
float y = 1.0f / RSqrt(1.0f - x*x);
return ATan2(y,x);
}
float ATan(float value){
return ATan2(value,1.0f);
}
这里用到了上面例子里的RSqrt,用于ASin和ACos。首先说说ATan2吧,其实就是传入“对边”和“邻边”的长,这样在一个坐标系下我们就可以认为这个角为向量(“对边”,“邻边”)与X正半轴的夹角。求解的过程也是一步步逼近,最后得到一个近似的估值。
基本的思路:首先是旋转这条边到一四象限,并累加旋转的角度。之后就是不停地旋转使其靠近X正半轴并累加旋转的角度。
radin数组是tan值为1,1/2,1/4....的角(这些值事先按计算器求得:) )一直二分下去对应的弧度值的1000000倍(由于只有1000000倍,所以分到第21次时,是1了分不下去,要更高精度的话请用更大倍数)。用整数可以减少浮点计算,故意用了1000000倍来容纳小数点后的几位,因为用到的弧度值基本小于1。最后把结果乘以0.000001了。
至于 tx = ix + (iy >> i); ty = iy - (ix >> i);这种是坐标旋转的变体(x'/cos = x + y*tan; y'/cos = y - x*tan),无视向量的长度,除以cos来减少计算。而旋转的角特地选tan为1,1/2,1/4的,所以就相当于除2的i次方,而这种除2的次方又可以用右移代替。
最后的ATan是由于tan = y / x(“对”除“临”),所以当X = 1时,Y = tan 了,所以直接就这么用。写的时候就这思路,至于弊端的话,我发现误差会比ATan2大些。而且还是调用的ATan2,所以性能也差些。
总结
虽然都是些估算的算法,但是却省下了不少的资源,尤其在单片机不给力的情况下。这些已经是很久前搞单片机时写的了,当时写的时候为了优化想了很多方法,所以代码里某些代码可能会有点怪(比如那1000000倍;那强行X = 1等)。