Por que meu AVR é ​​redefinido quando ligo para wdt_disable () para tentar desativar o temporizador do watchdog?

34

Estou tendo um problema em que a execução de uma sequência de watchdog de desativação em um AVR ATtiny84A está realmente redefinindo o chip, mesmo que o temporizador deva ter bastante tempo nele. Isso acontece inconsistentemente e ao executar o mesmo código em várias partes físicas; alguns são redefinidos toda vez, outros são redefinidos algumas vezes e outros nunca.

Para demonstrar o problema, escrevi um programa simples que ...

  1. Ativa o cão de guarda com um tempo limite de 1 segundo
  2. Redefine o watchdog
  3. Pisca o LED branco por 0,1 segundos
  4. Piscou o LED branco por 0,1 segundos
  5. Desativa o cão de guarda

O tempo total entre a ativação e desativação do watchdog é inferior a 0,3 segundos, mas às vezes ocorre uma redefinição do watchdog quando a sequência de desativação é executada.

Aqui está o código:

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

    }
}

Na inicialização, o programa verifica se a redefinição anterior foi causada por um tempo limite do watchdog e, nesse caso, acende o LED vermelho e apaga o sinalizador de redefinição do watchdog para indicar que ocorreu uma redefinição do watchdog. Acredito que esse código nunca deve ser executado e o LED vermelho nunca deve acender, mas geralmente ocorre.

O que está acontecendo aqui?

bigjosh
fonte
7
Se você decidiu escrever sua própria pergunta e resposta aqui sobre esse problema, posso imaginar a dor e o sofrimento necessários para descobri-lo.
Vladimir Cravero 31/01
3
Pode apostar! 12 horas sobre este bug. Por um tempo, o bug ocorreria SOMENTE fora do local. Se eu trouxesse as placas para a minha área de trabalho, o bug desapareceria, provavelmente por causa dos efeitos da temperatura (minha casa é fria, o que faz com que o oscilador do watchdog seja um pouco mais lento em relação ao relógio do sistema). Foram necessários mais de 30 testes para reproduzi-lo e capturá-lo em flagrante em vídeo.
bigjosh
Eu quase posso sentir a dor. Eu não sou um EE velho e navegado, mas às vezes me encontrava nessas situações. Grande captura, tomar uma cerveja e manter a resolução de problemas;)
Vladimir Cravero

Respostas:

41

Há um erro na rotina da biblioteca wdt_reset ().

Aqui está o código ...

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

A quarta linha se expande para ...

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

A intenção desta linha é escrever um 1 no WD_CHANGE_BIT, o que permitirá que a linha a seguir escreva um 0 no bit de ativação do watchdog (WDE). A partir da folha de dados:

Para desativar um Watchdog Timer ativado, o seguinte procedimento deve ser seguido: 1. Na mesma operação, escreva uma lógica no WDCE e no WDE. Uma lógica deve ser gravada no WDE, independentemente do valor anterior do bit WDE. 2. Nos próximos quatro ciclos de clock, na mesma operação, escreva os bits WDE e WDP conforme desejado, mas com o bit WDCE limpo.

Infelizmente, essa atribuição tem o efeito colateral de também definir os 3 bits inferiores do Watchdog Control Register (WDCE) como 0s. Isso define imediatamente o pré-calibrador para seu valor mais curto. Se o novo pré-calibrador já foi acionado no momento em que esta instrução é executada, o processador será redefinido.

Como o timer do watchdog roda em um oscilador fisicamente independente de 128 kHz, é difícil prever qual será o estado do novo pré-calibrador em relação ao programa em execução. Isso explica a ampla gama de comportamentos observados, nos quais o erro pode ser correlacionado com a tensão de alimentação, temperatura e lote de fabricação, pois todas essas coisas podem afetar a velocidade do oscilador do watchdog e do relógio do sistema assimetricamente. Este foi um bug muito difícil de encontrar!

Aqui está o código atualizado que evita esse 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" \
)

As wdrinstruções extras redefinem o timer do watchdog; portanto, quando a linha a seguir potencialmente mudar para um pré-calibrador diferente, é garantido que o tempo limite ainda não foi atingido.

Isso também pode ser corrigido ORing os bits WD_CHANGE_BIT e WDE no WD_CONTROL_REGISTER, conforme sugerido nas folhas de dados ...

; 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

... mas isso requer mais código e um registro extra de rascunho. Como o contador do watchdog é redefinido quando desativado, a redefinição extra não atrapalha nada e não tem efeitos colaterais não intencionais.

bigjosh
fonte
7
Eu também gostaria de lhe dar adereços, porque quando fui verificar a lista de problemas do avr-libc, parece que você (presumivelmente você) a enviou já savannah.nongnu.org/bugs/?44140
vicatcu
11
ps "josh.com" é real ... impressionante
vicatcu 31/01