¿Por qué mi AVR se reinicia cuando llamo a wdt_disable() para intentar apagar el temporizador de vigilancia?

Tengo un problema en el que ejecutar una secuencia de vigilancia desactivada en un AVR ATtiny84A en realidad está reiniciando el chip a pesar de que el temporizador debería tener mucho tiempo. Esto sucede de manera inconsistente y cuando se ejecuta el mismo código en muchas partes físicas; algunos se reinician cada vez, algunos se reinician a veces y otros nunca.

Para demostrar el problema, he escrito un programa simple que...

  1. Habilita el perro guardián con un tiempo de espera de 1 segundo
  2. Restablece el perro guardián
  3. Parpadea el LED blanco durante 0,1 segundos
  4. Parpadeó el LED blanco durante 0,1 segundos
  5. Deshabilita el perro guardián

El tiempo total entre la activación y desactivación del mecanismo de vigilancia es inferior a 0,3 segundos, aunque a veces se produce un restablecimiento del mecanismo de vigilancia cuando se ejecuta la secuencia de desactivación.

Aquí está el 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.

    }
}

Al inicio, el programa verifica si el restablecimiento anterior fue causado por un tiempo de espera de vigilancia y, de ser así, enciende el LED rojo y borra el indicador de restablecimiento de vigilancia para indicar que se produjo un restablecimiento de vigilancia. Creo que este código nunca debe ejecutarse y el LED rojo nunca debe encenderse, pero a menudo lo hace.

¿Que esta pasando aqui?

Si decidiste escribir aquí tus propias preguntas y respuestas sobre este problema, puedo imaginar el dolor y el sufrimiento que fue necesario para descubrirlo.
¡Apuesta! 12 horas en este error. Por un tiempo, el error SOLO ocurriría fuera del sitio. Si llevara las placas a mi escritorio, el error desaparecería, probablemente debido a los efectos de la temperatura (mi lugar es frío, lo que hace que el oscilador de vigilancia funcione un poco más lento en relación con el reloj del sistema). Se necesitaron más de 30 intentos para reproducirlo y capturarlo en el acto en video.
Casi puedo sentir el dolor. No soy un EE antiguo y navegado, pero a veces me encontré en tales situaciones. Gran captura, tómate una cerveza y sigue resolviendo problemas ;)

Respuestas (1)

Hay un error en la rutina de la biblioteca wdt_reset().

Aquí está el 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" \
)

La cuarta línea se expande a...

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

La intención de esta línea es escribir un 1 en WD_CHANGE_BIT, lo que permitirá que la siguiente línea escriba un 0 en el bit de habilitación de vigilancia (WDE). De la hoja de datos:

Para deshabilitar un Watchdog Timer habilitado, se debe seguir el siguiente procedimiento: 1. En la misma operación, escribir uno lógico a WDCE y WDE. Se debe escribir un uno lógico en WDE independientemente del valor anterior del bit WDE. 2. Dentro de los siguientes cuatro ciclos de reloj, en la misma operación, escriba los bits WDE y WDP como desee, pero con el bit WDCE borrado.

Desafortunadamente, esta asignación tiene el efecto secundario de establecer también los 3 bits inferiores del Registro de control de vigilancia (WDCE) en 0. Esto establece inmediatamente el prescaler a su valor más corto. Si el nuevo prescaler ya se disparó en el momento en que se ejecuta esta instrucción, el procesador se restablecerá.

Dado que el temporizador de vigilancia ejecuta un oscilador de 128 kHz físicamente independiente, es difícil predecir cuál será el estado del nuevo preescalador en relación con el programa en ejecución. Esto explica la amplia gama de comportamientos observados en los que el error se puede correlacionar con el voltaje de suministro, la temperatura y el lote de fabricación, ya que todas estas cosas pueden afectar la velocidad del oscilador de vigilancia y el reloj del sistema de forma asimétrica. ¡Este fue un error muy difícil de encontrar!

Aquí hay un código actualizado que evita este 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" \
)

La instrucción adicional wdrrestablece el temporizador de vigilancia, por lo que cuando la siguiente línea cambia potencialmente a un preescalador diferente, se garantiza que aún no se ha agotado el tiempo.

Esto también podría solucionarse colocando los bits WD_CHANGE_BIT y WDE en WD_CONTROL_REGISTER como se sugiere en las hojas de datos...

; 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

...pero esto requiere más código y un registro adicional de scratch. Dado que el contador de vigilancia se reinicia cuando está deshabilitado de todos modos, el reinicio adicional no daña nada y no tiene efectos secundarios no intencionales.

También me gustaría darle apoyo porque cuando fui a revisar la lista de problemas de avr-libc, parece que usted (presumiblemente usted) ya la envió allí savannah.nongnu.org/bugs/?44140
pd "josh.com" es real... impresionante