Einleitung
Der im vorigen Beitrag bereits angekündigte RTL-Code für das FPGA und die zugehörige Firmware sind jetzt funktionsfähig und soweit, daß man beides vorzeigen kann. Es ist noch der eine oder andere Testcode enthalten, daher ist beides noch nicht als „Release“ gekennzeichnet. Außerdem soll es noch ein paar Änderungen in der nächsten Revision der Leiterplatte geben.
Nach ein paar grundlegenden Bemerkungen zum Messprinzip wird nachfolgend zuerst der Verilog-Code beschrieben, anschließend die Software und zum Abschluss werden ein paar Beispielmessungen mit einem Signalgenerator gezeigt. Am Ende des Beitrags folgen noch ein paar Anmerkungen zum PicoRV32-Risc-V-Core.
Das verwendete Tool zum Erstellen des FPGAs ist der Gowin FPGA Designer V1.9.11.01. Für die Softwareerstellung wird GMD (Gowin for Embedded C/C++ Developers), Version: 2025.01 verwendet.
Als Referenz soll hier zunächst nochmal das Blockdiagramm aus dem vorigen Beitrag gezeigt werden, weil sich die nachfolgenden Erklärungen darauf beziehen:
Das Messprinzip
Die vor- und rücklaufenden Analogsignale werden in den Komparatoren digitalisiert und anschließend im FPGA digital ausgewertet. Hier ist exemplarisch ein Timingdiagramm für ein 10-MHz-Signal und eine Abtastung mit 200 MHz gezeigt:

Oben wird das vorlaufende Signal und darunter das reflektierte und um 90° phasenverschobene rücklaufende Signal gezeigt. Die dritte und vierte Kurve zeigen jeweils das digitalisierte Signal und danach ist das exklusiv-veroderte Phasendifferenzsignal dargestellt, aus dem die Phasenverschiebung bestimmt wird. Das Abtastsignal hat eine deutlich höhere Frequenz, in diesem Fall 200 MHz.
Ein Messintervall dauert typischerweise zwischen 10 und 100 ms. Während dieses Intervalls werden verschiedene Zähler unter bestimmten Bedingungen mit dem Takt des Abtastsignals hochgezählt und am Ende des Messintervalls von der CPU ausgelesen und ausgewertet. Beim Start des nächsten Messintervalls werden diese Zähler automatisch wieder gelöscht und starten bei null.
Phase Difference Counter PDC
Alle Zähler sind im Modul „Reflection_Measurement“ in der Datei „RefMeas.v“ definiert. Der Phase Difference Counter PDC wird immer dann inkrementiert, wenn das Phasendifferenzsignal auf high liegt, wenn also beide Eingangssignale verschieden sind. Am Ende des Messintervalls zeigt das Verhältnis des PDC-Zählerstandes zur Gesamtanzahl der Takte während des Intervalls also die Phasenverschiebung zwischen den Eingangssignalen an.
Edge Counter EC
Die Phasenmessung mit dem Phase Difference Counter liefert ein zweideutiges Ergebnis, denn eine negative Phasenverschiebung führt zu demselben Zählerstand, wie eine positive Verschiebung. Diese Mehrdeutigkeit löst der Edge Counter EC auf. Dieser Zähler wird dann inkrementiert, wenn das rücklaufende Signal bei einer steigenden Flanke des vorlaufenden Signals low ist. Ein weiterer gleichartiger Zähler wird dann inkrementiert, wenn das vorlaufende Signal bei einer steigenden Flanke des rücklaufenden Signals low ist. Am Ende der Messung trifft die CPU eine Mehrheitsentscheidung und bestimmt so das Vorzeichen der Phasenverschiebung. Bei 0° und 180° kommt es hier zu Unsicherheiten, die aber im Rahmen der Messgenauigkeit liegen.
Frequency Counter FC1 und FC2
Die beiden Frequenzzähler FC1 und FC2 werden immer dann inkrementiert, wenn eine steigende Flanke auf dem jeweiligen Eingangssignal festgestellt wurde. Aus dem Zählerstand am Ende der Messung bestimmt die CPU dann die jeweilige Frequenz.
Diese Frequenzen sind naturgemäß gleich, weil das rücklaufende Signal ja vom vorlaufenden Signal abgeleitet ist. Bei nahezu idealer Anpassung geht aber der Pegel des reflektierten Signals immer weiter gegen null und die gemessene Frequenz genauso. Der Vergleich dieser Frequenzen ist daher ein Maß für die Zuverlässigkeit der Phasenmessung. Weichen sie (deutlich) voneinander ab, ist die Phasenmessung mit den oben genannten Registern ungültig.
High und Low Counter
Im unten vorgestellten Design sind noch vier weitere Zähler implementiert, die im Blockdiagramm nicht gezeigt sind und zukünftig entfallen sollen: F‑High, F‑Low, R‑High und R‑Low. Die High-Zähler werden inkrementiert, wenn das entsprechende Eingangssignal bei steigender Flanke des Abtasttaktes high ist, die (redundanten) Low-Zähler dann, wenn das Signal low ist. Wie im Beitrag „Gamma-Messgerät – Nachtrag zu den Messungen“ beschrieben wurde, gibt es beim Prototyp einen Offset in der Vergleichsspannung der Komparatoren. Das führt dazu, daß die Umschaltpegel der Komparatoren vom Nulldurchgang der Eingangssignale verschoben ist. Der Effekt ist nachfolgend etwas übertrieben dargestellt:

Als Folge davon sind die erkannten positiven Halbwellen kürzer als die negativen Halbwellen. Dieser Fehler ist bei hohen Eingangspegeln gering, wird aber immer höher, je weiter die Pegel sinken. Am Ende des Messintervalls zeigen die Zählerstände also die Dauer der high- und low-Phasen an, also die Qualität des jeweiligen Signals.
Weitere Funktionsblöcke im FPGA
Intervall Timer T1
T1 ist ein einfacher Intervall Timer, der automatisch nach einer einstellbaren Zeit einen CPU-Interrupt generiert. Er wird im Beispielcode auf 10 ms eingestellt. Anders als der in der CPU eingebaute Timer, muß er nach dem Auslösen eines Interrupts nicht wieder neu initialisiert werden. Damit wird also unabhängig von der Interrupt-Antwortzeit ein festes zeitliches Raster erzeugt, zumindest solange keine Interrupts verloren gehen.
Asynchroner Transmitter ATx
In der CPU können zwei verschiedene UARTs angelegt werden, ein „simple UART“ und ein „WB UART“. Keines davon ist besonders gut dokumentiert, so daß es mir nicht auf Anhieb gelungen ist, einen Interrupt auszulösen, wenn der Sendepuffer leer ist. Da zum Senden der Messwerte nur ein Transmitter nötig ist, schien es mir einfacher, einen eigenen asynchronen Sender zu schreiben. Ich habe ihn ATx genannt, nicht UART, weil er weder universell ist noch einen Empfänger beinhaltet. Immerhin ist er gepuffert und löst einen Interrupt aus, sobald der Puffer ins Schieberegister übertragen wird. Die Baudrate ist von der CPU einstellbar. Es wird ein Stoppbit generiert und Parity ist nicht unterstützt.
Der RTL Code
Die oben im Blockdiagramm gezeigten und anschließend erklärten Funktionen wurden in Verilog geschrieben und für das FPGA synthetisiert. Hier ist das gesamte komprimierte Gowin_V1.9.11.01 FPGA Designer Projekt inklusive der vom IP Core Generator erzeugten Module. Mit welchen Parametern diese Module erzeugt wurden steht in src/readme.txt.
Das Top-Level Design findet sich in der Datei „Antennenkoppler.v“. Hier werden die FPGA-Pins definiert, die auf dem Prototypenboard angeschlossen sind. Außer den generierten Modulen werden die Module „Reflection_Measurement“, „ATx“ und „IntervalTimer“ instanziiert. Deren Quellcode findet sich in den entsprechenden Dateien im selben Verzeichnis.
Das Top-Level-Modul generiert die Reset-Signale für die CPU und die Reflexionsmessung, sowie die Signale für das Wishbone-Memory-Interface. Es definiert auch die Signale für die Adress-Decodierung. Weitere Erläuterungen finden sich in den jeweiligen Quelltexten.
Außerdem instanziiert das Top-Level-Modul die PLL, die aus dem Eingangstakt von 50 MHz den Abtasttakt von 200 MHz und den CPU-Takt von 20 MHz erzeugt.
Der IP Core Generator
Exemplarisch soll hier die Erzeugung des Gowin PicoRV32 CPU-Cores mit dem IP Core Generator gezeigt werden. Der IP Core Generator wird entweder unter „Tools“-„IP Core Generator“ gestartet oder durch Anklicken des zugehörigen Icons mit den drei Würfeln auf der Tools-Leiste. Er meldet sich mit einem Angebot verschiedener Hard- und Soft-Module:

Hier wählt man unter Soft IP-Core den Gowin_PicoRV32 mit einem Doppelklick aus. Es wird eine Eingabemaske zu Konfiguration des gewünschten Funktionsumfangs gestartet:

Durch Doppelklick auf die jeweilige Funktionsgruppe kann diese ausgewählt (grün hinterlegt) oder wieder entfernt werden. Bei konfigurierbaren Elementen öffnet sich ein entsprechendes Menü. Starten wir mit der Konfiguration der CPU:
Gowin PicoRV32 CORE
In diesem Dialog wird der CPU-Core konfiguriert.

Man kann zunächst in engen Grenzen den verwendeten Befehlssatz auswählen. Immer ausgewählt ist der RV32I Integer Befehlssatz, optional kann man die komprimierten Instruktionen (RV32C) und die Unterstützung für Integer-Multiplikation und Division (RV32M) zu- oder abwählen. Darüberhinaus sind einige Features wählbar, die den Funktionsumfang und die Performance beeinflussen, was jeweils zugunsten oder zuungunsten der Anzahl verwendeter Logikgatter führt. Da es bei dem hier vorgestellten System weder Einschränkungen bei den Logikgattern noch bei der Performance gibt, lassen wir alles auf den oben gezeigten Default-Einstellungen.
ITCM
Mit der etwas ungewöhnlichen Abkürzung ITCM wird der Befehlsspeicher für den Prozessor konfiguriert und sein Inhalt festgelegt.

Der Befehlsspeicher kann 8 kB, 16 kB, 32 kB oder 64 kB groß sein. Für dieses Projekt belassen wir es bei dem Default-Wert von 32 kB.
Als nächstes muß der Boot-Mode ausgewählt werden. Bei den ersten beiden Modi wird ein SPI-Interface generiert, über das das Programm beim Start aus dem externen Flash in das ITCM geladen wird. Damit ist die Softwareentwicklung quasi unabhängig von der Hardwareentwicklung. Hier ist es im Moment allerdings einfacher, die Software bereits in den FPGA-Bitstream einzumassieren. Dazu wählt man „MCU boot and run in ITCM“ und trägt den Namen der zu verwendenten Hex-Datei ein. Das hat allerdings den Nachteil, daß die Software bereits bei Generierung des Bitstreams vorhanden sein und dieser bei jeder Softwareänderung neu generiert werden muß.
DTCM
Die Konfiguration des Datenspeichers erfolgt analog zu der des Instruktionsspeichers.

Der Datenspeicher wird beim Starten durch die CPU initialisiert. Ein Hex-File wie beim Instruktionsspeicher entfällt daher.
WB UART
Das Wishbone UART wird tatsächlich in dem hier vorgestellten Projekt nicht mehr verwendet, aber aus historischen Gründen sind die Signale noch definiert. Daher muß es enabled werden, um Fehlermeldungen zu vermeiden. Es wird in einer zukünftigen Version entweder wieder benutzt oder ganz entfernt.
WB SPI Master
Das SPI Master Interface wird zur Kommunikation mit dem externen AD-Wandler ADS1118 benötigt. Es wird enabled und folgendermaßen konfiguriert:

Das SPI-Interface wird mit dem CPU-Takt von 20 MHz betrieben und der AD-Wandler darf mit höchstens 4 MHz getaktet werden. Um genügend Marge zu haben, wird er tatsächlich mit 2 MHz betrieben, was einem CLOCK_SEL=4 entspricht (SPICLK = CPUCLK/(2*(CLOCK_SEL+1))). Shift-Richtung, Clock Phase und Polarität sind vom ADC vorgegeben und müssen wie hier gezeigt gewählt werden, die Kommunikation erfolgt mit einer Datenlänge von 32 Bit. Die anderen Parameter bleiben auf ihren Default-Werten.
Open WB Interface
Abschließend wird noch das Open Wishbone Interface enabled. Es wird für das Lesen und Schreiben der Register in den oben genannten Funktionsblöcken benötigt.
Andere CPU Funktionsblöcke
Simple UART, Open AHB Interface, WB I2C Master, WB SPI Slave, WB GPIO und ADV SPI-Flash werden hier nicht benutzt und dementsprechend disabled.
Generieren der konfigurierten CPU
Nachdem nun alle Blöcke wie gewünscht konfiguriert sind, kann nun die CPU generiert werden. Vor dem Klicken auf „ok“ sollte aber noch der Haken bei „Use Gowin PicoRV32 as top module“ abgewählt werden, denn die CPU ist hier ein Sub-Modul.

Nach dem Klicken auf „ok“ wird noch nachgefragt, ob das existierende Verzeichnis überschrieben werden darf, falls es aus vorherigen Durchgängen bereits existiert. Wir klicken auf „yes“ und nun hat er erstmal 30 Sekunden zu tun. Dann wird nachgefragt, ob die neu generierten Dateien zum Projekt hinzugefügt werden sollen. Das sollte man beim ersten mal mit „yes“ beantworten, sonst muß man sie anschließend manuell hinzufügen. Bei weiteren Durchgängen ist die Antwort egal, denn sie sind ja schon Teil des Projekts.
Synthese, Placement & Routing
Jetzt ist das Projekt bereit zur Synthese. Dabei werden die Logikfunktionen aufgelöst und das gesamte Projekt in Logikgatter umgewandelt. Wenn das fehlerfrei durchläuft, folgt anschließend das Placement & Routing. Dabei werden die Pins und Gatter an die berechneten Positionen platziert und elektrisch miteinander verbunden.
Wenn die Synthese für ein definiertes Projekt verwendet werden soll, dann muß man vor dem Placement zumindest die angeschlossenen Pins im Floorplanner zuweisen. Nicht angeschlossene Pins bekommen eine Default-Konfiguration (meist Input mit pull-up Widerständen). Nicht im Floorplanner zugewiesene Signale werden irgendwohin gelegt, wo es dem Placement genehm ist. Das kann man bei ersten Versuchen machen, wenn es noch keine Leiterplatte gibt. Das automatische Placement kann sogar eine Hilfe bei der Pinauswahl für das Layout sein.
Synthese, Placement & Routing benötigen für dieses Projekt auf meinem PC etwa 2 Minuten. Im Verlauf werden mehrere Berichte erstellt, die man sich gelegentlich anschauen sollte. Sie können insbesondere bei Fehlern sehr hilfreich sein. Auch die Timing Analyse sollte man nicht ignorieren. Hier werden die maximal möglichen Taktfrequenzen angezeigt und zwar in rot, wenn die Vorgaben nicht getroffen werden. Gerade in der Entwicklungsphase kann man mal ein Auge zudrücken, wenn die Abweichungen gering sind und beispielsweise nur 195 MHz statt 200 MHz erreicht werden, wir arbeiten ja schließlich unter definierten Bedingungen und bei Zimmertemperatur. Sobald das Modul aber für den Außenbereich released wird und damit dem gesamten Temperaturbereich und womöglich Spannungsschwankungen ausgeliefert ist, würde ich keine roten Meldungen mehr tolerieren. Im ungünstigsten Moment fällt dann das Modul aus und man hat keinen Hinweis, was schiefgelaufen sein könnte.
Die Software
Hier zunächst das komplette Eclipse-Projekt:
Es müsste direkt übersetzbar sein, aber gegebenenfalls müssen Pfade angepasst werden.
Das eigentliche Hauptprogramm steht in der Datei „RiscV_Phasenmessung.c“. Im „library“ Verzeichnis befinden sich die Quellcodes, die von Gowin zur Verfügung gestellt wurden, einige von ihnen wurden für dieses Projekt adaptiert, insbesondere die Interrupt-Funktionen in irq.c und irq.h.
Das Hauptprogramm
Im Hauptprogramm werden nach der Initialisierung lokaler Variablen als erstes die verwendeten Peripherieelemente im FPGA initialisiert. Der asynchrone Sender wird auf 115,2 kBd eingestellt, der Intervalltimer auf 10 ms und ein Messzyklus wird gestartet. Anschließend werden die Interrupts freigegeben.
Der Intervalltimer
Der Intervalltimer erzeugt alle 10 ms einen Interrupt, der die Funktion user1_int() aufruft. Dort wird im wesentlichen der nächste ADC Messzyklus gestartet und das Ergebnis der vorherigen Messung gelesen. Der SPI-Transfer zum ADC läuft automatisch und die CPU muß nicht auf dessen Ergebnisse warten. Sie werden einfach bei der nächsten Gelegenheit nach 10 ms abgeholt. Um die Ausführungszeit des Interrupt-Handlers kurz zu halten, werden die Messergebnisse nicht konvertiert, sondern als Rohdaten abgespeichert.
Die Phasenmessung
Die Phasenmessung erfolgt wie oben beschrieben im FPGA. Sie wird durch Schreiben des Controlregisters gestartet und läuft dann automatisch ab. Der Status der Messung wird im Statusregister angezeigt, das hier der Einfachheit halber gepollt wird. Ist die Messung beendet, werden die Ergebnisse ausgelesen, formatiert und die serielle Übertragung wird gestartet. Der Datentransfer nach dem ersten Byte erfolgt interruptgesteuert. Direkt nach dem Start der Übertragung wird der nächste Messzyklus gestartet.
Die Formatierung der Ergebnisse ist im Moment in lesbarer Form gehalten, damit man sie direkt in einem Terminalprogramm beobachten kann. Daher wurde auch auf einen Zeilenvorschub verzichtet, es wird also jedesmal die vorherige Zeile überschrieben. Das wird in einem endgültigen System sicherlich adaptiert.
Beispielmessungen
Nachfolgend sollen einige Beispielmessungen z.Zt. noch mit reiner Textausgabe dokumentiert werden.
Messung ohne Eingangssignale
Dieser Block zeigt die gesamte Textausgabe nach einem Reset. Die folgenden Beispiele zeigen jeweils nur die Messung.
-------- Reflection Measurement, under development, date: Jan 8 2026, time: 12:39:23 --------
CPU clock: 20.000 MHz - Sampling clock: 200.000 MHz - Measurement interval: 25 ms
Ff: 0.000 MHz, Fr: 0.000 MHz, Delta_P: +0.00°, High_f: 0.00%, High_r: 0.00%, ADC0=2 mV, ADC1=3303 mV, ADC2=398 mV, ADC3=381 mV, T=+23,750 °C
ADC1 zeigt die Versorgungsspannung in Millivolt, T die aktuelle Temperatur des AD-Wandlers. Es sind keine Eingangssignale angelegt, daher ist die Frequenz 0 Hz. An den Ausgängen der logarithmischen Verstärker liegen knapp 400 mV an.
21.104 MHz, F=0 dBm, R=0 dBm, ∆P=0°
Ff: 21.104 MHz, Fr: 21.104 MHz, Delta_P: +0.99°, High_f: 45.39%, High_r: 45.16%, ADC0=2 mV, ADC1=3303 mV, ADC2=1873 mV, ADC3=1864 mV, T=+25,844 °C
Hier wurde absichtlich eine krumme Frequenz von 21.104 MHz gewählt, denn mitunter ergeben sich bei ausgesuchten Eingangswerten zufällig die gewünschten korrekten Ergebnisse. Die Frequenzen werden korrekt gemessen und es zeigt sich eine Phasenverschiebung von +0.99°, was im Rahmen der Messgenauigkeit liegt. Die an den LogAmps gemessenen Pegel liegen bei 1,87 V mit einer Abweichung von etwa 10 mV. High_f und High_r geben die relative High-Zeit des jeweiligen Signals während der Messperiode an. Sie liegen etwas unter 50%, wodurch die Messung bei diesem Eingangspegel schon unzuverlässig wird.
21.104 MHz, F=20 dBm, R=0 dBm, ∆P=100°
Ff: 21.104 MHz, Fr: 21.104 MHz, Delta_P: +108.90°, High_f: 49.78%, High_r: 45.12%, ADC0=2 mV, ADC1=3303 mV, ADC2=2372 mV, ADC3=1862 mV, T=+26,813 °C
Wir bleiben bei der krummen Frequenz und erhöhen den Pegel des vorlaufenden Signals auf +20 dBm und die Phasenverschiebung auf 100°. Die Frequenzmessung bleibt korrekt und die Qualität der vorlaufenden Signals verbessert sich auf 49.78%, also nahezu auf den Idealwert.
21.104 MHz, F=20 dBm, R=20 dBm, ∆P=100°
Ff: 21.104 MHz, Fr: 21.104 MHz, Delta_P: +101.99°, High_f: 49.77%, High_r: 49.65%, ADC0=2 mV, ADC1=3303 mV, ADC2=2371 mV, ADC3=2363 mV, T=+27,531 °C
Jetzt wird auch der Pegel des rücklaufenden Signals auf +20 dBm erhöht. Die Qualität beider Signale nähert sich dem Idealwert von 50% und die gemessene Phasenverschiebung liegt nun 2° neben dem tatsächlichen Wert. Beide LogAmps zeigen nun etwa 2,37 V Ausgangsspannung an.
21.104 MHz, F=0 dBm, R=-15 dBm, ∆P=-100°
Ff: 21.104 MHz, Fr: 21.104 MHz, Delta_P: -103.13°, High_f: 45.24%, High_r: 29.50%, ADC0=0 mV, ADC1=3303 mV, ADC2=1871 mV, ADC3=1484 mV, T=+28,375 °C
Der Vorlauf-Pegel liegt nun wieder bei 0 dBm, der Rücklauf-Pegel bei ‑15 dBm und die Phasenverschiebung wurde am Signalgenerator auf ‑100° eingestellt. Die Frequenzmessungen sind nach wie vor korrekt, aber die Qualität des rücklaufenden Signals sinkt auf unter 30%. Es wird also ein erheblicher Teil der positiven Halbwelle abgeschnitten. Trotzdem weicht die gemessene Phasenverschiebung nur um gut 3° von der tatsächlichen Verschiebung ab.
21.104 MHz, F=0 dBm, R=-19 dBm, ∆P=-100°
Ff: 21.104 MHz, Fr: 21.104 MHz, Delta_P: -109.76°, High_f: 45.49%, High_r: 15.77%, ADC0=2 mV, ADC1=3303 mV, ADC2=1871 mV, ADC3=1385 mV, T=+27,500 °C
Der Rücklauf-Pegel liegt jetzt bei ‑19 dBm. Die Frequenzmessungen sind nach wie vor korrekt, aber die Qualität des rücklaufenden Signals sinkt auf unter 16%.
21.104 MHz, F=0 dBm, R=-20 dBm, ∆P=-100°
Ff: 21.104 MHz, Fr: 0.000 MHz, Delta_P: -81.88°, High_f: 45.30%, High_r: 0.00%, ADC0=2 mV, ADC1=3303 mV, ADC2=1871 mV, ADC3=1358 mV, T=+27,500 °C
Der Rücklauf-Pegel liegt jetzt bei ‑20 dBm, nur 1 dB weniger als bei der vorigen Messung. Das Signal wird nicht mehr erkannt, die Frequenz sinkt auf 0 Hz und die Qualität auf 0%. Kanal 3 des ADC zeigt noch 1,358 V, es liegt also noch ein hinreichend starkes Signal an.
Das ist genau das Problem, das einer Nachbesserung bedarf und im nächsten Redesign behoben werden soll.
Anmerkungen zur PicoRV32 CPU
Im FPGA wird eine PicoRV32 CPU synthetisiert. Das ist ein 32-bit-Mikroprozessor mit einer Risc-V-Architektur. Zum Erstellen des RTL-Codes wird der „IP Core Generator“ des Gowin FPGA Designers benutzt. Alternativ kann der gesamte Verilog-Code auch von GitHub heruntergeladen werden. Damit hat man alle Freiheiten, inklusive der Adaption des Codes, aber eben auch alle Last der korrekten Konfiguration. Weitere Informationen zur Risc-V-Architektur findet man auf deren Website.
PicoRV32 Designziele
PicoRV32 ist für die Synthese für ein FPGA optimiert. Dazu soll der Ressourcenverbrauch gering sein, die Rechenleistung ist von untergeordneter Bedeutung. Im hier verwendeten FPGA wird bei der Standardkonfiguration der CPU ungefähr ein drittel der vorhandenen Logikgatter benötigt.
PicoRV32 Performance
Fälschlicherweise wird der Begriff RISC oft mit „eine Instruktion pro Takt“ gleichsetzt, was zwar oft stimmt, aber nicht die eigentliche Bedeutung ist. RISC bedeutet nur, daß der Befehlssatz auf solche Operationen reduziert ist, die ohne zeitlich unbestimmte Pipeline-Locks ausgeführt werden können. Die Pipeline darf durchaus mehrere Takte für eine Instruktion benötigen, sie darf aber nicht beispielsweise durch Wait-States beim Zugriff auf andere Ressourcen auf unbestimmte Zeit blockiert werden. Es sind nur solche Instruktionen erlaubt, die das sicherstellen. Damit fallen insbesondere die Befehle weg, die Daten im Speicher direkt modifizieren, wie es bei CISC-Prozessoren üblich ist. Für Operationen dieser Art wird in einem RISC Prozessor zunächst der Speicherwert in ein Register gelesen, dort modifiziert und anschließend zurückgeschrieben (Load/Store-Architektur). Während des Lese- oder Schreibzugriffs kann die CPU prinzipiell weiterarbeiten, was eine höhere Rechenleistung ermöglicht. In einer einfach gehaltenen CPU darf man Optimierungen dieser Art allerdings nicht erwarten. Auch die etwas komplexeren mathematischen Operationen Multiplikation und Division benötigen einige Dutzend Taktzyklen.
Dhrystone Benchmark Ergebnisse
Die PicoRV32-CPU benötigt beim Dhrystone Benchmark 4,1 Takte pro Instruktion, was 0,516 DMIPS/MHz entspricht. Das ist für RISC-Verhältnisse nicht überwältigend, aber in Anbetracht des geringen Ressourcenverbrauch doch erstaunlich gut. Auf jeden Fall ist es für die typischen embedded-Anwendungen mehr als ausreichend. Eine Taktfrequenz von bis zu 50 MHz soll möglich sein, hier sind 20 MHz eingestellt und völlig ausreichend.
Interrupt Handling
Ein 32-bit RISC-Prozessor hat oft (aber nicht immer) 32 Register zur allgemeinen Verwendung. Das gilt auch für den PicoRV32. Das ist für viele Anwendungsfälle großzügig und bequem, hat aber für Interrupt-Handler den Nachteil, daß beim Start des Handlers alle Register außer R0 gesichert und am Ende wiederhergestellt werden müssen. Das dauert seine Zeit. Für die hier verwendete Variante RV32IMC wurden typischerweise 700 Takte zwischen dem Interrupt-Request und dem Interrupt-Acknowledge gemessen. In dieser Zeit ist der Interrupt-Handler gestartet, hat die Register gesichert und springt zum Interrupt-Code des Anwenders. Dessen Ausführungszeit, das Wiederherstellen der Register und der Rücksprung in das unterbrochene Programm kommt zur gesamten Ausführungszeit noch dazu.
Es gibt eine RV32E-Version, die dieses Problem verringert, indem sie nur 16 Register unterstützt. Sie wird allerdings vom „IP Core Generator“ nicht angeboten.
Der Betrieb mit einem 10-MHz-Takt konnte für den asynchronen seriellen Sender ATx interruptgesteuert keinen kontinuierlichen Datenstrom bei 115 kBd erreichen. Erst durch Takterhöhung auf 20 MHz wurde das möglich. Das liegt natürlich auch daran, daß in jedem Interrupt nur ein Byte in den Sender geschrieben wird. Mit einem Fifo aus mehreren Bytes ließe sich das beheben.
Probleme
Einen schwerwiegenden Fehler im Interrupt-Handling konnte ich lokalisieren, wenngleich noch nicht beheben. Zu Beginn der sprintf-Funktion und ihren Derivaten wird die variabel lange Parameterliste ausgewertet. Das wird mit va_start() bewerkstelligt und ist ungewöhnlich, weil bei fast allen anderen C‑Funktionen die Anzahl der Parameter beim Aufruf bekannt ist. Wenn zwischen dem Funktionsaufruf und va_start() ein Interrupt akzeptiert wird (z.B. der Timer-Interrupt), dann passieren schlimme Dinge. Als Workaround werden daher vor dem Aufruf alle Interrupts disabled und nach der Ausführung von va_start() wieder enabled. Das Problem muß natürlich zügig analysiert und behoben werden, aber für den Moment muß der Workaround reichen.
Nachtrag (07.02.2026)
Die Ursache des hier beschriebenen Problems ist gefunden und behoben. Es ist ein Fehler in dem von Gowin angepassten Startup code in der Datei start.S und betrifft den Interrupt Handler. Vor dem Rücksprung in das unterbrochene Programm wird der Inhalt des x31 (=t6) Registers überschrieben. Das ist ein temporäres Register, dessen Inhalt in vielen Fällen nicht relevant ist, daher geht es meist gut. Es hat auch nichts mit der printf-Funktion zu tun, außer daß es da schnell auffällt.
Original-Code:
picorv32_getq_insn(x1, q1)
picorv32_getq_insn(x2, q2)
jal t6, return_from_irq
neuer korrekter Code:
picorv32_getq_insn(x1, q1)
picorv32_getq_insn(x2, q2)
picorv32_retirq_insn()
jal t6… springt unnötigerweise in eine C‑Funktion und speichert die Rücksprungadresse in t6. Ein Rücksprung erfolgt aber nicht, denn es ist kein Funktionsaufruf sondern die Rückkehr zum unterbrochenen Programm. Das erledigt die retirq Instruktion.
Sehr hilfreich bei der Fehlersuche war der Originalcode des PicoRV32 und das dazugehörige readme.md-File.
Next Steps…
Ich werde in den nächsten Wochen ein Redesign der Leiterplatte angehen. Dabei soll das Problem der Digitalisierung der Eingangssignale behoben und die Leiterplatte deutlich verkleinert werden, denn die vielen Testmöglichkeiten des Prototypen sind dann nicht mehr nötig.
Bis die Leiterplatte dann auf dem Tisch liegt, soll die PC-Software so angepasst werden, daß der Datenstrom analysiert und das Messergebnis im Smith-Diagramm dargestellt wird.
