Hallo,
ich soll ein 8052-Programm vom Keil C51-Compiler auf den SDCC portieren,
mit dem ich bisher leider noch nicht gearbeitet hab.
Als MCU kommt ein SiLabs Controller zum Einsatz, Single-Cycle-Core,
24.5MHz.
Der Controller implementiert eine 10-Kanal Software-PWM mit 8-Bit
Auflösung, 100Hz, gesteuert wird per Kommandos über UART.
Folgende Änderungen an der Software habe ich für die Portierung
vorgenommen:
- Anpassungen der Speichertypen (also z.B. 'data' -> '__data')
- der Keil C51 unterstützt im Gegensatz zum SDCC unions als
Funktionsparameter und Rückgabewert -> passend geändert (die unions sind
für den einfachen Zugriff auf High/Low-Byte bei 'unsigned int' gedacht)
- Interrupt-Prototypen in der main-Funktion (lt. Handbuch SDCC nötig)
- lokale Variablen in Interrupts 'vorsichtshalber' als 'static'
deklariert (ist beim Keil C51 nicht nötig)
Das Erstellen lief ohne Fehler durch. Soweit, so gut...
Jedoch ist es nun so, dass der Timer-Interrupt für die PWM wesentlich
länger braucht, und zwar so lange dass der nächste Timer-Interrupt
bereits noch während der Abarbeitung des aktuellen auftritt.
Mir ist klar, dass der SDCC nicht so gut optimiert wie der Keil
Compiler, aber ich frage mich nun, ob bzw. wo ich da noch tunen kann.
Liegt es eher an der Art, wie man programmiert (z.B. verschachtelte
if-Abfragen) oder sollte ich prinzipiell eine andere Register-Bank für
den Interrupt verwenden, oder oder oder...
Mir fehlt jetzt einfach grad der richtige Ansatzpunkt, wo ich am besten
versuchen kann, das Programm auch sauber mit SDCC ans Laufen zu bringen,
wenn möglich eben so, dass die Sourcen auf beiden Compilern
funktionieren.
Wie optimiert man für den SDCC bzw. welche "Konstrukte" sollte man
vermeiden?
Ralf
Immer nach dem Motto "KISS" (keep it small and simple).
Also keine ausgebufften Konstrukte, möglichst keine Pointeroperationen
(die kommen bei so einem kleinen 8 Bitter nicht gut) und drauf achten,
daß man keine Überläufe programmiert, das geht entweder schief oder
bläht den Code auf.
Ansonsten: Guck dir mal den jeweiligen Assemblercode an.
W.S.
SDCC kann mit C51 Keil weder in Codegröße noch in Geschwindigkeit
mithalten. Eventuell musst Du das ganze Interruptkonzept und die
Wiederholraten der Timer neu durchdenken. Alternativ, sofern Dein 8051
das mitmacht, die Taktfrequenz erhöhen.
Keil braucht fast kein 'volatile'. Der Compiler erkennt fast immer, wenn
eine Variable im main und Interrupt verwendet wird. SDCC kann das nicht,
was häufig zu Fehlverhalten im Programm führt.
Der Keil Compiler ist zweifelsfrei die bessere Wahl. Ich habe schon
recht große Programme auf den SDCC portiert und eine Codegrößendifferenz
von max 10% dabei beobachtet. Ich glaube kaum das dein Problem damit zu
tun hat daher wäre es besser den Code herzuzeigen. Dein Programm sollte
auch auf dem SDCC schnell genug laufen. Ohne deinen Code zu kennen kann
man aber kaum helfen.
Hallo,
danke für eure Antworten.
@W.S.:
> Immer nach dem Motto "KISS" (keep it small and simple).
Eigentlich dachte ich, dass meine Implementierung KISS ist :)
> Also keine ausgebufften Konstrukte, ...
Hmmm... ich mache einige Zugriffe auf structs im Interrupt, weil ich die
jeweiligen PWM-Kanäle mit "Eigenschaften" wie 'Enable', 'Polarity', etc.
ausgestattet hab, dafür hab ich dann halt structs verwendet, um es
"zusammen" zu halten.
> ... möglichst keine Pointeroperationen (die kommen bei so einem kleinen> 8 Bitter nicht gut)...
Guter Hinweis. Ein Teil der structs liegt im idata-Bereich, also geht
der Zugriff nur über Pointer, aber generell würde bei einem struct ja
sowieso über Pointer zugegriffen, selbst wenn die structs im
data-Bereich liegen würden, oder?
> und drauf achten, daß man keine Überläufe programmiert, das geht> entweder schief oder bläht den Code auf.
Was genau meinst du mit Überläufen?
> Ansonsten: Guck dir mal den jeweiligen Assemblercode an.
Ja, n Blick hab ich schon riskiert, aber ich muss erstmal durchsteigen
wie der SDCC was löst. Die ISR ist jedenfalls knapp 50% größer als die
vom Keil. Ich vermute das Problem ist auch nicht direkt die Schleife in
der ich die PWM-Kanäle durchlaufe, sondern der Inhalt, wobei das
eigentlich nur if-Abfragen sind, in denen in Abhängigkeit der
Einstellungen für die Kanäle eben das entsprechende Bit gesetzt wird.
@Matthias K.:
> SDCC kann mit C51 Keil weder in Codegröße noch in Geschwindigkeit> mithalten. Eventuell musst Du das ganze Interruptkonzept und die> Wiederholraten der Timer neu durchdenken. Alternativ, sofern Dein 8051> das mitmacht, die Taktfrequenz erhöhen.
Ich benutze den internen Oszillator mit 24.5MHz, mehr kann der
Controller eh nicht und für einen externen wären auch keine Pins frei.
Ich denke, ich werde tatsächlich versuchen, das Konzept zu überarbeiten.
Der Timer läuft mit etwa 25,6kHz (100Hz * 8-Bit Auflösung), im IRQ wird
der PWM-Zähler mit den Werten der Kanäle verglichen und entsprechend das
Ausgangsbit gesetzt. "Mehr" ist das eigentlich nicht, also fraglich ob
ich da was tun kann. Ich kann nur versuchen, das gleiche auf eine andere
Art zu implementieren.
> Keil braucht fast kein 'volatile'. Der Compiler erkennt fast immer, wenn> eine Variable im main und Interrupt verwendet wird. SDCC kann das nicht,> was häufig zu Fehlverhalten im Programm führt.
Deswegen hab ich schon vorab bei Variablen im IRQ oder gemeinsam
genutzten Variablen 'static' oder 'volatile' gesetzt.
@Bernd N:
> Der Keil Compiler ist zweifelsfrei die bessere Wahl.
Sehe ich auch so :) Der Wunsch war eben Sourcen zu haben, die auf beiden
laufen, ohne große Abweichungen zu haben.
> Ich habe schon recht große Programme auf den SDCC portiert und eine> Codegrößendifferenz von max 10% dabei beobachtet. Ich glaube kaum das> dein Problem damit zu tun hat daher wäre es besser den Code herzuzeigen.
Ich denke auch nicht, dass es die Größe an sich ist, nur eben die
Optimierung bzw. die Laufzeit der Schleife.
> Dein Programm sollte auch auf dem SDCC schnell genug laufen. Ohne deinen> Code zu kennen kann man aber kaum helfen.
Ich werd den Code posten, bin nur grad nicht am Entwicklungsrechner.
Ralf
Ralf schrieb:>> Also keine ausgebufften Konstrukte, ...> Hmmm... ich mache einige Zugriffe auf structs im Interrupt, weil ich die> jeweiligen PWM-Kanäle mit "Eigenschaften" wie 'Enable', 'Polarity', etc.> ausgestattet hab, dafür hab ich dann halt structs verwendet, um es> "zusammen" zu halten.
Ist halt die Frage wie Du das konkret implementiert hast. Wenn Du z.B.
für Enable- und Polarity- Bits jeweils eine if-Abfrage machst, dann
bekommst Du natürlich auch ordentlich Overhead der sich vermeiden lässt.
Angenommen Du fasst diese Kontrollbits für die 8 Kanäle jeweils in einem
Byte zusammen, also 1 Byte für die Enable-Bits, eines für die Polarity.
Dann hast Du ein Byte für die 8 PWM Ausgangskanäle. Zuerst setzt Du die
Bits in dem Ausgabebyte abhängig vom PWM-Wert, ohne irgendwelche extra
Tests. Anschließend machst Du ein XOR dieses Bytes mit dem
Polarity-Byte. Somit hast Du dann ganz einfach die Polarität gesetzt.
Dann machst Du ein AND mit dem Enable-Byte.
So kann man das ganz einfach und effizient lösen ohne da irgendwelche
if-Abragen zu benutzen. Je nachdem wie dein Port-Zuweisungen reicht es
dann auch wenn das fertigy Byte ganz einfach zum Port geschickt wird.
Sollte das auf mehrere Ports verteilt sein dann musst das halt wieder
aufsplitten.
Grüße,
Chris
@Christian Klippel:
> Ist halt die Frage wie Du das konkret implementiert hast. Wenn Du z.B.> für Enable- und Polarity- Bits jeweils eine if-Abfrage machst, dann> bekommst Du natürlich auch ordentlich Overhead der sich vermeiden lässt.
Genauso ist es grad, jedes PWM-Kanal-struct wird geprüft, ob es
freigeschaltet ist, dann der PWM-Wert, dann die Polarität und in
Abhängigkeit davon das Bitmuster setzen, welches an die Ports soll. Nach
dem Schleifendurchlauf wird das Bitmuster auf die Ports verteilt.
> Angenommen ...
Ich hab aber 10 Kanäle :) Ein Byte reicht also nicht. Trotzdem gefällt
mir die Idee, ich könnte versuchen, in der Schleife anstatt der
if-Abfragen tatsächlich die jeweiligen Enable/Polarity-Bytes der
einzelnen Kanäle logisch zu verknüpfen und schauen, ob das beim SDCC
effizienter ist.
> Sollte das auf mehrere Ports verteilt sein dann musst das halt wieder> aufsplitten.
Ja, das muss ich leider, da wird ein bisschen maskiert, geschoben, etc.
Da komm ich nicht drumrum.
Ich werd das bei nächster Gelegenheit mal ausprobieren, danke.
Ralf
Ralf schrieb:> Ich hab aber 10 Kanäle :) Ein Byte reicht also nicht. Trotzdem gefällt> mir die Idee, ich könnte versuchen, in der Schleife anstatt der> if-Abfragen tatsächlich die jeweiligen Enable/Polarity-Bytes der> einzelnen Kanäle logisch zu verknüpfen und schauen, ob das beim SDCC> effizienter ist.>>> Sollte das auf mehrere Ports verteilt sein dann musst das halt wieder>> aufsplitten.> Ja, das muss ich leider, da wird ein bisschen maskiert, geschoben, etc.> Da komm ich nicht drumrum.>> Ich werd das bei nächster Gelegenheit mal ausprobieren, danke.>> Ralf
Naja, dann nimmst Du halt jeweils zwei Bytes, ist ja nicht so
dramatisch. Falls es der Speicher hergibt kannst Du auch mehrere nehmen
und die Bits dort gleich passend zu den Ports verwenden. Am Ende machst
dann eine Oder-Verknüpfung mit dem, was sonst noch so an den jeweiligen
Ports ausgegeben werden soll. Ein paar logische Operatoren sind in der
Regel immer effizienter als irgendwelche verschachtelten if-Abfragen.
Ist halt immer die Sache. Man kann viel optimieren wenn man bereit ist
dafür etwas Speicher zu opfern. Beispielsweise, wenn die PWM ausgänge
auf zwei POrts verteilt sind, und Du das Ausgangs-Latch des jeweiligen
Port's lesen kannst, könnte das als Pseudocode so aussehen:
1
unsigned char pwmbits_1, pwmbits_2;
2
unsigned char pwmenable_1, pwmenable_2;
3
unsigned char pwmpol_1, pwmpol_2;
4
usingned char pwm_1, pwm_2, pwm_3, ....
5
unsigned char pwm_count;
6
unsigned char pwm_temp;
7
8
interrupt()
9
{
10
//
11
pwmbits_1 = 0x00;
12
pwmbits_2 = 0x00;
13
14
if(pwm_1 > pwm_count)
15
{
16
pwmbits_1 |= 0x01;
17
}
18
19
if(pwm_2 > pwm_count)
20
{
21
pwmbits_1 |= 0x04;
22
}
23
24
if(pwm_3 > pwm_count)
25
{
26
pwmbits_1 |= 0x20;
27
}
28
29
....
30
31
if(pwm_10 > pwm_count)
32
{
33
pwmbits_2 |= 0x02;
34
}
35
36
pwm_count++;
37
38
// polarität anpassen
39
pwmbits_1 ^= pwmpol_1;
40
41
// nur enabelte PWM bits nehmen
42
pwmbits_1 &= pwmenable_1;
43
44
45
pwmbits_2 ^= pwmpol_2;
46
pwmbits_2 &= pwmenable_2;
47
48
// aktuellen status eines ports lesem
49
pwm_temp = PORTA_LATCH;
50
51
// alle alten PWM bits ausmaskieren
52
pwm_temp &= ~pwmenable_1;
53
54
// aktuelle PWM bits einfügen
55
pwm_temp |= pwmbits_1;
56
57
// port aktualisieren
58
PORTA_LATCH = pwm_temp;
59
60
pwm_temp = PORTB_LATCH;
61
pwm_temp &= ~pwmenable_2;
62
pwm_temp |= pwmbits_2;
63
PORTB_LATCH = pwm_temp;
64
}
Hier habe ich die polarität halt vor dem Enable-Maskieren gemacht, damit
wäre jeder nicht benutzte Kanal immer auf logisch 0, unabhängig der
Polarität. Sollte die Polarität auch für ausgeschaltete PWM Kanäle
gelten dann muss die Reihenfolge der beiden Anweisungen halt vertauscht
werden.
Wenn die PWM auf mehr als zwei Ports verteilt ist dann halt entsprechend
mehr Bytes verwenden. Man opfert hier also etwas Speicher um sich dann
aber eine Menge Code zu sparen.
Keine Ahnung ob dein µC es erlaubt den aktuellen Ausgabezustand eines
Ports zu lesen, ich habe das einfach mal angenommen. Falls nicht muss
man halt eine andere Lösung dafür finden. Entweder man nimmt weitere
Bytes und setzt dort die Bits entsprechend dem was man sonst im Code an
Portbits setzt, oder man beisst in den sauren Apfel und testet halt die
einzelnen Bits der fertigen PWM Bytes und setzt/löscht dann die Portbits
entsprechend einzeln.
Grüße,
Chris
Achja,
und falls auf einem 8-Bit Port alles für PWM verwendet wird dann kann
man sich das vorherige auslesen, maskieren, verknüpfen und
zurückschreiben natürlich sparen und gibt stattdessen direkt das
PWM-Byte auf dem Port aus.
Grüße,
Chris
@Chris:
Besten Dank! Ich werd das mal so umsetzen und Feedback geben, ob's
funktioniert. Bin echt gespannt. Jedenfalls wär's interessant zu lernen,
wo bzw. wie man den SDCC "manuell" optimieren kann :)
Ralf
Ralf schrieb:> @Chris:> Besten Dank! Ich werd das mal so umsetzen und Feedback geben, ob's> funktioniert. Bin echt gespannt. Jedenfalls wär's interessant zu lernen,> wo bzw. wie man den SDCC "manuell" optimieren kann :)>> Ralf
Es lohnt sich auf jeden Fall immer sich mit dem Assembler-Code, den der
jeweilige Compiler produziert, auseinanderzusetzen. Das gilt ganz
besonders für "Universalcompiler" die mehr als eine Architektur bedienen
können. Nur zu oft kommt es da nämlich vor das viele Dinge einfach zu
Allgemein gehalten sind, obwohl der jeweilige µC optimiertere Methoden
anbietet.
Auch haben Compiler manchmal Eigenheiten die einem die Bedienung zwar
erleichtern, aber den Code dafür aufblähen. Genau da gibt es für den
SDCC auch ein schöne Beispiel in Bezug auf die PIC's. Diese haben ja
getrennten RAM und Flash Speicher. Bei Microchip's C18 ist es dann so
das man angeben muss aus welchem Part was kommt, damit er den richtigen
Code erzeugt. Wenn man z.B. eine Textausgabe-Routine hat, muss man die
doppelt anlegen: einmal für Strings aus dem RAM und einmal für Strings
aus dem Flash.
Bei'm SDCC hingegen ist das nicht nötig. Sieht natürlich erstmal wie ein
Vorteil aus, was es ja eigentlich auch ist. Aber wenn man sich den
generierten Assembler-Code ansieht wird schnell klar wo das Problem
dabei ist: Es wird ganz einfach für beide Speicher-Versionen generiert
und aufgrund eines Bits im übergebenen Pointer dann entschieden ob der
Teil für RAM- oder Flash-Zugriff ausgeführt werden soll. Nachher wundert
man sich dann warum der Code so groß wird...
Da haben spezialisierte Compiler natürlich oft einen Vorteil. Die müssen
halt genau eine Architektur ansprechen, und "wissen" daher meistens wie
gewisse Probleme am effizientesten gelöst werden können.
Grüße,
Chris
@Chris:
So, habe nun diverse Versuche gemacht. Den Ansatz der logischen
Verknüpfung habe ich wieder verwerfen müssen, das hat's z.T. noch
schlimmer gemacht.
Allerdings hab ich zu struct-Arrays eine Aussage von einem
SDCC-Entwickler bekommen, dass das nicht optimal über den Index
angesprochen wird, ich sollte besser einen Pointer auf das Array
verwenden. Zusammen mit der Angabe des passenden Speichertyps, auf den
der Pointer zeigt, hab ich jetzt eine Version, die in einem Schnelltest
wesentlich besser reagiert hat.
Ralf