La conception de circuits logiques en VHDL ou Verilog est souvent perçue comme rébarbative. Les débutants sont régulièrement perdus dans la masse de détails à maîtriser en une seule fois pour obtenir un circuit fonctionnel. Les utilisateurs expérimentés pourraient être plus productifs en automatisant certaines tâches au lieu d'éditer trop souvent le code bas niveau à la main. Cet article donne une (courte) introduction à Migen, une bibliothèque Python permettant de manipuler et générer plus facilement des circuits logiques complexes.
1. Installation de Migen
Logo Migen (CC-BY-SA)
Obtenez tout d'abord les sources depuis le dépôt Git. Migen est encore nouveau et expérimental, et il n'existe pas de « version stable ».
git clone git://github.com/milkymist/migen.git
cd migen
Migen est prévu pour fonctionner avec Python 3.2. Pour installer les fichiers de façon permanente sur votre système, lancez le script setup.py fourni :
python3 setup.py
Vous pouvez aussi tout simplement utiliser la variable d'environnement PYTHONPATH à la place :
export PYTHONPATH=`pwd`
Pour pouvoir simuler votre circuit, vous aurez besoin d'Icarus Verilog. Cependant, il n'est pas encore possible d'utiliser les paquets de votre distribution car un bug dans l'API vpi_put_value() d'Icarus Verilog empêche le bon fonctionnement des écritures de signaux depuis Python. Vous devrez donc patcher et recompiler Icarus Verilog manuellement :
git clone git://github.com/steveicarus/iverilog.git
cd iverilog
patch -p1 < 0001-Copy-vector-values-in-vpi_put_value.patch
./configure && make && make install
Le fichier patch se trouve dans le répertoire vpi des sources de Migen [NDLA : Update, le patch est upstream, il suffit maintenant de compiler Icarus à partir des sources Git].
Compilez et installez ensuite le module permettant la communication entre Migen et Icarus Verilog :
cd migen/vpi
make && make install
Bien évidemment, si vous êtes maintainer dans une distribution, il ne tient qu'à vous de rendre ces étapes plus simples pour tout le monde ;)
2. Modèle de circuit
Après ces considérations d'ordre pratique, intéressons-nous à la façon dont les circuits sont modélisés par Migen. Il n'est possible ici de modéliser que des circuits logiques synchrones, qui représentent la quasi-totalité de la logique présente sur une puce numérique (et en particulier d'un FPGA) de nos jours.
Dans notre modèle, un circuit est composé de deux parties :
- une liste d'instructions combinatoires, qui sont évaluées à chaque changement de leurs entrées.
- une liste d'instructions synchrones, qui sont évaluées à chaque cycle d'horloge.
Les instructions sont décrites par une structure d'objets Python faisant référence à des signaux - ces derniers représentant des points de connexion nommés dans le circuit.
Par exemple, voici comment représenter une porte AND ayant les signaux a et b pour entrée, et le signal x pour sortie :
a = Signal()
b = Signal()
x = Signal()
and = _Assign(x, _Operator('&', [a, b]))
La syntaxe de cette dernière ligne étant particulièrement lourde, Migen redéfinit les opérateurs Python pour les signaux et fournit plusieurs méthodes et fonctions permettant une syntaxe plus lisible. Il s'agit de la technique « internal domain specific language » assez à la mode chez les Rubyistes. On peut ainsi réécrire cette ligne de cette façon :
and = x.eq(a & b)
Les listes d'instructions combinatoires et synchrones sont ensuite encapsulées dans un fragment, qui peut ensuite être combiné avec d'autres, simulé, ou converti en Verilog synthétisable pour une implémentation matérielle.
3. Exemple pratique
3.1 Description du circuit
Pour débuter, nous allons réaliser un simple diviseur de fréquence générant un sous-multiple de l'horloge système. Ce circuit a l'avantage de pouvoir faire clignoter une LED sur une carte FPGA, et de tester ainsi la technique d'implémentation matérielle avec un exemple trivial.
Notre diviseur consiste en un décompteur synchrone qui, lorsqu'il atteint 0, provoque l'inversion du signal de sortie. Cela permet d'obtenir un rapport cyclique de 50% et donc un clignotement visible de la LED.
Nous encapsulons également notre code dans une classe Python. Même si cela n'apporte pas d'avantage clair ici, cela permet de démontrer cette possibilité qui peut être fort utile avec certaines architectures.
from migen.fhdl.structure import *
class FreqDivider:
def __init__(self, ratio):
self.out = Signal()
self.ratio = ratio
def get_fragment(self):
r = self.ratio//2 - 1
counter = Signal(BV(bits_for(r)))
sync = [
If(counter == 0,
self.out.eq(~self.out),
counter.eq(r)
).Else(
counter.eq(counter - 1)
)
]
return Fragment(sync=sync)
Remarquez qu'à l'initialisation du circuit, tous les signaux ont par défaut la valeur 0 (Migen insère automatiquement le code Verilog pour cela).
3.2 Simulation
Créons une instance de notre diviseur avec un faible rapport de fréquence (afin de pouvoir observer facilement le fonctionnement sur un chronogramme) et obtenons le fragment correspondant :
my_divider = FreqDivider(8)
my_fragment = my_divider.get_fragment()
Importons ensuite les modules de simulation, et exécutons une simulation pour 20 cycles :
from migen.sim.generic import Simulator, TopLevel
from migen.sim.icarus import Runner
sim = Simulator(my_fragment, Runner(), TopLevel("my.vcd"))
sim.run(20)
Ouvrez ensuite le fichier my.vcd avec GTKWave, Dinotrace ou un équivalent pour observer le chronogramme :
Fig. 1 : Chronogramme de notre diviseur de fréquence
Attention : le fichier VCD peut être corrompu si le simulateur est encore en attente. Pour éviter cela, terminez l'interpréteur Python, ou détruisez uniquement l'objet avec :
del sim
Remarquez que Migen génère automatiquement l'horloge système ainsi qu'un signal de reset.
Cet exemple très simple ne fait qu'effleurer les possibilités du simulateur. Il est notamment possible d'exécuter, pendant la simulation, du code Python qui manipule les signaux afin d'effectuer des tests complexes et puissants. Des exemples et une documentation plus complète (en anglais) sont fournis avec le code source de Migen. Pour vous donner un aperçu, voici un extrait de wb_initiator.py qui utilise les générateurs Python pour exécuter des transactions sur un bus Wishbone avec un style de code séquentiel :
def my_generator():
prng = Random(92837)
# Ecritures
for x in range(10):
t = TWrite(x, 2*x)
yield t
print("Ecriture en " + str(t.latency) + " cycle(s)")
# Insère des cycles morts pour simuler l'inactivité sur le bus
for delay in range(prng.randrange(0, 3)):
yield None
# Lectures
for x in range(10):
t = TRead(x)
yield t
print("Lecture de " + str(t.data) + " en " + str(t.latency) + " cycle(s)")
for delay in range(prng.randrange(0, 3)):
yield None
3.3 Implémentation matérielle
Nous allons maintenant utiliser notre diviseur de fréquence pour faire clignoter une LED sur une carte FPGA. Tout d'abord, créez un nouveau diviseur avec un rapport plus raisonnable pour cela :
led_divider = FreqDivider(50000000)
led_fragment = led_divider.get_fragment()
Vous pouvez ensuite transcrire le fragment correspondant en Verilog :
from migen.fhdl import verilog
verilog_source = verilog.convert(led_fragment, ios={led_divider.out})
print(verilog_source)
Le paramètre ios donne l'ensemble des signaux qui doivent devenir des ports dans le code généré. Par défaut, les ports sys_clk et sys_rst pour l'horloge et le reset sont implicites. Cela donne le résultat suivant :
/* Machine-generated using Migen */
module top(
input sys_rst,
output reg out,
input sys_clk
);
reg [24:0] counter;
always @(posedge sys_clk) begin
if (sys_rst) begin
out <= 1'd0;
counter <= 25'd0;
end else begin
if ((counter == 1'd0)) begin
out <= (~out);
counter <= 25'd24999999;
end else begin
counter <= (counter - 1'd1);
end
end
end
endmodule
Vous pouvez utiliser ce code source avec vos outils FPGA préférés (ou détestés) pour réaliser l'essai sur carte de développement. Le dépôt Git milkymist-ng donne des exemples de scripts pour les outils Xilinx et la plate-forme Milkymist One.
4. Pour aller plus loin
Outre le fait de pouvoir exécuter du code Python lors de la simulation, le point fort de Migen réside dans la possibilité de générer facilement des circuits depuis Python, en construisant des listes d'instructions synchrones et combinatoires. Il est ainsi possible d'utiliser les objets, les fonctions, la métaprogrammation, etc., pour organiser le code, réduire la redondance, et le rentre paramétrable (et donc plus réutilisable).
Pour illustrer cela, Migen dispose notamment d'un module permettant de réaliser automatiquement des bancs de registres de configuration et d'état à partir d'une description abstraite. Par exemple, pour concevoir un SoC ou un microcontrôleur, les emplacements mémoire configurant la vitesse de transmission d'une UART et contenant les caractères transférés seront simplement listés avec le comportement voulu pour chaque registre (lecture seule depuis le bus, signaux de contrôle voulus côté UART, etc.). Cette liste générique sera lue par un module qui produira la logique implémentant l'interface de bus choisie (Wishbone, CSR-2, ...). Il est également possible de construire les contrôleurs d'interruption de cette manière.
L'interconnexion de composants sur un bus et l'arbitration de ce bus sont également très simples grâce aux composants de la bibliothèque. Par exemple, pour connecter plusieurs maîtres Wishbone à plusieurs esclaves par l'intermédiaire d'un bus partagé, il suffit d'écrire ceci (il est bien sûr possible de contrôler les adresses des esclaves avec des paramètres supplémentaires) :
wishbonecon0 = wishbone.InterconnectShared(
[
cpu0.ibus,
cpu0.dbus
], [
norflash0.bus,
sram0.bus,
wishbone2asmi0.wishbone,
wishbone2csr0.wishbone
Enfin (mais cela n'est pas encore très développé pour le moment), il sera possible de réaliser des accélérateurs matériels assez simplement en utilisant le paradigme « dataflow », peut-être avec un langage tel que RVC-CAL.
5. Domaines d'application
Migen (Milkymist generator) va devenir la base de la prochaine version majeure du system-on-chip libre Milkymist. Si vous avez une Milkymist One, vous pouvez d'ores et déjà essayer la version de développement actuelle :
git clone git://github.com/milkymist/milkymist-ng.git
cd milkymist-ng
cd software/bios
make # nécessite Clang/LLVM avec patch
make flash # écriture du nouveau BIOS via UrJTAG et m1nor
cd ../..
make # nécessite Xilinx ISE
make load # chargement du nouveau bitstream via UrJTAG
Le nouveau BIOS est compilé avec Clang et LLVM. Le support du processeur LM32 utilisé dans Milkymist n'étant pas encore inclus dans les versions officielles, vous aurez besoin de compiler ces logiciels à partir de nos dépôts Git (merci à JP Bonn pour ce travail) :
git clone git://github.com/milkymist/llvm-lm32.git
cd llvm-lm32/tools
git clone git://github.com/milkymist/clang-lm32.git clang
cd ../..
mkdir build && cd build
cmake .. && make && make install
Un autre domaine d'application de Migen est la plate-forme de radio logicielle Rhino, développée à l'université du Cap, en Afrique du Sud. Migen est utilisé pour automatiser la construction d'une « application Rhino », c'est-à-dire un ensemble de composants (chaînes de traitement numérique du signal, interfaces avec les ADC/DAC, ...) chargés dans le FPGA et communiquant avec le logiciel fonctionnant sur un SoC externe faisant tourner Linux. Il est également envisagé d'utiliser les futures fonctionnalités « dataflow » de Migen pour réaliser les traitements sur les signaux, notamment dans des systèmes d'imagerie radar sophistiqués.
Fig. 2 : Schéma-bloc de la plate-forme Rhino (CC-BY-SA Alan Langman, Simon Scott)
Fig. 3 : Photographie d'un système Rhino (CC-BY-SA Alan Langman)
Si vous avez des questions, n'hésitez pas à venir les poser sur IRC (canal #milkymist sur le réseau Freenode) ou via la liste devel@lists.milkymist.org. À bientôt !