Objectif
Une fois le signal radio réceptionné, nous souhaitons démoduler le signal et réaliser un traitement automatisé des données afin de tracer automatiquement les courbes et visualiser la trajectoire du ballon sur Google Map.
De plus ce dispositif nous sera utile pour la chasse au ballon : nous irons avec le scanner radio sur la dernière position connue du ballon et ainsi nous devrions capter la position exacte de l'atterrissage.
Présentation
Pour
réaliser la démodulation FSK nous avons utiliser
le circuit spécialisé XR2211.
Comme le montre le schéma de principe, il est constitué :
- d'un étage amplificateur à contrôle automatique du gain (patte2)
- d'une boucle à verrouillage de phase (PLL) et d'un VCO
- d'un filtre passe bas externe RF et CF
- d'un trigger pour la remise en forme du signal.
CAO - Proteus
A partir de ce datasheet nous avons élaboré notre carte d'extension Arduino.
Pour le choix des composants nous avons aussi utilisé le document Planète Sciences suivant.
Nous avons adapté l'étage de sortie pour être compatible
avec un arduino : plus besoin d'un MAX 232, l'Arduino nous
offre une liaison UART 0 - 5 V compatible avec le XR2211 et
une sortie USB (ainsi qu'une liaison bluetooth avec le
module HC06).
Son entrée est la sortie audio de notre scanner radio. Elle
récupère notre modulation FSK audio sur la patte 2 :
- 900 Hz -> 0 logique
- 1500 Hz -> 1 logique
Pour les séparer à la sortie de la PLL nous allons utiliser un filtre passe bas du second ordre constitué :
- d'une cellule RF CF ;
- d'un suiveur ;
- d'une seconde cellule RF2 CF2.

Test et analyse de notre montage
Oscillogramme 1 : signal modulé
Sortie de notre modulateur XR2206 modulé par la sortie UART de notre Arduino modulateur |
![]() |
Oscillogramme 2 : sortie PLL
On observe que le signal de sortie PLL contient le signal modulant et des résidus de porteuse très important.
|
![]() |
Oscillogramme 3 : sortie passe bas donnée 1
Les résidus de porteuse sont légèrement atténués. |
![]() |
Oscillogramme 4 : sortie passe bas donnée 2
Les résidus de porteuse sont atténués. Nous pouvons bien séparer l'état haut (> 2,5 V) et l'état bas (<0.94 V) comme le montre les curseurs. |
![]() |
Oscillogramme 5 : sortie trigger intégré (patte 7 : Data Output)
Nous récupérons bien le signal modulant mais inversé. |
![]() |
Oscillogramme 6 : sortie inverseur
On note les défauts de l’ampli Op
suivants (Alimentation 0 - 5 V) Le 0 est
à 0.64 V Le 1 est à 3.92 V Mais cela reste compatible avec la
liaison UART. Les signaux de départ et d’arrivée
transportent bien le même message |
![]() |
Le Contrôle Automatique de Gain
Nous avons utilisé un analyseur logique associé au logiciel Saleae afin d'acquérir le signal traité.
Lors d'un premier essai, réalisé par une liaison filaire, nous n'avions pas de bruit. Mais lors du test de la chaîne de transmission complète avec liaison radio nous avons récupéré beaucoup de bruits lorsqu'il n'y avait aucun signal émis.
On récupère bien les 3 trames mais lors de la période de "silence", le contrôle automatique de gain du XR2211 amplifie le bruit et génère un signal aléatoire.
Pour éliminer ce signal aléatoire de manière logicielle nous disposons de 3 repères :
- le signal de synchro (octet 0) qui commence par un 255,
- les 10 octets suivants ne comportent pas de 255,
- le cheksum qui se situe 11 octets après.
Un signal comportant un 255 suivi de 10 octets sans 255 avec sur l'octet 11 un checksum valide sera considéré comme bon.
Après traitement logiciel nous n'isolons que le signal valide :
Détail d'une trame (Synchro : 255 ; 8 capteurs : 0,0,0,0,0,0,0,0 ; batterie : 115 ; checksum : 57) :
Voici le traitement réalisé par l'Arduino pour supprimer le bruit :
void loop() {
if(Serial.available()) {
c=Serial.read();
if (c==255) j=0; // initialisation de j lors de la rencontre d'un 255
if (j<11) Trame[j]= c; // Stockage des 10 octets suivant le 255 dans Trame[j]
if (j==10) Check(); // vérification validité trame
j++;
if (valide) {
Traitement();
while(millis()-t1<1000) Serial.read(); // on vide le buffer série et on attend une seconde depuis la trame valide
valide=false;
}
}
}
void Check() {
for ( l=1;l<10;l++)chk+=Trame[l]; // Calcul du checksum des octets 1 à 9
chk=chk/2;
// debug();
if (chk==Trame[10]){ // Comparaison du checksum avec l'octet 10
valide=true;
dt=millis()-t1;
t1=millis();
}
oldchk=chk;
chk=0;}
Le typon
Pour éviter les faux contacts le jour J nous avons réalisé sur ARES un typon.
Cette carte a les bonnes dimensions pour s'enficher avec un Arduino UNO.
Le programme démodulation simple
Voici un premier programme démodulation pour fonctionner avec Kicapt uniquement.
L'électronique reçoit sur les pattes FSK le signal de sortie de notre scanner radio, le XR2211 démodule le signal et le transmet à la patte Rx de l'Arduino Uno.
Attention pour téléverser le programme il faut donc déconnecter la carte d'extension sinon il y a un conflit sur la patte Rx et le programme ne se téléverse pas.
Le programme ci-dessous vérifie la validité des trames reçues et retransmet sur la patte Tx les trames valides répétées 3 fois.
Le programme Kicapt recoit alors ces trames sur le port com de l'Arduino.
// 1.c) Les variables globales
int LED=13;
byte Trame[11];
byte i,j,c,chk;
unsigned long t1;
/* 2) Zone 2 : Initialisation (le setup) */
void setup() {
// put your setup code here, to run once:
pinMode(LED,OUTPUT); // Control de la LED
Serial.begin(600) ;
}
/* 3) Zone 3 : le Programme Principal */
void loop() {
if(Serial.available()) {
c=Serial.read();
if (c==255) j=0; // initialisation de j lors de la rencontre d'un 255
if (j<11) Trame[j]= c; // Stockage des 10 octets suivant le 255 dans Trame[j]
if (j==10) Check(); // vérification validité trame
j++;
}
}
/* 4) Zone 4 : les sous programmes (ou fonctions) */
void Check() {
chk=0;
for ( i=1;i<10;i++)chk+=Trame[i]; // Calcul du checksum des octets 1 à 9
chk=chk/2;
// debug();
if (chk==Trame[10]){ // Comparaison du checksum avec l'octet 10
t1=millis();
digitalWrite(LED,HIGH);
for (byte trame=0;trame<3;trame++) { // Pour être compatible avec le logiciel Kicapt
for(i=0;i<11;i++) Serial.write(Trame [i]); // On renvoie 3 fois la trame valide reçue
Serial.flush(); // on attend que la trame soit réellement envoyée
delay(32); // durée environ 615 ms
}
while(millis()-t1<1000) Serial.read(); // on vide le buffer série et on attend une seconde depuis la trame valide
digitalWrite(LED,LOW);
}
}
Notre programme démodulation complet :
Ce programme reprend le même algorithme que précédemment mas en
ajoutant de nouvelles fonctions :
- traiter les trames reçues afin de reconstituer les coordonnées GPS et les mesures effectuées,
- afficher les coordonnées GPS reçues sur un écran LCD,
- transmettre à un émetteur bluetooth les coordonnées GPS et les mesures
- stocker toutes les mesures reçues sur une carte SD.
Il nécessite les bibliothèques I2C (Wire), écran LCD I2C, sérial software et SD. De ce fait il utilise quasiment toute la mémoire d'un Arduino Uno.
/* 1) Zone 1 : les déclarations */
// 1.a) Les bibliothèques et création d'objets
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <SoftwareSerial.h>
#include <SD.h>
LiquidCrystal_I2C lcd(0x27,16,4); //création de l'objet LCD(I2C adresse 0x27) 16 colonnes 4 lignes
int Txd=6, Rxd=7; // HC05 Bluetooth
SoftwareSerial BT(Txd, Rxd); // RX, TX : il faut relier Rx de l'Arduino au Tx du Bluetooth
// 1.c) Les variables globales
int chipSelect=10; // chipSelect est la seule patte à déclarer :
// Les autres sont déjà déclarées par la bibliothèque
int LED=13;
unsigned long t0,t1,dt;
word n=0;
byte Trame[11];
float N,E;
word Alti;
word E1,N1,Alti1;
word E2,N2,Alti2;
word AltiOld,NOld,EOld;
byte chk=0,oldchk,voie;
int Pext,L1,A;
int nerreur;
float Tint,Text,Pile;
byte c;
byte i,j,k,l,m;
boolean valide=false, OK=false, silence=true, SDOK;
/* 2) Zone 2 : Initialisation (le setup) */
void setup() {
// put your setup code here, to run once:
pinMode(LED,OUTPUT); // Control de la LED
lcd.init();
lcd.noBacklight();
lcd.print("Demodulateur Kiwi");
Serial.begin(600) ;
BT.begin(9600);
BT.println("Demodulateur Kiwi;");
enteteSD();
}
/* 3) Zone 3 : le Programme Principal */
void loop() {
if(Serial.available()) {
c=Serial.read();
if (c==255) j=0; // initialisation de j lors de la rencontre d'un 255
if (j<11) Trame[j]= c; // Stockage des 10 octets suivant le 255 dans Trame[j]
if (j==10) Check(); // vérification validité trame
j++;
if (valide) {
digitalWrite(LED,HIGH);
Traitement();
AfficheLcd();
ecritureSD();
while(millis()-t1<1000) Serial.read(); // on vide le buffer série et on attend une seconde depuis la trame valide
valide=false;
digitalWrite(LED,LOW);
}
}
if (millis()-t1>3999) erreur();
}
/* 4) Zone 4 : les sous programmes (ou fonctions) */
void Check() {
for ( l=1;l<10;l++)chk+=Trame[l]; // Calcul du checksum des octets 1 à 9
chk=chk/2;
// debug();
if (chk==Trame[10]){ // Comparaison du checksum avec l'octet 10
valide=true;
dt=millis()-t1;
t1=millis();
}
oldchk=chk;
chk=0;}
void Traitement() {
n++; // Numéro de trame valide
nerreur=0;
if (Trame[9]==254)
{if (m==4) OK=true;
m=0;}
else m++;
switch (m%5){
case 1:
N1=Trame[9]; break;
case 2:
E1=Trame[9]; break;
case 3:
Alti1=Trame[9]; break;
case 4:
Pile=(Trame[9]+14.0)/16.0; break; }
AltiOld=Alti2;
NOld=N2;
EOld=E2;
Alti2=Trame[1];
E2=Trame[2];
N2=Trame[3];
L1=Trame[4];
A=Trame[5];
Tint=(Trame[6]-80.0)/4.0;
Text=(Trame[7]-150.0)/3.0;
Pext=Trame[8]*4;
if (abs(float(Alti2)-float(AltiOld))>100 && m%5!=3) { // Si brusque variation de Alti1 et Alti2 non transmis
if (AltiOld>200) Alti1++; // Si l'ancien Alti1 > 200 alors on incrémente l'octet de poids fort (Alti2)
if (AltiOld<100) Alti1--;} // Si l'ancien Alti1 < 100 alors on décrémente l'octet de poids fort (Alti2)
if (abs(float(N2)-float(NOld))>100 && m%5!=1) {
if (NOld>200) N1++;
if (NOld<100) N1--;}
if (abs(float(E2)-float(EOld))>100 && m%5!=2) {
if (EOld>200) E1++;
if (EOld<100) E1--;}
Alti=Alti1*256+Alti2;
N=float(N1*256+N2)/10000.0 + 40.0;
E=float(E1*256+E2)/10000.0;}
void AfficheLcd() {
lcd.setCursor(0, 0); // Envoi des coordonnées sur l'écran LCD
if (OK) lcd.print("Trame OK num = ");
else lcd.print("Insuffisant! = ");
lcd.print(n);
lcd.print (" ");
lcd.setCursor(0, 1);
lcd.print("N=");lcd.print(N,4);
lcd.print(" E=");lcd.print(E,4);
lcd.setCursor(0, 2);
lcd.print("Altitude = ");lcd.print(Alti);
lcd.print (" m ");
lcd.setCursor(0, 3);
lcd.print("Pile = ");lcd.print(Pile);
lcd.print (" V ");
lcd.setCursor(0, 0);
if (OK) BT.print("Trame OK"); // Envoi des coordonnées sur le Bluetooth
else BT.print("Insuffisant!"); // Durée approximative : 75 ms d'après l'analyseur logique
if(!SDOK) BT.print(" SD! ");
BT.print(";");BT.print(n);BT.print(";");
BT.print(N,5);BT.print (";");BT.print(E,5);BT.print (";");
BT.print(Alti);BT.print (";");BT.print(Pext);BT.print (";");
BT.print(Text,1);BT.print (";");BT.print(Tint,1);BT.print (";");
BT.print(L1);BT.print (";");BT.print(A);BT.print(";");
BT.print(Pile);BT.println (";");
for (byte trame=0;trame<3;trame++) { // Pour être compatible avec le logiciel Kicapt
for(l=0;l<11;l++) Serial.write(Trame [l]); // On renvoi 3 fois la trame valide reçue
Serial.flush(); // on attend que la trame soit réellement envoyée
delay(32); // durée environ 615 ms
}
}
void erreur() {
dt=millis()-t1;
t1=millis();
nerreur=nerreur+2;
while (Serial.available()) Serial.read(); // on vide le buffer série
lcd.setCursor(0, 0);
lcd.print("Erreur trame = ");lcd.print(n);
lcd.print (" ");
BT.print("Trame = ");BT.print(n+nerreur);
BT.print(" Perte = ");BT.print(2*nerreur);BT.print(" s ");
BT.print("Taux = ");BT.print(100*n/(n+nerreur));BT.print(" % ");
BT.println(";");
}
void debug() {
lcd.setCursor(0, 2);
lcd.print("Chk");lcd.print(chk);
lcd.setCursor(0, 3);
lcd.print("Trame");lcd.print(Trame[10]);
for(l=0;l<11;l++) {
Serial.print(Trame [l]); Serial.print("\t");
}
Serial.print ("Chk=");Serial.println(chk);
Serial.println(dt);
}
void enteteSD(){ // Exemple d'une série de mesure à stocker
// Format csv le séparateur de colonne est : ";"
pinMode(chipSelect, OUTPUT);
if (!SD.begin(chipSelect)) {lcd.println("Erreur carte SD"); SDOK=false; return;}
SDOK=true;
File GPStab = SD.open("Mesures.csv", FILE_WRITE);
if (GPStab) {
GPStab.println("Statut;Trame;Latitude;Longitude;Altitude;Tint;Text;Pression;L1;A;Pile");
GPStab.close();
}
}
void ecritureSD() {
File GPStab = SD.open("Mesures.csv", FILE_WRITE);
if (GPStab) {
if (OK) {GPStab.print("OK"); GPStab.print(";");}
else {GPStab.print("Ins"); GPStab.print(";");}
GPStab.print(n+nerreur); GPStab.print(";");
GPStab.print(N,5); GPStab.print(";");
GPStab.print(E,5); GPStab.print(";");
GPStab.print(Alti); GPStab.print(";");
GPStab.print(Tint,1); GPStab.print(";");
GPStab.print(Text,1); GPStab.print(";");
GPStab.print(Pext); GPStab.print(";");
GPStab.print(L1); GPStab.print(";");
GPStab.print(A); GPStab.print(";");
GPStab.print(Pile); GPStab.print(";");
GPStab.println(); // fin de ligne
GPStab.close();
SDOK=true;
}
else {
SDOK=false;
}
}