Ecrire un décodeur pour sigrok

image



Si vous travaillez avec la technologie numérique, alors tôt ou tard, vous aurez besoin d'un analyseur logique. L'analyseur logique DSLogic de DreamSourceLab est l'un des outils disponibles pour les radioamateurs. Il a été mentionné à plusieurs reprises sur le site, au moins: un , deux et trois .



Sa fonctionnalité est open source , ainsi que le fait que la bibliothèque sigrok open-source est responsable du décodage des signaux . Outre une liste impressionnante de décodeurs de signaux existants, cette bibliothèque fournit des API pour écrire les vôtres. Voilà ce que nous allons faire.



Stand de démonstration



, . TTP229-BSF. 8- 16- . Arduino, DSLogic.



image





, , . , . , , .



Chaque décodeur dans sigrok est un package distinct écrit en Python 3 et possède son propre répertoire dans le dossier decoders. Comme tout paquet Python, le décodeur contient __init__.py et, selon la dénomination acceptée dans sigrok, un fichier pd.py contenant l'implémentation elle-même.



image



Le code dans __init__.py est standard et comprend une docstring décrivant le protocole, et une importation de décodeur ( d28dee9 ):



'''
Protocol description.
'''

from .pd import Decoder


Le fichier pd.py contient l'implémentation du décodeur ( d28dee9 ):



class Decoder(srd.Decoder):
    api_version = 3
    id = 'empty'
    name = 'empty'
    longname = 'empty decoder'
    desc = 'Empty decoder that can be loaded by sigrok.'
    license = 'mit'
    inputs = ['logic']
    outputs = ['empty']
    tags = ['Embedded/industrial']
    channels = (
        {'id': 'scl', 'name': 'SCL', 'desc': 'Clock'},
        {'id': 'sdo', 'name': 'SDO', 'desc': 'Data'},
    )

    def start(self):
        pass

    def reset(self):
        pass

    def decode(self):
        pass


Il s'agit d'une implémentation minimale qui peut être chargée par la bibliothèque mais ne décode rien. Analysons les propriétés requises:



  • api_version – Protocol decoder API, . libsigrokdecode 3- Protocol decoder API. .
  • id – , . , .
  • name, longname, desc – , , . .
  • inputs, outputs – . 'logic' . . , , SPI. SPI . , SPI . 'spi'.
  • license, tag – . DSView 1.1.1 + libsigrokdecode tags - .
  • canaux - une liste de lignes de signaux utilisées par le décodeur. Cette propriété est requise pour les décodeurs dont le format de données d'entrée est logique.


et méthodes requises:



  • start () - la méthode appelée avant le début du décodage. Dans cette méthode, les paramètres de la session de décodage en cours doivent être définis.
  • reset () - méthode appelée lorsque le décodage est arrêté. Doit ramener le décodeur à son état initial.
  • decode () est une méthode appelée pour décoder un signal.


Après avoir traité la mise en œuvre minimale du décodeur, vous pouvez commencer à décoder le signal réel.



Décodeur complet



Pour commencer, considérez un chronogramme d'un signal de données. Le TTP229-BSF a plusieurs modes de fonctionnement, et je ne montre que le chronogramme du mode qui sera utilisé ci-dessous. Des informations plus détaillées sur tous les modes de fonctionnement du microcircuit peuvent être trouvées dans la documentation correspondante.



image



La première chose et la plus importante est de décrire l'ensemble des lignes requises avec lesquelles le décodeur fonctionnera. Dans ce cas, il y en a deux, une ligne d'horloge (SCL) et une ligne de données (SDO).



class Decoder(srd.Decoder):
    ...
    inputs = ['logic']
    channels = (
        {'id': 'scl', 'name': 'SCL', 'desc': 'Clock'},
        {'id': 'sdo', 'name': 'SDO', 'desc': 'Data'},
    )


Lorsque le microcircuit détecte une pression sur un bouton, il définit le signal de données valides (DV) sur la ligne SDO, selon lequel le récepteur doit commencer à lire les données. Trouvons et décodons ce signal.



sigrok , . . , , . Protocol decoder API . . wait(). . , self.samplenum .



, , – , :



  • «l» - niveau bas, 0 logique;
  • «h» - niveau élevé, logique 1;
  • «r» - montée du signal, transition de l'état bas à l'état haut;
  • «f» - déclin du signal, transition de haut en bas;
  • «e» - changement de signal arbitraire, montée ou descente;
  • 's' - état stable, 0 ou 1.


Ainsi, pour trouver le début du signal DV, une condition décrivant que la ligne SCL est élevée et que le roulement de signal se produit sur la ligne de données (SDO). Nous appelons la fonction wait () avec la condition et sauvegardons le numéro de l'échantillon:



        self.wait({0: 'h', 1: 'f'})
        self.dv_block_ss = self.samplenum


pour trouver la fin du signal DV, il est nécessaire de définir une condition lorsque la ligne SCL reste élevée et la ligne de données devient haute:

        self.wait({0: 'h', 1: 'r'})


À la fin du dernier appel à la fonction wait (), les numéros d'échantillons du début et de la fin du signal DV seront connus. Il est temps de créer une annotation pour cela. Pour ce faire, ajoutez des annotations au décodeur et regroupez-les (annotation_rows):



class Decoder(srd.Decoder):
    ...
    annotations = (
        ('dv', 'Data valid'),
    )
    annotation_rows = (
        ('fields', 'Fields', (0,)),
    )


où 0 est l'index de l'annotation dans le tuple self.annotations qui appartient à ce groupe. Vous devrez également enregistrer la sortie des annotations:



    def start(self):
        self.out_ann = self.register(srd.OUTPUT_ANN)


Vous êtes maintenant prêt à placer l'annotation pour le signal DV. Cela se fait en appelant la fonction put () ( f613b83 ):



        self.put(self.dv_block_ss, self.samplenum,
                 self.out_ann, [0, ['Data valid', 'DV']])


Paramètres de fonction: numéro d'échantillon de début d'annotation (self.dv_block_ss), numéro d'échantillon de fin d'annotation (self.samplenum), identificateur de sortie d'annotation (self.out_ann) et données pour l'annotation. Les données sont présentées sous la forme d'une liste d'index d'annotation (0) et d'une liste imbriquée de chaînes, de la plus longue à la plus courte, à afficher dans la description. Si plus d'une ligne est spécifiée, l'interface peut sélectionner indépendamment la ligne affichée, par exemple, en fonction de l'échelle utilisée:







De même, nous ajoutons une annotation pour le délai Tw entre la fin du signal DV et le début de la lecture des données du microcontrôleur. Ensuite, vous pouvez commencer à décoder les données de pression de bouton.



TTP229-BSF, selon le mode sélectionné, peut fonctionner avec 8 ou 16 boutons tactiles. Dans ce cas, les données transmises ne contiennent pas d'informations sur le mode de fonctionnement du microcircuit. Par conséquent, pour le décodeur, il convient d'ajouter une option qui spécifie le mode dans lequel le microcircuit fonctionne.



class Decoder(srd.Decoder):
    ...
    options = (
        {'id': 'key_num', 'desc': 'Key number', 'default': 8,
         'values': (8, 16)},
    )
    def start(self):
        ...
        self.key_num = self.options['key_num']


Cette option sera disponible pour définir la valeur dans l'interface utilisateur lors du choix d'un décodeur.



Comme vous pouvez le voir sur le chronogramme, les données sur la ligne SDO sont définies lorsque le SCL passe au niveau actif (bas) et sont sauvegardées lorsque le signal revient au niveau passif. À ce moment, le microcontrôleur et le décodeur peuvent tous deux fixer l'ensemble de données sur la ligne SDL. La transition de SCL au niveau actif peut être considérée comme le début de l'envoi de données suivant. Dans ce cas, la fonction de décodage ressemblera à ( ca9a370 ):



    def decode(self):
        self.wait({0: 'h', 1: 'f'})
        self.dv_block_ss = self.samplenum

        self.wait({0: 'h', 1: 'r'})
        self.put(self.dv_block_ss, self.samplenum,
                 self.out_ann, [0, ['Data valid', 'DV']])
        self.tw_block_ss = self.samplenum

        self.wait([{0: 'f', 1: 'h'}, {0: 'f', 1: 'f'}])
        self.put(self.tw_block_ss, self.samplenum,
                 self.out_ann, [1, ['Tw', 'Tw']])
        self.bt_block_ss = self.samplenum

        for i in range(self.key_num):
            (scl, sdo) = self.wait({0: 'r'})
            sdo = 0 if sdo else 1

            self.wait({0: 'f'})
            self.put(self.bt_block_ss, self.samplenum,
                     self.out_ann, [2, ['Bit: %d' % sdo, '%d' % sdo]])
            self.bt_block_ss = self.samplenum


Mais cette approche de placement d'annotations présente un inconvénient, l'annotation pour le dernier bit se poursuivra jusqu'à la prochaine lecture par le microcontrôleur des données.







. . , SCL . , SCL 2 ., . 'skip', , , . , . metadata(). Hz.



    def metadata(self, key, value):
        if key == srd.SRD_CONF_SAMPLERATE:
            self.timeout_samples_num = int(2 * (value / 1000.0))


Ensuite, la condition dans la fonction de décodage sera écrite en utilisant skip sous la forme suivante, plus une vérification supplémentaire que lors de la lecture des données sur le bouton pressé, le microcircuit n'est pas revenu à son état initial ( 6a0422d ).



    def decode(self):
        ...
        for i in range(self.key_num):
            ...
            self.wait([{0: 'f'}, {'skip': self.timeout_samples_num}])
            self.put(self.bt_block_ss, self.samplenum,
                     self.out_ann, [2, ['Bit: %d' % sdo, '%d' % sdo]])
            if (self.matched & 0b10) and i != (self.key_num - 1):
                break


Le décodeur peut désormais gérer l'envoi complet des données. Et ce sera pratique si, en plus des informations sur les bits individuels, une annotation est ajoutée sur le bouton qui a été appuyé. Pour ce faire, ajoutez une description d'une annotation supplémentaire. Étant donné que l'annotation pour appuyer sur le bouton fait référence à l'ensemble de la transmission de données et croise les annotations ajoutées précédemment, elle doit être placée dans un groupe distinct. Créons pour cela un nouveau groupe d'annotations 'Message clé'. ( 91c64e6 ).



class Decoder(srd.Decoder):
    ...
    annotations = (
        ('dv', 'Data valid'),
        ('tw', 'Tw'),
        ('bit', 'Bit'),
        ('key', 'Key press status'),
    )
    annotation_rows = (
        ('fields', 'Fields', (0, 1, 2)),
        ('keymsg', 'Key message', (3,)),
    )
    def decode(self):
        ...
        keys_pressed = list()

        for i in range(self.key_num):
            ...
        else:
            key_msg = \
                'Key: %s' % (','.join(keys_pressed)) if keys_pressed else 'Key unpressed'
            key_msg_short = \
                'K: %s' % (','.join(keys_pressed)) if keys_pressed else 'KU'

            self.put(self.dv_block_ss, self.samplenum,
                     self.out_ann, [3, [key_msg, key_msg_short]])






Jusqu'à ce point, tout le code ne fonctionnait qu'avec la première prémisse. Avez-vous remarqué 19% à côté du nom du décodeur? Il s'agit du pourcentage d'échantillons traités avant la sortie de la fonction decode (). Pour traiter tous les échantillons, il reste à ajouter une boucle sans fin autour du code pour décoder un envoi de données séparé ( 48f95fb ).



    def decode(self):
        ...
        while True:
            self.wait({Pin.SCL: self.passive_signal, Pin.SDO: self.front_edge})
            self.dv_block_ss = self.samplenum
            ...


Parce que le décodage se terminera automatiquement si la fonction wait () les parcourt tous lors de la recherche de l'échantillon suivant. En raison de ce changement, tous les échantillons et toutes les transmissions de données seront traités comme indiqué dans le KDPV .



La touche finale reste pour ajouter la possibilité de sélectionner le niveau de signal actif et un décodeur à part entière pour TTP229-BSF sera prêt. Le code source de la version finale est également disponible sur GitHub .




All Articles