Generate sine signal in C without using the standard function

后端 未结 11 744
情歌与酒
情歌与酒 2021-02-02 09:41

I want to generate a sine signal in C without using the standard function sin() in order to trigger sine shaped changes in the brightness of a LED. My basic idea was to use a lo

相关标签:
11条回答
  • 2021-02-02 09:41

    OP's main problem is in generating the index for the table look-up.

    OP's code attempts to access outside array sine_table[40] leading to undefined behavior. Fix that at least.

    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
    

    Suggested change

        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])*...  
    

    There exist much better approaches, yet to focus on OP's method see below.

    #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;
      }
    }
    

    Output

             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
    

    Better code would not pass the values i, x1, x2, y as global variables, but as function parameters or function variables. Perhaps that is an artifact of OP's debugging.


    Does anybody have a better idea to implement a sine generator in C?

    This is quite broad. Better as in speed, precision, code space, portability, or maintainability? sine() functions are easy to make. High-quality ones take more effort.

    Although fuzzy, OP's use of a small look-up table is a good beginning - although I see it can be done without any floating point math. I recommend for OP to construct a tested and working solution and post it in Code Review for improvement ideas.

    0 讨论(0)
  • 2021-02-02 09:46

    You could use the first few terms of the Taylor series expansion of sin. You can use as few terms as needed to reach your intended level of precision - a few more terms than the below example should start to bump up against the limits of a 32-bit float.

    Example:

    #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;
    }
    

    Example output:

    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
    

    As you can see there is still some room for improvement in precision. I am not a genius with floating point arithmetic so probably some of it has to do with the floor/fmod implementations or the specific order the mathematical operations are performed in.

    0 讨论(0)
  • Have you considered modelling the portion of the sine curve from [0..PI] as a parabola? If the brightness of the LED is only intended to be observed by a human eye, the shapes of the curves ought to be similar enough so that little difference would be detected.

    You would just need to figure out the appropriate equation to describe it.

    Hmmm, ...

    Vertex at (PI/2, 1)

    X-axis intersections at (0, 0) and (PI, 0)

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

    Where K would be ...

    K = 4 / (PI * PI)
    
    0 讨论(0)
  • 2021-02-02 09:48

    For a LED, you could probably just do with 16 or so steps without even interpolating. That said, I can see at least two odd things in your sin1() function:

    1) You have 40 data points in sine_table, but you're taking the index x1 modulo 41 of the input. That doesn't seem the right way to handle the periodicity, and lets x1 point past the last index of the array.
    2) You're then adding +1, so x2 can be even more over the limits of the array.
    3) You're using i in the function, but it's only set in the main program. I can't tell what it's supposed to do, but using a global like that in a simple calculation function seems dirty at minimum. Maybe it's supposed to provide the fractional part for the interpolation, but shouldn't you use phase for that.

    Here's a simple interpolator, which seems to work. Adjust to taste.

    #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]);
    }
    

    Some arbitrary sample outputs:

    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
    

    (I'll leave it to the interested reader to replace all occurrences of 3 with a properly defined symbolic constant, to generalize the function further, and to implement calculating the negative phase too.)

    0 讨论(0)
  • 2021-02-02 09:48

    Since you try to generate a signal, i think use a differential equation should not be a bad idea ! it give something like that

    #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);
        }
    }
    

    Still set the amplitude and frequency of the signal is a pain in the neck :/

    0 讨论(0)
  • 2021-02-02 09:50

    Generating an accurate sine function requires an amount of resource (CPU cycles and memory) that is unwarranted in this application. Your aim to generate a "smooth" sine curve is failing to consider the requirements of the application.

    • While when you draw the curve you may observe imperfections, when you apply that curve to an LED PWM drive, the human eye will not perceive those imperfections at all.

    • Neither is the human eye likely to perceive the difference in brightness between adjacent values in even a 40-step curve, so interpolation is not necessary.

    • It will be more efficient in general if you generate a sine function that generates the appropriate PWM drive values directly without floating point. In fact rather than a sine function a scaled raised cosine would be more appropriate, so that an input of zero results in an output of zero, and an input of half the number of values in the cycle results in the maximum value for your PWM drive.

    The following function generates a raised cosine curve for an 8-bit FSD PWM from a 16 value (and 16 bytes) lookup generating a 59-step cycle. So it is both memory and performance efficient compared to your 40 step floating point implementation.

    #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 ;
    }
    

    For an input of 0 <= theta < PWM_THETA_MAX the curve looks like this:

    Which is I suggest plenty smooth enough for illumination.

    In practice you might use it thus:

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

    If your PWM range is not 0 to 255, simply scale the output of the function; 8-bit resolution is more than enough for the task.

    0 讨论(0)
提交回复
热议问题