Nativer FPGA-Softprozessor mit Hochsprachen-Compiler oder Song of the Mouse

Eigener FPGA-Softprozessor mit Hochsprachen-Compiler oder Song of the Mouse - Erfahrung in der Anpassung eines Hochsprachen-Compilers an den Stapelprozessorkern.

Ein häufiges Problem für Softwareprozessoren ist das Fehlen von Entwicklungswerkzeugen für sie, insbesondere wenn ihr Befehlssystem keine Teilmenge der Anweisungen eines ihrer beliebten Prozessorkerne ist. Entwickler müssen in diesem Fall dieses Problem lösen. Die direkte Lösung besteht darin, einen Assembler-Compiler zu erstellen. In modernen Realitäten ist es jedoch nicht immer bequem, in Assembler zu arbeiten, da sich das Befehlssystem im Verlauf der Projektentwicklung beispielsweise aufgrund sich ändernder Anforderungen ändern kann. Daher ist die Aufgabe der einfachen Implementierung eines Hochsprachen-Compilers (JAV) für einen Softwareprozessor relevant.

Python-Compiler - Uzh scheint ein einfaches und praktisches Toolkit für die Entwicklung von Software für Softwareprozessoren zu sein. Das Toolkit zum Definieren von Grundelementen und Makros als Funktionen der Zielsprache ermöglicht die Implementierung kritischer Stellen in der Assemblersprache des Prozessors. In diesem Dokument werden die wichtigsten Punkte der Compileranpassung für Prozessoren mit Stapelarchitektur erläutert.

Anstelle eines Epigraphs:

Wenn Sie eine erwachsene Maus nehmen
und sie vorsichtig halten und
die Nadeln hineinstecken, erhalten
Sie einen Igel.

Wenn dieser Igel,
Nase verstopft, um nicht zu atmen,
Wo tiefer, in den Fluss werfen, werden
Sie eine Halskrause bekommen.

Wenn diese Halskrause, halten Sie Ihren
Kopf in einem Schraubstock,
ziehen Sie fester am Schwanz
Sie werden eine Schlange bekommen.

Wenn dies schon,
nachdem er zwei Messer vorbereitet hat ...
Allerdings wird er wahrscheinlich sterben,
aber die Idee ist gut!


Einführung


In vielen Fällen ist es bei der Implementierung von Messgeräten und Forschungsgeräten vorzuziehen, rekonfigurierbare FPGA / FPGA-Lösungen als Hauptkern des Systems zu verwenden. Dieser Ansatz hat viele Vorteile aufgrund der Fähigkeit, einfach und schnell Änderungen an der Arbeitslogik vorzunehmen, sowie aufgrund der Hardwarebeschleunigung von Datenverarbeitungs- und Steuersignalen.

Für eine Vielzahl von Aufgaben wie digitale Signalverarbeitung, eingebettete Steuerungssysteme, Datenerfassungs- und Analysesysteme hat sich der Ansatz bewährt, der darin besteht, die von der FPGA-Logik implementierten Lösungsblöcke für kritische Prozesse und Programmsteuerelemente auf der Basis von einem oder einem zu kombinieren Mehrere Softwareprozessoren für die allgemeine Verwaltung und Koordination sowie für die Implementierung der Interaktion mit dem Benutzer oder externen Geräten / Knoten. Die Verwendung von Softwareprozessoren in diesem Fall ermöglicht es uns, den Zeitaufwand für das Debuggen und Überprüfen von Systemsteuerungsalgorithmen oder Interaktionsalgorithmen einzelner Knoten geringfügig zu reduzieren.

Typische Wunschliste


In diesem Fall erfordern Softprozessoren häufig keine ultrahohe Leistung (da dies einfacher zu erreichen ist, verwende ich die logischen FPGA- und Hardwareressourcen). Sie können recht einfach sein (und aus Sicht moderner Mikrocontroller - fast primitiv), weil Sie können auf ein komplexes Interrupt-System verzichten, arbeiten nur mit bestimmten Knoten oder Schnittstellen. Es ist nicht erforderlich, ein bestimmtes Befehlssystem zu unterstützen. Es kann viele von ihnen geben, während jeder von ihnen nur einen bestimmten Satz von Algorithmen oder Unterprogrammen ausführen kann. Die Kapazität von Softprozessoren kann auch beliebig sein, einschließlich nicht eines Vielfachen eines Bytes - abhängig von den Anforderungen der aktuellen Aufgabe.

Typische Ziele für Softprozessoren sind:

  • ausreichende Funktionalität des Befehlssystems, möglicherweise für die Aufgabe optimiert;
  • , .. ;
  • – , .

Ein Problem für Softwareprozessoren ist natürlich das Fehlen von Entwicklungswerkzeugen für sie, insbesondere wenn ihr Befehlssystem keine Teilmenge der Anweisungen eines ihrer beliebten Prozessorkerne ist. Entwickler müssen in diesem Fall dieses Problem lösen. Die direkte Lösung besteht darin, einen Assembler-Compiler für den Softwareprozessor zu erstellen. In modernen Realitäten ist es jedoch nicht immer bequem, in Assembler zu arbeiten, insbesondere wenn sich das Teamsystem während der Entwicklung des Projekts ändert, beispielsweise aufgrund sich ändernder Anforderungen. Daher ist es logisch, zu den obigen Anforderungen die Anforderung einer einfachen Implementierung eines Hochsprachen-Compilers (HLV) für den Soft-Prozessor hinzuzufügen.

Quellkomponenten


Stapelprozessoren erfüllen diese Anforderungen mit einem hohen Prozentsatz an Compliance Es besteht keine Notwendigkeit, Register zu adressieren. Die Bittiefe des Befehls kann gering sein.
Die Bittiefe der Daten für sie kann variieren und ist nicht an die Bittiefe des Befehlssystems gebunden. Da es sich de facto (wenn auch mit einigen Einschränkungen) um eine Hardware-Implementierung der Zwischendarstellung des Programmcodes während der Kompilierung handelt (eine virtuelle gestapelte Maschine oder in Bezug auf kontextfreie Grammatiken - ein Geschäftsautomat), ist es mit geringen Arbeitskosten möglich, die Grammatik einer beliebigen Sprache in ausführbaren Code zu übersetzen. Darüber hinaus ist für Stapelprozessoren die Fort-Sprache praktisch die „Muttersprache“. Die Arbeitskosten für die Implementierung eines Fort-Compilers für einen Stapelprozessor sind mit denen von Assembler vergleichbar, wobei die Implementierung von Programmen in Zukunft wesentlich flexibler und effizienter ist.

Mit der Aufgabe, ein System zum Sammeln von Daten von intelligenten Sensoren in einem Modus nahe der Echtzeit aufzubauen, wurde der Fort-Prozessor als Referenzlösung (das sogenannte Referenzdesign) des in [ 1 ] beschriebenen Soft-Prozessors ausgewählt (im Folgenden beschrieben) manchmal mit dem Spitznamen des Autors als whiteTiger-Prozessor bezeichnet).

Seine Hauptmerkmale:

  • Daten trennen und Stapel zurückgeben
  • Architektur der Harvard-Speicherorganisation (separater Programm- und Datenspeicher, einschließlich Adressraum);
  • Erweiterung mit Peripheriegeräten über einen einfachen Parallelbus.
  • Der Prozessor verwendet keine Pipeline, die Ausführung der Befehle erfolgt per Push-Pull:

    1. Befehle und Operanden abrufen;
    2. Ausführung des Befehls und Speichern des Ergebnisses.

Der Prozessor wird durch einen UART-Loader mit Programmcode ergänzt, mit dem Sie das ausführbare Programm ändern können, ohne das Projekt für FPGAs neu zu kompilieren.

In Bezug auf die Konfiguration des Blockspeichers im FPGA wird die Kapazität der Befehle auf 9 Bit eingestellt. Die Bittiefe der Daten ist auf 32 Bit eingestellt, kann aber grundsätzlich beliebig sein.

Der Prozessorcode wird in VHDL ohne Verwendung spezifischer Bibliotheken geschrieben, sodass Sie mit diesem Projekt an FPGAs aller Hersteller arbeiten können.

Für eine relativ weit verbreitete Verwendung, das Verringern der "Eingabeschwelle" sowie das Wiederverwenden von Code und das Anwenden von Codeentwicklungen ist es zweckmäßiger, auf eine andere Java-Engine als Fort umzusteigen (dies ist teilweise auf den Aberglauben und die falschen Vorstellungen von Mine-Stream-Programmierern hinsichtlich der Komplexität dieser Sprache und der Lesbarkeit ihres Codes zurückzuführen (Übrigens hat einer der Autoren dieser Arbeit eine ähnliche Meinung zu C-ähnlichen Sprachen)).

Basierend auf einer Reihe von Faktoren wurde die Python-Sprache (Python) für das Experiment ausgewählt, um den Softwareprozessor und die Java Language Engine zu „binden“. Dies ist eine allgemeine Programmiersprache auf hoher Ebene, die sich auf die Verbesserung der Entwicklerproduktivität und der Lesbarkeit von Code konzentriert und verschiedene Programmierparadigmen unterstützt, darunter strukturelle, objektorientierte, funktionale, imperative und aspektorientierte [ 2].

Für unerfahrene Entwickler ist die Erweiterung MyHDL [ 3 , 4 ] interessant , mit der Hardwareelemente und -strukturen in Python beschrieben und in VHDL- oder Verilog-Code übersetzt werden können.

Vor einiger Zeit wurde der Uzh-Compiler [ 5 ] angekündigt - ein kleiner Compiler für den Zmey-FPGA-Softwareprozessor (32-Bit-Stack-Architektur mit Multithreading-Unterstützung - wenn Sie die Kette von Versionen / Modifikationen / Verifizierungen verfolgen - Zmey ist ein entfernter Nachkomme des whiteTiger-Prozessors).
Uzh ist auch eine statisch kompilierte Teilmenge von Python, die auf dem vielversprechenden raddsl-Toolkit basiert (eine Reihe von Tools zum schnellen Erstellen von Prototypen von DSL-Compilern) [ 6 , 7 ].

Somit können die Faktoren, die die Wahl der Arbeitsrichtung beeinflusst haben, ungefähr so ​​formuliert werden:

  • Interesse an Tools, die die "Einstiegsschwelle" für unerfahrene Entwickler von Geräten und Systemen auf FPGAs senken (syntaktisch ist Python für Anfänger nicht so "beängstigend" wie VHDL);
  • Streben nach Harmonie und einem einzigen Stil im Projekt (es ist theoretisch möglich, die erforderlichen Hardwareblöcke und Software des Softwareprozessors in Python zu beschreiben);
  • zufälliger Zufall.

Kleine, „fast“ bedeutungslose Nuancen


Der Quellcode des Zmey-Prozessors ist nicht geöffnet, es ist jedoch eine Beschreibung der Funktionsprinzipien und einiger Architekturfunktionen verfügbar. Obwohl es auch stapelbar ist, gibt es eine Reihe von wesentlichen Unterschieden zum whiteTiger-Prozessor:

  • Stapel sind Software - d.h. durch Zeiger dargestellt und an verschiedenen Adressen im Datenspeicher abgelegt;
  • , - ;
  • ;
  • , .

Dementsprechend berücksichtigt der Uzh-Compiler diese Funktionen. Der Compiler akzeptiert Python-Code und generiert am Ausgang einen Boot-Stream, um den Programmspeicher und den Prozessordatenspeicher zu initiieren. Der entscheidende Punkt ist, dass alle Sprachfunktionen in der Kompilierungsphase verfügbar sind.

Um den Uzh-Compiler zu installieren, laden Sie einfach sein Archiv herunter und entpacken Sie ihn in einen beliebigen Ordner (es ist besser, die allgemeinen Empfehlungen für spezielle Software einzuhalten - um Pfade zu vermeiden, die Kyrillisch und Leerzeichen enthalten). Sie müssen auch das raddsl-Toolkit herunterladen und in den Hauptordner des Compilers entpacken.

Der Compiler-Testordner enthält Beispiele für Programme für den Soft-Prozessor, der src-Ordner enthält die Quelltexte der Compiler-Elemente. Der c.py C:\D\My_Docs\Documents\uzh-master\tests\abc.py Einfachheit halber ist es besser, eine kleine Batch-Datei (Erweiterung .cmd) mit dem Inhalt: zu erstellen, wobei abc.py der Name der Datei mit dem Programm für den Soft-Prozessor ist.

Eine Schlange beißt sich in den Schwanz oder leckt Eisen und Software


Um Uzh an den whiteTiger-Prozessor anzupassen, sind einige Änderungen erforderlich, und der Prozessor selbst muss leicht korrigiert werden.

Glücklicherweise gibt es im Compiler nicht viele Stellen, die angepasst werden müssen. Die wichtigsten "hardwareabhängigen" Dateien:

  • asm.py - Assembler und die Bildung von Zahlen (Literalen);
  • gen.py - Regeln zur Codegenerierung auf niedriger Ebene (Funktionen, Variablen, Übergänge und Bedingungen);
  • stream.py - einen Boot-Stream bilden;
  • macro.py - Makrodefinitionen - Erweiterungen der Basissprache mit hardwarespezifischen Funktionen.

Im ursprünglichen WhiteTiger-Prozessordesign initialisiert der UART-Loader nur den Programmspeicher. Der Bootloader-Algorithmus ist einfach, aber gut etabliert und zuverlässig:

  • Beim Empfang eines bestimmten Steuerbytes setzt der Loader den aktiven Pegel auf der internen Leitung des Prozessor-Resets.
  • Der zweite Bytebefehl setzt den Speicheradresszähler zurück.
  • Das Folgende ist eine Folge von Notizbüchern des übertragenen Wortes, beginnend mit dem jüngsten, kombiniert mit einer Notizbuchnummer.
  • Nach jedem Byte mit einem gepackten Notizbuch folgt ein Paar Steuerbytes, von denen das erste den aktiven Pegel in der Speicherschreibberechtigungszeile festlegt und das zweite ihn zurücksetzt.
  • Nach Abschluss der Sequenz gepackter Notebooks wird der aktive Pegel in der Rücksetzzeile durch das Steuerbyte entfernt.

Da der Compiler auch Datenspeicher verwendet, muss der Loader so geändert werden, dass er auch den Datenspeicher initialisieren kann.

Da der Datenspeicher an der Logik des Prozessorkerns beteiligt ist, müssen seine Daten- und Steuerleitungen gemultiplext werden. Hierzu werden zusätzliche Signale DataDinBtemp, LoaderAddrB, DataWeBtemp eingeführt - Daten, Adresse und Aufzeichnungsauflösung für den Port-In-Speicher.

Der Bootloader-Code sieht jetzt folgendermaßen aus:

uart_unit: entity work.uart
--uart_unit: entity uart
  Generic map(
    ClkFreq => 50_000_000,
    Baudrate => 115200)
  port map(
    clk => clk,
    rxd => rx,
    txd => tx,
    dout => receivedByte,
    received => received,
    din => transmitByte,
    transmit => transmit);
    
process(clk)
begin
  if rising_edge(clk) then
    if received = '1' then
      case conv_integer(receivedByte) is
      -- 0-F   - 0-3 bits
        when 0 to 15 => CodeDinA(3 downto 0) <= receivedByte(3 downto 0);
		                  DataDinBtemp(3 downto 0) <= receivedByte(3 downto 0);
      -- 10-1F -4-7bits
        when 16 to 31 => CodeDinA(7 downto 4) <= receivedByte(3 downto 0);
		                   DataDinBtemp(7 downto 4) <= receivedByte(3 downto 0); 
      -- 20-2F -8bit 
        when 32 to 47 => CodeDinA(8) <= receivedByte(0);
	                   DataDinBtemp(11 downto 8) <= receivedByte(3 downto 0);
	  when 48 to 63 => DataDinBtemp(15 downto 12) <= receivedByte(3 downto 0);
	  when 64 to 79 => DataDinBtemp(19 downto 16) <= receivedByte(3 downto 0);
	  when 80 to 95 => DataDinBtemp(23 downto 20) <= receivedByte(3 downto 0);
	  when 96 to 111 => DataDinBtemp(27 downto 24) <= receivedByte(3 downto 0);
        when 112 to 127 => DataDinBtemp(31 downto 28) <= receivedByte(3 downto 0);

      -- F0 addr=0
        when 240 => CodeAddrA <= (others => '0');
      -- F1 - WE=1
        when 241 => CodeWeA <= '1';
      -- F2 WE=0 addr++
        when 242 => CodeWeA <= '0'; CodeAddrA <= CodeAddrA + 1;
      -- F3 RESET=1
        when 243 => int_reset <= '1';
      -- F4 RESET=0
        when 244 => int_reset <= '0';

      -- F5 addr=0
        when 245 => LoaderAddrB <= (others => '0');
      -- F6 - WE=1
        when 246 => DataWeBtemp <= '1';
      -- F7 WE=0 addr++
        when 247 => DataWeBtemp <= '0'; LoaderAddrB <= LoaderAddrB + 1;
		  
		  
        when others => null;
      end case;
    end if;
  end if;
end process;

---- end of loader


Bei einem aktiven Rücksetzpegel werden die Signale DataDinBtemp, LoaderAddrB und DataWeBtemp mit den entsprechenden Datenspeicherports verbunden.

if reset = '1' or int_reset = '1' then
      DSAddrA <= (others => '0');      
      
      RSAddrA <= (others => '0');
      RSAddrB <= (others => '0');
      RSWeA <= '0';
      
      DataAddrB <= LoaderAddrB;
		DataDinB<=DataDinBtemp;
		DataWeB<=DataWeBtemp;
      DataWeA <= '0';

Entsprechend dem Bootloader-Algorithmus muss das Modul stream.py geändert werden. Jetzt hat es zwei Funktionen. Die erste Funktion - get_val () - teilt das Eingabewort in die gewünschte Anzahl von Tetraden auf. Für 9-Bit-Anweisungen des whiteTiger-Prozessors werden sie in Gruppen von drei Tetraden und 32-Bit-Daten in einer Folge von acht Tetraden umgewandelt. Die zweite Funktion make () bildet den Bootstrap direkt.
Die endgültige Form des Stream-Moduls:

def get_val(x, by_4):
  r = []
  for i in range(by_4):
    r.append((x & 0xf) | (i << 4))
    x >>= 4
  return r

def make(code, data, core=0):
  #        0  
  stream = [243,245] 
  for x in data:
    #    32- 
    #         
    stream += get_val(x, 8) + [246, 247]
  #       0
  stream += [240]
  for x in code:
    #    9-  
    #         
    stream += get_val(x, 3) + [241, 242]
  #  
  stream.append(244)

  return bytearray(stream)


Die folgenden Änderungen im Compiler wirken sich auf das Modul asm.py aus, das das Prozessorbefehlssystem (Befehlsmnemonik und Befehlsopcodes werden geschrieben) und die Art und Weise der Darstellung / Kompilierung numerischer Werte - Literale - beschreibt.

Befehle werden in ein Wörterbuch gepackt, und die Funktion lite () ist für Literale verantwortlich. Wenn mit dem Befehlssystem alles einfach ist - die Liste der Mnemonics und der entsprechenden Opcodes ändert sich nur, dann ist die Situation mit Literalen etwas anders. Der Zmey-Prozessor verfügt über 8-Bit-Anweisungen und es gibt eine Reihe spezieller Anweisungen zum Arbeiten mit Literalen. In whiteTiger gibt das 9. Bit an, ob der Opcode ein Befehl oder ein Teil einer Zahl ist.

Wenn das höchste (9.) Bit eines Wortes 1 ist, wird der Opcode als Zahl interpretiert - beispielsweise bilden vier aufeinanderfolgende Opcodes mit einem Vorzeichen einer Zahl eine 32-Bit-Zahl. Ein Zeichen für das Ende einer Nummer ist das Vorhandensein des Befehls-Opcodes. Um die Bestimmtheit zu gewährleisten und die Einheitlichkeit sicherzustellen, ist das Ende der Nummernbestimmung der Opcode des NOP-Befehls („keine Operationen“).

Infolgedessen sieht die modifizierte Funktion lit () folgendermaßen aus:


def lit(x):
  x &= 0xffffffff
  r = [] 
  if (x>>24) & 255 :
    r.append(int((x>>24) & 255) | 256)
  if (x>>16) & 255:
    r.append(int((x>>16) & 255) | 256)
  if (x>>8) & 255:
    r.append(int((x>>8) & 255) | 256)
  r.append(int(x & 255) | 256)
  r += asm("NOP")
  return list(r)


Die wichtigsten und wichtigsten Änderungen / Definitionen befinden sich im gen.py-Modul. Dieses Modul definiert die grundlegende Logik der Arbeit / Ausführung von Code auf hoher Ebene auf Assembler-Ebene:

  • bedingte und bedingungslose Sprünge;
  • Funktionen aufrufen und Argumente an sie übergeben;
  • Rückkehr von Funktionen und Rückgabe von Ergebnissen;
  • Anpassungen der Größe des Programmspeichers, des Datenspeichers und der Stapel;
  • Abfolge von Aktionen beim Start des Prozessors.

Um Java zu unterstützen, muss der Prozessor in der Lage sein, beliebig mit Speicher und Zeigern zu arbeiten und über einen Speicherbereich zum Speichern lokaler Variablenfunktionen verfügen.

Im Zmey-Prozessor wird ein Rückgabestapel verwendet, um mit lokalen Variablen und Funktionsargumenten zu arbeiten - Funktionsargumente werden darauf übertragen und während der weiteren Arbeit wird über das Zeigerregister des Rückgabestapels auf sie zugegriffen (Lesen, Ändern nach oben / unten, Lesen an der Zeigeradresse). Da sich der Stapel physisch im Datenspeicher befindet, werden solche Operationen im Wesentlichen einfach auf Speicheroperationen zurückgeführt, und globale Variablen befinden sich im selben Speicher.

In whiteTiger sind Rückgabe- und Datenstapel dedizierte Hardware-Stapel mit ihrem Adressraum und haben keine Stapelzeigeranweisungen. Folglich müssen Operationen mit Übergabe von Argumenten an Funktionen und Arbeiten mit lokalen Variablen über den Datenspeicher organisiert werden. Es ist wenig sinnvoll, das Volumen der Datenstapel zu erhöhen und für die mögliche Speicherung relativ großer Datenfelder zurückzugeben. Es ist logischer, einen etwas großen Datenspeicher zu haben.

Um mit lokalen Variablen zu arbeiten, wurde ein dediziertes LocalReg-Register hinzugefügt, dessen Aufgabe darin besteht, einen Zeiger auf den für lokale Variablen zugewiesenen Speicherbereich (eine Art Heap) zu speichern. Es wurden auch Operationen für die Arbeit damit hinzugefügt (Datei cpu.vhd - Befehlsdefinitionsbereich):


          -- group 1; pop 0; push 1;
          when cmdLOCAL => DSDinA <= LocalReg;
			 when cmdLOCALadd => DSDinA <= LocalReg; LocalReg <= LocalReg+1;
			 when cmdLOCALsubb => DSDinA <= LocalReg; LocalReg <= LocalReg-1;
          -- group 2; pop 1; push 0;
          when cmdSETLOCAL => LocalReg <= DSDinA;

LOCAL - Gibt den aktuellen Wert des LocalReg-Zeigers an den Datenstapel zurück.
SETLOCAL - Legt den neuen Zeigerwert fest, der vom Datenstapel empfangen wird.
LOCALadd - belässt den aktuellen Wert des Zeigers auf dem Datenstapel und erhöht ihn um 1;
LOCALsubb - Belässt den aktuellen Wert des Zeigers auf dem Datenstapel und verringert ihn um 1.
LOCALadd und LOCALsubb werden hinzugefügt, um die Anzahl der Ticks während der Übergabe von Funktionsparametern zu verringern und umgekehrt.

Im Gegensatz zum ursprünglichen whiteTiger wurden die Datenspeicherverbindungen geringfügig geändert. Jetzt wird der In-Speicher-Port ständig von der Ausgabe der ersten Zelle des Datenstapels adressiert. Die Ausgabe der zweiten Zelle des Datenstapels wird ihrer Eingabe zugeführt:

-- ++
DataAddrB <= DSDoutA(DataAddrB'range);
DataDinB <= DSDoutB;

Die Logik zum Ausführen der Befehle STORE und FETCH wurde ebenfalls geringfügig korrigiert. FETCH empfängt den Ausgabewert des Ports im Speicher oben im Datenstapel, und STORE steuert einfach das Schreibfreigabesignal für Port B:

-- group 3; pop 1; push 1;
          when cmdFETCH => DSDinA <= DataDoutB;
          when cmdSTORE =>            
            DataWeB <= '1';

Im Rahmen der Schulung sowie für einige Hardwareunterstützungen für Schleifen auf niedriger Ebene (und auf Compilerebene der Fort-Sprache) wurde dem whiteTiger-Kern ein Stapel von Schleifenzählern hinzugefügt (Aktionen ähneln denen beim Deklarieren von Daten und Rückgabestapeln):

--  
type TCycleStack is array(0 to LocalSize-1) of DataSignal;
signal CycleStack: TCycleStack;
signal CSAddrA, CSAddrB: StackAddrSignal;
signal CSDoutA, CSDoutB: DataSignal;
signal CSDinA, CSDinB: DataSignal;
signal CSWeA, CSWeB: std_logic;
--  
process(clk)
begin
  if rising_edge(clk) then
    if CSWeA = '1' then
      CycleStack(conv_integer(CSAddrA)) <= CSDinA;
      CSDoutA <= CSDinA;
    else
      CSDoutA <= CycleStack(conv_integer(CSAddrA));
    end if;
  end if;
end process;


Zykluszählerbefehle wurden hinzugefügt.

DO - verschiebt die Anzahl der Iterationen des Zyklus vom Datenstapel zum Zählerstapel und legt den inkrementierten Wert des Befehlszählers auf den Rückgabestapel.

LOOP - prüft, ob der Zähler auf Null gesetzt ist. Wenn er nicht erreicht wird, wird das oberste Element des Zählerstapels dekrementiert und der Übergang zur Adresse am oberen Rand des Rückgabestapels ausgeführt. Wenn die Oberseite des Zählerstapels Null ist, wird das obere Element zurückgesetzt, und die Rücksprungadresse zum Beginn des Zyklus von der Oberseite des Rückgabestapels wird ebenfalls zurückgesetzt.


	when cmdDO => -- DO - 
               RSAddrA <= RSAddrA + 1; -- 
               RSDinA <= ip + 1;
               RSWeA <= '1';
				
               CSAddrA <= CSAddrA + 1; --
         		CSDinA <= DSDoutA;
 		         CSWeA <= '1';
		         DSAddrA <= DSAddrA - 1; --
		         ip <= ip + 1;	-- 

      when cmdLOOP => --            
           if conv_integer(CSDoutA) = 0 then
	          ip <= ip + 1;	-- 
		         RSAddrA <= RSAddrA - 1; -- 
		         CSAddrA <= CSAddrA - 1; -- 
            else
		         CSDinA <= CSDoutA - 1;
		         CSWeA <= '1';
		         ip <= RSDoutA(ip'range);
            end if;
			 

Jetzt können Sie den Code für das gen.py-Modul ändern.

* _SIZE-Variablen benötigen keine Kommentare und erfordern nur die Ersetzung der im Prozessorkernprojekt angegebenen Werte.

Die STUB-Liste ist ein temporärer Stub, um einen Platz für Übergangsadressen zu erstellen und diese dann mit dem Compiler zu füllen (aktuelle Werte entsprechen dem 24-Bit-Adressraum des Codespeichers).

STARTUP-Liste - Legt die Reihenfolge der Aktionen fest, die der Kernel nach einem Zurücksetzen ausführt. In diesem Fall wird die Startadresse des Speichers lokaler Variablen auf 900 und der Übergang zum Startpunkt festgelegt (wenn Sie nichts ändern, wird der Start- / Einstiegspunkt in der Anwendung in den Datenspeicher in der Datenspeicheradresse geschrieben 2):

STARTUP = asm("""
900  SETLOCAL
2 NOP FETCH JMP
""")

Die Definition von func () schreibt die Aktionen vor, die ausgeführt werden, wenn die Funktion aufgerufen wird, nämlich die Übertragung von Funktionsargumenten in den Bereich lokaler Variablen und die Speicherzuordnung für ihre eigenen lokalen Variablen der Funktion.

@act
def func(t, X):
  t.c.entry = t.c.globs[X]
  t.c.entry["offs"] = len(t.c.code) # - 1
  args = t.c.entry["args"]
  temps_size = len(t.c.entry["locs"]) - args
#      
  t.out = asm("LOCALadd STORE " * args)
  if temps_size:
#      
    t.out += asm("LOCAL %d PLUS SETLOCAL" % temps_size)
  return True

Epilog () definiert Aktionen bei der Rückkehr von einer Funktion. Dabei wird der Speicher temporärer Variablen freigegeben und zum Aufrufpunkt zurückgekehrt.

def epilog(t, X):
  locs_size = len(t.c.entry["locs"])
#    
  t.out = asm("RET")
  if locs_size:
#    ()  
    t.out = asm("LOCAL %d MINUS SETLOCAL" % locs_size) + t.out
  return True


Die Arbeit mit Variablen erfolgt über ihre Adressen. Die Schlüsseldefinition hierfür lautet push_local (), wodurch die Adresse der Variablen "high-level" auf dem Datenstapel verbleibt.

def push_local(t, X):
#          
#  
  t.out = asm("LOCAL %d MINUS" % get_loc_offset(t, X))
  return True

Die folgenden Schlüsselpunkte sind bedingte und bedingungslose Übergänge. Der bedingte Sprung im whiteTiger-Prozessor überprüft das zweite Element des Datenstapels auf 0 und springt zur Adresse oben im Stapel, wenn die Bedingung erfüllt ist. Ein bedingungsloser Sprung setzt einfach den Wert des Befehlszählers auf den Wert oben im Stapel.

@act
def goto_if_0(t, X):
  push_label(t, X)
  t.out += asm("IF")
  return True

@act
def goto(t, X):
  push_label(t, X)
  t.out += asm("JMP")
  return True


Die folgenden zwei Definitionen spezifizieren Bitverschiebungsoperationen - nur auf einer niedrigen Ebene werden Schleifen angewendet (dies führt zu einer gewissen Verbesserung der Codegröße) - im Original setzt der Compiler einfach die erforderliche Anzahl von Elementarverschiebungsoperationen in eine Reihe.

@act
def shl_const(t, X):
  t.out = asm("%d DO SHL LOOP" %(X-1))
  return True

@act
def shr_const(t, X):
  t.out = asm("%d DO SHR LOOP" %(X-1))
  return True

Die Hauptdefinition des Compilers auf niedriger Ebene ist ein Satz von Regeln für Sprachoperationen und das Arbeiten mit dem Speicher:

stmt = rule(alt(
  seq(Push(Int(X)), to(lambda v: asm("%d" % v.X))),
  seq(Push(Local(X)), push_local),
  seq(Push(Global(X)), push_global),
  seq(Load(), to(lambda v: asm("NOP FETCH"))),
  seq(Store(), to(lambda v: asm("STORE"))),
  seq(Call(), to(lambda v: asm("CALL"))),
  seq(BinOp("+"), to(lambda v: asm("PLUS"))),
  seq(BinOp("-"), to(lambda v: asm("MINUS"))),
  seq(BinOp("&"), to(lambda v: asm("AND"))),
  seq(BinOp("|"), to(lambda v: asm("OR"))),
  seq(BinOp("^"), to(lambda v: asm("XOR"))),
  seq(BinOp("*"), to(lambda v: asm("MUL"))),
  seq(BinOp("<"), to(lambda v: asm("LESS"))),
  seq(BinOp(">"), to(lambda v: asm("GREATER"))),
  seq(BinOp("=="), to(lambda v: asm("EQUAL"))),
  seq(BinOp("~"), to(lambda v: asm("NOT"))),
  seq(ShlConst(X), shl_const),
  seq(ShrConst(X), shr_const),
  seq(Func(X), func),
  seq(Label(X), label),
  seq(Return(X), epilog),
  seq(GotoIf0(X), goto_if_0),
  seq(Goto(X), goto),
  seq(Nop(), to(lambda v: asm("NOP"))),
  seq(Asm(X), to(lambda v: asm(v.X)))
))

Mit dem Modul macro.py können Sie das Wörterbuch der Zielsprache mithilfe von Makrodefinitionen im Assembler des Zielprozessors etwas erweitern. Für den Java-Compiler unterscheiden sich die Definitionen in macro.py nicht von den "nativen" Operatoren und Funktionen der Sprache. So wurden beispielsweise im ursprünglichen Compiler E / A-Funktionen des Werts im externen Port definiert. Testsequenzen von Operationen mit Speicher und lokalen Variablen und eine Zeitverzögerungsoperation wurden hinzugefügt.

@macro(1,0)
def testasm(c,x):
  return Asm("1 1 OUTPORT 0 1 OUTPORT 11 10 STORE 10 FETCH 1 OUTPORT  15 100 STORE 100  FETCH 1 OUTPORT")

@macro(1,0)
def testlocal(c,x):
   return Asm("1 100 STORE 2 101 STORE 100 SETLOCAL LOCAL NOP FETCH 1 OUTPORT LOCAL 1 PLUS NOP FETCH 1 OUTPORT")

@prim(1, 0)
def delay(c, val):
  return [val, Asm("DO LOOP")]


Testen


Ein kleines Testprogramm auf hoher Ebene für unseren Prozessor enthält die Definition einer Funktion zur Berechnung der Fakultät und die Hauptfunktion, die die serielle Ausgabe von Fakultätswerten von 1 bis 7 an den Port in einer Endlosschleife implementiert.

def fact(n):
  r = 1
  while n > 1:
    r *= n
    n -= 1
  return r


def main():
  n=1
  while True:
     digital_write(1, fact(n))
     delay(10)
     n=(n+1)&0x7


Es kann zum Kompilieren beispielsweise durch ein einfaches Skript oder über die Befehlszeile in der folgenden Reihenfolge gestartet werden : Als Ergebnis wird eine Bootdatei stream.bin generiert, die über die serielle Schnittstelle (in modernen Realitäten über eine von Konvertern bereitgestellte virtuelle serielle Schnittstelle) auf den Prozessorkern im FPGA übertragen werden kann USB-UART-Schnittstellen). Das Programm belegt somit 146 Wörter (9 Bit) des Programmspeichers und 3 im Datenspeicher.
c.py C:\D\My_Docs\Documents\uzh-master\tests\fact2.py




Fazit


Im Allgemeinen scheint der Uzh-Compiler ein einfaches und praktisches Toolkit für die Entwicklung von Software für Softwareprozessoren zu sein. Es ist eine großartige Alternative zum Assembler, zumindest im Hinblick auf die Benutzerfreundlichkeit des Programmierers. Das Toolkit zum Definieren von Grundelementen und Makros als Funktionen der Zielsprache ermöglicht die Implementierung kritischer Stellen in der Assemblersprache des Prozessors. Für Prozessoren mit Stapelarchitektur ist das Compiler-Anpassungsverfahren nicht zu kompliziert und langwierig. Wir können sagen, dass dies nur dann der Fall ist, wenn die Verfügbarkeit des Quellcodes des Compilers hilft - die Schlüsselabschnitte des Compilers ändern sich.

Die Ergebnisse der Prozessorsynthese (32-Bit-Kapazität, 4 KB Programmspeicher und 1 KB RAM) für die FPGA Altera Cyclone V-Serie ergeben Folgendes:

Family	Cyclone V
Device	5CEBA4F23C7
Logic utilization (in ALMs)	694 / 18,480 ( 4 % )
Total registers	447
Total pins	83 / 224 ( 37 % )
Total virtual pins	0
Total block memory bits	72,192 / 3,153,920 ( 2 % )
Total DSP Blocks	2 / 66 ( 3 % )

Literatur

  1. Vierter Prozessor auf VHDL // m.habr.com/de/post/149686
  2. Python - Wikipedia // en.wikipedia.org/wiki/Python
  3. Wir beginnen FPGA auf Python _ Habr // m.habr.com/en/post/439638
  4. MyHDL // www.myhdl.org
  5. GitHub - true-grue_uzh_ Uzh-Compiler // github.com/true-grue/uzh
  6. GitHub - true-grue_raddsl_ Tools für das Rapid Prototyping von DSL-Compilern // github.com/true-grue/raddsl
  7. sovietov.com/txt/dsl_python_conf.pdf

Der Autor dankt den Entwicklern des Zmey-Softwareprozessors und des Uzh-Compilers für Beratung und Geduld.

All Articles