Perché il mio AVR si reimposta quando chiamo wdt_disable () per provare a disattivare il timer del watchdog?


34

Sto riscontrando un problema in cui l'esecuzione di una sequenza di watchdog di disabilitazione su un AVR ATtiny84A sta effettivamente ripristinando il chip anche se al timer dovrebbe essere rimasto un sacco di tempo. Ciò accade in modo incoerente e quando si esegue lo stesso codice su molte parti fisiche; alcuni ripristinano ogni volta, alcuni ripristinano a volte e altri mai.

Per dimostrare il problema, ho scritto un semplice programma che ...

  1. Abilita il watchdog con un timeout di 1 secondo
  2. Reimposta il cane da guardia
  3. Lampeggia il LED bianco acceso per 0,1 secondi
  4. Lampeggiava il LED bianco spento per 0,1 secondi
  5. Disabilita il cane da guardia

Il tempo totale tra l'abilitazione e la disabilitazione del watchdog è inferiore a 0,3 secondi, tuttavia a volte si verifica un reset del watchdog quando viene eseguita la sequenza di disabilitazione.

Ecco il codice:

#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.

    }
}

All'avvio, il programma verifica se il ripristino precedente è stato causato da un timeout del watchdog e, in tal caso, accende il LED rosso e cancella il flag di reset del watchdog per indicare che si è verificato un reset del watchdog. Credo che questo codice non dovrebbe mai essere eseguito e il LED rosso non dovrebbe mai accendersi, eppure lo fa spesso.

Cosa sta succedendo qui?


7
Se hai deciso di scrivere qui le tue domande e risposte su questo problema, posso immaginare il dolore e la sofferenza necessari per scoprirlo.
Vladimir Cravero,

3
Scommetti! 12 ore su questo bug. Per un po ', il bug si verificava SOLO fuori dal sito. Se avessi portato le schede sul mio desktop, il bug sarebbe scomparso, probabilmente a causa degli effetti della temperatura (il mio posto è freddo, il che rende l'oscillatore del watchdog leggermente più lento rispetto all'orologio di sistema). Ci sono volute oltre 30 prove per riprodurlo e catturarlo nell'atto in video.
bigjosh,

Posso quasi sentire il dolore. Non sono un vecchio e navigato EE ma a volte mi ritrovo in tali situazioni. Grande cattura, bere una birra e continuare a risolvere i problemi;)
Vladimir Cravero,

Risposte:


41

C'è un bug nella routine della libreria wdt_reset ().

Ecco il codice ...

__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" \
)

La quarta riga si espande fino a ...

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

L'intenzione di questa riga è di scrivere un 1 su WD_CHANGE_BIT, che consentirà alla riga seguente di scrivere uno 0 nel bit di abilitazione watchdog (WDE). Dal foglio dati:

Per disabilitare un timer watchdog abilitato, è necessario seguire la seguente procedura: 1. Nella stessa operazione, scrivere una logica su WDCE e WDE. Una logica deve essere scritta in WDE indipendentemente dal valore precedente del bit WDE. 2. Entro i successivi quattro cicli di clock, nella stessa operazione, scrivere i bit WDE e WDP come desiderato, ma con il bit WDCE azzerato.

Sfortunatamente, questa assegnazione ha l'effetto collaterale di impostare anche i 3 bit inferiori del Watchdog Control Register (WDCE) su 0. Ciò imposta immediatamente il prescaler sul valore più breve. Se il nuovo prescaler è già stato attivato al momento dell'esecuzione di questa istruzione, il processore verrà ripristinato.

Poiché il timer del watchdog esegue un oscillatore a 128 kHz fisicamente indipendente, è difficile prevedere quale sarà lo stato del nuovo prescaler in relazione al programma in esecuzione. Ciò spiega l'ampia gamma di comportamenti osservati in cui il bug può essere correlato alla tensione di alimentazione, alla temperatura e al lotto di produzione poiché tutte queste cose possono influenzare la velocità dell'oscillatore del watchdog e dell'orologio di sistema in modo asimmetrico. Questo è stato un errore molto difficile da trovare!

Ecco un codice aggiornato che evita questo problema ...

__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" \
)

L' wdristruzione aggiuntiva reimposta il timer del watchdog, quindi quando la riga seguente passa potenzialmente a un altro prescaler, è garantito che non sia ancora scaduto il timeout.

Questo potrebbe anche essere risolto ORingando i bit WD_CHANGE_BIT e WDE nel WD_CONTROL_REGISTER come suggerito nei fogli dati ...

; 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

... ma questo richiede più codice e un registro scratch aggiuntivo. Poiché il contatore del watchdog viene ripristinato quando viene disabilitato comunque, il ripristino extra non ostruisce nulla e non ha effetti collaterali involontari.


7
Vorrei anche darti degli oggetti di scena perché quando sono andato a controllare la lista dei problemi di avr-libc, sembra che tu (presumibilmente tu) l'abbia già inviata lì savannah.nongnu.org/bugs/?44140
vicatcu

1
ps "josh.com" è reale ... impressionante
vicatcu
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.