-
Notifications
You must be signed in to change notification settings - Fork 1
Dokumentation
In dieser Dokumentation werden zunächst Hinweise zur Verwendung des Simulators gegeben, danach werden Simulatoren im Allgemeinen vorgestellt und schließlich geht es um die Besonderheiten und Funktionen dieses PIC-Simulators.
- Der Simulator unterstützt nur LST-Dateien in UTF-8 Format.
- LST-Dateien müssen richtig formatiert sein. Der Simulator setzt dies voraus.
- Offiziell läuft der PicSim sowohl auf Windows als auch Android. Die Apk- und Exe-Datei befinden sich im Release-Tab des Repositories. Tatsächlich ließe sich der Simulator auch für macOS, Linux und iOS kompilieren lassen. Mit diesem Hintergedanken wurde der Simulator entwickelt. Dies wurde aber bisher noch nicht getestet.
Ein Simulator versucht die Realität nachzubilden. Da die Realität allerdings sehr komplex ist, muss diese abstrahiert werden. Dazu werden Modelle gesucht, welche bestimmte Spezifikationen besitzen, die wichtig für den Anwendungszweck der Simulation sind. Für den PIC-Simulator ist das Ziel schon vorgegeben. Es soll ein PIC simuliert werden.
Simulationen können unterteilt werden in Simulationen mit oder ohne der Verwendung Informationstechnik. Eine Simulationen ohne Informationstechnik ist z.B. der Auto-Crashtest, bei welchem ein Verkehrsunfall wird.[^SimulationWiki] Damit keine Menschen dabei gefährdet werden sollen, werden dafür Crashtest-Dummys verwendet. Da die Realität bei einem Crashtest sehr vereinfacht wird, ist es möglich verschiedene Autos beim Crash besser vergleichen zu können. Simulationen mit Informationstechnologien sind besser bekannt als Computersimulationen. Dazu zählen z.B. Fahrsimulatoren, Flugsimulatoren usw.
Es gibt verschiedene Gründe eine Simulationen zu entwickeln. Oft wären Test am realen System zu teuer oder ethisch nicht vertretbar. Es kann aber auch sein, dass das reale System noch gar nicht existiert (z.B. Strömungsmodelle bei einem neuen Flugzeug) oder zu komplex oder noch zu unverstanden ist (z.B. Urknall) um es an einem realen System zu testen.[^SimulationGründe]
Vorteile einer Simulation sind hauptsächlich die oben genannten Gründe (Grundsätzliches zu einem Simulator). Für den PIC-Simulator sind nur zwei Gründe wirklich relevant: Aufwand und Kosten. Durch eine Simulation werden immer gleiche Bedingungen erzeugt. Dadurch lässt sich das Programmieren mit einem PIC einfacher lernen. Denn es wird keiner Hardware aus einem Computer vorausgesetzt. Es gibt keine Hardwarefehler und der ausgeführte Programmcode lässt sich einfacher überwachen.
Da ein Simulator das Verhalten eines Systems nur vereinfacht darstellt, kann ein Simulator nicht das gesamte System repräsentieren. In diesem Fall wäre ein Emulator wesentlich besser geeignet. Dieser emuliert sowohl Software als auch Hardware.
Der Simulator soll dem realen PIC sehr ähnlich. Deswegen werden Daten möglichst in Bit Schreibweise geschrieben sein. Da Dart allerdings keine Bits bzw. Bytes als Datentypen kennt, werden alle Daten intern als Strings gespeichert. Weil Strings auch in Dart einfache Char-Arrays sind, können Bit-Befehle wie bcf
oder bsf
sehr einfach implementiert werden. Der Simulator arbeitet auch wie der PIC nur mit den Instructions. Außerdem wird der ProgramCounter (PC) im Simulator mit einem Integer-Wert dargestellt. Dadurch sind Zugriffe auf das Array des Programmcodes einfacher realisierbar. Wie beim realen Vorbild setzt sich dieser aus PCL und PCLATH zusammen.
Der Simulator besteht aus zwei Anzeigen: einer Startseite bei der ein Programm ausgewählt und geladen werden kann und einer weiteren Seite, die den eigentlichen Simulator ausmacht. Die Logik des Simulators ist weitesgehend getrennt von der Benutzeroberfläche. Dadurch ließe sich der Simulator mit geringem Aufwand in einer Kommandozeile oder mit einer anderen Benutzeroberfläche laufen lassen.
Nachdem die LST Datei ausgewählt und in den PicSim geladen wurde, zeigt die GUI durch einen Highlighter den nächsten Befehl an der abgearbeitet wird. Im Hintergrund wurden bis dahin, wie im Ablaufdiagramm zu sehen ist, die Instructions in das Programm-Array geladen, die Variablen initalisiert und das Programm wartet auf die Nutzereingabe. Sobald der Nutzer den Start-Button klickt startet das Programm mit dem ersten Befehl, analysiert die Instruction, arbeitet diesen Befehl ab, der Programm Counter wird angepasst, der Highlighter springt zum entsprechenden Befehl und der nächste Befehl wird ausgeführt. Sollte jedoch der Stop Button geklickt worden sein wartet das Programm bis zum nächsten Start, bevor der nächste Befehl ausgeführt wird. Beim Step wartet das Programm vor jedem Befehl auf die nächste Eingabe.
GUI |
Ablaufdiagramm |
Um einen PIC zu simulieren müssen dessen Komponenten softwaremäßig nachgebildet werden. Hierfür wird der Hauptspeicher mit seinen Regisstern als String Array abgespeichert. In jeder "Speicherzelle" steht ein String mit 8-Zeichen. Initialisiert werden diese Speicherzellen mit acht Nullen. Alle Speicherzellen können angeklickt und manuell in Hex- oder Binär-Schreibweise gesetzt werden. Der Wert wird zur einfacheren Lesbarkeit immer als Hex angezeigt. Intern wird jedoch ein 8-Zeichen String verwendet.
RAM-Speicher |
Das Programm wird aus der LST-Datei ausgelesen. Dabei wird die Instruction extrahiert und als Program-Array abgespeichert. Auf diese Speicherstellen zeigt der Programmcounter (PC). Dieser wird als Int-Wert realisiert welcher auf den auszuführenden Befehl zeigt. Nach jedem duchgeführten Befehl wird dieser hochgezählt. Sprungbefehle geben den absuluten Wert an an den gesprungen werden soll. Der Stack wurde wie das Programm als Array mit dazugehörigem Stack-Pointer realisiert. Dies ermöglicht, dass bei einem Überlauf über den Inhalt von 8 wieder das erste Element überschrieben werden kann. Der Stack ist mit null
initalisiert und wird bei jedem CALL
weiter gefüllt und bei jedem RETURN
wieder geleert. Der Stack wird in der GUI als Hex-Zahl angegeben, wie in der folgenden Abbildung zu sehen ist, intern werden die Zahlen jedoch als acht Zeichen langer String gespeichert.
Stack |
Um die Runtime zu ermitteln wird unsere globale Runtime Variable (int) bei jedem Befehl um die jeweilige Anzahl Cyles hochgezählt die eine Instruction benötigt. Je nach eingestellter Quarzfrequenz wird die absolute Zeit ermittelt und in der GUI angezeigt. Das FSR-Register, das W-Register, der PCL
, das PCLATH
und das Status-Register werden auch in der GUI ausgegeben. Alle sowohl als 8-Zeichen-String als auch zur einfacheren Lesbarkeit als Hex-Zahl. Die Anzeige ist in der folgenden Abbildung farblich markiert. Das wReg (W-Register) ist nicht Teil des Storages sondern wird als eigene String-Variable realisiert. Der PCL besteht aus den unteren acht Bits des ProgramCounters. Für die GUI wird dazu der Integer-Wert in einen String mit Binärzahlen konvertiert. Danach wird der String auf acht Stellen normalisiert und schließlich angezeigt. Das FSR
ist Teil des Storage und steht dort an Stelle vier. Es wird als Speicher für die indirekte Adressierung verwendet Das PCLATH
ist ebenso Teil des Storage, es steht an Stelle zehn. Verwendet wird es um die Programm-Page umzuschalten. Alle Register können manuell, durch anklicken, mit einer Hex- oder Binär-Zahl gesetzt werden.
Ebenso ist in der Abbildung die einstellbare Quarzfrequenz, die Runtime sowie der Button Runtime Reset
zu sehen. Die Runtime wird intern als Int-Variable bei jedem Befehl hochgezählt. Die Variable wird pro Cycle um eins inkrementiert. Die Cycle-Anzahl wird dann mithilfe der Quarzfrequenz in die tatsächliche Laufzeit umgerechnet und ausgegeben. Der Button Reset Runtime
setzt den Wert wieder zurück auf null.
W-Register und Laufzeitmessung |
Das Tris-Register (RA-RE) stellt die In/Out-Schnittsstellen dar. Dieses Register ist Teil des Storages an Stelle fünf und sechs. Hier werden die Pins auf Input oder Output gesetzt. Die Werte können auch manuell durch klicken auf das entsprechende Bit eingestelt werden.
Tris-Register |
Klassendiagramm |
Wie in diesem Diagramm zu erkennen ist, werden bei diesem Simulator Logik und UI strikt getrennt. Die Datei main.dart
beinhaltet die Homepage auf der eine Datei für den Simulator ausgewählt werden kann. Die Klassen dieser Datei erben alle vom Framework von Flutter, welches die UI rendert. Dasselbe gilt auch für die UI welche in der simscreen.dart
-Datei deklariert wird.
Die Logik erstreckt sich über zwei Klassen: den InstructionCycler
und den InstructionRecognizer
. Im InstructionRecognizer
werden alle Befehle definiert, die der Simulator simulieren kann. Von außen aufgerufen wird nur die recognize()
-Funktion. Diese nimmt eine Instruction und den aktuellen ProgramCounter entgegen und liefert nachdem der Befehl ausgeführt wurde den neuen ProgrammCounter zurück.
Der InstructionCycler
arbeitet etwas abstrahierter und besitzt die Funktionen start()
, stop()
und step()
, welche von der Benutzeroberfläche aufgerufen werden. Damit wird der Programmablauf gesteuert. Außerdem wird in dieser Klasse auch auf Interrupts (interrupt()
) geprüft. Auch der Timer0 (timer0()
) wird hier inkrementiert.
Da die Programmiersprache für den PIC-Simulator frei wählbar war, entschieden wir uns relativ schnell für die Sprache Dart. Dart bietet eine sehr einfache Syntax, die vergleichbar mit Typescript und Kotlin ist. Ein weiterer Grund der für Dart spricht ist das UI-Framework Flutter. Mit Flutter lassen sich sehr einfach ansprechende User Interfaces entwickeln. Da Flutter und Dart außerdem auf vielen verschiedenen Geräten laufen können, lag es nahe auch die Crossplatform-Möglichkeiten zu nutzen.
Die Flags zum Steuern des PICs werden in drei Registern gespeichert, dem Status-Register an Stelle drei im Storage, dem Option-Register an Stelle 128 und dem Intcom-Register an Stelle 11. Somit sind die Flags wie jedes Register ein String mit 8-Zeichen, wobei jedes einzelne Flag einer Stelle des Strings entspricht. In der GUI werden diese wie unten zu sehen aufgelistet. Dabei kann jeder Wert einzeln durch klicken invertiert werden. Die Funktionen der einzelnen Flags sind hier nachlesbar (ab S 15).
Flags |
Der Timer0, das spezielle Zählregister des PICs, ist auch im PicSim integriert. Anders als die normale Laufzeitzählung kann dieses Register nicht einfach pro Cycle inkrementiert werden. Damit der Timer zuverlässig funktioniert, wird eine Routine bei jedem Befehl ausgeführt. Die Routine lässt sich in drei Teile unterteilen: die Berechnung des Prescalers, das Inkremtieren des Timers und das Überprüfen auf ein Overflow.
Die Funktion für das berechnen des Prescalers des Timers ist trivial. Die Bits PS2, PS1 und PS0 werden in einen Integer-Wert konvertiert und um eins inkrementiert. Dies geschieht in Zeile drei des Listings. Danach wird die Zahl zwei mit sich selbst psa
-Mal multipliziert. Zum Schluss wird noch überprüft, ob sich der Prescaler überhaupt verändert. Dies wird über eine Variable gelöst, die den alten Prescaler-Wert zwischenspeichert.
Um den Prescaler für den Watchdog zu berechnen, dürfte zum anfänglichen psa
-Wert nicht die eins hinzuaddiert werden. Ansonsten ist die Berechnung identisch.
int calculatePSA() {
if (storage.value[129][4] == "1") return 1;
num psa = int.parse(storage.value[129].substring(5), radix: 2) + 1;
psa = pow(2, psa);
if (oldPSA != psa) {
oldPSA = psa.toInt();
psaCounter = 1;
}
return psa.toInt();
}
void timer0() {
int timerValue = int.parse(storage.value[1], radix: 2);
int i = 0;
if (storage.value[129][2] == "0") {
var psa = calculatePSA();
if (psa <= psaCounter) {
storage.value[1] = recognizer.normalize(8, timerValue + (psaCounter ~/ psa));
psaCounter = psaCounter - (psa - 1);
} else {
psaCounter++;
}
print(psaCounter);
// check for timer0 overflow
if (storage.value[1] == "00000000") {
if (storage.value[3][recognizer.statustoBit("RP0")] == "0") {
i = 11;
} else {
i = 139;
}
storage.value[i] = storage.value[i].substring(0, 2) + "1" + storage.value[i].substring(3);
}
}
}
Immer wenn der Prescaler-Wert kleiner oder gleich dem psaCounter
wird, kann das Timer0-Register um eins erhöht werden. Danach wird der psaCounter
zurückgesetzt. Allerdings nicht auf den Wert eins, sondern auf die Differenz von psaCounter
und psa-1
. Dadurch werden Befehle mit zwei Cycles berücksichtigt.
Zuletzt wird noch auf ein Timer0-Überlauf geprüft. Wird einer erkannt, wird das entsprechende Bit (T0IE) im Interrupt-Register gesetzt.
Bisher wurde nur das Timer-Interrupt in den Simulator implementiert. Die Basis ist aber für alle fehlenden Interrupts gegeben. Bei jedem Befehl den der Simulator ausführt wird immer auch überprüft, ob ein Intterupt im Interrupt-Register ausgelöst wurde. Falls dieser Fall eintritt, springt der Simulator an die vordefinierte Stelle (Stelle: 4) im Programmcode. Von dort aus wird dann die ISR ausgeführt.
bool interrupt() {
if (storage.value[11][0] == "1" && storage.value[11][2] == "1" &&
storage.value[11][5] == "1") {
recognizer.call(int.parse(storage.value[10] + storage.value[2], radix: 2), "00000000000100");
// set ISR-address
storage.value[10] = "00000000";
storage.value[2] = "00000100";
return true;
}
return false;
}
Beim Interrupt wird ein Sprung ausgeführt. Dafür wird der Programcounter auf 4 gesetzt. Denn dort startet die Intterupt Service Routine. Bevor der Sprung ausgeführt wird, muss zuerst überprüft werden, ob überhaupt ein Interrupt ausgelöst werden soll. Dies geschieht in der if-Abfrage. Dafür wird geschaut, ob im Interrupt-Register (Index: 11) die Bits für ein Timer-Interrupt (GIE, T01E, T01F) gesetzt sind.
Der Simulator beherrscht die meisten Befehle und Funktionen die ein echter PIC beherrscht. Und die dazugehörige Benutzeroberfläche ermöglicht eine einfache Bedienung für das Debugging und das Ausführen von Programmen. Natürlich fehlen noch einige Funktionen, wie z.B. der Watchdog, die restlichen Funktionen des TRIS-Registers und die restlichen Interrupts. Diese könnten mit geringem Aufwand jeweils hinzugefügt werden. Beispielsweise existiert schon das Grundgerüst für Interrupts, da der Timer0-Interrupt schon implementiert ist. So ist das auch mit dem Watchdog und dem Prescaler. Die Prescaler-Funktion hierfür existiert bereits und kann dafür wiederverwendet werden.
Die Entwicklung des Simulators hat u.A. zum Verständnis des PICs beigetragen. Dieses tiefergehende Verständnis hätte auch in einer Klausur weiterhelfen können.
Die Designentscheidung Dart und Flutter zu verwenden war richtig. So konnten die Kenntnisse mit der Programmiersprache und dem UI-Framework stark vergrößert werden. Dies kann in folgenden Projekten weiterhelfen. Durch Flutter war es auch möglich direkt von Beginn an eine funktionierende Benutzeroberfläche zu entwickeln. So fiel das Debugging der implementierten Funktionen einfacher aus als Anfangs erwartet, weil Fehler nicht immer über die Ausgabe einer Konsole gesucht werden mussten.
- Quellcode: Github
- Dokumentation: Wiki
- Letzter Release: Release v0.1.1
- Flutter: flutter.dev
- Dart: dart.dev
- Dokumentation PIC16F8x: siehe Moodle oder hier
[^SimulationWiki]: "Simulation" https://de.wikipedia.org/wiki/Simulation 27.04.2021 [^SimulationGründe]: "Simulationen" https://javainformatikblog.wordpress.com/simulationen/ 27.04.2021