Das Titelbild dieses Beitrags ist ein Comic von xkcd.com.

Ein weiterer xkcd-Comic, der den Punkt trifft. Allerdings ist das mit meinem neuen Hardware-Debugger ja nun Vergangenheit 😎. Vor kurzem habe ich eines meiner elektronischen Geocaching-Gadgets debuggt und war positiv überrascht, wie einfach es war, die eigenen Fehler zu lokalisieren und die richtige Lösung zu finden.

Ich hatte eine kleine Box gebaut, auf der man Tetris spielen kann. Sie sieht aus wie im folgenden Bild und verwendet Touch-Tasten, die das kapazitiven Touch-Keypad von Sparkfun basierend auf MPR121 nutzen.

Tetris Spiel

Während normalerweise alles gut funktionierte, reagierte die Box manchmal nicht auf Tastenberührungen. Das Problem war, dass manchmal bei der Initialisierung des MPR121 die anfängliche Kalibrierung falsch ist, zum Beispiel, wenn man Finger auf die Touch-Tasten legt, während der MPR121 kalibriert.

Um mit dem Problem umzugehen, habe ich Neustartcode in die Firmware eingefügt, die eine Neukalibrierung auslöst. Um einen MCU-Reset zu provozieren, verwende ich die von Atmel vorgeschlagene Methode, nämlich den Watchdog-Timer mit sehr kurzer Zeit zu aktivieren und dann eine Endlosschleife zu starten. Dies sieht wie folgt aus.

#include <avr/wdt.h>

...
wdt_enable(WDTO_15MS);
while (1);

Nun muss man darauf achten, dass man nicht in einer Neustart-Schleife landet, wenn die Bedingung, die zum Reset führt, bestehen bleibt. Das wäre ärgerlich für den Nutzer und würde auch die Batterie entladen. Man kann eine solche Neustart-Schleife verhindern, indem man eine Variable einsetzt, die Resets überlebt. Solche Variablen können wie folgt deklariert werden.

unsigned char restarts __attribute__ neu ((section (".noinit"))); 

Ich nenne solche Variablen superglobal, weil ihre Lebensdauer länger ist als die von normalen globalen Variablen. Da sie nach einem Programm-Reset nicht initialisiert werden, muss man sicherstellen, dass sie beim Einschalten der MCU einen angemessenen Wert erhalten. Grundsätzlich gibt es zwei Möglichkeiten, dies zu tun. Man kann eine spezielle 4-Byte-Variable einsetzen, in die man einen „magischen“ Wert schreibt, der signalisiert, dass die MCU gestartet worden ist.

#define MAGICVALUE 0xA6B3CCF1UL
unsigned char restarts __attribute__ ((section (".noinit"))); 
unsigned long magic __attribute__ ((section (".noinit"))); 

void setup () {
  if (magic != MAGICVALUE) {
   magic = MAGICVALUE; // signal that initialization has been done
   restarts = 0;       // initialize super-global variable
  }
...
}

Eine direktere Möglichkeit besteht darin, den Grund für den Reset im MCUSR-Register unmittelbar nach dem Programmstart zu überprüfen. Dies funktioniert jedoch nur, wenn man keinen Bootloader verwendet. Wenn man das Programm nicht mit einem Bootloader startet, benötigt man in jedem Fall eine Watchdog-Timer-Initialisierungsfunktion, da man sonst u.U. in WDT-Neustartschleifen landen kann. Also, los geht’s:

void wdt_init(void) __attribute__((naked)) __attribute__((section(".init3"))) __attribute__((used));
void wdt_init(void) {
  if (MCUSR & _BV(PORF)) // reset reason is power-up reset
    restarts = 0;
  MCUSR = 0;
  wdt_disable();
} 

Jetzt muss man nur noch die Anzahl der Neustarts überprüfen und dann eine Fehlermeldung ausgeben, wenn diese Anzahl zu hoch wird. Ich hatte ähnliche Dinge schon früher ausprobiert und sie haben immer funktioniert. Diesmal hat das Zurücksetzen der Variablen auf 0 nach einer Fehlermeldung jedoch nicht funktioniert. Ich habe dann den Hardware-Debugger dw-link eingesetzt und eine Reihe von Breakpoints gesetzt, den Wert der Variablen restarts überprüft und ziemlich leicht das Missverständnis gefunden, das ich über meinen eigenen Code (den ich vor rund einem Jahr geschrieben hatte) entwickelt hatte. Das war jetzt viel weniger schmerzhaft, als überall print-Anweisungen zu platzieren, neu zu kompilieren, usw.

Am erstaunlichsten war jedoch die Tatsache, dass der Debugger viel besser funktionierte, als ich erwartet hatte. Aus Atmels Liste der bekannten Probleme des AVR JTAGICE mkII Debuggers für debugWIRE hatte ich die Warnung „BOD and WDT resets lead to loss of connection“ in das dw-link Handbuch kopiert. Nun, es stellte sich heraus, dass ein Target-Reset die Verbindung zum Debugger überhaupt nicht beeinflusst. Wenn die Target-MCU zurückgesetzt wird, befindet sie sich immer noch im debugWIRE-Modus und wird nach dem Zurücksetzen normal gestartet. Wenn man dann die MCU asynchron mit Strg-C stoppt oder die MCU einen Breakpoint erreicht, dann sendet sie eine Break-Bedingung und den Buchstaben ‚U‘ auf der debugWIRE-Verbindung. Das ‚U‘ wird vom Debugger verwendet, um sich erneut mit der MCU zu synchronisieren. Das einzige kleine Problem ist, dass die MCU nach einem Reset den Hardware-Breakpoint vergisst. Da dieser Hardware-Breakpoint vom Debugger als ‚Joker‘ verwendet wird, kann man nie sicher sein, welcher der gesetzten Breakpoints den Hardware-Breakpoint verwendet. Wenn man also nach einem Reset in der Initialisierungsroutine mit Sicherheit stoppen will, muss man in der Setup-Routine zwei verschiedene Breakpoints setzen.

Höchstwahrscheinlich sind auch ein paar andere Warnungen in der Liste der bekannten Probleme unbegründet. Auch das Ändern des OSCCAL-Wertes, der die Taktfrequenz der MCU ändert, sollte unproblematisch sein, da der Debugger nach einem Stopp immer wieder neu synchronisiert. Vermutlich war der AVR JTAGICE mkII Debugger einfach weniger ausgereift.

Alles in allem war es für mich wahrscheinlich das erste Mal, dass ich etwas Neues mit dw-link und avr-gdb gemacht habe, das nicht dazu geführt hat, dass ich einen weiteren problematischen Punkt gefunden habe (z. B. Sprünge in ISRs beim Single-Stepping, Wegoptimieren wichtiger Debugging-Informationen oder andere lustige Überraschungen). Stattdessen funktionierte der Debugger besser als erwartet.

Views: 19