Forum: Mikrocontroller und Digitale Elektronik DDS Sinus Tabelle wie berechnen ?


Announcement: there is an English version of this forum on EmbDev.net. Posts you create there will be displayed on Mikrocontroller.net and EmbDev.net.
von Markus (Gast)


Lesenswert?

Hallo,
ich versuche mich gerade an einem DDS via PWM mit einem Mega8.
Soweit ich DDS verstanden habe legt man eine Tabelle mit Sinus Werten
in den Speicher und variiert dann via Offset Frequenz und Amplitude.
Doch wie berechne ich diese Sinus Tabelle korrekt ?
Sie müßte ja von 0 über 1 zu 0 gehen, wobei 0 jeweils den Index 0 und
255 hätte, dann wäre 1 bei Index 127 zu finden.
Stimmt das ?
Wie berechne ich nun die Tabelle korrekt ?
Es gibt ja keine Gleitkomma-Zahlen, sondern ich muß den Wert auf einen
8bit Integer abbilden.
Ich suche keine fertige Tabelle, sondern eine Formel oder Erklärung wie
man eine solche Tabelle richtig berechnet !
Meine Tabelle soll mit 255 8bit Werten einen vollen Sinus abbilden, bei
8bit Fast PWM.
Danke,
Markus

von Christoph Kessler (db1uq) (Gast)


Lesenswert?

Zum Beispiel mit einem Tabellenkalkulationsprogramm, Excel oder
OpenOffice-Calc. Damit geht sogar eine direkte Ausgabe als Hex-Zahl.

von Benedikt (Gast)


Lesenswert?

Schreib dir ein C Programm das die Werte berechnet:
for (i=0; i<255; i++)
wert[i]=128+127*sin((double)i/256*2*pi);

Da du den Wert ja als 8bit ausgibst, normierst du die Sinuskurve
einfach auch auf den 8bit Bereich: +1 -> +127, -1 -> -127, 0->0

von Markus (Gast)


Lesenswert?

Zitat Benedikt:
"Schreib dir ein C Programm das die Werte berechnet:
for (i=0; i<255; i++)
wert[i]=128+127*sin((double)i/256*2*pi);

Da du den Wert ja als 8bit ausgibst, normierst du die Sinuskurve
einfach auch auf den 8bit Bereich: +1 -> +127, -1 -> -127, 0->0"

Hmmmmm,
da fällt mir gerade auf das der Sinus ja in den negativen Bereich geht,
ich aber ja nur von 0V auf VCC gehen kann ?
Wie wird dann die negative Hälfte des Sinus dargestellt ?
Null-Linie bei VCC/2 ?
Und Dein Typecast ist wegen der sin-Funktion, stimmts ?
Danke für die Hilfe ;)
Bye,
Markus

von Benedikt (Gast)


Lesenswert?

Ja, -127 -> 0V, 0 -> Vcc/2, +127 -> Vcc
OK, nicht ganz, da eigentlich -128 0V entspricht, aber dieses 1 bit
bemerkt man sowiso nicht.

Der Typecast ist, damit die Division nicht als int durchgeführt und
somit der Kommateil abgeschnitten wird.

von Karl heinz B. (kbucheg)


Lesenswert?

> Wie wird dann die negative Hälfte des Sinus dargestellt ?

Du bist aber sehr fanatasielos.

> Null-Linie bei VCC/2 ?
Zum Bleistift.

> Und Dein Typecast ist wegen der sin-Funktion, stimmts ?

Nein.
Traditionell heist eine Integer-Laufvariable in einer
Schleife 'i'. Wir können also davon ausgehen, dass i ein
int ist.
Was ist dann das Ergebnis von i/256

Hinweis: i ist ein int, 256 ist ein int. Also ist das Ergebnis
von i/256 ebenfalls ein int und die Division wird als ganze
Zahl ausgeführt.

Mit dem cast wird der Compiler gezwunden, die Division als
Gleitkommaoperation durchzuführen, da er dann einen double
durch einen int dividiert. Gleiches hätte man auch
erreichen können, indem man die 256 zu einem double macht:

    i / 256.0

von hal (Gast)


Lesenswert?

Mit DDS ist es möglich nur (Sinus)Signale mit Frequenzen etwa = 1/8 der 
Stützstellen vernünftig darzustellen, max. jedoch bis 1/2 der 
Stützstellen. Also, wenn man pro Periode 256 Stützstellen hat, kann man 
mehr oder weniger sinus-ähnlich 256 Samples/Sek/8 = 32 Hz Signale 
ausgeben.

von Falk B. (falk)


Lesenswert?

@ hal (Gast)

Schon mal aufs Datum des letzten Postings geschaut?

MfG
Falk

von Stefan M. (stefan_m)


Lesenswert?

>
> Schon mal aufs Datum des letzten Postings geschaut?

Scheinbar nicht :D

von Alex (Gast)


Lesenswert?

Welches Datum?

von Gebhard R. (Firma: Raich Gerätebau & Entwicklung) (geb)


Lesenswert?

Hier ein kleines C-Programm wo du Auflösung,Amplitude und Offset 
einstellen kannst.
Es liefert gleich einen Integer-Buffer, der mitcompiliert werden kann.
1
#include <ansi_c.h>
2
3
#define LEN  512
4
#define PI 3.141592654
5
#define MAX_AMP 1154
6
#define OFFSET  0x07FF
7
8
void main(void)
9
{
10
  int i,n;
11
  double sin_val;
12
  int cal_val;
13
14
  n=0;
15
16
  printf("\n");
17
  printf("#define TABLES_LEN %d\n", LEN);
18
  printf("const uint16_t TableS[] = {\n");
19
20
  while(n<LEN){
21
    for(i=0;i<8;i++){
22
      sin_val= sin(2.0*PI*n/LEN);
23
      cal_val= sin_val*MAX_AMP + OFFSET;
24
      printf("%d,",cal_val);
25
      n++;
26
    }
27
    printf("\n");          
28
  }          
29
30
  printf("}\n"); 
31
}

: Bearbeitet durch User
von Oli P. (atomicjunkie)


Angehängte Dateien:

Lesenswert?

Ich hab mal für einen Sinus Generator 2-PWM-Ausgänge benutzt.
Vorteil ist, man hat eine höhere Auflösung (0-1 entspricht 0-PWM_MAX), 
da nur eine Viertelperiode in der Tabelle gespeichert werden muss.
Dazu habe ich mir zu nutze gemacht, dass die Sinuskurve 2x gespiegelt 
werden kann (horizontal / vertikal)
Mit nachgeschaltetem Operationsverstärker kann man somit negative 
Sinuswerte erzeugen.

Das kurze kurze Testprogramm und ein Screenshot der Simulation befindet 
sich im Anhang.

von Tom (Gast)


Lesenswert?

Hi!

Was ist das für eine Simulationssoftware, die hier verwendet wurde?

Gruss,
Tom

von HierBeliebigenNamenEinfügen (Gast)


Lesenswert?

Würde ich auch gerne Wissen

von Ingenieur (Gast)


Lesenswert?

Hm, ist das gfs "electronics workbench"?

von Harald W. (wilhelms)


Lesenswert?

Markus schrieb 2006:

Was ist das denn für ein seltsamer Thread? Wird der alle 1...2
Jahre einmal geweckt?

von Helmut L. (helmi1)


Lesenswert?

Harald Wilhelms schrieb:
> Was ist das denn für ein seltsamer Thread? Wird der alle 1...2
> Jahre einmal geweckt?

Ist wie der Hoppeditz, der wird auch einmal im Jahr geweckt.

von Henrik H. (Firma: TU Chemnitz) (heha)


Lesenswert?

Weil das Problem immer wieder auftaucht: Mit einem externen Programm 
(awk, Excel, C usw.) Tabellen für einen Mikrocontroller zu berechnen ist 
seit C++14 obsolet oder zumindest old-school.

Aus der C++-Dokumentation schlau zu werden ist eine andere Sache. 
Deshalb habe ich schon etwas herumprobiert.

Gemeinhin ist bekannt, dass das in C++ immer schon möglich war, aber 
irre aufwändig und unübersichtlich: Sinuswerte hätte man ausschließlich 
mit rekursiven Templates reihenentwickeln müssen. Stichwort: 
Templatemetaprogrammierung (TMP). Vermutlich wäre es genauso leserlich 
wie in der Programmiersprache Brainfuck (BF).

Erst mit C++14 ist das Erzeugen einer Tabelle per Compiler halbwegs 
übersichtlich und nachvollziehbar, durch die Einführung von constexpr 
(C++11) und dem Wegfall der Beschränkung von constexpr-Funktionen auf 
eine einzige return-Anweisung (C++14).

So kann man mit avr-gcc -std=c++14 (ab Version 5.xx) eine Sinustabelle 
direkt vom Compiler erstellen lassen:
1
#include <avr/pgmspace.h>
2
3
constexpr float PI=__builtin_atan(1)*4;
4
5
template<class T,size_t N,int max>struct sintab_t{
6
  T a[N] {};
7
  constexpr sintab_t() {
8
    for (size_t x=0; x<N; x++)
9
     a[x]=__builtin_round(__builtin_sin(x*2*PI/N)*max);
10
  }
11
  char operator[](size_t i) const {
12
    return pgm_read_byte(a+i);
13
  }
14
};
15
16
constexpr PROGMEM sintab_t<char,256,120> sintab;
Dieser parametrierbare Sinustabellen-Generator generiert eine ganze 
Periode mit 256 Stützstellen vom Typ "char" und dem Wertebereich ±120. 
Nichts muss der arme AVR machen. Die __builtin_-Funktionen ersparen 
#include <math.h>.
Der operator[] macht den Tabellenzugriff erst möglich und erledigt das 
mit LPM; dazu MUSS die Objekt-Instantiierung via PROGMEM gehen!

Den Konstruktor kann man mit entsprechend angepassten Kode füttern, um 
beliebige Kurven zu generieren, wie folgt einen Viertelsinus. Dabei kann 
man die Schablone (Template) und die Spezialisierung weglassen, es wird 
allerdings nur wenig kürzer.
1
#include <avr/pgmspace.h>
2
3
constexpr float PI=__builtin_atan(1)*4;
4
typedef unsigned char byte;
5
6
struct sintab_t{  // Viertelsinus mit 255 als Maximum
7
  byte a[256] {};
8
  constexpr sintab_t() {
9
    for (size_t x=0; x<256; x++)
10
     a[x]=__builtin_round(__builtin_sin(x*PI/512)*255);
11
  }
12
  byte operator[](size_t i) const {
13
    return pgm_read_byte(a+i);
14
  }
15
};
16
17
constexpr PROGMEM sintab_t sintab;

Noch einfacher geht es erst mal nicht, auch nicht mit C++20.
Hinweise:
* Die PI-Berechnung findet nur zur Compilezeit statt.
* float als Maximalwert ist für Templates nicht erlaubt.
* Das innere Array (hier: a) MUSS mit einer (hier leeren) 
Initialisierungsliste ({}) initialisiert werden, sonst Syntaxfehler. 
Erlaubt ist hier auch ein Kopierkonstruktor (={}).
* Man könnte statt struct auch class schreiben, um das innere Array a zu 
verstecken. Dann muss danach public: stehen, denn Konstruktor und 
Arrayzugriff muss öffentlich sein.
* Der Konstruktor MUSS constexpr sein und MUSS parameterlos sein. Daher 
Parametrierung ausschließlich via Template möglich. Für das Array ist's 
logisch, aber der Maximalwert wäre ein guter (float-)Kandidat gewesen. 
Kommt vielleicht mit C++23.
* Die einfachste Methode, das innere Array nach außen zugänglich zu 
machen ist ein Typecast-Operator-Overload der Gestalt "operator const 
byte*() const {return a;}", alles andere (bspw. Indizierung) erledigt 
dann der Compiler. Mit dem hier implementierten 
Array-Lesezugriffsoperator versperrt man dem Compiler andere 
Zugriffsmöglichkeiten und zwingt ihn zum Assemblerbefehl LPM = Load 
Program Memory.
* In sintab_t kann man nach Belieben weitere Memberfunktionen 
hineinstecken, etwa eine die mittels Quadrantenbeziehungen aus dem 
Viertelsinus einen ganzen Sinus macht. Die Wirkung für das C++-Programm 
entspricht dann einem Namespace.
1
  ...
2
  int sin(int arg) const{  // Periode (2*PI) = 1024
3
   int w=arg;
4
   if (w&0x100) { // Zweierkomplement ist der Freund der Bitmanipulation
5
    if (w&0x1FF==0x100) ++w; // Überlaufeffekt beachten
6
    w=512-w;    // zweiter oder vierter Quadrant: Laufrichtung ändern
7
   }
8
   int a=operator[](w&255);
9
   if (w&0x200) a=-a;       // dritter oder vierter Quadrant: Vorzeichen ändern
10
   return a;  // richtig für alle <arg>, sogar negative!
11
  }
12
  ...

von hard werker (Gast)


Lesenswert?

Henrik H. schrieb:
> Hinweise:

Also ich schätze mal genau so wird es der gemeine Arduino-
Nutzer machen. Und der gemeine AVR-Benutzer (ohne Arduino)
wird sich auch die Finger danach schlecken.

von c-hater (Gast)


Lesenswert?

hard werker schrieb:

> Also ich schätze mal genau so wird es der gemeine Arduino-
> Nutzer machen.

Bestimmt nicht. Der kann typischerweise kein C++ und auch kein C. Nur 
notdürftig kleine Bruchteile davon und auch die meist nicht richtig.

> Und der gemeine AVR-Benutzer (ohne Arduino)
> wird sich auch die Finger danach schlecken.

Bestimmt ebenfalls nicht. Der weiss nämlich, dass die reine Tabelle 
höchstens die halbe Miete ist. Für wirklich effizenten Zugriff muss sie 
auch noch "well-aligned" im Flash liegen und der Zugriff muss diesen 
Sachverhalt dann auch noch gnadenlos nutzen.

Ich würde mal so sagen: Erst wenn mir so ein C++-Hipster diese Sache:

Beitrag "Re: Westminster Soundgenerator mit ATtiny85"

in C++ ohne jeden Asm-Einschub lauffähig liefert (natürlich auf der 
gegebenen Hardware), dann hat der Compiler wirklich eine gewisse Reife 
erreicht...

Dann, und erst dann, würde C++ auf einem AVR8 für mich brauchbar 
erscheinen.

Beitrag #6874322 wurde von einem Moderator gelöscht.
Beitrag #6874323 wurde von einem Moderator gelöscht.
von W.S. (Gast)


Lesenswert?

Markus schrieb:
> Soweit ich DDS verstanden habe

Das mit einer Tabelle für eine ganze Periode ist nur ein einziger Weg 
von vielen Wegen. Man kann auch per CORDIC aus einer Winkelangabe (und 
dem Einheitsvektor) einen Sinus oder Cosinus berechnen oder auch nur 
eine Tabelle für den ersten Quadranten machen und die 
Symmetrieeigenschaften der Winkelfunktionen benutzen, um auf die Werte 
in den anderen drei Quadranten zu kommen.

Grundsätzlich mußt du in deinem verfügbaren Wertevorrat die beiden 
Extremwerte vorhalten, also z.B. 0 entspricht -1 und 255 entspricht +1. 
Dann denkst du dir eine Tabelle aus, die so etwa 3 mal so lang ist wie 
'hoch', weil der Vollkreis ja von 0 bis 2 Pi geht und der Sinus von -1 
bis +1 reicht, also eine Spanne von 2.0 überstreicht. Wenn du die 
Tabelle kürzer machst, dann wird dein DDS schlechter, weil dann die 
Fehler durch die zeitliche Rasterung früher so groß werden wie die 
Fehler durch die Rasterung der Amplitude. Zumeist ist es einfacher, die 
Tabelle viermal so lang wie 'breit' zu machen. Also für einen 
Wertevorrat von 256 (also 8 Bit) dann eine Tabellenlänge von 1K (also 
1024 Werte). Das ist aus Gründen der Behandelbarkeit einfacher, aber 
dafür umfänglicher ohne daß es an Präzision einen Gewinn gibt.

Das Berechnen so einer Tabelle ist hingegen eher einfach, hier mal aus 
dem Stegreif (alles außer i als float oder double):
 for i:= 0 to 1023 do
 begin
   Winkel:= i;
   Winkel:= Winkel * PI / 512.0;
   Value[i]:= round(128.0 + sin(Winkel) * 127.0);
 end
(hoffentlich hab ich hier keinen Flüchtigkeitsfehler drin)

W.S.

von W.S. (Gast)


Lesenswert?

Sorry, hatte das Datum nicht gesehen...

von kein pfannkuchen (Gast)


Lesenswert?

> Zugriff

Immer wieder lustik: in eine Berliner Kneipe gehn und
dann einmal laut "Zugriff" rufen.

Har, har har....

von kein pfannkuchen (Gast)


Lesenswert?

> Sorry, hatte das Datum nicht gesehen...

Das macht doch nichts. Die anderen koennen doch auch nicht lesen.

von Marek N. (Gast)


Lesenswert?

Eigentlich reicht es, den Bereich von 0 bis 90° zu tabellieren.

von kein pfannkuchen (Gast)


Lesenswert?

> Eigentlich reicht es, den Bereich von 0 bis 90° zu tabellieren.

Eigentlich sogar nur von 0 bis (90° - STEP).
Allerdings verteuert das die einfache Ausgabemimik so,
dass leistungsschwache AVR dann ueberfordert werden.

von c-hater (Gast)


Lesenswert?

W.S. schrieb:

> Sorry, hatte das Datum nicht gesehen...

Nicht schlimm. Der eigentliche Verursacher war Henrik H. (Firma: TU 
Chemnitz) (heha). Der kann nicht nur kein Datum lesen, sondern ist auch 
noch jemand, der glaubt, mit den neuesten C++-Errungenschaften irgendwas 
gebacken zu bekommen, was sonst keiner kann...

Was klar doppelt falsch ist: Erstens kann jeder alles, was C++ kann, 
natürlich auch ohne C++. Das liegt in der Natur der Sache.

Und weitens kann man es schneller als es C++ selbst in der "bleeding 
edge"-Inkarnation kann. Ganz sicher und jederzeit problemlos beweisbar 
jedenfalls für AVR8 als Target.

von Wolfgang (Gast)


Lesenswert?

W.S. schrieb:
> Das Berechnen so einer Tabelle ist hingegen eher einfach, hier mal aus
> dem Stegreif (alles außer i als float oder double):
>  for i:= 0 to 1023 do
>  begin
>    Winkel:= i;
>    Winkel:= Winkel * PI / 512.0;
>    Value[i]:= round(128.0 + sin(Winkel) * 127.0);
>  end

Du willst jetzt nicht ernsthaft 1024 mal die sin()-Funktion aufrufen, 
oder?
Wenn dein Rechner sich nicht sowieso grenzenlos langweilen würde, käme 
man nie auf die Idee, das so zu implementieren.
Die Additionstheoreme für Winkelfunktionen sollten einem irgendwie mal 
in der 10. Klasse begegnet sein. Ein Aufruf von sin() und einer von 
cos() reicht völlig aus, um die Tabelle durch rekursive Berechnung zu 
füllen - nur Multiplikation und Addition.

von kein pfannkuchen (Gast)


Lesenswert?

> Du willst jetzt nicht ernsthaft 1024 mal die sin()-Funktion aufrufen,
> oder?

Du kennst den Unterschied zwischen Compiletime und Runtime.

[ ]

von Wolfgang (Gast)


Lesenswert?

kein pfannkuchen schrieb:
> Du kennst den Unterschied zwischen Compiletime und Runtime.

Egal - unsinniger Einsatz von Resourcen.

Was wird hier für ein Zinnober um eine Sinustabelle gemacht?
Als ob die Welt über 15 Jahre auf diesen Programmiererguss gewartet hat.

von kein pfannkuchen (Gast)


Lesenswert?

> Egal - unsinniger Einsatz von Resourcen.

Ein 8087 braucht fuer einen Sinus ca. 80 us.
Vermutlich geht es heute sogar noch etwas schneller.

> Was wird hier für ein Zinnober um eine Sinustabelle gemacht?

Geh halt wieder in deine Huette in deinem Bergdorf
und belaestige nicht den Rest der Welt mit deiner Ahnungslosigkeit.

von W.S. (Gast)


Lesenswert?

Der heutzutage sinnvollste Weg, zu einem funktionalen DDS zu kommen, ist 
das Einkaufen eines passenden DDS-Chips von AD. Die kleinsten davon sind 
allemal weitaus besser, als man es mit einem gewöhnlichen µC hinkriegen 
könnte und sie sind auch nicht teuer. Mal dran denken, daß man sowas wie 
einen AD9833 mit Quarz und Leiterplatte für so etwa 5 Euro kriegt und 
dabei ein DDS mit 10 Bit Amplitudenauflösung und 25 MHz Takt bekommt.

Da sind alle Überlegungen, wie man sowas in einem 8 bittigen µC 
hinbekommen würde, nur noch Theoretisieren.

W.S.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.