Hallo allerseits.
Ich kämpfe gerade mit dem AVR-GCC inline assembler. Ich arbeite an einer
zeitkritischen IRQ-Routine, die fürs Multiplexen einer LED-Matrix
zuständig ist. Dabei wird je nach aktiver Spalte ein Portbit gelöscht.
In C sieht das so aus:
1
/* enable new column */
2
if (col < 4) {
3
PORTD &= ~(1 << (col+4));
4
} else if (col < 0x0a) {
5
PORTB &= ~(1 << (col-4+2));
6
} else {
7
PORTA &= ~(1 << (col-0x0a));
8
}
Wenn ich mir den daraus generierten Assembler angucke und anfange,
Zyklen zu zählen, drängt sich mir der Gedanke auf, dass das schneller
gehen muss. Meine Idee war etwa folgendes:
1
__asm__ __volatile__ ( ""
2
"ldi r31,hi(start%=)" "\n\t" // "Error: garbage at end of line"
3
"ldi r30,lo(start%=)" "\n\t" // "Error: garbage at end of line"
4
"add r30,%[col]" "\n\t"
5
"adc r31,__zero_reg__" "\n\t"
6
"add r30,%[col]" "\n\t"
7
"adc r31,__zero_reg__" "\n\t"
8
"add r30,%[col]" "\n\t"
9
"adc r31,__zero_reg__" "\n\t"
10
"ijmp" "\n"
11
"start%=:" "\n\t"
12
"cbi %[portd], 4" "\n\t"
13
"jmp out%=" "\n\t"
14
"cbi %[portd], 5" "\n\t"
15
"jmp out%=" "\n\t"
16
[...]
17
"cbi %[porta], 6" "\n\t"
18
"jmp out%=" "\n\t"
19
"cbi %[porta], 7" "\n\t"
20
"jmp out%=" "\n"
21
"out%=:" "\n\t"
22
23
:
24
: [col] "d" (col),
25
[porta] "I" (_SFR_IO_ADDR (PORTA)),
26
[portb] "I" (_SFR_IO_ADDR (PORTB)),
27
[portd] "I" (_SFR_IO_ADDR (PORTD))
28
);
Ich will also die Adresse des "start%="-Labels in das Z-Register laden,
dann darauf dreimal col addieren (cbi: 1 Wort, jmp: 2 Worte) und dann
mittels ijmp an die resultierende Adresse springen.
Nur kriege ich es nicht hin, die Adresse des Labels in das Z-Register zu
laden, mir ist die Syntax des Assemblers total unklar. Der Assembler ist
der Meinung, dass bei meinem obigen Versuch "garbage at end of line"
steht.
Wie referenziere ich denn auf diese Adresse?
Danke für die Tipps,
Simon
Schreib doch die ganze Funktion direkt in Assembler, und lass den
inline-Quatsch sein. Inline-Assembler im gcc ist nunmal Krampf, und ist
für mehr als drei zeilen ungeeignet.
Oliver
Das geth auch schneller.
Wenn man realisiert, das eine Schiebeoperation mit einer variablen
Bitanzahl eine teure Sache ist.
Mach dir ein Array, in dem du die Ausgabewerte direkt vorliegen hast,
indiziere mit col und du bist wieder auf full Speed. Ganz ohne
Assembler.
Edit: natürlich 3 Arrays. Für jeden Port eines
Ob es jetzt einfacher ist einfach alle 3 Ports jeweils komplett neu zu
bestücken oder vorher mittels if rauszufinden, welcher Port ein Update
braucht, müsste man sich ansehen. Auch ein if kostet Zeit.
Das was du da gerade mühsam in Assembler zu konstruieren versuchst, ist
nichts anderes als das was der Compiler bei einem switch/case erzeugen
würde. Warum kannst du den nicht in C schreiben, wenn du schon keine
Array Lösung mit vordefinierten auszugebenden Konstanten machen willst?
Alles in allem: erst mal in der Hochsprache das letzte aus dem Code
rausholen. Da geht meistens noch viel. Assembler ist deine letzte
Option, wenn du auch noch den letzten Taktzyklus, auf den der Compiler
nicht geachtet hat, rausholen willst. Und meistens braucht man den auch
gar nicht.
Oliver schrieb:> Inline-Assembler im gcc ist nunmal Krampf, und ist> für mehr als drei zeilen ungeeignet.
Anders: inline-Assembler im GCC ist genial und unwahrscheinlich gut
mit dem Compiler integrierbar, aber das hat seinen Preis. Der Preis
heißt "constraints", denn die beschreiben dem Compiler, wie er
den Assemblercode mit dem Compilat optimal angepasst bekommt.
Aber ich sehe das auch so, entweder die ganze ISR vollständig in
einer Assemblerdatei hinterlegen, oder eben erstmal schauen, was
man im C-Code noch besser schreiben kann.
Im Übrigen heißen die Operatoren nicht "hi" und "lo", sondern "hi8"
und "lo8".
Jörg Wunsch schrieb:> Im Übrigen heißen die Operatoren nicht "hi" und "lo", sondern "hi8"> und "lo8".
Äh, guter Punkt. Ich habe das jetzt so realisiert, und es funktioniert:
1
__asm__ __volatile__ ( "" // 11 cycles:
2
"ldi r30,lo8(pm(start%=))" "\n\t" // 1
3
"ldi r31,hi8(pm(start%=))" "\n\t" // 1
4
"add %[col],%[col]" "\n\t" // 1
5
"add r30,%[col]" "\n\t" // 1
6
"adc r31,__zero_reg__" "\n\t" // 1
7
"ijmp" "\n" // 2
8
"start%=:" "\n\t"
9
"cbi %[portd], 4" "\n\t"
10
"rjmp out%=" "\n\t"
11
"cbi %[portd], 5" "\n\t"
12
"rjmp out%=" "\n\t"
13
[...]
14
"cbi %[porta], 6" "\n\t" // 2
15
"rjmp out%=" "\n\t" // 2
16
"cbi %[porta], 7" "\n"
17
"out%=:" "\n\t"
18
:
19
: [col] "d" (col),
20
[porta] "I" (_SFR_IO_ADDR (PORTA)),
21
[portb] "I" (_SFR_IO_ADDR (PORTB)),
22
[portd] "I" (_SFR_IO_ADDR (PORTD))
23
: "r30", "r31"
24
);
Das sind jetzt 11 Zyklen (bzw. 10 für 18/PA7). Ich habe so meine
Zweifel, dass man das mit einer Lookup-Table schneller hinbekomt. Das C
ist sicherlich noch nicht ausgereizt, aber die switch()-Anweisung die
ich zwischendrin ausprobiert hat, hat irgendwie gruslig viel Kram
erzeugt. Der Compiler erkennt vermutlich nicht, dass alle case-Fälle
gleich viel Code
enthalten und daher Arithmetik mit dem switch-Argument gemacht werden
kann.
> Aber ich sehe das auch so, entweder die ganze ISR vollständig in> einer Assemblerdatei hinterlegen, oder eben erstmal schauen, was> man im C-Code noch besser schreiben kann.
Ich werde mir die anderen Punkte in der ISR definitiv noch angucken und
ggf. nach Assembler portieren. Alles komplett in asm ist definitiv
interessant, schon alleine um die Menge der verwendeten Register zu
minimieren und den push/pop-Overhead zu reduzieren.
Vielen Dank für die Hinweise.
Simon
Karl Heinz Buchegger schrieb:> Das was du da gerade mühsam in Assembler zu konstruieren versuchst, ist> nichts anderes als das was der Compiler bei einem switch/case erzeugen> würde. Warum kannst du den nicht in C schreiben, wenn du schon keine> Array Lösung mit vordefinierten auszugebenden Konstanten machen willst?
Hm, ok. Der Assembler den er jetzt mit einem Switch generiert sieht
schon besser aus, als das was ich da gestern abend gemeint habe zu
lesen. Keine Ahnung, ob das an Leseinkompetenz auf meiner Seite, oder an
unterschiedlichen Compilerflags liegt.
Folgender C-Code
1
switch (col) {
2
case 0: PORTD &= ~(1 << 4) ; break;
3
case 1: PORTD &= ~(1 << 5) ; break;
4
case 2: PORTD &= ~(1 << 6) ; break;
5
[...]
6
case 15: PORTA &= ~(1 << 5) ; break;
7
case 16: PORTA &= ~(1 << 6) ; break;
8
case 17: PORTA &= ~(1 << 7) ; break;
9
}
wird zu diesem Assembler übersetzt:
1
.LM17:
2
movw r30,r20
3
cpi r30,18
4
cpc r31,__zero_reg__
5
brsh .L8
6
subi r30,lo8(-(gs(.L27)))
7
sbci r31,hi8(-(gs(.L27)))
8
lsl r30
9
rol r31
10
lpm __tmp_reg__,Z+
11
lpm r31,Z
12
mov r30,__tmp_reg__
13
ijmp
14
.data
15
.section .progmem.gcc_sw_table, "a", @progbits
16
.p2align 1
17
.L27:
18
.data
19
.section .progmem.gcc_sw_table, "a", @progbits
20
.p2align 1
21
.word gs(.L9)
22
.word gs(.L10)
23
.word gs(.L11)
24
[...]
25
.word gs(.L24)
26
.word gs(.L25)
27
.word gs(.L26)
28
.section .text.__vector_16
29
.L26:
30
.stabn 68,0,188,.LM18-.LFBB2
31
.LM18:
32
cbi 34-32,7
33
.L8:
34
.stabn 68,0,254,.LM19-.LFBB2
35
// Hier folgt dann der Rest des IRQ-Handlers
36
37
[...]
38
// Beispielhaft ein paar der Sprungziele
39
.L15:
40
.stabn 68,0,177,.LM37-.LFBB2
41
.LM37:
42
cbi 37-32,4
43
rjmp .L8
44
.L10:
45
.stabn 68,0,172,.LM38-.LFBB2
46
.LM38:
47
cbi 43-32,5
48
rjmp .L8
49
.L11:
50
.stabn 68,0,173,.LM39-.LFBB2
51
.LM39:
52
cbi 43-32,6
53
rjmp .L8
54
.L9:
55
.stabn 68,0,171,.LM40-.LFBB2
56
.LM40:
57
cbi 43-32,4
58
rjmp .L8
59
[...]
d.h. man hat hier die Indirektionstabelle, die aus dem Program-Memory
geladen wird. Ok, nur eine Handvoll Zyklen mehr, aber an denen knabbere
ich halt gerade...
> Alles in allem: erst mal in der Hochsprache das letzte aus dem Code> rausholen. Da geht meistens noch viel. Assembler ist deine letzte> Option, wenn du auch noch den letzten Taktzyklus, auf den der Compiler> nicht geachtet hat, rausholen willst. Und meistens braucht man den auch> gar nicht.
Ich würde es nicht tun, wenn ich nicht das Gefühl hätte, an die Grenzen
zu stoßen. Die C-Implementation funktioniert gut, man sieht auf der
Matrix (18x8) allerdings durchaus noch ein Flimmern, dass ich gerne
durch eine Erhöhung der Framerate loswürde.
Im Moment erreiche ich bei 12MHz eine Bildwiederholfrequenz von ca.
168Hz, die kürzeste Zeitscheibe für mein Timerinterrupt (mit variablen
Zeitintervallen für 5-bit-Graustufen) ist dann 128 Zyklen lang. Das soll
weniger werden. Wenn ich es z.B. schaffen würde, auf 64 Zyklen
runterzukommen hätte ich ruck-zuck die doppelte Bildwiederholfrequenz.
Deswegen suche ich gerade intensiv nach überflüssigen Zyklen... :)
Viele Grüße,
Simon
wobei die Arrays so aufgeblasen werden, dass man keinen Zugriffsschutz
und keine Arithmetik an col braucht. Die paar Bytes zusätzlich tun nicht
weh
Das müsste dann sein
> man sieht auf der Matrix (18x8) allerdings durchaus noch ein Flimmern,> dass ich gerne durch eine Erhöhung der Framerate loswürde.
Bei 168Hz siehst du noch ein Flimmern?
Mit dir möchte ich nicht ins Kino gehen. Was siehst du dort? Eine
Abfolge von Standbildern?
Hm, ich sehe schon - ich muss nachher mal ein großes Code-Shootout
veranstalten... Vielen Dank für die vielen Vorschläge :-)
Karl Heinz Buchegger schrieb:>> man sieht auf der Matrix (18x8) allerdings durchaus noch ein Flimmern,>> dass ich gerne durch eine Erhöhung der Framerate loswürde.>> Bei 168Hz siehst du noch ein Flimmern?> Mit dir möchte ich nicht ins Kino gehen. Was siehst du dort? Eine> Abfolge von Standbildern?
Ja, das habe ich mich auch schon gefragt... :-)
Wenn man ruhig auf die Matrix draufguckt, ist das auch kein Problem. Es
wird dann problematisch, wenn die Platine (gar nicht mal so) schnell
durch die Gegend geschwenkt wird, oder man die Platine mit einer
Augenbewegung überstreift. Dann löst sich das Bild in ein komisches
Punktmuster auf.
Im Gegensatz zum Kino habe ich halt auch bei 100% Helligkeit nur einen
Duty-Cycle von 1/18, möglicherweise trägt das dazu bei, k. A.
Viele Grüße,
Simon
Simon Budig schrieb:> Wenn man ruhig auf die Matrix draufguckt, ist das auch kein Problem. Es> wird dann problematisch, wenn die Platine (gar nicht mal so) schnell> durch die Gegend geschwenkt wird,> oder man die Platine mit einer> Augenbewegung überstreift. Dann löst sich das Bild in ein komisches> Punktmuster auf.
Schon.
Aber 168Hz ist schon reichlich viel
>> Im Gegensatz zum Kino habe ich halt auch bei 100% Helligkeit nur einen> Duty-Cycle von 1/18, möglicherweise trägt das dazu bei, k. A.
Du hast aber schon die 1/18 in die 168Hz eingerechnet (genauso wie deine
Graustufen). D.h. du kriegst in 1 Sekunde 18*168 = 2688 komplette Bilder
hin (was an sich schon recht sportlich ist)
Wieso eigentlich 1/18? Hast du das Multiplexen etwa über die Spalten
gemacht? Man multiplext immer über die kleinere Zahl. Anstelle von 18
Spalten multiplexen, multiplext man dann eben 8 Zeilen und schon hat man
einen besseren Duty-Cycle und auch die Framerate geht in die Höhe.
Alleine diese einfache Änderung verdoppelt dir schon mal die Framerate.
Ich denke, das beste ist eine Assembler-Implementierung so wie du sie
ursprünglich geplant hattest:
1
#define __zero_reg__ r1
2
#define __tmp_reg__ r0
3
4
#include <avr/io.h>
5
6
#define IO(x) _SFR_IO_ADDR(x)
7
8
.macro XSET port, bit
9
cbi IO(\port), \bit $ rjmp 1f
10
.endm
11
12
.macro DEFUN name
13
.global \name
14
.func \name
15
\name:
16
.endm
17
18
.macro ENDF name
19
.size \name, .-\name
20
.endfunc
21
.endm
22
23
.data
24
.text
25
26
DEFUN TIMER1_COMPA_vect
27
;; prolog
28
push r31
29
push r30
30
in r30, IO(SREG)
31
push r30
32
33
;; jump
34
lds r30, col
35
lsl r30
36
lsl r30
37
clr r31
38
subi r30, lo8(-(pm(0f)))
39
sbci r31, hi8(-(pm(0f)))
40
ijmp
41
42
0: XSET PORTB, 4
43
XSET PORTC, 1
44
XSET PORTD, 2
45
XSET PORTD, 3
46
XSET PORTD, 4
47
XSET PORTD, 5
48
1:
49
;; epilog
50
pop r30
51
out IO(SREG), r30
52
pop r30
53
pop r31
54
reti
55
ENDF TIMER2_COMPA_vect
Allein der effektiven Pro/Epilog spart mehr als das Rumgefummel mit
Tabellen, zB wird so weder __tmp_reg__ noch __zero_reg__ benötigt.
Weitere 2 Ticks können gespart werden, indem col nicht um 1 hochgezählt
wird in main, sondern um 4 :-) Und IRQs nur möglichst kurz sperren um
die IRQ-Latenz nicht unnötig zu erhöhen (andere ISRs!).
Den größten Gewinn würde aber ein schnellerer Quarz bringen, zB 24 MHz.
Ich hab meine Scope-Clock mit 24 MHz laufen. Das geht problemlos
(ATmega168@5V) und reicht sogar um 50000 Pixel/Sekunde auf die Röhre zu
bekommen und Animation für Asteroids/Snake-Spiel zu berechnen :-)
Karl Heinz Buchegger schrieb:> Du hast aber schon die 1/18 in die 168Hz eingerechnet (genauso wie deine> Graustufen). D.h. du kriegst in 1 Sekunde 18*168 = 2688 komplette Bilder> hin (was an sich schon recht sportlich ist)
Nein, es sind 168 Graustufen-Bilder pro Sekunde.
> Wieso eigentlich 1/18? Hast du das Multiplexen etwa über die Spalten> gemacht? Man multiplext immer über die kleinere Zahl. Anstelle von 18> Spalten multiplexen, multiplext man dann eben 8 Zeilen und schon hat man> einen besseren Duty-Cycle und auch die Framerate geht in die Höhe.> Alleine diese einfache Änderung verdoppelt dir schon mal die Framerate.
Ja ich multiplexe über die Spalten. Ich habe eben mal im
"Kunstwerke"-Thread die Platine vorgestellt da werden vielleicht ein
paar Hintergründe klarer:
Beitrag "Re: Zeigt her Eure Kunstwerke !"
Ich multiplexe über die Spalten, weil ich auf der Platine keinen Platz
für Treiberbausteine oder Transistoren habe, der Atmel treibt also die
Low-Current-LEDs direkt. Und es sind nur 8 statt 18 Vorwiderstände, was
auf meiner Platinengröße echt hilft... :)
Wenn eine Spalte mit 8 LEDs mit ca. 2mA gleichzeitig leuchtet, dann kann
der Atmel den Drain-Strom locker verkraften, bei 18*2mA wäre ich schon
deutlich außerhalb der Spec. Außerdem kommt diese Organisation einem
schnellen Update sehr entgegen: Die Zeilen hängen an einem Port, um also
die Daten einer Spalte zu aktualisieren brauche ich nur ein Byte auf den
PORTC zu schreiben.
Viele Grüße,
Simon
Simon Budig schrieb:> Hm, ich sehe schon - ich muss nachher mal ein großes Code-Shootout> veranstalten... Vielen Dank für die vielen Vorschläge :-)
So, habe ich mal gemacht, irgendwie muss man sich die Nacht ja um die
Ohren schlagen... :)
Ich habe vier Ansätze miteinander verglichen:
* switch() zu verschiedenen Einzel-Bit-Befehlen
* switch() mit Tabellenlookup für die drei Ports
* Sprunglose Variante mit drei Tabellen für die Ports
* Sprunglose Variante mit einer Tabelle und einer kleinen Optimierung.
Die erste Variante (switch zu Einzel-Bit-Ops) ist mit 17 Zyklen die
beste. Es folgt das Switch mit Tabellenlookup (18 Zyklen, evtl. abhängig
von dem Wert von col).
Die Varianten ohne Sprünge (also einfach stur auf alle Ports schreiben)
fallen etwas ab: 25 Zyklen für die Variante mit drei Tabellen und 19
Zyklen für die vereinheitlichte Lookup-Tabelle (und der Optimierung,
dass man in PORTA einfach so reinschreiben kann, weil der ausschließlich
Spalten-Bits hat).
Ich habe jetzt nur die Zyklen gezählt, die diese Kern-Funktionalität
umfassen, insbes. gehe ich von "col" in einem Register aus.
unterschiedliche Prolog-Größen etc. habe ich nicht betrachtet, aber auch
da sieht mir die Bit-Operation-switch()-Variante am besten aus (drei
Register werden gesichert).
Also, sie sind alle dicht beieinander, sind aber doch noch ein Eck von
dem handoptimierten Assembler weg. Ich werde in der nächsten Zeit dann
auch nochmal die anderen Sachen in der ISR angucken und sehen, was sich
da noch rausholen lässt.
Johannes: Danke für die Anregungen, wie man ein eigenständiges
Assembler-File mit in ein (ansonsten) C-Projekt integriert muss ich mir
nochmal im Detail angucken, auch mit Makros habe ich noch nix sinnvolles
gemacht, das sieht aber so aus als könnte das den Code deutlich
übersichtlicher machen.
(4 ist aber glaube ich der falsche Faktor, 2 müsste richtig sein, da im
Program-Memory wortweise adressiert wird und XSET zwei Worte umfasst)
Viele Grüße,
Simon