Dlaczego mój AVR resetuje się, gdy wywołuję wdt_disable (), aby spróbować wyłączyć timer nadzorujący?

34

Mam problem polegający na tym, że wykonanie sekwencji wyłączania watchdoga w AVR ATtiny84A faktycznie resetuje układ, mimo że zegar powinien mieć na to dużo czasu. Dzieje się to niekonsekwentnie i podczas uruchamiania tego samego kodu na wielu częściach fizycznych; niektóre resetują się za każdym razem, niektóre resetują czasami, a niektóre nigdy.

Aby zademonstrować problem, napisałem prosty program, który ...

  1. Włącza watchdoga z 1-sekundowym limitem czasu
  2. Resetuje watchdog
  3. Miga białą diodą LED przez 0,1 sekundy
  4. Błysnęła biała dioda LED na 0,1 sekundy
  5. Wyłącza organ nadzoru

Całkowity czas między włączeniem i wyłączeniem watchdoga jest krótszy niż 0,3 sekundy, ale czasami resetowanie watchdoga następuje po wykonaniu sekwencji wyłączania.

Oto kod:

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

    }
}

Podczas uruchamiania program sprawdza, czy poprzedni reset został spowodowany przekroczeniem limitu czasu watchdoga, a jeśli tak, to zapala czerwoną diodę LED i usuwa flagę resetowania watchdoga, aby wskazać, że nastąpiło resetowanie watchdoga. Uważam, że ten kod nigdy nie powinien zostać wykonany, a czerwona dioda LED nigdy nie powinna się zapalić, ale często tak się dzieje.

Co tu się dzieje?

bigjosh
źródło
7
Jeśli zdecydujesz się napisać tutaj własne pytania i odpowiedzi na temat tego problemu, mogę sobie wyobrazić ból i cierpienie, które były potrzebne, aby go odkryć.
Vladimir Cravero
3
Obstawiasz! 12 godzin na ten błąd. Przez pewien czas błąd występowałby TYLKO poza witryną. Gdybym przyniósł tablice na pulpit, błąd zniknąłby, prawdopodobnie z powodu efektów temperaturowych (moje miejsce jest zimne, co powoduje, że oscylator kontrolny pracuje nieco wolniej w stosunku do zegara systemowego). Ponad 30 prób zajęło odtworzenie go i uchwycenie go na filmie.
bigjosh
Prawie czuję ból. Nie jestem stary i nawigowany EE, ale czasami znalazłem się w takich sytuacjach. Świetny połów, piwo i rozwiązywanie problemów;)
Vladimir Cravero

Odpowiedzi:

41

W procedurze biblioteki wdt_reset () występuje błąd.

Oto kod ...

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

Czwarta linia rozwija się do ...

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

Intencją tego wiersza jest zapisanie 1 do WD_CHANGE_BIT, który umożliwi następnej linii zapisanie 0 do bitu włączającego watchdog (WDE). Z arkusza danych:

Aby wyłączyć włączony Timer Watchdog, należy postępować zgodnie z następującą procedurą: 1. W tej samej operacji zapisz logiczną w WDCE i WDE. Logika musi być zapisana w WDE, niezależnie od poprzedniej wartości bitu WDE. 2. W ciągu następnych czterech cykli zegara, w tej samej operacji, zapisz bity WDE i WDP zgodnie z potrzebą, ale z bitem WDCE wyczyszczonym.

Niestety, to przypisanie ma również efekt uboczny polegający na ustawieniu dolnych 3 bitów rejestru kontrolnego Watchdog (WDCE) na 0. To natychmiast ustawia preskaler na jego najkrótszą wartość. Jeśli nowy prescaler został już uruchomiony w momencie wykonania tej instrukcji, procesor zostanie zresetowany.

Ponieważ zegar nadzorujący uruchamia się niezależnie od fizycznie oscylatora 128 kHz, trudno jest przewidzieć, jaki będzie stan nowego preskalera w stosunku do uruchomionego programu. Uwzględnia to szeroki zakres zaobserwowanych zachowań, w których błąd może być skorelowany z napięciem zasilania, temperaturą i partią produkcyjną, ponieważ wszystkie te rzeczy mogą wpływać na prędkość oscylatora nadzorującego i zegara systemowego asymetrycznie. To był bardzo trudny błąd do znalezienia!

Oto zaktualizowany kod, który pozwala uniknąć tego problemu ...

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

Dodatkowa wdrinstrukcja resetuje licznik czasu watchdoga, więc gdy następna linia potencjalnie przełączy się na inny prescaler, na pewno nie upłynął limit czasu.

Można to również naprawić poprzez ORing WD_CHANGE_BIT i bitów WDE w WD_CONTROL_REGISTER, jak sugerowano w arkuszach danych ...

; 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

... ale wymaga to więcej kodu i dodatkowego rejestru scratch. Ponieważ licznik watchdoga jest zresetowany, gdy jest wyłączony, dodatkowy reset niczego nie blokuje i nie ma żadnych niezamierzonych skutków ubocznych.

bigjosh
źródło
7
Chciałbym również dać wam rekwizyty, ponieważ kiedy poszedłem sprawdzić listę problemów z avr-libc, wygląda na to, że (prawdopodobnie ty) przesłałeś ją tam już savannah.nongnu.org/bugs/?44140
vicatcu
1
ps „josh.com” jest naprawdę ... imponujący
vicatcu