PICs programmieren - von der Pike auf
Als IDE kommt MPLAB X zum Einsatz, als Zielobjekt ein PIC16F18877. Die Programmierung erfolgt über die ICSP-Schnittstelle, und beim Entwicklungsboard haben Sie die freie Auswahl (auch eine Steckplatine genügt).
Ein virtueller Analog-Digital-Wandler soll mit 32 diskreten Werten pro Schwingung „gefüttert“ werden, die wir vorher ausrechnen. Da die in Excel enthaltene Funktion sin() aber mit Werten von 0...31 nichts anfangen kann, müssen wir diese zunächst in ein brauchbares Format umwandeln. Die Generierung der Werte erledigen wir in Excel und bauen dabei einen kleinen Fehler ein. Ganz links in der Excel-Tabelle in Bild 1 finden wir eine gewöhnliche Laufvariable (Schritt). Zur Berechnung dient die Excel-Formel =A2*((2*PI())/32). Die Laufwerte reichen nun von 0 bis inklusive 2*π. In der dritten Sinuswert-Spalte wird mit =SIN(B2) der Sinus der Laufwerte berechnet. Leider verläuft diese Sinusfunktion im Wertebereich -1...+1, der DAC arbeitet dagegen nur mit positiven Werten. Wir führen mit =1+C2 eine Werttransformation durch, um den Wertebereich von -1...+1 nach 0...2 zu verschieben. Die bereinigten Werte werden schließlich mit D3*(255/2) in Richtung der DAC-Werte 0...255 transformiert und, um allzu krumme Werte zu vermeiden, mit ABRUNDEN(E10) abgerundet.
Tabellen aus dem Datenspeicher
Da sich die Werte nicht ändern, können wir sie im Datenspeicher ablegen. Für Datentabellen wird das RETLW-Mnemonic genutzt, das nach dem RETURN einen bestimmten Wert in W zurücklässt.
Aus technischen Gründen ist es empfehlenswert, dass Tabellen an bestimmten Adressen beginnen. Den Grund werden wir später erkennen - akzeptieren wir im Moment, dass die betreffende Code-Sektion mit einer Zieladresse (hier 0x200) versehen werden sollte:
dt 127, 152, 176, 198, 217, 233, 245, 252, 255, 252, 245, 233, 217, 198, 176, 152, 127, 102, 78, 56, 37, 21, 9, 2, 0, 2, 9, 21, 37, 56, 78, 102
END
dt nimmt eine Liste von Werten entgegen. Jeder Wert wird zu einem Eintrag im Programmspeicher. Das Programm ist schon kompilierbar, denn der MPLAB-Assembler betrachtet viele kritische Situationen nicht als Fehler. Wer die Ausgabe betrachtet, sieht Warnungen:
Warning[202] C:\USERS\TAMHA\MPLABXPROJECTS\CH6-DEMO1.X\NEWPIC_8B_SIMPLE.ASM 72 : Argument out of range. Least significant bits used.
Solche Fehler sollte man ernst nehmen. Die Tabelle funktioniert nämlich nicht, aber genau dies liefert erfreulicherweise Möglichkeiten für interessante Experimente.
Exkurs: Disassemblage und Konstanten
Abgesehen von Makros besteht ein direkter Zusammenhang zwischen Mnemonics und Maschinencode. Maschinencode entsteht durch Assemblage der Mnemonics – der umgekehrte Weg heisst Disassemblage.
Doppelklicken Sie im unten rechts eingeblendeten Dashboard-Tab den Eintrag Usage Symbols Disabled (Bild 2). Die IDE zeigt ein Einstellungsfenster. Markieren Sie die Checkbox Load Symbols when programming or building for production, um die Analysewerkzeuge in den Kompilationsprozess einzubinden.
Nach dem Anklicken von Apply kompilieren Sie neu - die Speicherverbrauchsanzeigen aktualisieren sich. Zudem steht die Option Window > Debugging > Output > Disassembly Listing File Project zur Verfügung, die die Disassemblage-Ansicht zeigt (Bild 3).
Die Ausgabe besteht aus sechs Spalten: Die Zahlen ganz links beschreiben die „logische“ Adresse des Worts im Programmspeicher des PICs. Die nächste Spalte zeigt die dezimale Beschreibung des Befehls. In der dritten Spalte findet sich die Disassemblage, die aus dem Hex-File entsteht (so werden beispielsweise Konstantennamen nicht aufgelöst). Die vierte Spalte gibt die Zeilennummer in der .asm-Datei an, nach dem Doppelpunkt findet sich die jeweilige Zeile, die für das weiter links befindliche Binärwort verantwortlich zeichnet.
Der zu unserer Datentabelle gehörende Code sieht folgendermaßen aus. Wer sich mit Hexadezimalwerten auskennt, sieht aber sofort, dass der Gutteil der verwendeten Zahlen viel zu klein ist:
0201 3452 RETLW 0x52
0202 3476 RETLW 0x76
0203 3498 RETLW 0x98
0204 3417 RETLW 0x17
0205 3433 RETLW 0x33
. . .
Die Ursache für das seltsame Verhalten ist eine Eigenart des Assemblers. Er kennt fünf als Radix bezeichnete Arten, um Zahlen anzuschreiben. Statt die Zahlen „einfach so“ zu schreiben, müssen vor unsere dezimale Konstanten Punkte gesetzt werden, um den Assembler zu einem korrekten Verhalten zu zwingen.
END
Tabellenzugriff
Zum Lesen der in den Programmspeicher geschriebenen Informationen muss der RETLW-Block angesprungen werden. Die Return-Anweisungen sorgen dafür, dass das W-Register (Omega) mit dem jeweiligen Parameter befüllt wird. Zum Verständnis der Operation muss die hexadezimale Startadresse 0x0200 in einen binären Wert umgewandelt werden - das Resultat lautet 0000 0000 0000 0010 0000 0000.
Bewegliche Aufrufe von Zielen im Programmspeicher erfolgen über das CALLW-Mnemonic, dessen Deklaration Bild 4 zeigt.
Wegen der Größe des Programmspeichers des PICs von 32768 Worten muss der Adresszeiger 15 Bit lang sein. Omega ist nur 8 Bit lang, weshalb wir zur Vervollständigung einer Adresse zusätzliche Bits aus dem Register PCLATH benötigen.
Zur Vermeidung von Timing-Konflikten greift der PIC auf den Wert von PCLATH nur dann zu, wenn dies von Mnemonics verlangt wird. Wir können den Wert zusammenbauen, bevor wir den Sprungbefehl abfeuern. Schreibvorgänge in PCLATH lassen den Programmzeiger unbeeindruckt. Die Initialisierung des Programms beginnt mit dem Inkrementieren des Laufwerts:
BANKSEL DAC1CON1
MOVLW B'00000001'
ADDWF PortCache, 1
Angesichts der obigen Adresse der Tabelle weiß man, wie der obere Teil des Binärworts aussieht. Man muss über das Omega-Register in Richtung von PCLATH wandern:
MOVWF PCLATH
Damit sind wir für den eigentlichen Sprung bereit. CALLW erwartet in Omega den Zielwert. Nach dem Aufruf finden wir in Omega den Rückgabewert aus der Tabelle, der in Richtung des DACs wandert:
CALLW
MOVWF DAC1CON1
CALL WAIT
Wer das Programm im vorliegenden Zustand ausführt, erlebt eine Überraschung. Der PIC „dreht durch“, bis MPLAB die Ausführung irgendwann anhält.
Die Ursache für dieses Verhalten ist, dass unsere Tabelle nur 32 Werte aufweist, wir aber 255 Werte abarbeiten. Die darauffolgenden Speicherstellen sind beliebige Instruktionen, die aus dem letzten Programmierdurchlauf liegengeblieben sind. Im Rahmen des Anwachsens des Sprungwerts finden wir uns im „unbeschriebenen“ Bereich wieder.
Der folgende Code beschränkt den Wert von PortCache mit CLRF:
SUBLW .31
BTFSC STATUS, Z
CLRF PortCache
Zu guter Letzt springen wir nach oben, um den nächsten Durchlauf zu starten:
An dieser Stelle können wir unser Programm ausführen. Doch die Wellenform in Bild 5 hat wenig mit einer Sinusschwingung gemein. Der Grund: Die in das DAC-Register geschriebenen Werte laufen von null bis 255. Der DAC kann allerdings nur mit Werten von null bis 31 etwas anfangen, was zu Chaos führt.
Tabellen aus dem RAM
Zwar könnten wir zu in Excel zurückkehren, Faktoren anpassen und eine lauffähige Tabelle erzeugen, doch diese Vorgehensweise ist nicht ratsam. Wir wollen die Anpassung der Parameter lieber direkt am Controller vorführen.
Das „Linksschieben“ eines Werts multipliziert diesen mit dem Faktor zwei, eine Rechtsschiebeoperation um ein Bit entspricht einer Division durch zwei. Da wir von 256 auf 32 kommen müssen, entspricht dies einer Division durch acht. Das lässt sich auch als Folge von drei Divisionen durch zwei beschreiben.
Beim „Kopieren“ der Werte müssen wir Zieladressen im Arbeitsspeicher berechnen. Dazu werfen wir einen Blick auf die Core-Register. PCLATH ist bekannt, nun interessieren uns INDF und FSR.
Unser PIC besitzt zwei Paare aus Pointer-und Adressregister – ein Entgegenkommen an Entwickler, die an zwei Stellen gleichzeitig arbeiten. Die INDF-Register (steht für INDirect File) sind nicht physikalisch implementiert. Sie verweisen auf die Adresse, die in den zu ihnen gehörenden FSRs (File Select Register) eingestellt wird. Stellen Sie dort eine Adresse im Arbeitsspeicher ein, so können Sie über INDF sowohl lesen als auch schreiben. Verweisen die FSR-Register in den Programmspeicher, so können sie (normalerweise) nur lesen.
Eine Besonderheit des PIC ist, dass die Auswahl zwischen Programm- und Arbeitsspeicher über das siebente Bit des oberen Adressregisters erfolgt. Ist es gesetzt, so wird der Rest der Adresse auf den Programmspeicher bezogen, wenn nicht, so adressiert es den Datenspeicher.
Wir beginnen abermals mit dem Anlegen einer Variablen. Diesmal sind insgesamt 32 Byte Speicher erforderlich - eine Allokationsgröße, die den „geteilten“ Speicherbereich der MCU überfordern würde.
Wir greifen uns Speicher in einer von Assembler auszuwählenden Bank und arbeiten mit relativer Adressierung – MPASM gewährt wegen des Fehlens von Zusatzparametern freie Wahl bei der Platzierung von DataBuffer:
LoopC res 1
PortCache res 1
udata
DataBuffer res 32
Fraglich bleiben die hohen und die niedrigen Teile der Adresse. Erfreulicherweise macht uns der Linker mit zwei wenig bekannten Operatoren die Arbeit einfach:
MOVLW high DataBuffer
MOVLW low DataBuffer
Mit diesem Wissen können wir Informationen aus dem Programmspeicher in den Arbeitsspeicher kopieren. Da Schiebebefehle nicht direkt in W arbeiten, ist eine zusätzliche Variable erforderlich:
LoopC res 1
PortCache res 1
WorkArea res 1
Bei der Arbeit mit Datentabellen ist die Anfangsbedingung wichtig. Wir laden PortCache mit 1111.1111 vor, weil die Schleife vor ihrer Verwendung inkrementiert. Der erste Durchlauf erfolgt so mit 0 - würden wir 0 eingeben, so schriebe der erste Durchlauf in die Speicherstelle 1:
. . .
MOVLW B'11111111'
MOVWF PortCache
MOVLW B'11111111'
MOVWF LoopC
Unser Programm besitzt zwei Schleifen. Die PREP-Schleife ist für die Vorbereitung der Datentabelle verantwortlich, während Work die Werte in Richtung des DAC kopiert. PREP beginnt mit einem Aufruf der Tabelle:
MOVLW B'00000001'
ADDWF PortCache, 1
MOVLW B'00000010'
MOVWF PCLATH
MOVFW PortCache
CALLW
Danach müssen wir mit drei Aufrufen von RRF Werte verschieben. Da die Schiebeoperation nur in einem F-Register erfolgen kann, müssen wir den Wert vorher in eine Ablage schreiben:
BCF STATUS, Z
RRF WorkArea, 1
BCF STATUS, Z
RRF WorkArea, 1
BCF STATUS, Z
RRF WorkArea, 1
Um das Einsammeln von Fehlern aus dem Carry-Bit zu vermeiden, wird vor jedem RRF-Durchlauf BCF STATUS, Z aufgerufen. In diesem Listing befinden sich noch zwei kleine Fehler, und zwar beabsichtigt, denn die Behebung ist interessant!
Danach müssen wir INDF auf die richtige Speicherstelle zeigen lassen. Dazu werden die Register FSR0H und FSR0L mit Adressdaten beladen. Das H-Register (High) nimmt dabei den höherwertigeren Teil auf, das L-Register (Low) den niederen Teil:
MOVWF FSR0H
MOVLW low DataBuffer
MOVWF FSR0L
INDF0 zeigt nun auf den Beginn des Speicherbereichs. Wir müssen den Offset hinzu addieren, der die jeweilige Speicherstelle identifiziert. Dabei ist nicht sicher, dass der Beginn des Feldes am Beginn einer Seite liegt. Kommt es beim Addieren des Offsets im L-Teil zu einem Überlauf, so würde der H-Teil dies nicht mitbekommen. Als Lösung dieses Problems wird der Wert des C-Bits überprüft und der Wert von FSR0H inkrementiert, wenn ein Überlauf vorliegt:
CLRC
ADDWF FSR0L
BTFSC STATUS, C
INCF FSR0H
Das Kommando CLRC (Clear Carry) ist ein Makro, das das C-Bit im Statusregister löscht. Damit ist INDF0 korrekt konfiguriert. Wir müssen den in der Work Area zwischengespeicherten Wert laden und herausschreiben:
MOVLW INDF0
Zu guter Letzt müssen wir sicherstellen, dass die Schleife weiterläuft.
SUBLW .31
BTFSS STATUS, Z
GOTO PREP
CLRF PortCache
Da der Code recht komplex ist, wäre es wünschenswert, wenn wir uns von der Korrektheit der Ausgabe überzeugen könnten.
Fehlersuche mit Assembler
Das Platzieren von Breakpoints ist theoretisch einfach. Klicken Sie in der IDE auf die Zeilennummern, um die roten Stopp-Symbole zu platzieren. Da aber der Mikrocontroller nur einen einzigen Hardware-Breakpoint verwalten kann, erscheint eine Fehlermeldung.
MPLAB verbraucht Debuggingressourcen, um das stufenweise Ausführen zu ermöglichen. Die Emulation von Breakpoints in der Software ist im Moment nicht erwünscht, weshalb wir die Fehlermeldung mit einem Klick auf No beseitigen. Da der PIC Software-Breakpoints unterstützt, können Sie dies bejahen (Yes): Sobald Sie mehr als einen Breakpoint in der Assemblerdatei platzieren, aktiviert Microchip die Funktion automatisch.
Da unser PIC nur einen Hardware-Breakpoint unterstützt, klicken Sie in der Toolbar auf den nach unten zeigenden Pfeil neben dem Debugger-Symbol und wählen Sie die Option Debug main project. MPLAB öffnet ein Disassemblagefenster, das wir sofort wieder schließen. Nach dem Erreichen des Haltepunkts präsentiert sich die IDE wie in Bild 6.
Die grün hinterlegte Zeile mit dem Pfeil links ist die aktuelle Instruktion. Stopp- und Sprungsymbole in der Toolbar erlauben die Interaktion mit dem Programmzustand. Window Target Memory Views File Registers öffnet ein Fenster, das den Speicher des PIC zeigt. Legen Sie Cursor wie Mauszeiger über die Deklaration von DataBuffer, um ein Tooltip-Fenster mit der Adresse des ersten Bytes und seinem Wert zu öffnen. Auf der Workstation des Autors lautete die Position 0xD20.
Bequemer ist es, im Fenster File Registers auf den nach unten zeigenden blauen Pfeil (GoTo) zu klicken. Wählen Sie im GoTo-What-Feld Symbol aus, und wählen Sie DataBuffer. Schließen Sie das Popup nach dem Anklicken des Go To-Buttons, um sich am in Bild 7 gezeigten Resultat zu erfreuen. Das rot hervorgehobene Kästchen ist das erste Byte der Allokation. Es ist offensichtlich, dass das vorliegende Programm nicht funktioniert, denn Werte wie FC liegen außerhalb des erlaubten Wertebereichs.
Zur Suche ist es empfehlenswert, den Speicherbereich mit einem leicht erkennbaren Pattern auszufüllen. Ein guter Versuch würde das Literal 1111.1111 in das indirekte Register schreiben:
CLRZ
ADDWF FSR0L
BTFSC STATUS, C
INCF FSR0H
MOVFW B'11111111'
MOVLW INDF0
Wer den Wertebereich im Debugger öffnet, sieht eine Sequenz desselben Werts. In den meisten Fällen dürfte er nicht FF lauten. Der Code weist einen kleinen Fehler auf. Wir nutzen den Befehl MOVLW, der den Wert einer Speicherstelle ins Omega-Register lädt:
MOVLW INDF0
Aus Sicht von MPLAB ist das Literal INDF0 eine Zahl: Nach der Kompilation sind auch Werte wie PORTA nur Zahlen. Unser Programm kopiert also die Adresse des Registers in alle 32 Speicherstellen. Trotzdem sind wir einen Schritt weiter, da wir schon das Ansprechen der Adressdaten verifiziert haben. Eine korrigierte Version des Programms sieht folgendermaßen aus:
MOVWF INDF0
Die Berechnung der Speicheradressen funktioniert, wir können den eigentlichen Rechenfehler ausmerzen. Das erste Problem war die Verwendung von MOVLW statt MOVWF, was das Schreiben in INDF außer Gefecht setzte:
MOVWF INDF0
MOVFW PortCache
SUBLW .31
BTFSS STATUS, Z
GOTO PREP
Bei Betrachtung der Ausgabe stellen wir fest, dass bei sehr kleinen Werten Einsen auftauchen. Die Ursache dieses Problems ist, dass das RRF-Mnemonic mit dem Carry-Bit arbeitet. Unser Code hat aber bisher das Z-Bit gelöscht, was zu dieser kleinen Änderung führt:
BCF STATUS, C
RRF WorkArea, 1
BCF STATUS, C
RRF WorkArea, 1
BCF STATUS, C
RRF WorkArea, 1
Damit ist das Programm einsatzbereit, die Sinustabelle erscheint im Debuggerfenster. Zur Fertigstellung müssen wir in der Arbeitsschleife dafür sorgen, dass die Werte aus dem Datenspeicher entnommen werden. Dazu ist eine Inkrementierung der Laufvariable erforderlich, um einen fortlaufenden Index zu erzeugen:
BANKSEL DAC1CON1
MOVLW B'00000001'
ADDWF PortCache, 1
Die indirekte Adressierung ist zum Lesen und zum Schreiben geeignet. Wir laden die beiden Teile der Adresse des Puffers in FSR0H und FSR0L. Danach addieren wir den Fehlbetrag und prüfen auf einen Überlauf: Tritt ein Überlauf auf, so inkrementieren wir das höhere Register:
MOVWF FSR0H
MOVLW low DataBuffer
MOVWF FSR0L
MOVFW PortCache
CLRZ
ADDWF FSR0L
BTFSC STATUS, C
INCF FSR0H
Neu ist, dass wir aus dem Register INDF0 lesen. Der Wert wandert in das Ausgaberegister des DAC:
MOVWF DAC1CON1
CALL WAIT
Der Rest des Programms ist eine gewöhnliche Schleife, die unter anderem die Inkrementierung vornimmt:
SUBLW .31
BTFSC STATUS, Z
CLRF PortCache
GOTO WORK
Damit ist das Programm fertig – die Ausgabe präsentiert sich wie in Bild 8.
Fazit
Der Artikel zeigt, dass man auf Achtbittern interessante Experimente mit Assemblerbefehlen durchführen kann. Mehr dazu finden Sie unter anderem in meinem neuen Lehrbuch „Mikrocontroller-Basics mit PIC“. Der Autor freut sich über Feedback!
(200154-01)
----------------------------------------------------------------------------------------------------------------------
Wollen Sie weitere Elektor-Artikel lesen? Jetzt Elektor-Mitglied werden und nichts verpassen!
----------------------------------------------------------------------------------------------------------------------