Forum: Mikrocontroller und Digitale Elektronik Schnelle Arithmetik


von Michael (Gast)


Lesenswert?

Hallo,

in meiner Sinus-Motoransteuerung muss ich den PWM Tastgrad laufend 
ändern. Meine Berechnung ist aber zu langsam, als dass ich schnelle 
Drehzahlen hin bekomme. Mein XMEGA16E5 läuft schon mit 34MHz...

Hat jemand eine Idee, wie ich die Berechnung schneller durchführen 
könnte?

Die variable pwm_output hat werte zwischen 0-1000 (entspricht 0-100%, 
also Promille)

In der Sinustabelle sinTable liegen 360 Werte zwischen 0 und 8000. In 
das PWM Register TCC4_CCABUF müssen dann Werte zwischen 0 und 800. Für 
0% bis 100%.

Im Moment ists wie unten gelöst. Dauert aber sehr lange (rund 32µs). 
Jede Division braucht ca. 6µs.

Wie könnte man das schneller machen?

Gruß,
Michael


1
void sinus_pwm_update(u16 pwm_output)
2
  {
3
    led_red_on();
4
        u16 motor_pwm_divider_factor_u16 = (10000/pwm_output);
5
        u16 calculatet_x_pwm_value_u16;
6
        
7
        
8
        calculatet_x_pwm_value_u16 = (sineTable[(phase_u_pwm_sinus_location_u16_v)])/motor_pwm_divider_factor_u16;
9
        if (calculatet_x_pwm_value_u16<MINIMUM_SINUS_PWM)
10
        {
11
          calculatet_x_pwm_value_u16=MINIMUM_SINUS_PWM;
12
        }
13
        TCC4_CCABUF = calculatet_x_pwm_value_u16;
14
        
15
16
        calculatet_x_pwm_value_u16 = (sineTable[(phase_v_pwm_sinus_location_u16_v)])/motor_pwm_divider_factor_u16;
17
        
18
        if (calculatet_x_pwm_value_u16<MINIMUM_SINUS_PWM)
19
        {
20
          calculatet_x_pwm_value_u16=MINIMUM_SINUS_PWM;
21
        }
22
        TCC4_CCBBUF = calculatet_x_pwm_value_u16;
23
        
24
        
25
26
        calculatet_x_pwm_value_u16 = (sineTable[(phase_w_pwm_sinus_location_u16_v)])/motor_pwm_divider_factor_u16;
27
        
28
        if (calculatet_x_pwm_value_u16<MINIMUM_SINUS_PWM)
29
        {
30
          calculatet_x_pwm_value_u16=MINIMUM_SINUS_PWM;
31
        }
32
        TCC4_CCCBUF = calculatet_x_pwm_value_u16;
33
    
34
    led_red_off();
35
  }

von isidor (Gast)


Lesenswert?

Michael schrieb:
> Wie könnte man das schneller machen?

Die Division in Assembler schreiben und aufrollen (ohne
Schleife laufen lassen). Dabei fällt vielleicht noch etwas
Optimierung ab wenn ein paar Register nicht gesichert/wieder-
hergestllt werden müssen.

Oder einen 32 Bit ARM nehmen, da bist du solche Sorgen los.

von Karl H. (kbuchegg)


Lesenswert?

Das sich deine Drehzahl ja wohl nicht von einer µs auf die andere 
ändert:
Was spricht dagegen, sich eine für die jeweilige Drehzahl exakt passende 
Tabelle zurechtzulegen.

Wird die Drehzahl geändert, wird einmalig eine neue passende Tabelle 
zurecht gelegt. Die ganze Rechnerei für die TCCRxxx Register fällt dann 
weg.


Fasst man deine beiden Formeln zu einer zusammen, dann ergibt sich:
1
  value = sine_value * pwm_output / 1000;

schon mal 1 Division weniger, zugunsten einer Multiplikation (welche 
meist schneller ist).
Die DIvisionn durch 1000 lässt sich auch noch vereinfachen, wenn du 
deine Promille aufgibst und sie durch eine 2-er Potenz ersetzt.

Was für uns Zahlen im 10-er System sind, sind für einen Computer Zahlen 
zur Basis 2: sie ermöglichen so manche Abkürzung im Rechenweg.
'Dividierst' du im 10-er System 5678 durch 100 ohne zu rechnen, sondern 
nur indem du 'das Komma verschiebst' und 56 erhältst, so tut sich auch 
ein Computer ohne Divisionshardware leichter, wenn er durch 2-er 
Potenzen 'dividieren' darf. Dann muss nichts gerechnet werden, sondern 
es wird nur die Bitdarstellung entsprechend verschoben (was effektiv 
einem Verschieben des Binär-Kommas gleich kommt)

: Bearbeitet durch User
von Lothar M. (Firma: Titel) (lkmiller) (Moderator) Benutzerseite


Lesenswert?

Michael schrieb:
> Wie könnte man das schneller machen?
Die Divisionen durch eine Multiplikation ersetzen...
1
        u16 x = (10000/w);
2
               
3
        r = (Table[y])/x;
Ist das selbe wie:
1
        r = (Table[y])*w/10000;
Und wenn jetzt noch die Skalierung in die Tabelle aufgenommen wird 
(10000 fach kleinere Werte in der Tabelle), oder "shiftfreundliche" 
Zahlen verwendet werden (z.B. 8192 oder 16384 statt 10000), dann geht 
das recht knackig...

BTW1: besser könnte man es machen, wenn die ganzen Unterstriche weg 
wären. Die machen m.E. jeden Quelltext zum "Qualtext", weil man immer 
erst mal schauen muss, ob das Wort vorbei ist oder ob noch ein 
Unterstrich kommt:
1
        u16 motor_pwm_divider_factor_u16 = (10000/pwm_output);
2
// vs:
3
        u16 motor_pwm divider_factor u16 = (10000/pwm_output);
4
// vs:
5
        u16 MotorPwmDividerFactor = (10000/PwmOutput);

BTW2:  deutsche Wörter nehmen, wenn englische nicht geläufig sind: 
"calculatet"

EDIT: Hut ab, Karl Heinz war schneller... ;-)

: Bearbeitet durch Moderator
von isidor (Gast)


Lesenswert?

Karl Heinz schrieb:
> Was spricht dagegen, sich eine für die jeweilige Drehzahl exakt passende
> Tabelle zurechtzulegen.

Eventuell der zu kleine Speicher.....

von foobar (Gast)


Lesenswert?

Wie geschon gesagt, besser auf 2-er Potenzen skalieren.  Die Division 
wird dann zu nem simplen shift-right und es gibt gar keine Divison mehr. 
Die 16384 hab ich gewählt, damit die Werte noch in 16 Bit passen.

Und btw, wie du evtl schon gemerkt hast, zu lange Namen (und zu viele 
Klammern) sind kontraproduktiv - in der Kürze liegt die Würze ;-)
1
static u16 sinus_tab[360] = { /* i=0..359: 16384*(1+sin(i*pi/180)) */ };;
2
3
static u16 fp2pwm(u16 fact, u16 phase)
4
{
5
    u16 x;
6
7
    x = fact * sinus_tab[phase] / 16384;
8
    if (x < MINIMUM_SINUS_PWM)
9
        x = MINUMUM_SINUS_PWM;
10
11
    return x;
12
}
13
14
statuc void pwm_update(u16 fact, u16 phase_u, u16 phase_v, u16 phase_w)
15
{
16
    led_red_on();
17
    TCC4_CCABUF = fp2pwm(fact, phase_u);
18
    TCC4_CCBBUF = fp2pwm(fact, phase_v);
19
    TCC4_CCCBUF = fp2pwm(fact, phase_w);
20
    led_red_off();
21
}

von isidor (Gast)


Lesenswert?

foobar schrieb:
> TCC4_CCABUF = fp2pwm(fact, phase_u);
> TCC4_CCBBUF = fp2pwm(fact, phase_v);
> TCC4_CCCBUF = fp2pwm(fact, phase_w);

Schön geschrieben, aber gerade die Funktionsaufrufe vergeuden
wieder wertvolle Zeit mit Springen, Register Retten/Restaurieren
und Parameter-Übergabe (kommt drauf an...)

von Karl H. (kbuchegg)


Lesenswert?

isidor schrieb:

> Schön geschrieben, aber gerade die Funktionsaufrufe vergeuden
> wieder wertvolle Zeit mit Springen, Register Retten/Restaurieren
> und Parameter-Übergabe (kommt drauf an...)


Yep.
Zm Beispiel davon, ob der Compiler die Funktion inlined.
EIn Blick ins Assembler Listing schafft da Klarheit.

von foobar (Gast)


Lesenswert?

> aber gerade die Funktionsaufrufe vergeuden wieder wertvolle Zeit ...

Das sind Mikrooptimierungen, mit denen man sich erst beschäftigt, wenn 
es überhaupt nicht mehr anders geht.  Und selbst dann, schaut man 
erstmal, ob der Compiler das nicht eh inlined hat.  Wenn nicht, macht 
man aus der Funktion eine inline-Funktion.  Und wenn das dann immer noch 
zu langsam ist, dann nimmt man einen schnelleren Proz ;-)

von isidor (Gast)


Lesenswert?

Karl Heinz schrieb:
> ob der Compiler die Funktion inlined.

Wenn man es dem Compiler nicht sagt (das inlinen) wird er es nicht 
machen.

von isidor (Gast)


Lesenswert?

foobar schrieb:
> Das sind Mikrooptimierungen, mit denen man sich erst beschäftigt, wenn
> es überhaupt nicht mehr anders geht.

Je nach Gesamtlage fällt bei Sichern/Restaurieren von Registern
einiges an Arbeit für den Prozessor an .....

von Karl H. (kbuchegg)


Lesenswert?

isidor schrieb:
> Karl Heinz schrieb:
>> ob der Compiler die Funktion inlined.
>
> Wenn man es dem Compiler nicht sagt (das inlinen) wird er es nicht
> machen.

Das würde ich mal als kühne Behauptung auffassen.
Compiler optimieren eine Menge Dinge, wenn man nur den Optimizer 
einschaltet und ohne das man es ihnen sagt. Vor allen Dingen wenn die 
Funktion static ist und sie daher davon ausgehen können, dass ausserhalb 
dieser Translation Unit die Funktion nicht aufgerufen werden kann.
Function Inlining hat es schon gegeben, da hat es das Schlüsselwort noch 
gar nicht gegeben.

: Bearbeitet durch User
von foobar (Gast)


Lesenswert?

> Wenn man es dem Compiler nicht sagt (das inlinen) wird er es nicht
> machen.

Der GCC schon.  Wenn er meint, inlinen ist kürzer/schneller macht er 
das, auch ohne inline Keyword.  Mit Keyword, zwingt man ihn dazu.

von Michael (Gast)


Lesenswert?

Danke für eure Vorschläge.

Mit foobars Vorschlag konnte ich die Rechenzeit halbieren. Modifiziert 
auf einen Teiler /512. Mit 16384 würde die variable schon bei einem fact 
= 2 überlaufen. (sollte ja 0-100% darstellen)

isidor schrieb:
> Oder einen 32 Bit ARM nehmen, da bist du solche Sorgen los.

Darauf wird's wohl hinauslaufen...

von Falk B. (falk)


Lesenswert?

@ Michael (Gast)

>> Oder einen 32 Bit ARM nehmen, da bist du solche Sorgen los.

>Darauf wird's wohl hinauslaufen...

Jaja, und dann kommen die nächsten "Experten" die eine Motorsteuerung in 
Java machen wollen, dann muss es halt ein Quad-Core sein.

Brain 2.0 ist immer noch unschlagbar und sehr zu empfehlen!

von Pandur S. (jetztnicht)


Lesenswert?

Eine weitere Optimierung waere die Sinutabelle auf 128 Phasen Werte zu 
reduzieren. Und die Sinustabelle auf eine Wertbreite von 256. Das waere 
dann ein Wert fuer alle 3 Grad, und eine Amplitudenaufloesung von 1%.

: Bearbeitet durch User
von isidor (Gast)


Lesenswert?

Falk Brunner schrieb:
> Jaja, und dann kommen die nächsten "Experten" die eine Motorsteuerung in
> Java machen wollen, dann muss es halt ein Quad-Core sein.

Wenn der Prozessor an der "Kotzgrenze" mit 34 MHz arbeitet
und die Optimierungen schon grenzwertige Ergebnisse zeigen,
dann muss es wohl auf einen Prozessorwechsel hinauslaufen.

Divisionen/Multiplikationen in wenigen Takten (bei noch höherer
Taktfrequenz) gegenüber einigen hundert Takten in einem ATxMega
sprechen eine deutliche Sprache.

von Karl H. (kbuchegg)


Lesenswert?

Ich würde mich mal fragen, warum es notwendig ist, die Drehzahl alle 
36µs neu einzurechnen. Kein Motor ändert so schnell seine Drehzahl 
nennenswert.

weiters würde mich interessieren, wo eigentlich pwm_output herkommt. 
Auch wenn es nicht erwähnt wird, geh ich davon aus dass das ein 
Vorgabewert ist. Und auch der ändert sich (im Normalfall) nicht im µs 
Takt, wodurch es völlig sinnlos wäre, die Division 1000/pwm_output 
laufend zu machen.

Aber ohne mehr von der Umgebung des Code-SChnipsel zu kennen bzw. zu 
sehen, ist das alles Spekulation. Zeit in einem Programm holt man sich 
nicht durch Mikrooptimierung von einzelnen Anweisungen sondern indem man 
sich das Gesamtkonzept ansieht und überlegt, welche Berechnungen wann 
unbedingt notwendig sind.

: Bearbeitet durch User
von Robin E. (why_me)


Lesenswert?

Versuch anstelle der durch 10.000 zu teilen:
X/1024 * 105 / 1024
Ergibt einen Fehler von 1,4 Promille. Sollte also reichen, als 
Annäherung.

E: Es ist allerdings notwendig mir 24 Bit fest-Komma u rechnen. Also 
16.8 Format.

: Bearbeitet durch User
von Michael K. (Gast)


Lesenswert?

Du brauchst nur eine Sinustabelle von 0-90°, der Rest sind 
Wiederholungen (rückwärts auslesen, Vorzeichen ändern)

Bleib bei 8bit Arithmetik, die Massenträgheit des Motor integriert 
hervorragend.

Für Drehzahländerungen verändere ich nur die Zeitkonstante mit der ich 
die Sinustabelle lese bzw. den PWM setze.
Wo das nicht reicht überspringe ich Werte in der Tabelle.

So habe ich schon mit deutlich schwächeren MCUs DTMF Töne erzeugt.
(zwei überlagerte Sinustöne aus einer Tabelle bis 1.6Khz)

isidor schrieb:
> dann muss es wohl auf einen Prozessorwechsel hinauslaufen.
Ja ?
Man kann auch einfach effizienten Code schreiben.

von isidor (Gast)


Lesenswert?

Karl Heinz schrieb:
> Ich würde mich mal fragen, warum es notwendig ist, die Drehzahl alle
> 36µs neu einzurechnen. Kein Motor ändert so schnell seine Drehzahl
> nennenswert.

Das wurde ja eingangs schon erwähnt. Ich habe mir vorgestellt die
Divisionen im Interrupt-Kontext zu rechnen, sozusagen in low
Priority im Hintergrund. Aber ob soviel Zeit (vermutlich viele
hundert Zyklen) bleibt um nicht den Gesamtablauf in Stottern zu
bringen ist so schwer zu beurteilen. Leider müsste eine Division
komplett im Interrupt-Kontext ausgeführt werden, das ist wohl
nicht noch zusätzlich zeitlich aufteilbar. Die Update-Rate der
Berechnungen könnte dann durch die höhe der Interrupt-Rate
gesteuert werden.

Eine Verbesserung wäre auch die Berechnung in drei Phasen
aufzuteilen, d.h. nur eine Division pro Durchlauf, jedesmal eine
andere. Ob das praktikabel ist hängt natürlich auch von Umständen
ab die ich nicht kenne.

von Pandur S. (jetztnicht)


Lesenswert?

Im Interrupt macht man ganz sicher keine Berechnungen.

: Bearbeitet durch User
von Falk B. (falk)


Lesenswert?

@ isidor (Gast)

>Wenn der Prozessor an der "Kotzgrenze" mit 34 MHz arbeitet
>und die Optimierungen schon grenzwertige Ergebnisse zeigen,
>dann muss es wohl auf einen Prozessorwechsel hinauslaufen.

Nö. Weder das Problem noch die Lösung wurden hinreichend analysiert, 
geschweige denn optimiert.
Ich empfehle weiterhin Brain 2.0. Eine Division in dieser Art ist nicht 
nötig. Ausserdem würde ich prüfen, ob der Compiler schlau genug war, die 
Division in eine deutlich schnellere Schiebeoperation umzuwandeln. Oder 
noch einfacher, die Schiebeoperation DIREKT hinzuschreiben.

>Divisionen/Multiplikationen in wenigen Takten (bei noch höherer
>Taktfrequenz) gegenüber einigen hundert Takten in einem ATxMega
>sprechen eine deutliche Sprache.

Bla. Die AVRs haben einen MUL Befehl, der dauer für 8x8 Bit 2 Takte, für 
16x16 Bit vielleicht ein Dutzend Takte incl. der Addidtionen.

von isidor (Gast)


Lesenswert?

Jetzt Nicht schrieb:
> Im Interrupt macht man ganz sicher keine Berechnungen.

So? Ganz sicher? Ohne Begründung?

von Pandur S. (jetztnicht)


Lesenswert?

Interrupts muessen so schnell wie moeglich sein. Allenfalls eine 
Variable setzen. Mehr nicht.

von Karl H. (kbuchegg)


Lesenswert?

Jetzt Nicht schrieb:
> Interrupts muessen so schnell wie moeglich sein.

Sagen wir mal: so schnell wie notwendig.
Das kann je nach Aufgabenstellung unterschiedlich sein.
Wenn es in die µs geht, dann muss man eben ein wenig 'vorsichtiger' zu 
Werke gehen, als wenn man 'alle Zeit der Welt' hat. In letzterem Fall 
kann man durchaus auch naiv vorgehen und einen Sinus in Zehntelgrad 
Abstufung und mit 8000 Amplitudenabstufungen generieren, auch wenn das 
keiner braucht.

: Bearbeitet durch User
von isidor (Gast)


Lesenswert?

Falk Brunner schrieb:
> Bla. Die AVRs haben einen MUL Befehl, der dauer für 8x8 Bit 2 Takte, für
> 16x16 Bit vielleicht ein Dutzend Takte incl. der Addidtionen.

Ich habe mich auf die Divisionen bezogen um die es bei der
Problemstellung gegangen ist. Und da dauert es deutlich länger.

Siehe __udivmodhi4

von Falk B. (falk)


Lesenswert?

@ isidor (Gast)

>Ich habe mich auf die Divisionen bezogen um die es bei der
>Problemstellung gegangen ist. Und da dauert es deutlich länger.

Schon klar, aber deren Notwendigkeit bezweiflich ich im Moment noch. 
Letztendlich ist das Ganze nur eine Variation des DDS Algorithmus, 
und der braucht auch keine Divisionen. Allein schon die Sinustabelle mit 
360 anstatt eher 256 oder 512 Einträgen ist diskussionswürdig.

von Maxx (Gast)


Lesenswert?

Jetzt Nicht schrieb:
> Interrupts muessen so schnell wie moeglich sein. Allenfalls eine
> Variable setzen. Mehr nicht.

Ich sag meinen Kindern auch immer, dass man nicht an den Herd geht. 
Selber jedoch habe ich kein Problem damit.

Soll heißen: Die Regel "im Interrupt setzt man höchstens eine Variabel" 
kommt daher, dass man schon wissen sollte wie Interrupts funktionieren, 
was an Fragestellungen durch die Nebenläufigkeit und der Priorisierung 
auftaucht und wo die Fallstricke liegen. Das ist bei vielen Einsteigern 
nicht gegeben, genausowenig wie das Kind weiss worauf es aufpassen muss. 
Es als "die Wahrheit" zu verkaufen, dass Herde nicht angefasst werden, 
bzw. ISRs nur Variablen schubsen ist jedoch der Weg in die Tiefkühlkost.

Wenn man aber irgendwann mal lernt, bzw. sich mit dem Thema ausser durch 
Generalisierungen beschäftigt ist auch das Arbeiten in ISRs wie das 
Kochen auf dem Herd: Alltag.

Nicht zuletzt existieren gar HW Unterstzützungen für vielschichtige 
Aufsplittung von ISRs und deren Priorisierungen untereinander, sowie 
wiedereinsprung-sicheren IRQ Quellen (vgl. z.B. Cortex).

von isidor (Gast)


Lesenswert?

Maxx schrieb:
> Es als "die Wahrheit" zu verkaufen, dass Herde nicht angefasst werden,
> bzw. ISRs nur Variablen schubsen ist jedoch der Weg in die Tiefkühlkost.

So ist es.

Wenn man in seiner Hauptschleife im Programm nichts zu tun hat kann
man (wenn man es kann) seine rechenintensiven Aufgaben durchaus
im Interrupt-Kontext abarbeiten.

von Lothar M. (Firma: Titel) (lkmiller) (Moderator) Benutzerseite


Lesenswert?

Falk Brunner schrieb:
> schon die Sinustabelle mit 360 ... Einträgen ist diskussionswürdig.
Ja, der Mensch hat eben 10 Finger und ein Kreis 360 Grad. Davon kommt 
man nicht so leicht weg...

von Falk B. (falk)


Lesenswert?

@ Lothar Miller (lkmiller) (Moderator) Benutzerseite

>> schon die Sinustabelle mit 360 ... Einträgen ist diskussionswürdig.
>Ja, der Mensch hat eben 10 Finger und ein Kreis 360 Grad. Davon kommt
>man nicht so leicht weg...

Gib mir mal ein scharfes Messer . . . 8-)

von Pandur S. (jetztnicht)


Lesenswert?

> ..  und ein Kreis 360 Grad. Davon kommt man nicht so leicht weg...

Fuer Fachleute sind das dann 2 Pi

von Lothar M. (Firma: Titel) (lkmiller) (Moderator) Benutzerseite


Lesenswert?

Jetzt Nicht schrieb:
> Fuer Fachleute sind das dann 2 Pi
Sind das die, die dann gleich mit 64Bit Fließkommazahlen rechnen?

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.