4 Bit Prozessor, Teil 4 – Erweiterungen

Ich habe einen Simulator in Java geschrieben damit man den Instruktionssatz der 4 Bit CPU einen Praxistest unterziehen kann, ohne dass man Hardware aufbauen und verändern muss. Und erste Gehversuche haben gleich ein paar Schwächen aufgedeckt.

Als ich den Entwurf gestartet habe, hatte ich die Register R1 bis R4 als general purpose Register angesehen und darum den Befehlssatz gebaut. Allerdings hätte eine 2-Adress-Maschine zu viele Bits für die Quell- und Zielregister verbraucht. Deshalb habe ich einen Akkumulator eingeführt, welcher bei Operationen mit zwei Operanden implizit den ersten Operanden enthält und somit zwei Bit spart. Dabei habe ich aber nicht ausreichend darauf geachtet, dass der Akku nun auch ein general purpose Register sein sollte, so fehlte z.B. ein Indexregister indirect load. Wenn der Akku aus der Indexregister Adresse geladen werden sollte, wurde ein temporäres Register benötigt und die beiden Befehle ld R1 [IX1] und ld A R1. Das ist jetzt nach der Erweiterung nicht mehr notwendig, man kann direkt ld A [IX1] ausführen.

Weiterhin waren die Indexregister für streaming Aufgaben gedacht – man kann sie hoch- oder runterzählen. Wenn man sie aber für einen Zugriff innerhalb einer Struktur verwenden will, dann muss man auf eine Basisadresse einen Offset aufaddieren. Das war nur mit Umwegen über den Speicher möglich. Nach der Erweiterung können die drei Digits des Indexregisters einzeln in den Akku übernommen werden und nach der Berechnung wieder eingefügt werden.

Noch eine – hoffentlich nicht fehlgeleitete – Optimierung hat sich aus den ersten Programmierversuchen ergeben. Das Carry/ Borrow Flag wurde von allen ALU Funktionen aktualisiert, auch von einem einfachen Increment oder Decrement. Das ist aber bei Berechnungen, die in einer Schleife laufen, unpraktisch.

Für eine 32 Bit Addition wird in einer Schleife mit acht Runden jeweils das nächste Digit aufaddiert und dabei der Überlauf – das Carry Flag – in der nächsten Runde mit einbezogen. Allerdings finden nach der Addition noch inc und dec Aktionen statt zur Kontrolle der Schleife und zum Weiterzählen der Wertezeiger. Diese hatten in der ursprünglichen Version das Carry Flag verändert, es hätte also irgendwo geeignet zwischengespeichert und wieder abgefragt werden müssen. Dadurch, dass inc/ dec das Carry Flag nicht mehr verändern, spart man diesen Aufwand. Ich hoffe nicht, dass mir diese Entscheidung später auf die Füße fällt, aktuell sehe ich aber keine Probleme.

Im User Interface gibt es auch ein paar Erweiterungen. Zeilen mit einer besonderen Bedeutung werden nun farblich hervorgehoben.

Sprungziele werden Türkis markiert, lokale Sprünge Orange und Subroutine Aufrufe Grün. Fehlerhafte Zeilen werden Rot eingefärbt. Somit kann man im Assembler-Code schneller bestimmte Stellen auffinden.

4 Bit Prozessor, Teil 3 – Hardwaresimulation

Es wird immer noch kein Lötkolben heraus geholt. Ich beginne damit, einzelne Teile zu simulieren. Da ich nur wenig Erfahrung mit dem Entwurf digitaler Schaltungen habe, ist nicht klar, ob die Teile auch so funktionieren wie sie sollen. Fehlersuche und Korrektur sind in der Simulation wesentlich einfacher als bei der echten Hardware. Und wenn sich eine Vorgehensweise als nicht realisierbar herausstellt, hat man weniger Verluste.

Als erste Übung – insbesondere zur Einarbeitung in das Simulationsprogramm – habe ich mit der Addition/ Subtraktion angefangen. Es gibt zwar eine komplette ALU als TTL Baustein, die ist aber nur schwer zu bekommen und deckt nur 90% meines Bedarfs ab. Deshalb habe ich mich entschlossen die ALU komplett selber aufzubauen.

Die 4 Bit Addition ist einfach, es gibt hierzu mit dem 74LS83 einen fertigen Baustein. Und mit geringen Zusatzaufwand kann man daraus auch eine Subtraktion machen. Hierzu muss man nur das zweite Eingabewort negieren (nicht einfach invertieren). Invertieren ist einfach mit XOR Gattern zu erreichen. Zum negieren muss aber dann noch eins abgezogen werden. Das geht aber auch überraschend einfach indem man das carry in ebenfalls invertiert (in diesem Rahmen auch als borrow zu bezeichnen). Zudem muss bei der Subtraktion dann auch noch das carry out invertiert werden.

Das scheint soweit zu funktionieren. Bei der Addition wird die Summe bei aktiven carry in um eins erhöht und bei einer Summer größer als 15 (F Hexadezimal) wird das carry out aktiv. Bei der Subtraktion wird die Differenz bei einem carry in (oder borrow) um eins erniedrigt und sobald der Wert negativ wird, wird carry out aktiv. Die weiteren Funktionen der ALU werde ich in der kommenden Woche simulieren, diese sind jedoch noch einfacher und sollten keine Probleme verursachen.

Mein zweiter Test nimmt das vier Arbeitsregister ins Visier. Sie benötigen einen input port zusammen mit einer zwei Bit Adresse zur Auswahl des Registers welches beschrieben werden soll und einen output port ebenfalls mit einer zwei Bit Adresse zur Auswahl des Ausgabewerts. Hier setze ich auf ein 74LS173 – ein 4 Bit flankengesteuertes Latch. Zur Unterstützung der Simulation habe ich noch einen 4 Bit counter zur Erzeugung des Eingabewerts sowie zwei 2 Bit counter zur Auswahl des Schreib- und des Leseports eingefügt.

Zum Lesen wird die Adresse des Registers über SelOut angelegt und die Leitung OutEna(ble) aktiviert. Im inaktiven Zustand sind alle Ausgänge der vier Register hochohmig. Erst mit der Aktivierung wird über einen 2 nach 4 Demultiplexer das gewählte Register freigeschaltet und gibt seinen Inhalt aus.

Das Schreiben funktioniert ähnlich. Es wird die Schreibadresse über SelIn angelegt und die Leitung InLatch aktiviert. Mit der steigenden Flanke wird dann der aktuelle Wert von DataIn in das gewählte Register übernommen. Alle anderen Register bleiben unverändert. Leider wird bei der Bildschirmaufzeichnung die Mausbewegung nicht mit gespeichert, deshalb kann man nur schlecht verfolgen, was passiert. Es wird zuerst eine 1 in das Register 0, 2 in Register 1, 3 in Register 2 und 4 in Register 3 geschrieben. Danach werden die Inhalte der Register 0 bis 3 nacheinander angezeigt.

Als nächstes müssen die Ausgänge des Registerblocks und des Akkumulators auf die ALU geschaltet werden sowie das Ergebnis der ALU Operation wiederum in den Registerblock eingespeichert werden. Das wird die Aufgabe für die nächsten Tage werden.

4-Bit Prozessor, Teil 2 – Assembler und Simulator

Wenn man sofort anfängt seine Ideen in Hardware zu realisieren, hat man große Probleme, wenn sich Teile der Gedanken als nicht realisierbar oder unpraktisch erweisen. Deshalb habe ich als allererstes einen Simulator mit eingebauten Assembler geschrieben.

Mein Assembler ist offensichtlich stark vom Z80 beeinflusst. Das ist die CPU mit der ich als Jugendlicher zuerst in Berührung gekommen bin. Allerdings gibt es ein paar Vorgehensweisen die erheblich von normaler Assemblerprogrammierung abweichen.

  1. Ein Label für ein Sprungziel steht immer alleine (abgesehen von Kommentaren) und wird durch ein Doppelpunkt abgeschlossen. Funktionen, die per Call aufgerufen werden, haben ein Label welches durch ein fn (für function) eingeleitet werden. Nur solche Labels können von beliebiger Stelle aufgerufen werden. Normale Label werden automatisch „Funktions-lokal“. Das heißt, dass man nicht darauf achten muss, dass alle internen Sprungziele unterschiedliche Namen bekommen. Es ist also erlaubt, dass das Label „loop:“ in mehreren Funktionen verwendet wird und der Assembler verwendet dann jeweils die lokale Version.
  2. Da Label eindeutig durch einen Doppelpunkt gekennzeichnet werden, können OpCodes direkt in der ersten Spalte beginnen. Es ist nicht notwendig mit einem TAB anzufangen – aber es ist erlaubt.
  3. Alle Werte sind Hexadezimal.
  4. Da es keinen Linker gibt, wird auch unterschiedliche Code- und Datenbereiche verzichtet. Es gibt den Pseudobefehl org zur Festlegung des Startpunkts der nachfolgenden Befehle und eine data Anweisung welche nachfolgende Speicherinhalte kennzeichnet.
  5. Push und Pull Befehle gibt es nicht pro Register, sondern es gibt jeweils einen Befehl und dazu eine Registerliste mit den betroffenen Registern.

Der Simulator ist als JavaFX Anwendung realisiert. Er lädt direkt die Assemblerdatei und erzeugt daraus den Maschinencode, löst alle absoluten und relativen Sprungadressen auf und initialisiert den Speicher und die internen Register.

Load: öffnet einen Dateiauswahl Dialog welcher auf die Extension .zw4 voreingestellt ist. Es wird jeweils das zuletzt verwendete Verzeichnis voreingestellt. Falls es eine Funktion mit dem Label main: gibt, wird der PC im Simulator so voreingestellt, dass diese direkt ausgeführt wird.

Reload: lädt die aktuelle Datei neu. Es gibt keinen eingebauten Editor, zum Bearbeiten der Assemblerdatei muss ein externer Editor verwendet werden. Es reicht dann, nach einer Änderung, ein Speichern im Editor (er muss nicht geschlossen werden) und ein Klick auf Reload aus um den Simulator zu aktualisieren.

Quit: beendet den Simulator.

Step: liest den aktuellen PC und führt den nächsten Befehl aus. Der PC kann im User Interface zu beliebigen Zeiten geändert werden und der neue Wert wird beim nächsten Step verwendet. Das ist hilfreich, wenn man im Programm ein paar Schritte zurückspringen möchte, oder um sich aus einer toten Schleife zu befreien. Auch die übrigen Eingabefelder werden beim weiteren Programmablauf verwendet wenn sie manuell verändert wurden. Während des Programmablaufs wird das Textfenster mit dem Assemblercode automatisch aktualisiert, so dass der aktuelle Code sichtbar bleibt.

Run und Stop: führt das Programm aus bis es auf einen Breakpoint trifft oder mit Stop angehalten wurde. Einen Breakpoint kann man setzen indem man in der Programmliste auf die Adresse der jeweiligen Zeile klickt. Das Addressfeld wird dann in Folge rot angezeigt. Durch einen weiteren Klick wird der Breakpoint wieder gelöscht.

Funktions: diese Drop Down Box enthält eine Liste aller definierten Funktionen. Durch Anklicken wird der PC mit der jeweiligen Adresse initialisiert und die Assembleranzeige so gescrollt das diese Stelle sichtbar ist.

Registerliste: hier werden alle internen Registerwerte angezeigt. Die Registerwerte können auch manuell geändert werden und diese Werte werden dann beim nächsten Step verwendet. Umgekehrt werden die geänderten Werte nach dem Step angezeigt. Diese Liste wird auch im Run Modus aktualisiert, eine manuelle Änderung während des Programmlaufs ist deshalb praktisch unmöglich, da die Liste im Millisekunden Rahmen aktualisiert wird.

Speicheranzeige: der Simulator zeigt ein Speicherfenster von 256 Digits an. Die Startadresse kann frei eingestellt werden. Diese Anzeige wird auch während des Programmlaufs aktualisiert. Eine manuelle Änderung ist nicht vorgesehen.

Assembleranzeige: hier wird der Programmcode angezeigt. Die Stelle des aktuellen PCs wird markiert und bei Bedarf das Fenster so gescrollt, dass sie sichtbar ist. Die Anzeige wird auch während eines Programmlaufs mit Run aktualisiert. Falls der Assemblercode Fehler enthält, werden diese ebenfalls hier in der jeweiligen Codezeile angezeigt.

In einem ersten Schritt wird der Simulator dazu verwendet sich selber zu debuggen. Hierzu habe ich verschiedene kleine Assemblerfunktionen geschrieben, die die einzelnen Befehle aufrufen und prüfen, ob das gewünschte Ergebnis erzielt wurde. Im Augenblick habe ich ca. ein Viertel der Befehle getestet und dabei mehrere Abweichungen zwischen dem generierten Maschinencode und der simulierten CPU gefunden. An dieser Stelle sind Korrekturen jedoch viel einfacher als wenn sie erst auffallen nachdem die Hardware zusammengelötet wurde.

Aktuell umfasst der Simulator ca. 65 kB Programmcode. Es sind aber noch ein paar Erweiterungen geplant, wie z.B. die Unterstützung von Makros. Zudem wird der Assemblercode nur rudimentär auf Fehler geprüft. Hier muss auch noch weiterer Aufwand getrieben werden.

4-Bit Prozessor, Teil 1 – Projektbeschreibung

Wie viele aus meiner Generation habe ich oft darüber nachgedacht einen eigenen Prozessor zu implementieren. Das hat keinen praktischen Nutzen, es geht lediglich darum eine Vielzahl von Techniken zu erlernen. Man muss sich einen sinnvollen Befehlssatz überlegen, die notwendigen Entwicklungswerkzeuge implementieren, eine Prozessorarchitektur entwerfen und letztendlich das Teil auch tatsächlich realisieren. Die ersten Schritte sind für mich als Softwareentwickler relativ einfach. Der letzte Teil ist für mich die größte Herausforderung. Ich kann im Augenblick noch nicht absehen, wie weit ich tatsächlich komme und das Projekt wird sicherlich eine Weile dauern.

Ich habe mich für einen 4-Bit Prozessor entschieden damit die Hardware möglichst überschaubar bleibt. Da er am Ende ohnehin keinen sinnvollen Nutzen hat, ist es auch egal wie leistungsfähig er ist. Hauptsache ist, dass er läuft.

Der Befehlssatz ist an eine RISC Architektur angelehnt. Das heißt, es gibt logische und arithmetische Funktionen nur auf den Registern, keine direkte Speichermanipulation. Es ist aber kein echter RISC Befehlssatz, da ich variable lange Befehlssequenzen habe. Um die Codewörter möglichst kurz zu halten, habe ich zwei Beschränkungen gewählt. Zum einen gibt es nur vier Register (zwei Bit für die Registercodierung) und zum anderen wird mit einem Akkumulator gearbeitet, der bei Zwei-Operanden Befehlen implizit verwendet wird. Die meisten (alle?) Ein-Operanden Befehle können aber auch auf den Registern ausgeführt werden.

Im Überblick eine bullet point Liste mit den geplanten Eigenschaften:

  • 4 Bit Operationen (im Wesentlichen)
  • Von Neumann Architektur
  • 4 Register plus Akkumulator
  • 2 Indexregister
  • 1 Stackpointer
  • 12 Bit Adressen für PC, SP, Indexregister
  • 1 8 Bit Zero Page Register, analog zum Direct Page Register im 6809, 4 Bit Offset bei Verwendung
  • Befehlslänge 2 bis 5 Digits, jeweils 4 Bit

Die Anzahl der Adressierungsarten ist aus Gründen der einfachen Hardware beschränkt.

  • Immediate – nur für Ladevorgänge
  • Absolute – Laden, Speichern und Sprünge (Jump und Call)
  • PC Relative – nur für Branch, mit Condition Code, 8 Bit signed offset
  • ZP Relative – Laden und Speichern, 4 Bit lower digit (ZP liefert die oberen 8 Bit)
  • Indexregister – nur Laden und Speichern
  • Register – Register Transfer

In einem ersten Schritt wird ein Assembler für den geplanten Befehlssatz und ein Simulator zur Ausführung entwickelt. Die Ausführung im Simulator hat den Vorteil, dass man viel schneller prüfen kann, ob der Befehlssatz sinnvoll eingesetzt werden kann. Zudem sind Testdurchläufe viel schneller auszuführen und ein debugging des Verhaltens wesentlich einfacher.

Der Befehlssatz sieht vorläufig so aus:


00rr 0000 o1        ld R1..4 zero page 4 bit offset
00ix 0001 o1        ld IX1 IX2 A ZP 4 bit offset
00rr 0010           ld A from R1..4
0000 0011 v1 v2     ld ZP imme 8 bit value
0001 0011 v1        ld A imme 4 bit
0010 0011           ld ZP low digit from accu
0011 0011           ld ZP high digit from accu
00rr 0100 v1 v2 v3  ld R1..4 from abs address
00ix 0101 v1 v2 v3  ld IX1 IX2 A SP from abs address
00rr 0110 v1        ld R1..4 imme 4 bit value
00ix 0111 v1 v2 v3  ld IX1 IX2 SP ZP 12 / 8 bit imme value
00rr 1000           ld R1..4 from [IX1]
00rr 1001           ld R1..4 from [Ix2]
00rr 1010
00rr 1011

01rr 0000 o1        st R1..4 zero page 4 bit offset
01ix 0001 o1        st SP IX1 IX2 zero page 4 bit offset
01rr 0010           st A to R1..4
01?? 0011
01rr 0100 v1 v2 v3  st R1..4 to abs address
01ix 0101 v1 v2 v3  st IX1 IX2 A SP to abs address
01?? 0010
01?? 0011
01rr 1000           st R1..4 to [IX1]
01rr 1001           st R1..4 to [Ix2]
01rr 1010
01rr 1011

1000 d1 d2 d3       jmp 12 bit address
1001 d1 d2 d3       call 12 bit address
1010 cc d1 d2       bra 8 bit signed offset
1011 ????

11rr 0000           add
11rr 0001           sub
11rr 0010           addc
11rr 0011           subb
11rr 0100           and
11rr 0101           or
11rr 0110           xor
11rr 0111           inc R1..4
11rr 1000           dec R1..4
11ix 1001           inc IX1 IX2 A ZP
11ix 1010           dec IX1 IX2 A ZP
1100 1011           not A
1101 1011           neg A
1110 1011           stc set carry
1111 1011           clc clear carry
11rr 1100           cmp
11?? 1101
1100 1110 m1 m2     push m1 mask R1..4, m2 mask IX1 IX2 A ZP
1101 1110 m1 m2     pull
1100 1111           clr A
1101 1111           
1110 1111           ret
1111 1111           nop

Im Augenblick sind ca. 200 von den 256 möglichen Codewörtern belegt. Aber es werden vermutlich noch ein paar Befehle hinzu kommen. Da geplant ist, die Ausführung über einen Microcode Sequenzer zu steuern, ist es aus technischen Gründen nicht besonders wichtig, dass der Code möglichst symmetrisch ist – das ist eher eine Frage der Ästhetik. Die Befehle werden alle durch die beiden ersten Digits bestimmt, die nachfolgenden 0 bis 3 Digits sind dann nur noch Parameter, wie z.B. Adressen oder Offset Werte. Das Microcode ROM wird einfach 256 Einsprungpunkte erhalten. Die beiden letzten Aktionen einer Sequenz bestehen darin, die beiden Codeteile für den nächsten Befehl in das Code Register zu laden.

Nach einem Reset wird das Code Register mit dem jmp Befehl (1000 1101) und der Program Counter mit ffe initialisiert. Der Microcode dazu lädt dadurch den Startpunkt für das Programm aus der Adresse ffd aus (die letzten drei Digits) und springt an diese Adresse.

Vermutlich wird es im Laufe des Projekts hier noch die eine oder andere Änderung geben. Es ist halt ein Lern- und Forschungsprojekt welches nicht vorab vollständig geplant werden kann.

Arduino delay() considered harmful

Das Arduino Blinky Programm ist das Hello World der embedded Anwender. Es wird auch von Entwicklern verwendet, die sich gut in C oder C++ auskennen – es dient als Test für die Entwicklungsumgebung und Anbindung an den jeweiligen Microcontroller. Für Einsteiger trägt es aber ein hohes Risiko, dass diese frühzeitig auf einen falschen Pfad geleitet werden.

Das Blinky Programm enthält zwei delay() Aufrufe mit einer erheblichen Wartezeit:

void loop() {
  digitalWrite(LED_BUILTIN, true);
  delay(500);
  digitalWrite(LED_BUILTIN, false);
  delay(500);
}

In diesem einfachen Fall ist das noch nicht mal falsch oder problematisch. Aber es ist einer der wenigen Sonderfälle in denen so ein delay() vernünftig einsetzbar ist. Der Sonderfall hier: es gibt nur einen einzigen Handlungsstrang, jede Sekunde wird eine LED aus- und wieder eingeschaltet. Soweit so gut. Aber wie sieht es aus, wenn eine zweite LED mit halber Zeitverzögerung geschaltet werden soll:

void loop() {
  digitalWrite(LED_1, true);
  digitalWrite(LED_2, true);
  delay(250);
  digitalWrite(LED_2, false);
  delay(250);
  digitalWrite(LED_1, false);
  digitalWrite(LED_2, true);
  delay(250);
  digitalWrite(LED_2, false);
  delay(250);
  digitalWrite(LED_1, false);
}

Und das ist noch einfach. Wie es aussieht, wenn die Zeit für die zweite LED 144 Millisekunden beträgt, mag ich gar nicht hinschreiben. Man sieht, dass man mit delay() schnell in eine Sackgasse gerät wenn es mehrere Handlungsstränge mit unterschiedlichen Zeitanforderungen gibt.

Ein erster Gedanke wäre der Einsatz eines real time operating systems (RTOS) und mehreren Threads. Das ist aber ein größeres Unterfangen. Zum Einen muss man sich in das System einarbeiten. Zum Anderen belegt jeder Thread nicht unerhebliche Ressourcen. Insbesondere bei den kleinen Microcontrollern ist der RAM Bereich oft sehr begrenzt, sodass sich der Einsatz nicht unbedingt lohnt.

Eine flexiblere Lösung in C

Eine naheliegende Lösung wäre z.B., dass man für jede Aktion eine Zeitvariable anlegt, die angibt, wann sie das nächste Mal ausgeführt werden soll.

void loop() {
  unsigned long now = millis();

  if (now > nextLed1) {
    nextLed1 = now + 500;
    digitalWrite(LED_1, led1State);
    led1State = !led1State;
  }

  if (now > nextLed2) {
    nextLed2 = now + 250;
    digitalWrite(LED_2, led2State);
    led2State = !led2State;
  }
}

Jetzt kann man leicht mehrere Handlungsstränge mit beliebigen Zeitverhalten ausführen. Die delay() Funktion wird gar nicht mehr verwendet, jede Teilaktion ist also in sehr kurzer Zeit abgeschlossen und wird erst wieder nach Ablauf der angegeben Zeit wieder aktiv. Aus meiner Sicht ist das für kleinere Projekte und insbesondere für Anfängerprojekte durchaus eine sinnvolle Vorgehensweise. Wenn das Projekt wächst, bekommt man im Laufe der Zeit aber eine unschöne Kette von if Abfragen in der loop Schleife.

Eine Variante für C++

Wenn man sich entschlossen hat, das Projekt nicht in C, sondern in C++ zu erstellen, bietet sich die Möglichkeit an, die Handlungsstränge jeweils in eine eigene Klasse zu packen. Der Konstruktor der Klasse meldet den Strang dann selbständig in der loop Schleife an. Die Aktion wird in einer Methode hinterlegt. Ich habe einen Rahmen hierzu erstellt, welcher einfach als #include tmux.hpp in das Programm eingebunden werden kann. Für jeden Handlungsstrang legt man dann eine Klasse an, die die jeweiligen Aktionen enthält.

https://github.com/Matthias-Thiele/TMux/tree/main

Der Rahmen für so einen Strang kann in der Entwicklungsumgebung als Template hinterlegt werden und somit leicht eingefügt werden:

class Handlungsstrang: public TMWorker {
  private:

  public:
    void setup() {
    }

    void loop() {
    }

} strang1;

Die Klasse muss im Anschluss einen sinnvollen Namen bekommen und die setup Methode mit dem Code zur Initialisierung (analog zur Setup() Funktion im Arduino) gefüllt werden. In der loop() Methode werden dann die Befehle ausgeführt, die zu dem gewünschten Zeitpunkt ausgeführt werden sollen. Der lokale Status wird im private: Abschnitt gespeichert. Blinky sieht dann so aus:

class LEDBlink: public TMWorker {
  private:
    bool m_LEDstate;

  public:
    void setup() {
      setDelay(500);
    }

    void loop() {
      digitalWrite(LED_BUILTIN, m_LEDstate);
      m_LEDstate = !m_LEDstate;
    }

} ledBlink;

In der setup Methode wird die Wartezeit von 500 Millisekunden voreingestellt, die zwischen zwei Aufrufen eingehalten werden soll. In der loop Methode wird dann die LED ein- oder ausgeschaltet, der Zustand wird über die Membervariable m_LEDstate bestimmt.

Das ist erst mal deutlich mehr Code als in der einfachen Variante mit den Zeitvariablen. Der Vorteil dieser Lösung ist, dass die einzelnen Handlungsstränge besser voneinander entkoppelt sind und auf den ersten Blick sichtbar wird, was zum jeweiligen Handlungsstrang gehört. Und der größte Teil des Codes kann durch ein Template automatisch eingefügt werden. Bei einfachen Aktionen kann man die Klassen direkt in der main.cpp Datei unterbringen. Wenn die Aktionen komplexer werden und deutlich mehr Code benötigen, kann es sinnvoll sein, diese in jeweils eigene Dateien aufzuteilen. Diese Aufteilung sorgt dann ebenfalls dazu, dass schneller sichtbar wird, was zu der jeweiligen Aktion dazu gehört oder nicht.

Die Basisklasse TMWorker kümmert sich im Konstruktor darum, dass jede Instanz dieser Klasse in einer Liste gespeichert wird. Diese ist als Array und nicht als eine variabel lange Liste angelegt, da ich keinen Heap verwenden möchte. Das Array enthält nur Zeiger auf die Instanzen, ist also nicht besonders groß und kann bei Bedarf verlängert werden. Die Liste wird in einer Variablen tmux gespeichert, welche automatisch durch das include der hpp Datei angelegt wird. Ein Verständnis dieser Internas ist aber nicht notwendig zur Verwendung der Klasse.

Auf der Arduino Seite muss in der loop Schleife nur noch die Methode tmux.loop() aufgerufen werden und in der Arduino setup Methode tmux.setup(). Die loop Methode durchläuft die Liste der registrierten Handlungsstränge und führt diese nacheinander aus.

void loop() {
  tmux.loop();
}

Weitere Funktionen

Falls die Zeiten zwischen den Aufrufen nicht konstant sind, kann die nächste Ausführung in der loop() Methode explizit eingestellt werden. Die geänderte Wartezeit gilt dann für alle nachfolgenden Aufrufe bis zur nächsten Änderung.

class LEDBlink: public TMWorker {
  private:
    bool m_LEDstate;
    int  m_actDelay = 250;

  public:
    void setup() {
      setDelay(m_actDelay);
    }

    void loop() {
      digitalWrite(LED_BUILTIN, m_LEDstate);
      m_LEDstate = !m_LEDstate;

      // Wartezeit bei jedem Durchlauf um 100 Millisekunden verlängern
      m_actDelay += 100;
      setDelay(m_actDelay);
    }

} ledBlink;

Ebenfalls kann man eine Startverzögerung einstellen, die dafür sorgt, dass der erste Aufruf der Aktion erst später stattfindet. Das kann z.B. sinnvoll sein, wenn sich der Zustand des Systems erst stabilisieren soll bevor eine Messung ausgeführt wird. Hierfür gibt es die Methode setStartupDelay():

    void setup() {
      setDelay(250);
      setStartupDelay(3000);
    }

Externe Ereignisse per Interrupt einbinden

Das Arduino Framework unterstützt die Verwendung von Interrupts zur schnellen Reaktion auf externe Ereignisse. Die Interrupt-Routine sollte aber möglichst kurz gehalten werden, andernfalls kann es Störungen in anderen Teilen des Programms oder des Frameworks geben. Falls die Aktion etwas aufwändiger ist, kann es deshalb sinnvoll sein, die Aktion als eine TMWorker Klasse (genau genommen – von TMWorker abgeleitete Klasse) mit ‚unendlicher‘ Wartezeit anzulegen. In der Interrupt Routine wird dann nur die Ausführung getriggert, sie ist also in sehr kurzer Zeit wieder beendet. Die Kopplung erfolgt dabei über die Methode attachWorker() welche das Worker Objekt mit einer Interrupt-Quelle verbindet.

class InterruptButton: public TMWorker {
  public:
    void setup() {
      pinMode(PA8, INPUT_PULLUP);
      attachWorker(0, PA8, CHANGE);
    }

    void loop() {
      Serial1.println("Button 2 changed.");
    }
} interruptButton;

In der setup Methode wird der Pin PA8 als Input mit pullup Widerstand definiert. Mittels attachWorker wird dieses Objekt als Interrupt-Quelle 0 zum Port PA8 angemeldet. CHANGE definiert, dass der Interrupt bei jedem Pegelwechsel ausgeführt werden soll. Vordefiniert sind drei Interrupt-Quellen (0, 1 und 2), falls weitere benötigt werden, muss die hpp Datei angepasst werden. In der setup Methode sieht man keinen Aufruf von setDelay() – das führt dazu, dass die default Wartezeit ‚unendlich‘ verwendet wird. Die Aktion soll schließlich nur beim Auftreten einer Port Änderung ausgeführt werden.

Damit die Aktion zu einem Interrupt möglichst schnell ausgeführt wird, speichert das tmux Objekt den Handlungsstrang mit dem letzten aktiven Interrupt und führt diesen beim nächsten loop als erstes aus, auch wenn er in der normalen Reihenfolge noch nicht dran wäre.

Verwendung einer State Machine

Im einfachsten Fall führt ein Handlungsstrang periodisch eine Aktion immer wieder aus. In der Praxis ist es aber oft komplizierter. In Abhängigkeit von der Vorgeschichte sollen unterschiedliche Aktionen ausgeführt werden. Hierfür werden gerne endliche Automaten (State Machine) eingesetzt. Sie sind einfach zu implementieren und können Handlungsketten gut und übersichtlich abbilden. So eine State Machine setzt sich im Wesentlichen aus zwei Informationen zusammen: welche Zustände gibt es und welche Übergänge zwischen den Zuständen sind möglich.

Eine einfache Möglichkeit der Implementierung ist ein switch Statement über alle Zustände. Dazu kann man zur besseren Lesbarkeit ein enum definieren, welche alle Zustände mit sprechenden Namen versieht. Im folgenden möchte ich es anhand einer einfachen Ampelsteuerung zeigen. Das enum bildet die Zustände Warten, Bereit zum Fahren, Fahren, Halten erwarten ab. Zusätzlich gibt es eine Initialisierung, dabei ist es eine Geschmacksfrage, ob man diesen Code in den Konstruktor legt oder in einen eigenen Zustand. Wenn es Übergänge gibt, die eine erneute Initialisierung erfordern (z.B. ein Reset im Fehlerfall), dann würde ich die Initialisierung immer in die State Machine übernehmen.

enum TrafficLightStates {
  INIT,
  STOP,
  PREPARE_GO,
  GO,
  PREPARE_STOP
};

In der Klasse TrafficLight wird eine lokale Member Variable angelegt welche den aktuellen Zustand speichert – oder genauer gesagt: den Zustand, der beim nächsten Schritt ausgeführt werden soll. Sie wird mit INIT belegt, damit wird die Ampel dann in den Grundzustand gesetzt.

class TrafficLight: public TMWorker {
  private:
    TrafficLightStates nextState = INIT;

In der setup Methode werden die Microcontroller Ausgänge für die LEDs gesetzt. An dieser Stelle soll die State Machine noch ohne Verzögerung zum nächsten Zustand übergehen, setDelay(0) übernimmt diese Aufgabe.

    void setup() {
      pinMode(TLED_RED, OUTPUT);
      pinMode(TLED_YELLOW, OUTPUT);
      pinMode(TLED_GREEN, OUTPUT);
      setDelay(0);
    }

In der loop() Methode gibt es dann das switch Statement über den nextState.

    void loop() {
      switch(nextState) {

Für jeden Zustand aus den TrafficLightStates gibt es dann ein case Label innerhalb des switch Statements. In dem INIT Zustand werden alle Lichter dunkel geschaltet und als nächster Zustand wird Halten (STOP: Rot) gewählt.

        case INIT:
          digitalWrite(TLED_RED, false);
          digitalWrite(TLED_YELLOW, false);
          digitalWrite(TLED_GREEN, false);
          nextState = STOP;
          break;

Im STOP Zustand wird die rote LED eingeschaltet. Da dieser State normalerweise aus der Gelb-Phase kommt, muss die gelbe LED noch abgeschaltet werden. Wenn man auf Nummer Sicher gehen will, kann man auch die grüne LED noch abschalten, sie ist an dieser Stelle aber immer aus.

        case STOP:
          Serial1.println("RED");
          digitalWrite(TLED_RED, true);
          digitalWrite(TLED_YELLOW, false);
          setDelay(5000);
          nextState = PREPARE_GO;
          break;

Die Rot-Phase soll 5 Sekunden dauern, das wird mittels setDelay(5000) erreicht. Nach diesen 5 Sekunden soll die Ampel in die Rot-Gelb Phase übergehen, der nextState wird deshalb auf PREPARE_GO gesetzt. Wichtig: nicht das break am Ende jedes Zustands vergessen!

So werden nach und nach alle Zustände implementiert. Die komplette State Machine könnte dann so aussehen:

int TLED_RED = PA_10, TLED_YELLOW = PA_11, TLED_GREEN = PA_12;

enum TrafficLightStates {
  INIT,
  STOP,
  PREPARE_GO,
  GO,
  PREPARE_STOP
};

class TrafficLight: public TMWorker {
  private:
    TrafficLightStates nextState = INIT;

  public:
    void setup() {
      pinMode(TLED_RED, OUTPUT);
      pinMode(TLED_YELLOW, OUTPUT);
      pinMode(TLED_GREEN, OUTPUT);
      setDelay(0);
    }

    void loop() {
      switch(nextState) {
        case INIT:
          digitalWrite(TLED_RED, false);
          digitalWrite(TLED_YELLOW, false);
          digitalWrite(TLED_GREEN, false);
          nextState = STOP;
          break;

        case STOP:
          digitalWrite(TLED_RED, true);
          digitalWrite(TLED_YELLOW, false);
          setDelay(5000);
          nextState = PREPARE_GO;
          break;

        case PREPARE_GO:
          digitalWrite(TLED_YELLOW, true);
          setDelay(1000);
          nextState = GO;
          break;

        case GO:
          digitalWrite(TLED_RED, false);
          digitalWrite(TLED_YELLOW, false);
          digitalWrite(TLED_GREEN, true);
          setDelay(4000);
          nextState = PREPARE_STOP;
          break;

        case PREPARE_STOP:
          digitalWrite(TLED_YELLOW, true);
          digitalWrite(TLED_GREEN, false);
          setDelay(1000);
          nextState = STOP;
          break;
      }
    }
} trafficLight;

Das Schreiben der Aktionen direkt in das switch Statement ist nur sinnvoll, wenn die Zahl der Zustände klein bleibt und die jeweiligen Aktionen überschaubar sind. Andernfalls bekommt man ein langes switch Statement welches unübersichtlich ist und schnell zu Fehlern führt. In diesem Fall sollte man jede Zustandsaktion in eine eigene Methode auslagern und aus dem switch Statement heraus nur die jeweilige Methode aufrufen.

    void loop() {
      switch(nextState) {
        case INIT:
          processInit();
          break;

        case STOP:
          processStop();
          break;

        case PREPARE_GO:
          processPrepareGo();
          break;

        case GO:
          processGo();
          break;

        case PREPARE_STOP:
          processPrepareStop();
          break;
      }
    }

Jetzt hat man schon wieder zusätzlichen Code geschrieben, dafür aber den Vorteil, dass die Aktionen der Zustände deutlicher getrennt werden. Im richtigen Leben sollte man die einzelnen Teile natürlich auch mit Dokumentation versehen. Die komplette Ampelsteuerung sieht dann so aus:

int TLED_RED = PA_10, TLED_YELLOW = PA_11, TLED_GREEN = PA_12;

enum TrafficLightStates {
  INIT,
  STOP,
  PREPARE_GO,
  GO,
  PREPARE_STOP
};

class TrafficLight: public TMWorker {
  private:
    TrafficLightStates nextState = INIT;

    void processInit() {
      digitalWrite(TLED_RED, false);
      digitalWrite(TLED_YELLOW, false);
      digitalWrite(TLED_GREEN, false);
      nextState = STOP;
    }

    void processStop() {
      digitalWrite(TLED_RED, true);
      digitalWrite(TLED_YELLOW, false);
      setDelay(5000);
      nextState = PREPARE_GO;
    }

    void processPrepareGo() {
      digitalWrite(TLED_YELLOW, true);
      setDelay(1000);
      nextState = GO;
    }

    void processGo() {
      digitalWrite(TLED_RED, false);
      digitalWrite(TLED_YELLOW, false);
      digitalWrite(TLED_GREEN, true);
      setDelay(4000);
      nextState = PREPARE_STOP;
    }

    void processPrepareStop() {
      digitalWrite(TLED_YELLOW, true);
      digitalWrite(TLED_GREEN, false);
      setDelay(1000);
      nextState = STOP;
    }
    
  public:
    void setup() {
      pinMode(TLED_RED, OUTPUT);
      pinMode(TLED_YELLOW, OUTPUT);
      pinMode(TLED_GREEN, OUTPUT);
      setDelay(0);
    }

    void loop() {
      switch(nextState) {
        case INIT:
          processInit();
          break;

        case STOP:
          processStop();
          break;

        case PREPARE_GO:
          processPrepareGo();
          break;

        case GO:
          processGo();
          break;

        case PREPARE_STOP:
          processPrepareStop();
          break;
      }
    }
} trafficLight;

Hier befindet sich die GitHub Seite zu der hpp Datei:

https://github.com/Matthias-Thiele/TMux/tree/main

Für PlatformIO habe ich ein Snippet erstellt, welches man sich in der CPP Snippet Datei hinterlegen kann. Wenn man tmuxw (für TMux Worker) eintippt und Enter drückt, wird ein komplettes Worker Termplate angelegt.

	"TMux Task":{
		"prefix": "tmuxw",
		"body": [
			"class $1: public TMWorker {",
			"  private:",
			"",
			"  public:",
			"    void setup() {",
			"    }",
			"",
			"    void loop() {",
			"    }",
			"",
			"} $0story1;",
			""
		],
		"description": "Task Multiplexer action template"
	}

Ein 6809 Minimalsystem

Als der 6809 veröffentlicht wurde, war die Begeisterung groß, auch bei mir. Da ich mit dem Eurocom 1 ein 6802 (nicht 6502) System hatte, war ohnehin eine gewisse Nähe gegeben. Ich wollte unbedingt so ein System haben.

Allerdings waren sie in der Anfangszeit kaum zu bekommen und wenn doch, dann viel zu teuer. Als Student habe ich mir dann Anfang der 80er eine 6809 CPU gekauft und wollte damit ein System analog zum 6809 aufbauen. Allerdings hatte ich konkurrierend auch ein Z80 CP/M System aus Fertigplatinen aufgebaut und auch viel Software dazu, die ich für den 6809 nicht bekommen hätte (oder eben auch wieder viel zu teuer). Deshalb ist das Projekt eingeschlafen und hat mehr als 40 Jahre in der Bastelkiste geschlummert.

Anfang des Jahres ist mir die CPU beim Aufräumen wieder in die Finger gefallen und ich habe beschlossen, dieses Projekt endlich mal anzugehen. Nicht als dauerhaftes System, sondern als ein einfacher Breadboard Computer um mal ein wenig mit dem 6809 zu spielen.

Glücklicherweise ist das heute viel einfacher als vor 40 Jahren. Man muss nicht für jeden Versuch ein EPROM brennen und in das System einstecken. Ich habe einen Arduino Nano verwendet, der bei Bedarf die 6809 CPU in den Halt Zustand versetzt und dann direkt auf einen RAM Baustein zugreift. Das ganze System besteht (abgesehen vom Arduino) und aus der CPU, einem 32k RAM Baustein, einem Adressdekoder und einem Multiplexer. Dieser wurde notwendig, da der 6809 das E Signal, welches für den Speicherzugriff benötigt wird, im Halt Zustand nicht hochohmig macht. Der Arduino ist über eine serielle Verbindung mit dem PC verbunden auf dem ein spezielles Terminalprogramm läuft welches den RAM Bereich schreiben und lesen kann. Allerdings wäre der Zyklus Edit – Assemble – Upload – Test immer noch recht aufwändig wenn man die ersten Schritte auf einem neuen System macht. Der Test auf dem Breadboard wäre auch umständlich, da man kaum Logging- oder andere Ausgabemöglichkeiten hat.

Aus diesem Grund habe ich mir auch einen 6809 Simulator besorgt. Hier kann man die ersten Programme im Single Step oder mit Breakpoints ausprobieren, die Registerinhalte untersuchen oder Speicherbereiche anzeigen lassen. Wenn eine Funktion dann lief, habe ich sie auf den 6809 übertragen und geprüft, ob sie auf der realen Hardware auch noch korrekt ausgeführt wird.

Ich habe so ein rudimentäres Monitor System entwickelt, welches über eine Software-Interrupt Schnittstelle Funktionen wie Textausgabe, Texteingabe, Datei öffnen, lesen, schreiben und schließen zur Verfügung stellt. Kein komplettes Betriebssystem, aber mit zusätzlichen Aufwand hätte ich dorthin kommen können. Da es aber ohnehin kein dauerhaftes Projekt sein sollte, wollte ich auch die Zeit dafür in Grenzen halten.

Auch hier stellt sich die Frage: wohin führt das Projekt. Und wieder die Antwort: nirgendwo hin. Es macht mir Spaß, mal mit einem System zu arbeiten bei dem man jedes Bit unter Kontrolle hat und das man bis in den letzten Winkel verstanden hat. Aber im praktischen Einsatz ist es mir dann doch zu reduziert. Zudem – ich bin in der Z80 Assembler Welt groß geworden. Damit ist mir der extrem reduzierte Registersatz einfach zu wenig. Die Zero Page (oder Direct Page beim 6809) ist ein kleiner Trost, löst letztendlich aber eine Vielzahl von Memory read/ write Zugriffen aus. Beim 6809 kann man relativ gut mit lokalen Variablen auf dem Stack arbeiten, aber auch diese lösen Memory Zugriffe aus. Für seine Zeit war der 6809 sicherlich ein gutes System. Allerdings kam er viel zu spät – zu diesem Zeitpunkt waren die bereits die ersten 16 Bit CPUs auf den Sprung in den Massenmarkt – und er war leider auch nicht besonders schnell. Gut im Vergleich mit anderen 8 Bit CPUs aber deutlich langsamer als die 16 Bitter.

Mein Eurocom 1 lebt wieder

Meine ersten Kontakte zu einem Computer waren in einem VHS Kurs. Der Kursleiter hatte einen Nascom 1. Im Rahmen des Kurses wurden grundlegende Programmierkonzepte gezeigt und der Z80 Befehlssatz erklärt. Wenn man etwas Glück hatte, konnte man auch einmal ein Programm mit dem Hex Monitor eintippen.

Für mich als Schüler war der Nascom zum teuer, aber einen Eurocom 1 für 350 DM konnte ich mir leisten.

Das Netzteil musste selber zusammen gelötet werden, es kam als ein Bausatz. Die CPU Platine mit Hex Tastatur und 7-Segment Display kam fertig aufgebaut an.

In den kommenden Wochen habe ich das Handbuch x-mal durchgelesen. Zu diesem Zeitpunkt kam das Monitorprogramm noch mit einem ausführlich kommentierten Listening. Ich habe mich so intensiv damit beschäftigt, dass das Handbuch im Laufe der Zeit komplett auseinander gefallen ist und die Hex Tastatur so ausgeleiert war, dass man kaum noch damit arbeiten konnte.

So etwa 1980 hat mein Vater dann einen Nascom 2 als Familiencomputer angeschafft. Tatsächlich habe vorwiegend ich damit gearbeitet, mein nächst-jüngerer Bruder auch eine Zeit lang. Der Rest der Familie hat sich nicht so sehr dafür interessiert. Dieser Rechner mit ASCII Tastatur, Bildschirmausgabe und Basic war dann viel interessanter, der Eurocom 1 geriet in Vergessenheit. Das Netzteil habe ich für andere Projekte zweckentfremdet, die Eurocom 1 Platine aber zum Glück aufgehoben.

Sie lag in einer Bastelkiste bis sie mir vor ein paar Jahren mal wieder in den Sinn gekommen ist. Ich bin aber davon ausgegangen, dass nach über 40 Jahren die EPROMS keine Daten mehr enthalten und habe nichts weiter unternommen. In diesem Jahr habe ich ein 6809 Projekt gestartet, welches ich ähnlich wie den Eurocom 1 aufbauen wollte (es ist dann aber anders gekommen, ein richtiger Assembler ist schon angenehmer als eine Hex Eingabe von Maschinensprachbefehlen). Das hat dann den Anreiz gegeben, einen Versuch zu starten, den alten Computer wiederzubeleben.

Als erstes benötigte ich ein neues Netzteil. Der Eurocom 1 verwendet 2708 EPROMs, die brauchen 3 Versorgungsspannungen (+5V, +12V und -5V) und diese müssen auch in einer bestimmten Reihenfolge anliegen. Zum Glück gibt es im Handbuch einen Schaltplan zum Originalnetzteil und dort habe ich gesehen, dass sich Eltec nicht viele Sorgen um diese Reihenfolge gemacht hat. Es reicht offensichtlich, dass sie ungefähr gleichzeitig anliegen.

Interessanterweise habe ich in meiner Sammlung auch noch den alten Original-Trafo gefunden. Die -12V werden vermutlich nur für die RS 232 Schnittstelle benötigt, der Vollständigkeit halber habe ich aber alle 4 Spannungen, wie im Original, aufgebaut.

Nächster Schritt: Eurocom 1 Platine anschließen und sehen, ob er noch etwas sagt. Das war zuerst enttäuschend, das Display blieb dunkel. Aber ein wenig Rumdrücken auf der Platine hat gezeigt, dass es sich nur um einen Wackelkontakt handelt. Also auf alle Sockel etwas Kontaktreiniger gesprüht und anschließend die gesamte Platine mit Isopropanol gereinigt. Jetzt kam zumindest schon mal die „Eurocon Control“ Meldung (ein m ist mit einem 7 Segment Display nicht darstellbar).

Weiter ging es mit der Tastatur. Sie ist ein den 40 Jahren nicht besser geworden. Viele Tasten hatten gar keine Funktion mehr und die verbleibenden haben so stark geprellt, dass man sie auch nicht mehr verwenden konnte. Zudem hat sich die Kombination von Kleber und Hautfett über die Jahre zu einer dunklen Masse entwickelt. Mein erster Plan war, die Tasten gegen neue zu ersetzen. Allerdings hatte ich keine mit einem identischen foot print. Also mussten die vorhandenen wieder Instand gesetzt werden.

Zum Glück konnte man die Tasten relativ leicht öffnen. Der Kontakt war ein einfacher Neusilber-Draht und eine winzige Tellerfeder. Beides konnte ich mit etwas Geduld wieder reinigen und die Tasten wieder zusammensetzen. Das größte Problem bestand darin, die noch winzigere Feder richtig zu platzieren ohne, dass sie wegspringt. Eine ist entkommen, jetzt ist die Taste „Y“ ohne Funktion.

Die alte Beschriftung war einfach eine Folie mit Laser-Drucker Beschriftung. Diese habe ich entfernt, die Tastenkappen gereinigt und eine neue Beschriftung ausgedruckt.

Wie geht es weiter: ich fürchte – erst mal gar nicht. So viel Spaß die Erinnerung an alte Zeiten bringt, ich möchte so reduziert nicht mehr arbeiten. Der Eurocom 1 wird wieder in einer Bastelkiste verschwinden – diesmal aber mit Netzteil – und darauf warten, dass ich irgendwann doch wieder Lust darauf bekomme.

Ein einfacher Slider für HTML Seiten

Da ich auf meiner Hauptseite viele Bilder in Galerieform habe, bin ich mit dem Standard-Slider unzufrieden. Er ist nicht einfach zu bedienen und zeigt die Bilder nicht leicht durchblätterbar und in voller Bildschirmgröße an. Ich habe mir deshalb mal einige Slider aus dem Internet angesehen und habe nicht das richtige gefunden. Zum einen arbeiten einige Slider nicht sauber mit dem Firefox Browser zusammen. Insbesondere die reinen CSS Slider machen so viele Verrenkungen, dass es mich nicht wundert, das es an allen Ecken und Kanten hakt. Ich denke, dass einige Web Entwickler nur noch auf Chrome testen. Zum anderen muss man bei fast allen Slidern die html Seite anpassen. Ich habe keinen generischen Slider gesehen, der über Parameter gesteuert werden kann.

Im Nachhinein muss ich erkennen, dass ich länger nach einem brauchbaren Slider gesucht habe als die Eigenentwicklung Zeit gekostet hat. Wichtig für mich war, dass der Slider die Bilder aus dem WordPress Media Pool anzeigen kann, er sollte nicht für jede Galerie eine eigene Web Seite benötigen und er sollte per Maus und Tastatur gesteuert werden können. Und das Wichtigste: er sollte die Bilder in voller Größe anzeigen. Ein reiner CSS Slider war für mich hingegen nicht erstrebenswert. Wegen der Parametrisierbarkeit benötige ich ohnehin JavaScript. Ein einfacher CSS Aufbau hingegen ist ein echter Vorteil.

Das komplette Projekt besteht nur aus einer Datei, sie kann von meinem Github Repository herunter geladen werden: https://github.com/Matthias-Thiele/HTML-Image-Slider

Der Grundgedanke beruht wie bei vielen Slidern darauf, dass die Bilder übereinander gestapelt werden und immer nur ein Bild angezeigt wird. Das kann man über CSS leicht mit „display: none;“ steuern. Der Image Bereich ist bei mir zum Start leer, er wird beim Aufruf der Seite aus der Parameterliste heraus gefüllt.

          <div id="imagelist">
          </div>

Der JavaScript Code dazu ist überschaubar. Die Bilder können entweder durchnummeriert kommen oder mit einem beschreibenden Namen. Es wird ein IMG Element erzeugt, der Pfad zum Bild eingefügt und das Element in die imagelist eingehangen.

for (var i = imgStart; i <= imgEnd; i++) {
        var img = document.createElement("img");
        var imgSrc;

        if (descriptionName) {
            imgSrc = this.startPath + imgName + this.description[i] + "." + imgExt;
        } else {
            var num = (i < 10) ? (pendingZero + i) : i;
            imgSrc = this.startPath + imgName + num + "." + imgExt;
        }

        img.src = imgSrc;
        if (i === imgStart) {
            img.className = "active";
        }
        listRoot.appendChild(img);
        this.items.push(img);
    }

Die Statuszeile überlagert halb-transparent den unteren Bildteil. Vielleicht erweitere ich es später mal so, dass man sie auch ausblenden kann. In den meisten Fällen wird sie aber nicht stören.

          <nav class="slider-nav">
              <button class="previous" data-key="true" style="width:52%; text-align: right">
              <span>
                  <i>&lt;</i>
              </span>
            </button>
            <button class="next" data-key="false" style="text-align: left">
              <span>
                <i>&gt;</i>
              </span>
            </button>
              <span id="counter" style="float:right; width: 100pt; padding: 2pt;">1/6</span>
          </nav>
        </div>

Die Bild-Weiterschaltung läuft im Kreis. Wenn man am letzten Bild angekommen wird, wird als nächstes das erste Bild angezeigt und umgekehrt. Damit man die Orientierung nicht verliert und X-mal im Kreis läuft, wird in der Statuszeile angezeigt, wie viele Bilder es gibt und auf welchem Bild man gerade steht.

            Slider.prototype.advance = function(leftRight) {
                this.items[this.count].classList.remove('active');
                this.count += (leftRight) ? -1 : 1;
                
                if (this.count < 0) {
                    this.count = this.items.length -1;
                } else if (this.count >= this.items.length) {
                    this.count = 0;
                }
                
                this.items[this.count].classList.add('active');
                this.updateView();
            };
            
            Slider.prototype.updateView = function() {    
                var status = document.getElementById("counter");
                status.innerText = " " + (this.count + 1) + " / " + this.items.length; 
                
                var desc = document.getElementById("description");
                if (this.count >= this.description.lengt || !this.description[this.count]) {
                    desc.style = "display: none";
                } else {
                    desc.innerText = this.description[this.count];
                    desc.style = "display: inline-block";
                }
            };

Die Tastatursteuerung ist einfach. Es gibt nur zwei Kommandos – vorwärts und zurück. Diese können über Pfeil links/ rechts und über Bild hoch/ runter ausgelöst werden.

            Slider.prototype.keyPress = function(event) {
                event = event || window.event;
                var slider = document.querySelector('.next').slider;
                var keyCode = event.keyCode;
                
                if (keyCode === 33 || keyCode === 37) {
                  slider.advance(true);
                } else if (keyCode === 34 || keyCode === 39) {
                  slider.advance(false);
                }
            };

Die HTML Datei enthält keine Informationen zu den Bildern. Diese wird beim Aufruf der Seite über Parameter mitgegeben. Somit kann man die Seite irgendwo hinterlegen und aus beliebigen Anwendungen heraus aufrufen. Innerhalb der Web Seite kann man optional den Start des Pfades der Quelle hinterlegen. Damit kann man erzwingen, dass ein bestimmter Bereich nicht einfach verlassen werden kann und zusätzlich ist dann der Parametersatz etwas kürzer, da dieser Teil nicht jedes mal mit angegeben werden muss.

            var slider = new Slider("imagelist", "https://mmth.de/wp-content/uploads/");

Der erste Parameter gibt die id des DIV Elements an welches die Imageliste enthalten soll und der zweite Parameter den Start der URL jedes Bildes. Man kann diesen auch auf einen Leerstring setzen (nicht einfach weglassen).

http://localhost:8080/SliderTest/slider.html?name=2023/09/&ext=jpg&desc=Mainau1~Mainau2~Mainau3~Mainau5

Folgende Parameter gibt es:

nameEnthält den festen Namensanteil des Bildes. Bei durchnummerierten Bildern wird die Nummer am Ende automatisch angefügt. Falls der Startpfad aus der html Datei noch um Unterverzeichnisse ergänzt werden muss, kommen diese hier vor den Namen. Bei Bildern deren Name aus der Beschreibung generiert wird, kann man hier den Verzeichnispfad hinterlegen.name=Mainau
name=2023/Mainau
startBei durchnummerierten Bilder wird hier die Nummer des ersten Bilds angegeben. Im Normalfall wird das 1 sein, das muss aber nicht zwingend so sein. Wenn man nur eine Teilsequenz anzeigen möchte, kann man hier auch eine höhere Zahl angeben.
Falls die Bilder nicht Bild1, Bild2, .., Bild9, Bild10 durchnummeriert sind, sondern mit fester Nummernbreite Bild01, Bild 02, .., Bild09, Bild10 bezeichnet wurden, muss man beim Startwert auch eine führende 0 einfügen: start=01
start=1
endHier wird die Nummer des letzten Bildes eingetragenend=5
extAlle Bilddateien müssen vom gleichen Typ sein, da man nur eine Extension (ohne Punkt) angeben kann. Der Name setzt sich dann aus Startpfad + Name + Nummer + Punkt + Extension zusammen.ext=jpg
descBei durchnummerierten Bildern ist der Beschreibungsteil optional. Wenn er vorhanden ist, wird die Beschreibung zu jedem Bild oben links eingeblendet. Bei Bilddateinamen aus der Beschreibung muss dieser Parameter eine Liste aller Namen der Bilddateien enthalten.desc=Haus~Auto~Boot

Test eines einfachen Ultraschall Sensors für 80 Cent

In diesem Video teste ich einen einfachen Ultraschallsensor, der bei Aliexpress gerade mal 80 Cent kostet. Die Ansteuerung ist sehr einfach und schnell (wenn man keine unnötigen Fehler macht) und die Ergebnisse sind erstaunlich gut.

#include <Arduino.h>

const int TRIG_PIN = 12;
const int ECHO_PIN = 13;
const int ITEM_COUNT = 50;

long simpleMultiMeasure();

void setup() {
  Serial.begin(9600);
  pinMode(TRIG_PIN,OUTPUT);
  digitalWrite(TRIG_PIN, LOW);

  pinMode(ECHO_PIN,INPUT);
}

void loop() {
  long duration = simpleMultiMeasure();
  long distanceMm;
 
  // convert the time into a distance
  distanceMm = duration * 100l / 582l;
 
  if (distanceMm <= 0)
  {
    Serial.println("Out of range");
  }
  else 
  {
    Serial.print(duration);
    Serial.print(" ticks, ");
    Serial.print(distanceMm);
    Serial.print(" mm");
    Serial.println();
  }
  
  delay(500);
}

long simpleMultiMeasure() {
  long sum = 0;

  for (int i = 0; i < ITEM_COUNT; i++) {
    digitalWrite(TRIG_PIN, HIGH);
    delayMicroseconds(20);
    digitalWrite(TRIG_PIN, LOW);
    sum += pulseIn(ECHO_PIN,HIGH);
    delayMicroseconds(20000);
  }

  return sum / ITEM_COUNT;
}


Ein RISC-V Test mit dem Sipeed Longan Nano Board

Ich wollte schon lange mal mit einem RISC-V Prozessor experimentieren. Mit dem Longan Nano Board für 2,50 Euro steht in Development Board zur Verfügung bei dem man nicht lange nachdenken muss. Für ein Euro Aufpreis bekommt man noch ein farbiges LCD dazu.

Falls jemand an dem winzigen Assembler Programm aus dem Video interessiert ist, kann man es hier kopieren:

.section .text
.align 2
.global toggle

.equ GPIO_PORT_B_CONTROL, 0x40010c10
.equ SET_ALL_BITS, 0x0000ffff
.equ RESET_ALL_BITS, 0xffff0000


toggle:
  li t0, GPIO_PORT_B_CONTROL
  li t1, SET_ALL_BITS
  li t2, RESET_ALL_BITS

loop:
  sw t1, (t0)
  sw t2, (t0)
  sw t1, (t0)
  sw t2, (t0)
  j loop

Startbild: RISC V prototype chip – crop of File:Yunsup Lee holding RISC V prototype chip.jpg. Wikimedia, Creative-Commons-Lizenz „CC0 1.0 Verzicht auf das Copyright“