Processeur logiciel FPGA natif avec compilateur de langage de haut niveau ou Song of the Mouse

Propre processeur logiciel FPGA avec compilateur de langage de haut niveau ou Song of the Mouse - expérience dans l'adaptation d'un compilateur de langage de haut niveau au cœur du processeur de pile.

Un problème commun pour les processeurs logiciels est le manque d'outils de développement pour eux, surtout si leur système d'instruction n'est pas un sous-ensemble des instructions de l'un de leurs cœurs de processeur populaires. Les développeurs dans ce cas devront résoudre ce problème. Sa solution directe est de créer un compilateur de langage assembleur. Cependant, dans les réalités modernes, il n'est pas toujours commode de travailler dans Assembler, car dans le processus de développement du projet, le système de commande peut changer en raison, par exemple, des exigences changeantes. Par conséquent, la tâche d'implémentation facile d'un compilateur de langage de haut niveau (JAV) pour un processeur logiciel est pertinente.

Compilateur Python - Uzh semble être une boîte à outils simple et pratique pour développer des logiciels pour les processeurs logiciels. La boîte à outils pour définir des primitives et des macros en tant que fonctions du langage cible permet d'implémenter des endroits critiques dans le langage d'assemblage du processeur. Cet article traite des principaux points de l'adaptation du compilateur pour les processeurs d'architecture de pile.

Au lieu d'une Ă©pigraphe:

Si vous prenez une souris adulte
et, en la tenant soigneusement,
fourrez-y les aiguilles,
vous obtiendrez un hérisson.

Si ce hérisson,
Nez bouché, pour ne pas respirer,
Où plus profondément, jetez dans la rivière
Vous obtiendrez une collerette.

Si cette collerette, Tenant votre
tĂŞte dans un Ă©tau,
Tirez plus fort par la queue,
vous obtiendrez un serpent.

Si cela déjà,
Ayant préparé deux couteaux ...
Cependant, il mourra probablement,
Mais l'idée est bonne!


introduction


Dans de nombreux cas, lors de la mise en œuvre d'instruments de mesure, d'équipements de recherche, il est préférable d'utiliser des solutions FPGA / FPGA reconfigurables comme cœur du système. Cette approche présente de nombreux avantages, en raison de la possibilité d'apporter facilement et rapidement des modifications à la logique de travail, ainsi qu'en raison de l'accélération matérielle du traitement des données et des signaux de contrôle.

Pour un large éventail de tâches, telles que le traitement numérique du signal, les systèmes de contrôle intégrés, les systèmes d'acquisition et d'analyse de données, l'approche a fait ses preuves, consistant à combiner en une seule solution des blocs mis en œuvre par la logique FPGA pour les processus critiques et des éléments de contrôle de programme basés sur un ou plusieurs processeurs logiciels pour la gestion générale et la coordination, ainsi que pour la mise en œuvre de l'interaction avec l'utilisateur ou les périphériques / nœuds externes. L'utilisation de processeurs logiciels dans ce cas nous permet de réduire légèrement le temps consacré au débogage et à la vérification des algorithmes de contrôle du système ou des algorithmes d'interaction des nœuds individuels.

Liste de souhaits typique


Souvent, les processeurs logiciels dans ce cas ne nécessitent pas d'ultra-hautes performances (comme c'est plus facile à réaliser, j'utilise les ressources logiques et matérielles FPGA). Ils peuvent être assez simples (et du point de vue des microcontrôleurs modernes - presque primitifs), car ils peuvent se passer d'un système d'interruption complexe, ne fonctionner qu'avec certains nœuds ou interfaces, il n'est pas nécessaire de prendre en charge un système de commande particulier. Il peut y en avoir plusieurs, tandis que chacun d'eux ne peut exécuter qu'un certain ensemble d'algorithmes ou de sous-programmes. La capacité des processeurs logiciels peut également être quelconque, y compris pas un multiple d'un octet - en fonction des exigences de la tâche en cours.

Les cibles typiques pour les processeurs logiciels sont:

  • fonctionnalitĂ© suffisante du système de commande, Ă©ventuellement optimisĂ©e pour la tâche;
  • , .. ;
  • – , .

Bien sûr, un problème pour les processeurs logiciels est le manque d'outils de développement pour eux, surtout si leur système d'instruction n'est pas un sous-ensemble des instructions de l'un de leurs cœurs de processeur populaires. Les développeurs dans ce cas devront résoudre ce problème. Sa solution directe est de créer un compilateur de langage assembleur pour le processeur logiciel. Cependant, dans les réalités modernes, il n'est pas toujours pratique de travailler dans Assembler, en particulier si le système d'équipe change pendant le développement du projet en raison, par exemple, de l'évolution des exigences. Par conséquent, il est logique d'ajouter aux exigences ci-dessus l'exigence d'une mise en œuvre facile d'un compilateur de langage de haut niveau (HLV) pour le processeur logiciel.

Composants source


Les processeurs de pile satisfont à ces exigences avec un pourcentage élevé de conformité, comme il n'est pas nécessaire d'adresser les registres, la profondeur de bits de la commande peut être petite.
La profondeur de bits des données peut varier et n'est pas liée à la profondeur de bits du système de commande. Étant une implémentation matérielle de facto (quoique avec quelques mises en garde) de la représentation intermédiaire du code de programme lors de la compilation (une machine virtuelle empilée, ou en termes de grammaires hors contexte - un automate de magasin), il est possible avec de faibles coûts de main-d'œuvre de traduire la grammaire de n'importe quelle langue en code exécutable. De plus, pour les processeurs de pile, la langue Fort est pratiquement la langue «native». Les coûts de main-d'œuvre de la mise en œuvre d'un compilateur Fort pour un processeur de pile sont comparables à ceux d'Assembler, avec une flexibilité et une efficacité beaucoup plus grandes dans la mise en œuvre des programmes à l'avenir.

Ayant pour tâche de construire un système de collecte de données de capteurs intelligents dans un mode proche du temps réel, le processeur Fort a été choisi comme solution de référence (le soi-disant Design de référence) du processeur logiciel, décrit dans [ 1 ] (ci-après il sera parfois appelé processeur whiteTiger par le surnom de son auteur).

Ses principales caractéristiques:

  • DonnĂ©es distinctes et piles de retour
  • Architecture d'organisation de la mĂ©moire de Harvard (programme sĂ©parĂ© et mĂ©moire de donnĂ©es, y compris l'espace d'adressage);
  • extension avec des pĂ©riphĂ©riques Ă  l'aide d'un simple bus parallèle.
  • Le processeur n'utilise pas de pipeline, l'exĂ©cution des commandes est push-pull:

    1. récupérer les commandes et les opérandes;
    2. exécution de la commande et sauvegarde du résultat.

Le processeur est complété par un chargeur UART de code de programme, qui vous permet de modifier le programme exécutable sans recompiler le projet pour les FPGA.

En ce qui concerne la configuration de la mémoire de bloc dans le FPGA, la capacité des instructions est fixée à 9 bits. La profondeur de bits des données est définie sur 32 bits, mais peut être pratiquement n'importe laquelle.

Le code du processeur est écrit en VHDL sans l'utilisation de bibliothèques spécifiques, ce qui vous permet de travailler avec ce projet sur des FPGA de n'importe quel fabricant.

Pour une utilisation relativement répandue, en abaissant le "seuil d'entrée", ainsi que pour réutiliser du code et appliquer des développements de code, il est plus judicieux de passer à un moteur Java autre que Fort (cela est en partie dû aux superstitions et aux idées fausses des programmeurs de mines concernant la complexité de ce langage et la lisibilité de son code (à propos, l'un des auteurs de ce travail a une opinion similaire sur les langages de type C)).

Sur la base d'un certain nombre de facteurs, le langage Python (Python) a été choisi pour l'expérience pour «lier» le processeur logiciel et le moteur de langage Java. Il s'agit d'un langage de programmation polyvalent de haut niveau axé sur l'amélioration de la productivité des développeurs et la lisibilité du code, prenant en charge plusieurs paradigmes de programmation, notamment structurels, orientés objet, fonctionnels, impératifs et orientés aspect [ 2].

Pour les développeurs débutants, son extension MyHDL [ 3 , 4 ] est intéressante , ce qui permet de décrire des éléments matériels et des structures en Python et de les traduire en VHDL ou en code Verilog.

Il y a quelque temps, le compilateur Uzh [ 5 ] a été annoncé - un petit compilateur pour le processeur logiciel Zmey FPGA (architecture de pile 32 bits avec support multithreading - si vous suivez la chaîne des versions / modifications / vérification - Zmey est un lointain descendant du processeur whiteTiger).
Uzh est également un sous-ensemble de Python compilé statiquement, basé sur la boîte à outils raddsl prometteuse (un ensemble d'outils pour créer rapidement des prototypes de compilateurs DSL) [ 6 , 7 ].

Ainsi, les facteurs qui ont influencé le choix de la direction du travail peuvent être formulés approximativement comme ceci:

  • intĂ©rĂŞt pour les outils qui abaissent le "seuil d'entrĂ©e" pour les dĂ©veloppeurs novices de dispositifs et de systèmes sur FPGA (syntaxiquement, Python n'est pas aussi "effrayant" pour un dĂ©butant que VHDL);
  • rechercher l'harmonie et un style unique dans le projet (il est thĂ©oriquement possible de dĂ©crire les blocs matĂ©riels et logiciels requis du processeur logiciel en Python);
  • coĂŻncidence alĂ©atoire.

Petites nuances «presque» dénuées de sens


Le code source du processeur Zmey n'est pas ouvert, mais une description des principes de son fonctionnement et de certaines fonctionnalités d'architecture est disponible. Bien qu'il soit également empilable, il existe un certain nombre de différences clés par rapport au processeur whiteTiger:

  • les piles sont des logiciels - c.-Ă -d. reprĂ©sentĂ©s par des pointeurs et placĂ©s dans la mĂ©moire de donnĂ©es Ă  diffĂ©rentes adresses;
  • , - ;
  • ;
  • , .

Par conséquent, le compilateur Uzh prend en compte ces fonctionnalités. Le compilateur accepte le code Python et génère un flux de démarrage pour la sortie de la mémoire du programme et de la mémoire des données du processeur à la sortie, le point clé est que toutes les fonctionnalités du langage sont disponibles au stade de la compilation.

Pour installer le compilateur Uzh, téléchargez simplement son archive et décompressez-la dans n'importe quel dossier pratique (il est préférable de respecter les recommandations générales pour les logiciels spécialisés - évitez les chemins contenant du cyrillique et des espaces). Vous devez également télécharger et décompresser la boîte à outils raddsl dans le dossier principal du compilateur.

Le dossier de test du compilateur contient des exemples de programmes pour le processeur logiciel; le dossier src contient les textes sources des éléments du compilateur. Pour plus de commodité, il est préférable de créer un petit fichier batch (extension .cmd) avec le contenu :, c.py C:\D\My_Docs\Documents\uzh-master\tests\abc.py où abc.py est le nom du fichier avec le programme pour le processeur logiciel.

Un serpent se mordant la queue ou clapotant du fer et des logiciels


Pour adapter Uzh au processeur whiteTiger, certaines modifications seront nécessaires, ainsi que le processeur lui-même devra être légèrement corrigé.

Heureusement, il n'y a pas beaucoup d'endroits à ajuster dans le compilateur. Les principaux fichiers "dépendants du matériel":

  • asm.py - assembleur et formation de nombres (littĂ©raux);
  • gen.py - règles de gĂ©nĂ©ration de code de bas niveau (fonctions, variables, transitions et conditions);
  • stream.py - formation d'un flux de dĂ©marrage;
  • macro.py - dĂ©finitions de macro, en fait - extensions du langage de base avec des fonctions spĂ©cifiques au matĂ©riel.

Dans la conception originale du processeur whiteTiger, le chargeur UART initialise uniquement la mémoire du programme. L'algorithme du chargeur de démarrage est simple, mais bien établi et fiable:

  • Ă  la rĂ©ception d'un certain octet de commande, le chargeur dĂ©finit le niveau actif sur la ligne interne de la rĂ©initialisation du processeur;
  • la deuxième commande d'octets rĂ©initialise le compteur d'adresses mĂ©moire;
  • ce qui suit est une sĂ©quence de cahiers du mot transmis, en commençant par le plus jeune, combinĂ©e avec un numĂ©ro de cahier;
  • après chaque octet avec un bloc-notes emballĂ©, une paire d'octets de contrĂ´le suit, dont le premier dĂ©finit le niveau actif sur la ligne d'autorisation d'Ă©criture en mĂ©moire, le second le rĂ©initialise;
  • Ă  la fin de la sĂ©quence de cahiers emballĂ©s, le niveau actif sur la ligne de rĂ©initialisation est supprimĂ© par l'octet de commande.

Étant donné que le compilateur utilise également la mémoire de données, il est nécessaire de modifier le chargeur afin qu'il puisse également initialiser la mémoire de données.

La mémoire de données étant impliquée dans la logique du coeur du processeur, il est nécessaire de multiplexer ses lignes de données et de contrôle. Pour cela, des signaux supplémentaires DataDinBtemp, LoaderAddrB, DataWeBtemp sont introduits - données, adresse et résolution d'enregistrement pour le port en mémoire.

Le code du chargeur de démarrage ressemble maintenant à ceci:

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


Avec un niveau de réinitialisation actif, les signaux DataDinBtemp, LoaderAddrB, DataWeBtemp sont connectés aux ports de mémoire de données correspondants.

…
    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';
…

Conformément à l'algorithme du chargeur de démarrage, il est nécessaire de modifier le module stream.py. Maintenant, il a deux fonctions. La première fonction - get_val () - divise le mot d'entrée en le nombre souhaité de tétrades. Ainsi, pour les instructions 9 bits du processeur whiteTiger, elles seront transformées en groupes de trois tétrades et en données 32 bits dans une séquence de huit tétrades. La deuxième fonction make () forme directement le bootstrap.
La forme finale du module de flux:

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)


Les modifications suivantes du compilateur affecteront le module asm.py, qui décrit le système de commande du processeur (les mnémoniques de commande et les opcodes de commande sont écrits) et la manière de représenter / compiler les valeurs numériques - littéraux.

Les commandes sont regroupées dans un dictionnaire et la fonction lite () est responsable des littéraux. Si tout est simple avec le système de commande - la liste des mnémoniques et les opcodes correspondants ne font que changer, alors la situation avec les littéraux est un peu différente. Le processeur Zmey a des instructions 8 bits et il existe un certain nombre d'instructions spécialisées pour travailler avec des littéraux. Dans whiteTiger, le 9e bit indique si l'opcode est une commande ou une partie d'un nombre.

Si le bit le plus élevé (9e) d'un mot est 1, alors l'opcode est interprété comme un nombre - par exemple, quatre opcodes consécutifs avec un signe de nombre forment un nombre de 32 bits. Un signe de la fin d'un nombre est la présence de l'opcode de commande - pour être précis et assurer l'uniformité, la fin de la détermination du nombre est l'opcode de la commande NOP («aucune opération»).

Par conséquent, la fonction lit () modifiée ressemble à ceci:


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)


Les modifications / définitions principales et les plus importantes se trouvent dans le module gen.py. Ce module définit la logique de base du travail / exécution de code de haut niveau au niveau assembleur:

  • sauts conditionnels et inconditionnels;
  • appeler des fonctions et leur passer des arguments;
  • retour des fonctions et retour des rĂ©sultats;
  • ajustements des tailles de la mĂ©moire de programme, de la mĂ©moire de donnĂ©es et des piles;
  • sĂ©quence d'actions au dĂ©marrage du processeur.

Afin de prendre en charge Java, le processeur doit être capable de travailler arbitrairement avec la mémoire et les pointeurs et avoir une zone de mémoire pour stocker les fonctions variables locales.

Dans le processeur Zmey, une pile de retour est utilisée pour travailler avec les variables locales et les arguments de fonction - les arguments de fonction lui sont transférés et pendant les travaux ultérieurs, ils sont accessibles via le registre de pointeur de la pile de retour (lecture, modification haut / bas, lecture à l'adresse du pointeur). Étant donné que la pile est physiquement située dans la mémoire de données, ces opérations se résument essentiellement à des opérations de mémoire et les variables globales sont situées dans la même mémoire.

Dans whiteTiger, les piles de retour et de données sont des piles matérielles dédiées avec leur espace d'adressage et n'ont pas d'instructions de pointeur de pile. Par conséquent, les opérations avec passage d'arguments aux fonctions et travail avec des variables locales devront être organisées via la mémoire de données. Il n'est pas très logique d'augmenter le volume de piles de données et de retours pour le stockage possible de matrices de données relativement volumineuses, il est plus logique d'avoir une mémoire de données légèrement grande.

Pour travailler avec des variables locales, un registre LocalReg dédié a été ajouté, dont la tâche est de stocker un pointeur sur la zone mémoire allouée aux variables locales (une sorte de tas). Également ajouté des opérations pour travailler avec lui (fichier cpu.vhd - zone de définition de commande):


          -- 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 - renvoie à la pile de données la valeur actuelle du pointeur LocalReg;
SETLOCAL - définit la nouvelle valeur de pointeur reçue de la pile de données;
LOCALadd - laisse la valeur actuelle du pointeur sur la pile de données et l'incrémente de 1;
LOCALsubb - laisse la valeur actuelle du pointeur sur la pile de données et la diminue de 1.
LOCALadd et LOCALsubb sont ajoutés pour réduire le nombre de ticks pendant les opérations de passage des paramètres de fonction et vice versa.

Contrairement au whiteTiger d'origine, les connexions de la mémoire de données ont été légèrement modifiées - maintenant le port In memory est constamment adressé par la sortie de la première cellule de la pile de données, la sortie de la deuxième cellule de la pile de données est alimentée à son entrée:

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

La logique d'exécution des commandes STORE et FETCH a également été légèrement corrigée - FETCH reçoit la valeur de sortie du port En mémoire en haut de la pile de données, et STORE contrôle simplement le signal d'autorisation d'écriture pour le port B:

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

Dans le cadre de la formation, ainsi que pour une prise en charge matérielle des boucles de bas niveau (et au niveau du compilateur du langage Fort), une pile de compteurs de boucles a été ajoutée au noyau whiteTiger (les actions sont similaires à celles lors de la déclaration des données et des retours de piles):

…
--  
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;


Des commandes de compteur de cycles ont été ajoutées.

DO - déplace le nombre d'itérations du cycle de la pile de données vers la pile de compteur et place la valeur incrémentée du compteur d'instructions sur la pile de retour.

LOOP - vérifie la remise à zéro du compteur, si elle n'est pas atteinte, l'élément supérieur de la pile de compteur est décrémenté, la transition vers l'adresse en haut de la pile de retour est effectuée. Si le haut de la pile de compteur est à zéro, l'élément supérieur est réinitialisé, l'adresse de retour au début du cycle à partir du haut de la pile de retour est également réinitialisée.


	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;
			 

Vous pouvez maintenant commencer Ă  modifier le code du module gen.py.

* Les variables _SIZE n'ont pas besoin de commentaires et nécessitent uniquement la substitution des valeurs spécifiées dans le projet de processeur principal.

La liste STUB est un stub temporaire permettant de créer un emplacement pour les adresses de transition, puis de les remplir avec le compilateur (les valeurs actuelles correspondent à l'espace d'adressage 24 bits de la mémoire de code).

Liste STARTUP - définit la séquence d'actions effectuées par le noyau après une réinitialisation - dans ce cas, l'adresse de départ de la mémoire des variables locales est définie sur 900 et la transition vers le point de départ (si vous ne changez rien, le point de départ / d'entrée dans l'application est écrit sur le compilateur dans l'adresse de mémoire de données) 2):

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

La définition de func () prescrit les actions qui sont effectuées lors de l'appel de la fonction, à savoir le transfert des arguments de la fonction à la région des variables locales, l'allocation de mémoire pour ses propres variables locales de la fonction.

@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 () définit les actions lors du retour d'une fonction - libérer la mémoire des variables temporaires, revenir au point d'appel.

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


Le travail avec les variables se fait via leurs adresses, la définition clé pour cela est push_local (), qui laisse l'adresse de la variable "de haut niveau" sur la pile de données.

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

Les points clés suivants sont des transitions conditionnelles et inconditionnelles. Le saut conditionnel dans le processeur whiteTiger vérifie le deuxième élément de la pile de données pour 0 et saute à l'adresse en haut de la pile si la condition est remplie. Un saut inconditionnel définit simplement la valeur du compteur d'instructions à la valeur en haut de la pile.

@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


Les deux définitions suivantes spécifient les opérations de décalage de bits - juste à un niveau bas, des boucles sont appliquées (cela donnera un certain gain dans la taille du code - dans l'original, le compilateur met simplement le nombre requis d'opérations de décalage élémentaires dans une rangée.

@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

Et la définition principale du compilateur à un bas niveau est un ensemble de règles pour les opérations de langage et le travail avec la mémoire:

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)))
))

Le module macro.py vous permet de «développer» le dictionnaire de la langue cible en utilisant quelque peu les définitions de macro dans l'assembleur du processeur cible. Pour le compilateur Java, les définitions dans macro.py ne différeront pas des opérateurs et fonctions "natifs" du langage. Ainsi, par exemple, dans le compilateur d'origine, les fonctions d'E / S de la valeur dans le port externe ont été définies. Des séquences de tests d'opérations avec mémoire et variables locales et une opération de temporisation ont été ajoutées.

@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")]


Essai


Un petit programme de test de haut niveau pour notre processeur contient la définition d'une fonction de calcul factoriel et la fonction principale qui implémente la sortie série des valeurs factorielles de 1 à 7 vers le port dans une boucle infinie.

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


Il peut être lancé pour la compilation, par exemple, par un simple script ou à partir de la ligne de commande par la séquence: En conséquence, un fichier de démarrage stream.bin sera généré, qui peut être transféré au cœur du processeur dans le FPGA via le port série (dans les réalités modernes, via n'importe quel port série virtuel que les convertisseurs fournissent Interfaces USB-UART). En conséquence, le programme occupe 146 mots (9 bits) de mémoire de programme et 3 dans la mémoire de données.
c.py C:\D\My_Docs\Documents\uzh-master\tests\fact2.py




Conclusion


En général, le compilateur Uzh semble être une boîte à outils simple et pratique pour développer des logiciels pour les processeurs logiciels. C'est une excellente alternative à l'assembleur, au moins en termes de convivialité du programmeur. La boîte à outils pour définir des primitives et des macros en tant que fonctions du langage cible permet d'implémenter des endroits critiques dans le langage d'assemblage du processeur. Pour les processeurs d'architecture de pile, la procédure d'adaptation du compilateur n'est ni trop compliquée ni trop longue. Nous pouvons dire que c'est juste le cas lorsque la disponibilité du code source du compilateur aide - les sections clés du compilateur changent.

Les résultats de la synthèse du processeur (capacité 32 bits, 4 k mots de mémoire de programme et 1 Ko de RAM) pour la série FPGA Altera Cyclone V donnent les éléments suivants:

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 % )

Littérature

  1. Forth processeur sur VHDL // m.habr.com/en/post/149686
  2. Python - Wikipedia // en.wikipedia.org/wiki/Python
  3. Nous commençons FPGA sur Python _ Habr // m.habr.com/en/post/439638
  4. MyHDL // www.myhdl.org
  5. GitHub - compilateur true-grue_uzh_ Uzh // github.com/true-grue/uzh
  6. GitHub - true-grue_raddsl_ Outils pour le prototypage rapide de compilateurs DSL // github.com/true-grue/raddsl
  7. sovietov.com/txt/dsl_python_conf.pdf

L'auteur remercie les développeurs du processeur logiciel Zmey et du compilateur Uzh pour leurs consultations et leur patience.

All Articles