Forum: Mikrocontroller und Digitale Elektronik Keil C51 auf SDCC portieren -> optimieren


von Ralf (Gast)


Lesenswert?

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

von W.S. (Gast)


Lesenswert?

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.

von Matthias K. (matthiask)


Lesenswert?

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.

von Bernd N (Gast)


Lesenswert?

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.

von Ralf (Gast)


Lesenswert?

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

von Christian K. (Firma: Atelier Klippel) (mamalala)


Lesenswert?

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

von Ralf (Gast)


Lesenswert?

@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

von Christian K. (Firma: Atelier Klippel) (mamalala)


Lesenswert?

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

von Christian K. (Firma: Atelier Klippel) (mamalala)


Lesenswert?

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

von Ralf (Gast)


Lesenswert?

@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

von Christian K. (Firma: Atelier Klippel) (mamalala)


Lesenswert?

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

von Ralf (Gast)


Lesenswert?

@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

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.