Hallo
Ich versuche gerade eine i2c-Schnittstelle für einen Atmega328P auf einem Arduino Uno über Interrups zu programmieren und habe Timingprobleme, d.h. die Zeit die ich zwischen den SCL habe, ist schrecklich kurz. Deshalb habe ich eine Testroutine geschrieben, um das mal zu messen:
1
#include<avr/interrupt.h>
2
3
intmain(){
4
DDRD&=~(1<<6);//D6 (PCINT22) als TEST_IN 100kHz Rechteck 4,6 Vpp
5
DDRD|=(1<<2);//D2 als TEST_ERG-Ausgang
6
PCMSK2|=(1<<PCINT22);
7
PCICR|=(1<<PCIE2);
8
sei();
9
while(1){
10
__asm__("nop");
11
}
12
return0;
13
}
14
15
ISR(PCINT2_vect){
16
PORTD|=0b00000100;//D2 HIGH und LOW
17
PORTD&=0b11111011;
18
}
Im Bild sieht man dann, das es über 1,3 us dauert, bis eine Reaktion erfolgt! Bzw. 21 Takte.
Die Interrup-Routine zeigt aber nur 10 Takte bis HIGH erscheint (und der Sprung ausgelöst vom Interrupt kommt ja auch noch dazu!):
1
00000080<__vector_5>:
2
80:1f92pushr12Takte
3
82:0f92pushr02Takte
4
84:0fb6inr0,0x3f;631Takt
5
86:0f92pushr02Takte
6
88:1124eorr1,r11Takt
7
8a:5a9asbi0x0b,2;112Takte
8
8c:5a98cbi0x0b,2;11
9
8e:0f90popr0
10
90:0fbeout0x3f,r0;63
11
92:0f90popr0
12
94:1f90popr1
13
96:1895reti
Wenn man sich die Vectortabelle anschaut
1
00000000<__vectors>:
2
0:0c943400jmp0x68;0x68<__ctors_end>
3
4:0c943e00jmp0x7c;0x7c<__bad_interrupt>
4
8:0c943e00jmp0x7c;0x7c<__bad_interrupt>
5
c:0c943e00jmp0x7c;0x7c<__bad_interrupt>
6
10:0c943e00jmp0x7c;0x7c<__bad_interrupt>
7
14:0c944000jmp0x80;0x80<__vector_5>
8
18:0c943e00jmp0x7c;0x7c<__bad_interrupt>
9
1c:0c943e00jmp0x7c;0x7c<__bad_interrupt>
10
...
dann frag ich mich, ob der Vektor-Zeiger von Zeile 0 beginnend, Zeile für Zeile durchjuckelt bis er vector_5 erreicht hat. Dann wäre es ja besser PCINT0 zu benutzen, weil der früher dran kommt. Stimmen meine Überlegungen?
Und kann man dies push/pop-Sachen unterbinden, die da automatisch mit eingebaut werden?
Liebe Grüße
Eine reine Software-Implementierung eines I2C Targets ist zeitlich ein ziemlich heisses Eisen, wenn man beim Takt nicht passend nachgeben will. Deshalb gibt es oft eine Hardware-Unterstützung und darin das für µC-Targets wichtige Clock Stretching. Bereits wenn man auf Clock Stretching verzichten muss, weil der Initiator damit nicht umgehen kann (z.B. ein RasPi), wird es spannend.
dann frag ich mich, ob der Vektor-Zeiger von Zeile 0 beginnend, Zeile
für Zeile durchjuckelt bis er vector_5 erreicht hat.
Eine rege Fantasie!
[C]
ISR(PCINT2_vect) {
PIND = 0b00000100; // toggle
PIND = 0b00000100; // toggle
}
[/C]
Könnte das ein oder andere Push Pop einsparen.
Insbesondere mit naked
dann frag ich mich, ob der Vektor-Zeiger von Zeile 0 beginnend, Zeile
für Zeile durchjuckelt bis er vector_5 erreicht hat. Dann wäre es ja
besser PCINT0 zu benutzen, weil der früher dran kommt. Stimmen meine
Überlegungen?
Nein, das stimmt nicht. Aber in der Vektortabelle muss nicht unbedingt ein jmp stehen. Wenn du also einen Vektor benutzt, bei dem einige darauf in der Tabelle folgende Vektoren unbenutzt sind, dann könntest du die ISR (wenn sie kurz genug ist) direkt in die Vektortabelle schreiben und dir so die drei Takte für den jmp sparen.
Das geht schon mal um einiges flotter. Aber ob das im realen Leben auch
ohne Seitenefekte funktioniert, habe ich noch nicht ausprobiert.
Wenn die ISR SOOO einfach ist, dann ja. Aber sobald auch nur ein einziges Register der CPU beschrieben oder die Staus-Flags geändert werden, dann nicht. Eine "naked" ISR ist nur dann sinnvoll, wenn man dort VOLLSTÄNDIG in Inline-ASM schreibt und weiß was man tut. Mit reinem C ist es mehr Glück als Verstand. Inline-ASM ist aber nervig im Vergleich zu einer reinen ASM-ISR, die man normal schreiben kann.
dann frag ich mich, ob der Vektor-Zeiger von Zeile 0 beginnend, Zeile
für Zeile durchjuckelt bis er vector_5 erreicht hat.
Eine rege Fantasie!
[C]
ISR(PCINT2_vect) {
PIND = 0b00000100; // toggle
PIND = 0b00000100; // toggle
}
[/C]
Könnte das ein oder andere Push Pop einsparen.
Insbesondere mit naked
1
00000080<__vector_5>:
2
80:84e0ldir24,0x04;4
3
82:89b9out0x09,r24;9
4
84:89b9out0x09,r24;9
braucht leider einen mehr, aber es geht ja auch nicht um das Pingewackel.
hier https://www.i2c-bus.org/speed/ steht u.a. "This does not imply that a transmission may not take place at any lower speed or even at a somewhat variable bit rate."
Also mach ich etwas langsammer. Ist ja auch nur intern und für mich.
Vielen Dank für die interessanten Anregungen.
Wie sieht das denn aus mit dem Takt? Verstehen den die meisten Bausteine
auch I2C bei 80 kHz?
Wenn dein AVR der Master ist, kannst du den beliebig langsam takten, das machen alle I2C Bausteine als Slave mit. Dann braucht man aber auch kein I2C mit Interrupts.
Wenn dein AVR ein Slave ist und ein anderer IC der Master ist, dann geht das nur, wenn der Master clock stretching unterstützt. Dann kann ein langsamer Slave den Takt ausbremsen.
die Zeit die ich zwischen den SCL habe, ist schrecklich kurz.
Hab schon länger nix mehr mit I²C gemacht; aber ein (langsamer) Slave darf doch SCL auf low ziehen, um dem Master mitzuteilen, wann er fertig ist un dass er langsam ist?
Das genannte Clock Stretching. Damit kann aber nicht jeder Master umgehen. Etwa die Raspberry Pis. Das ist besonders ärgerlich, weil sich eine Highlevel/Lowlevel Trennung von Steuerungsfunktionen auf Basis von I2C förmlich anbietet, aber genau deshalb problematisch ist.
Eine andere Moeglichkeit waere, den Controller zu wechseln. AVRs sind nicht gerade die besten, was Interrupt Latency/Response Time betrifft. Zudem haben die AVRs auch noch einen Jitter von mindestens einem Taktzyklus. PICs z.B. haben das nicht.
Das ist aber keine verlorene Zeit, es wird ja hier noch Nutzcode ausgeführt, nur halt nicht der in der ISR. Also nicht schädlich für den "Durchsatz", nur schädlich für die Latenz.
Solche Sachen gibt es übrigens noch mehr: Code konkurrierender ISRs und unter Interruptsperre laufender code in main(). auch hier wird was nützliches gemacht, es verzögert aber halt den Eintritt in die konkrete ISR.
Mal mehr, mal weniger, je nach Situation zum Zeitpunkt eines IRQ. Deswegen nennt man das alles auch variable Latenz. Im Gegensatz zum folgenden Rest, der die statische Latenz darstellt, die immer und unter allen Umständen anfällt.
interrupt execution response: >= 5
push PC -> stack: 2
Erstens sind es 4 Takte und nicht 5 (außer bei externem RAM und/oder 22Bit-PC, dafür kommen noch Takte dazu, aber der 328P hat weder das eine noch das andere) und zweitens ist in diesen Takten das Sichern des PC bereits enthalten. Genau das ist übrigens, was die zusätzlichen Takte unter den genannten Bedingungen verursacht.
jmp to vector: 3
Kann man u.U. ganz einsparen, mindestens aber durch rjmp mit nur 2 Takten ersetzen. Man muss halt ISRs günstig im Flash plazieren, also in der Nähe der Vektortabelle.
Und nun der Vollständigkeit halber noch das, was nicht zur statischen Latenz gehört, weil es erst nach Durchlaufen des Nutzcodes in der ISR passiert, aber zum sog. "minimalen Interruptrahmen", also dem unvermeidlichen Overhead einer Interruptverarbeitung. Das ist das reti am Ende der ISR, was (beim 328P) mit 4 Takten zu Buche schlägt. Auch hier gilt wieder: bei Devices mit externen RAM und/oder 22Bit-PC kommen da noch Takte dazu. Schuld ist auch hier wieder der PC, also dessen Rückladen aus dem RAM.
Ich hatte in das 32U4 Datenblatt hinein geschaut (das war gerade zur Hand), da ist explizit von 5 oder mehr die Rede. Mag sein das es für den 328P anders ist.
jmp to vector: 3
kann man u.U. ganz einsparen, mindestens aber durch rjmp mit nur 2
Takten ersetzen.
7.7.1 Interrupt Response Time
The interrupt execution response for all the enabled AVR interrupts is four clock cycles minimum
After four clock cycles the program vector address for the actual interrupt handling routine is executed.
During this four clock cycle period, the Program Counter is pushed onto the Stack.
The vector is normally a jump to the interrupt routine, and this jump takes three clock cycles.
If an interrupt occurs during execution of a multi-cycle instruction, this instruction is completed before the interrupt is served.
If an interrupt occurs when the MCU is in sleep mode, the interrupt execution response time is increased by four clock cycles.
Either: CPU is running, best case 8 cycles, worst case 12 cycles
1 physical INTERRUPT
· finish single cycle instruction (1 cycle)
· maybe finish multi-cycle instruction (additional 1…4 cycles)
2 PC->STACK (4 cycles)
3 jump to the interrupt routine (normally 3 cycles)
Or: CPU is sleeping, 11 cycles
1 physical INTERRUPT
· wake from sleep mode (4 cycles)
2 PC->STACK (4 cycles)
3 jump to the interrupt routine (normally 3 cycles)
Ich hatte in das 32U4 Datenblatt hinein geschaut (das war gerade zur
Hand), da ist explizit von 5 oder mehr die Rede.
Ja, auch RET/RETI dauert hier 5 Takte. Steht auch irgendwo im DB, dass drei Byte PC auf dem Stack landen.
Sprich: Das Teil hat offensichtlich einen 22Bit-PC. Warum auch immer, an der Größe des Flash, der normalerweise der Grund dafür ist, kann es hier ja nicht liegen.
Mag sein das es für den
328P anders ist.
Ist es. Bei den allermeisten Tiny und Mega-Typen gelten die von mir genannten 4 Takte.
Wozu?
Wenn da sowieso niemand hin hüpft,
dann muss man auch keine Girlanden aufhängen.
Besser als ein im Fehlerfall Amok laufendes Programm ist ein RTI allemal. Eine Diagnose dran zu hängen, die dann einen Denkanstoß gibt, ist der nächste Level.
Oder du schreibst einfach fehlerfreie Programme - dann kann man sich die paar Codezeilen natürlich sparen.
Besser als ein im Fehlerfall Amok laufendes Programm ist ein RTI
allemal.
Wir reden ja hier von ›C/C++‹.
Wenn da eine ISR erstellt wird, dann wird der Vector vollautomagisch mit dem richtigen Wert befruchtet.
Und einen Interrupt zu aktivieren ohne eine ISR, nun ja, was soll ich sagen?
Oder du schreibst einfach fehlerfreie Programme - dann kann man sich die
paar Codezeilen natürlich sparen.