Warum wird mein AVR zurückgesetzt, wenn ich wdt_disable() aufrufe, um zu versuchen, den Watchdog-Timer auszuschalten?

Ich habe ein Problem, bei dem das Ausführen einer Deaktivierungs-Watchdog-Sequenz auf einem AVR ATtiny84A den Chip tatsächlich zurücksetzt, obwohl der Timer noch genügend Zeit haben sollte. Dies geschieht inkonsistent und wenn derselbe Code auf vielen physischen Teilen ausgeführt wird; Einige werden jedes Mal zurückgesetzt, einige werden manchmal zurückgesetzt und andere nie.

Um das Problem zu demonstrieren, habe ich ein einfaches Programm geschrieben, das ...

  1. Aktiviert den Watchdog mit einem Timeout von 1 Sekunde
  2. Setzt den Watchdog zurück
  3. Blinkt die weiße LED für 0,1 Sekunden
  4. Blinkte die weiße LED für 0,1 Sekunden aus
  5. Deaktiviert den Watchdog

Die Gesamtzeit zwischen der Watchdog-Aktivierung und -Deaktivierung beträgt weniger als 0,3 Sekunden, dennoch tritt manchmal ein Watchdog-Reset auf, wenn die Deaktivierungssequenz ausgeführt wird.

Hier ist der Code:

#define F_CPU 1000000                   // Name used by delay.h. We are running 1Mhz (default fuses)

#include <avr/io.h>
#include <util/delay.h>
#include <avr/wdt.h>


// White LED connected to pin 8 - PA5

#define WHITE_LED_PORT PORTA
#define WHITE_LED_DDR DDRA
#define WHITE_LED_BIT 5


// Red LED connected to pin 7 - PA6

#define RED_LED_PORT PORTA
#define RED_LED_DDR DDRA
#define RED_LED_BIT 6


int main(void)
{
    // Set LED pins to output mode

    RED_LED_DDR |= _BV(RED_LED_BIT);
    WHITE_LED_DDR |= _BV(WHITE_LED_BIT);


    // Are we coming out of a watchdog reset?
    //        WDRF: Watchdog Reset Flag
    //        This bit is set if a watchdog reset occurs. The bit is reset by a Power-on Reset, or by writing a
    //        logic zero to the flag

    if (MCUSR & _BV(WDRF) ) {

        // We should never get here!


        // Light the RED led to show it happened
        RED_LED_PORT |= _BV(RED_LED_BIT);

        MCUCR = 0;        // Clear the flag for next time
    }

    while(1)
    {
        // Enable a 1 second watchdog
        wdt_enable( WDTO_1S );

        wdt_reset();          // Not necessary since the enable macro does it, but just to be 100% sure

        // Flash white LED for 0.1 second just so we know it is running
        WHITE_LED_PORT |= _BV(WHITE_LED_BIT);
        _delay_ms(100);
        WHITE_LED_PORT &= ~_BV(WHITE_LED_BIT);
        _delay_ms(100);

        // Ok, when we get here, it has only been about 0.2 seconds since we reset the watchdog.

        wdt_disable();        // Turn off the watchdog with plenty of time to spare.

    }
}

Beim Start prüft das Programm, ob das vorherige Zurücksetzen durch eine Watchdog-Zeitüberschreitung verursacht wurde, und wenn dies der Fall ist, leuchtet es die rote LED auf und löscht das Watchdog-Reset-Flag, um anzuzeigen, dass ein Watchdog-Reset stattgefunden hat. Ich glaube, dass dieser Code niemals ausgeführt werden sollte und die rote LED niemals aufleuchten sollte, aber es tut es oft.

Was geht hier vor sich?

Wenn Sie sich entschieden haben, hier Ihre eigenen Fragen und Antworten zu diesem Problem zu schreiben, kann ich mir vorstellen, welche Schmerzen und Leiden nötig waren, um es zu entdecken.
Sie wetten! 12 Stunden an diesem Fehler. Für eine Weile trat der Fehler NUR außerhalb des Standorts auf. Wenn ich die Platinen auf meinen Desktop bringen würde, würde der Fehler verschwinden, wahrscheinlich aufgrund von Temperatureffekten (mein Platz ist kalt, wodurch der Watchdog-Oszillator relativ zur Systemuhr etwas langsamer läuft). Es waren mehr als 30 Versuche nötig, um es zu reproduzieren und es in Aktion auf Video festzuhalten.
Ich kann den Schmerz fast spüren. Ich bin kein alter und navigierter EE, aber ich fand mich manchmal in solchen Situationen wieder. Toller Fang, trink ein Bier und löse weiter Probleme ;)

Antworten (1)

Es gibt einen Fehler in der Bibliotheksroutine wdt_reset().

Hier ist der Code...

__asm__ __volatile__ ( \
   "in __tmp_reg__, __SREG__" "\n\t" \
   "cli" "\n\t" \
   "out %0, %1" "\n\t" \
   "out %0, __zero_reg__" "\n\t" \
   "out __SREG__,__tmp_reg__" "\n\t" \
   : /* no outputs */ \
   : "I" (_SFR_IO_ADDR(_WD_CONTROL_REG)), \
   "r" ((uint8_t)(_BV(_WD_CHANGE_BIT) | _BV(WDE))) \
   : "r0" \
)

Die vierte Zeile erweitert sich zu ...

out _WD_CONTROL_REG, _BV(_WD_CHANGE_BIT) | _BV(WDE)

Die Absicht dieser Zeile besteht darin, eine 1 in das WD_CHANGE_BIT zu schreiben, wodurch die folgende Zeile aktiviert wird, um eine 0 in das Watchdog-Aktivierungsbit (WDE) zu schreiben. Aus dem Datenblatt:

Um einen aktivierten Watchdog-Timer zu deaktivieren, muss wie folgt vorgegangen werden: 1. Schreiben Sie im selben Vorgang eine logische Eins in WDCE und WDE. Unabhängig vom vorherigen Wert des WDE-Bits muss eine logische Eins in WDE geschrieben werden. 2. Schreiben Sie innerhalb der nächsten vier Taktzyklen in der gleichen Operation die WDE- und WDP-Bits wie gewünscht, aber mit gelöschtem WDCE-Bit.

Leider hat diese Zuweisung den Nebeneffekt, dass auch die unteren 3 Bits des Watchdog Control Registers (WDCE) auf 0 gesetzt werden. Dadurch wird der Prescaler sofort auf seinen kürzesten Wert gesetzt. Wenn der neue Prescaler zum Zeitpunkt der Ausführung dieses Befehls bereits ausgelöst wurde, wird der Prozessor zurückgesetzt.

Da der Watchdog-Timer von einem physikalisch unabhängigen 128-kHz-Oszillator betrieben wird, ist es schwer vorherzusagen, wie der Zustand des neuen Prescalers in Bezug auf das laufende Programm sein wird. Dies erklärt die breite Palette von beobachteten Verhaltensweisen, bei denen der Fehler mit Versorgungsspannung, Temperatur und Herstellungscharge korreliert werden kann, da all diese Dinge die Geschwindigkeit des Watchdog-Oszillators und der Systemuhr asymmetrisch beeinflussen können. Das war ein sehr schwer zu findender Fehler!

Hier ist aktualisierter Code, der dieses Problem vermeidet ...

__asm__ __volatile__ ( \
   "in __tmp_reg__, __SREG__" "\n\t" \
   "cli" "\n\t" \
   "wdr" "\n\t" \
   "out %0, %1" "\n\t" \
   "out %0, __zero_reg__" "\n\t" \
   "out __SREG__,__tmp_reg__" "\n\t" \
   : /* no outputs */ \
   : "I" (_SFR_IO_ADDR(_WD_CONTROL_REG)), \
   "r" ((uint8_t)(_BV(_WD_CHANGE_BIT) | _BV(WDE))) \
   : "r0" \
)

Die zusätzliche wdrAnweisung setzt den Watchdog-Timer zurück, sodass, wenn die folgende Zeile möglicherweise zu einem anderen Prescaler wechselt, diese garantiert noch nicht abgelaufen ist.

Dies könnte auch behoben werden, indem die WD_CHANGE_BIT- und WDE-Bits in WD_CONTROL_REGISTER ODER-verknüpft werden, wie in den Datenblättern vorgeschlagen ...

; Write logical one to WDCE and WDE
; Keep old prescaler setting to prevent unintentional Watchdog Reset
in r16, WDTCR
ori r16, (1<<WDCE)|(1<<WDE)
out WDTCR, r16

...aber das erfordert mehr Code und ein zusätzliches Scratch-Register. Da der Watchdog-Zähler beim Deaktivieren ohnehin zurückgesetzt wird, macht das Extra-Reset nichts kaputt und hat keine ungewollten Seiteneffekte.

Ich möchte Ihnen auch Requisiten geben, denn als ich die Liste der avr-libc-Probleme überprüfte, scheint es, dass Sie (vermutlich Sie) sie dort bereits eingereicht haben savannah.nongnu.org/bugs/?44140
ps "josh.com" ist echt... beeindruckend