Dans 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]
Nous allons commencer une capture en laissant le périphérique de démonstration générer des signaux aléatoires :
Nous allons ensuite ajouter un décodeur assez simple qui calcule le rapport cyclique d’un signal numérique :
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 :
Le calcul du rapport cyclique est appliqué sur le canal et reporté directement dans l’interface de PulseView :
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 libsigrokdecod
e 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)
où 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 :
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 :
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 :
Notes
- Le script complet de visualisation est disponible sur Github. Il se veut simpliste afin de donner un exemple de rendu visuel et peut sans aucun doute être optimisé.
- Le décodeur de latence pour sigrok a été soumis aux développeurs du projet et est upstream ici
- Le firmware des puces Cypress FX2 a parfois du mal avec des fréquences d’échantillonnage haute et se bloque.