AUTOR GÜNTER WOIGK

 

Im ersten Teil der Miniserie zum Raspberry Pi RP2040 im MATHEMA Blog habe ich den RP2040 und insbesondere seine programmierbaren I/Os bewertet. Diese PIOs sind innovativ und können eine Lücke zwischen reinen Microcontrollern und FPGAs füllen.
 

In diesem Teil geht es darum, auf dem Raspberry Pico mit Hilfe einer PIO eine bisher nicht unterstützte Schnittstelle zu implementieren, um ihre Möglichkeiten auszuloten.
 

Vorsicht: In diesem Artikel wird es technisch und ich werde das Programm in seiner vollen Länge von 24 Befehlen eingehend erklären.  (Lächeln)

Zuvor aber eine kurze Darstellung dieser Schnittstelle und nochmal eine Zusammenfassung der Programmierung der PIOs.

 

Das XY2-100 Laserscanner-Interface

Das XY2-100 Interface ist ein Interface für Laserscanner, wie sie in der Industrie zum Beschriften von Produkten verwendet werden, z.B. für ein Mindesthaltbarkeitsdatum oder eine fortlaufende Artikelnummer. Diese Schnittstelle ist für diesen Anwendungszweck sehr verbreitet, findet sich aber auf praktisch keinem Microcontroller. Deshalb war bisher meist ein FPGA notwendig.

Über das XY2-100 Interface wird einem X/Y-Scankopf 100.000-mal pro Sekunde eine neue Position übermittelt - daher der Name. Es gibt auch eine Version mit doppeltem Takt - das XY2-200 Interface - und Geräte mit 3 Koordinaten. Auch die lassen sich mit dem RP2040 darstellen. Der doppelte Takt ist problemlos möglich und für eine dritte Koordinate benutzt man die noch freie vierte State Machine. Bei anderen Varianten, z.B. mit 18 statt 16 Datenbits und mit Befehlen im Datenstrom, muss der Pico wahrscheinlich passen.

Das XY2-100 Interface besteht aus 4 differenziellen Leitungspaaren für CLOCK, SYNC, X-DATA und Y-DATA. Die Datenworte sind 20 Bit lang und beginnen mit der Bitfolge '001', gefolgt von der 16-Bit Position für den Scanner (MSB first) und einem Parity-Bit. Während dem Parity-Bit wird die SYNC-Leitung auf 0 gesetzt, sonst ist sie immer 1.

 

 

Signaldiagramm

 

ABBILDUNG 1: BEISPIEL MIT 2 FRAMES,

X=0x8800, 0 UND Y=0x4400, 0

 

 

Die PIOs - kurze Wiederholung

Die PIOs werden über ihre State Machines programmiert: Es gibt 2 PIO-Blöcke mit je 4 State Machines und knapp bemessenen 32 Worten Programmspeicher pro Block.

Der Befehlssatz ist mit insgesamt 9 Befehlen spartanisch. Dafür werden alle Befehle in nur einem Takt und mit bis zu 125 MHz abgearbeitet.

 

 

State Machines

 

ABBILDUNG 2: REGISTERÜBERSICHT EINER PIO STATE MACHINE

 

 

Jede State Machine verfügt über 2 "universelle" Register, dazu je ein Shift Register samt Fifo mit jeweils 4 Worten für IN und OUT, oder eine einzige Fifo mit 8 Worten, wenn nur eine Datenrichtung benötigt wird - wie in unserem Fall.

Die State Machines eines PIO-Blocks können über Statusbits untereinander kommunizieren und sich so synchronisieren oder Interrupts auslösen.

Die Befehle können auf alle GPIO-Pins zugreifen, diese aber zumeist nicht explizit adressieren. Stattdessen werden die State Machines bei der Initialisierung auf GPIO-Bereiche gemappt. Dadurch kann z.B. das gleiche Programm von mehreren State Machines abgearbeitet werden, die jeweils nur anderen Port-Pins zugewiesen sind, wie wir es für X- und Y-Data machen werden. Pro State Machine gibt es 4 solche Mappings für die Befehle IN, OUT, SET, (der Befehl MOV übernimmt je nach Quelle oder Ziel das Mapping für IN oder OUT) und für Side-Set (s.u.). Die Befehle haben zumeist eine implizite Quelle, Ziel oder Datenbreite. Die anderen Angaben werden über Bit-Felder ausgewählt. Die implizite Datenbreite ist entweder die beim Mapping eingestellte oder das Maximum von 32 Bit, z.B. bei IN oder beim Kopieren von Registern mit MOV.

 

Es gibt insgesamt 9 Befehle:

 

Jeder der Befehle kann 3 Sachen gleichzeitig machen:

 

Ein PIO-Assemblerbefehl sieht dann beispielsweise so aus:

 


mov pins, x side 0b01 [2]

 

Die beiden "Nebeneffekte" Side-Set und Wartetakte werden über die gleichen 5 Bit in jedem Befehl gesteuert und die Zuordnung wird für jede State Machine fest programmiert. Man kann z.B. 2 Bit für Side-Set vorsehen, dann bleiben noch 3 Bit für bis zu 7 Wartetakte in jedem Befehl. Man kann die Side-Set Bits für eine State Machine auch optional machen, dann wird eins der 5 Bit für diese Option benötigt und die restlichen 4 können dann entweder nur für Wartetakte oder für Side-Set plus Wartetakte verwendet werden, je nachdem was günstiger ist.

 

 

Der lange Weg zum PIO-Programm

 

Datenfluss.png

 

ABBILDUNG 3:DATENFLUSS DURCH PIO UND STATE MACHINE

 

 

Die hier vorgestellte Implementierung der XY2-100 Schnittstelle benutzt 3 State Machines einer PIO, die zwei unterschiedliche Programme abarbeiten: Eins für Clock und Sync und ein Programm für die beiden Datenleitungen. Die State Machines können sehr einfach synchronisiert werden, indem sie einfach gleichzeitig gestartet werden. Vorausgesetzt man hat sich beim Taktzyklen zählen nicht verzählt, werden sie für immer synchron laufen.

Während die Programmierung des immer gleichen Clock- und Sync-Signals recht einfach ist, bieten die beiden Datenkanäle der XY2-Schnittstelle einige Herausforderungen:

Zunächst einmal muss zu jedem Datenwort ein Parity-Bit berechnet werden. Da die Arithmetik-Möglichkeiten sehr begrenzt sind, habe ich mich schnell für eine Speicherung "im Programmfluss" entschieden: Je nachdem, ob die Parität gerade ungerade/Odd oder gerade/Even ist, wird ein anderer Programmzweig benutzt. Das führt dazu, dass die Schleife über die Datenbits effektiv doppelt vorhanden ist. Es wäre wahrscheinlich auch möglich, das X- oder Y-Register mit 0 zu laden und bei jedem gesetzten Datenbit zu komplementieren. Dann wird weniger Code gedoppelt, der einzelne Codepfad enthält aber mehr Befehle. Das führt vielleicht dazu, dass wir mit den verfügbaren Befehlen pro Takt nicht mehr auskommen und den Takt erhöhen müssten – was natürlich noch ginge, da unsere Lösung bisher nur 4 Takte pro Bit benötigt und somit mit 8 MHz läuft – von maximal möglichen 125 MHz. – Wie immer gibt es natürlich noch viele andere Wege zum Ziel.

Ich wollte alle 8 Leitungen für die 4 komplementären Leitungspaare direkt vom Pico aus ansteuern, um die negativen Signale nicht noch extern invertieren zu müssen bevor ich sie über einen 8-Bit Bustreiber mit 5 Volt zum Scanner sende. Hierfür habe ich einen 74HCT254 verwendet, da die Eingangsschwelle für HCT gut zum 3.3V CMOS-Ausgangssignal des RP2040 passt. Das bedeutet aber, dass ich die Datenbits nicht einfach aus dem Sendeschieberegister auf einen Port-Pin rausschieben kann, was ich wegen der Parity-Berechnung sowieso nicht könnte. Stattdessen wird das Bit in eines der Universalregister geschoben, getestet und dann verzweigt, um entsprechend '01' oder '10' auf die Port-Pins für das Leitungspaar auszugeben.

Hier haben wir dann noch die Wahl, ob wir das mit SET oder mit Side-Set machen: Das Clock-und-Sync-Programm benutzt SET, für die Datenleitungen benutze ich aber Side-Set, weil das parallel zur Programmflusslogik ausgeführt wird, und so Zeit und Platz spart. Für die genaue Synchronisierung der 3 State Machines muss man dabei beachten, dass Side-Set schon am Anfang eines Befehls wirkt, SET aber erst am Ende, also praktisch einen Takt später.

Die Wahl von Side-Set erwies sich noch aus einem anderen Grund als hilfreich: Sie hat eine eigene Pin-Zuordnung und machte so die Zuordnung für IN und OUT frei. Dazu gleich mehr.

 

Der Programmablauf für einen Datenkanal sieht jetzt wie folgt aus:

Während die drei Header-Bits '001' ausgegeben werden, liest die State Machine das nächste Datenwort aus der Sende-FIFO ins Sendeschieberegister, was sogar automatisch geschehen kann. Sie hat also viel Zeit die sie ab-'wartet'. Die Register der PIO sind 32 Bit breit. Unsere 16 Datenbits müssen MSB first gesendet werden, die oberen 16 Bit sind also zu viel – die werfen wir deshalb gleich weg. Die verbleibenden 16 Datenbits werden nacheinander aus dem Schieberegister gelesen, entsprechend verzweigt und mit Side-Set ausgegeben. Zuletzt wird das Parity-Bit ausgegeben, wodurch unsere Hauptschleife zwei Enden hat. Das macht den Einsatz von '.wrap' unmöglich, was uns sonst den Befehl für den Schleifen-Rücksprung gespart hätte. Für die Ausgabe des Parity-Bits mittels Side-Set benötigen wir an dieser Stelle aber sowieso einen Befehl, in den es als Nebeneffekt hineinkodiert wird.

Soweit so gut und das klappt auch wunderbar, ...

bis die FIFOs mal keine Daten enthalten.

Für diesen Fall kann man einstellen, dass das Schieberegister aus dem X-Register nachgeladen wird. Es ist in diesem Fall sinnvoll, den zuletzt ausgegebenen Wert zu wiederholen. Deshalb kopieren wir den aktuellen Wert jeweils auch in das X-Register, bevor wir ihn ausgeben.

Problem gelöst ...

solange die FIFOs immer gleichzeitig leer laufen. Tun sie aber nicht, weil die CPU sie nur nacheinander beschreiben kann. In einer Race-Condition kann also ein Kanal bereits neue Daten vorfinden während der andere seinen letzten Punkt noch einmal wiederholt. In diesem Fall sollten aber beide Kanäle ihren letzten Punkt wiederholen. Ein Informationsaustausch zwischen den State Machines ist aber nur begrenzt möglich, weil sie jeweils eigene Registersätze haben und die der anderen nicht sehen.

Eine Möglichkeit wäre, nicht 2 State Machines sondern nur eine gemeinsam für X und Y zu benutzen. Dann müsste die CPU die beiden 16-Bit-Koordinaten als ein 32-Bit-Wort in eine einzelne FIFO schreiben. Dadurch wird das Programm für die State Machine aber sehr kompliziert: Zunächst kommt sie an die oberen 16 Bit des 2. Datenwortes gar nicht so einfach heran und die Parity-Berechnung würde den Programmfluss jetzt nicht nur verdoppeln sondern sogar vervierfachen.

Die PIO bietet mit WAIT und IRQ die Möglichkeit, mehrere State Machines zu synchronisieren. Dadurch werden aber Wartetakte eingefügt und das ist das Letzte, was wir jetzt brauchen. Die Synchronisierung mit Clock+Sync, die nur durch penibles Zählen der Takte pro Befehl erreicht wurde, wäre dahin.

Jetzt wird es eng: 

Finden wir keine Lösung für das Problem, müssen wir zumindest kurzzeitig eine Abweichung um einen Frame, also um 1/100.000 Sekunde bzw. 10µs zwischen den Kanälen tolerieren und auf der CPU-Seite die FIFOs entsprechend handhaben, oder die PIO-Lösung ist gestorben.


Die PIOs bieten aber noch einige oft unerwartete Möglichkeiten, die, richtig kombiniert, auch dieses Problem lösen :-)

  1. Der Füllgrad einer FIFO kann von der State Machine in ein Register ausgelesen werden: Die State Machine kann also aktiv mit einer Verzweigung im Programmfluss darauf reagieren. Sonst kann sie die FIFO nur automatisch nachfüllen lassen, oder mit PULL 'blind' in das Schieberegister laden, hat dann aber keine Information darüber, ob Daten drin waren.
  2. Ein Register, und damit der wie oben ermittelte Fifo-Status,  kann mit MOV auf die GPIO-Pins ausgegeben werden. MOV benutzt dabei das OUT-Pin-Mapping, und OUT benutzen wir bisher nicht, weil wir Side-Set verwenden. Damit können wir den Fifo-Status mit MOV auf einen 'Synchronisierungs-'' Pin ausgeben.
  3. Wenn mehrere State Machines einen Port-Pin gleichzeitig setzen, 'gewinnt' die State Machine mit der höheren Nummer. Wir können jetzt vereinbaren, dass die FIFO für Y-DATA von der CPU immer zuletzt befüllt wird und die State Machine für Y-DATA die höhere Nummer haben muss und – voilà – gibt dieser Pin den Zustand derjenigen FIFO aus, die später gefüllt wird und in einer Race Condition diejenige ist, die noch keine Daten enthält.

 

Diesen Zustand müssen wir nun wieder einlesen und entsprechend im Programm verzweigen und das Sende-Shiftregister manuell aus der FIFO oder eben aus dem X-Register nachladen. Dafür können wir auch wieder MOV verwenden, weil wir auch das IN-Mapping noch nicht verwendet haben.

Dazu müssen für OUT und IN nicht einmal zwei unterschiedliche Pins benutzt werden, die man außerhalb des Chips verbindet, man kann auch den Zustand des OUT-Pins selbst lesen.

Was leider nicht geht, ist dafür einen der nicht herausgeführten GPIOs zu benutzen: die wurden beim Design des RP2040 soweit wegoptimiert, dass jetzt 'seltsame Sachen' passieren, wenn man das versucht.


Es kommt aber noch zu einem letzten Problem:

Der MOV-Befehl, mit dem man den Pin wieder einlesen könnte, hat zwar Bits für Quelle und Ziel, aber keine Bits zur Kodierung der Datenbreite. Diese wird von der Programmierung des PINCTRL Registers bestimmt. Dieses enthält in Bitfeldern Start und Länge für OUT, SET und SIDESET, aber nur den Start für IN. Mit MOV und Quelle=PINS und Ziel=Register werden deshalb alle 32 Port-Bits gelesen – wir können das Zielregister danach aber nur als Ganzes testen.

Deshalb wird das Bit stattdessen mit IN in das bisher auch noch nicht benutzte Input-Schieberegister geladen, weil wir hier die Anzahl der zu lesenden Bits angeben können. Danach kopieren wir das Shiftregister ins Y-Register um es testen zu können, um dann endlich entsprechend zu verzweigen und das Output-Schieberegister aus der richtigen Quelle nachzufüllen.

 

Fazit

Es wäre schwer gewesen, vor einer realen Implementierung vorherzusagen, ob das XY2-100 Interface mit einer RP2040 PIO darstellbar ist. Das hätte auch anders ausgehen können. Im Gegensatz dazu kann man bei einem FPGA ab einer bestimmten Größe sicher davon ausgehen, dass das möglich ist. Der Entwicklungsaufwand dort, wenn man nicht einen fertigen Funktionsblob verwendet, ist aber ebenfalls nicht zu unterschätzen.

 

 

Das C++-Programm

Jetzt aber endlich zur Implementierung. Die Source-Dateien sind im Anhang noch einmal verlinkt, die beiden PIO-Programme werden hier aber komplett gelistet und dann abschnittsweise erklärt.

 

Initialisierung von PIO und State Machines

Der PIO-Assembler erstellt beim Compilieren des Projektes eine Header-Datei, die in das C oder C++ Programm eingebunden werden muss. Sie enthält unsere PIO-Programme in Form von static const Arrays und einige #defines für Label und Konstanten, die in den PIO-Programmen als "public" deklariert wurden.

 

Das C++-Programm initialisiert eine PIO und darin 3 State Machines für Clock+Sync, X-Data und Y-Data, startet diese synchron und zeichnet dann eine langsam rotierende Lissajous-Figur. Beim Klick auf das untenstehende Bild öffnet sich dazu ein kurzes Video:



Lissajous Demo Image

 

-->https://www.youtube.com/watch?v=hZ327MhepIE

 

 

Die Initialisierung der PIO umfasst die Zuweisung der GPIO-Pins, was verwirrenderweise mit Hilfe einer State Machine geschieht, und das Laden der PIO-Programme.

Zur Initialisierung der State Machines gehört das Einstellen der Clock, das Laden der Programmstartadresse und das Mapping von GPIO-Pins für IN, OUT, SET und Side-Set, soweit verwendet. Die Konfiguration von wrap und der Side-Set-Bits geschieht schon in der erzeugten Header-Datei. Für die State Machines der Datenleitungen wird auch noch Breite und Schieberichtung des Output-Schieberegisters festgelegt und eingestellt, dass 'MOV STATUS' den Status der Output-Fifo liest. Außerdem wird die unbenutzte Input-Fifo der Output-Fifo zugeschlagen.

 

 

Das PIO-Programm für CLOCK und SYNC ist relativ einfach

Es erzeugt das CLOCK-Signal mit 2 MHz für eine Datenrate von 100kHz und in jedem 20. Takt einen Impuls auf der SYNC-Leitung.

 

Signaldiagramm

 

.program XY2_100_clock
.define public sm_clock 8000000

.define CLOCK_HIGH 0b01 ; pin N = 1, pin N+1 = !1
.define CLOCK_LOW 0b10  ; pin N = 0, Pin N+1 = !0
.define SYNC_HIGH 0b0100
.define SYNC_LOW 0b1000

public start:
.wrap_target
  set x, 18  ; => 19 loops 
bit_loop: 
  set pins, (SYNC_HIGH + CLOCK_HIGH) [1] 
  set pins, (SYNC_HIGH + CLOCK_LOW) 
  jmp x-- bit_loop

  set pins, (SYNC_LOW + CLOCK_HIGH)  [1] ; parity bit
  set pins, (SYNC_LOW + CLOCK_LOW) 
.wrap

 

Schritt für Schritt:

.program XY2_100_clock

 

Jedes Pio-Programm beginnt mit einer .program Deklaration und einem Namen, der in der erzeugten Header-Datei in die diversen #defines für dieses Programm eingebaut wird.

 

.define public sm_clock 8000000

 

Definition der Clock für die State Machine.

.define definiert eine Konstante. Kommt an zweiter Stelle das Schlüsselwort public, wird sie, erweitert um den Programmnamen, auch als #define in die generierte Headerdatei exportiert, in diesem Fall also als #define XY2_100_clock_sm_clock. Im C++-Programm wird es dann benutzt, um die Clock der State Machine zu konfigurieren.

 

.define CLOCK_HIGH 0b01 ; pin N = 1, pin N+1 = !1
.define CLOCK_LOW 0b10 ; pin N = 0, Pin N+1 = !0
.define SYNC_HIGH 0b0100
.define SYNC_LOW 0b1000

 

Convenience-Definition der Bitmuster, mit denen die differenziellen Leitungspaare auf '0' oder '1' gesetzt werden.

 

public start:

 

Definition des Programm-Startpunktes:
Das wird als #define XY2_100_clock_offset_start exportiert und im C++-Programm zur Konfiguration der State Machine verwendet.

 

.wrap_target

 

Das Programm benutzt 'wrap' für seine äußere Schleife. Das ist eine weitere Besonderheit der RP2040 PIOs, die den sonst nötigen Rücksprung am Ende einer Schleife einsparen kann. Da die State Machines üblicherweise ein Programm unendlich oft wiederholen, spart das nicht nur einen Befehl sondern auch einen Takt, was entscheidend sein kann.

(Glühbirne) Man kann sich das Programm wie auf einer Walze vorstellen: eigentlich hat sie gar keinen Anfang und kein Ende, sie ist nur gezwungenermaßen irgendwo aufgeschnitten. Hier wird sie wieder mit '.wrap' und '.wrap_target' zusammengeklebt, ohne einen Befehl und somit auch einen Taktzyklus einzufügen.

 

    set x, 18
bit_loop:
set pins, (SYNC_HIGH + CLOCK_HIGH) [1]
set pins, (SYNC_HIGH + CLOCK_LOW)
jmp x-- bit_loop

 

Register X wird als Schleifenzähler für die innere Schleife benutzt. Der Test am Schleifenende bewirkt, dass sie einmal mehr, also 19-mal durchlaufen wird. Die Schleife für ein Bit benötigt genau 4 Takte:

 

    set pins, (SYNC_LOW + CLOCK_HIGH) [1] 
set pins, (SYNC_LOW + CLOCK_LOW)

 

Nach der Schleife wird noch ein einzelnes Bit mit SYNC LOW für das Parity-Bit ausgegeben.
Auch dieses Bit benötigt 4 Takte: Der 4. Takt wird am Programm-Anfang in 'set x,18' verbraucht.

 

.wrap

 

Ende der äußeren Schleife: Die State Machine macht ohne Unterbrechung oben bei '.wrap_target' weiter.

 

 

Das PIO-Program für die X und Y Data Lines

Die State Machines für X-DATA und Y-DATA arbeiten das selbe Programm ab, lediglich ihr Side-Setting wurde im C++-Programm auf andere GPIO-Pins gemappt.
 

Auch diese State Machines laufen mit 8 MHz, haben also pro Bit 4 Takte zur Verfügung.

 



Programmfluss

 

ABBILDUNG 4: PROGRAMMFLUSS DER STATE MACHINE FÜR EINE DATA LINE

 

 

Pro Daten-Frame werden 3 Header-Bits '001', 16 Datenbits und ein Parity-Bit mit gerader Parität gesendet. Während der immer gleiche Header gesendet wird, synchronisieren die beiden State Machines ihre Entscheidung, ob sie das nächste Wort aus der FIFO lesen oder den letzten Wert wiederholen. Während die Datenbits gesendet werden, verzweigt die State Machine ihren Programmfluss nicht nur gemäß der zu sendenden Datenbits sondern auch gemäß der aktuellen Parität. Dem Weg durch die innere Schleife ist also immer ein bestimmter Parity-Zustand zugeordnet.

Das Schleifenende wird daran erkannt, dass das Schieberegister leer ist. Dann wird das jeweilige Parity-Bit ausgegeben. Weil die äußere Schleife so zwei Enden hat, kann 'wrap' nicht sinnvoll verwendet werden. Stattdessen wird ganz konventionell JMP benutzt.

 

Das Programm am Stück:

.program XY2_100_data
.define public sm_clock 8000000
.side_set 2 opt ; the data+ and data- pins

.define DATA_HIGH 0b01 ; pin N = 1, pin N+1 = !1
.define DATA_LOW 0b10 ; pin N = 0, Pin N+1 = !0

public start:
nop [1] ; start delay to run in sync with the clock SM
next_word:
mov pins, status side DATA_LOW ; set pin from TX fifo status
in null, 31 [1] ; clear isr
in pins, 1 ; read pin back
mov y, isr ; move bit to y for testing
jmp !y, tx_not_empty

tx_empty:
mov osr,x
jmp tx_common

tx_not_empty:
pull block
mov x, osr ; store value for repeating

tx_common:
out null,16 side DATA_HIGH [1] ; discard high bits

; send data bits until OSR empty:

loop_odd:
out y, 1 ; get next data bit
jmp !y, send_0_odd
send_1_odd:
jmp loop_end_even side DATA_HIGH ; send '1' and toggle parity
send_0_odd:
jmp loop_end_odd side DATA_LOW ; send '0' and keep parity

loop_even:
out y, 1 ; get next data bit
jmp !y, send_0_even
send_1_even:
jmp loop_end_odd side DATA_HIGH ; send '1' and toggle parity
send_0_even:
jmp loop_end_even side DATA_LOW ; send '0' and keep parity

loop_end_odd:
jmp !osre, loop_odd
nop [1]
jmp next_word side DATA_HIGH [3] ; send '1' to make parity even

loop_end_even:
jmp !osre, loop_even
nop [1]
jmp next_word side DATA_LOW [3] ; send '0' to keep parity even

 

Schritt für Schritt:

 

.side_set 2 opt

 

Wir benutzen Side-Set um die Data Lines zu setzen. Dadurch können wir den Programmfluss im 'eigentlichen' Code steuern und gleichzeitig die Datenleitungen setzen. Wir haben 2 Pins pro Kanal (DATA+ und DATA-) und wir setzen sie nicht in jedem Opcode, deshalb ist Side-Set in diesem Programm optional.

 

.define DATA_HIGH 0b01 ; pin N = 1, pin N+1 = !1
.define DATA_LOW 0b10 ; pin N = 0, Pin N+1 = !0

 

Convenience-Definition der Bitmuster, mit denen die differenziellen Leitungspaare auf '0' oder '1' gesetzt werden.

 

public start:

 

Definition eines 'public label' für den Programmstart. Dieses wird als #define XY2_100_data_offset_start exportiert.

 

  nop [1]
next_word:

 

Der Programmstart muss um zwei Takte verzögert werden, damit wir mit dem Clock+Sync Programm synchron sind. Unser Programm setzt mit Side-Set sofort – am Anfang des Befehls – das neue Bit auf die Datenleitung, während Clock+Sync zwei Takte Vorlauf hat: für 'set x,18' und noch einen, weil das dort verwendete SET erst am Ende des Befehls wirkt.

Danach das Label für den Rücksprung zum Start der äußeren Schleife.

 

  mov pins, status side DATA_LOW

 

Setze den Fifo-Synchronisierungs-Pin gemäß unseres TX FIFO Status.
Beide State Machines schreiben ihren Status gleichzeitig auf den selben Pin. Dabei 'gewinnt' die State Machine für die Y-Daten, wie oben erklärt.
Per Side-Set wird '0' ausgegeben: Das erste Bit des Headers '001'.
Das Pin-Mapping für OUT und Side-Set und die Bedeutung von 'status' wurde im C++-Programm auf 'TX FIFO Füllstand' konfiguriert.

 

  in null, 31 [1] 
in pins, 1

 

Dann lesen beide State Machines den Zustand dieses Pins wieder ein. Er enthält jetzt, wie oben erklärt, den Status der Y-Data State Machine. Um nur ein einzelnes Bit lesen zu können, benutzen wir dafür das Empfangs-Shiftregister, dessen andere 31 Bits wir zuvor löschen, indem wir sie auslesen. Dabei werden Nullen nachgeladen.
Das Pin-Mapping für IN wurde im C++-Programm konfiguriert.

  mov y, isr 
jmp !y, tx_not_empty

 

Danach kopieren wir das erfolgreich vereinzelte Bit in das Y-Register, testen es und verzweigen entsprechend.

 

tx_empty: 
mov osr,x
jmp tx_common

 

Die TX FIFO von Y-DATA ist leer und wir müssen den letzten Wert aus dem X-Register wiederholen:
Wir laden das OSR (das Output Shiftregister) aus dem X-Register nach und springen zum gemeinsamen Teil.

 

tx_not_empty:
pull
mov x, osr

 

Die TX FIFO von Y-DATA enthält Daten:
Wir laden das OSR (das Output Shiftregister) aus der FIFO nach und speichern den Wert für eine eventuelle Wiederholung im X-Register.

 

tx_common: 
out null,16 side DATA_HIGH [1]

 

Ab hier wieder gemeinsam:
Das XY2-100 Interface sendet das MSB zuerst, wir schieben also nach links. Deshalb werfen wir die oberen 16 Bits des 32 Bit breiten Schieberegisters gleich weg.
Außerdem sind seit Start 8 Takte verstrichen (zählt nach! (Lächeln)) und wir müssen die Datenleitung für das letzte Bit des '001' Headers auf '1' setzen. Deshalb gibt es jetzt wieder eine Side-Set Anweisung im Befehl.

 

Jetzt werden die 16 Datenbits gesendet.

Das Ende der inneren Schleife wird durch Abfrage des TX Shiftregister Status ermittelt.
Es gibt zwei Wege durch die Schleife, die den Status 'Parity Even' und 'Parity Odd' repräsentieren. Am Anfang ist die Parität Odd, weil im Header bereits ein einzelnes gesetztes Bit gesendet wurde. Entsprechend starten wir bei loop_odd.
 

Parity ist im Moment 'Odd':

 

loop_odd:
out y, 1
jmp !y, send_0_odd

 

Lade das nächste Bit aus dem Output Shiftregister nach Y um es dort zu testen.
Wir können das Bit nicht mit 'out pins,1' direkt auf die Datenleitungen ausgeben, weil wir 2 komplementäre Bits ausgeben müssen und dafür Side-Set benutzen. Wir testen also das Bit und verzweigen in den Pfad, der das richtige Side-Set macht und gleichzeitig so den neuen Parity-Status 'speichert'.

 

send_1_odd:
  jmp loop_end_even  side DATA_HIGH  ; send '1' and toggle parity

 

Sende '1' mittels Side-Set und verzweige in den Programmpfad für Parity 'Even', weil sich die Parität der gesendeten Einsen geändert hat.

 

send_0_odd:
  jmp loop_end_odd  side DATA_LOW  ; send '0' and keep parity

 

Sende '0' und bleibe im Zweig für Parity 'Odd'.


Das selbe Spiel, aber Parity ist im Moment 'Even':

 

loop_even:
  out y, 1 
  jmp !y, send_0_even

send_1_even:
  jmp loop_end_odd  side DATA_HIGH  ; send '1' and toggle parity

send_0_even:
  jmp loop_end_even  side DATA_LOW  ; send '0' and keep parity

 

Schleifen-End-Tests für Parity 'odd':

 

loop_end_odd:
jmp !osre, loop_odd

 

Ende der inneren Schleife:
Wir prüfen mit einem bedingten Sprung, ob das Output Shiftregister leer ("empty") ist: "!osre". Wenn nicht, springen wir zum Schleifenstart für ungerade Parität zurück. 

 

  nop [1]
  jmp next_word side  DATA_HIGH [3]  ; send '1' to make parity even

 

Alle Datenbits sind übertragen:
Wir senden als Paritätsbit eine '1', um gerade Parität herzustellen.
Zuerst müssen wir noch 2 Takte warten, weil seit dem Side-Set des letzten Datenbits erst 2 Takte vergangen sind. Dafür muss ausnahmsweise, weil sonst nichts zu tun ist, ein NOP herhalten.
Das Paritätsbit wird insgesamt 4 Takte lang ausgeben, dann erfolgt der Neustart der äußeren Schleife. (Lächeln)

 

Schleifen-End-Tests für Parity 'even':

 

loop_end_even: 
  jmp !osre, loop_even 
  nop [1]
  jmp next_word side  DATA_LOW [3]  ; send '0' to keep parity even

 

Das selbe Spiel, jetzt aber bei gerader Parität:
Wir prüfen, ob das Output Shiftregister noch Daten enthält und springen dann zum Schleifenstart für gerade Parität zurück.
Sonst senden wir als Paritätsbit eine '0', um die gerade Parität zu erhalten.
Das Paritätsbit wird insgesamt 4 Takte lang ausgeben, dann erfolgt der Neustart der äußeren Schleife.

 

(Lächeln)

 

 

 

  

 

 

Vorschau

In Kürze erscheint noch ein dritter Artikel, in dem ich auf Anwendungsmöglichkeiten der Interpolier-Einheiten des RP2040 eingehen werden.

 

Über den Autor

Günter Woigk arbeitet bei der Mathema GmbH im Bereich Embedded als Software Developer mit dem Schwerpunkt auf C und C++ Programmierung. Momentan interessiert er sich für die Möglichkeiten, die der neue Ansatz im RP2040 für kommerzielle Entwicklungen bietet.

 

Links

 

Sourcen

Die Programme in diesem Artikel sind Copyright (c) MATHEMA GmbH und werden unter der BSD-3-Clause Lizenz zur Verfügung gestellt.

 

Video

 

Quellenangaben

 

Weitere Artikel zu RP2040

news

News

contact

Kontakt

jobs

Jobs