28 votes

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

Je veux générer un signal sinusoïdal en C sans utiliser la fonction standard sin() afin de déclencher des changements sinusoïdaux dans la luminosité d'une LED. Mon idée de base était d'utiliser une table de consultation 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é sinusoïdaux d'une LED, mais ceux-ci sont très mauvais).

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

0 votes

Les commentaires ne sont pas destinés à une discussion approfondie ; cette conversation a été déplacé vers le chat .

21voto

chux Points 13185

Le problème principal du PO est de générer l'index pour la consultation de la table.

Le code de l'OP tente d'accéder à un tableau extérieur sine_table[40] conduisant à comportement indéfini . Réparez au moins ça.

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

Modification suggérée

    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 du PO, 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 passerait pas les valeurs i, x1, x2, y comme variables globales, mais comme paramètres de fonction ou variables de fonction. Peut-être est-ce un artefact du débogage de l'OP.


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

C'est assez large. Meilleur en termes de vitesse, de précision, d'espace de code, de portabilité ou de maintenabilité ? sine() sont faciles à réaliser. Celles de haute qualité demandent plus d'efforts.

Bien que floue, l'utilisation par le PO d'une petite table de consultation est un bon début - même si je vois que cela peut être fait sans aucune mathématique à virgule flottante. Je recommande à l'OP de construire une solution testée et fonctionnelle et de la poster dans le forum de l'UE. Examen du code pour des idées d'amélioration.

18voto

ryyker Points 3662

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

Edit : Suggérer une première lecture cet article pour avoir une idée de ce que demande l'OP.

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

Le programme CORDIC ( Rotation des coordonnées Ordinateur numérique ) est très bien adapté à une utilisation sur des processeurs plus petits et des implémentations FPGA dont les capacités de calcul mathématique sont limitées, car il permet de réduire les coûts. calcule le sinus et le cosinus d'une valeur en utilisant uniquement l'arithmétique de base (addition, soustraction et décalage). . 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 de mise en œuvre d'algorithmes. CORDIC simple est celle qui comprend des explications détaillées sur la façon de générer un tableau qui peut ensuite être précompilé pour être utilisé sur votre périphérique cible, ainsi que du code pour tester la sortie de la fonction suivante (qui utilise des mathématiques en virgule fixe) :

(Voir la documentation des fonctions suivantes et d'autres fonctions 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;
}

Edit :
J'ai trouvé la documentation pour utiliser les exemples à l'adresse suivante CORDIC simple très facile à suivre. Cependant, j'ai rencontré un petit problème lors de la compilation du fichier cordic-test.c l'erreur : Utilisation de l'identifiant non déclaré "M_PI". s'est produit. Il semble que lors de l'exécution du programme compilé gentable.c (qui génère le fichier cordic-test.c ) 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 ce problème résolu, tout a fonctionné comme prévu.

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é en reflétant et en transposant la section de données originale.

enter image description here

15voto

Clifford Points 29933

La génération d'une fonction sinusoïdale précise nécessite une quantité de ressources (cycles de CPU et mémoire) qui n'est pas justifiée 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 dessinez la courbe vous pouvez observer des imperfections, lorsque vous appliquer cette courbe à une commande PWM de LED, l'œil humain ne percevra pas ces imperfections.

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

  • Ce sera plus efficace en général si vous générez une fonction sinus qui génère les valeurs de commande PWM appropriées directement sans virgule flottante. En fait, plutôt qu'une fonction sinus, une fonction cosinus surélevé mis à l'échelle serait plus approprié, de sorte qu'une entrée de zéro résulte en une sortie de zéro, et une entrée de la moitié du nombre de valeurs dans le cycle résulte en la valeur maximale pour votre commande PWM.

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

#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 à ça :

enter image description here

Ce qui est, je pense, suffisamment lisse pour être éclairé.

En pratique, vous pourriez 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 comprise entre 0 et 255, mettez simplement à l'échelle la sortie de la fonction ; une résolution de 8 bits est plus que suffisante pour cette tâche.

7voto

Eric Postpischil Points 36641

La méthode classique pour dessiner un cercle (et donc aussi pour générer une onde sinusoïdale) est la suivante 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 obtiendrez peut-être des valeurs légèrement supérieures à 1, mais vous pourrez les ajuster en divisant par le maximum ou en arrondissant. Il est également possible d'utiliser l'arithmétique des nombres entiers et d'éliminer la multiplication/division en utilisant une puissance négative de deux pour les valeurs suivantes e On peut donc utiliser shift à la place.

6voto

Sparky Points 4660

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 devraient être suffisamment similaires pour que peu de différence soit détectée.

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

Hmmm, ...

Sommet à (PI/2, 1)

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

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

Où K serait...

K = 4 / (PI * PI)

4 votes

Une série sur Taylor serait très bien. f(x) = x - x^3 / 3! + x^5 / 5! - x^7 / 7! + ... . Vous pouvez étendre la série aussi loin que vous le souhaitez pour plus de précision.

2 votes

Avec seulement 5 termes, l'erreur la plus importante est de 0,007 sur l'intervalle (-pi, pi).

3 votes

@Gregor vous pouvez obtenir la même précision avec seulement 3 termes si vous développez en polynômes de Chebyshev jusqu'au degré 3 : f(x) = 0.98402080986412*x-0.15330167221686*x^3+0.00545232213057963*‌​x^5 .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X