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
Zum Beispiel mit einem Tabellenkalkulationsprogramm, Excel oder OpenOffice-Calc. Damit geht sogar eine direkte Ausgabe als Hex-Zahl.
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
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
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.
> 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
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.
@ hal (Gast) Schon mal aufs Datum des letzten Postings geschaut? MfG Falk
> > Schon mal aufs Datum des letzten Postings geschaut? Scheinbar nicht :D
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
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.
Hi! Was ist das für eine Simulationssoftware, die hier verwendet wurde? Gruss, Tom
Markus schrieb 2006: Was ist das denn für ein seltsamer Thread? Wird der alle 1...2 Jahre einmal geweckt?
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.
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 | ...
|
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.
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.
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.
> Zugriff
Immer wieder lustik: in eine Berliner Kneipe gehn und
dann einmal laut "Zugriff" rufen.
Har, har har....
> Sorry, hatte das Datum nicht gesehen...
Das macht doch nichts. Die anderen koennen doch auch nicht lesen.
Eigentlich reicht es, den Bereich von 0 bis 90° zu tabellieren.
> 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.
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.
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.
> Du willst jetzt nicht ernsthaft 1024 mal die sin()-Funktion aufrufen, > oder? Du kennst den Unterschied zwischen Compiletime und Runtime. [ ]
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.
> 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.
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
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.