Classes, héritage et polymorphisme | FPGA Pour Tous
Résumé
Classes, héritage et polymorphisme
Modéliser des transactions de test, comprendre les handles et étendre un comportement sans recopier l'environnement.
Un objet de test, pas un circuit
Une classe décrit des données et les méthodes qui les manipulent. Elle est allouée dynamiquement et n'est normalement pas synthétisable. Dans un testbench, une classe représente souvent une transaction, un générateur ou un composant de contrôle.
class bus_transaction; rand bit write; rand bit [15:0] address; rand bit [31:0] data; function new(bit [15:0] address = '0); this.address = address; endfunction virtual function void display(); $display("write=%0b address=%04h data=%08h", write, address, data);
endfunction
endclass
La déclaration de classe définit un type. L'objet n'existe qu'après new :
tr est un handle, comparable à une référence. Copier tr vers un autre handle ne copie pas automatiquement l'objet. Les deux handles désignent alors la même instance.
Le constructeur sert à initialiser l'objet. Il ne devrait ni lancer un thread ni appeler randomize() : une méthode temporelle run() et le générateur rendent ces actions visibles et contrôlables.
Hériter pour une vraie différence
Une classe dérivée reprend les champs et méthodes de sa classe de base.
class error_transaction extends bus_transaction; bit inject_parity_error; function new(); super.new(); inject_parity_error = 1'b1; endfunction virtual function void display(); super.display(); $display("inject_parity_error=%0b", inject_parity_error); endfunctionendclass
L'héritage convient lorsque la classe dérivée reste bien une forme de la classe de base. Il ne faut pas créer une hiérarchie profonde uniquement pour partager quelques lignes de code.
Polymorphisme et méthodes virtuelles
Un handle de type base peut désigner un objet dérivé :
Comme display est virtual, l'appel utilise la version de error_transaction. Cela permet à un générateur ou un driver de travailler avec un type de base tout en acceptant des variantes.
Sans virtual, la méthode choisie dépendrait du type du handle, pas du type réel de l'objet.
Copie et durée de vie
Une affectation de handle copie seulement la référence. new original construit bien un nouvel objet, mais les handles contenus dans cet objet restent partagés : c'est une copie superficielle. Une copie profonde doit aussi recréer chaque sous-objet mutable.
Pour une transaction sans sous-objet, une méthode explicite ajoutée dans bus_transaction suffit :
virtual function bus_transaction copy(); bus_transaction result = new(); result.write = write; result.address = address; result.data = data; return result;endfunction
Si la transaction contient par exemple un handle header, result.header = header resterait superficiel. Il faut appeler la copie de header et appliquer la même règle aux queues ou tableaux de handles. Les méthodes copy, compare, pack et unpack doivent toutes traiter les mêmes champs.
Le patron blueprint
Une mailbox transporte des handles. Réutiliser puis rerandomiser le même objet pendant que le driver le traite modifie donc une transaction déjà « en vol ». Un générateur robuste conserve un objet modèle, ou blueprint, puis envoie une copie indépendante :
task run(); bus_transaction blueprint = new(); repeat (100) begin if (!blueprint.randomize()) $fatal(1, "Randomisation impossible"); gen_to_drv.put(blueprint.copy()); endendtask
Créer un objet neuf à chaque tour évite aussi le partage, mais réinitialise l'historique d'un champ randc. Randomiser le blueprint puis le copier conserve à la fois cet historique et l'indépendance des transactions envoyées.
Descendre dans une hiérarchie avec contrôle
Un handle de base peut recevoir directement un objet dérivé. L'opération inverse nécessite $cast et son résultat doit être contrôlé :
error_transaction err;if (!$cast(err, base_handle)) $fatal(1, "La transaction n'est pas une error_transaction");
Ne pas cacher ce cast dans une assertion s'il est indispensable au fonctionnement : une configuration de simulation peut désactiver l'évaluation des assertions.
Les objets sont libérés lorsqu'aucun handle ne les référence plus. Les références circulaires et les handles conservés inutilement compliquent le débogage d'un environnement long.
À retenir
Une variable de classe est un handle et l'objet est créé par new.
Copier un handle ne duplique pas l'objet.
Une copie profonde recrée aussi les sous-objets mutables.
Le blueprint est randomisé, puis une copie indépendante est transmise.
L'héritage représente une spécialisation réelle du type de base.
Une méthode virtual permet au type réel de l'objet de choisir l'implémentation.
Les classes servent au testbench, pas au RTL synthétisable courant.