# 7. Aufgabe 3 - Motorsteuerung ## 7.1. Wissen ### 7.1.1. JTAG-Debugging **Funktionsdefinition** Der Joint Test Action Group (JTAG)-Debugger ermöglicht es, Eingriffe in den Programmablauf vorzunehmen. Außerdem unterstützt er den Entwickler dabei, den Programmzustand zu inspizieren. Dazu lässt sich der Speicher auslesen und die daraus gewonnenen Informationen werden ausgewertet und zu Analysezwecken aufbereitet. Plattformen, die Multitasking unterstützen, bieten Übersichten zu laufenden Tasks. Damit wird das Überwachen der Nebenläufigkeit vereinfacht. Fortgeschrittene Debuggerprogramme bieten die Möglichkeit die Interprozesskommunikation, zum Beispiel Semaphoren und Nachrichten, auszuwerten. Der Standard in dem Bereich des Debuggings für eingebettete Systeme ist der GNU-Debugger (GDB), welcher Teil der GNU Compiler Collection (GCC) ist. Das JTAG-Interface hat den Zweck ein Verfahren zu ermöglichen, mit dem Schaltungen getestet werden können, während sie sich verlötet auf der Leiterplatte befinden [[jta](references.md)]. JTAG-kompatible Systeme haben im Normalbetrieb abgetrennte Komponenten, die erst dann aktiviert werden, wenn das JTAG-Interface genutzt werden soll. Technisch gesehen ist die Schnittstelle als Schieberegister verwirklicht. Das Zielsystem ist über das JTAG-Interface mit der Debugginghardware verbunden. Die Kommunikation zwischen der Entwicklungsplattform auf dem PC und der Debuggingplattform findet über USB statt (Abbildung 7.1). #### Quelltextansicht Im Gegensatz zu Assembler-Debugging kann der Code via High Level Language (HLL)- Debugging in der Quelltextansicht inspiziert werden (siehe Abbildung 7.2). Der Programmablaufzähler wird eingeblendet und es lässt sich nachvollziehen, an welcher Stelle im Quelltext sich das Programm gerade befindet. Diese Möglichkeiten bieten sich, weil der Compiler beim Erzeugen der Executable and Linking Format (elf)-Datei Debuginformationen hinzufügt. Diese werden von dem Debugwerkzeug interpretiert und die Assembler-Instruktionen werden den Zeilen im Quelltext zugeordnet. ![Screenshot](img/png/jtag-connection.png)
Abbildung 7.1.: Cross Debugging via Joint Test Action Group (JTAG)-Debugging ![Screenshot](img/jpg/lauterbach-aufgabe3-3.png)
Abbildung 7.2.: Quelltextansicht in der Lauterbach Umgebung #### (Single-)Stepping Das Programm kann in Einzelschritten ausgeführt werden. Dabei können entweder die Schritte der Hochsprache oder der Assembler-Ebene einzeln ausgeführt werden. Außerdem ist es möglich, die aktuelle Methode zu Ende laufen zu lassen oder in die auszuführende Unterroutine hineinzuspringen, beziehungsweise erst bei deren Rückkehr zu stoppen. Diese Möglichkeiten ergeben sich, wie auch die Quelltextansicht, aus den vom Compiler der elf-Datei hinzugefügten Debuginformationen. #### Starten und Stoppen des Programmablaufs Das Programm kann angehalten werden. Dies kann hilfreich sein, wenn man an bestimmten Stellen im Programm Variablen auslesen möchte. Das manuelle Stoppen des Programmflusses ist allerdings sehr ungenau. Daher sollten für das gezielte Anhalten des Programms Breakpoints genutzt werden. #### Haltepunkt (Breakpoint) Das Setzen eines Breakpoints beschreibt die Auswahl einer Stelle im Programmfluss, an der die Ausführung des Programms gestoppt wird, bevor der markierte Befehl ausgeführt wird. Aus [[Gra](references.md), S. 115], Vorgehen beim Debuggen mit Breakpoints: - Aufstellen einer These über die mögliche Position des Defekts - Setzen eines Haltepunkts vor der vermuteten Position - Annäherung mit Hilfe von Breakpoints / Stepping, dabei: Überprüfung des Programmzustands - These falsch / Korrigieren des Defekts Es wird zwischen Software und Hardware Breakpoints unterschieden [arm]. Erstere werden temporär in den RAM des Zielsystems geschrieben und ersetzen bis zum Eintritt des Breakpoints die ursprüngliche Instruktion. Diese wird durch eine Breakpoint-Instruktion überschrieben und die CPU geht bei der Ausführung in einen Debugstatus. Hardware Breakpoints werden durch das Überprüfen des Instruction Fetch von einer spezifischen Speicheradresse aus umgesetzt (siehe Abbildung 7.5). Im Gegensatz zu Software Breakpoints können Hardware Breakpoints auch auf Befehle aus dem ROM angewendet werden. Sollte eine Memory Management Unit (MMU) Adressbereiche neu zuordnen, so kann es zum Überschreiben von Software Breakpoints kommen. Das Setzen eines Breakpoints erfolgt über einen Doppelklick neben die Programmzeile (siehe Abbildung 7.3). ![Screenshot](img/jpg/lauterbach-aufgabe3-1.png)
Abbildung 7.3.: Setzen eines Breakpoints in Zeile 28. ![Screenshot](img/jpg/lauterbach-aufgabe3-2.png)
Abbildung 7.4.: Breakpoint-Übersichtsfenster in der Lauterbach Umgebung #### Überwachungspunkt (Watchpoint) Ein Watchpoint überwacht eine gewünschte Variable und hält die Ausführung des Programms an, wenn diese verändert werden. Die Möglichkeiten der Überwachung hängen von dem genutzten Debugprogramm ab. Möglich ist zum Beispiel die Überprüfung auf einen Wertebereich oder auf Lese/Schreibzugriffe auf eine Variable. Nicht überwachen lassen sich alle Datenströme, die an der CPU vorbei laufen. Sollten Speicherbereiche zum Beispiel durch Direct Memory Access (DMA) verändert werden, so kann dies nicht mit Watchpoints an der CPU detektiert werden. Die gesetzten Break- und Watchpoints erscheinen im Übersichtsfenster, wo auch ihr Typ näher spezifiziert ist. ![Screenshot](img/png/jtag-breakpoint.png)
Abbildung 7.5.: Hardware Breakpoint Realisierung ![Screenshot](img/jpg/lauterbach-aufgabe3-5.png)
Abbildung 7.6.: Watch-Fenster Breakpoint Realisierung #### Speicherzugriff Der Speicherzugriff ermöglicht das Auslesen des Speichers. Die Daten können in verschiedenen Formatierungen angezeigt werden. Es ist möglich den Inhalt direkt als ASCII String, hexadezimal, dezimal und binär darzustellen. #### Watch Fenster Mit Hilfe der Debuginformationen aus der elf-Datei können die, aus dem Speicher gelesenen, Daten im Watch Fenster geordnet und entsprechend der zugehörigen Datenstrukturen dargestellt werden (siehe Abbildung 7.6). Es ist möglich, sich die Daten in Arrayform oder in anderen Formatierungen anzeigen zu lassen. Konstanten werden von dem Debugger nicht aufgelöst. #### Auswertung des Call Stacks Die im Call Stack enthaltenen Daten lassen sich auswerten und eine Aufrufliste daraus rekonstruieren (siehe Abbildung 7.7). Außerdem werden beim Verlassen einer Unterroutine die lokalen Variablen auf den Call Stack gelegt und lassen sich von vielen Debuggern auswerten. ![Screenshot](img/jpg/lauterbach-aufgabe3-4.png)
Abbildung 7.7.: Watch-Fenster Breakpoint Realisierung ### 7.1.2. Beispiel: Schreiboperation auf Variablen überprüfen Wenn es zu nicht nachvollziehbaren Änderungen von Variablen kommt ist, ist es sinnvoll diese mit Watchpoints zu überwachen. Es ist möglich, dass die Variable durch einen fehlerhaften Schreibvorgang einer anderen Variable beeinflusst wird. Ein Beispiel, wie es zu solch einer Situation kommen kann, ist in dem Quellcode 7.1 zu betrachten. Die Tabelle 7.1 zeigt, dass der in der globalen Variable a gespeicherte Wert durch die Schreiboperation auf numbers[4] überschrieben wurde. Das Ergebnis der Addition in Zeile 25 des Quellcodes 7.1 ist somit falsch. ``` /∗ ∗∗ zur Veranschaulichung: ∗∗ alle Variablen global und im gleichen Speichersegment ∗/ uint8_t numbers[4]; uint8_t a = 10; uint8_t b = 15; uint8_t result ; void main (void) { /∗ ∗∗ for−Schleife mit Fehler in Abbruchbedingung ∗∗ Schreiboperation auf numbers[4] ∗∗ −> fehlerhafte Daten in Speicherbereich der Variable a ∗/ for ( uint8_t i = 0; i <= 4; i ++) { numbers[i] = i ; } /∗ falsches Ergebnis durch fehlerhafte Daten in Variable a ∗/ result = a + b; } ``` Quellcode 7.1: Beispielcode zum Überschreiben von Speicherbereichen und dadurch entstehende Folgefehler ![Screenshot](img/png/array-overflow.png)
Tabelle 7.1.: Visualisierung des Speicherinhalts bei Ausführung des Programms 7.1 ### 7.1.3. Grenzen des JTAG-Debuggings JTAG-Debugging eignet sich nur dann, wenn das System zur Erfassung des Fehlers auch pausiert werden kann. Sobald eine Analyse des Systems ausgeführt wird, wird das Zeitverhalten stark verändert, weil das System angehalten werden muss. Fehler, die auf Zeitverhalten beruhen, lassen sich damit nur schwer untersuchen. Dazu gehören auch Interrupts oder Unterbrechungen durch höherpriore Tasks. Es ist außerdem nicht möglich, im Programmablauf zurück zu gehen und sich den Hergang des Fehlers genau anzusehen. Dafür bedarf es der Aufzeichnung des Programmflusses. ### 7.1.4. Schrittmotoren Schrittmotoren können schrittweise und somit sehr genau gesteuert werden. In unserem Anwendungsfall ist die schrittweise Ansteuerung allerdings nicht wichtig. Es ist interessant, in welcher Frequenz die Schritte ausgelöst werden, denn damit wird die Geschwindigkeit des Motors geregelt. Um die Schrittmotoren einfacher ansteuern zu können, werden Schrittmotortreiber genutzt. Das Datenblatt zu dem Treiber A4988 findet sich dabei in der Quelle [[All](references.md)]. Für jeden Schritt, den der Motor machen soll, muss ein Puls an den Eingang des Schrittmotortreibers gesendet werden. Schauen Sie sich im Datenblatt zu dem Zynq-7000 [[Xil18](references.md)] das Kapitel zu dem Thema Triple Timer Counter an. Hier finden sich Informationen, wie man diese Pulse erstellen könnte, ohne dass man sich in der Software um das Zählen direkt kümmern müsste. Die Drehrichtung wird von einem Signal über GPIOs gesetzt. ## 7.2. Pre-Kolloquium Versuche Sie herauszufinden, wie man die Motoren mit Hilfe der Timer mit verschiedenen Geschwindigkeiten ansteuert. Halten Sie Ihre Ergebnisse zunächst schriftlich fest. Anregungen: - Welche Vorteile bieten Timer gegenüber dem Zählen in Software? - Ist es möglich Signale auf GPIOs zu geben, wie kann davon Nutzen gemacht werden? Können diese Signale auch von Timern erzeugt werden? - Schauen Sie sich die verschiedenen Zählmodi (Interval Mode, Overflow Mode) an: Wann startet der Timer wieder bei 0? - Finden Sie die Bedeutung von Match Value und Interval Length heraus - Wie lang muss der Puls sein? Schauen Sie sich das Datenblatt des Motortreibers an. - Wie können Sie in der Lauterbach Skriptsprache Breakpoints generieren, laden und speichern? - Wie hoch ist die Clockfrequenz des Timers? Was wäre ein passender Prescaler um c.a. 1µs pro Timer-Tick zu generieren? - Was macht die Funktion XTtcPs_CalcIntervalFromFreq? Ist es sinnvoll diese Funktion hier anzuwenden? ## 7.3. Aufgabe Schreiben Sie die Software für das Ansteuern der Motortreiber. Es soll auf einen PID Wert zwischen -1000 und 1000 reagiert werden und die Motorleistung, sowie Drehrichtung entsprechend geregelt werden. Die Motoren drehen bis zu einer Pulsfrequenz von c.a. 10kHz flüssig. Als Mindestfrequenz sollten c.a. 1kHz gesetzt werden. - Configs: - Ein Timer Tick entspricht c.a Mikrosekunde - Nutzen Sie die Timer TTC0_0 und TTC0_1 - Die GPIO-Pinnummern für die Richtungseinstellung sind 54 und 55 - Simulieren Sie einen PID-Regler indem: - Sie einen globale Variable pidValue in main.c anlegen - Sie In der Methode InitDoneCallback eine Schleife erstellen, die die globale Variable in ihrem Wertebereich hoch zählt. - Tipp: Denken Sie daran, dass Sie die Variable nicht unendlich schnell hoch zählen. - Initialisieren Sie einen Timer in der ttc_timer.c: - Erstellen Sie dafür eine init-Methode - Timer Instanz erstellen (global) ``` XTtcPs (...) ``` - Erstellen Sie eine Konfigurationsinstanz für den Timer ``` XTtcPs_Config (...) ``` - Die Konfigurationsinstanz des Timers füllen dazu die folgende Methode nutzen ``` XTtcPs_LookupConfig(XPAR_PS7_TTC_0_DEVICE_ID); ``` - Den Timer initialisieren ``` XTtcPs_CfgInitialize (...) ``` - Modus des Timers setzen (Tipp: Welcher Timermodus soll gewählt werden? Wann soll ein HIGH/LOW am Ausgang erzeugt werden? Welche Option wird benötigt, um den Match Value zu nutzen?) ``` XTtcPs_SetOptions (...) ; ``` - Prescaler setzen ``` XTtcPs_SetPrescaler (...) ``` - Match Value erstellen und setzen ``` XTtcPs_SetMatchValue(...) ``` - Intervalllänge erstellen und setzen ``` XTtcPs_SetInterval (...) ``` - Schreiben Sie Methoden zum Starten und Stoppen der soeben erstellten Timer (Hinweise finden sich in der Datei xttcps.h) - GPIO Initialisierung für das Festlegen der Drehrichtung des Motors - Erstellen Sie eine Konfigurationsinstanz für GPIOs ``` XGpioPs_Config (...) ``` - Implementieren Sie die Funktion ``` timer_gpio_Init () ``` - Füllen Sie die Konfigurationsinstanz (ähnlich wie bei der Erstellung des Timers) - Initialisieren Sie die GPIO-Instanz (Tipp: XPAR_PS7_GPIO_0_DEVICE_ID) - Definieren Sie die Richtung der Pins mit ``` XGpioPs_SetDirectionPin(gpioInstanz, Pinnummer, 1); XGpioPs_SetOutputEnablePin(gpioInstanz, Pinnummer, 1); ``` - Erstellen Sie eine Funktion motor_Set_Moving_Direction, die die Drehrichtung der Schrittmotoren in Abhängigkeit des PID Values bestimmt. Nutzen Sie dabei die Methode ``` XGpioPs_WritePin() ``` - Erstellen Sie eine Funktion timer_Set_Interval_Length, die die Interval-Länge sowie das Match-Value eines Timers setzt. - Schreiben sie eine Task-Funktion timer_Task, die die Drehrichtung der Schrittmotoren sowie deren Geschwindigkeit in Abhängigkeit des pidValue setzt. Diese soll die Timer stoppen, die GPIOs korrekt setzen, die Intervall-Längen aus dem pidValue berechnen und dann die Timer wieder starten. Erstellen Sie in ihrer main.c einen Task, der zunächst die timer_Init() Funktion aufruft und dann alle 2ms die timer_Task() Funktion. - Da ihre Methode in Abhängigkeit zum PID Wert steht, und dieser in der main.c simuliert wird, bietet es sich an die globale Variable pidValue mit extern in ihre Datei einzubinden. - Überlegen Sie sich welche Konsequenzen nebenläufiger Schreib- oder Lesezugriff auf eine Variable haben kann und wie Sie diese Effekte verhindern können. - Tipp: Nutzen Sie Critical Sections beim Zugriff auf die globale pidValue Variable. ## 7.4. Post-Kolloquium - Zeichnen Sie mit der Funktion ”iprobe.timing” die Pulse auf die Sie mit dem Timer generieren. - Wie können in Trace32 Breakpoints erstellt werden? Welchen Einfluss haben Breakpoints auf das Zeitverhalten eines Systems? - Was ist im Bereich der Programmierung eine sogenannte "Atomare Operation" und wie hängen diese mit Critical Sections zusammen?