web-dev-qa-db-ja.com

標準関数を使用せずにCで正弦信号を生成

LEDの輝度のサイン形状の変化をトリガーするために、標準関数sin()を使用せずにCでサイン信号を生成します。私の基本的なアイデアは、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でサインジェネレーターを実装するより良いアイデアはありますか?

27
Peter123

OPの主な問題は、テーブル検索用のインデックスを生成することです。

OPのコードは、配列_sine_table[40]_の外部にアクセスして、 undefined behavior にアクセスしようとします。少なくともそれを修正してください。

_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 に投稿して改善案を出すことをお勧めします。

21
chux

... Cでサインジェネレーターを実装するより良いアイデア?

編集:最初に読むことを提案この記事OPが何を求めているのかを理解してください。

あなたの質問で提供されたコンテキストから、Wordbetterはサイズに関係していると思われます小型のマイクロプロセッサで実行するために必要になる可能性のある、コンパイルされたコードの速度。

CORDIC(COordinate Rotation DIgital Computer)アルゴリズムは、より小さなuP、および基本的な算術演算(加算、減算、シフト)のみを使用した値のサインとコサイン。 CORDICの詳細と、それを使用して角度のサイン/コサインを生成する方法ここで提供

アルゴリズムの実装例を提供するサイトもいくつかあります。 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;
}

Edit:
サンプルを使用するためのドキュメントは、Simple CORDICサイトで非常にわかりやすいとわかりました。しかし、ファイルのコンパイル時に小さな問題が発生しましたcordic-test.cエラー:undeclared identifier 'M_PI'が発生しました。コンパイルされたgentable.cファイル(cordic-test.cファイルを生成する)を実行すると、次の行が表示されます。

#define M_PI 3.1415926535897932384626

独自の宣言には含まれていますが、ファイルcordic-test.cの生成に使用されるprintfステートメントには含まれていません。これが修正されると、すべてが宣伝どおりに機能しました。

文書化されているように、生成されるデータの範囲は、完全な正弦サイクルの1/4(-π/ 2-π/ 2)を生成します。次の図には、水色のドットの間に生成された実際のデータの表現が含まれています。サイン信号の残りの部分は、元のデータセクションのミラーリングと転置によって作成されます。

enter image description here

18
ryyker

正確な正弦関数を生成するには、このアプリケーションでは保証されない量のリソース(CPUサイクルとメモリ)が必要です。 「滑らかな」正弦曲線を生成するという目的は、アプリケーションの要件を考慮することに失敗しています。

  • 曲線を描くと欠陥が観察される場合がありますが、その曲線をLED PWMドライブに適用すると、人間の目はそれらの欠陥をまったく認識しません。

  • 人間の目は、40ステップの曲線であっても、隣接する値の間の明るさの違いを認識しそうにないため、補間は必要ありません。

  • 一般に、浮動小数点を使用せずに適切なPWM駆動値を直接生成する正弦関数を生成すると、より効率的です。実際には、正弦関数ではなく、スケーリングされたコサインがより適切であるため、ゼロの入力はゼロの出力になり、入力はサイクル内の値の数が半分になると、PWMドライブの最大値になります。

次の関数は、59値のサイクルを生成する16値(および16バイト)ルックアップから8ビットFSD PWMのコサインロールオフ曲線を生成します。したがって、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ビットの解像度で十分です。

14
Clifford

LEDの場合は、おそらく補間することなく、16程度のステップで実行できます。そうは言っても、sin1()関数には少なくとも2つの奇妙なことがあります。

1)sine_tableには40個のデータポイントがありますが、入力の41を法とするインデックスx1を取っています。これは周期性を処理する正しい方法ではないようで、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
ilkkachu

[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)
6
Sparky

円を描く(したがって正弦波も生成する)古典的なハックは Hakmem#149 by Marvin Minsky です。例えば。、:

#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を使用できます。

6

私はサイン関数の近似である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

5
AgapwIesu

アプリケーションが真の精度を必要としない限り、40ポイントの正弦波または余弦波のアルゴリズムを思い付かないでください。また、テーブルの値は、LEDのpwm入力の範囲と一致する必要があります。

とはいえ、私はあなたのコードとその出力を見て、あなたがポイント間を補間していないと考えました。少し修正して修正しましたが、Excelのサイン関数とユーザーのサイン関数との間のエラーは、最大で約0.0032程度オフです。この変更の実装は非常に簡単で、Cアルゴリズムテストの個人的な目的であるtccを使用してテストされています。

最初に、サイン配列にもう1点追加しました。最後のポイントは、サイン配列の最初の要素と同じ値に設定されます。これにより、特にx1を(int)phase%40に設定し、x2をx1 + 1に設定した場合に、正弦関数の計算が修正されます。 x2を(x1 + 1)%40に設定できるため、余分なポイントを追加する必要はありませんが、最初のアプローチを選択しました。これを実現するためのさまざまな方法を指摘しています。剰余の計算も追加しました(基本的にフェーズ int)フェーズ)。残りを補間に使用しています。一時的なサインバリューホルダーとデルタ変数も追加しました。

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

5
bill

sinTaylorシリーズ展開 の最初のいくつかの用語を使用できます。目的の精度レベルに到達するために必要な数の用語を使用できます。以下の例よりもさらに多くの用語は、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実装または数学演算が実行される特定の順序に関係しています。

2
Teh JoE

信号を生成しようとするので、微分方程式を使用することは悪い考えではないはずです!それはそのようなものを与えます

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

それでも信号の振幅と周波数を設定すると、首の痛みです:/

2
alkaya

組み込み関数が必要ない理由を説明すると役立ちますが、他の人が言ったように、テイラー級数は値を推定する1つの方法です。ただし、他の答えは、実際にはテイラーではなく、Maclaurinシリーズを使用しているようです。サインとコサインの両方のルックアップテーブルが必要です。次にxを見つける、ルックアップテーブルで必要なxに最も近いx値を見つけ、d = x-xを見つける。それから

sin(x)= sin(x)+ cos(x)* d-sin(x)* d2/ 2-cos(x)* d3/ 6 + ...

ルックアップテーブルがd <.01である場合、用語ごとに2桁以上の精度が得られます。

別の方法は、x = x+ d、その後

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

ルックアップテーブルを使用して、sin(x)およびcos(x)、Maclaurinシリーズを使用してcos(d)およびsin(d)を取得します。

1
Acccumulation