在Arduino中以不同方式(最快)计算正弦(和余弦)


9

我正在使用Arduino Uno开发板来计算系统角度(机械臂)。使用ADC的整个范围,角度实际上是ADC的10位值(0至1023)。我只会在第一象限(0至90度)中操作,正弦和余弦均为正,因此负数没有问题。我的疑问可以用3个问题来表达:

  1. 在Arduino上计算这些三角函数有哪些不同的方法?

  2. 什么是最快的方法?

  3. Arduino IDE中有sin()和cos()函数,但是Arduino如何实际计算它们(例如它们是否使用查找表或近似值等)?它们似乎是一个显而易见的解决方案,但是在尝试之前,我想知道它们的实际实现。

PS:我对Arduino IDE上的标准编码和汇编编码以及未提及的任何其他选项都开放。我也没有错误和近似的问题,这对于数字系统是不可避免的。但是,如果可能的话,最好提一下可能的错误程度


您可以接受近似值吗?
sa_leinad

是的,实际上,但是我想知道不同方法的错误程度。这不是精密产品,而是我的附带项目。实际上,几乎所有实现数学功能的数字系统(如果没有的话)都是不可避免的近似值
Transistor Overlord

我以为你想学习学位。您是否要输入角度的整数或十进制数字?
sa_leinad

度是。我认为编写代码和测试是否使用整数会更容易,所以我会这样做。我将提供有关编辑的更明确的信息
Transistor Overlord'Apr

1
对于仅90(整数)度,一个90条目的查找表将是最快和最有效的。实际上,对于完整的360度,您可以使用90项查找表。只需将其反向阅读90-179,然后将其翻转180-269。两者都进行270-359。
Majenko

Answers:


11

两种基本方法是数学计算(使用多项式)和查找表。

Arduino的数学库(libm,avr-libc的一部分)使用了前者。它针对AVR进行了优化,因为它是使用100%汇编语言编写的,因此几乎无法跟随其工作(注释也为零)。请放心,尽管这将是最优化的纯浮点实现方法,远远超过我们所能想到的。

但是那里的钥匙是浮动的。与纯整数相比,Arduino上涉及浮点的所有内容都会变得重量级,并且由于您仅请求0到90度之间的整数,因此简单的查找表是迄今为止最简单,最有效的方法。

由91个值组成的表格将为您提供0到90(含)之间的所有内容。但是,如果使用0.0到1.0之间的浮点值表,那么使用浮点数仍然效率低下(当然,效率不如sin使用浮点数计算效率低),因此存储定点值会效率更高。

这可能很简单,就像存储乘以1000的值一样,因此您拥有0到1000之间的值,而不是0.0到1.0之间的值(例如sin(30)将被存储为500而不是0.5)。更有效的方法是将这些值存储为Q16值,其中每个值(位)代表1.0的1 / 65536th。这些Q16值(以及相关的Q15,Q1.15等)使用起来效率更高,因为您拥有计算机喜欢使用的2幂次方,而不是他们讨厌使用的10幂次方。

也不要忘记该sin()函数期望弧度,因此首先必须将整数度转换为浮点弧度值,sin()与可以直接使用整数度值的查找表相比,使用效率更低。

但是,可以将两种情况组合在一起。线性插值可让您获得两个整数之间的浮点角的近似值。这很简单,只需计算出查找表中两点之间的距离,然后根据两个值之间的距离创建加权平均值。例如,如果您处在23.6度,则采取(sintable[23] * (1-0.6)) + (sintable[24] * 0.6)。基本上,您的正弦波会变成一系列由直线连接在一起的离散点。您以准确性为代价。


不久前,我写了一个库,它使用针对Taylor sin / cos的泰勒多项式比库快。给定的是,我使用浮点弧度作为两者的输入。
tuskiomi

8

这里有一些很好的答案,但是我想添加一种尚未提及的方法,一种非常适合在嵌入式系统上计算三角函数的方法,这就是CORDIC技术 。Wiki Entry Here它可以仅使用shift和添加一个小查询表。

这是C语言中的一个粗略示例。实际上,它使用CORDIC实现了C库的atan2()函数(即,在给定两个正交分量的情况下找到一个角度。)它使用浮点,但可以用于定点算法。

/*
 * Simple example of using the CORDIC algorithm.
 */

#include <stdio.h>
#include <math.h>

#define CORDIC_TABLE_SIZE  16

double cordic_table[CORDIC_TABLE_SIZE];

void init_table(void);
double angle(double I, double Q);

/*
 * Given a sine and cosine component of an
 * angle, compute the angle using the CORIDC
 * algoritm.
 */
double angle(double I, double Q)
{
    int L;
    double K = 1;
    double angle_acc = 0;
    double tmp_I;

    if (I < 0) {
        /* rotate by an initial +/- 90 degrees */
        tmp_I = I;
        if (Q > 0.0) {
            I = Q;           /* subtract 90 degrees */
            Q = -tmp_I;
            angle_acc = -90;
        } else {
            I = -Q;          /* add 90 degrees */
            Q = tmp_I;
            angle_acc = 90;
        }
    } else {
        angle_acc = 0;
    }

    /* rotate using "1 + jK" factors */
    for (L = 0, K = 1; L <= CORDIC_TABLE_SIZE; L++) {
        tmp_I = I;
        if (Q >= 0.0) {
            /* angle is positive: do negative roation */
            I += Q * K;
            Q -= tmp_I * K;
            angle_acc -= cordic_table[L];
        } else {
            /* angle is negative: do positive rotation */
            I -= Q * K;
            Q += tmp_I * K;
            angle_acc += cordic_table[L];
        }
        K /= 2.0;
    }
    return -angle_acc;
}

void init_table(void)
{
    int i;
    double K = 1;

    for (i = 0; i < CORDIC_TABLE_SIZE; i++) {
        cordic_table[i] = 180 * atan(K) / M_PI;
        K /= 2.0;
    }
}
int main(int argc, char **argv)
{
    double I, Q, A, Ar, R, Ac;

    init_table();

    printf("# Angle,    CORDIC Angle,  Error\n");
    for (A = 0; A < 90.0; A += 0.5) {

        Ar = A * M_PI / 180; /* convert to radians for C's sin & cos fn's */

        R = 5;  // Arbitrary radius

        I = R * cos(Ar);
        Q = R * sin(Ar);

        Ac = angle(I, Q);
        printf("%9f, %9f,   %12.4e\n", A, Ac, Ac-A);
    }
    return 0;
}

但是,请先尝试使用本机Arduino触发函数-无论如何它们可能足够快。


1
过去,我在stm8上采用了类似的方法。它需要两个步骤:1)从sin(2x)计算sin(x)和cos(x),然后2)从sin(x),sin(x / 2)计算sin(x +/- x / 2) ,cos(x)和cos(x / 2)->通过迭代,您可以逼近目标。以我为例,我以45度(0.707)开始,然后设法到达目标。它比标准IAR sin()函数要慢得多。
dannyf '17

7

我一直在使用定点多项式逼近在Arduino上计算正弦和余弦。与标准cos()sin()avr-libc 相比,这是我对平均执行时间和最坏情况错误的度量:

function    max error   cycles   time
-----------------------------------------
cos_fix()   9.53e-5     108.25    6.77 µs
sin_fix()   9.53e-5     110.25    6.89 µs
cos()       2.98e-8     1720.8   107.5 µs
sin()       2.98e-8     1725.1   107.8 µs

它基于仅用4个乘法计算的6次多项式。乘法本身是在汇编中完成的,因为我发现gcc无法高效地实现它们。角度以uint16_t1/65536圈为单位表示,这使得角度的运算自然以一圈为模。

如果您认为这适合您的账单,请使用以下代码:定点三角函数。抱歉,我仍然没有翻译此页面,它是法语的,但是您可以理解方程式,并且代码(变量名,注释...)是英语的。


编辑:由于服务器似乎已消失,这是一些有关我发现的近似信息。

我想以象限为单位(或等效地,依次)以二进制定点编写角度。而且我还想使用偶数多项式,因为它们比任意多项式更有效地计算。换句话说,我想要一个多项式P()使得

对于x∈[0,1],cos(π/ 2 x)≈P(x 2

我还要求近似值在区间的两端都必须精确,以确保cos(0)= 1和cos(π/ 2)=0。这些约束导致形式

P(u)=(1 − u)(1 + uQ(u))

其中Q()是任意多项式。

接下来,我根据Q()的程度搜索了最佳解决方案,并发现了这一点:

        Q(u)              degree of P(x²)  max error
─────────────────────────┼─────────────────┼──────────
          0                       2         5.60e-2
       0.224                     4         9.20e-4
0.2335216 + 0.0190963 u          6         9.20e-6

以上解决方案中的选择是速度/精度的权衡。第三种解决方案比16位解决方案具有更高的精度,这是我为16位实现选择的解决方案。


2
真是太神奇了,@ Edgar。
SDsolar

您如何找到多项式?
TLW

@TLW:我要求它具有一些“不错的”属性(例如cos(0)= 1),该属性必须限制为(1-x²)(1 +x²Q(x²))形式,其中Q(u)是任意的多项式(在页面中进行说明)。我取了一级Q(仅2个系数),通过拟合找到了近似系数,然后通过反复试验手动优化了优化。
Edgar Bonet

@EdgarBonet-有趣。请注意,尽管可以缓存,但该页面不会为我加载。您能否将用于此答案的多项式相加?
TLW

@TLW:将其添加到答案中。
埃德加·博内特

4

您可以创建几个使用线性逼近的函数来确定特定角度的sin()和cos()。

我在想这样的事情: 对于每一个,我都将sin()和cos()的图形表示分为三个部分,并对该部分进行了线性近似。
线性近似

理想情况下,您的函数将首先检查angel的范围是否在0到90之间。
然后它将使用一条ifelse语句确定它属于3个部分中的哪个,然后进行相应的线性计算(即output = mX + c


这会不会涉及浮点乘法?
晶体管霸王

1
不必要。您可以使用它,以便将输出缩放为0-100,而不是0-1。这样,您正在处理整数,而不是浮点数。注意:100是任意的。没有理由不能在0-128或0-512或0-1000或0-1024之间缩放输出。通过使用2的倍数,您只需要进行右移即可将结果缩小。
sa_leinad

非常聪明,@ sa_leinad。赞成。我记得在使用晶体管偏置时会这样做。
SDsolar

4

我寻找了近似于cos()和sin()的其他人,然后我发现了这个答案:

dtb对“使用预先计算的转换数组的快速Sin / Cos”的回答

基本上,他计算出数学库中的math.sin()函数比使用值查找表要快。但是据我所知,这是在PC上计算出来的。

Arduino包含一个数学库,可以计算sin()和cos()。


1
PC内置有FPU,可以使其快速运行。Arduino没有,这使它变慢。
Majenko

答案也适用于C#,它可以进行数组边界检查之类的工作。
迈克尔

3

查找表将是查找异常的最快方法。而且,如果您愿意使用定点数(整数位不在二进制位0右边的整数)进行计算,则使用正弦的进一步计算也将更快。然后,该表可以是单词表,可能在Flash中以节省RAM空间。请注意,在数学运算中,可能需要使用long以获得较大的中间结果。


1

通常,查找表>近似->计算。内存>闪光。整数>定点>浮点。预计算>实时计算。镜像(正弦到余弦或余弦到正弦)与实际计算/查找...。

每个都有其优缺点。

您可以进行各种组合,以查看哪种组合最适合您的应用程序。

编辑:我做了快速检查。使用8位整数输出,使用查找表计算1024个sin值需要0.6毫秒,而使用浮点数则需要133毫秒,或慢200倍。


1

我对OP有一个类似的问题。我想制作一个LUT表,以将正弦函数的第一象限计算为从0x8000到0xffff的无符号16位整数。最后我为了娱乐和利润而写了这篇。注意:如果我使用'if'语句,这将更有效。而且它不是很准确,但是对于声音合成器中的正弦波来说足够准确

void sin_lut_ctor(){

//Make a Look Up Table for 511 terms of the sine function.
//Plugin in some polynomials to do some magic
//and you get an aproximation for sines up to π/2.
//

//All sines yonder π/2 can be derived with math

const uint16_t uLut_d = 0x0200; //maximum LUT depth for π/2 terms. 
uint16_t uLut_0[uLut_d];        //The LUT itself.
//Put the 2 above before your void setup() as global variables.
//This coefficients will only work for uLut_d = 511.

uint16_t arna_poly_0 = 0x000a; // 11
uint16_t arna_poly_1 = 0x0001; // 1
uint16_t arna_poly_2 = 0x0007; // 7
uint16_t arna_poly_3 = 0x0001; // 1   Precalculated Polynomials
uint16_t arna_poly_4 = 0x0001; // 1   
uint16_t arna_poly_5 = 0x0007; // 7
uint16_t arna_poly_6 = 0x0002; // 2
uint16_t arna_poly_7 = 0x0001; // 1

uint16_t Imm_UI_0 = 0x0001;              //  Itterator
uint16_t Imm_UI_1 = 0x007c;              //  An incrementor that decreases in time

uint16_t Imm_UI_2 = 0x0000;              //  
uint16_t Imm_UI_3 = 0x0000;              //              
uint16_t Imm_UI_4 = 0x0000;              //
uint16_t Imm_UI_5 = 0x0000;              //
uint16_t Imm_UI_6 = 0x0000;              //  Temporary variables
uint16_t Imm_UI_7 = 0x0000;              //
uint16_t Imm_UI_8 = 0x0000;              //
uint16_t Imm_UI_9 = 0x0000;              //
uint16_t Imm_UI_A = 0x0000;
uint16_t Imm_UI_B = 0x0000;

uint16_t Imm_UI_A = uLut_d - 0x0001;     //  510

uLut_0[0x0000] = 0x8000;        //Assume that the middle point is 32768 (0x8000 hex)
while (Imm_UI_0 < Imm_UI_A) //Construct a quarter of the sine table
  {
Imm_UI_2++;                                   //Increase temporary variable by 1

Imm_UI_B = Imm_UI_2 / arna_coeff_0;           //Divide it with the first coefficient (note: integer division)
Imm_UI_3 += Imm_UI_B;                         //Increase the next temporary value if the first one has increased up to the 1st coefficient
Imm_UI_1 -= Imm_UI_B;                         //Decrease the incrementor if this is the case
Imm_UI_2 *= 0x001 - Imm_UI_B;                 //Set the first temporary variable back to 0

Imm_UI_B = Imm_UI_3 / arna_poly_1;           //Do the same thing as before with the next set of temporary variables
Imm_UI_4 += Imm_UI_B;
Imm_UI_1 -= Imm_UI_B;
Imm_UI_3 *= 0x0001 - Imm_UI_B;

Imm_UI_B = Imm_UI_4 / arna_poly_2;           //And again... and again... you get the idea.
Imm_UI_5 += Imm_UI_B;
Imm_UI_1 -= Imm_UI_B;
Imm_UI_4 *= 0x0001 - Imm_UI_B;

Imm_UI_B = Imm_UI_5 / arna_poly_3;
Imm_UI_6 += Imm_UI_B;
Imm_UI_1 -= Imm_UI_B;
Imm_UI_5 *= 0x0001 - Imm_UI_B;

Imm_UI_B = Imm_UI_6 / arna_poly_4;
Imm_UI_7 += Imm_UI_B;
Imm_UI_1 -= Imm_UI_B;
Imm_UI_6 *= 0x0001 - Imm_UI_B;

Imm_UI_B = Imm_UI_7 / arna_poly_5;
Imm_UI_8 += Imm_UI_B;
Imm_UI_1 -= Imm_UI_B;
Imm_UI_7 *= 0x0001 - Imm_UI_B;

Imm_UI_B = Imm_UI_8 / arna_poly_6;
Imm_UI_9 += Imm_UI_B;
Imm_UI_1 -= Imm_UI_B;
Imm_UI_8 *= 0x0001 - Imm_UI_B;

Imm_UI_B = Imm_UI_9 / arna_poly_7          //the last set won't need to increment a next variable so skip the step where you would increase it.
Imm_UI_1 -= Imm_UI_B;
Imm_UI_9 *= 1 - Imm_UI_B;

uLut_0[Imm_UI_0] = (uLut_0[Imm_UI_0 - 0x0001] + Imm_UI_1); //Set the current value as the previous one increased by our incrementor
Imm_UI_0++;              //Increase the itterator
  }   
  uLut_0[Imm_UI_A] = 0xffff; //Lastly, set the last value to 0xffff

  //And there you have it. A sine table with only one if statement (a while loop)
}

现在使用该函数获取值,它接受0x0000到0x0800之间的值并从LUT返回适当的值

uint16_t lu_sin(uint16_t lu_val0)
{
  //Get a value from 0x0000 to 0x0800. Return an appropriate sin(value)
  Imm_UI_0 = lu_val0/0x0200; //determine quadrant
  Imm_UI_1 = lu_val0%0x0200; //Get which value
  if (Imm_UI_0 == 0x0000)
  {
    return uLut_0[Imm_UI_1];
  }
  if (Imm_UI_0 == 0x0001)
  {
    return uLut_0[0x01ff - Imm_UI_1];
  }
  if (Imm_UI_0 == 0x0002)
  {
    return 0xffff - uLut_0[Imm_UI_1];
  }
  if (Imm_UI_0 == 0x0003)
  {
    return 0xffff - uLut_0[0x01ff - Imm_UI_1];
  }
}// I'm using if statements here but similarly to the above code block, 
 //you can do without. just with integer divisions and modulos

记住,这不是执行此任务的最有效方法,我只是想不出如何使taylor系列获得合适范围内的结果。


您的代码无法编译:Imm_UI_A被声明两次,a ;和一些变量声明丢失,uLut_0应该是全局的。使用必要的修复程序后,lu_sin()速度很快(在27至42个CPU周期之间),但是却非常不准确(最大错误≈5.04e-2)。我无法理解这些“ Arnadathian多项式”:似乎是很繁重的计算,但是结果几乎和简单的二次逼近一样差。该方法还具有巨大的存储成本。最好在PC上计算表并将其作为PROGMEM数组放入源代码中。
埃德加·波内(Edgar Bonet)

1

仅出于乐趣,并证明它可以完成,我完成了一个AVR组装例程,以24位(3字节)的形式计算sin(x)结果,但有一点错误。输入角度以度为单位,带有一个十进制数字,仅对于第一象限为000到900(0〜90.0)。它使用不到210条AVR指令,平均运行时间为212微秒,范围从211us(角度= 001)到213us(角度= 899)。

考虑到AVR微控制器,没有浮点,消除了所有可能的划分,花了几天的时间来完成所有工作,超过10天(空闲时间)才考虑了最佳的计算算法。要花费更多时间来为整数设置正确的升压值,要具有良好的精度,需要将1e-8的值升压为2 ^ 28或更大的二进制整数。一旦发现所有精度和舍入误差元,将其计算分辨率提高2 ^ 8或2 ^ 16,就可以达到最佳结果。我首先在Excel上模拟了所有计算,同时将所有值都设为Int(x)或Round(x,0)来表示AVR核心处理。

例如,在算法中,角度必须以弧度为单位,输入以度为单位,以方便用户使用。要将度数转换为弧度,平凡的公式是rad = degrees * PI / 180,这看起来很容易,但事实并非如此,PI是一个无限数-如果使用很少的数字,则会在输出中产生错误,除以180需要AVR位操作,因为它没有除法指令,并且除此以外,由于涉及远远低于整数1的数字,结果将需要浮点运算。例如,1°(度)的Radian为0.017453293。由于PI和180是常数,为什么不为了简单的乘法而反转呢?PI / 180 = 0.017453293,乘以2 ^ 32,得出的常数为74961320(0x0477D1A8),然后将此数字乘以角度(以度为单位),假设900代表90°,然后将其右移4位(÷16),以获得4216574250(0xFB53D12A),即90°的弧度(具有2 ^ 28的扩展),适合4个字节,不作任何除法(除4右移)。在某种程度上,这种技巧所包含的误差小于2 ^ -27。

因此,所有进一步的计算都需要记住它要高出2 ^ 28并加以注意。您需要将运行中的结果除以16、256甚至65536,只是为了避免它使用不必要的,无法帮助解决的增长饥饿字节。这是一项艰巨的工作,只是在每个计算结果中找到最少的位数,使结果精度保持在24位左右。在Excel流程中,按尝试/错误方式使用高位或低位进行多次计算中的每一项,在结果中观察错误位的总数,该图显示0-90°,其中宏运行代码900次,十分之一度。这种“可视化” Excel方法是我创建的工具,它为代码的每个部分找到了最佳解决方案,这大有帮助。

例如,将此特殊的计算结果13248737.51舍入为13248738.或仅丢失“ 0.51”小数,它将对所有900个输入角度(00.1〜90.0)测试的最终结果精度产生多大影响?

在每次计算中,我都能使动物保持在32位(4字节)之内,并最终获得了魔术,从而在结果的23位之内获得了精度。当检查结果的整个3个字节时,误差为±1 LSB,未解决。

用户可以根据自己的精度要求从结果中抓取一个,两个或三个字节。当然,如果仅一个字节就足够了,我建议使用一个256字节的sin表,并使用AVR'LPM'指令来抓取它。

一旦我使Excel序列运行平稳,整洁,从Excel到AVR程序集的最终翻译就用了不到2个小时,通常,您应该首先考虑更多,而以后再减少工作。

那时,我可以进一步压缩并减少寄存器的使用。实际的(不是最终的)代码使用大约205条指令(〜410字节),平均以212us运行sin(x)计算,时钟为16MHz。以该速度,它每秒可以计算4700+ sin(x)。并不重要,但是它可以运行高达4700Hz的精确正弦波,具有23位的精度和分辨率,而无需任何查找表。

基本算法基于针对sin(x)的泰勒级数,但进行了很多修改,以适应AVR微控制器和精度的要求。

即使使用2700字节的表(900个条目* 3字节)也会吸引速度,这有什么乐趣或学习经验呢?当然,也考虑使用CORDIC方法,也许稍后,这里的重点是将Taylor挤入AVR岩心并从干燥的岩石中取水。

我想知道Arduino“ sin(78.9°)”是否可以在不到212us的时间内以23位的精度运行处理(C ++),并且所需的代码小于205条指令。可能是C ++使用CORDIC。Arduino草图可以导入汇编代码。

在此处发布代码没有任何意义,稍后我将编辑此帖子以包括指向该链接的Web链接,可能在我的博客上,网址为。该博客主要使用葡萄牙语。

这项没有钱的爱好很有趣,在没有除法指令的情况下,仅以8x8位进行乘法运算,就将16VIPS的AVR引擎的极限提高到16MHz。它允许计算sin(x),cos(x)[= sin(900-x)]和tan(x)[= sin(x)/ sin(900-x)]。

最重要的是,这有助于保持我63岁的大脑的光彩和油脂。当青少年说“老年人”对技术一无所知时,我回答“再想一想,您认为谁创造了今天享受的一切的基础?”。

干杯


真好!一些注意事项:1 .标准sin()功能的精度与您的精度大致相同,并且是两倍。它也基于多项式。2.如果必须将任意角度四舍五入到最接近的0.1°倍数,则可能导致舍入误差高达8.7e-4,这抵消了23位精度的好处。3.您介意共享多项式吗?
Edgar Bonet

1

正如其他人提到的那样,如果要提高速度,查找表是必经之路。我最近一直在研究ATtiny85上的三角函数的计算,以使用快速矢量平均值(在我的情况下为风)。总是需要权衡...对我而言,我只需要1度角分辨率,因此查找360 int(将-32767缩放为32767,只能与int一起使用)的查找表是最好的选择。检索正弦仅是提供索引0-359的问题...非常快!我的测试得出的一些数字:

FLASH查找时间(美国):0.99(使用PROGMEM存储的表)

RAM查找时间(毫秒):0.69(RAM中的表)

解放时间(美国):122.31(使用Arduino解放)

请注意,这些是每个360点样本的平均值。测试是在nano上完成的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.