Forum: Compiler & IDEs Inline-Assembler für extrem schnellen Interrupt


von Leo B. (luigi)


Lesenswert?

Servus zusammen,

ich möchte einen Drehgeber (oft auch Encoder genannt) mit AB-Ausgang 
auswerten. Ganz wie hier beschrieben: 
http://www.mikrocontroller.net/articles/Drehgeber

Nun dreht die Welle, an welcher sich der Encoder befindet recht schnell, 
sodass ich den Interrupt so kurz wie irgendwie möglich halten möchte um 
die Prozessorlast so gering wie möglich zu halten.

Ich habe mich daher an die Arbeit gemacht und mich über inline-Assembler 
schlauer gelesen. Leider habe ich ein paar Schwierigkeiten den Code zu 
compilieren. Das lesen einer Progmem-Variable scheint mir nicht gerade 
DIE Einstiegs-Aufgabe zu sein...
ich scheitere an der Fehlermeldung
error: memory input 1 is not directly addressable
welcher in der Zeile "m" (encoder_tab) auftritt.

Jetzt poste ich einfach meinen ganzen Versuch in der Hoffnung, ihr könnt 
mir etwas unter die Arme greifen und vielleicht seht ihr ja sogar noch 
etwas Optimierungspotential.

also hier mein Versuch:
Der Code in C:
1
const int8_t encoder_tab[16] PROGMEM = {0,1,-1,0,-1,0,0,1,1,0,0,-1,0,-1,1,0};
2
volatile int8_t  enc_delta = 0;
3
4
ISR( PCINT0_vect )
5
{
6
  static uint8_t code;
7
  // read in new Encoder-Position
8
  code = (code<<2) & 0x0F;
9
    code |= PINB & 3;
10
11
    enc_delta += pgm_read_byte( &encoder_tab[ code ] );
12
}

selbiges als inline-Assembler
1
const int8_t encoder_tab[16] PROGMEM = {0,1,-1,0,-1,0,0,1,1,0,0,-1,0,-1,1,0};
2
volatile int8_t  enc_delta = 0;
3
4
void PCINT0_vect (void) __attribute__ ((signal, naked));
5
void PCINT0_vect (void)
6
{
7
  static uint8_t code = 0;
8
  __asm__ __volatile
9
  (
10
    "push  r1"      "\n\t"
11
    "push  r0"      "\n\t"
12
    "in    r0, __SREG__"  "\n\t"
13
    "push  r0"      "\n\t"
14
    "push  r30"    "\n\t"
15
    "push  r31"    "\n\t"
16
    // tmp = (code<<2) & 0x0F;
17
    "lds  r30, %0"  "\n\t"
18
    "ldi  r31, 0x04"  "\n\t"
19
    "mul  r30, r31"  "\n\t"
20
    "andi  r0, 0x0F"  "\n\t"
21
    // tmp |= PINB & 3;
22
    "in    r30, 0x03"  "\n\t"
23
    "andi  r30, 0x03"  "\n\t"
24
    "or    r30, r0"  "\n\t"
25
    // code = tmp;
26
    "sts  %0, r30"  "\n\t"
27
    // tmp = pgm_read_byte( &encoder_tab[ tmp ] );
28
    "ldi  r31, 0xFF"  "\n\t"
29
    "subi  r30, %A1"  "\n\t"
30
    "sbci  r31, %B1"  "\n\t"
31
    "lpm  r30, Z"    "\n\t"
32
    // enc_delta += tmp;
33
    "lds  r31, %2"  "\n\t"
34
    "add  r31, r30"  "\n\t"
35
    "sts  %0, r31"  "\n\t"
36
    "pop  r31"  "\n\t"
37
    "pop  r30"  "\n\t"
38
    "pop  r0"  "\n\t"
39
    "out  __SREG__, r0"  "\n\t"
40
    "pop  r0"  "\n\t"
41
    "pop  r1"  "\n\t"
42
    :  "+m" (code)
43
    :  "m" (encoder_tab),
44
      "m" (enc_delta)
45
    :  "memory"
46
  );
47
  asm volatile ( "reti" );
48
}

ich hoffe man muss nicht direkt die Hände über dem Kopf zusammenschalgen 
und es besteht noch Hoffnung für mich. Ich habe mich wirklich sehr 
bemüht das Beste aus meinem Unwissen zu machen!

Danke schonmal
lg Leo

von (prx) A. K. (prx)


Lesenswert?

Der AVR kann nicht nur multiplizieren, sondern auch schieben. Das spart 
dir R0,R1 ein.

Der Name eines Arrays ist keine direkt adressierbare Variable sondern 
eine Adresskonstante. Daher die Fehlermeldung.

von (prx) A. K. (prx)


Lesenswert?

Wenn es wirklich eng zugeht: Viele AVRs stellen im I/O-Bereich ein paar 
frei verwendbare Bytes als "General Purpose I/O Register" zur Verfügung, 
an die man per IN/OUT schneller ran kommt als per LDS/STS ans RAM.

Etwas härterer Tobak: Tabelle so im Speicher platzieren, dass sie nicht 
über eine 256-Byte-Grenze geht, oder noch besser exakt an einer solchen 
beginnt. Reduziert die Adressrechnung.

von Falk B. (falk)


Lesenswert?

@  Leo B. (luigi)

>ich möchte einen Drehgeber (oft auch Encoder genannt) mit AB-Ausgang
>auswerten. Ganz wie hier beschrieben:
>http://www.mikrocontroller.net/articles/Drehgeber

Kaum. Denn dann würdest du nicht den Pin Change Interrupt verwenden. 
Aber dazu zum 100202034mal zu diskutieren hab ich keine Lust.

Wenn man schon schnelle ISRs braucht, sollte man sie besser komplett in 
ASM als eigene Datei aufsetzen, das ist deutlich einfacher und wartbarer 
als Inline ASM.

von Leo B. (luigi)


Lesenswert?

Ihr seid einfach einsame Spitze! Danke

A. K. schrieb:
> Der AVR kann nicht nur multiplizieren, sondern auch schieben. Das spart
> dir R0,R1 ein.
Haste recht. Zugegeben, ich hab den Compiler meinen C-Code compilieren 
lassen und hab mich am lss-File orientiert. Etwas gemogelt, aber für nen 
Einsteiger wie mich nicht Falsch glaube ich.

A. K. schrieb:
> Der Name eines Arrays ist keine direkt adressierbare Variable sondern
> eine Adresskonstante. Daher die Fehlermeldung.
Danke das löst den Fehler. Nur muss ich dann die Adresse oder die 
Variable übergeben? also "i" (encoder_tab) oder "i" (&encoder_tab)?
und stimmt der Constraint i dann überhaupt?

A. K. schrieb:
> Wenn es wirklich eng zugeht: Viele AVRs stellen im I/O-Bereich ein paar
> frei verwendbare Bytes als "General Purpose I/O Register" zur Verfügung,
> an die man per IN/OUT schneller ran kommt als per LDS/STS ans RAM.
Hab ich nun umgesetzt denke ich. Danke für den Hinweis!
( enc_delta ist ne globale variable und wär mich auch lieber wenn es 
eine solche bleibt )

A. K. schrieb:
> Etwas härterer Tobak: Tabelle so im Speicher platzieren, dass sie nicht
> über eine 256-Byte-Grenze geht, oder noch besser exakt an einer solchen
> beginnt. Reduziert die Adressrechnung.
Da geb ich dir Recht, nur wie ich dem guten Compiler beibringe wo ich 
die Daten gerne hätte?! Da hab ich absolut keinen Ansatz wo ich anfangen 
sollte google oder Bücher zu bemühen...


Jetzt sieht's folgendermaßen aus:
1
const int8_t encoder_tab[16] PROGMEM = {0,1,-1,0,-1,0,0,1,1,0,0,-1,0,-1,1,0};
2
volatile int8_t  enc_delta = 0;
3
4
void PCINT0_vect (void) __attribute__ ((signal, naked));
5
void PCINT0_vect (void)
6
{
7
  asm volatile
8
  (
9
  "push  r30"      "\n\t"
10
  "in    r30, __SREG__"  "\n\t"
11
  "push  r30"      "\n\t"
12
  "push  r31"      "\n\t"
13
  // tmp = (code<<2) & 0x0F;
14
  "in    r31, %1"    "\n\t"
15
  "lsl  r31"      "\n\t"
16
  "lsl  r31"      "\n\t"
17
  "andi  r31, 0x0F"    "\n\t"
18
  // tmp |= PINB & 3;
19
  "in    r30, %2"    "\n\t"
20
  "andi  r30, 0x03"    "\n\t"
21
  "or    r30, r31"    "\n\t"
22
  // code = tmp;
23
  "out  %1, r30"    "\n\t"
24
  // tmp = pgm_read_byte( &encoder_tab[ tmp ] );
25
  "ldi  r31, 0xFF"    "\n\t"
26
  "subi  r30, %A3"    "\n\t"
27
  "sbci  r31, %B3"    "\n\t"
28
  "lpm  r30, Z"      "\n\t"
29
  // enc_delta += tmp;
30
  "lds  r31, %0"    "\n\t"
31
  "add  r31, r30"    "\n\t"
32
  "sts  %0, r31"    "\n\t"
33
  "pop  r31"      "\n\t"
34
  "pop  r30"      "\n\t"
35
  "out  __SREG__, r30"  "\n\t"
36
  "pop  r30"      "\n\t"
37
  :  "+m" (enc_delta)
38
  :  "I" (_SFR_IO_ADDR (GPIOR0)),
39
    "I" (_SFR_IO_ADDR (PINB)),
40
    "i" (encoder_tab)
41
  :  "memory"
42
  );
43
  asm volatile ( "reti" );
44
}
Kann noch jemand Fehler oder Verbesserungen sehen?
vielen Dank
lg Leo

Edit: hab grad noch ein paar r30 zu r31 und ein r0 zu r 31 gemacht... 
Flücktigkeitsfehler, wollte auch schnell sein so wie ihr...

von Leo B. (luigi)


Lesenswert?

Ja ich entdecke einen Fehler! Nur eine Lösung? wo ist der Fehler 
genau???
Der Compiler sollte ja die Adresse der encoder_tab irgendwie so 
auflösen, dass später im Code steht:
1
subi  r30, 0xCC  ; 204
2
sbci  r31, 0xFF  ; 255
was er allerdings tut ist:
1
subi  r30, 0x34  ; 52
2
sbci  r31, 0x34  ; 52
Dass da noch das Einer-Komplement mit drin steckt ist mir klar aber mein 
Ergebniss ist ja völlig Falsch...
Wie löst man das??

Ich versuche mich verzweifelt an folgendem Beispiel aber irgendwie 
klapt's nicht!
http://www.rn-wissen.de/index.php/Inline-Assembler_in_avr-gcc#Zugriff_aufs_SRAM

von (prx) A. K. (prx)


Lesenswert?

%A3/B3 ergibt nur bei Registern Sinn. Bei Konstanten wärs eher lo8(%3) 
und hi8(%3).

von Leo B. (luigi)


Lesenswert?

Ah ok, danke. ja das löst das halbe Problem. Nur wie ich ihm beibringe 
das Komplement zu bilden ist mir noch unklar. Er weigert sich 
hartnäckig.
Minus klappt nicht, die Schlange ~(...) auch nicht...
Wird wohl auch nicht allzu häufig gebraucht hab ich das Gefühl, da die 
allwissende Suchmaschine mich nicht aufklären möchte. Ich hab hier einen 
echten Hänger. Sehr nervig!

Danke für jeden Tip!

von (prx) A. K. (prx)


Lesenswert?

Minus klappt durchaus - aber an der richtigen Stelle.

Q: Wie kriege ich sowas raus?

A: Frag den Compiler, der muss es ja wissen:
1
extern char a[];
2
3
char * f(int x)
4
{
5
        return a+x;
6
}
ergibt
1
        subi r24,lo8(-(a))
2
        sbci r25,hi8(-(a))

von Leo B. (luigi)


Lesenswert?

Genial, Danke!
so ganz 100% klar ist mir zwar gerade noch nicht, wie du das raus 
bekommen hast (auch wenn du mir das wohl gerade beibringen wolltest, 
danke dafür). aber ich werde da dran bleiben und mir das nochmal genauer 
ansehen!

von troll (Gast)


Lesenswert?

Leo B. schrieb:
> so ganz 100% klar ist mir zwar gerade noch nicht, wie du das raus
> bekommen hast
A.K. dürfte einfach den gezeigten C-Code compiliert und dann ins 
Listfile (.lss) geschaut haben.

von (prx) A. K. (prx)


Lesenswert?

troll schrieb:
> A.K. dürfte einfach den gezeigten C-Code compiliert und dann ins
> Listfile (.lss) geschaut haben.

Das lss-File bringt nix, ich will ja den Code vor dem Assembler 
wissen, nicht disassemblerten Code. Also
   avr-gcc -S t1.c
und dann ins t1.s schauen.

von fonsana (Gast)


Lesenswert?

Leo B. schrieb:
> Nun dreht die Welle, an welcher sich der Encoder befindet recht schnell,
> sodass ich den Interrupt so kurz wie irgendwie möglich halten möchte um
> die Prozessorlast so gering wie möglich zu halten.

Was heisst denn recht schnell in Zahlen?
Wieviele Impulse erzeugt denn der Encoder je Umdrehung?

Das sollte doch ersteinmal ueberschlagen werden, bevor man ueberhaupt 
ueber Tricks und Klimmzuege nachdenkt.

fonsana

von troll (Gast)


Lesenswert?

A. K. schrieb:
> troll schrieb:
>> A.K. dürfte einfach den gezeigten C-Code compiliert und dann ins
>> Listfile (.lss) geschaut haben.
>
> Das lss-File bringt nix, ich will ja den Code vor dem Assembler
> wissen, nicht disassemblerten Code. Also
>    avr-gcc -S t1.c
> und dann ins t1.s schauen.

Ah so, wieder was gelernt. Danke.

von Leo B. (luigi)


Lesenswert?

troll schrieb:
> Ah so, wieder was gelernt. Danke.

Dem schließe ich mich an, Danke.

fonsana schrieb:
> Was heisst denn recht schnell in Zahlen?
> Wieviele Impulse erzeugt denn der Encoder je Umdrehung?

1000 Striche pro Umdrehung, jeder löst 4 Interrupts/Flanken aus, bei 
einer Drehzahl von 1000U/min und mehr. Der aktuelle Interrupt brauch mit 
rjmp zum Interrupt und reti 39 Zyklen. Macht 2.600.000 Zyklen pro 
Sekunde für den Encoder. Statt den 68 Zyklen den Compilierten C-Codes 
por Interrupt, respektive 4.533.333 Zyklen pro Sekunde finde ich ist das 
eine starke Verbesserung. Da der µC noch 3 zeitkritische Trigger (so 
genau wie möglich, auf jeden Fall aber weniger als 100µs Abweichung vom 
Soll) verwalten soll, nebenbei noch SPI-Master an einem Bus mit 3 Slaves 
und eine UART->RS232->USB->PC Verbindung verarbeiten darf, ist der µC 
doch nicht schlecht ausgelastet. Ich bin also froh um die paar Prozent 
gewonnener Prozessorzeit!

von Leo B. (luigi)


Lesenswert?

Ich glaube ich hab da einen Fehler, kann ihn aber nicht ganz 
nachvollziehen. Vielleicht könnt ihr mir nochmal kurz auf die Sprünge 
helfen.
Es geht um die Berechnung der Speicheradresse:
Der Compiler produziert aus dem c-Code folgendes:
1
  uint8_t tmp;
2
[...]
3
  code = tmp;
4
  90:  e0 93 02 01   sts  0x0102, r30  // => r30 = tmp
5
6
  tmp = pgm_read_byte( &encoder_tab[ tmp ] );
7
  94:  ff 27         eor  r31, r31
8
  96:  e7 fd         sbrc  r30, 7
9
  98:  f0 95         com  r31
10
  9a:  ec 5c         subi  r30, 0xCC  ; 204
11
  9c:  ff 4f         sbci  r31, 0xFF  ; 255
12
  9e:  e4 91         lpm  r30, Z
Wenn ich das richtig deute, dann lässt sich der 2. Absatz in folgenden, 
für mich leserlicheren "nicht ganz C"-Code zurück übersetzen:
1
  uint16 tmp = (uint16)(tmp);  // r30 = tmp; r31 = 0
2
  if( tmp >= 128 )             // sbrc  r30, 7
3
    tmp -= 1;                  // com  r31
4
  tmp -= (uint16)(-34);
5
  tmp = pgm_read_byte( tmp );  // lpm  r30, Z
Aber warum das ganze mit dem -1 ?
Entweder ich mach beim übersetzen einen Fehler oder mir ist nicht klar 
warum er das macht, der gute Compiler?

Kann mir da grad nochmal jemand helfen bitte?
lg Leo

von Piefke (Gast)


Lesenswert?

Leo B. schrieb:
> Kann mir da grad nochmal jemand helfen bitte?

Folgenden Hinweis einfach beachten:

Falk Brunner schrieb:
> Wenn man schon schnelle ISRs braucht, sollte man sie besser komplett in
> ASM als eigene Datei aufsetzen, das ist deutlich einfacher und wartbarer
> als Inline ASM.

von (prx) A. K. (prx)


Lesenswert?

Leo B. schrieb:
>   94:  ff 27         eor  r31, r31
>   96:  e7 fd         sbrc  r30, 7
>   98:  f0 95         com  r31

Übersetzt: 8-Bit mit Vorzeichen erweitert auf 16 Bit mit Vorzeichen.

von Stefan E. (sternst)


Lesenswert?

A. K. schrieb:
> Übersetzt: 8-Bit mit Vorzeichen erweitert auf 16 Bit mit Vorzeichen.

Was mich doch sehr stark an diesem hier zweifeln lässt:
1
uint8_t tmp;

von (prx) A. K. (prx)


Lesenswert?

Piefke schrieb:
> Folgenden Hinweis einfach beachten:
>
> Falk Brunner schrieb:
>> Wenn man schon schnelle ISRs braucht, sollte man sie besser komplett in
>> ASM als eigene Datei aufsetzen, das ist deutlich einfacher und wartbarer
>> als Inline ASM.

Ist für so ziemlich alle Fragen hier völlig irrelevant. Dafür hat man 
aber Zusatzkram wie File-Kopf und extern-Deklarationen an der Backe, die 
man sich bei inline-Code erspart. Einfacher ist eigentlich nur die 
Tipparbeit der Funktion selbst, weil der \n\t Kram entfällt.

von Leo B. (luigi)


Lesenswert?

A. K. schrieb:
> Übersetzt: 8-Bit mit Vorzeichen erweitert auf 16 Bit mit Vorzeichen.

Tatsache... Dann rechnet der GCC offenbar einfach mit einem Vorzeichen 
behafteten Index ohne Warnung, Hinweis, o.Ä.... klingt gefährlich!
Folgender Code ist zweifellos der Quellcode zu obigem assemblierten 
Ausschnitt, ich hab's nochmal nachgeprüft!
1
ISR( PCINT0_vect )
2
{
3
  uint8_t tmp;
4
  static uint8_t code = 0; 
5
  tmp = (code<<2) & 0x0F;
6
  tmp |= PINB & 3;
7
  code = tmp;
8
9
  tmp = pgm_read_byte( &encoder_tab[ tmp ] );
10
11
  enc_delta += tmp;
12
}

von Stefan E. (sternst)


Lesenswert?

Leo B. schrieb:
> Dann rechnet der GCC offenbar einfach mit einem Vorzeichen
> behafteten Index ohne Warnung, Hinweis, o.Ä.... klingt gefährlich!

Warum sollte er warnen? Ein Array-Index ist in C immer Vorzeichen 
behaftet.

Leo B. schrieb:
> Folgender Code ist zweifellos der Quellcode zu obigem assemblierten
> Ausschnitt, ich hab's nochmal nachgeprüft!

Schwer zu glauben. Ich würde mich dann mal auf die Suche nach der 
Definition von uint8_t machen.

von Piefke (Gast)


Lesenswert?

A. K. schrieb:
> Ist für so ziemlich alle Fragen hier völlig irrelevant. Dafür hat man
> aber Zusatzkram wie File-Kopf und extern-Deklarationen an der Backe, die
> man sich bei inline-Code erspart.

Das ist ja nun keine unlösbare Aufgabe.
Wenn man wenig tippen möchte, kann man sich auch die .c-Funktion in eine 
Assemblerquelle übersetzen lassen und ggf. optimieren. Dann stimmen 
zumindest das Gerüst und die Grundfunktion.
Der obige inline-Text ist doch Spagetti pur!

von (prx) A. K. (prx)


Lesenswert?

Stefan Ernst schrieb:
> Warum sollte er warnen? Ein Array-Index ist in C immer Vorzeichen
> behaftet.

Aber nicht die Erweiterung von uint8_t zu int. Die anschliessende 16-Bit 
Addition vom Index zur Basisadresse darf er dann gerne mit Vorzeichen 
machen.

von Stefan E. (sternst)


Lesenswert?

A. K. schrieb:
> Aber nicht die Erweiterung von uint8_t zu int.

Natürlich nicht. Habe ich das mit der zitierten Aussage auch nur 
ansatzweise behauptet?

von (prx) A. K. (prx)


Lesenswert?

War als Erklärung gemeint, nicht so sehr an dich adressiert.

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.