Transformer une carte son ou un lecteur audio en port série TTL

For fun and profit

Image d'entête

par skywodd | | Licence (voir pied de page)

Catégories : Projets | Mots clefs : Musique Serial UART Hack


Dans ce second mini projet, je vais vous montrer comment utiliser une carte son ou n'importe quel lecteur audio en tant que port série TTL. Ce projet pourra vous servir de base pour concevoir des applications utilisant une sortie audio pour transmettre des trames série, infrarouge, voir même radio.

Sommaire

Bonjour à toutes et à tous !

Dans un précédent article, je vous avais montré comment transformer un port série en carte son minimaliste. C'était un mini projet fort sympathique, mais pas particulièrement utile dans la vraie vie.

Aujourd'hui, vous propose de faire l'exact opposé de mon précédent mini projet, à savoir transformer une carte son (ou plus généralement n'importe quel lecteur audio, smartphone inclus) en port série TTL.

Ce second mini projet peut sembler aussi inutile que le premier, mais c'est loin d'être le cas. Beaucoup de systèmes électroniques actuellement en vente utilisent ce genre d'astuce pour permettre aux utilisateurs de faire des mises à jour avec simplement via un PC, un smartphone ou un bête lecteur MP3.

Le plan

Illustration du format d'une trame série TTL

Illustration du format d'une trame série TTL

Dans mon précédent article, j'avais expliqué le format d'une trame série (c'est à dire comment un octet est physiquement transmis). Je vous invite donc à lire (ou relire) le chapitre à ce sujet ;)

Pour rappel, une trame série "standard" se compose :

  • D'un bit de start, signifiant le début d'un octet,

  • De 8 bits de données,

  • D'un bit de stop, signifiant la fin de l'octet.

De plus, par défaut, le port série est à l'état haut.

Trame série TTL vue avec le logiciel Audacity

Signal d'une trame série TTL dans un fichier audio

L'idée de cet article est de générer par logiciel une trame série et de l'encoder sous la forme d'un fichier audio. Après tout, une carte son est un périphérique capable de sortie des signaux analogiques avec une grande précision (entre 16 et 24 bits pour les meilleures cartes son) et une fréquence assez rapide (au maximum entre 44100 et 48000 échantillons audio par seconde en fonction de la carte son). Or, tout périphérique capable de générer un signal analogique peut générer un signal numérique comme une trame série. Un signal numérique n'est rien de plus qu'un signal analogique avec seulement deux états possibles.

Le montage

Avant de générer le fichier audio, il faut être sûr que la partie matérielle va suivre le rythme. Pour cela, il va falloir réaliser un petit montage, assez simple, réalisable avec seulement 6 composants.

Le but de ce montage est de convertir le signal audio (+/- 1 volt) en un signal numérique "propre" avec deux états : 0 volt et 5 volts. Pour ce faire, le montage utilise un amplificateur opérationnel en comparateur inverseur de tension.

Photographie du matériel nécessaire à la réalisation du montage de génération de trames série à partir d'un fichier audio

Matériel nécessaire

Pour réaliser ce premier montage, il va nous falloir :

  • Un module USB-série (et son câble USB),

  • Un amplificateur opérationnel fonctionnant en 5 volts (j'utilise ici un LM324, mais un TLC274 ferait aussi l'affaire, moyennant une modification du câblage),

  • Deux résistances de 4.7K ohms, code couleur jaune – violet – rouge,

  • Un potentiomètre de 10K ohms,

  • Un connecteur audio jack 3.5mm,

  • Une plaque d'essai et des fils pour câbler notre montage.

Vue prototypage du montage de génération de trames série à partir d'un fichier audio

Vue prototypage du montage

Vue schématique du montage de génération de trames série à partir d'un fichier audio

Vue schématique du montage

Pour commencer le câblage, il va falloir mettre la masse du module USB-série à la broche GND de l'amplificateur opérationnel (broche 11 du LM324). Il faudra aussi connecter le second anneau de la prise audio (celui du côté du câble) à la masse du module USB-série. Il conviendra ensuite de relier l'alimentation du module USB-série à la broche VCC de l'amplificateur opérationnel (broche 4 du LM324).

Vient ensuite le potentiomètre de réglage de 10K. Il faudra le câbler entre la masse du module USB-série et l'alimentation de celui-ci. La sortie du potentiomètre devra elle être connectée à l'entre non inverseuse de l'amplificateur opérationnel (broche 3 du LM324).

Il faudra câbler la pointe de la prise audio (annotée "tip") sur l'entrée inverseuse de l'amplificateur opérationnel (broche 2 du LM324). Il faudra aussi prendre soin de mettre une résistance de 4.7K ohms entre la pointe de la prise audio et la masse du circuit, de manière à ce que la carte son ou le lecteur audio génère une tension.

Photographie du montage de génération de trames série à partir d'un fichier audio

Le montage fini

Une fois tout cela câblé, il suffit de câbler la sortie de l'amplificateur opérationnel (broche 1 du LM324) à l'entrée RX du module USB-série pour terminer le montage. Il faudra là aussi prendre soin de mettre une résistance de 4.7K ohms entre la sortie de l'amplificateur et l'alimentation du module USB-série, de manière à ce que l'amplificateur fonctionne correctement.

Le code

Le plus gros de cet article réside dans la partie programme qui génère le fichier audio. Pour garder le code simple et lisible, j'ai décidé de faire un script Python 3.

1
2
3
4
5
# Vitesse de communication série
SERIAL_BAUDRATE = 2400

# Fréquence d'échantillonnage (min 4000 ~ max 48000)
AUDIO_SAMPLE_RATE = 44100

Le code commence par la déclaration de la vitesse du port série et la fréquence d'échantillonnage du fichier audio.

N.B. La vitesse du port série doit être au moins dix fois inférieur à la fréquence du fichier audio pour que le script fonctionne (un octet = 10 bits).

Une carte son ou un lecteur audio supporte en général une fréquence maximum de 44 100 Hertz, voir 48 000 Hertz pour les cartes son de PC. Certaines cartes son haut de gamme peuvent aller jusqu'à 96 000 Hertz.

Pour une compatibilité maximum avec les différents types de cartes son et lecteurs audio, j'utilise une fréquence de port série "audio" à 2400 bits par seconde et une fréquence d'échantillonnage de 44100 Hertz.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def convert_binary_file(input_filename, output_filename):
    """
    Transforme le fichier binaire fourni en un fichier audio contenant les données binaires du fichier source sous forme de trames série TTL.
    """

    # Ouvre le fichier d'entrée et de sortie
    with open(input_filename, 'rb') as fi:
        with wave.open(output_filename, 'wb') as fo:

            # Configure l'entête du fichier Wave
            fo.setnchannels(1)  # Mono
            fo.setsampwidth(1)  # 8 bits
            fo.setframerate(AUDIO_SAMPLE_RATE)

La fonction convert_binary_file() s'occupe de la conversion des données brutes en fichier audio.

La fonction commence par ouvrir le fichier source et le fichier de sortie en mode binaire. La fonction configure ensuite l'entête du fichier audio pour obtenir en sortie un fichier audio mono canal, 8 bits, non compressés, à la fréquence d'échantillonnage désirée.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
            # Calcul le nombre d'échantillons audio par bit
            # Plus il y a d'échantillons par bit plus la qualité du signal audio sera grande
            # Une fréquence d'échantillonnage élevé donnera donc les meilleurs résultats
            NB_SAMPLES_PER_BIT = AUDIO_SAMPLE_RATE // SERIAL_BAUDRATE

            # Constantes pour la communication série
            BIT_ONE = b'\x00' * NB_SAMPLES_PER_BIT
            BIT_ZERO = b'\xFF' * NB_SAMPLES_PER_BIT
            FRAME_BREAK = BIT_ZERO * 20
            FRAME_IDLE = BIT_ONE * 20
            writeframes = fo.writeframes

Plusieurs constantes sont ensuite calculées par la fonction :

  • le nombre d'échantillons audio par bit du port série,

  • l'équivalent audio d'un "1" et d'un "0",

  • l'équivalent audio d'un "break" (coupure de signal série) et d'un blanc (pas de transmission série).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
            # Début du fichier audio
            writeframes(FRAME_BREAK)
            writeframes(FRAME_IDLE)

            # Conversion octet par octet
            for data in fi.read():
                
                # Ajoute un bit de start au buffer audio
                writeframes(BIT_ZERO)
             
                # Ajoute 8 bits de données au buffer audio
                for i in range(0, 8):
                    if data & (1 << i):
                        writeframes(BIT_ONE)
                    else:
                        writeframes(BIT_ZERO)

                # Ajoute un bit de stop au buffer audio
                writeframes(BIT_ONE)

            # Fin du fichier audio
            writeframes(FRAME_IDLE * 10)

Pour finir, une boucle lit chaque octet du fichier source, convertit l'octet en données audio et ajoute les bits de start et de stop.

N.B. Un break suivi d'un blanc est ajouté en début de fichier audio pour être sûr que le port série recevant les données est dans un état connu. Dix blancs en fin de fichier audio permettent de s'assurer que les données en fin de communication sont bien lues correctement. Les lecteurs audio ont toujours tendance à faire n'importe quoi en fin de lecture.

Le code complet avec commentaires :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
"""
Transforme un fichier binaire en un fichier audio contenant les données binaires sous forme de trames série TTL.
"""

import wave
import argparse


# Vitesse de communication série
SERIAL_BAUDRATE = 2400

# Fréquence d'échantillonnage (min 4000 ~ max 48000)
AUDIO_SAMPLE_RATE = 44100


def convert_binary_file(input_filename, output_filename):
    """
    Transforme le fichier binaire fourni en un fichier audio contenant les données binaire du fichier source sous forme de trames série TTL.
    """

    # Ouvre le fichier d'entrée et de sortie
    with open(input_filename, 'rb') as fi:
        with wave.open(output_filename, 'wb') as fo:

            # Configure l'entête du fichier Wave
            fo.setnchannels(1)  # Mono
            fo.setsampwidth(1)  # 8 bits
            fo.setframerate(AUDIO_SAMPLE_RATE)
 
            # Calcul le nombre d'échantillons audio par bit
            # Plus il y a d'échantillons par bit plus la qualité du signal audio sera grande
            # Une fréquence d'échantillonnage élevé donnera donc les meilleurs résultats
            NB_SAMPLES_PER_BIT = AUDIO_SAMPLE_RATE // SERIAL_BAUDRATE

            # Constantes pour la communication série
            BIT_ONE = b'\x00' * NB_SAMPLES_PER_BIT
            BIT_ZERO = b'\xFF' * NB_SAMPLES_PER_BIT
            FRAME_BREAK = BIT_ZERO * 20
            FRAME_IDLE = BIT_ONE * 20
            writeframes = fo.writeframes
            
            # Début du fichier audio
            writeframes(FRAME_BREAK)
            writeframes(FRAME_IDLE)

            # Conversion octet par octet
            for data in fi.read():
                
                # Ajoute un bit de start au buffer audio
                writeframes(BIT_ZERO)
             
                # Ajoute 8 bits de données au buffer audio
                for i in range(0, 8):
                    if data & (1 << i):
                        writeframes(BIT_ONE)
                    else:
                        writeframes(BIT_ZERO)

                # Ajoute un bit de stop au buffer audio
                writeframes(BIT_ONE)

            # Fin du fichier audio
            writeframes(FRAME_IDLE * 10)


# CLI wrapper
if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Turn binary data into a serial-like signal audio wave file.')
    parser.add_argument('input_filename', metavar='<INPUT FILE>', help='The input binary file.')
    parser.add_argument('output_filename', metavar='<OUTPUT FILE>', help='The output audio wave file.')

    args = parser.parse_args()
    convert_binary_file(args.input_filename, args.output_filename)

L'extrait de code ci-dessus est disponible en téléchargement sur cette page.

Le résultat

Pour tester le bon fonctionnement du montage et du code, j'ai converti une page de "Lorem Ipsum" en fichier audio avant de lire ledit fichier via la carte son de mon PC.

Exemple de transmission de données via le port série "audio"

Exemple de transmission de données

Tout semble fonctionner à première vue. Les données sont biens reçues dans le terminal série en même temps que le fichier audio est lu.

Vérification des checksum des données envoyées et reçues via le port série "audio"

Pas d'erreur de communication en vue

Pour m'assurer que la communication est parfaite (pas d'erreur de transmission ou de perte de données), j'ai enregistré l'intégralité des données reçues dans un fichier que j'ai comparé à l'original avec une somme de contrôle SHA-256. Résultat : aucune erreur de communication détectée, les données émises et reçues ne sont identiques.

Bonus : des idées de projets dérivés

Dans cet article, je me suis limité à faire de l'émission. Il serait tout à fait envisageable de faire de la réception avec un système d'écoute audio en temps réel. Toutes les cartes son d'ordinateurs ont une entrée micro. Seulement cela est (beaucoup) plus complexe à réaliser en terme de code.

Cependant, simplement en faisant de l'émission de signaux, on pourrait imaginer faire des choses fort sympathiques en modifiant le code ci-dessus. Voici quelques idées :

  • reprogrammer un microcontrôleur,

  • commander une télévision en simulant les signaux infrarouges de la télécommande,

  • transmettre des données via un module radio,

  • etc.

Conclusion

Ce mini projet est désormais terminé.

Si ce mini projet vous a plu, n'hésitez pas à le commenter sur le forum, à le partager sur les réseaux sociaux et à soutenir le site si cela vous fait plaisir.