Greboca  

Suport technique et veille technologique

Aujourd’hui, les grandes entreprises et administrations publiques hésitent entre continuer à utiliser des logiciels propriétaires ou basculer vers les Logiciels Libres. Pourtant, la plupart des logiciels libres sont capables de bien traiter les données issues des logiciels propriétaire, et parfois avec une meilleur compatibilité.

C’est alors la barrière de la prise en main qui fait peur, et pourtant...

Les logiciels libres

L’aspect « Logiciel Libre » permet une évolution rapide et une plus grande participation des utilisateurs. Les aides et tutoriels foisonnent sur Internet ou sont directement inclus dans le logiciel lui-même.

Enfin, les concepteurs sont plus proches des utilisateurs, ce qui rend les logiciels libres plus agréable à utiliser et conviviaux.

Grâce à la disponibilité des logiciels libres, vous trouverez facilement des services de support techniques et la licence n’est plus un frein à l’utilisation de ces logiciels par votre personnel.

Notre support technique concerne essentiellement les logiciels libres, que ce soit sous forme de services ponctuels ou de tutoriels.

LinuxFr.org : les journaux  -  Hydromel : mon langage de description de matériel idéal ?

 -  Avril 2022 - 

Sommaire

Sur LinuxFr, on me connaît (ou pas) comme le développeur du logiciel de présentation Sozi mais ce n'est pas ma principale activité. Loin du JavaScript et du SVG, mon travail quotidien relève en fait du domaine des systèmes embarqués et des FPGA. Dans ce cadre, je pratique et j'enseigne le langage VHDL. J'anime également des TP d'initiation au langage Verilog, un peu par obligation.

VHDL et Verilog appartiennent à la famille des langages de description de matériel, ou HDL pour Hardware Description Languages. Pourtant, pour des débutants, il est souvent difficile de comprendre le rapport entre le code que l'on écrit et le circuit que l'on devrait obtenir. VHDL, par exemple, ressemble à une sorte de langage de programmation concurrent avec des concepts et une terminologie très éloignés du domaine de l'électronique numérique : entité, architecture, instruction concurrente, instanciation, processus, liste de sensibilité, fonction de résolution, etc.

Pour utiliser correctement les outils de synthèse automatique de circuits, il faut comprendre comment ils traduisent les constructions du langage en composants, et pourquoi ce n'est pas toujours possible. On apprend alors à sélectionner un sous-ensemble synthétisable du langage et à appliquer des bonnes pratiques de codage pour décrire des circuits combinatoires, des registres, des compteurs, des machines à états. VHDL peut alors paraître inutilement riche et verbeux pour l'utilisation que l'on en fait.

Quelles sont les alternatives ? Ces dernières années, sont apparus de nouveaux langages qui prétendent moderniser le domaine de la modélisation de circuits ou de systèmes numériques. Beaucoup d'entre eux sont implémentés comme des extensions de langages de programmation (on utilise également le terme Embedded Domain-Specific Language). De manière peut-être simpliste, je dirais que ce sont des bibliothèques offrant des API pour la modélisation, la simulation et la synthèse de circuits. En voici quelques exemples :

Je ne les pas tous essayés, et je ne saurais pas donner un avis éclairé sur la plupart d'entre eux. Aucun ne m'a totalement convaincu mais j'en ai retiré quelques idées pour imaginer Hydromel, mon langage de description de matériel idéal. Clash a été la principale source d'inspiration, et c'est pourquoi il mérite un petit paragraphe dans ce journal.

Un aperçu du langage Clash

Clash est un langage de description de matériel fonctionnel qui s'appuie sur Haskell. Il permet de décrire des circuits combinatoires et des circuits synchrones avec un ou plusieurs domaines d'horloge.

De manière très naturelle, un circuit combinatoire peut être représenté par une simple fonction :

mac a b c =
  a + b * c

Un circuit séquentiel est représenté par une fonction qui transforme une séquence de valeurs en une autre séquence de valeurs. Cette notion de séquence est réalisée par le type Signal qui s'apparente à une liste infinie. Dans l'exemple ci-dessous, la fonction prédéfinie register est utilisée pour produire un signal s dont la valeur initiale est 0 et les valeurs suivantes sont calculés à l'aide de l'expression s + 1. Le signal d'horloge est implicite.

counter = s
    where
        s = register 0 (s + 1)

Clash propose également des fonctions pour faciliter la création de circuits selon les modèles de Moore et de Mealy. L'exemple ci-dessous implémente le calcul du plus grand diviseur commun de deux nombres par soustractions successives à l'aide de l'algorithme d'Euclide. La fonction gcdStep calcule l'état suivant du circuit en fonction de l'état courant et des entrées. La fonction gcdResult calcule les sorties en fonction de l'état courant :

gcdStep (a, b) (a0, b0, start) =
    if start then
        (a0, b0)
    else if a > b then
        (a - b, b)
    else if a < b then
        (a, b - a)
    else
        (a, b)

gcdResult (a, b) =
    (a, a == b)

gcd' :: HiddenClockResetEnable dom
     => Signal dom (Int, Int, Bool)
     -> Signal dom (Int, Bool)
gcd' = moore gcdStep gcdResult (0, 0)

Dans ces exemples, Clash est clairement plus concis que VHDL, et il semble plus facile de comprendre à quoi ressemble le circuit que l'on décrit. En revanche, en s'appuyant sur Haskell, Clash apporte aussi sa propre complexité :

  • Comme en VHDL ou Verilog, certaines constructions du langage Haskell, et une partie de sa bibliothèque standard, ne sont pas utilisables pour générer du code synthétisable. On le découvre souvent à ses dépens.
  • Clash s'appuie sur le système de types d'Haskell mais subit également ses limites. Par exemple, si je veux appliquer une fonction f sur les valeurs d'un signal s, je ne peux pas simplement écrire f s. Je dois comprendre que le type Signal implémente la classe Functor et écrire : fmap f s.
  • Enfin, même si l'idée de décrire chaque composant par une fonction est séduisante, je trouve la notion d'entité VHDL plus lisible pour décrire leurs interfaces.

Je crée mon langage de description de matériel

Pour implémenter Hydromel, j'ai choisi d'utiliser le langage Racket et de mettre à l'épreuve ses qualités de Language-Oriented Programming Language. J'ai procédé par étapes et j'ai relaté mes premières expérience dans deux séries d'articles de blog (en anglais) :

Pour dissiper tout malentendu, je tiens à préciser deux choses. Contrairement aux langages mentionnés en introduction, Hydromel est un langage autonome ; ce n'est pas un extension de Racket. La syntaxe d'Hydromel n'est pas basée sur des S-expressions ; elle contient un nombre raisonnable de parenthèses et ressemble plus à VHDL qu'à Lisp.

Où en sommes-nous ?

À l'heure actuelle, la définition du langage Hydromel est suffisamment aboutie pour répondre aux besoins les plus courants. Pour m'en convaincre, j'ai réécrit mon exemple de processeur RISC-V en Hydromel. Son code source est à votre disposition dans ce dépôt.

À ce jour, l'implémentation de référence réalise les opérations suivantes :

  1. L'analyse syntaxique.
  2. L'analyse sémantique.
  3. La vérification des types.
  4. La simulation.

L'étape suivante consistera à développer un convertisseur d'Hydromel vers VHDL ou Verilog pour cibler les outils de synthèse.

Hydromel par l'exemple

Nous allons utiliser Hydromel pour décrire un circuit file d'attente (FIFO) utilisant le protocole de synchronisation ready/valid. Dans un premier temps, nous proposerons plusieurs variantes d'un composant fifo1 capable de mémoriser une valeur. Ensuite, nous en mettrons plusieurs instances en cascade pour réaliser des files d'attente plus longues.

Une FIFO à un élément

Pour commencer, précisons que les fichiers sources Hydromel devront toujours commencer par la ligne :

#lang hydromel

Cette ligne est utilisée par Racket pour charger la définition du langage qui servira à traiter le reste du fichier.

Commençons par déclarer les ports du composant fifo1. En Hydromel, un composant est équivalent à un couple entité-architecture VHDL, ou à un module Verilog. Le composant fifo1 possède un paramètre T qui correspond au type des données que la FIFO transportera.

#lang hydromel

component fifo1(T : type)
    # Les ports du côté "consommateur" de la FIFO.
    port c_valid : in  bit
    port c_ready : out bit
    port c_data  : in  T
    # Les ports du côté "producteur" de la FIFO.
    port p_valid : out bit
    port p_ready : in  bit
    port p_data  : out T

    ...
end

Son comportement respectera ce graphe d'états :

Graphe d'états d'une FIFO à un élément

  • Dans l'état Empty :
    • La FIFO est toujours prête à accepter de nouvelles données (c_ready = 1).
    • Elle se comporte de manière transparente, c'est-à-dire qu'elle copie les entrées c_valid et c_data sur les sorties p_valid et p_data.
    • Si une nouvelle donnée est disponible en entrée (c_valid = 1) pendant que le côté producteur est bloqué (p_ready = 0), la FIFO mémorise c_data (write = 1) dans un registre (que nous appellerons r_data) et passe dans l'état Full.
  • Dans l'état Full :
    • la FIFO signale qu'elle a une donnée disponible (p_valid = 1).
    • Il s'agit de la dernière donnée qui a été mémorisée (p_data = r_data).
    • La FIFO est prête à accepter une nouvelle donnée à chaque fois que le côté producteur est débloqué (c_ready = p_ready).
    • Si une donnée est disponible en entrée au moment où la donnée de sortie est consommée (c_valid = 1 et p_ready = 1), on peut écraser le registre r_data (write = 1) et la FIFO reste pleine.
    • Si la donnée de sortie est consommée (p_ready = 1) et si aucun nouvelle donnée n'arrive en entrée (c_valid = 0), la FIFO retourne dans l'état Empty.

Je propose de représenter l'état par un signal full sur un bit mémorisé dans une bascule. Sa valeur initiale est 0 et à chaque front d'horloge, il est mis à jour avec le résultat de l'expression if ci-dessous :

signal full : bit = register(0, if full then
                                    c_valid or not p_ready
                                else
                                    c_valid and not p_ready)

Le plus souvent, le type d'un signal peut être déterminé automatiquement à partir de l'expression qui lui est affectée lorsqu'il n'y a pas de dépendance circulaire. J'ai dû indiquer le type du signal full explicitement mais je n'ai pas besoin de le faire pour ces deux autres signaux :

signal write  = c_valid and (if full then p_ready else not p_ready)
signal r_data = register(zero(T), c_data when write)

Le signal r_data mémorise la valeur de c_data lorsque write est vrai. Le mot-clé when utilisé dans le deuxième argument de register correspond à l'entrée enable du registre. Comme le type T n'est pas connu, on peut utiliser la fonction zero pour obtenir une valeur nulle de ce type. Par exemple, si T est un type tableau, zero(T) retournera un tableau de zéros.

Pour finir, voici les affectations des ports de sortie :

c_ready = p_ready or not full
p_valid = c_valid or full
p_data  = if full then r_data else c_data

Et la description complète du composant fifo1 :

#lang hydromel

component fifo1(T : type)
    port c_valid : in  bit
    port c_ready : out bit
    port c_data  : in  T

    port p_valid : out bit
    port p_ready : in  bit
    port p_data  : out T

    signal full : bit = register(0, if full then
                                        c_valid or not p_ready
                                    else
                                        c_valid and not p_ready)

    signal write  = c_valid and (if full then p_ready else not p_ready)
    signal r_data = register(zero(T), c_data when write)

    c_ready = p_ready or not full
    p_valid = c_valid or full
    p_data  = if full then r_data else c_data
end

L'interface producer

L'interface de fifo1 est composée de deux groupes de ports qui se ressemblent beaucoup. Pourquoi ne pas les déclarer dans une interface que l'on pourrait réutiliser à volonté ?

interface producer(T : type)
    port valid : out bit
    port ready : in  bit
    port data  : out T
end

component fifo1(T : type)
    port c : flip producer(T)
    port p : producer(T)

    ...
end

c et p sont des ports composites. Le mot-clé flip permet d'utiliser l'interface producer en inversant le sens de ses ports. On évite ainsi de déclarer une interface consumer.

On modifie également le corps du composant pour accéder aux ports valid, ready et data à partir des ports c et p :

signal full : bit = register(0, if full then
                                    c.valid or not p.ready
                                else
                                    c.valid and not p.ready)

signal write  = c.valid and (if full then p.ready else not p.ready)
signal r_data = register(zero(T), c.data when write)

c.ready = p.ready or not full
p.valid = c.valid or full
p.data  = if full then r_data else c.data

L'interface conducer

Dans la suite de cet article, nous allons créer d'autres composants qui auront un port consommateur et un port producteur. Regroupons c et p dans une interface conducer :

interface conducer(T : type)
    port c : flip producer(T)
    port p : producer(T)
end

component fifo1(T : type)
    port cp : conducer(T)

    ...
end

Par contre, ce ne serait pas très joli de devoir écrire cp.c.valid, cp.c.ready, cp.c.data, etc. Ajoutons le mot-clé splice dans la déclaration de cp pour que ses ports c et p soient directement accessibles dans fifo1 comme avant :

component fifo1(T : type)
    port cp : splice conducer(T)

    signal full : bit = register(0, if full then
                                        c.valid or not p.ready
                                    else
                                        c.valid and not p.ready)

    signal write  = c.valid and (if full then p.ready else not p.ready)
    signal r_data = register(zero(T), c.data when write)

    c.ready = p.ready or not full
    p.valid = c.valid or full
    p.data  = if full then r_data else c.data
end

Une FIFO à deux éléments

Le composant fifo2 expose l'interface conducer et se compose de deux instances de fifo1 en cascade :

import "fifo1.mel"

component fifo2(T : type)
    port cp : splice conducer(T)

    instance f = fifo1(T)
    f.c.valid  = c.valid
    f.c.data   = c.data
    c.ready    = f.c.ready

    instance g = fifo1(T)
    g.c.valid  = f.p.valid
    g.c.data   = f.p.data
    f.p.ready  = g.c.ready

    p.valid   = g.p.valid
    p.data    = g.p.data
    g.p.ready = p.ready
end

Pour alléger l'écriture, on peut connecter deux ports composites qui ont la même interface en une seule instruction :

component fifo2(T : type)
    port cp : splice conducer(T)

    instance f = fifo1(T)
    f.c = c

    instance g = fifo1(T)
    g.c = f.p
    p   = g.p
end

Une FIFO à N éléments

Nous arrivons finalement à la description d'un composant fifo de longueur N réglable. On peut l'écrire de façon récursive. L'instruction if utilisée ci-dessous est analogue à l'instruction if-generate de VHDL.

component fifo_rec(T : type, N : natural)
    port cp : splice conducer(T)

    if N == 0 then
        p = c
    else
        instance f = fifo1(T)
        f.c = c

        instance g = fifo_rec(T, N-1)
        g.c = f.p
        p   = g.p
    end
end

On peut également l'écrire de façon itérative, avec une boucle for qui correspond à l'instruction for-generate de VHDL :

component fifo(T : type, N : natural)
    port cp : splice conducer(T)

    if N == 0 then
        p = c
    else
        instance f = fifo1(T)
        f<0>.c = c

        for n in 1 .. N-1 loop
            f.c = f.p
        end

        p = f.p
    end
end

Petit détail de syntaxe : en Hydromel, on distingue les délimiteurs [...], qui sont utilisés pour manipuler des valeurs de type tableau, et les délimiteurs <...>, qui permettent de manipuler des tableaux de ports composites ou des tableaux d'instances.

Conclusion

Il y aurait encore beaucoup de choses à écrire mais ce journal est déjà bien long et il faut rester raisonnable. En l'écrivant et en travaillant sur les exemples, j'ai remis en question certains choix. J'ai même découvert et corrigé des bugs passés inaperçus jusque-là.

Hydromel n'est pas encore utilisable pour des projets sérieux et je ne sais pas s'il le sera un jour. Il n'y a peut-être même pas de marché pour un tel langage aujourd'hui.

Évidemment, pour aller plus loin, il faudra encore écrire un convertisseur vers VHDL ou Verilog pour la synthèse. Ce ne sera pas facile. Et pour l'utilisation quotidienne, il faudra améliorer les messages d'erreur, accélérer le simulateur, et documenter tout ça.

Si ce journal vous a intéressé, vous pouvez visiter le projet Hydromel sur GitHub et consulter la description d'un processeur, simple mais complet, inspiré de l'architecture RISC-V. Le code source des deux projets est disponible sous les conditions de la licence MPL 2.0.

Commentaires : voir le flux Atom ouvrir dans le navigateur

par Guillaume Savaton

LinuxFr.org : les journaux

LinuxFr.org : Journaux

firefox, nouvelle fenêtre dans une session isolée

 -  15 avril - 

Les fenêtres de navigation privées de firefox partagent leurs cookies de session or je souhaitais avoir des fenêtres de navigation isolées, (qui ne (...)


Pretendo tente de déprogrammer l'obsolescence des consoles Nintendo

 -  9 avril - 

Ah Nal,Gros N vient de faire un gros doigt aux utilisateurs de ses consoles 3DS et Wii U en annonçant la fermeture des services en ligne pour (...)


[Trolldi] Vulgarisation sur l'IA pour décideur pressé

 -  5 avril - 

Cher 'Nal,Je fais un article-marque-page sur un post tout frais de Ploum où il est question d'un fantasme vieux comme le Talmud avec le Golem. (...)


Super Marian and Robin: les roms en collant

 -  3 avril - 

Bonjour Nal,Je t'écris pour te proposer de tester mon nouveau jeu: Super Marian and Robin.Il s'agit d'un jeu de plateformes pour un ou deux (...)


Le roi est mort, vive le roi ! Les alternatives de Redis sont là

 -  3 avril - 

Bonjour Nal !Après le changement de licence de Redis, ce qui devait arriver arriva, et des alternatives libres apparaissent.Tout d'abord, on a (...)