Hallo Community,
ich habe ein komisches Laufzeit-Verhalten festgestellt bei der
main-Funktion bzw. der darin befindlichen while(1) Schleife.
Einfaches Ziel: Timer0 löst alle 1ms einen Interrupt aus -> ein Flag
wird gesetzt -> das Flag wird in der while-Schleife abgefragt und eine
Aktion ausgeführt.
Hier mein Test-Programm:
Wenn ich jetzt ein Oszilloskop an die Ausgänge anschließe, erhalte ich
folgendes Verhalten:
PC4: (im 1-ms Takt)
1
_ _ _ _ _
2
_| |_| |_| |_| |_| | ....
PC3: (das ist das komische)
1
_ _____ _
2
_| |_| |_| |____ .... Das Muster wiederholt sich ständig.
CPU-Takt ist 8MHz interner Oszillator.
Ich hätte erwartet, dass PC5 dem PC4 folgt, denn 1ms sind ja Welten, um
die 3 Zeilen Code auszuführen.
Hat jemand eine Erklärung dafür? An welcher Stelle stehe ich auf dem
Schlauch?
Danke und viele Grüße
Hi,
wie sieht denn das Verhalten aus, wenn du das zurücksetzen von
trigger_1ms mit in die Klammmer der if-Abfrage ziehst?
Vermutlich wird nämlichhäufig dein trigger_1ms zurückgesetzt bevor die
if-Abfrage überhaupt stattfindet.
Ich habe mal gelernt, wenn an Registern eines Moduls herumgespielt wird,
stellt man den IRQ aus. Ich kann mir vorstellen, dass das Schreiben des
Zählregisters bei laufendem Timer mit aktivem IRQ zu komischen Effekten
führen kann. Zb ein erneutes auslösen des IRQ.
Aahh ja klar, wenn die ISR zwischen If-Abfrage und der folgenden
Anweisung kommt. Das macht Sinn. Allerdings ist es dafür aber seeehr
regelmäßig.
Wenn ich das Rücksetzen von trigger_1ms in die if-Abfrage rein ziehe,
geht PC3 gar nicht mehr an.... Warum das denn nun wieder?!?
Ste R. schrieb:> if (trigger_1ms == true)> {> PORTC ^= (1<<PC3);> }> trigger_1ms = false;> }
Man sollte das Flag erst nach Nutzung löschen, nicht dann wann es
auftritt....
Ich habe
- trigger_1ms volatile gemacht
- ins if reingezogen
- und das neu Laden des Timers als erstes in der ISR ausgeführt
(funktioniert mit und ohne das En-/Disablen der Interrupts)
Danke für die schnelle Hilfe!
Danke auch für die Hinweise! Für CTC müsste ich den Timer1 nehmen (wobei
ich den bis jetzt mangels besseren Wissens für anderes aufsparen
wollte). Nun bin ich schlauer :-)
> Perfekt
Nicht wirklich, da sind immer noch zwei Probleme:
a) ^= ist eine evtl nicht-atomare Read-Modify-Write-Operation, die von
einer ISR unterbrochen werden kann. Wenn die die gleichen Daten
verändert, geht's schief: lese Register, ISR(toggelt Bit 4), schreibe
Register mit altem Bit 4. Wird zwar beim AVR oft zu einer atomaren
Operation optimiert, aber eben nicht immer.
b) Die Abfrage in der main klappt nur, solange die Schleife oft genug
abgearbeitet wird. Wenn da mal Verzögerungen auftreten, verlierst du
schnell einen Interrupt.
Btw, cli/sei in der ISR? Besser nicht ...
Ste R. schrieb:> - und das neu Laden des Timers als erstes in der ISR ausgeführt> (funktioniert mit und ohne das En-/Disablen der Interrupts)
Damit werden deine Zeiten ungenau, falls aus anderen Gründen die
Ausführung der ISR um eine Timertaktperiode oder mehr verzögert wird.
Ste R. schrieb:> Ich habe> - trigger_1ms volatile gemacht> - ins if reingezogen> - und das neu Laden des Timers als erstes in der ISR ausgeführt
Wenn du jetzt noch verstehst, was das Problem überhaupt war, und wodurch
es letztendlich gelöst wurde, hast du was gelernt.
Bisher nicht.
Oliver
Oliver S. schrieb:> Ste R. schrieb:>> Ich habe>> - trigger_1ms volatile gemacht>> - ins if reingezogen>> - und das neu Laden des Timers als erstes in der ISR ausgeführt>> Wenn du jetzt noch verstehst, was das Problem überhaupt war, und wodurch> es letztendlich gelöst wurde, hast du was gelernt.>> Bisher nicht.
In der Tat.
@ OP
Siehe Interrupt.
Ste R. schrieb:> Danke auch für die Hinweise! Für CTC müsste ich den Timer1 nehmen (wobei> ich den bis jetzt mangels besseren Wissens für anderes aufsparen> wollte). Nun bin ich schlauer :-)
Die meisten "moderneren" AVRs können CTC auch am Timer 0 und Timer 2,
siehe Datenblatt.
Ste R. schrieb:> Ich habe> - trigger_1ms volatile gemacht> - ins if reingezogen> - und das neu Laden des Timers als erstes in der ISR ausgeführt> (funktioniert mit und ohne das En-/Disablen der Interrupts)>> Danke für die schnelle Hilfe!
Diese Lösung funktioniert zwar zufällig in diesem Fall, ist jedoch immer
noch nicht wirklich interrupsicher.
Es ist immer noch möglich dass zwischen dem Setzen von PORTC und dem
löschen von trigger_1ms der 1ms Interrupt erneut kommt. Es funktioniert
nur zufällig in diesem Beispiel weil fast kein Code drin ist. Bei
größeren Programmen kommt es sporadisch (1x in paar Tagen) zu
Fehlfunktionen und man sucht sich einen Wolf.
Daher besser das machen:
1
volatileinttrigger_1ms;
2
volatileinttrigger_1ms_main;
3
intmain(void)
4
{
5
trigger_1ms=trigger_1ms_main=0;
6
:::
7
while(1)
8
{
9
if(trigger_1ms!=trigger_1ms_main)
10
{
11
PORTC^=(1<<PC3);
12
trigger_1ms_main++;
13
}
14
}
15
}
16
17
// Timer 0 @1ms
18
ISR(TIMER0_OVF_vect)// Overflow 8-Bit timer
19
{
20
//cli();
21
TCNT0=PRELOAD_TIMER_FOR_1ms;
22
//sei();
23
PORTC^=(1<<PC4);
24
trigger_1ms++;
25
}
Zur Erklärung:
Nur im Interrupt wird "trigger_1ms" geschrieben, in der Main nur
gelesen. Damit umgeht man das Read-Modify-Write Problem und dass da
dazwischen immer Interrupts auftreten können. Schließlich kann ein
Interrupt zu jeder Zeit unterbrechen, z.B. wenn eine C Codezeile in
mehrere Assembler Befehle unterteilt wird, kann der Interrupt zwischen
jedem einzelnen Assembler Befehl die main Routine unterbrechen.
PS: Der Portzugriff "C" ist damit immer noch nicht Interruptsicher.
Ste R. schrieb:> Hier mein Test-Programm:> ...> bool trigger_1ms;
Da C den Datentyp bool nicht kennt, ist das entweder nicht das Programm,
was du tatsächlich hast laufen lassen, oder du benutzt kein C.
Oliver
Markus M. schrieb:> PS: Der Portzugriff "C" ist damit immer noch nicht Interruptsicher.
Wenn ich wissen dürfte, welcher AVR da zum Einsatz kommt, würde ich
sagen:
1
PINC=(1<<PC3);
Toggelt den Pin auf eine wiedereintritsfähige Art und weise.
Oliver S. schrieb:> Da C den Datentyp bool nicht kennt, ist das entweder nicht das Programm,> was du tatsächlich hast laufen lassen, oder du benutzt kein C.
c99 kennt _Bool
Und hier <stdbool.h> wird dann auch bool kreiert.
Arduino Fanboy D. schrieb:> Wenn ich wissen dürfte, welcher AVR da zum Einsatz kommt, würde ich> sagen:PINC=(1<<PC3);> Toggelt den Pin auf eine wiedereintritsfähige Art und weise.
Was ist "PC3"? Eine Konstante?
Dann toggelt da nichts.
Markus M. schrieb im Beitrag #6552199:
> Arduino Fanboy D. schrieb:>> Wenn ich wissen dürfte, welcher AVR da zum Einsatz kommt, würde ich>> sagen:PINC=(1<<PC3);>> Toggelt den Pin auf eine wiedereintritsfähige Art und weise.>> Nein. Ich weiß jetzt nicht aus wie vielen Assembler Befehlen diese eine> Codezeile besteht, jedoch garantiert mindestens aus 2 und damit ist es> nicht Interruptsicher.
Da es um einen AVR geht:
ldi r,PC3
out PINC,r
Und das ist interruptfest.
Der Witz daran ist die atomare Eigenheit nicht zu alter AVRs, beim
Schreiben auf den Input-Port jene Output-Pins zu toggeln, in die eine 1
geschrieben wird.
Markus M. schrieb:> Aber der TO wollte das haben:> PORTC ^= (1<<PC4);
In der Funktion entspricht bei AVRs ab 2004
PINC = 1<<PC4;
dem klassischen
PORTC ^= (1<<PC4);
Aber eben nicht im exakten Code.
> PORTC ^= (1<<PC3);
Ist nicht nicht atomar
> PINC = (1<<PC3);
Ist atomar
Tun beide das gleiche, mit dem Pin wackeln
-
Markus M. schrieb:> Was ist "PC3"? Eine Konstante?>> Dann toggelt da nichts.
Du irrst.
Arduino Fanboy D. schrieb:>> PINC = (1<<PC3);> Ist atomar>> Tun beide das gleiche, mit dem Pin wackeln
OK, dann habe ich auch was gelernt :-)
Avr ist nicht mein "Spezialgebiet"
Markus M. schrieb:> OK, dann habe ich auch was gelernt :-)
Genau das ist die richtige Einstellung!
Dafür bekommst du auch direkt eine negative Bewertung von mir.
Denn hier werden auch mal Irrtümer und falsche Aussagen belohnt,
richtige und sinnhafte Aussagen bestraft.
Markus M. schrieb:> Daher besser das machen:> volatile int trigger_1ms;> volatile int trigger_1ms_main;
Sicher nicht. Die 2. Variable ist überflüssig und erzeugt nur Stress und
Chaos. Es reicht EINE Variable. Siehe
Multitasking: Verbesserter Ansatz mit Timer
Hi :-)
Ich habe gelernt:
- dass meine Trigger-Variable voaltile sein muss, weil sie in der ISR
verändert wird und in der zyklischen Main-Funktion benutzt wird.
Offensichtlich hat der Compiler gemeint, hier optimieren zu dürfen,
deshalb lief es zwischendurch nicht. Mit volatile ist das verhindert und
läuft wie gewünscht.
- dass Interrupts en- und disablen in einer ISR Blödsinn ist (gut, um
13:40Uhr merke ich das eher als 1:00Uhr nachts ;-)
- dass bei CTC Funktion ich nix am eigentlichen Zählerwert rumwurschteln
muss, was die Fehleranfälligkeit verringert/verhindert. Deshalb CTC
vorziehen vor Zähler vorbelegen
- Dass beim Toggeln von IO-Ports in Betracht gezogen werden muss, dass
in der Anweisung eine ISR zuschlagen kann --> allerdings weniger
relevant, da das Port-getoggle ja nur meinem Debugging diente und im
finalen Programm nicht mehr drin ist. Aber trotzdem gut zu wissen.
Ansonsten weil die Fragen kamen:
- ich benutze den ATmega8
- Das Programm ist in C++
Das "Perfekt" oben galt den Hinweisen, nicht dass ich das Programm für
perfekt hielt ;-)
VG
Ste R. schrieb:> Ich habe gelernt:
Das fehlt noch eine Erkenntnis.
1
while(1)
2
{
3
if(trigger_1ms==true)
4
{
5
PORTC^=(1<<PC3);
6
}
7
trigger_1ms=false;
8
}
Das dauerhafte, bedingungslose Löschen von trigger_1ms war falsch, denn
das löscht die Varible oft, ohne daß die ausgewertet wurde! In diesem
Beispiel dauert die while(1) Endllosschleife nur wenige Prozessortakte,
sowas die Variable vielleicht mit 1 MHz immer wieder gelöscht wird! Aber
auch bei deutlich langsamerer Schleife wäre es falsch, denn das
Handshake zwischen ISR und Hauptprogramm ist dadurch im Eimer.
Merke. Ein Flag wird nur dann gelöscht, wenn es vorher als aktiv erkannt
wurde!
Es kommt auf das Design drauf an. Sollte die Main Schleife mal länger
benötigen als 1ms dann bleibt das ganze mit "while(1);" "Hängen".
Für kleine Programme sollte es kein Problem sein dass mindestens 1x je
ms der Main-Loop läuft. Hingegen bei großen kann es durchaus mal länger
dauern. Und dann funktioniert die 1-Variablen Variante nicht mehr.
Hingegen die 2-Variablen Variante zeigt einem wie viele Interrupts
dazwischen waren und macht die Funktion so oft wie der Zähler != Zähler
aus dem Interrupt ist.
Es kommt darauf an was man genau machen möchte, daher ist zwar die
1-Variable-Variante gut, jedoch immer noch nicht 100% Interruptsicher,
je nach Menge von Code der aus dem Main Loop sonst noch aufgerufen wird.
Beispiel Ablauf:
Main Programmbearbeitung aktiv...
Timer: Timer setzt signal
Main: Main Programm immer noch aktiv wird nach 999,9999µs jedoch fertig.
Main: Im Main wird das "Signal" erkannt -> springe in if()
Timer: Genau jetzt sind 1000,000µs vorbei und der Interrupt kommt erneut
Timer: das "signal" ist immer noch gesetzt
Main: ist jetzt kurz nach dem if() und löscht das "signal"
-> Somit ist das zweite Signal nicht mehr bearbeitet.
Dieses "Multitasking" Beispiel bleibt mit "while(1)" hängen, wenn die
Bearbeitung der main länger als 1ms dauert, kann ja sein dass es
beabsichtigt ist um zu sehen dass man "zu viel" programmiert hat.
Der TO wollte dass jeder Timer ISR auch in der main Schleife ausgegeben
wird, also die gleiche Anzahl der Pulse. In dem Fall ist die 1-Variablen
Variante nicht optimal.
Ob er dennoch die 1-Variablen Variante einsetzen möchte, da ihm der
Seiteneffekt mit der Zykluszeit Überwachung nützlich ist, ist ihm
überlassen.
(PS zu diesem Artikel:
https://www.mikrocontroller.net/articles/Multitasking#Verbesserter_Ansatz_mit_Timer)
Markus M. schrieb:> Es kommt auf das Design drauf an. Sollte die Main Schleife mal länger> benötigen als 1ms dann bleibt das ganze mit "while(1);" "Hängen".
Nö, dann wird ein Timerzyklus übersprungen. Wie man es richtig macht,
steht in meinem Link.
> Für kleine Programme sollte es kein Problem sein dass mindestens 1x je> ms der Main-Loop läuft. Hingegen bei großen kann es durchaus mal länger> dauern. Und dann funktioniert die 1-Variablen Variante nicht mehr.
Unfug!
> Hingegen die 2-Variablen Variante zeigt einem wie viele Interrupts> dazwischen waren und macht die Funktion so oft wie der Zähler != Zähler> aus dem Interrupt ist.
Jaja, du und deine Parallelwelt, alles schon 100mal diskutiert.
> Dieses "Multitasking" Beispiel bleibt mit "while(1)" hängen, wenn die> Bearbeitung der main länger als 1ms dauert, kann ja sein dass es> beabsichtigt ist um zu sehen dass man "zu viel" programmiert hat.
[ ] Du hast das Beispiel verstanden.
Probier's noch mal.
> Der TO wollte dass jeder Timer ISR auch in der main Schleife ausgegeben> wird, also die gleiche Anzahl der Pulse. In dem Fall ist die 1-Variablen> Variante nicht optimal.
Jaja, immer schön weiter plappern, bloß nicht mal zugeben, daß man sich
geirrt hat. Erbärmlich!
>> Daher besser das machen:>> volatile int trigger_1ms;>> volatile int trigger_1ms_main;>> Sicher nicht. Die 2. Variable ist überflüssig und erzeugt nur Stress und> Chaos.
Die Methode ist schon gut, wenn man sporadisch "verlorene" Interrupts
nachholen möchte. Ok, "volatile int" ist blöd (uint8_t wäre ok) und
"trigger_1ms_main" braucht nicht volatile zu sein, aber ansonsten ok.
> Es reicht EINE Variable. Siehe Multitasking: Verbesserter Ansatz mit Timer
Das ist holprig und eigentlich nur zum Debuggen zu gebrauchen ...
Falk B. schrieb:> [ ] Du hast das Beispiel verstanden.
Ja, ich habe das Beispiel aus dem verlinkten Artikel verstanden. Damit
wir auch wirklich vom gleichen reden habe ich mal den Codeabschnitt aus
dem Artikel kopiert.
Durch das "while(1);" bleibt das dann "hängen".
Und nun erkläre mir doch bitte wie das ganze zu dem Wunsch vom TO passt.
Zu Erinnerung: TO wollte nur genau gleich viele Impulse wie im
Interrupt, egal wie viel Arbeit der Main-Loop zu bewältigen hat.
Hingegen eine Zykluszeitüberwachung war nicht gefordert.
Nach wie vor:
Die Lösung von mir funktioniert immer.
Die Lösung von dir nur so lange der Main-Loop nicht zu viel Arbeit hat.
Klar, beide Lösungen funktionieren im aktuellen Quellcode, in dem man
ein paar Codezeilen zum Test programmiert, jedoch wird man damit böse
Erlebnisse erfahren wenn man mal was sehr großes macht.
> Die Lösung von mir funktioniert immer.
Naja, mit "immer" wäre ich vorsichtig ;-) Wenn main konstant zu langsam
ist fällt sie auf die Schnauze - es muß schon genügend Zeit vorhanden
sein, die Interrupts nachzuholen.
Allerdings versucht sie, das Problem zu behandeln anstatt das Problem
nur zu erkennen und dann den Dienst einzustellen.
foobar schrieb:> Naja, mit "immer" wäre ich vorsichtig ;-) Wenn main konstant zu langsam> ist fällt sie auf die Schnauze
Stimmt ... dann hat man jedoch ein viel größeres grundlegendes
Konzeptproblem.
Arduino Fanboy D. schrieb:> c99 kennt _Bool> Und hier <stdbool.h> wird dann auch bool kreiert.
Würde es, wenn’s im Code stehen würde. Tut es aber nicht.
Ste R. schrieb:> Ich habe gelernt:> - dass meine Trigger-Variable voaltile sein muss, weil sie in der ISR> verändert wird und in der zyklischen Main-Funktion benutzt wird.> Offensichtlich hat der Compiler gemeint, hier optimieren zu dürfen,> deshalb lief es zwischendurch nicht. Mit volatile ist das verhindert und> läuft wie gewünscht.
Sowohl gcc 7.2.0 als auch 10.2.0 optimieren die Variable nicht weg. Das
war also in dem Fall nicht das Problem.
Ste R. schrieb:> - dass Interrupts en- und disablen in einer ISR Blödsinn ist (gut, um> 13:40Uhr merke ich das eher als 1:00Uhr nachts ;-)
Es verbrät zwei Prozessorzyklen für nichts, Mahr geht dadurch aber nicht
kaputt. Das war also in dem Fall nicht das Problem.
> - dass bei CTC Funktion ich nix am eigentlichen Zählerwert rumwurschteln> muss, was die Fehleranfälligkeit verringert/verhindert. Deshalb CTC> vorziehen vor Zähler vorbelegen
Das „rumwurschteln“ am Zähler wird dann ein Problem, wenn es direkt vor
oder gleichzeitig mit Compare-Matches stattfindet. Da du die aber nicht
nutzt, und den Zähler in einer OVF-ISR setzt, macht das bei dir nichts.
Das war also in dem Fall nicht das Problem. Richtig ist natürlich, daß
man für die Aufgabenstellung besser Den CTC-Mode benutzt, wenn
vorhanden.
> - Dass beim Toggeln von IO-Ports in Betracht gezogen werden muss, dass> in der Anweisung eine ISR zuschlagen kann --> allerdings weniger> relevant, da das Port-getoggle ja nur meinem Debugging diente und im> finalen Programm nicht mehr drin ist.
Das sollte man beachten, wobei bei allen neueren AVRs nach dem
steinalten Mega8 die Operation atomar toggelt.
Das eigentliche Problem war die Behandlung des Flags.
Oliver