Construire un environnement en couches réutilisable | FPGA Pour Tous
Résumé
Construire un environnement en couches réutilisable
Assembler transaction, générateur, driver, monitors, modèle de référence, scoreboard et couverture dans un testbench extensible.
Une chaîne de responsabilités stable
Un environnement en couches sépare les rôles afin qu'une erreur soit localisable et que les tests réutilisent la même infrastructure :
le test règle configuration, contraintes et nombre de transactions ;
le générateur randomise une transaction blueprint et en envoie une copie ;
le driver transforme cette transaction en activité signal par signal ;
le monitor d'entrée reconstruit ce que le DUT a réellement accepté ;
le modèle de référence produit l'attendu à partir de cette observation ;
le monitor de sortie reconstruit la réponse réelle ;
le scoreboard apparie et compare attendu et observé ;
la couverture échantillonne les transactions réellement acceptées.
Le driver ne décide pas si le résultat est correct. Le monitor ne pilote jamais le DUT. Le modèle ne lit aucun signal interne. Ces frontières empêchent un même code de produire le stimulus et de confirmer sa propre erreur.
1. Définir la frontière physique une seule fois
L'interface centralise le protocole temporel. Driver et monitor reçoivent des vues différentes de la même instance physique :
Le top instancie DUT, horloge et interface. Les objets dynamiques reçoivent ensuite une interface virtuelle par leur constructeur ou par la configuration de l'environnement.
2. Transporter une transaction indépendante
class stream_item; rand logic [31:0] data; rand int unsigned idle_cycles; constraint legal_idle { idle_cycles inside {[0:8]}; } function stream_item copy(); stream_item result = new(); result.data = data; result.idle_cycles = idle_cycles; return result; endfunctionendclass
Le générateur conserve un blueprint afin de préserver l'historique randc éventuel, contrôle chaque randomize() et place blueprint.copy() dans une mailbox typée. Ainsi, le driver ne voit jamais un objet que le générateur peut encore modifier.
3. Driver au niveau transaction, signaux au clocking block
class stream_driver; stream_drv_vif_t vif; mailbox #(stream_item) inbox; int unsigned driven_count; function new(stream_drv_vif_t vif, mailbox #(stream_item) inbox); this.vif = vif; this.inbox = inbox; endfunction task run(); stream_item item; forever begin inbox.get(item); repeat (item.idle_cycles) @vif.drv_cb; vif.drv_cb.valid <= 1'b1; vif.drv_cb.data <= item.data; do @vif.drv_cb; while (!vif.drv_cb.ready); vif.drv_cb.valid <= 1'b0; driven_count++; end endtaskendclass
Le driver respecte le handshake cycle par cycle et n'accède jamais à la hiérarchie interne du DUT. Les affectations vers les sorties du clocking block sont non bloquantes.
4. Le monitor reconstruit ce qui a eu lieu
task run(); forever begin @vif.mon_cb; if (vif.mon_cb.rst_n && vif.mon_cb.valid && vif.mon_cb.ready) begin stream_item observed = new(); observed.data = vif.mon_cb.data; observed_mb.put(observed); end endendtask
Le monitor est passif. Il ne publie une transaction qu'après son acceptation par le protocole. Pour un bloc transformant un flux, utiliser un monitor à l'entrée et un autre à la sortie. L'attendu doit partir de l'entrée observée, pas de l'objet que le générateur espérait envoyer : reset, injection d'erreur ou interruption peuvent avoir modifié le transfert réel.
5. Prédire, mémoriser et apparier
Le modèle de référence applique une règle plus simple et indépendante du RTL à chaque transaction d'entrée observée. Il transmet une copie de la réponse prédite au scoreboard.
Le stockage dépend du contrat :
réponse strictement ordonnée : comparer la tête d'une queue ;
réponse réordonnée avec identifiant unique : tableau associatif par identifiant ;
réponse réordonnée sans clé directe : méthode de localisation, puis contrôle de zéro, une ou plusieurs correspondances.
Après une correspondance, le scoreboard retire l'attendu. Il incrémente séparément les nombres attendus, observés, appariés et erronés. Son message d'écart contient identifiant, attendu, observé, temps et contexte utile.
6. Construire, exécuter, puis vider
L'environnement regroupe les mailboxes et les handles des composants. Sa méthode build() les crée et les connecte ; elle ne lance aucun thread. Sa méthode run() démarre les activités temporelles en parallèle. Son wrap_up() contrôle les compteurs et les attendus restants.
La seule condition pending() == 0 serait insuffisante : elle peut être vraie juste après la fin du générateur, avant que le driver ou le modèle n'ait produit le premier attendu. Le squelette attend donc successivement l'acceptation de toutes les commandes, la création de tous les attendus puis leur comparaison. Il attend compared_count, pas le nombre de succès : une réponse fausse doit conduire rapidement au bilan FAIL, et non bloquer jusqu'au timeout. Un watchdog reste indispensable si une réponse n'arrive jamais. Dans un protocole autorisant zéro ou plusieurs réponses par commande, ces compteurs doivent représenter explicitement cette règle plutôt que supposer une correspondance un-pour-un.
La phase de bilan échoue si aucun contrôle n'a eu lieu, si une erreur a été vue ou si un attendu reste dans le scoreboard. Une campagne en échec ne fournit pas de couverture à fusionner.
7. Étendre sans réécrire l'environnement
Le test ne remplace pas driver et monitors pour chaque scénario. Il modifie plutôt :
les contraintes du blueprint ;
la configuration et le nombre de transactions ;
une classe dérivée créée à la place d'un type de base ;
des callbacks prévus autour d'une transmission ou d'une observation.
L'ordre des callbacks est important. Une perturbation doit intervenir avant l'envoi ; le modèle attendu et la couverture interviennent après confirmation que la transaction a réellement été transmise ou reçue. Dans un grand environnement, une publication vers plusieurs abonnés remplace avantageusement des appels directs, mais la responsabilité reste la même.
Valider aussi le testbench
Injecter volontairement une mauvaise réponse doit déclencher le scoreboard. Supprimer une réponse doit déclencher le timeout ou le contrôle des restes. Dupliquer une réponse doit produire un observé inattendu. Un DUT manifestement incorrect qui obtient PASS révèle un testbench non discriminant, quelle que soit sa couverture.
À retenir
L'interface physique et les composants en classes restent séparés par des interfaces virtuelles.
Le générateur randomise un blueprint et transmet une copie indépendante.
Le driver applique ; les monitors observent ; le modèle prédit ; le scoreboard tranche.
L'attendu vient d'une entrée réellement acceptée, et la couverture d'une transaction réellement observée.
Build, Run et Wrap-up rendent construction, parallélisme, vidange et verdict explicites.
Les tests changent configuration, contraintes et extensions, pas l'infrastructure commune.