在不使用标准功能的情况下在C中生成正弦信号

时间:2017-12-20 12:45:51

标签: c embedded cortex-m

我希望在不使用标准函数sin()的情况下在C中生成正弦信号,以触发LED亮度的正弦形状变化。我的基本想法是使用一个包含40个点和插值的查找表。

这是我的第一个方法:

const int sine_table[40] = {0, 5125, 10125, 14876, 19260, 23170, 26509, 29196,
31163, 32364, 32767,  32364, 31163, 29196, 26509, 23170, 19260, 14876, 10125,
5125, 0, -5126, -10126,-14877, -19261, -23171, -26510, -29197, -31164, -32365,
-32768, -32365, -31164, -29197, -26510, -23171, -19261, -14877, -10126, -5126};

int i = 0;
int x1 = 0;
int x2 = 0;
float y = 0;

float sin1(float phase)
{
    x1 = (int) phase % 41;
    x2 = x1 + 1;
    y = (sine_table[x2] - sine_table[x1])*((float) ((int) (40*0.001*i*100) % 4100)/100 - x1) + sine_table[x1];
    return y;
}

int main()
{
    while(1)
    {
    printf("%f      ", sin1(40*0.001*i)/32768);
    i = i + 1;
    }
}

不幸的是,这个函数有时会返回远大于1的值。此外,插值看起来并不好(我用它来创建LED的正弦形亮度变化,但这些非常不合理)。

有人有更好的想法在C中实现正弦发生器吗?

12 个答案:

答案 0 :(得分:21)

OP的主要问题是为表查找生成索引。

OP的代码尝试访问导致undefined behavior的外部数组sine_table[40]。至少修正一下。

const int sine_table[40] = {0, 5125, 10125, ...
    ...
    x1 = (int) phase % 41;                     // -40 <= x1 <= 40
    x2 = x1 + 1;                               // -39 <= x2 <= 41  
    y = (sine_table[x2] - sine_table[x1])*...  // bad code, consider x1 = 40 or x2 = 40,41

建议的更改

    x1 = (int) phase % 40;   // mod 40, not 41
    if (x1 < 0) x1 += 40;    // Handle negative values
    x2 = (x1 + 1) % 40;      // Handle wrap-around 
    y = (sine_table[x2] - sine_table[x1])*...  

有更好的方法,但关注OP的方法见下文。

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

const int sine_table[40] = { 0, 5125, 10125, 14876, 19260, 23170, 26509, 29196,
31163, 32364, 32767, 32364, 31163, 29196, 26509, 23170, 19260, 14876, 10125,
5125, 0, -5126, -10126, -14877, -19261, -23171, -26510, -29197, -31164, -32365,
-32768, -32365, -31164, -29197, -26510, -23171, -19261, -14877, -10126, -5126 };

int i = 0;
int x1 = 0;
int x2 = 0;
float y = 0;

float sin1(float phase) {
  x1 = (int) phase % 40;
  if (x1 < 0) x1 += 40;
  x2 = (x1 + 1) % 40;
  y = (sine_table[x2] - sine_table[x1])
      * ((float) ((int) (40 * 0.001 * i * 100) % 4100) / 100 - x1)
      + sine_table[x1];
  return y;
}

int main(void) {
  double pi = 3.1415926535897932384626433832795;
  for (int j = 0; j < 1000; j++) {
    float x = 40 * 0.001 * i;
    float radians = x * 2 * pi / 40;
    printf("%f %f %f\n", x, sin1(x) / 32768, sin(radians));
    i = i + 1;
  }
}

输出

         OP's     Reference sin()
0.000000 0.000000 0.000000
0.040000 0.006256 0.006283
0.080000 0.012512 0.012566
...
1.960000 0.301361 0.303035
2.000000 0.308990 0.309017
2.040000 0.314790 0.314987
...
39.880001 -0.020336 -0.018848
39.919998 -0.014079 -0.012567
39.959999 -0.006257 -0.006283

更好的代码不会将值i, x1, x2, y作为全局变量传递,而是作为函数参数或函数变量传递。也许这是OP调试的工件。

  

有人有更好的想法在C中实现正弦发生器吗?

这是相当广泛的。速度,精度,代码空间,可移植性或可维护性更好? sine()函数很容易实现。高质量的产品需要更多的努力。

虽然模糊,OP使用小型查找表是一个好的开始 - 虽然我看到它可以在没有任何浮点数学的情况下完成。我建议OP构建一个经过测试和运行的解决方案,并将其发布在Code Review中以获得改进的想法。

答案 1 :(得分:18)

...在C中实现正弦发生器更好吗?

编辑:建议先阅读 this article ,以了解OP的要求。

从您问题中提供的上下文中,我假设 更好 这个词可能与编译代码的大小和速度有关,因为可能需要运行一个小型微处理器。

CORDIC(协调旋转数据计算机)算法非常适合用于较小的uP和具有有限数学计算能力的FPGA实现,因为它计算值的正弦和余弦仅使用基本算术(加法,减法和移位)。有关CORDIC的更多信息,以及如何使用它来生成角度 are provided here 的正弦/余弦。

还有几个站点提供算法实现示例。 Simple CORDIC 包含有关如何生成表格的详细说明,然后可以对目标设备进行预编译,以及测试以下函数输出的代码(使用定点数学):

(参见以下文档和链接中的其他功能)

#define cordic_1K 0x26DD3B6A
#define half_pi 0x6487ED51
#define MUL 1073741824.000000
#define CORDIC_NTAB 32
int cordic_ctab [] = {0x3243F6A8, 0x1DAC6705, 0x0FADBAFC, 0x07F56EA6, 0x03FEAB76, 0x01FFD55B, 
0x00FFFAAA, 0x007FFF55, 0x003FFFEA, 0x001FFFFD, 0x000FFFFF, 0x0007FFFF, 0x0003FFFF, 
0x0001FFFF, 0x0000FFFF, 0x00007FFF, 0x00003FFF, 0x00001FFF, 0x00000FFF, 0x000007FF, 
0x000003FF, 0x000001FF, 0x000000FF, 0x0000007F, 0x0000003F, 0x0000001F, 0x0000000F, 
0x00000008, 0x00000004, 0x00000002, 0x00000001, 0x00000000 };

void cordic(int theta, int *s, int *c, int n)
{
  int k, d, tx, ty, tz;
  int x=cordic_1K,y=0,z=theta;
  n = (n>CORDIC_NTAB) ? CORDIC_NTAB : n;
  for (k=0; k<n; ++k)
  {
    d = z>>31;
    //get sign. for other architectures, you might want to use the more portable version
    //d = z>=0 ? 0 : -1;
    tx = x - (((y>>k) ^ d) - d);
    ty = y + (((x>>k) ^ d) - d);
    tz = z - ((cordic_ctab[k] ^ d) - d);
    x = tx; y = ty; z = tz;
  }  
 *c = x; *s = y;
}

<强> 编辑:
我发现在 Simple CORDIC 网站上使用示例的文档非常容易理解。但是,我遇到的一件小事是编译文件cordic-test.c时出现错误:使用了未声明的标识符“M_PI”。在执行编译的gentable.c文件(生成cordic-test.c文件)时,似乎该行:

#define M_PI 3.1415926535897932384626

虽然包含在自己的声明中,但未包含在用于生成文件cordic-test.c的printf语句中。一旦解决了这个问题,一切都按照宣传的方式进行。

如记载的那样,产生的数据范围产生完整正弦周期的1/4(-π/ 2-π/ 2)。下图包含浅蓝点之间产生的实际数据的表示。通过镜像和转置原始数据部分来制造正弦信号的其余部分。

enter image description here

答案 2 :(得分:14)

生成准确的正弦函数需要一定量的资源(CPU周期和内存),这在此应用程序中是没有根据的。您生成“平滑”正弦曲线的目的是未能考虑应用程序的要求。

  • 当您绘制曲线时,您可能会观察到瑕疵 将该曲线应用于LED PWM驱动器,人眼无法察觉 那些不完美之处。

  • 即使是40步曲线,人眼也不可能察觉到相邻值之间的亮度差异,因此不需要插值。

  • 一般情况下,如果生成正弦函数直接生成没有浮点的PWM驱动值,效率会更高。事实上,不是正弦函数,缩放的升余弦会更合适,因此输入为零会导致输出为零,并且输入为循环中值的一半会导致PWM驱动器的最大值。

以下函数从16位(和16字节)查找生成8位FSD PWM的升余弦曲线,生成59步循环。因此,与40步浮点实现相比,它具有内存和性能效率。

#include <stdint.h>

#define LOOKUP_SIZE 16
#define PWM_THETA_MAX (LOOKUP_SIZE * 4 - 4)

uint8_t RaisedCosine8bit( unsigned n )
{
    static const uint8_t lookup[LOOKUP_SIZE] = { 0, 1, 5, 9,
                                                 14, 21, 28, 36,
                                                 46, 56, 67, 78,
                                                 90, 102, 114, 127} ;
    uint8_t s = 0 ;
    n = n % PWM_THETA_MAX ;

    if( n < LOOKUP_SIZE )
    {
        s = lookup[n] ;
    }
    else if( n < LOOKUP_SIZE * 2 - 1 )
    {
        s = 255 - lookup[LOOKUP_SIZE * 2 - n - 2] ;
    }
    else if( n < LOOKUP_SIZE * 3 - 2 )
    {
        s = 255 - lookup[n - LOOKUP_SIZE * 2 + 2] ;
    }
    else
    {
        s = lookup[LOOKUP_SIZE * 4 - n - 4] ;
    }

    return s ;
}

对于0 <= theta < PWM_THETA_MAX的输入,曲线如下所示:

enter image description here

我建议使用足够光滑的照明。

在实践中你可以这样使用它:

for(;;)
{
    for( unsigned i = 0; i < PWM_THETA_MAX; i++ )
    {
        LedPwmDrive( RaisedCosine8bit( i ) ) ;
        Delay( LED_UPDATE_DLEAY ) ;
    }
}

如果您的PWM范围不是0到255,只需缩放功能的输出; 8位分辨率足以完成任务。

答案 3 :(得分:6)

您是否考虑过将[0..PI]的正弦曲线部分建模为抛物线?如果LED的亮度仅供人眼观察,那么曲线的形状应该足够相似,以便检测到很小的差异。

您只需要找出适当的等式来描述它。

嗯,......

顶点(PI / 2,1)

(0,0)和(PI,0)处的X轴交点

f(x) = 1 - K * (x - PI/2) * (x - PI/2)

K会是......

K = 4 / (PI * PI)

答案 4 :(得分:6)

绘制圆圈(因此也产生正弦波)的经典黑客是Hakmem #149 by Marvin Minsky。 。E.g,:

#include <stdio.h>

int main(void)
{
    float x = 1, y = 0;

    const float e = .04;

    for (int i = 0; i < 100; ++i)
    {
        x -= e*y;
        y += e*x;
        printf("%g\n", y);
    }
}

它会略微偏心,不是一个完美的圆圈,你可能会略微超过1,但你可以通过除以最大值或舍入来调整。此外,可以使用整数运算,并且可以通过对e使用2的负幂来消除乘法/除法,因此可以使用shift。

答案 5 :(得分:6)

对于LED,您可能只需要进行16步左右而无需进行插值。也就是说,我可以在sin1()函数中看到至少两个奇怪的东西:

1)sine_table中有40个数据点,但您输入的索引x1模数为41。这似乎不是处理周期性的正确方法,并让x1指向数组的最后一个索引。
2)您然后添加+1,因此x2甚至可以超出数组的限制。
3)您在函数中使用i,但它仅在主程序中设置。我不能告诉它应该做什么,但在简单的计算函数中使用这样的全局似乎很脏。也许它应该为插值提供小数部分,但是你不应该使用phase

这是一个简单的插补器,似乎可行。调整味道。

#include <assert.h>

int A[4] = {100, 200, 400, 800};    
int interpolate(float x)
{
    if (x == 3.00) {
        return A[3];
    }
    if (x > 3) {
        return interpolate(6 - x);
    }
    assert(x >= 0 && x < 3);
    int i = x;
    float frac = x - i;
    return A[i] + frac * (A[i+1] - A[i]);
}

一些任意的样本输出:

interpolate(0.000000) = 100
interpolate(0.250000) = 125
interpolate(0.500000) = 150
interpolate(1.000000) = 200
interpolate(1.500000) = 300
interpolate(2.250000) = 500
interpolate(2.999900) = 799
interpolate(3.000000) = 800
interpolate(3.750000) = 500

(我将把它留给感兴趣的读者用适当定义的符号常量替换所有出现的3,以进一步推广该函数,并实现计算负相位。 )

答案 6 :(得分:5)

我会选择Bhaskara,我接近正弦函数。使用度数,从0到180,您可以像这样近似值

float Sine0to180(float phase)
{
    return (4.0f * phase) * (180.0f - phase) / (40500.0f - phase * (180.0f - phase));
}

如果您想考虑任何角度,请添加

float sine(float phase)
{
    float FactorFor180to360 = -1 * (((int) phase / 180) % 2 );
    float AbsoluteSineValue = Sine0to180(phase - (float)(180 * (int)(phase/180)));
    return AbsoluteSineValue * FactorFor180to360;
}

如果你想以弧度为单位,你可以添加

float SineRads(float phase)
{
    return Sine(phase * 180.0f / 3.1416);
}

这是一个图表,显示了用这个近似值计算的点数,以及用正弦函数计算的点数。你几乎看不到从实际正弦点下面偷看的近似点。

enter image description here

答案 7 :(得分:5)

除非你的应用程序需要真正的精确度,否则不要自己想出一个40点正弦波或余弦波的算法。此外,表格中的值应与LED的pwm输入范围相匹配。

那就是说,我看了你的代码和它的输出,并认为你没有在点之间进行插值。经过一点修改,我修复了它,并且excel的标志功能和你之间的错误最多约为0.0032左右。这个变化非常容易实现,并且已经使用tcc进行了测试,tcc是我个人的C算法测试。

首先,我向你的正弦数组添加了一个点。最后一个点设置为与正弦数组中的第一个元素相同的值。这将修复正弦函数中的数学运算,特别是当您将x1设置为(int)phase%40,x2设置为x1 + 1时。添加额外的点是不必要的,因为你可以将x2设置为(x1 + 1)%40,但我选择了第一种方法。我只是指出了你可以通过不同的方式实现这一目标。我还添加了余数的计算(基本相 - (int)阶段)。我使用余数进行插值。我还添加了一个临时正弦值持有者和一个delta变量。

const int sine_table[41] = 
{0, 5125, 10125, 14876, 19260, 23170, 26509, 29196,
31163, 32364, 32767,  32364, 31163, 29196, 26509, 23170, 
19260, 14876, 10125, 5125, 0, -5126, -10126,-14877,
-19261, -23171, -26510, -29197, -31164, -32365, -32768, -32365,
-31164, -29197, -26510, -23171, -19261, -14877, -10126, -5126, 0};

int i = 0;
int x1 = 0;
int x2 = 0;
float y = 0;

float sin1(float phase)
{
    int tsv,delta;
    float rem;

    rem = phase - (int)phase;
    x1 = (int) phase % 40;
    x2 = (x1 + 1);

    tsv=sine_table[x1];
    delta=sine_table[x2]-tsv;

    y = tsv + (int)(rem*delta);
    return y;
}

int main()
{
    int i;  
    for(i=0;i<420;i++)
    {
       printf("%.2f, %f\n",0.1*i,sin1(0.1*i)/32768);
    }
    return 0;
}

结果看起来很不错。比较线性近似与系统的浮点正弦函数给出了如下所示的误差图。

Error Plot

Combined Error vs Sine graph

答案 8 :(得分:2)

您可以使用sin的{​​{3}}的前几个字词。您可以根据需要使用尽可能少的术语来达到预期的精度水平 - 比下面的示例更多的术语应该开始突破32位浮动的限制。

示例:

#include <stdio.h>

// Please use the built-in floor function if you can. 
float my_floor(float f) {
    return (float) (int) f;
}

// Please use the built-in fmod function if you can.
float my_fmod(float f, float n) {
    return f - n * my_floor(f / n);
}

// t should be in given in radians.
float sin_t(float t) {
    const float PI = 3.14159265359f;

    // First we clamp t to the interval [0, 2*pi) 
    // because this approximation loses precision for 
    // values of t not close to 0. We do this by 
    // taking fmod(t, 2*pi) because sin is a periodic
    // function with period 2*pi.
    t = my_fmod(t, 2.0f * PI);

    // Next we clamp to [-pi, pi] to get our t as
    // close to 0 as possible. We "reflect" any values 
    // greater than pi by subtracting them from pi. This 
    // works because sin is an odd function and so 
    // sin(-t) = -sin(t), and the particular shape of sin
    // combined with the choice of pi as the endpoint
    // takes care of the negative.
    if (t >= PI) {
        t = PI - t;
    }

    // You can precompute these if you want, but
    // the compiler will probably optimize them out.
    // These are the reciprocals of odd factorials.
    // (1/n! for odd n)
    const float c0 = 1.0f;
    const float c1 = c0 / (2.0f * 3.0f);
    const float c2 = c1 / (4.0f * 5.0f);
    const float c3 = c2 / (6.0f * 7.0f);
    const float c4 = c3 / (8.0f * 9.0f);
    const float c5 = c4 / (10.0f * 11.0f);
    const float c6 = c5 / (12.0f * 13.0f);
    const float c7 = c6 / (14.0f * 15.0f);
    const float c8 = c7 / (16.0f * 17.0f);

    // Increasing odd powers of t.
    const float t3  = t * t * t;
    const float t5  = t3 * t * t;
    const float t7  = t5 * t * t;
    const float t9  = t7 * t * t;
    const float t11 = t9 * t * t;
    const float t13 = t9 * t * t;
    const float t15 = t9 * t * t;
    const float t17 = t9 * t * t;

    return c0 * t - c1 * t3 + c2 * t5 - c3 * t7 + c4 * t9 - c5 * t11 + c6 * t13 - c7 * t15 + c8 * t17;
}

// Test the output
int main() {
    const float PI = 3.14159265359f;
    float t;

    for (t = 0.0f; t < 12.0f * PI; t += (PI * 0.25f)) {
        printf("sin(%f) = %f\n", t, sin_t(t));
    }

    return 0;
}

示例输出:

sin(0.000000) = 0.000000
sin(0.785398) = 0.707107
sin(1.570796) = 1.000000
sin(2.356194) = 0.707098
sin(3.141593) = 0.000000
sin(3.926991) = -0.707107
sin(4.712389) = -1.000000
sin(5.497787) = -0.707098
sin(6.283185) = 0.000398
...
sin(31.415936) = 0.000008
sin(32.201332) = 0.707111
sin(32.986729) = 1.000000
sin(33.772125) = 0.707096
sin(34.557522) = -0.000001
sin(35.342918) = -0.707106
sin(36.128315) = -1.000000
sin(36.913712) = -0.707100
sin(37.699108) = 0.000393

正如您所看到的,精确度仍有提升空间。我不是浮点运算的天才,所以可能其中一些与floor / fmod实现或执行数学运算的特定顺序有关。

答案 9 :(得分:2)

既然你试图生成一个信号,我认为使用微分方程应该不是一个坏主意!它给出了类似的东西

#include <stdlib.h>
#include <stdio.h>

#define DT (0.01f) //1/s
#define W0 (3)     //rad/s

int main(void) {
    float a = 0.0f;
    float b = DT * W0;
    float tmp;

    for (int i = 0; i < 400; i++) {
        tmp = (1 / (1 + (DT * DT * W0 * W0))) * (2 * a - b);
        b = a;
        a = tmp;

        printf("%f\n", tmp);
    }
}

仍设置信号的幅度和频率是颈部疼痛:/

答案 10 :(得分:1)

如果你解释为什么你不想要内置函数会有所帮助,但正如其他人所说,泰勒系列是估算价值的一种方法。然而,其他答案似乎实际上是使用Maclaurin系列,而不是泰勒。你应该有一个正弦和余弦的查找表。然后找到x 0 ,查找表中与您想要的x最接近的x值,找到d = x-x 0 。然后

sin(x)= sin(x 0 )+ cos(x 0 )* d-sin(x 0 )* d 2 / 2-cos(x 0 )* d 3 / 6 + ...

如果您的查找表是d <.01,那么每个术语的精度将超过两位数。

另一种方法是使用如果x = x 0 + d,那么

sin(x)= sin(x 0 )* cos(d)+ cos(x 0 )* sin(d)

你可以使用查找表来获得sin(x 0 )和cos(x 0 ),然后使用Maclaurin系列获得cos(d)和sin (d)。

答案 11 :(得分:0)

如果您需要浮点结果并接受整数度的分辨率,则可以使用此代码查找 sin。如果您需要 int(非浮点数)数字或更小的表,可以轻松修改代码:

    /**
     * Sinus lookup table
     * The table only covers whole degrees lookup, and only in I. quarter of the circle. The remaining three quarters
     * of the circle are figured out with logic.
     */
    const float sinus_I_quarter[91] =
    {
        0.0000, 0.0175, 0.0349, 0.0523, 0.0698, 0.0872, 0.1045, 0.1219, 0.1392, 0.1564, // 00 .. 09
        0.1736, 0.1908, 0.2079, 0.2250, 0.2419, 0.2588, 0.2756, 0.2924, 0.3090, 0.3256, // 10 .. 19
        0.3420, 0.3584, 0.3746, 0.3907, 0.4067, 0.4226, 0.4384, 0.4540, 0.4695, 0.4848, // 20 .. 29
        0.5000, 0.5150, 0.5299, 0.5446, 0.5592, 0.5736, 0.5878, 0.6018, 0.6157, 0.6293, // 30 .. 39
        0.6428, 0.6561, 0.6691, 0.6820, 0.6947, 0.7071, 0.7193, 0.7314, 0.7431, 0.7547, // 40 .. 49
        0.7660, 0.7771, 0.7880, 0.7986, 0.8090, 0.8192, 0.8290, 0.8387, 0.8480, 0.8572, // 50 .. 59
        0.8660, 0.8746, 0.8829, 0.8910, 0.8988, 0.9063, 0.9135, 0.9205, 0.9272, 0.9336, // 60 .. 69
        0.9397, 0.9455, 0.9511, 0.9563, 0.9613, 0.9659, 0.9703, 0.9744, 0.9781, 0.9816, // 70 .. 79
        0.9848, 0.9877, 0.9903, 0.9925, 0.9945, 0.9962, 0.9976, 0.9986, 0.9994, 0.9998, // 80 .. 89
        1.0000                                                                          // 90
    };

    #define CIRCLE_QUARTER_1        1
    #define CIRCLE_QUARTER_2        2
    #define CIRCLE_QUARTER_3        3
    #define CIRCLE_QUARTER_4        4
    

    /**
     * The function converts passed angle into 0..89 degrees and looks up a corresponding sin() value.
     * After lookup the return value is adjusted depending what was the original passed angle by figuring
     * out belonging to circle quarter I., II, II. or IV.
     * Any angle outside the accepted angle range of 0 .. 359 will be automatically corrected.
     *
     * @param angle     Whole angle in degrees as used in mathematics: 0 degrees is EAST (in meteorology, 0 degrees wind is NORTH)
     * @return          The looked-up 4-decimal points accurate sinus
     */
    float sinus_lookup (unsigned int angle)
    {
        float sin_value;
        unsigned int circle_quarter;
    
        // correct angles outside the accepted angle range into 0 .. 359
        if (angle > 359u)
            angle = angle % 360u;
    
        circle_quarter = 1 + (angle / 90u);
    
        switch (circle_quarter)
        {
            case CIRCLE_QUARTER_1: // 00 .. 89
                sin_value = sinus_I_quarter[angle];
                break;
    
            case CIRCLE_QUARTER_2: // 90 .. 179
                sin_value = sinus_I_quarter[180 - angle];
                break;
    
            case CIRCLE_QUARTER_3: // 180 .. 269
                sin_value = -sinus_I_quarter[angle - 180];
                break;
    
            case CIRCLE_QUARTER_4: // 270 .. 359
                sin_value = -sinus_I_quarter[360 - angle];
                break;
        }
    
        return sin_value;
    }