Wiele czasu ostatnio spędziłem na walce z watchdogiem. Ten niepozorny i dość prosty mechanizm może przysporzyć sporo problemów, zwłaszcza gdy nie bierze się pod uwagę paru dość ważnych następstw jego działania. Jako, że źródła internetowe piszą o problematyce różnie i przeważnie tylko o jej fragmencie, postanowiłem zebrać w całość wszystkie znane mi pułapki i je szerzej omówić.
Watchdog po zadziałaniu się sam nie wyłączy
Watchdog jest różnie implementowany na AVRach. Te mniejsze, o ile internet nie kłamie, mają WD, który po swoim zadziałaniu wraca do domyślnego stanu, czyli wyłączenia, o ile fuse’y nie stanowią inaczej. Na tę informację dość często można natrafić. Z kolei bardziej rozbudowane scalaki, w tym ATmega328p mają tak zwanego Enhanced Watchdoga, który po swoim zadziałaniu dalej jest aktywny pomimo restartu programu. Na domiar złego po restarcie będzie on działał z najmniejszym prescalerem, dając restarty po 15ms! Z tego też powodu, jeśli przy starcie aplikacji ma on nie działać, to należy go w pierwszych liniach kodu wyłączyć, mimo że czysto teoretycznie nie został on wcześniej aktywowany.
Przykładowo, na mikrokontrolerze ATmega328p z domyślnie zaprogramowanymi fuse’ami, poniższy kod powinien objawiać się pierwszym dłuższym zaświeceniem diody, a następnie jej krótkimi mignięciami:
#define F_CPU 1000000L #include <avr/io.h> #include <avr/interrupt.h> #include <util/delay.h> #include <avr/wdt.h> void main(void) { DDRC |= _BV(PC5); PORTC |= _BV(PC5); _delay_ms(100); wdt_enable(WDTO_15MS); while(1); }
Wyłączenie Watchdoga musi nastąpić dość szybko
Datasheet mikrokontrolerów AVR definiuje procedurę zmiany konfiguracji WD jako:
- Wyzerowanie bitu restartu WD w MCUSR,
- Wstawienie w jednej operacji jedynki do pól WDE oraz WDCE,
- W przeciągu kolejnych czterech cyklów procesora przestawienie bitów WDE lub WDP z wyzerowanym bitem WDCE.
Aby ułatwić programistom życie, istnieją funkcje wdt_enable() , wdt_disable() oraz wdt_reset() . Problem w tym, że o ile funkcja wdt_reset() jest aliasem jednej instrukcji asemblerowej, to pozostałe dwie są już bardziej złożone i lubią nie działać w zależności od użytych flag optymalizacyjnych, a także od wersji nagłówków AVR – niektóre bowiem są zbugowane. Problem ten można łatwo odwołując się do rejestrów na piechotę:
MCUSR = 0; WDTCSR|=_BV(WDCE) | _BV(WDE); WDTCSR=0;
Dla pewności na czas wyłączenia WD powinny zostać wyłączone przerwania, o ile są używane. Z funkcją wdt_enable() póki co problemu nie miałem, choć pewnie to tylko kwestia czasu. Można by zastąpić czymś ją analogicznym – różnica będzie tylko w drugiej operacji na WDTCSR. W przypadku użycia wdt_disable() trzeba pamiętać o resecie MCUSR – z nieznanych mi przyczyn gotowiec tego nie robi.