Jitter in ISR von Atmega

OP #4645663
Lesenswert?

Auf einem ATmega mit 16 Mhz führe ich diese ISR aus:
1
ISR(ADC_vect)
2
{  
3
        PORTB |= (1<<PB0);
4
  switch (ADMUX & 0x03) {
5
  case 0:  LED_R_PWM = pwm_value[ADCH];
6
      ADMUX |= (1<<MUX0);
7
      ADMUX &= ~(1<<MUX3); //test
8
      break;
9
  case 1: LED_G_PWM = pwm_value[ADCH];
10
      ADMUX |= (1<<MUX1);
11
      ADMUX &= ~(1<<MUX0);
12
      break;
13
  case 2: LED_B_PWM = pwm_value[ADCH];
14
      ADMUX &= ~(1<<MUX1);
15
      ADMUX &= ~(1<<MUX3); //test
16
      break;
17
  }
18

19
  PORTB &= ~(1<<PB0);  
20
}

Das setzen von PB0 für die Dauer die ISR dient nur dem Bestimmen der 
Ausführungszeit von der ISR.
Mich verwundert aber ein Jitter, den in dabei messen kann.
In dem Oszi-Screenshot (Ch1 ist PB0, Ch2 der Takt) sieht man, dass die 
Ausführungszeit um bis zu drei Takte schwankt, obwohl ich extra noch die 
drei Cases angeglichen habe, indem ich für Case 0 und Case 2 noch 
jeweils einen unnützen Befehl (mit //test kommentiert) eingefügt habe...
pwm_value[] ist ein Array für die Logarithmus-Korrektur, falls das von 
Interesse ist.

Nicht dass der Jitter meinem Programm irgendeinen Abbruch tun würde, 
aber es interessiert mich einfach mal, woher er kommt, da ich den Grund 
nicht herausfinden konnte.
Angehängte Dateien:
Moderator (Firma: Titel) Persönliche Seite #4645679
Lesenswert?

Julian S. schrieb:
> obwohl ich extra noch die drei Cases angeglichen habe, indem ich für
> Case 0 und Case 2 noch jeweils einen unnützen Befehl (mit //test
> kommentiert) eingefügt habe...
Ähm, dir ist klar, das nicht jede C-Zeile auf dem uC die selbe Laufzeit 
hat? Nicht mal auf Assembler-Ebene ist das so...
BTW: Der "Idealfall" wäre sogar, wenn eine Zeile gar keine Laufzeit 
hat und das Programm blitzschnell ausgeführt wird.

> da ich den Grund nicht herausfinden konnte.
Wie gesagt: wenns um einzelne Takte geht, dann ist das Assemblerlisting 
der Anlaufpunkt...
Gast #4645686
Lesenswert?

Hallo,

die Wege eines C-Compilers sind manchmal etwas merkwürdig.

Richtig herausbekommen kann man das eigentlich nur, wenn man sich das 
Disassembly bzw das *.lss file das Atmel Studio anlegt anschaut.

Mit welchen Optimierungseinstellungen kompilierst du? Manchmal kann es 
sein, dass aufgrund "schlechter" Optimierung ein Stück nutzloser Code 
oder sowas übrig bleibt, was dann einige Takte schluckt.

Wenn du das elf-File oder das lss-File hochlädst, kann ich mal schauen.

Spontan fällt mir als Grund nur ein, wie der switch-case implementiert 
wird.
Auf dem AVR wird er als "if", also als Vergleich, implementiert. und je 
nachdem in welchen Case du hineinspringst müssen davor unterschiedlich 
viele vergleiche durchgeführt werden, was zu einer Laufzeitveränderung 
führt.
Springt er beispielsweise in den dritten Case hat er davor schon zweimal 
die ersten beiden abgeglichen. Dies muss er nicht tun, wenn der erste 
Case bereits ein "Hit" ist.
OP #4645688
Lesenswert?

@Ingo Less
Ahh, daran habe ich nicht gedacht. Ergibt natürlich Sinn.

@Lothar Miller
Theoretisch war mir das schon klar, ich kenne auch zum Großteil die 
Ausführungszeiten der ASM-Befehle, aber ich bin schlicht nicht drauf 
gekommen, dass die zwei gleichen Befehle zu einem Zusammengefasst werden 
können.

Danke =)
Gast #4645689
Lesenswert?

Ingo L. schrieb:
> Julian S. schrieb:
>> ADMUX &= ~(1<<MUX1);
>>       ADMUX &= ~(1<<MUX3); //test
> Wird zu einer Aktion Optimiert un
>
> Julian S. schrieb:
>> ADMUX |= (1<<MUX1);
>>       ADMUX &= ~(1<<MUX0);
> Hier müssen jedoch zwei unterschiedliche Aktionen gemacht werden, die
> exakt 3 Takte dauern Read, Modify, Write.

Das sollte nicht passieren. Die Register sind als "volatile" deklariert. 
Somit werden Zugriffe nicht vom Compiler gruppiert.

Ingo L. schrieb:
> Außerdem, wenn du unnützen Kram einbaust, ist es die Aufgabe des
> Optimizers diese Sache zu finden und weg zu optimieren, was er hier wohl
> auch tut...

Der Compiler weiß aber nicht, was davon unnötig ist und was nicht :D
OP #4645698
Lesenswert?

Ingo L. schrieb:
> Wenn ich vorher etwas mit 3 maskiere kann es nicht größer als 3 sein,
> das sollte jeder Compiler trotz volatile optimieren oder?

Naja, nur weil mich für die Case-Bedingung nur die letzten beiden Bits 
des Registers interessieren heißt das ja noch lange nicht, dass ich 
nicht auch irgendwie ein höherwertigeres Bit setzen/zurücksetzen kann!
Gast #4645699
Lesenswert?

Ingo L. schrieb:
> Ich habe gerade mal in einem meiner *.lss
> nachgeschaut: Zwei Zugriffe (odernd) auf TCCR1B werden tatsächlich nicht
> zusammengefasst.

Was mich an diesem Listing irgendwie wundert, ist dass der Compiler eine 
indirekte Adressierung über den Z-Pointer baut.

Wäre nicht auch ein Zugriff mittels lds/sts und einer Konstanten in 
diesem Fall möglich? Dass würde Takte sparen.
#4645703
Lesenswert?

Ich hänge es nochmal an, ist beim ändern versehentlich verloren 
gegangen:
1
  // Input Capture
2
  TIFR1 |= (1<<ICF1);
3
 2f8:  b5 9a         sbi  0x16, 5  ; 22
4
  TIMSK1 |= (1<<ICIE1);
5
 2fa:  ef e6         ldi  r30, 0x6F  ; 111
6
 2fc:  f0 e0         ldi  r31, 0x00  ; 0
7
 2fe:  80 81         ld  r24, Z
8
 300:  80 62         ori  r24, 0x20  ; 32
9
 302:  80 83         st  Z, r24
10
  TCCR1B |= (1<<ICNC1);
11
 304:  e1 e8         ldi  r30, 0x81  ; 129
12
 306:  f0 e0         ldi  r31, 0x00  ; 0
13
 308:  80 81         ld  r24, Z
14
 30a:  80 68         ori  r24, 0x80  ; 128
15
 30c:  80 83         st  Z, r24
16
  TCCR1B |= (1<<CS11);      // Prescaler 8
17
 30e:  80 81         ld  r24, Z
18
 310:  82 60         ori  r24, 0x02  ; 2
19
 312:  80 83         st  Z, r24
OP #4645708
Lesenswert?

M. H. schrieb:
> Spontan fällt mir als Grund nur ein, wie der switch-case implementiert
> wird.
> Auf dem AVR wird er als "if", also als Vergleich, implementiert. und je
> nachdem in welchen Case du hineinspringst müssen davor unterschiedlich
> viele vergleiche durchgeführt werden, was zu einer Laufzeitveränderung
> führt.
Daran wird es dann wohl liegen.
Ich hab mal den ASM-Code angehangen, aber dass spätere Cases mehr 
Vergleiche "benötigen", erschließt sich mir schon.
Angehängte Dateien:
Gast #4645711
Lesenswert?

Julian S. schrieb:
> Ich hab mal den ASM-Code angehangen,

Ja. Die Vergleiche für die einzelnen Cases machen da nen Unterschied.
Aber was mich wundert ist, dass dein Compiler Registerzugriffe mit sts 
implementiert wohingegen die Registerzugriffe aus dem Listing von Ingo 
Less mit st implementiert sind.

Irgendwie verwirrt mich das gerade. War eigentlich der Meinung, dass der 
AVR-GCC in 99% der Fälle recht zuverlässig arbeitet und ausgereift ist. 
Aber bei der Implementierung mittels st dauert ein Schreibzugriff ja 
doppelt so lang.
#4645719
Lesenswert?

Ingo L. schrieb:
> M. H. schrieb:
>> Der Compiler weiß aber nicht, was davon unnötig ist und was nicht :D
> Wenn ich vorher etwas mit 3 maskiere kann es nicht größer als 3 sein,
> das sollte jeder Compiler trotz volatile optimieren oder?

Nein, da der Compiler die Seiteneffekte dieser Anweisung nicht kennt. 
Die Zuweisung könnte irgendwas auslösen oder zwischen den Zuweisungen 
könnte der Wert bereits wieder von der Hardware verändert werden. 
volatile-Variablen behandelt der Compiler wie angeknackste rohe Eier. 
Ansonsten könnte er folgendes auch optimieren:
1
volatile uint8_t HardwareFlag;
2

3
void foo(){
4
  HardwareFlag = true;
5
  while(HardwareFlag){
6
    /* warten */
7
  }
8
}
#4646368
Lesenswert?

Julian S. schrieb:
> Mich verwundert aber ein Jitter, den in dabei messen kann.

Das ist doch noch harmlos.
Sobald Du noch andere Interrupts benutzt, kann der Jitter leicht 50 
CPU-Zyklen und mehr betragen.

Mit ISR_NOBLOCK kann man zwar manche Interrupts unterbrechbar machen und 
den Zusatz-Jitter auf ~10 Zyklen verkürzen, aber z.B. bei der UART geht 
das in die Hose (Stacküberlauf).
#4646376
Lesenswert?

Warum sind die 3 Case-Pfade unterschiedlich lang?
Weil der Compiler
1. diese nicht über eine Tabelle mit Zieladressen für 0,1,2 anspringt, 
was in der Konstelation teurer wäre als Vergleiche und Sprünge
2. er um das jeweils nächste Case rumspringt zum return, was beim 
letzten nicht notwendig ist.
3. C keine Garantie zu taktgenauen Ausführungszeiten macht.

Falls man letzteres wirklich braucht, ist Assembler angesagt. Allerdings 
funktioniert der Trick dann auch nur auf den einfachen CPUs wie z.B. 
AVR.

BTW, in der ISR ist noch etwas Luft, denn ADMUX ist (da HW-Register) 
volatile deklariert, kann aber in einer (trickfreien) ISR als 
nicht-volatile angesehen werden. Wenn man es also in eine lokale 
Variable _ADMUX kopiert, diese in der ISR verwendet und am Ende wieder 
zurückkopiert, dann wird der Compiler ADMUX in einem (CPU-)Register 
"cachen". Wenn ich das .lss richtig überflogen hab, spart das 6x LDS 
Rn,0x007C und 4x STS 0x007C,Rn. Die sind alle 2-Wort-Befehle. Wird 
sicher auch ohne laufen, aber kurze ISRs sind besser, besonders, wenn 
man nur normale Sprachmittel benutzt hat.
#4646377
Lesenswert?

Ach je, was für eine Problematik ?

Der Compiler erzeugt funktionieren den Code in völlig ausreichender 
Qualität. Wer zyklengenaue ISRs benötigt, muß diese in Assembler 
programmieren. Bei dem Beispiel hier sinds ja, wie üblich, nur ein paar 
wenige Zeilen.

Jittern wirds dann allerdings immer noch, selbst dann, wenn nur eine 
einziger Interruption aktiv ist, weil die benötigen Zyklen zum Einsprung 
in eine ISR nicht konstant sind.

Oliver
#4646414
Lesenswert?

Versuchs mal so:
1
ISR(ADC_vect)
2
{  
3
        PORTB |= (1<<PB0);
4

5
  uint8_t _ADMUX = ADMUX;
6
  uint8_t _ADCH  = ADCH;
7

8
  switch (_ADMUX & 0x03) {
9
  case 0:  LED_R_PWM = pwm_value[_ADCH];
10
      _ADMUX++;
11
      break;
12
  case 1: LED_G_PWM = pwm_value[_ADCH];
13
      _ADMUX++;
14
      break;
15
  case 2: LED_B_PWM = pwm_value[_ADCH];
16
      _ADMUX &= ~(_BV(MUX1)|_BV(MUX0));   // zurück auf 0
17
      break;
18
  }
19

20
  ADMUX = _ADMUX;
21

22
  PORTB &= ~(1<<PB0);  
23
}

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren