web-dev-qa-db-fra.com

Générer un signal sinusoïdal en C sans utiliser la fonction standard

Je veux générer un signal sinus en C sans utiliser la fonction standard sin () afin de déclencher des changements en forme de sinus dans la luminosité d'une LED. Mon idée de base était d'utiliser une table de recherche avec 40 points et une interpolation.

Voici ma première approche:

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

Malheureusement, cette fonction renvoie parfois des valeurs bien plus grandes que 1. De plus, l'interpolation ne semble pas être bonne (je l'ai utilisée pour créer des changements de luminosité en forme de sinusoïde d'une LED, mais ceux-ci sont très mous).

Quelqu'un at-il une meilleure idée d'implémenter un générateur de sinus en C?

27
Peter123

Le principal problème d'OP est de générer l'index pour la recherche de table.

Le code OP tente d'accéder au tableau extérieur sine_table[40] Conduisant à comportement indéfini . Réparez cela au moins.

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

Changement suggéré

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

Il existe de bien meilleures approches, mais pour se concentrer sur la méthode OP voir ci-dessous.

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

Sortie

         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

Un meilleur code ne transmettrait pas les valeurs i, x1, x2, y En tant que variables globales, mais en tant que paramètres de fonction ou variables de fonction. C'est peut-être un artefact du débogage d'OP.


Quelqu'un at-il une meilleure idée d'implémenter un générateur de sinus en C?

C'est assez large. Mieux qu'en vitesse, précision, espace de code, portabilité ou maintenabilité? sine() les fonctions sont faciles à créer. Ceux de haute qualité demandent plus d'efforts.

Bien que floue, l'utilisation par OP d'une petite table de correspondance est un bon début - même si je vois que cela peut être fait sans aucun calcul en virgule flottante. Je recommande à OP de construire une solution testée et fonctionnelle et de la publier dans Code Review pour des idées d'amélioration.

21
chux

... une meilleure idée d'implémenter un générateur de sinus en C?

Edit: Suggérez la première lecture cet article pour comprendre ce que OP demande.

D'après le contexte fourni dans votre question, je suppose que le mot mieux pourrait avoir à voir avec la taille et la vitesse du code compilé , comme cela peut être nécessaire pour fonctionner sur un petit microprocesseur.

L'algorithme CORDIC (COordinate Rotation DIgital Computer) est très approprié pour une utilisation sur des implémentations uP plus petites et FPGA qui ont des capacités de calcul mathématiques limitées car il calcule le sinus et le cosinus d'une valeur en utilisant uniquement arithmétique de base (addition, soustraction et décalages). En savoir plus sur CORDIC, et comment l'utiliser pour produire le sinus/cosinus d'un angle sont fournis ici.

Il existe également plusieurs sites qui fournissent des exemples d'implémentation d'algorithmes. Simple CORDIC est celui qui comprend des explications détaillées sur la façon de générer une table qui peut ensuite être précompilée pour une utilisation sur votre appareil cible, ainsi que du code pour tester la sortie de la fonction suivante (qui utilise des mathématiques à virgule fixe):

(Voir la documentation des fonctions suivantes et autres dans le lien)

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

Modifier:
J'ai trouvé la documentation d'utilisation des exemples sur le site Simple CORDIC très facile à suivre. Cependant, une petite chose que j'ai rencontrée était lors de la compilation du fichier cordic-test.c l'erreur: l'utilisation de l'identifiant non déclaré 'M_PI' s'est produite. Il semble que lors de l'exécution du gentable.c fichier (qui génère le cordic-test.c fichier) la ligne:

#define M_PI 3.1415926535897932384626

bien qu'inclus dans ses propres déclarations, n'était pas inclus dans les instructions printf utilisées pour produire le fichier cordic-test.c. Une fois que cela a été corrigé, tout a fonctionné comme annoncé.

Comme documenté, la gamme de données produites génère 1/4 d'un cycle sinusoïdal complet (-π/2 - π/2). L'illustration suivante contient une représentation des données réelles produites entre les points bleu clair. Le reste du signal sinusoïdal est fabriqué via la mise en miroir et la transposition de la section de données d'origine.

enter image description here

18
ryyker

La génération d'une fonction sinus précise nécessite une quantité de ressources (cycles CPU et mémoire) qui n'est pas garantie dans cette application. Votre objectif de générer une courbe sinusoïdale "lisse" ne tient pas compte des exigences de l'application.

  • Alors que lorsque vous tracez la courbe, vous pouvez observer des imperfections, lorsque vous appliquez cette courbe à un lecteur PWM LED, l'œil humain ne percevra pas du tout ces imperfections.

  • L'œil humain n'est pas non plus susceptible de percevoir la différence de luminosité entre des valeurs adjacentes, même sur une courbe à 40 pas, une interpolation n'est donc pas nécessaire.

  • Il sera plus efficace en général si vous générez une fonction sinus qui génère directement les valeurs d'entraînement PWM appropriées sans virgule flottante. En fait, plutôt qu'une fonction sinusoïdale, un cosinus surélevé à l'échelle serait plus approprié, de sorte qu'une entrée de zéro entraîne une sortie de zéro et une entrée de la moitié du nombre de valeurs dans le cycle entraîne la valeur maximale pour votre lecteur PWM.

La fonction suivante génère une courbe en cosinus surélevée pour un PWM FSD 8 bits à partir d'une recherche de 16 valeurs (et 16 octets) générant un cycle de 59 étapes. Il est donc à la fois efficace en mémoire et en performances par rapport à votre implémentation à virgule flottante en 40 étapes.

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

Pour une entrée de 0 <= theta < PWM_THETA_MAX la courbe ressemble à ceci:

enter image description here

Ce qui est je suggère beaucoup assez lisse pour l'éclairage.

En pratique, vous pouvez l'utiliser ainsi:

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

Si votre plage PWM n'est pas de 0 à 255, modifiez simplement la sortie de la fonction; La résolution 8 bits est plus que suffisante pour la tâche.

14
Clifford

Pour une LED, vous pourriez probablement le faire avec environ 16 étapes sans même interpoler. Cela dit, je peux voir au moins deux choses étranges dans votre fonction sin1():

1) Vous avez 40 points de données dans sine_table, mais vous prenez l'index x1 modulo 41 de l'entrée. Cela ne semble pas être la bonne façon de gérer la périodicité et laisse x1 pointe au-delà du dernier index du tableau.
2) Vous ajoutez ensuite +1, donc x2 peut être encore plus au-dessus des limites du tableau.
3) Vous utilisez i dans la fonction, mais elle n'est définie que dans le programme principal. Je ne peux pas dire ce qu'il est censé faire, mais utiliser un global comme celui-ci dans une simple fonction de calcul semble au minimum sale. Peut-être que c'est censé fournir la partie fractionnaire pour l'interpolation, mais ne devriez-vous pas utiliser phase pour cela.

Voici un interpolateur simple, qui semble fonctionner. Ajustez au goût.

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

Quelques sorties d'échantillons arbitraires:

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

(Je laisse au lecteur intéressé le soin de remplacer toutes les occurrences de 3 avec une constante symbolique correctement définie, pour généraliser davantage la fonction et pour implémenter également le calcul de la phase négative.)

6
ilkkachu

Avez-vous envisagé de modéliser la partie de la courbe sinusoïdale de [0..PI] comme une parabole? Si la luminosité de la LED est uniquement destinée à être observée par un œil humain, les formes des courbes doivent être suffisamment similaires pour que peu de différence soit détectée.

Il vous suffirait de trouver l'équation appropriée pour la décrire.

Hmmm, ...

Sommet à (PI/2, 1)

Intersections de l'axe X en (0, 0) et (PI, 0)

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

Où K serait ...

K = 4 / (PI * PI)
6
Sparky

Le hack classique pour dessiner un cercle (et donc générer également une onde sinusoïdale) est Hakmem # 149 par Marvin Minsky . Par exemple.,:

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

Il sera légèrement excentrique, pas un cercle parfait, et vous pouvez obtenir des valeurs légèrement supérieures à 1, mais vous pouvez ajuster en divisant par le maximum ou en arrondissant. De plus, l'arithmétique entière peut être utilisée et vous pouvez éliminer la multiplication/division en utilisant une puissance négative de deux pour e, donc shift peut être utilisé à la place.

6
Eric Postpischil

J'irais avec l'approximation de Bhaskara I d'une fonction sinus. En utilisant des degrés, de 0 à 180, vous pouvez approximer la valeur ainsi

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

si vous voulez tenir compte de n'importe quel angle, vous ajouteriez

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

Si vous voulez le faire en radians, vous ajouteriez

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

Voici un graphique montrant les points calculés avec cette approximation ainsi que les points calculés avec la fonction sinus. Vous pouvez à peine voir les points d'approximation furtivement sous les points sinusoïdaux réels.

enter image description here

5
AgapwIesu

À moins que votre application ne demande une réelle précision, ne vous tuez pas en proposant un algorithme pour une onde sinusoïdale ou cosinusoïdale de 40 points. En outre, les valeurs de votre tableau doivent correspondre à la plage d'entrée pwm de votre LED.

Cela dit, j'ai regardé votre code et sa sortie et j'ai pensé que vous n'interpoliez pas entre les points. Avec une petite modification, je l'ai corrigé, et l'erreur entre la fonction de signe d'Excel et la vôtre est désactivée par un maximum d'environ 0,0032 ou environ. Le changement est assez facile à implémenter et a été testé en utilisant tcc, mon go-to personnel pour les tests d'algorithme C.

Tout d'abord, j'ai ajouté un point de plus à votre réseau sinusoïdal. Le dernier point est défini sur la même valeur que le premier élément du tableau sinus. Cela corrige le calcul dans votre fonction sinus, en particulier lorsque vous définissez x1 sur (int) la phase% 40 et x2 sur x1 + 1. L'ajout du point supplémentaire n'est pas nécessaire, car vous pouvez définir x2 sur (x1 + 1)% 40, mais j'ai choisi la première approche. Je souligne simplement différentes façons d'accomplir cela. J'ai également ajouté le calcul d'un reste (phase de base - phase (int)). J'utilise le reste pour l'interpolation. J'ai également ajouté un détenteur temporaire de valeur sinusoïdale et une variable 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;
}

Les résultats semblent plutôt bons. La comparaison de l'approximation linéaire et de la fonction sinusoïdale à virgule flottante du système m'a donné le graphique d'erreur ci-dessous.

Error Plot

Combined Error vs Sine graph

5
bill

Vous pouvez utiliser les premiers termes du extension de la série Taylor of sin. Vous pouvez utiliser aussi peu de termes que nécessaire pour atteindre le niveau de précision souhaité - quelques termes de plus que l'exemple ci-dessous devraient commencer à se heurter aux limites d'un flottant 32 bits.

Exemple:

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

Exemple de sortie:

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

Comme vous pouvez le voir, il y a encore de la place pour une amélioration de la précision. Je ne suis pas un génie de l'arithmétique à virgule flottante, donc cela a probablement à voir avec les implémentations floor/fmod ou l'ordre spécifique dans lequel les opérations mathématiques sont effectuées.

2
Teh JoE

Puisque vous essayez de générer un signal, je pense que l'utilisation d'une équation différentielle ne devrait pas être une mauvaise idée! ça donne quelque chose comme ça

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

Toujours régler l'amplitude et la fréquence du signal est une douleur dans le cou: /

2
alkaya

Il serait utile que vous expliquiez pourquoi vous ne voulez pas de la fonction intégrée, mais comme d'autres l'ont dit, la série Taylor est un moyen d'estimer la valeur. Cependant, les autres réponses semblent effectivement utiliser la série Maclaurin, pas Taylor. Vous devriez avoir une table de recherche de sinus et cosinus. Trouvez ensuite x, la valeur x la plus proche dans votre table de recherche du x souhaité et recherchez d = x-x. ensuite

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

Si votre table de recherche est telle que d <.01, alors vous obtiendrez plus de deux chiffres de précision par terme.

Une autre méthode consiste à utiliser le fait que si x = x+ d, puis

sin (x) = sin (x) * cos (d) + cos (x) * péché (d)

Vous pouvez utiliser une table de recherche pour obtenir sin (x) et cos (x), puis utilisez la série Maclaurin pour obtenir cos (d) et sin (d).

1
Acccumulation