Zeitlicher Determinismus durch Timer, ISRs und Hardware im SoC – Teil 2

Im ersten Teil unserer Reihe starteten wir mit einem einfachen Pulsgenerator auf Basis von Timer, ISR und PIO. Um aus didaktischen Gründen zwischen diesen Themen nicht allzu oft wechseln zu müssen, bleiben wir bei der PIO, in diesem Fall zur Einbindung eines Drehgebers.

Drehgeber und Stepper sind dabei mit einem Wellengetriebe 100:1 verbunden. Diese Anordnung hilft uns später als anschauliches Beispiel im Regelkreis, wir wollen eine bestimmte Position anfahren. Der Weg bis dahin ist aber noch weit, und dieser Teil dreht sich allein um die Einbindung des Drehgebers.


ACHTUNG: auch die Verwendung nur eines Drehgebers auf der getriebenen Achse des Wellengetriebes ist eine Vereinfachung aus didaktischen Gründen. Diese Vereinfachung ist für einfache Anwendungsfälle ausreichend. Sie verzichtet aber auf einen Sensor an der Motorachse, der die Rotorbewegung im Magnetfeld kontrolliert. Die Vorteile eines zweiten Drehgebers werden in einem separaten Beitrag zum Thema Motion Control beleuchtet.


Ein Drehgeber auf der getriebenen Achse hat seine Hauptfunktion im letzten Abschnitt einer Fahrt zu einer Zielposition. Er dient dort der Kontrolle zwischen vorgegebener und tatsächlicher Position. Vorher bei schneller Fahrt reichen primär die Steps.

Bedingt durch diese Aufgabe werden hier im Gegensatz zu Drehgebern auf der Achse des Steppers häufig Absolutgeber mit Bussen wie SSI, BiSS oder ganz einfach SPI gewählt.

Für unser Beispiel nutzen wir einen Hengstler ACURO AD34 mit 19 Bit Auflösung einer Umdrehung. Damit erreichen wir eine Auflösung von 0,0006866455078125 Grad, das sind rund 2,4719 Bogensekunden oder rund 0,76294 Milligon. Das mag im Alltag wenig erscheinen, im Vermessungswesen ist das bei vielen Einsatzzwecken sehr relevant.

Der Drehgeber nutzt das ACURO-Protokoll, einen Nachfolger von BiSS. Mehr Details sind in diesem Beitrag von mir zu lesen: https://thurow.de/projektbeispiel-vermessungsgeraet-teil-4/.

Sowohl IC-Haus als auch Hengstler haben hier gut durchdacht ihre Protokolle entwickelt, diese lassen sich mit wenig Aufwand sowohl in Soft- als auch Hardware abbilden. In Anlehnung an SPI generiert der Master ein Taktsignal, der Slave sendet seine Messwerte. ACURO ermöglicht sogar das Senden von Daten seitens des Masters durch Pulsweitencodierung, aber dazu später.

Der Master fordert durch den Start einer Sequenz von Taktsignalen vom Slave ein Paket an. Der Slave liest intern seine Drehposition aus, kann also nicht sofort mit dem Senden seines Pakets starten. Daher gibt es bis zum Paketstart zwei Wartezustände.

Zunächst zieht der Slave seine Leitung low und signalisiert damit den Empfang der Paketanforderung:

ACURO BiSS-Modus Zeitanforderungen T1, Quelle https://www.hengstler.de/gfx/file/shop/encoder/AC58/Technical_Manual_SSI_BiSS_ACURO_en.pdf Seite 12, bearbeitet

Anschließend bereitet der Drehgeber intern das zu sendende Paket vor. In dieser Zeit bleibt seine Datenleitung auf low. Ist das Paket sendebereit, so wird es beginnend mit einem Startbit synchronisiert zum Taktsignal gesendet:

ACURO BiSS-Modus Zeitanforderungen T2, Quelle https://www.hengstler.de/gfx/file/shop/encoder/AC58/Technical_Manual_SSI_BiSS_ACURO_en.pdf Seite 12, bearbeitet

Die steigende Flanke des Startbits signalisiert also dem Master den Beginn des Pakets. Seine Länge ist vom Typ des Drehgebers abhängig.

Dieses Protokoll lässt sich wieder hervorragend durch eine Kombination von PIO und ISR unterstützen. In diesem Fall ist es Aufgabe der PIO, das Taktsignal zu generieren und das Paket einzulesen. Die ISR muss erst nach vollständigem Empfang des Paketes aktiv werden und die einzelnen Bits ihrer Bedeutung zuführen.

Wie bereits im Teil 1 erläutert ist das Besondere an unserer Zustandsmaschine, dass jeder Befehl genau einen Takt dauert und wir bei jedem Takt auch den Pegel von Pins vorgeben können. Auch den Takt der Zustandsmaschine geben wir vor.

Darauf aufbauend schreiben wir unseren „ACURO-Treiber“. Dieser muss mindestens die fallende Flanke zwischen T1 und T2 und das Startbit erkennen können, die folgenden Datenbits lesen und über RX-FIFO/IRQ an die Software übergeben.

Dabei ist der längste und damit entscheidende Abschnitt der Zustandsmaschine der Payload-Bit-Block, also das Einlesen der Datenbits:

payload_low:
    nop                         side 0 [3]

payload_high:
    nop                         side 1 [1]
    in pins, 1                  side 1
    jmp x-- payload_low         side 1

Der Übergang von payload_low zu payload_high erzeugt eine steigende Flanke, wir lesen das Datenbit in etwa der Hälfte der Zeit des High-Pegels des Taktsignals, nach zwei Beruhigungszyklen im dritten HIGH-Zyklus. Und genau das macht der Abschnitt

; 
    nop                         side 1 [1]
    in pins, 1                  side 1
    jmp x-- payload_low         side 1

Der Befehl in pins, 1 liest das aktuelle Datenbit und schiebt es ins Input Shift Register, der Befehl jmp x-- payload_low dekrementiert den Counter x und prüft, ob alle Datenbits gelesen wurden (Anzahl der Datenbits – 1 wird Anfangs in x gelegt). Das sind 2 Takte. Daher wird ihnen ein nop side 1 [1] vorangesetzt, ein auf 2 Takte verlängertes NOP.

Als Konsequenz dieses Abschnitts muss nun jeder Pegelabschnitt 4 Takte lang sein. Wir sehen das schon am vorangehenden low-Abschnitt:

payload_low:
    nop                         side 0 [3]

Darauf aufbauend ist nun die Programmierung unserer Zustandsmaschine nicht mehr schwer. In jedem Schritt implementieren wir 4 Takte pro Pegel und setzen den Takt der Zustandsmaschine Faktor 8 zur Taktfrequenz (242 * 4, da zwei Halbtakte zu 4 Taktzyklen der Zustandsmaschine), die wir für das Taktsignal erreichen wollen.

So arbeitet auch die Erkennung von Acknow:

ack_low:
    nop                         side 0 [3]

ack_high:
    nop                         side 1 [1]
    jmp pin ack_still_high      side 1
    out y, 8                    side 1
...

ack_still_high:
    jmp x-- ack_low             side 1

ack_timeout:
    set x, 1                    side 1
    jmp emit_error              side 1

Der Befehl nop side 0 [3] erzeugt wieder 4 Take LOW-Pegel auf der Taktleitung, die Sequenzen je nach Verzweigung

nop                     side 1 [1]
jmp pin ack_still_high  side 1
jmp x-- ack_low         side 1
oder
nop                     side 1 [1]
jmp pin ack_still_high  side 1
out y, 8                side 1

4 Takte High.

Zuletzt noch die Erkennung des Startbits als Ende von Busy:

busy_low:
    nop                         side 0 [3]

busy_high:
    nop                         side 1 [1]
    jmp pin start_found         side 1
    jmp y-- busy_low            side 1

start_found:
    mov x, osr                  side 1

Hier sorgen die Sequenzen je nach Verzweigung

nop                     side 1 [1]
jmp pin start_found     side 1
mov x, osr              side 1

oder

nop                     side 1 [1]
jmp pin start_found     side 1
jmp y-- busy_low        side 1

für 4 Takte HIGH-Pegel.

Am Ende müssen nur noch die gelesenen Datenbits mittels unseres RX-FIFOs übergeben oder bei einem Protokollfehler dieser mitgeteilt werden. Als Ergebnis wieder die Sicht des Oszilloskops, hier auf die Startsequenz eines ACURO-Pakets:

Code ist Wahrheit, daher zum Abschluss der komplette Code der Zustandsmaschine mit erklärenden Kommentaren:

; Reader für Hengstler ACURO Protokoll
;
; MA: side-set clock output
; SL: input pin and JMP pin
;
; Die absolute PIO- und MA-Taktfrequenz wird im C/C++-Teil über den
; Clock-Divider der State Machine festgelegt.
;
; Für jede aktive MA-Periode gilt unabhängig von der absoluten Frequenz:
;   LOW  für 4 PIO-Zyklen
;   HIGH für 4 PIO-Zyklen
;
; Daraus folgt:
;   PIO-SM-Takt = 8 * MA-Takt
;
; Beispiel bei 8 MHz PIO-SM-Takt:
;   1 PIO-Zyklus = 125 ns
;   4 PIO-Zyklen = 500 ns
;   LOW 4 Zyklen + HIGH 4 Zyklen = 1 µs Periodendauer = 1 MHz MA
;
; side 0 / side 1 setzt den MA-Ausgang während der jeweiligen Instruktion.
; [3] bedeutet: 3 zusätzliche Delay-Zyklen.
; Eine Instruktion ohne Delay dauert 1 Zyklus.
; Eine Instruktion mit [3] dauert also 4 Zyklen.
;
; SL wird jeweils nach zwei vollständigen HIGH-Zyklen ausgewertet.
; Die auswertende Instruktion beginnt damit in der Mitte der vier PIO-Zyklen
; langen HIGH-Halbwelle.
;
; Ablauf eines Messkommandos:
;
;   1. Der Host schreibt ein gepacktes Kommandowort in die TX-FIFO.
;   2. Die State Machine entpackt ACK-Timeout, START-Timeout und Payload-Länge.
;   3. Sie erzeugt MA-Takte und wartet innerhalb des vorgegebenen
;      ACK-Fensters darauf, dass SL LOW wird.
;   4. Nach erkanntem ACK erzeugt sie weitere MA-Takte und wartet innerhalb
;      des vorgegebenen START-Fensters darauf, dass SL wieder HIGH wird.
;   5. Das erkannte START-Bit wird nicht in den ISR geschoben.
;   6. Danach werden die vorgegebene Anzahl Payload-Bits eingelesen.
;   7. Erfolg oder Fehler werden über dasselbe Zwei-Wort-Format in der
;      RX-FIFO zurückgegeben.
;
; Vor dem ersten MA-Takt wird SL absichtlich nicht geprüft. Laut 
; BiSS-Protokoll kann SL nach einem Reset oder Power On noch LOW liegen.
; Entscheidend sind daher nur die beiden protokollrelevanten Übergänge:
;
;   SL wird innerhalb des ACK-Fensters LOW.
;   SL wird danach innerhalb des START-Fensters wieder HIGH.
;
; TX FIFO pro Messung:
;
;   word 0: gepacktes Messkommando
;
;       Bits  7..0:  maximale Anzahl ACK-Samples minus 1
;       Bits 15..8:  maximale Anzahl START-Samples minus 1
;       Bits 23..16: Payload-Bitanzahl minus 1
;       Bits 31..24: reserviert, müssen 0 sein
;
; Das Entpacken setzt voraus, dass der OSR im C/C++-Teil nach rechts schiebt:
;
;   sm_config_set_out_shift(&config, true, false, 32);
;
; Beispiel:
;
;   ACK spätestens beim 4. Sample:
;       ACK-Zähler = 4 - 1 = 3
;
;   START spätestens beim 6. Sample:
;       START-Zähler = 6 - 1 = 5
;
;   44 Payload-Bits:
;       Payload-Zähler = 44 - 1 = 43
;
; Gepacktes Kommandowort:
;
;   command_word =
;       ((payload_bits - 1) << 16) |
;       ((start_samples - 1) << 8) |
;       ((ack_samples - 1) << 0);
;
; Die Zählerwerte beschreiben Protokolltakte, keine festen Mikrosekunden.
; Ändert der C/C++-Teil die MA-Frequenz, bleiben Ablauf, Tastverhältnis und
; relativer Sample-Zeitpunkt unverändert. Nur die absolute Zeit pro Takt
; ändert sich.
; 
; Beispiel Hengstler ACURO AD34/1219AF.0NBC2
; Frame payload after the start bit
;   MT:12 | ST:19 | ALIGN:5 | ERR:1 | WAR:1 | CRC:6 = 44 bits
;
; RX FIFO pro abgeschlossener Messung:
;
;   word 0:
;       Erfolg: erste 32 Payload-Bits
;       Fehler: 0
;
;   word 1:
;       Erfolg: restliche Payload-Bits ab Bit 4, Status = 0
;       Fehler: keine Payload, Status = Fehlercode
;
; Der Status liegt in den unteren vier Bits von RX word 1:
;
;       Bits 3..0: Statuscode
;
; Statuscodes:
;
;   0: Messung erfolgreich, Payload in word 0 und word 1 gültig
;   1: ACK-Timeout, SL wurde nicht rechtzeitig LOW
;   2: START-Timeout, SL wurde nach ACK nicht rechtzeitig HIGH
;
; Damit die vom Sensor MSB-zuerst übertragenen Bits bereits in der vom
; C/C++-Decoder erwarteten Reihenfolge in der RX-FIFO liegen, muss der ISR
; im C/C++-Teil nach links schieben und nach 32 Bits automatisch pushen:
;
;   sm_config_set_in_shift(&config, false, true, 32);
;
; Nach dem 32-Bit-Autopush stehen weitere eingelesene Payload-Bits bei
; nach links schiebendem ISR zunächst in dessen unteren Bits. Vor dem
; expliziten zweiten PUSH führt die State Machine deshalb zusätzlich
;
;   in null, 4
;
; aus. Dadurch wird der Payload-Rest um vier Stellen nach links geschoben
; und Bits 3..0 bleiben für den Statuscode frei. Eine softwareseitige
; Bitumkehr ist nicht erforderlich.
;
; Für 44 Payload-Bits gilt daher:
;
;   RX word 0, Bits 31..0:
;       erste 32 Payload-Bits in Übertragungsreihenfolge
;
;   RX word 1, Bits 15..4:
;       restliche 12 Payload-Bits in Übertragungsreihenfolge
;
;   RX word 1, Bits 31..16:
;       0, für spätere Diagnoseinformationen reserviert
;
;   RX word 1, Bits 3..0:
;       Statuscode
;
; PUSH und Autopush löschen den ISR auf 0. Deshalb sind die unbenutzten Bits
; bei einem erfolgreichen Frame automatisch 0. Im Fehlerfall wurde noch kein
; Payload-Bit eingelesen; der erste Fehler-PUSH liefert daher ebenfalls 0.
;
; Dieses feste Zwei-Wort-Format setzt eine Payload-Länge von 32 bis 59 Bits
; voraus:
;
;   - nach 32 Bits erzeugt Autopush RX word 0,
;   - die verbleibenden 0 bis 27 Payload-Bits werden um vier Nullbits ergänzt
;     und anschließend manuell als RX word 1 gepusht,
;   - die unteren 4 Bits von RX word 1 bleiben für den Status frei.
;
; 60 Payload-Bits sind in dieser Variante bewusst ausgeschlossen: 28
; verbleibende Payload-Bits plus die vier Status-Nullbits würden erneut die
; 32-Bit-Autopush-Schwelle erreichen und vor dem expliziten PUSH ein
; zusätzliches RX-Wort erzeugen.
;
; Der C/C++-Teil kann für 44 Payload-Bits auswerten:
;
;   status       = rx_word_1 & 0x0f;
;   payload_tail = (rx_word_1 >> 4) & 0x0fff;
;
; Erfolg und Fehler benutzen damit denselben Transportweg und dieselbe
; feste Anzahl von RX-Wörtern. Der IRQ meldet, dass beide Wörter
; vollständig in der RX-FIFO liegen. Die fachliche Information steht im
; Statusfeld von RX word 1.
;
; IRQ flags are relative to the state machine:
;   0 rel: vollständiges Zwei-Wort-Ergebnis liegt in der RX-FIFO
;
; Beispiel:
;   SM0: result IRQ 0
;   SM1: result IRQ 1
;   SM2: result IRQ 2
;   SM3: result IRQ 3
;
; Instruktionsspeicher:
;   verwendet: 26 von 32 Instruktionen
;   Reserve:    6 Instruktionen

.program ad34_read
.side_set 1                    ; Ein Side-Set-Bit wird benutzt: MA-Clock-Ausgang.

.wrap_target                   ; Nach der letzten Instruktion springt die PIO automatisch hierher zurück.

public wait_command:
    ;
    ; Ruhezustand zwischen zwei Frames.
    ; MA bleibt HIGH.
    ;
    ; Der Host schreibt für genau eine Messung ein gepacktes Wort in die
    ; TX-FIFO:
    ;
    ;   Bits  7..0:  ACK-Samples minus 1
    ;   Bits 15..8:  START-Samples minus 1
    ;   Bits 23..16: Payload-Bits minus 1
    ;   Bits 31..24: 0
    ;
    ; X erhält zunächst den ACK-Zähler.
    ; START-Zähler und Payload-Zähler verbleiben zunächst im OSR.
    ; Der START-Zähler wird erst beim erkannten ACK nach Y übertragen.
    ; Dadurch übernimmt die OUT-Instruktion zugleich den notwendigen
    ; vierten HIGH-Zyklus der ACK-Periode.
    ;
    pull block                  side 1
    ; Warte blockierend auf das gepackte Kommandowort.
    ; Solange nichts kommt, steht die State Machine hier.
    ; MA wird währenddessen HIGH gehalten.
    ; Wenn das Wort kommt: TX-FIFO -> OSR.

    out x, 8                    side 1
    ; Extrahiere bei nach rechts schiebendem OSR die unteren 8 Bits.
    ; X erhält die maximale Anzahl ACK-Samples minus 1.
    ;
    ; Im OSR liegen danach unten:
    ;   Bits  7..0:  START-Samples minus 1
    ;   Bits 15..8:  Payload-Bits minus 1
    ;   Bits 23..16: reserviert und 0
    ;
    ; Der START-Zähler wird bewusst noch nicht nach Y kopiert.
    ; Erst nach dieser Instruktion beginnt der aktive MA-Taktburst.

ack_low:
    ;
    ; LOW-Phase eines ACK-Suchtakts.
    ;
    ; Die State Machine beginnt mit der Suche nach ACK = LOW.
    ;
    nop                         side 0 [3]
    ; Keine Operation.
    ; MA = LOW.
    ; Dauer: 1 Instruktionszyklus + 3 Delay-Zyklen = 4 Zyklen.
    ; Ergebnis: LOW-Halbwelle von genau 4 PIO-Zyklen.
    ; Danach folgt ohne Sprung direkt die HIGH-Phase.

ack_high:
    ;
    ; HIGH-Phase während der ACK-Suche.
    ; Gesucht wird ein LOW-Pegel auf SL.
    ;
    nop                         side 1 [1]
    ; Keine Operation.
    ; MA = HIGH.
    ; Dauer: 1 Instruktionszyklus + 1 Delay-Zyklus = 2 Zyklen.
    ; Das sind HIGH-Zyklus 1 und 2.
    ; Sie bilden die erste Hälfte der HIGH-Halbwelle.

    ;
    ; Beginn von HIGH-Zyklus 3:
    ; SL wird über den konfigurierten JMP-Pin abgefragt.
    ; Der Sample-Zeitpunkt liegt nach zwei vollständigen HIGH-Zyklen
    ; und damit in der Mitte der vier Zyklen langen HIGH-Halbwelle.
    ;
    jmp pin ack_still_high      side 1
    ; Wenn SL HIGH ist:
    ;   ACK ist noch nicht gekommen -> ack_still_high.
    ;
    ; Wenn SL LOW ist:
    ;   ACK wurde erkannt -> nächste Instruktion.
    ;
    ; MA bleibt HIGH.
    ; Diese Instruktion ist HIGH-Zyklus 3.

    ;
    ; HIGH-Zyklus 4 bei erkanntem ACK.
    ;
    ; Der START-Zähler wird jetzt aus dem OSR nach Y geschoben.
    ; Damit erfüllt diese Instruktion gleichzeitig zwei Aufgaben:
    ;
    ;   - Abschluss der ACK-HIGH-Phase mit dem vierten Zyklus,
    ;   - Laden des START-Zählers für die folgende BUSY-/START-Suche.
    ;
    out y, 8                    side 1
    ; Y erhält die maximale Anzahl START-Samples minus 1.
    ; MA bleibt während dieser Instruktion HIGH.
    ;
    ; Nach dem OUT liegen im OSR unten:
    ;   Bits  7..0: Payload-Bitanzahl minus 1
    ;   Bits 15..8: reserviert und 0
    ;
    ; Danach fällt die Ausführung unmittelbar in busy_low.

busy_low:
    ;
    ; LOW-Phase während der BUSY-/START-Suche.
    ;
    ; Nach erkanntem ACK darf SL zunächst LOW bleiben: BUSY.
    ; Der erste innerhalb des erlaubten Fensters gesampelte HIGH-Pegel
    ; wird als START-Bit erkannt.
    ;
    nop                         side 0 [3]
    ; Keine Operation.
    ; MA = LOW für genau 4 PIO-Zyklen.
    ; Diese Instruktion erzeugt die saubere LOW-Halbwelle.
    ; Danach folgt ohne Sprung direkt die HIGH-Phase.

busy_high:
    ;
    ; HIGH-Phase während der BUSY-/START-Suche.
    ;
    nop                         side 1 [1]
    ; MA = HIGH für 2 PIO-Zyklen.
    ; HIGH-Zyklus 1 und 2 bilden die erste Hälfte der HIGH-Halbwelle.

    ;
    ; Beginn von HIGH-Zyklus 3:
    ; SL wird über den konfigurierten JMP-Pin abgefragt.
    ;
    jmp pin start_found         side 1
    ; Wenn SL HIGH ist:
    ;   START-Bit gefunden -> start_found.
    ;
    ; Wenn SL LOW ist:
    ;   Sensor ist weiterhin BUSY -> nächste Instruktion.
    ;
    ; MA bleibt HIGH.
    ; Der Sample-Zeitpunkt liegt wieder exakt in der Mitte der HIGH-Halbwelle.

    ;
    ; HIGH-Zyklus 4, wenn SL weiterhin LOW war.
    ; Der START-Zähler entscheidet, ob noch ein weiterer BUSY-Takt erlaubt ist.
    ;
    jmp y-- busy_low            side 1
    ; Y wird bei jeder Ausführung dekrementiert.
    ; Die Sprungentscheidung verwendet den Wert von Y vor dem Dekrement.
    ;
    ; Wenn Y vor dem Dekrement != 0 ist:
    ;   Sprung nach busy_low,
    ;   ein weiterer BUSY-/START-Suchtakt wird erzeugt.
    ;
    ; Wenn Y vor dem Dekrement == 0 ist:
    ;   kein Sprung,
    ;   Y läuft auf 0xffffffff über,
    ;   das erlaubte START-Fenster ist verbraucht,
    ;   Ausführung läuft direkt mit start_timeout weiter.
    ;
    ; MA bleibt HIGH.
    ; Diese Instruktion bildet den vierten HIGH-Zyklus der letzten Periode.

start_timeout:
    ;
    ; Fehlercode 2: SL wurde nach ACK nicht rechtzeitig wieder HIGH.
    ;
    set x, 2                    side 1
    ; X = 2.
    ; Der Wert wird im unmittelbar folgenden gemeinsamen Fehlerpfad als
    ; Statusfeld des zweiten RX-Worts ausgegeben.
    ; MA bleibt HIGH.
    ; Danach fällt die Ausführung direkt in emit_error.

emit_error:
    ;
    ; Gemeinsamer Fehlerpfad.
    ;
    ; Ziel:
    ;   - MA sicher HIGH lassen
    ;   - unabhängig vom Fehler immer zwei RX-Wörter erzeugen
    ;   - RX word 0 auf 0 setzen
    ;   - Fehlercode in den unteren vier Bits von RX word 1 ausgeben
    ;
    ; Bis zu diesem Punkt wurde noch kein Payload-Bit mit IN eingelesen.
    ; Der ISR ist daher 0:
    ;
    ;   - nach Reset zunächst 0,
    ;   - nach jedem PUSH oder Autopush erneut 0,
    ;   - nach jedem vollständig ausgegebenen vorherigen Ergebnis erneut 0.
    ;
    ; Bei einer softwareseitigen Neuinitialisierung oder einem erzwungenen
    ; SM-Restart sollte der C/C++-Teil diesen Zustand ebenfalls herstellen,
    ; beispielsweise einmalig mit MOV ISR, NULL über pio_sm_exec().
    ;
    push block                  side 1
    ; Schiebe den leeren ISR als RX word 0 in die RX-FIFO.
    ; RX word 0 = 0.
    ; Dieses Wort ersetzt im Fehlerfall die ersten 32 Payload-Bits.
    ; PUSH löscht den ISR anschließend erneut auf 0.

    mov isr, x                  side 1
    ; Kopiere den Fehlercode aus X in den ISR.
    ; Da X nur 1 oder 2 enthält, liegen alle gesetzten Bits im reservierten
    ; Statusfeld Bits 3..0.
    ; MA bleibt HIGH.

    push block                  side 1
    ; Schiebe den Fehlercode als RX word 1 in die RX-FIFO.
    ;
    ; Ergebnis bei ACK-Timeout:
    ;   RX word 0 = 0
    ;   RX word 1 = 1
    ;
    ; Ergebnis bei START-Timeout:
    ;   RX word 0 = 0
    ;   RX word 1 = 2

    jmp result_ready            side 1
    ; Überspringe die ACK-Fehlerbehandlung und den Erfolgs-/Payload-Pfad
    ; und melde das vollständige Zwei-Wort-Ergebnis.
    ; MA bleibt HIGH.

ack_still_high:
    ;
    ; HIGH-Zyklus 4, wenn SL beim ACK-Sample noch HIGH war.
    ; Der ACK-Zähler entscheidet, ob noch ein weiterer Suchtakt erlaubt ist.
    ;
    jmp x-- ack_low             side 1
    ; X wird bei jeder Ausführung dekrementiert.
    ; Die Sprungentscheidung verwendet den Wert von X vor dem Dekrement.
    ;
    ; Wenn X vor dem Dekrement != 0 ist:
    ;   Sprung nach ack_low,
    ;   ein weiterer ACK-Suchtakt wird erzeugt.
    ;
    ; Wenn X vor dem Dekrement == 0 ist:
    ;   kein Sprung,
    ;   X läuft auf 0xffffffff über,
    ;   das erlaubte ACK-Fenster ist verbraucht,
    ;   Ausführung läuft direkt mit ack_timeout weiter.
    ;
    ; MA bleibt HIGH.
    ; Diese Instruktion bildet den vierten HIGH-Zyklus der letzten Periode.

ack_timeout:
    ;
    ; Fehlercode 1: ACK wurde nicht rechtzeitig LOW.
    ;
    set x, 1                    side 1
    ; X = 1.
    ; Der Wert wird im gemeinsamen Fehlerpfad in den ISR kopiert und als
    ; Statusfeld des zweiten RX-Worts ausgegeben.
    ; MA bleibt HIGH.

    jmp emit_error              side 1
    ; Springe zum gemeinsamen Fehlerpfad.
    ; MA bleibt HIGH.
    ;
    ; Diese Instruktion verlängert die HIGH-Phase nur im bereits erkannten
    ; Fehlerfall. Der aktive Taktburst ist zu diesem Zeitpunkt beendet.

start_found:
    ;
    ; HIGH-Zyklus 4 der START-Bit-Periode.
    ;
    ; Das START-Bit selbst wird absichtlich NICHT in den ISR geschoben.
    ; Erst danach beginnt das Einlesen der Payload-Bits.
    ;
    mov x, osr                  side 1
    ; Kopiere den Payload-Zähler aus dem OSR nach X.
    ; MA bleibt HIGH.
    ;
    ; X enthält jetzt die Payload-Bitanzahl minus 1.
    ; Die reservierten oberen acht Bits des ursprünglichen Kommandoworts
    ; müssen 0 sein, damit X ausschließlich den Payload-Zähler enthält.
    ;
    ; Beispiel für 44 Bits:
    ;   X = 43
    ;
    ; Grund:
    ;   Pro Payload-Bit wird einmal in pins, 1 ausgeführt.
    ;   Danach entscheidet jmp x--, ob noch ein weiterer Bit-Takt kommt.
    ;   X = 43 ergibt genau 44 Samples.

payload_low:
    ;
    ; LOW-Phase vor jedem Payload-Bit.
    ;
    nop                         side 0 [3]
    ; Keine Operation.
    ; MA = LOW für genau 4 PIO-Zyklen.
    ; Diese Instruktion erzeugt die LOW-Halbwelle des Payload-Takts.
    ; Danach folgt ohne Sprung direkt die HIGH-Phase.

payload_high:
    ;
    ; HIGH-Phase eines Payload-Bits.
    ;
    nop                         side 1 [1]
    ; MA = HIGH für 2 PIO-Zyklen.
    ; HIGH-Zyklus 1 und 2 bilden die erste Hälfte der HIGH-Halbwelle.

    ;
    ; Beginn von HIGH-Zyklus 3:
    ; Das Payload-Bit wird vom konfigurierten IN-Pin gelesen.
    ;
    in pins, 1                  side 1
    ; Lies 1 Bit vom konfigurierten IN-Pin-Bereich.
    ; Das Bit wird in den Input Shift Register, ISR, geschoben.
    ; MA bleibt HIGH.
    ;
    ; Der Sample-Zeitpunkt liegt nach zwei vollständigen HIGH-Zyklen
    ; und damit exakt in der Mitte der HIGH-Halbwelle.
    ;
    ; Bei der erwarteten Konfiguration:
    ;   - ISR schiebt nach links,
    ;   - Autopush-Schwelle: 32 Bits,
    ;   - nach 32 Bits wird RX word 0 automatisch erzeugt,
    ;   - die verbleibenden Bits stehen zunächst in den unteren ISR-Bits,
    ;   - nach dem letzten Payload-Bit werden vier Nullbits nachgeschoben,
    ;     sodass die unteren vier Bits von RX word 1 Status 0 enthalten.

    ;
    ; HIGH-Zyklus 4:
    ; Payload-Zähler behandeln.
    ;
    jmp x-- payload_low         side 1
    ; X wird bei jeder Ausführung dekrementiert.
    ; Die Sprungentscheidung verwendet den Wert von X vor dem Dekrement.
    ;
    ; Wenn X vor dem Dekrement != 0 ist:
    ;   Sprung nach payload_low,
    ;   nächstes Payload-Bit wird getaktet.
    ;
    ; Wenn X vor dem Dekrement == 0 ist:
    ;   kein Sprung,
    ;   X läuft auf 0xffffffff über,
    ;   alle Payload-Bits wurden gelesen,
    ;   Ausführung läuft mit dem finalen push weiter.
    ;
    ; MA bleibt HIGH.
    ; Diese Instruktion bildet den vierten HIGH-Zyklus der Periode.

    ;
    ; Erfolgreiches Ende des aktiven Bursts.
    ; MA bleibt ab hier HIGH.
    ;
    ; Bei 44 Payload-Bits wurden die ersten 32 Bits bereits durch Autopush
    ; als RX word 0 in die RX-FIFO geschrieben.
    ; Die restlichen 12 Bits liegen zunächst in ISR Bits 11..0.
    ;
    ; Vier nachgeschobene Nullbits verschieben den Payload-Rest nach Bits
    ; 15..4 und reservieren Bits 3..0 als Statusfeld mit dem Erfolgswert 0.
    ; Die OUT-Schieberichtung des OSR ist davon unabhängig.
    ;
    in null, 4                  side 1
    ; Schiebe vier Nullbits in den nach links schiebenden ISR.
    ; Für 44 Payload-Bits gilt danach:
    ;   ISR Bits 15..4 = letzte 12 Payload-Bits
    ;   ISR Bits  3..0 = 0, also Status "Erfolg"
    ; MA bleibt HIGH. Der aktive Taktburst ist bereits beendet.

    push block                  side 1
    ; Schiebe den verbleibenden ISR-Inhalt als RX word 1 in die RX-FIFO.
    ; block bedeutet:
    ;   Wenn die RX-FIFO voll ist, wartet die State Machine hier.
    ;
    ; Das Ergebnis besteht damit vollständig aus:
    ;   RX word 0: erste 32 Payload-Bits
    ;   RX word 1: restliche Payload-Bits + Status 0
    ;
    ; PUSH löscht den ISR anschließend wieder auf 0.

result_ready:
    ;
    ; Gemeinsamer Abschluss für Erfolg und Fehler.
    ; Zu diesem Zeitpunkt liegen immer genau zwei vollständige Ergebniswörter
    ; in der RX-FIFO.
    ;
    irq 0 rel                   side 1
    ; Setze den relativen Ergebnis-IRQ 0.
    ; MA bleibt HIGH.
    ;
    ; Bei SM0 -> IRQ 0.
    ; Bei SM1 -> IRQ 1.
    ; Bei SM2 -> IRQ 2.
    ; Bei SM3 -> IRQ 3.
    ;
    ; Der IRQ unterscheidet nicht zwischen Erfolg und Fehler.
    ; Er bedeutet ausschließlich:
    ;   Zwei Ergebniswörter liegen vollständig in der RX-FIFO.
    ;
    ; Nach dieser Instruktion erreicht die State Machine .wrap.
    ; Dadurch springt sie automatisch zurück zu .wrap_target,
    ; also nach wait_command, und wartet auf das nächste Messkommando.

.wrap

% c-sdk {
static inline uint32_t ad34_read_make_command(uint32_t ack_samples,
                                               uint32_t start_samples,
                                               uint32_t payload_bits)
{
    return (((payload_bits - 1u) & 0xffu) << 16) |
           (((start_samples - 1u) & 0xffu) << 8) |
           (((ack_samples - 1u) & 0xffu) << 0);
}

static inline uint32_t ad34_read_result_status(uint32_t rx_word_1)
{
    return rx_word_1 & 0x0fu;
}

static inline uint32_t ad34_read_result_payload_tail(uint32_t rx_word_1,
                                                     uint32_t payload_bits)
{
    const uint32_t tail_bits = payload_bits - 32u;
    const uint32_t tail_mask = tail_bits == 0u ? 0u : ((1u << tail_bits) - 1u);
    return (rx_word_1 >> 4) & tail_mask;
}
%}