Python et FPGA. Test

Dans le prolongement du premier article , je veux montrer un exemple de travail avec FPGA (FPGA) en python comme exemple. Cet article couvrira plus en détail l'aspect test. Si le cadre MyHDL permet aux personnes travaillant sur python, en utilisant la syntaxe et l'écosystème familiers, de se pencher sur le monde du FPGA, les développeurs FPGA expérimentés ne comprennent pas la signification de l'utilisation de python. Les paradigmes de description du matériel pour MyHDL et Verilog sont similaires, et le choix d'une langue spécifique est une question d'habitude et de goût. Verilog / VHDL signifie que le firmware a été écrit dans ces langues depuis longtemps, et en fait, il est standard pour la description des équipements numériques. Python, en tant que novice dans ce domaine, peut rivaliser dans l'écriture d'environnements de test. Une grande partie du temps du développeur FPGA est consacrée à tester ses conceptions. Ensuite, je veux montrer avec un exemple comment cela se fait en python avec MyHDL.

Supposons qu'il existe une tâche pour décrire un périphérique fonctionnant avec de la mémoire sur le FPGA. Par souci de simplicité, je prendrai la mémoire qui communique avec d'autres appareils via une interface parallèle (et non via une interface série, par exemple, I2C). De tels microcircuits ne sont pas toujours pratiques compte tenu du fait que de nombreuses broches sont nécessaires pour fonctionner avec eux, d'autre part, un échange d'informations plus rapide et plus facile est fourni. Par exemple, le 1645RU1U domestique et ses analogues.



Description du module


L'enregistrement ressemble à ceci: FPGA donne une adresse de cellule 16 bits, des données 8 bits, génère un signal d'écriture WE (autorisation d'écriture). Étant donné qu'OE (validation de sortie) et CE (activation de puce) sont toujours activés, la lecture se produit lorsque l'adresse de cellule est modifiée. L'écriture et la lecture peuvent être effectuées à la fois de manière séquentielle dans plusieurs cellules d'affilée, à partir d'une adresse adr_start spécifique, écrite sur le bord d'attaque du signal adr_write, et d'une cellule à une adresse arbitraire (accès aléatoire).

Sur MyHDL, le code ressemble à ceci (les signaux d'écriture et de lecture viennent en logique inverse):

from myhdl import * @block def ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we): #     mem_z = data_memory.driver() #      @always(adr_write.posedge, write.posedge, read.negedge) def write_start_adr(): if adr_write: #    adr.next = adr_start else: #    / adr.next = adr + 1 @always(write) def write_data(): if not write: mem_z.next = data_in we.next = 0 #    ,    else: mem_z.next = None #        data_out.next = data_memory we.next = 1 return write_data, write_start_adr 

Si converti en Verilog à l'aide de la fonction:

 def convert(hdl): data_memory = TristateSignal(intbv(0)[8:]) data_in = Signal(intbv(0)[8:]) data_out = Signal(intbv(0)[8:]) adr = Signal(intbv(0)[16:]) adr_start = Signal(intbv(0)[16:]) adr_write = Signal(bool(0)) read, write, we = [Signal(bool(1)) for i in range(3)] inst = ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we) inst.convert(hdl=hdl) convert(hdl='Verilog') 

alors nous obtenons ce qui suit:
 `timescale 1ns/10ps module ram_driver ( data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we ); input [7:0] data_in; output [7:0] data_out; reg [7:0] data_out; output [15:0] adr; reg [15:0] adr; input [15:0] adr_start; input adr_write; inout [7:0] data_memory; wire [7:0] data_memory; input read; input write; output we; reg we; reg [7:0] mem_z; assign data_memory = mem_z; always @(write) begin: RAM_DRIVER_WRITE_DATA if ((!write)) begin mem_z <= data_in; we <= 0; end else begin mem_z <= 'bz; data_out <= data_memory; we <= 1; end end always @(posedge adr_write, posedge write, negedge read) begin: RAM_DRIVER_WRITE_START_ADR if (adr_write) begin adr <= adr_start; end else begin adr <= (adr + 1); end end endmodule 

Il n'est pas nécessaire de convertir un projet en Verilog pour la simulation, cette étape sera nécessaire pour flasher le FPGA.

Modélisation


Après la description de la logique, le projet doit être vérifié. Vous pouvez vous restreindre, par exemple, pour simuler les influences d'entrée et voir la réponse du module dans le diagramme temporel. Mais avec cette option, il est plus difficile de prédire l'interaction de votre module avec une puce mémoire. Par conséquent, pour vérifier complètement le fonctionnement du périphérique créé, vous devez créer un modèle de mémoire et tester l'interaction entre ces deux périphériques.

Puisque le travail se déroule en python, le type de dictionnaire donné (dictionnaire) se propose pour le modèle de mémoire. Les données dans lesquelles sont stockées en tant que {clé: valeur}, et dans ce cas {adresse: données}.

 memory = { 0: 123, 1: 456, 2: 789 } memory[0] >> 123 memory[1] >> 456 

Dans le même but, le type de données de liste convient, où chaque élément a ses propres coordonnées indiquant l'emplacement de l'élément dans la liste:

 memory = [123, 456, 789] memory[0] >> 123 memory[1] >> 456 

L'utilisation de dictionnaires pour simuler la mémoire semble préférable compte tenu d'une plus grande visibilité.

La description du shell de test (dans le fichier test_seq_access.py) commence par l'annonce des signaux, l'initialisation des états initiaux et leur lancement dans la fonction pilote de mémoire décrite ci-dessus:

 @block def testbench(): data_memory = TristateSignal(intbv(0)[8:]) data_in = Signal(intbv(0)[8:]) data_out = Signal(intbv(0)[8:]) adr = Signal(intbv(0)[16:]) adr_start = Signal(intbv(20)[16:]) adr_write = Signal(bool(0)) read, write, we = [Signal(bool(1)) for i in range(3)] ram = ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we) 

Ce qui suit décrit le modèle de mémoire. Les états initiaux sont initialisés, par défaut la mémoire est remplie de valeurs nulles. Limitez le modèle de mémoire à 128 cellules:

 memory = {i: intbv(0) for i in range(128)} 

et décrire le comportement de la mémoire: lorsque WE à l'état bas, écrire la valeur dans la ligne à l'adresse mémoire correspondante, sinon le modèle donne la valeur à l'adresse donnée:

 mem_z = data_memory.driver() @always_comb def access(): if not we: memory[int(adr.val)] = data_memory.val if we: data_out.next = memory[int(adr.val)] mem_z.next = None 

Ensuite, dans la même fonction, vous pouvez décrire le comportement des signaux d'entrée (dans le cas d'une écriture / lecture séquentielle): l'adresse de départ est enregistrée → 8 cellules d'informations sont enregistrées → l'adresse de départ est enregistrée → 8 cellules d'informations enregistrées sont lues.

 @instance def stimul(): init_adr = random.randint(0, 50) #   yield delay(100) write.next = 1 adr_write.next = 1 adr_start.next = init_adr #   yield delay(100) adr_write.next = 0 yield delay(100) for i in range(8): # 8    write.next = 0 data_in.next = random.randint(0, 100) yield delay(100) write.next = 1 yield delay(100) adr_start.next = init_adr #   adr_write.next = 1 yield delay(100) adr_write.next = 0 yield delay(100) for i in range(8): #   read.next = 0 yield delay(100) read.next = 1 yield delay(100) raise StopSimulation return stimul, ram, access 

Exécuter la simulation:

 tb = testbench() tb.config_sim(trace=True) tb.run_sim() 

Après avoir démarré le programme, le fichier testbench_seq_access.vcd est généré dans le dossier de travail, ouvrez-le dans gtkwave:

 gtkwave testbench_seq_access.vcd 

Et nous voyons l'image:



Les informations enregistrées ont été lues avec succès.

Vous pouvez voir le contenu de la mémoire en ajoutant le code suivant à testbench:

 for key, value in memory.items(): print('adr:{}'.format(key), 'data:{}'.format(value)) 

Ce qui suit apparaît dans la console:



Test


Après cela, vous pouvez effectuer plusieurs tests automatisés avec un nombre accru de cellules inscriptibles / lisibles. Pour ce faire, plusieurs cycles de test et dictionnaires factices sont ajoutés à testbench, où les informations écrites et lisibles et la construction assert sont ajoutées, ce qui provoque une erreur si deux dictionnaires ne sont pas égaux:

 @instance def stimul(): for time in range(100): temp_mem_write = {} temp_mem_read = {} init_adr = random.randint(0, 50) yield delay(100) write.next = 1 adr_write.next = 1 adr_start.next = init_adr yield delay(100) adr_write.next = 0 yield delay(100) for i in range(64): write.next = 0 data_in.next = random.randint(0, 100) temp_mem_write[i] = int(data_in.next) yield delay(100) write.next = 1 yield delay(100) adr_start.next = init_adr adr_write.next = 1 yield delay(100) adr_write.next = 0 yield delay(100) for i in range(64): read.next = 0 temp_mem_read[i] = int(data_out.val) yield delay(100) read.next = 1 yield delay(100) assert temp_mem_write == temp_mem_read, "   " for key, value in memory.items(): print('adr:{}'.format(key), 'data:{}'.format(value)) raise StopSimulation return stimul, ram, access 

Ensuite, vous pouvez créer un deuxième banc de test pour tester le fonctionnement en mode d'accès aléatoire: test_random_access.py.

L'idée du deuxième test est similaire: nous écrivons des informations aléatoires à une adresse aléatoire et ajoutons un couple {address: data} au dictionnaire temp_mem_write. Ensuite, nous parcourons les adresses de ce dictionnaire et lisons les informations de la mémoire, en les saisissant dans le dictionnaire temp_mem_read. Et à la fin avec la construction assert, nous vérifions le contenu de deux dictionnaires.

 import random from myhdl import * from ram_driver import ram_driver @block def testbench_random_access(): data_memory = TristateSignal(intbv(0)[8:]) data_in = Signal(intbv(0)[8:]) data_out = Signal(intbv(0)[8:]) adr = Signal(intbv(0)[16:]) adr_start = Signal(intbv(20)[16:]) adr_write = Signal(bool(0)) read, write, we = [Signal(bool(1)) for i in range(3)] ram = ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we) memory ={i:intbv(0) for i in range(128)} mem_z = data_memory.driver() @always_comb def access(): if not we: memory[int(adr.val)] = data_memory.val if we: data_out.next = memory[int(adr.val)] mem_z.next = None @instance def stimul(): for time in range(10): temp_mem_write = {} temp_mem_read = {} yield delay(100) for i in range(64): write.next = 1 adr_write.next = 1 adr_start.next = random.randint(0, 126) yield delay(100) adr_write.next = 0 yield delay(100) write.next = 0 data_in.next = random.randint(0, 100) temp_mem_write[int(adr_start.val)] = int(data_in.next) yield delay(100) write.next = 1 yield delay(100) for key in temp_mem_write.keys(): adr_start.next = key adr_write.next = 1 yield delay(100) adr_write.next = 0 yield delay(100) read.next = 0 temp_mem_read[key] = int(data_out.val) yield delay(100) read.next = 1 yield delay(100) assert temp_mem_write == temp_mem_read, '  random access' raise StopSimulation return stimul, ram, access tb = testbench_random_access() tb.config_sim(trace=True) tb.run_sim() 

Python dispose de plusieurs cadres pour automatiser l'exécution des tests. Je vais prendre pytest pour plus de simplicité, il doit être installé depuis pip:

 pip3 install pytest 

Lorsque la commande «pysest» est lancée à partir de la console, le framework recherche et exécute tous les fichiers du dossier de travail avec «test_ *» dans leurs noms.



Les tests ont réussi. Je ferai une erreur dans la description de l'appareil:

 @block def ram_driver(data_in, data_out, adr, adr_start, adr_write, data_memory, read, write, we): mem_z = data_memory.driver() @always(adr_write.posedge, write.posedge, read.negedge) def write_start_adr(): if adr_write: adr.next = adr_start else: adr.next = adr + 1 @always(write) def write_data(): if not write: mem_z.next = data_in we.next = 1 #  ,    else: mem_z.next = None data_out.next = data_memory we.next = 1 

Je lance des tests:



Comme prévu, dans les deux tests, les informations initiales (zéros) ont été prises en compte, c'est-à-dire qu'aucune nouvelle information n'a été enregistrée.

Conclusion


L'utilisation de python avec myHDL vous permet d'automatiser les tests des micrologiciels développés pour les FPGA et de créer presque n'importe quel environnement de test en utilisant les riches capacités du langage de programmation python.

L'article considère:

  • créer un module qui fonctionne avec la mémoire;
  • créer un modèle de mémoire;
  • création de cas de test;
  • automatisation des tests avec le framework pytest.

Source: https://habr.com/ru/post/fr442010/


All Articles