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
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.
Hier ein kleines C-Programm wo du Auflösung,Amplitude und Offset
einstellen kannst.
Es liefert gleich einen Integer-Buffer, der mitcompiliert werden kann.
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.
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:
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
constexprfloatPI=__builtin_atan(1)*4;
4
typedefunsignedcharbyte;
5
6
structsintab_t{// Viertelsinus mit 255 als Maximum
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
intsin(intarg)const{// Periode (2*PI) = 1024
3
intw=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
inta=operator[](w&255);
9
if(w&0x200)a=-a;// dritter oder vierter Quadrant: Vorzeichen ändern
10
returna;// richtig für alle <arg>, sogar negative!
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.
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.
> 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.
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.