Timer CTC Mode Atmega328P Verständnisproblem

OP #7353698
Lesenswert?

Hallo, ich habe vor mit dem Timer1 alle 10 ms eine Variable per Interrupt hochzuzählen. In der Schleife soll dann alle 500 ms eine LED umgeschaltet werden. Das funktioniert aber nicht. Es funktioniert nur, wenn mehr Code abgearbeitet wird, hier die auskommentierte For-Schleife, welche mindestens 17 Iterationen durchlaufen muss. Meine Vermutung ist, dass die Variable zu schnell ausgelesen wird. Aber das macht ja keinen Sinn, da die ISR ja abläuft, ohne dass die Variable ausgelesen wird. Wo liegt hier mein Denkfehler?

Hier noch das Programm:

1
#include <avr/io.h>
2
#include <avr/interrupt.h>
3

4
#define F_CPU 16000000UL
5

6
// Ein- und Ausgänge definieren
7
#define LED     PB0
8

9
// Globale Variablen
10
uint8_t gIR_sysTick_counter = 0;
11

12
// ISR
13
ISR(TIMER1_COMPA_vect)
14
{
15
  gIR_sysTick_counter++;
16
}
17

18

19
int main()
20
{
21
  // Setup ================================
22
  cli();
23

24
  // Ausgänge Setup
25
  DDRB |= (1 << LED);
26

27

28
  // Timer1 Mode CTC (Geht auf 0 wenn OCR1A erreicht) & Prescaler auf 256 (Bei 16 MHz wird alle 16 us hochgezählt)
29
  TCCR1B |= (1 << WGM12) | (1 << CS12);
30

31
  // Output Compare
32
  OCR1A = 625;  // 16 us * 625 = 10 ms
33

34
  // Timer1 Interrupt einstellen (Compare mit OCR1A)
35
  TIMSK1 |= (1 << OCIE1A);
36

37
  sei();
38
  // Ende Setup ===========================
39

40
  // Programm Loop
41
  while(1)
42
  {
43
      if(gIR_sysTick_counter >= 50)
44
      {
45
        PORTB ^= (1 << LED);
46
        gIR_sysTick_counter = 0;
47
      }
48
      
49
      /*
50
      for(int i = 0; i < 17; i++)
51
      {}
52
      */
53
  }
54
}
Gast #7353704
Lesenswert?

Schreibe mal "volatile" for

uint8_t gIR_sysTick_counter = 0;

Hintergrund: Die CPU kann auf Register schneller zugreifen, als auf RAM. Deswegen wird der Wert innerhalb der while Schleife in ein Register kopiert und dann nur noch das Register verwendet. Der Compiler ist nicht so schlau zu berücksichtigen, dass die ISR dazwischen kommen kann und den Wert ändern kann.

Mit dem Wort volatile zwingst du den Compiler dazu, bei jedem Zugriff wirklich auf das RAM zuzugreifen, ohne Abkürzungen zu verwenden.

Normalerweise setzt man den systick aber nicht auch 0 zurück, sondern lässt ihn durch laufen (samt Überlauf). In einem komplexeren Programm stößt du bei deiner Methode ganz schnell auf zahlreiche Probleme.

Benutze stattdessen die Subtraktion, um festzustellen, ob 50 Ticks stattgefunden haben:

vorher=gIR_sysTick_counter; if (gIR_sysTick_counter - vorher > 50) {...}

Das funktioniert auch nach einem Überlauf korrekt.

In dem Zusammenhang könnte dieser Aufsatz für dich hilfreich sein: http://stefanfrings.de/multithreading_arduino/index.html

Gast #7353713
Lesenswert?

Falls du deine Variable auf mehr als 8 Bit vergrößerst, schreibe dir eine Hilfsfunktion die beim Lese-Zugriff Interrupts blockiert:

1
uint16_t get_systick()
2
{
3
    uint16_t value;
4
    cli();
5
    value=gIR_sysTick_counter;
6
    sei();
7
    return value;
8
}

Das sieht aufwändig aus, aber der Maschinencode der daraus generiert wird, ist ziemlich kompakt.

So verhinderst du, dass ein Lesezugriff durch die ISR gestört wird, was dazu führen würde, dass die oberen 8 Bit und die unteren nicht zusammen passen. Frage nach, wenn du das weiter ausdiskutieren willst.

OP #7353777
Lesenswert?

Hier tritt bei meinem Ansatz auch ein Problem auf, welches ich mir nicht erklären kann.

1
  uint8_t past = 0;
2
  (...)
3
  // Programm Loop
4
  while(1)
5
  {
6
      
7
      if(gIR_sysTick_counter - past >= 50)
8
      {
9
        PORTB ^= (1 << LED);
10
        past = gIR_sysTick_counter;
11
      }
12
  }

Die LED blinkt dann drei mal und leuchtet dann permanent. In past müsste nach dem dritten Mal leuchten ja 250 stehen. Nach meiner Berechnung sollte die nächste if(..) ausgeführt werden, wenn gIR_sysTick_counter bei 44 ist. Funktioniert aber nicht. In past müsste doch dann eine 50 stehen, da ja beide unsigned sind.

Gast #7353782
Lesenswert?

Da bin ich jetzt überfragt. Das müsste eigentlich funktionieren.

Schaffe dir mal eine Möglichkeit zum Debuggen, oder zumindest Log-Meldungen ausgeben zu können. Dann kannst du die Werte der Variablen und auch das Ergebnis der Subtraktion kontrollieren.

Das "volatile" hast du aber noch drin, oder?

OP #7353788
Lesenswert?

Korrektur: "in past müsste doch dann eine 50 stehen" ist falsch, ich meinte natürlich das Ergebnis der Subtraktion.

Zu deiner Frage, ja, habe volatile stehen.

Hier nochmal der ganze Code:

1
#include <avr/io.h>
2
#include <avr/interrupt.h>
3

4
#define F_CPU 16000000UL
5

6
// Ein- und Ausgänge definieren
7
#define LED     PB0
8

9
// Globale Variablen
10
volatile uint8_t gIR_sysTick_counter = 0;
11

12
// ISR
13
ISR(TIMER1_COMPA_vect)
14
{
15
  gIR_sysTick_counter++;
16
}
17

18

19
int main()
20
{
21
  uint8_t past = 0;
22

23
  // Setup ================================
24
  cli();
25

26
  // Ausgänge Setup
27
  DDRB |= (1 << LED);
28

29

30
  // Timer1 Mode CTC (Geht auf 0 wenn OCR1A erreicht) & Prescaler auf 256 (Bei 16 MHz wird alle 16 us hochgezählt)
31
  TCCR1B |= (1 << WGM12) | (1 << CS12);
32

33
  // Output Compare
34
  OCR1A = 625;  // 16 us * 625 = 10 ms
35

36
  // Timer1 Interrupt einstellen (Compare mit OCR1A)
37
  TIMSK1 |= (1 << OCIE1A);
38

39
  sei();
40
  // Ende Setup ===========================
41

42
  // Programm Loop
43
  while(1)
44
  {
45
      
46
      if(gIR_sysTick_counter - past >= 50)
47
      {
48
        PORTB ^= (1 << LED);
49
        past = gIR_sysTick_counter;
50
      }
51
  }
52
}
Gast #7353812
Lesenswert?

Ich habe den Fehler mit dem Debugger und diesem Code nachvollziehen können:

1
#include <stdint.h>
2
#include <avr/io.h>
3

4
#define LED     PB0
5

6
volatile uint8_t gIR_sysTick_counter = 250; // damit es schneller geht
7

8
int main() 
9
{
10
    uint8_t past=0;
11
    while(1)
12
    {      
13
        if(gIR_sysTick_counter - past >= 50)
14
        {
15
            PORTB ^= (1 << LED);
16
            past = gIR_sysTick_counter;
17
        }
18

19
        // simuliere Interrupts
20
        gIR_sysTick_counter += 10;
21
    }
22
}

Offenbar ist das Ergebnis des Ausdruckes "gIR_sysTick_counter - past" ein 16 Bit Integer. Erzwinge 8 Bit so, dann geht es:

1
        uint8_t diff=gIR_sysTick_counter - past;
2
        if(diff >= 50)

Da ich solche Warteschleifen bisher immer nur mit 16 Bit oder größeren Variablen programmiert habe, hatte ich das nicht auf dem Schirm.

Milli schrieb:

Korrektur: "in past müsste doch dann eine 50 stehen" ist falsch, ich meinte natürlich das Ergebnis der Subtraktion.

Ist auch falsch, da müsste 250 drin stehen, vom vorherigen Moment wo die LED zuletzt getoggelt wurde.

Beitrag #7353813 wurde von einem Moderator gelöscht.
OP #7353884
Lesenswert?

Danke, es funktioniert jetzt. Habe es auch mal mit 16-Bit Variablen und der empfohlenen Funktion getestet. Da läuft es ohne Probleme, auch nach Überlauf.

Ich würde mir das auch gerne noch mal mit dem Debugger ansehen, welche IDE nutzt du dafür? Ich programmiere in VS Code mit Platformio, da geht das Debuggen nicht so gut beim Atmega328P (oder?).

Stefan F. schrieb:

Ist auch falsch, da müsste 250 drin stehen, vom vorherigen Moment wo die LED zuletzt getoggelt wurde.

Ja, in past müsste 250 stehen. Ich meinte im Ergebnis von gIR_sysTick_counter - past = 44 - 250 = 50.

Gast #7353894
Lesenswert?

Milli schrieb:

Ich würde mir das auch gerne noch mal mit dem Debugger ansehen, welche IDE nutzt du dafür?

Für solche kleinen Tests bevorzuge ich das alte AVR Studio in einer virtuellen Maschine mit Windows XP.

Ernsthafte Programme compiliere ich unter Linux im Terminalfenster mit einem Makefile. Als Editor nutze ich Qt Creator. Visual Studio Code tut es auch gut.

Debuggen kannst du AVR nach meinem Kenntnisstand nur mit den IDEs von Atmel/Microchip. Es gab mal Frickeleien mit Open-Source Tools (sorry, falls sich da jemand betroffen fühlt), die habe ich aber nicht ans Laufen gebracht.

Siehe http://stefanfrings.de/avr_tools/index.html

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