Image of an arrow

Analyseur logique : visualiser la latence entre deux signaux numériques en temps réel avec sigrok et matplotlib

Avatar

sbourdelin

Salae Logic AnalyzerDans mon billet précédent, je décrivais comment manipuler avec Python les données extraites d’un analyseur logique Saleae afin de visualiser la latence entre deux signaux numériques et en déduire la gigue temporelle de l’un d’entre eux. En conclusion, j’émettais l’hypothèse que le SDK de Saleae pourrait nous permettre de récupérer des informations sur les signaux en temps réel et non plus par l’intermédiaire d’un fichier CSV exporté. Mais au final, c’est un logiciel libre plus générique qui nous a fourni la meilleure solution.

La solution de Saleae et son SDK

Saleae propose aux développeurs deux solutions :

  • une API (en bêta à l’heure actuelle) qui permet uniquement de contrôler leur logiciel de visualisation depuis une socket.
  • un SDK qui permet d’étendre leur logiciel et écrire son propre analyseur de protocole.

Nous aurions pu a priori nous intéresser au SDK et écrire un analyseur qui exporte à la volée la latence entre deux signaux définis, mais cette solution est dépendante du logiciel de Saleae et de son évolution, ce qui nous garantit une très faible pérennité. De plus, le logiciel de visualisation de Saleae n’a pas pour vocation d’être utilisé avec un autre analyseur logique que ceux qu’ils produisent. Nous cherchions donc une solution plus générique et permanente.

Sigrok: une suite logicielle open-source pour les analyseurs logiques

C’est lors d’une discussion entre collègues qu’Emeric Vigier m’a mit sur la piste d’un logiciel nommé Sigrok. Ayant pour objectif de fournir une solution libre et générique pour les analyseurs logiques, Sigrok est une suite logicielle permettant d’extraire les données collectées par divers types d’analyseurs et de les afficher, voire de les traiter, à l’aide de décodeurs de protocole.

Cette suite se compose de plusieurs sous-projets :

  • libsigrok : une librairie écrite en C qui standardise l’accès aux pilotes des différents analyseurs.
  • libsigrokdecode : une librairie écrite en C qui fournit une API pour le décodage de protocole. Les décodeurs sont écrits en Python (>= 3).
  • Sigrok-cli : une interface en ligne de commande pour manipuler Sigrok.
  • PulseView : une interface graphique en Qt pour manipuler Sigrok.
  • Fx2lafw : Sigrok fournit également une implémentation open source du firmware des puces Cypress FX2 qui est la puce utilisé entre autre par Saleae dans toutes les déclinaisons de ses analyseurs logiques hormis le Logic Pro 16. Ce firmware permet de programmer le FPGA pour qu’il fonctionne comme un simple analyseur logique matériel.

Sigrok : exemple d’utilisation

Un exemple de cette suite mise bout à bout sera plus parlant qu’un long discours. Nous allons donc utiliser PulseView pour capturer et visualiser les signaux d’un analyseur logique.

À l’ouverture PulseView dans sa version 0.2.0 ressemble à ceci :
[cliquez sur les images pour les agrandir]

pulseview01

Nous allons commencer une capture en laissant le périphérique de démonstration générer des signaux aléatoires :

pulseview02

Nous allons ensuite ajouter un décodeur assez simple qui calcule le rapport cyclique d’un signal numérique :

pulseview03

Chaque décodeur dispose de ses options. Dans notre cas nous allons simplement définir le canal sur lequel nous voulons appliquer le décodeur :

pulseview04

Le calcul du rapport cyclique est appliqué sur le canal et reporté directement dans l’interface de PulseView :

pulseview05

Sigrok : écriture d’un décodeur pour le calcul de la latence entre deux signaux numériques

Ce qui suit est basé sur la libsigrok et la libsigrokdecode dans leurs version 0.3.0.
Chaque décodeur de protocole est un module Python qui possède son propre sous-répertoire dans le répertoire de décodeurs libsigrokdecode.

Un décodeur se compose de deux fichiers :

  • __init__.py : Ce fichier est requis pour l’initialisation du décodeur et contient une simple description du protocole.
  • pd.py : Ce fichier contient des métadonnées sur le décodeur, et son code, la plupart du temps implémenté dans la méthode decode()

Nous allons nous intéresser au fichier pd.py contenant le code de notre décodeur. L’API de la libsigrokdecode nous fournit toutes les informations sur les signaux capturés.

Rappelons nous que pour calculer la latence entre deux signaux numériques, notre logique est de considérer l’un des signaux comme une horloge de référence, et l’autre comme une résultante de ce premier. Les deux étant liés, à chaque transition d’état du signal d’horloge, le signal résultant changera aussi d’état avec une latence plus ou moins variable. Nous parlerons de la fluctuation de cette latence plus loin.

Les options du décodeur

La première chose à faire est donc de permettre à notre décodeur de définir quel canal correspond au signal d’horloge et quel canal correspond au signal résultant. La classe du décodeur permet justement de définir quelques attributs précisant ses options, notamment celle permettant à l’utilisateur de choisir les canaux :

class Decoder(srd.Decoder):
   # ...
   channels = (
      {'id': 'clk', 'name': 'Clock', 'desc': 'Clock reference channel'},
      {'id': 'sig', 'name': 'Resulting signal', 'desc': 'Resulting signal controlled by the clock'},
   )
   ...

La méthode decode()

La seconde étape va être d’implémenter le contenu de la méthode decode(). Cette méthode est appelé par la libsigrokdecode chaque fois qu’un nouveau bloc de données à traiter est disponible.

Chaque bloc est en fait un échantillon dépendant de la fréquence d’échantillonnage de nos signaux. Par exemple pour une valeur d’échantillonnage de 100 Hz, nous obtiendrons 100 blocs de données par seconde. Ces blocs de données contiennent le numéro de l’échantillon actuel, ainsi que l’état des différents signaux à cet instant.

À partir de ces informations, il est ainsi très facile d’implémenter une machine à état qui va noter à quel échantillon à eu lieu la transition du signal d’horloge et celle du signal résultant. Le nombre d’échantillons écoulés entre les deux, multiplié par la valeur de la fréquence d’échantillonnage, va nous donner la latence exprimée en secondes.

Dans sa version simplifiée, notre machine à état ressemble à cela :

def decode(self, ss, es, data):

    self.oldpin, (clk, sig) = pins, pins

    # State machine:
    # For each sample we can move 2 steps forward in the state machine.
    while True:
        # Clock state has the lead.
        if self.state == 'CLK':
            if self.clk_start == self.samplenum:
                # Clock transition already treated.
                # We have done everything we can with this sample.
                break
            else:
                if self.clk_edge(self.oldclk, clk) is True:
                    # Clock edge found.
                    # We note the sample and move to the next state.
                    self.clk_start = self.samplenum
                    self.state = 'SIG'

        if self.state == 'SIG':
            if self.sig_start == self.samplenum:
                # Signal transition already treated.
                # We have done everything we can with this sample.
                break
            else:
                if self.sig_edge(self.oldsig, sig) is True:
                    # Signal edge found.
                    # We note the sample, calculate the latency
                    # and move to the next state.
                    self.sig_start = self.samplenum
                    self.state = 'CLK'
                    # Calculate and report the latency.
                    self.putx((self.sig_start - self.clk_start) / self.samplerate)

    # Save current CLK/SIG values for the next round.
    self.oldclk, self.oldsig = clk, sig

Les sorties de notre décodeur

La libsigrokdecode propose différentes façons de remonter les informations de notre décodeur. Une méthode register() permet d’enregistrer les sorties que notre décodeur génère. Chaque sortie a un type défini; par exemple, le type OUTPUT_ANN est utilisé pour définir une sortie de type annotation qui sera représentée dans PulseView par les boîtes graphiques que nous avons vues précédemment avec le décodeur de rapport cyclique.

Pour notre décodeur, nous voulons principalement deux types de sorties:

  • une sortie de type annotation (OUTPUT_ANN) pour visualiser la latence mise en forme dans PulseView,
  • et une sortie de type binaire (OUTPUT_BINARY) pour sortir les valeurs de latence brute sans mise en forme afin d’être analysé.

La version finale de notre décodeur inclut également une sortie de type métadonnée (OUTPUT_META) rapportant des statistiques sur les signaux manqués, mais ce n’est pas important ici.

Nos deux sorties donc sont définies dans notre décodeur comme suit :

self.out_ann = self.register(srd.OUTPUT_ANN)
self.out_bin = self.register(srd.OUTPUT_BINARY)

Pour reporter des informations sur ces sorties nous utilisons la méthode put() qui a pour prototype :

put(debut_echantillon, fin_echantillon, type_de_sortie, donnees)

type_de_sortie est une de nos deux méthodes enregistrées au préalable (out_ann, out_bin).

Pour reporter la latence entre deux signaux en utilisant le type annotation nous pourrons par exemple écrire :

put(clk_start, sig_start, out_ann, [0, [ma_latence]])

Les résultats obtenus avec notre décodeur de latence

Voici un exemple des résultats obtenus avec notre décodeur dans PulseView :
pulseview06

Et les résultats obtenus avec sigrok-cli dans sa version 0.5.0 et la sortie binaire pour avoir les valeurs de latence brute :
sigrok-cli

Visualisation des données en temps réel pour en déduire la gigue temporelle

Comme nous venons de le voir, sigrok-cli nous permet de sortir les valeurs de latence brute sur la console en temps réel. Nous allons maintenant nous intéresser aux variations de cette latence. En effet, rien ne nous garantit que la latence entre ces deux signaux sera constante.

Considérant que le signal d’horloge est périodique et sans fluctuation, en théorie, le signal résultant devrait l’être aussi et la latence entre ces deux signaux devraient être constante. Si ce n’est pas le cas, c’est qu’il y a une fluctuation de cette latence que nous avions décrite dans le précédent article comme la gigue temporelle d’un signal, et c’est ce que nous voulons visualiser maintenant.

Nous pouvons donc imaginer récupérer ces valeurs à la volée et les mettre en forme dans un graphique. Pour cela, nous allons encore une fois écrire un script en Python.

Je me suis intéressé à la librairie matplotlib et son module d’animation, qui propose une méthode FuncAnimation() permettant de définir une fonction à appeler pour mettre à jour le graphique à chaque fois que de nouvelles données sont disponibles. Cette fonction prend en paramètre la figure sur laquelle nous travaillons, la fonction d’animation à appeler et l’ensemble de données à traiter.

anim = animation.FuncAnimation(fig, animate, frames=data_gen)

Ces données peuvent être de la forme d’un générateur Python qui va très bien de paire avec la lecture d’un flux de données (merci à Guillaume Roguez pour m’avoir présenté ce type d’objet).

Ainsi à chaque fois qu’une nouvelle latence sera écrite dans le flux, notre générateur récupérera une nouvelle donnée et la fonction d’animation sera appelée.

Voici à quoi ressemble le code du générateur :

# The data generator take its
# input from file or stdin
def data_gen():
    while True:
        line = fd.readline().strip()
        if line:
            yield line

Notre fonction d’animation va, si besoin est, mettre à jour l’abscisse pour visualiser toutes les latences, et ajouter la nouvelle valeur fournie par le générateur.

# The update graph function
def animate(data):
    global x_max, x_min, x, y

    try:
        # we must recalculate the abscissa range
        x_new = float(data)
        if x_max is None or x_new > x_max:
            x_max = x_new
        if x_min is None or x_new < x_min:
            x_min = x_new
        ax.set_xlim(x_min, x_max)

        # add the new plot coordinate
        x.append(x_new)
        y.append(0)
        line.set_data(x, y)

        return line,

    except KeyboardInterrupt:
        print("leaving...")

Finalement, il nous reste plus qu’à éxécuter sigrok-cli avec notre décodeur de latence et à récupérer ces valeurs dans notre script de visualisation.

sigrok-cli -d fx2lafw --config samplerate=24MHz --samples 2M -C 1,2 -P jitter:clk=1:sig=2 -B jitter | rt-draw.py

Ce qui nous donne le résultat final suivant et qui nous permet de visualiser la gigue temporelle du signal résultant :

rt_draw


Articles similaires

Image of an arrow

Nuremberg, 9 avril 2024 – À une époque où les menaces en matière de cybersécurité sont de plus en plus sophistiquées et répandues, Savoir-faire Linux, un fournisseur de premier plan en innovation technologique open source et en ingénierie logicielle pour les systèmes embarqués en Amérique du Nord et en Europe, est fier d’annoncer le lancement […]

Savoir-faire Linux est fière d’annoncer la sortie de la version v2.4.0 de l’extension officielle du Projet Yocto pour VS Code. Lisez l’article complet en anglais. Liens et ressources Pour en savoir plus sur cette ambitieuse extension du Projet Yocto pour VS Code : Téléchargez l’extension depuis le magasin VS Code Parcourez le code, signalez des […]

Savoir-faire Linux est fière d’annoncer la sortie de la version v2.3.0 de l’extension officielle du Projet Yocto pour VS Code. Lisez l’article complet en anglais. Liens et ressources Pour en savoir plus sur cette ambitieuse extension du Projet Yocto pour VS Code : Téléchargez l’extension depuis le magasin VS Code Parcourez le code, signalez des […]

Savoir-faire Linux est fière d’annoncer la sortie de la version v2.2.0 de l’extension officielle du Projet Yocto pour VS Code. Cette version majeure offre de nouvelles fonctionnalités très demandées par la communauté ! Parmi les nouveautés, la possibilité de gérer plusieurs configurations BitBake dans le même espace de travail VS Code, ou encore l’analyse des […]

[L’introduction est en français, le reste du texte en anglais]   L’économie d’énergie a toujours été une préoccupation majeure dans les systèmes embarqués, puisque par définition, ils peuvent avoir des contraintes énergétiques. Bien sûr, aujourd’hui, l’économie d’énergie est toujours au cœur des discussions. L’économie d’énergie est toujours un ensemble de compromis. En termes de disponibilité […]