Почему мой AVR сбрасывается, когда я вызываю wdt_disable(), чтобы попытаться выключить сторожевой таймер?

У меня возникла проблема, когда выполнение последовательности отключения сторожевого таймера на AVR ATtiny84A фактически сбрасывает чип, хотя на таймере должно быть достаточно времени. Это происходит непоследовательно и при запуске одного и того же кода на многих физических частях; некоторые сбрасываются каждый раз, некоторые сбрасываются иногда, а некоторые никогда.

Чтобы продемонстрировать проблему, я написал простую программу, которая...

  1. Включает сторожевой таймер с таймаутом в 1 секунду
  2. Сбрасывает сторожевой таймер
  3. Мигает белым светодиодом в течение 0,1 секунды
  4. Вспышка белого светодиода погасла на 0,1 секунды
  5. Отключает сторожевой таймер

Общее время между включением и отключением сторожевого таймера составляет менее 0,3 секунды, однако иногда при выполнении последовательности отключения происходит сброс сторожевого таймера.

Вот код:

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

    }
}

При запуске программа проверяет, не был ли предыдущий сброс вызван тайм-аутом сторожевого таймера, и если да, то зажигает красный светодиод и сбрасывает флаг сброса сторожевого таймера, указывая на то, что произошел сброс сторожевого таймера. Я считаю, что этот код никогда не должен выполняться, и красный светодиод никогда не должен загораться, хотя это часто происходит.

Что здесь происходит?

Если вы решили написать здесь свои вопросы и ответы по этой проблеме, я могу представить боль и страдания, которые потребовались, чтобы обнаружить ее.
Вы держите пари! 12 часов на эту ошибку. Некоторое время ошибка могла происходить ТОЛЬКО за пределами сайта. Если бы я поставил платы на свой рабочий стол, то ошибка исчезла бы, вероятно, из-за температурных эффектов (у меня холодное место, из-за чего сторожевой генератор работает немного медленнее относительно системных часов). Потребовалось более 30 попыток, чтобы воспроизвести его и заснять на видео в действии.
Я почти чувствую боль. Я не старый и опытный EE, но иногда попадал в такие ситуации. Отличный улов, выпей пива и продолжай решать проблемы ;)

Ответы (1)

В процедуре библиотеки wdt_reset() есть ошибка.

Вот код...

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

Четвертая строка расширяется до...

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

Эта строка предназначена для записи 1 в WD_CHANGE_BIT, что позволит следующей строке записать 0 в бит включения сторожевого таймера (WDE). Из даташита:

Чтобы отключить включенный сторожевой таймер, необходимо выполнить следующую процедуру: 1. В той же операции записать логическую единицу в WDCE и WDE. Логическая единица должна быть записана в WDE независимо от предыдущего значения бита WDE. 2. В течение следующих четырех тактов в той же операции запишите биты WDE и WDP по желанию, но со сброшенным битом WDCE.

К сожалению, это назначение имеет побочный эффект, заключающийся в установке младших 3 битов контрольного регистра сторожевого таймера (WDCE) в 0. Это немедленно устанавливает прескалер на самое короткое значение. Если новый прескалер уже был запущен в момент выполнения этой инструкции, процессор будет сброшен.

Поскольку сторожевой таймер работает от физически независимого генератора с частотой 128 кГц, трудно предсказать, каким будет состояние нового предделителя по отношению к работающей программе. Это объясняет широкий диапазон наблюдаемых поведений, когда ошибка может быть связана с напряжением питания, температурой и производственной партией, поскольку все эти факторы могут асимметрично влиять на скорость сторожевого генератора и системных часов. Эту ошибку было очень сложно найти!

Вот обновленный код, который позволяет избежать этой проблемы...

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

Дополнительная wdrинструкция сбрасывает сторожевой таймер, поэтому, когда следующая строка потенциально переключается на другой прескалер, это гарантирует, что время ожидания еще не истекло.

Это также можно исправить, объединив биты WD_CHANGE_BIT и WDE в WD_CONTROL_REGISTER, как это предлагается в таблицах данных...

; 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

... но для этого требуется больше кода и дополнительный временной регистр. Поскольку сторожевой счетчик сбрасывается, когда он отключен, дополнительный сброс ничего не стирает и не имеет непреднамеренных побочных эффектов.

Я также хотел бы поблагодарить вас, потому что, когда я пошел проверять список проблем avr-libc, кажется, вы (предположительно вы) уже отправили его туда savannah.nongnu.org/bugs/?44140
ps "josh.com" настоящий... впечатляет