Réalisation d’un VFO DDS
Par ON6FS
Il est bon d’expliquer au préalable les besoins !
Je souhaite réaliser le récepteur 406 MHz de Denis F5LEB (http://www.f1lvt.com/files/901-Tuto-Recepteur-406Mhz.161.pdf).
Ce récepteur associé à la carte de décodage de Jean-Paul F1LVT (http://www.f1lvt.com/files/340-Version_v-D5F.213.pdf) devrait me permettre de recevoir et décoder les balises de détresse, dans le cadre de l’ADRASEC.
A la base, l’oscillateur local de ce récepteur 406 MHz est piloté par un quartz de 24,70825 MHz.
Nous connaissons tous les difficultés pour obtenir un quartz taillé sur une fréquence exotique.
De plus, l’excursion en fréquence d’un oscillateur piloté par quartz est très limitée : de l’ordre de quelques KHz.
Pour remplacer ce quartz, j’ai fait le choix de retenir la solution d’un VFO DDS utilisant le bien connu AD9850.
On peut trouver sur le WEB le montage de Rich AD7C (http://www.ad7c.com/projects/ad9850-dds-vfo/) qui a été exploité par de nombreux OM’s et cela à toutes les sauces !
L’avantage avec cette méthode, le récepteur pourra fonctionner sur une plus grande plage de fréquences.
Pour mon projet, la plage de réception sera de 400 MHz à 410 MHz.
En complément des balises de détresse, ce récepteur pourra donc être exploité dans le cadre des poursuites de radiosondes météo.
Schéma
Voici le schéma dessiné par Jiri OK1DXK, auquel j’ai ajouté quelques capacités de découplage de 100 nF :
Le montage de Rich AD7C utilise un Arduino (j’ai opté pour la version Nano), un module AD9850 et un encoder rotatif pour faire varier la fréquence.
Dans ma version, je n’ai pas installé la résistance de 1 KΩ et l’interrupteur sur la pin A5, prévue pour organiser un décalage de fréquence (par exemple entre l’émission et la réception).
Circuit imprimé (PCB)
Ci-dessous les deux faces du circuit imprimé (PCB : Printed Circuit Board) :
Liste des composants
Tous les liens Aliexpress sont purement informatifs, ils vous permettront de bien identifier les composants.
De nombreux autres commerçants peuvent vous fournir les composants.
- U1 : Arduino Nano V3.0 (328p) https://fr.aliexpress.com/item/32647196840.html
- U3 : AD9850 https://fr.aliexpress.com/item/4001291186117.html
- Encoder rotatif https://fr.aliexpress.com/item/32718891419.html
- Écran LCD1602 2 lignes de 16 caractères https://fr.aliexpress.com/item/32511014601.html ATTENTION : ne pas prendre une version équipée d’un interface I2c !
- P1 : connecteur 4 contacts mâles Dupont (option I2c)
- P2 : connecteur 5 contacts mâles Dupont (encoder rotatif)
- P3 : deux fois connecteur 6 contacts mâles Dupont (écran LCD)
- P5 : connecteur SMA (sortie HF)
- P6 : connecteur 2 contacts XH 2.54 (Alimentation 9 V)
- 2 câbles de 6 conducteurs avec 2 connecteurs mâles-femelles Dupont (écran LCD) https://fr.aliexpress.com/item/4001196340888.html
- 1 câble de 5 conducteurs avec 2 connecteurs femelles Dupont (Encoder rotatif) https://fr.aliexpress.com/item/4001196340888.html
- 1 câble de 2 conducteurs avec 1 connecteurs femelle XH2.54 (alimentation) https://fr.aliexpress.com/item/32954418743.html
- 1 connecteur 16 contacts femelles Dupont (à souder sur l’écran LCD)
- 2 connecteurs 10 pins femelles Dupont (support AD9850)
- 2 connecteurs 15 pins femelles Dupont (support Arduino Nano)
- 2 connecteurs 6 contacts mâles Dupont (à souder sur l’écran LCD)
- Radj : Ajustable de 10 ou 25 KΩ (SMD)
- R1 : 100 Ω (SMD 1206)
- R2 : 100 Ω (SMD 1206)
- C1 : 100 nF (SMD 1206)
- C2 : 100 nF (SMD 1206)
- C3 : 100 nF (SMD 1206)
Les connecteurs Dupont mâles et femelles sont également vendus en barrettes de 40 pins. Vous pourrez les diviser en fonction de vos besoins https://fr.aliexpress.com/item/32798891275.html et https://fr.aliexpress.com/item/32902727085.html
Le PCB
Le PCB a été dessiné sur EasyPCB.com : https://easyeda.com/ON6FS/dectra.
De ce site web, vous pourrez le faire fabriquer chez JLCPCB.com (par exemple). Le coût de fabrication est de 1,70 € pour 5 PCB (min) + frais d’expédition de 10 à 20 € selon les délais (le transport le moins cher prend une dizaine de jours ouvrables).
Ci-dessous les vues de l’implantation des composants sur les 2 faces du PCB :
Vous remarquerez quelques straps dûs à des erreurs de conception. Le PCB sur EasyPCB.com a été corrigé depuis. Les 2 straps ne seront donc plus nécessaires !
Sketch original de Rich AD7C
Cette version de VFO DDS fonctionnera de 1 MHz à 30 MHz.
La fréquence par défaut est sur 7,2 MHz.
Un pas de 10 Hz modifiable (10 Hz, 50 Hz, 100 Hz, 500 Hz, 1 KHz, 2,5 KHz, 5 KHz, 10 KHz, 100 KHz et 1 MHz) par pression successive de l’encoder rotatif.
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 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
/* Main code by Richard Visokey AD7C - www.ad7c.com Revision 2.0 - November 6th, 2013 */ // Include the library code #include <LiquidCrystal.h> #include <rotary.h> #include <EEPROM.h> //Setup some items #define W_CLK 8 // Pin 8 - connect to AD9850 module word load clock pin (CLK) #define FQ_UD 9 // Pin 9 - connect to freq update pin (FQ) #define DATA 10 // Pin 10 - connect to serial data load pin (DATA) #define RESET 11 // Pin 11 - connect to reset pin (RST) #define pulseHigh(pin) {digitalWrite(pin, HIGH); digitalWrite(pin, LOW); } Rotary r = Rotary(2,3); // sets the pins the rotary encoder uses. Must be interrupt pins. LiquidCrystal lcd(12, 13, 7, 6, 5, 4); // I used an odd pin combination because I need pin 2 and 3 for the interrupts. int_fast32_t rx=7200000; // Starting frequency of VFO int_fast32_t rx2=1; // variable to hold the updated frequency int_fast32_t increment = 10; // starting VFO update increment in HZ. int buttonstate = 0; String hertz = "10 Hz"; int hertzPosition = 5; byte ones,tens,hundreds,thousands,tenthousands,hundredthousands,millions ; //Placeholders String freq; // string to hold the frequency int_fast32_t timepassed = millis(); // int to hold the arduino miilis since startup int memstatus = 1; // value to notify if memory is current or old. 0=old, 1=current. int ForceFreq = 1; // Change this to 0 after you upload and run a working sketch to activate the EEPROM memory. YOU MUST PUT THIS BACK TO 0 AND UPLOAD THE SKETCH AGAIN AFTER STARTING FREQUENCY IS SET! void setup() { pinMode(A0,INPUT); // Connect to a button that goes to GND on push digitalWrite(A0,HIGH); lcd.begin(16, 2); PCICR |= (1 << PCIE2); PCMSK2 |= (1 << PCINT18) | (1 << PCINT19); sei(); pinMode(FQ_UD, OUTPUT); pinMode(W_CLK, OUTPUT); pinMode(DATA, OUTPUT); pinMode(RESET, OUTPUT); pulseHigh(RESET); pulseHigh(W_CLK); pulseHigh(FQ_UD); // this pulse enables serial mode on the AD9850 - Datasheet page 12. lcd.setCursor(hertzPosition,1); lcd.print(hertz); // Load the stored frequency if (ForceFreq == 0) { freq = String(EEPROM.read(0))+String(EEPROM.read(1))+String(EEPROM.read(2))+String(EEPROM.read(3))+String(EEPROM.read(4))+String(EEPROM.read(5))+String(EEPROM.read(6)); rx = freq.toInt(); } } void loop() { if (rx != rx2){ showFreq(); sendFrequency(rx); rx2 = rx; } buttonstate = digitalRead(A0); if(buttonstate == LOW) { setincrement(); }; // Write the frequency to memory if not stored and 2 seconds have passed since the last frequency change. if(memstatus == 0){ if(timepassed+2000 < millis()){ storeMEM(); } } } ISR(PCINT2_vect) { unsigned char result = r.process(); if (result) { if (result == DIR_CW){rx=rx+increment;} else {rx=rx-increment;}; if (rx >=30000000){rx=rx2;}; // UPPER VFO LIMIT if (rx <=1000000){rx=rx2;}; // LOWER VFO LIMIT } } // frequency calc from datasheet page 8 = <sys clock> * <frequency tuning word>/2^32 void sendFrequency(double frequency) { int32_t freq = frequency * 4294967295/125000000; // note 125 MHz clock on 9850. You can make 'slight' tuning variations here by adjusting the clock frequency. for (int b=0; b<4; b++, freq>>=8) { tfr_byte(freq & 0xFF); } tfr_byte(0x000); // Final control byte, all 0 for 9850 chip pulseHigh(FQ_UD); // Done! Should see output } // transfers a byte, a bit at a time, LSB first to the 9850 via serial DATA line void tfr_byte(byte data) { for (int i=0; i<8; i++, data>>=1) { digitalWrite(DATA, data & 0x01); pulseHigh(W_CLK); //after each bit sent, CLK is pulsed high } } void setincrement(){ if(increment == 10){increment = 50; hertz = "50 Hz"; hertzPosition=5;} else if (increment == 50){increment = 100; hertz = "100 Hz"; hertzPosition=4;} else if (increment == 100){increment = 500; hertz="500 Hz"; hertzPosition=4;} else if (increment == 500){increment = 1000; hertz="1 Khz"; hertzPosition=6;} else if (increment == 1000){increment = 2500; hertz="2.5 Khz"; hertzPosition=4;} else if (increment == 2500){increment = 5000; hertz="5 Khz"; hertzPosition=6;} else if (increment == 5000){increment = 10000; hertz="10 Khz"; hertzPosition=5;} else if (increment == 10000){increment = 100000; hertz="100 Khz"; hertzPosition=4;} else if (increment == 100000){increment = 1000000; hertz="1 Mhz"; hertzPosition=6;} else{increment = 10; hertz = "10 Hz"; hertzPosition=5;}; lcd.setCursor(0,1); lcd.print(" "); lcd.setCursor(hertzPosition,1); lcd.print(hertz); delay(250); // Adjust this delay to speed up/slow down the button menu scroll speed. }; void showFreq(){ millions = int(rx/1000000); hundredthousands = ((rx/100000)%10); tenthousands = ((rx/10000)%10); thousands = ((rx/1000)%10); hundreds = ((rx/100)%10); tens = ((rx/10)%10); ones = ((rx/1)%10); lcd.setCursor(0,0); lcd.print(" "); if (millions > 9){lcd.setCursor(1,0);} else{lcd.setCursor(2,0);} lcd.print(millions); lcd.print("."); lcd.print(hundredthousands); lcd.print(tenthousands); lcd.print(thousands); lcd.print("."); lcd.print(hundreds); lcd.print(tens); lcd.print(ones); lcd.print(" Mhz "); timepassed = millis(); memstatus = 0; // Trigger memory write }; void storeMEM(){ //Write each frequency section to a EPROM slot. Yes, it's cheating but it works! EEPROM.write(0,millions); EEPROM.write(1,hundredthousands); EEPROM.write(2,tenthousands); EEPROM.write(3,thousands); EEPROM.write(4,hundreds); EEPROM.write(5,tens); EEPROM.write(6,ones); memstatus = 1; // Let program know memory has been written }; |
Avant de compiler le sketch vous devez ajouter la librairie « Rotary » à votre éditeur IDE Arduino.
Vous trouverez la librairie « Rotary » sur Github à l’adresse suivante : https://github.com/brianlow/Rotary
Téléchargez « Rotary-master.zip » et installez-le dans votre IDE.
Sketch modifié
Le programme du futur récepteur de signaux de détresse est adapté avec le sketch ci-dessous.
Au démarrage, le VFO se fixera sur 24,70825 MHz correspondant à l’écoute de la fréquence 406,032 MHz.
Par défaut, le pas de fréquences est de 1 KHz.
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 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
/* Main code by Richard Visokey AD7C - www.ad7c.com Revision 2.0 - November 6th, 2013 Adapted to Rx 400 to 410 MHz, by ON6FS Freddy Stievenart December 2020 */ // Include the library code #include <LiquidCrystal.h> #include <Rotary.h> #include <EEPROM.h> //Setup some items #define W_CLK 8 // Pin 8 - connect to AD9850 module word load clock pin (CLK) #define FQ_UD 9 // Pin 9 - connect to freq update pin (FQ) #define DATA 10 // Pin 10 - connect to serial data load pin (DATA) #define RESET 11 // Pin 11 - connect to reset pin (RST) #define pulseHigh(pin) {digitalWrite(pin, HIGH); digitalWrite(pin, LOW); } Rotary r = Rotary(2,3); // sets the pins the rotary encoder uses. Must be interrupt pins. LiquidCrystal lcd(12, 13, 7, 6, 5, 4); // I used an odd pin combination because I need pin 2 and 3 for the interrupts. int_fast32_t rx=6032000; // Starting frequency of VFO int_fast32_t rx2=1; // variable to hold the updated frequency int_fast32_t increment = 1000; // starting VFO update increment in HZ. int buttonstate = 0; String hertz = "1 KHz"; int hertzPosition = 5; byte ones,tens,hundreds,thousands,tenthousands,hundredthousands,millions ; //Placeholders String freq; // string to hold the frequency int_fast32_t timepassed = millis(); // int to hold the arduino miilis since startup int memstatus = 1; // value to notify if memory is current or old. 0=old, 1=current. int ForceFreq = 1; // Change this to 0 after you upload and run a working sketch to activate the EEPROM memory. YOU MUST PUT THIS BACK TO 0 AND UPLOAD THE SKETCH AGAIN AFTER STARTING FREQUENCY IS SET! void setup() { //Serial.begin(9600); //Serial.println("Start VFO"); pinMode(A0,INPUT); // Connect to a button that goes to GND on push digitalWrite(A0,HIGH); lcd.begin(16, 2); PCICR |= (1 << PCIE2); PCMSK2 |= (1 << PCINT18) | (1 << PCINT19); sei(); pinMode(FQ_UD, OUTPUT); pinMode(W_CLK, OUTPUT); pinMode(DATA, OUTPUT); pinMode(RESET, OUTPUT); pulseHigh(RESET); pulseHigh(W_CLK); pulseHigh(FQ_UD); // this pulse enables serial mode on the AD9850 - Datasheet page 12. lcd.setCursor(hertzPosition,1); lcd.print(hertz); // Load the stored frequency if (ForceFreq == 0) { freq = String(EEPROM.read(0))+String(EEPROM.read(1))+String(EEPROM.read(2))+String(EEPROM.read(3))+String(EEPROM.read(4))+String(EEPROM.read(5))+String(EEPROM.read(6)); rx = freq.toInt(); } } void loop() { if (rx != rx2){ showFreq(); sendFrequency(rx); rx2 = rx; } buttonstate = digitalRead(A0); if(buttonstate == LOW) { setincrement(); }; // Write the frequency to memory if not stored and 2 seconds have passed since the last frequency change. if(memstatus == 0){ if(timepassed+2000 < millis()){ storeMEM(); } } } ISR(PCINT2_vect) { unsigned char result = r.process(); if (result) { if (result == DIR_CCW){rx=rx+increment;} else {rx=rx-increment;}; if (rx >=10000000){rx=rx2;}; // UPPER VFO LIMIT if (rx < 1){rx=rx2;}; // LOWER VFO LIMIT } } // frequency calc from datasheet page 8 = <sys clock> * <frequency tuning word>/2^32 void sendFrequency(double frequency) { // Calcul de la fréquence du VFO DDS en fonction de la fréquence du récepteur // - 4000000000 : étant les 400 MHz // - frequency : Chiffres du MHz au Hz sur 7 digits // - 10700000 : Correspond au décalage de la moyen Fréquence 10,7 MHz // - 16 : Vu que la fréquence de l'oscillateur local est multimplié par 16 int32_t FreqVFO = ( 400000000 + frequency - 10700000 ) / 16; int32_t freq = FreqVFO * 4294967295/125000000; // note 125 MHz clock on 9850. You can make 'slight' tuning variations here by adjusting the clock frequency. for (int b=0; b<4; b++, freq>>=8) { tfr_byte(freq & 0xFF); } tfr_byte(0x000); // Final control byte, all 0 for 9850 chip pulseHigh(FQ_UD); // Done! Should see output } // transfers a byte, a bit at a time, LSB first to the 9850 via serial DATA line void tfr_byte(byte data) { for (int i=0; i<8; i++, data>>=1) { digitalWrite(DATA, data & 0x01); pulseHigh(W_CLK); //after each bit sent, CLK is pulsed high } } void setincrement(){ if (increment == 100){increment = 500; hertz="500 Hz"; hertzPosition=4;} else if (increment == 500){increment = 1000; hertz="1 Khz"; hertzPosition=6;} else if (increment == 1000){increment = 2500; hertz="2.5 Khz"; hertzPosition=4;} else if (increment == 2500){increment = 5000; hertz="5 Khz"; hertzPosition=6;} else if (increment == 5000){increment = 10000; hertz="10 Khz"; hertzPosition=5;} else if (increment == 10000){increment = 100000; hertz="100 Khz"; hertzPosition=4;} else if (increment == 100000){increment = 1000000; hertz="1 Mhz"; hertzPosition=6;} else{increment = 100; hertz = "100 Hz"; hertzPosition=5;}; lcd.setCursor(0,1); lcd.print(" "); lcd.setCursor(hertzPosition,1); lcd.print(hertz); delay(250); // Adjust this delay to speed up/slow down the button menu scroll speed. }; void showFreq(){ millions = int(rx/1000000); hundredthousands = ((rx/100000)%10); tenthousands = ((rx/10000)%10); thousands = ((rx/1000)%10); hundreds = ((rx/100)%10); tens = ((rx/10)%10); ones = ((rx/1)%10); lcd.setCursor(0,0); lcd.print(" "); if (millions > 9){lcd.setCursor(1,0);} else{lcd.setCursor(1,0);} lcd.print("40"); // Display "40" of 400 MHz lcd.print(millions); lcd.print("."); lcd.print(hundredthousands); lcd.print(tenthousands); lcd.print(thousands); lcd.print("."); lcd.print(hundreds); lcd.print(tens); lcd.print(ones); lcd.print(" Mhz "); timepassed = millis(); memstatus = 0; // Trigger memory write }; void storeMEM(){ //Write each frequency section to a EPROM slot. Yes, it's cheating but it works! EEPROM.write(0,millions); EEPROM.write(1,hundredthousands); EEPROM.write(2,tenthousands); EEPROM.write(3,thousands); EEPROM.write(4,hundreds); EEPROM.write(5,tens); EEPROM.write(6,ones); memstatus = 1; // Let program know memory has been written }; |
Oscillogramme
Conclusion
Ce VFO-DDS peut être aisément adapté à toutes les réalisations de récepteurs, d’émetteurs et de générateurs.
En testant l’état d’une des entrées/sorties analogiques ou digitales, vous pourrez aussi adapter la fréquence du VFO DDS, ceci en fonction de son mode réception ou émission (décalage de la moyenne fréquence).
Liens et références
- http://www.ad7c.com/projects/ad9850-dds-vfo/ : Article de Rich AD7C
- https://www.analog.com/media/en/technical-documentation/data-sheets/ad9850.pdf : Datasheet de l’AD9850
- https://www.sparkfun.com/datasheets/LCD/ADM1602K-NSW-FBS-3.3v.pdf : Datasheet écran LCD 1602
- https://github.com/brianlow/Rotary : Bibliothèque / Librairie de l’encoder rotatif