Générateur HF 1Hz-40MHz sinus pur
digital à DDS AD9850 (résolution 1Hz)
piloté par un ATmega8

Je possédais depuis plusieurs années d'un "générateur de fonction" 2MHz analogique du commerce (DF1641A) certes assez joli mais qui ne me donnait pas du tout satisfaction. Fréquence pas très stable,potentiomètres de réglages qui finissent par "cracher" (poussières, usure de la piste de carbone...) Alors voilà, j'ai conçu et réalisé ce générateur numérique synthétisé ultra stable et précis au Hz près sur TOUTE la gamme de 1Hz à 49 000 000HZ (49MHz).

1 Le circuit DDS AD9850

On peut se procurer le AD9850 seul en boîtier SMD de folie (j'en ai soudé un, je déconseille l'aventure!), ou bien comme ici monté sur un module au pas de 2.54mm ce qui facilite grandement la réalisation du circuit imprime.

Le AD9850 est un DDS (direct digital synthesis) qui synthétise directement un signal sinusoïdal à une fréquence précise. Dans le cas présent la fréquence peut être choisie entre 1Hz et plus de 40MHz avec une résolution de 0.0291 Hz lorsqu'il est piloté par une horloge (à quartz) de 125 MHz.
En interne le DDS génère une sinusoïde numérique qu'il convertit en un signal analogique avec un convertisseur N/A. La fréquence du signal est déterminée par un mot de 32 bits. La résolution de 0.0291 est égale à la fréquence du quartz divisée par 2 puissance 32. (125E6 / 4294967296 = 0.0291)

2 Le principe mis en oeuvre au sein du DDS (je vous traduis une toute petite partie du datasheet en anglais)

La circuiterie interne du DDS est un diviser digital de fréquence dont la résolution incrémentale (la finesse du pas) est déterminée par la fréquence de l'horloge de référence (ici 125MHz) divisée la 2^N nombre de bits du mot de commande de fréquence (un grand (32 bits) nombre binaire qui détermine la fréquence de sortie).
L'accumulateur (la mémoire) de phase est un compteur à module variable qui incrémente (ajoute) le nombre enregistré à chaque fois qu'il reçoit une impulsion d'horloge.
Lorsque le compteur déborde, il reboucle sur lui-même de sorte que le signal est généré continuellement.
Le mot de commande de fréquence (il sera fourni par l'ATmega en fonction de la fréquence désirée) constitue la valeur du module du compteur, ce qui en fin de compte détermine la taille de l'incrément (delta phase) qui est ajouté dans l'accumulateur de phase lors de l'impulsion d'horloge suivante. Plus l'incrément est large et plus vite l'accumulateur se remplit et déborde ce qui produit une fréquence plus élevée. Le signal numérique de sortie est issu d'un calcul mathématique d'une fonction cosinus avec comme variable la valeur de la phase.

3 -

La fréquence de sortie est donnée par la formule ci-contre:

4 Le schéma du générateur HF

5 -

Le mot de commande peut être chargé en mode parallèle ou en mode série. J'ai choisi le mode série pour économiser des ports de l'ATmega8.

Le fréquence désirée est saisie au moyen de deux boutons rotatifs pas à pas (encodeurs code gray) qui procurent un grand confort d'utilisation.

6 Le Firmware en langage C:

CODE SOURCE en langage C
  1. /*==============================================================================
  2. par Silicium628
  3. derniere mise à jour 16 juin 2015
  4. ================================================================================
  5. ATmega8
  6. Pilotage d'un circuit DDS AD9850 générateur de fréquence 0..40MHz sinus avec un quartz (pour le AD9850) de 125MHz
  7. -Affichage LCD 2 x 20 caractères ("splitté entre deux ports de l'ATmega)
  8. ================================================================================
  9. */
  10. // #include <math.h>
  11.  
  12.  
  13. #define F_CPU 16000000
  14.  
  15. #include <avr/io.h>
  16. #include <avr/interrupt.h>
  17. #include <util/delay.h>
  18. #include "dm_lcd.c"
  19.  
  20.  
  21. #define pin_WCL 0b00010000 // sur port D
  22. #define pin_FQU 0b10000000 // sur port D
  23. #define pin_RESET 0b01000000 // sur port D
  24. #define pin_DATA 0b00100000 // sur port D
  25.  
  26.  
  27.  
  28.  
  29. char * version = "6.1";
  30.  
  31.  
  32. uint32_t FTW; // le mot de 32 bits à transmettre au AD9951 en mode série sur le pin SDIO
  33. uint32_t f_out; // fréquence du signal de sortie sinusoïdal
  34.  
  35. uint8_t pos; // position du multiplicateur
  36. uint8_t pos_curseur; // position du curseur (pour affichage)
  37.  
  38. uint32_t pas;
  39. uint8_t etat;
  40. uint16_t compteur1;
  41.  
  42. uint32_t Frq_W32; // valeur binaire destine a l'AD9850, vaut environ 68,72 x frequence en Hz.
  43. uint8_t phase;
  44. uint8_t nb_cycles = 0;
  45.  
  46.  
  47.  
  48. void init_ports (void) // ports perso
  49. // 0 = entree, 1=sortie ; les 1 sur les pins en entrees activent les R de Pull Up (tirage à VCC)
  50. {
  51. PORTB = 0b00001000; // PB3 = MOSI -> entrée ICPS
  52. DDRB |= 0b11111111;
  53.  
  54. DDRC = 0b11111111;
  55.  
  56. DDRD = 0b11110000; // PD0..3 entrées codeurs rotatifs
  57. PORTD = 0b00001111; // valide R de Pull Up sur les entrées
  58. }
  59.  
  60.  
  61.  
  62.  
  63. void InitINTs (void)
  64. {
  65. GICR |= 0b11000000; // gere les INTs - bit9 ->INT0 request - voir page 67 du pdf
  66. MCUCR |= 0b00001010; // The falling edge of INT0 generates an interrupt request. The falling edge of INT1 generates an interrupt request. p:67 du pdf
  67. }
  68.  
  69.  
  70. void init_variables(void)
  71. {
  72. f_out = 1000;
  73. pas = 100;
  74. pos = 3;
  75.  
  76. }
  77.  
  78.  
  79.  
  80. void lcd_gotoxy_clrEOL (int x, int y)
  81. // place le curseur en x,y et efface jusqu'a la fin de la ligne
  82. {
  83. lcd_gotoxy(x, y);
  84. uint8_t i;
  85. for (i=x; i<20; i++)
  86. { lcd_puts(" "); }
  87. lcd_gotoxy(x, y);
  88. }
  89.  
  90.  
  91.  
  92. void lcd_aff_nb (uint32_t valeur, uint8_t nb_chiffres, uint8_t nb_decimales, uint8_t affi_zeros)
  93. {
  94. //affiche un nombre en representation decimale
  95. // affi_zeros = 1 affiche les zéros non significatifs à gauche.
  96. unsigned char r ;
  97. char tbl[7];
  98. char digit;
  99. uint8_t i;
  100.  
  101. for (i=1; i<=nb_chiffres; i++)
  102. {
  103. r=48 + valeur % 10; // modulo (reste de la division)
  104. valeur /= 10; // quotient
  105. tbl[i]=r;
  106. }
  107. uint8_t debut = 1-affi_zeros; // pour ne pas afficher des zeros à gauche du 1er chiffre
  108. for (i=1; i<=nb_chiffres; i++)
  109. {
  110. if (i== (nb_chiffres - nb_decimales +1) ) { lcd_puts("."); }
  111. digit = tbl[nb_chiffres +1 -i];
  112. if (digit != '0') {debut = 0;}
  113. if ((digit != '0') || (debut == 0)) {lcd_putc(digit);}
  114. }
  115. }
  116.  
  117.  
  118. void lcd_aff_nb_form3 (uint32_t valeur, uint8_t nb_chiffres)
  119. {
  120. //affiche un nombre en representation decimale en separant les groupes de 3 chiffres
  121. unsigned char r ;
  122. char tbl[8];
  123. uint8_t i;
  124.  
  125. for (i=1; i<=nb_chiffres; i++)
  126. {
  127. r=48 + valeur % 10; // modulo (reste de la division)
  128. valeur /= 10; // quotient
  129. tbl[i]=r;
  130. }
  131. for (i=1; i<= nb_chiffres; i++)
  132. {
  133. if (((i % 3) == 0 ) && (i>1)) { lcd_puts("."); } // separe les groupes de 3 chiffres
  134. lcd_putc(tbl[nb_chiffres +1 -i]);
  135. }
  136. }
  137.  
  138.  
  139.  
  140.  
  141.  
  142. void lcd_aff_bin (unsigned long int valeur, int nb_digits)
  143. {
  144. //affiche un nombre en representation binaire
  145. // 16 bits max
  146. unsigned char r ;
  147. char tbl[17];
  148. uint8_t i;
  149.  
  150. for (i=1; i<=nb_digits; i++)
  151. {
  152. r= 48 + valeur % 2; // modulo (reste de la division)
  153. valeur /= 2; // quotient
  154. tbl[i]=r;
  155. };
  156.  
  157. for (i=1; i<=nb_digits; i++)
  158. {
  159. lcd_putc(tbl[nb_digits +1 -i]);
  160. }
  161. }
  162.  
  163.  
  164.  
  165. void affiche_pas()
  166. {
  167. lcd_gotoxy_clrEOL(0, 3);
  168. lcd_puts("PAS = ");
  169.  
  170. switch (pas)
  171. {
  172. case 1 : lcd_puts("1Hz");
  173. break;
  174.  
  175. case 10 : lcd_puts("10Hz");
  176. break;
  177.  
  178. case 100 : lcd_puts("100Hz");
  179. break;
  180.  
  181. case 1000 : lcd_puts("1kHz");
  182. break;
  183.  
  184. case 10000 : lcd_puts("10kHz");
  185. break;
  186.  
  187. case 100000 : lcd_puts("100kHz");
  188. break;
  189.  
  190. case 1000000 : lcd_puts("1MHz");
  191. break;
  192.  
  193. case 10000000 : lcd_puts("10MHz");
  194. break;
  195.  
  196. }
  197. }
  198.  
  199.  
  200. void affiche_POS()
  201. {
  202. lcd_gotoxy(0, 2);
  203. lcd_puts("POS=");
  204. lcd_aff_nb(pos, 2,0,0);
  205. }
  206.  
  207.  
  208. void affiche_frequence(uint8_t nb_chiffres)
  209. {
  210.  
  211. lcd_gotoxy(0, 1);
  212. lcd_puts("F=");
  213. lcd_aff_nb_form3 (f_out, nb_chiffres);
  214. lcd_puts(" Hz");
  215.  
  216. pos_curseur = nb_chiffres+4-pos;
  217. if (pos > 3) {pos_curseur = nb_chiffres+3-pos;};
  218. if (pos > 6) {pos_curseur = nb_chiffres+2-pos;};
  219. lcd_gotoxy(pos_curseur, 1);
  220. }
  221.  
  222.  
  223.  
  224. void reset_DDS(void)
  225. {
  226. _delay_ms(1);
  227. PORTD |= pin_RESET;
  228. _delay_ms(10);
  229. PORTD &= ~pin_RESET; // l'operateur "~" donne le complement (inverse des bits). ne pas confondre avec l'opérateur "!"
  230. _delay_ms(10);
  231. }
  232.  
  233.  
  234.  
  235. void impulse_clk_W(void) // sur pin W_CL
  236. {
  237. _delay_us(5);
  238. PORTD |= pin_WCL;
  239. _delay_us(5);
  240. PORTD &= ~pin_WCL; // l'operateur "~" donne le complement (inverse des bits). ne pas confondre avec l'opérateur "!"
  241. _delay_us(5);
  242. }
  243.  
  244.  
  245. void impulse_FQ_U(void) // sur pin FQ_U
  246. {
  247. _delay_us(5);
  248. PORTD |= pin_FQU;
  249. _delay_us(5);
  250. PORTD &= ~pin_FQU; // l'operateur "~" donne le complement (inverse des bits). ne pas confondre avec l'opérateur "!"
  251. _delay_us(5);
  252. }
  253.  
  254.  
  255.  
  256. void out_DDS(uint32_t freq_i) // 40 bits vers AD9850; voir son datasheet
  257. {
  258. uint8_t n;
  259.  
  260. //envoi de la frequence
  261. uint32_t masque;
  262.  
  263. masque = 1;
  264. for (n=0; n<= 31; n++) // on sort le LSB (W0) en premier
  265. {
  266. masque += masque; // revient à x2 le masque le '1' se deplacant de droite a gauche, 32 bits en tout
  267. if ( (freq_i & masque) != 0) {PORTD |= pin_DATA;} else { PORTD &= ~pin_DATA; }
  268.  
  269. impulse_clk_W();
  270. }
  271.  
  272. PORTD &= ~pin_DATA; // (W32 toujours = 0)
  273. impulse_clk_W();
  274.  
  275. PORTD &= ~pin_DATA; // (W33 toujours = 0)
  276. impulse_clk_W();
  277.  
  278. PORTD &= ~pin_DATA; // (W34 = Power-Down = 0)
  279. impulse_clk_W();
  280.  
  281. // envoi de la phase (5 bits)
  282.  
  283. for (n=0; n<=4; n++) // on sort le LSB (W35) en premier et le MSB en dernier.
  284. {
  285. masque = (1 << n);
  286. if (phase & masque) {PORTD |= pin_DATA;} else { PORTD &= ~pin_DATA; }
  287.  
  288. impulse_clk_W();
  289. }
  290. // envoi impulsion FQ_UD
  291. impulse_FQ_U();
  292. }
  293.  
  294.  
  295.  
  296. /***********************************************************************
  297. f_out = FTW x CLKIN / 2³² nous dit le datasheet
  298. en en déduit :
  299.  
  300. FTW = f_out * 2³² / CLKIN
  301. ici CLKIN = 125MHz / 2 = 62.5 MHz
  302. 125MHZ c'est la fréquence du quartz
  303. cette fréquence est /2 en interne
  304.  
  305. FTW = f_out * (2³² / 62.5E6 ) = 68.71947674 x f_out
  306.  
  307.  
  308. ************************************************************************/
  309.  
  310.  
  311. void calcul_FTW()
  312. {
  313. float y;
  314. y = 68.71947674 * f_out;
  315. FTW = (uint32_t) y;
  316.  
  317. }
  318.  
  319.  
  320.  
  321.  
  322.  
  323. /***********************************************************************
  324. // traitement du codeur_rot()
  325.  
  326. // codeur incrémental code Gray avec en plus fonction bouton poussoir
  327. // le codeur rotatif utilisé est toujours à 11 en position stable
  328. // dans le sens CW il fait 11->01->00->10->11
  329. // et par conséquent dans le sens CCW il fait 11->10->00->01->11
  330.  
  331. // 11
  332. // 01
  333. // 00
  334. // 10
  335. // 11
  336.  
  337. // il suffit de connecter un bit sur le pin INT0 (PD2) et de déclencher l'INT sur front descendant (passage de 0 à 1)
  338. // cette INT lira alors l'état de l'autre bit (connecté à PD0 par exemple). Cet état diffère suivant le sens de rotation
  339.  
  340. ************************************************************************/
  341.  
  342.  
  343. ISR(BADISR_vect)
  344. {
  345. // évite de planter si une int est enable et pas de procedure associée écrite (ce qui fait reseter l'ATmega)
  346. }
  347.  
  348.  
  349. ISR (INT0_vect)
  350. {
  351. uint8_t n;
  352. //interruption sur front descendant sur l'entree Int0
  353. // declenchee par la rotation du codeur_ROT (2)
  354.  
  355. etat = PIND & 0b00000001;
  356. if (etat == 0)
  357. {
  358. if (pos < 8) {pos++ ;}
  359. }
  360. else
  361. {
  362. if (pos > 1) {pos--;}
  363. }
  364.  
  365. pas=1;
  366. for (n=1; n<pos; n++)
  367. {
  368. pas *=10;
  369. }
  370.  
  371. calcul_FTW();
  372. out_DDS(FTW);
  373. }
  374.  
  375.  
  376. ISR (INT1_vect)
  377. {
  378. //interruption sur front descendant sur l'entree Int0
  379. // declenchee par la rotation du codeur_ROT (1)
  380.  
  381. etat = PIND & 0b00000010;
  382. if (etat == 0)
  383. {
  384. if (f_out >= pas) {f_out -= pas;}
  385. }
  386. else
  387. {
  388. if ( (f_out+pas) <= 50000000) {f_out += pas;}
  389. if ( (f_out+pas) > 50000000) {f_out = 50000000;}
  390. }
  391. calcul_FTW();
  392. out_DDS(FTW);
  393. }
  394.  
  395.  
  396.  
  397.  
  398.  
  399.  
  400. int main (void)
  401. {
  402. init_variables();
  403. init_ports();
  404. InitINTs();
  405.  
  406. reset_DDS();
  407.  
  408. lcd_init(LCD_DISP_ON_CURSOR);
  409. lcd_clrscr();
  410. lcd_home();
  411. lcd_puts("Gene HF AD9850 v");
  412. lcd_puts(version);
  413.  
  414. _delay_ms(2000);
  415.  
  416.  
  417. lcd_clrscr();
  418.  
  419. lcd_gotoxy(13,0);
  420. lcd_puts("AD9850");
  421.  
  422. sei(); // enable interruptions
  423.  
  424. while(1)
  425. {
  426. compteur1++;
  427. if (compteur1 >= 10000)
  428. {
  429. affiche_pas();
  430. // affiche_POS(); // pour test
  431. affiche_frequence(8);
  432.  
  433. _delay_ms(10);
  434. compteur1=0;
  435. }
  436. _delay_us(20);
  437. }
  438.  
  439.  
  440. }
  441.  
  442.  

7 La carte logique (ATmega8)

C'est en fait la même que celle conçue pour le géné HF 160 MHz décrit sur ce site (avec un firmware différent bien entendu). On distingue le connecteur de programmation in-situ.

8 L'ensemble des cartes inter-connectées:

9 Programmation du firmware:

Le programmateur (USBASP) est décrit sur ce site (ici)
Je l'utilise conjointement avec AVRdude et le soft AVR8-Burn-O-Mat sous Linux Mint13 (Forks de Ubuntu 12.04)

TOUT ce qui est décrit ici est réalisé avec des logiciels libres sous Linux.

  • Programmation en C -> gcc-avr avec comme éditeur Geany
  • Circuits imprimés -> suite Kicad
  • ce site -> Bluefish
  • upload du site sur le serveur -> Filezilla

10 Le signal de sortie vu à l'oscilloscope, pour quelques fréquences...

1000Hz

10kHz

11 -

100kHz

1MHz

12 -

10MHz

40MHz

13 -

Au dessus de 10MHz l'amplitude diminue mais la sinusoïde est toujours aussi parfaite. (L'oscillo est un Tektronix 2245A - 100MHz)

L'analyseur de spectre ne laisse rien entrevoir qui dépasse de l'herbe...

Il faut préciser que le petit module sur lequel est intégré l'AD9850 comprend un filtre comme on peut le voir sur le petit schéma de ce module fourni par le vendeur de Shenzhen (Chine). Je vous recopie ce schéma ci-contre. Sans ce filtre l'amplitude reste constante jusqu'à 40MHz mais le signal devient entaché de fortes distorsions à mesure que la fréquence augmente.

J'ai pour ma part ajouté un condensateur céramique de 1nF entre les pins 3 (FQ_U) et 6 (GND) du module, ce qui a supprimé une modulation de phase parasite au delà de 10MHz... Apparemment le signal parasite ne provenait pas de l'ATmega, et FQ_U est une entrée... une entrée de data au repos en plus... Mystère donc... propre à la HF. Sans doute le routage du PCB du module fait que la piste FQ_U une fois découplée fait écran à de la HF?

Quant à la fréquence, j'ai testé au fréquencemètre numérique (0- 2,7GHz) et je confirme que la résolution et la précision de 1 Hz est garantie sur toute la gamme de 1Hz à 40 000 000Hz et qu'il est tout à fait possible de générer n'importe quelle fréquence, comme par exemple 41 642 598Hz. (Je l'ai bridé logiciellement à 50MHz (49,999))

14 NOUVELLES FONCTIONS

11 août 2012: Comme on peut le voir sur la photo ci-contre, de nouvelles fonctions apparaissent...

15 Documents techniques

Voici les autres fichiers utiles à cette réalisation:

Aperçu du contenu de l'archive

16 WOBULATEUR

19 septembre 2012: Cette nouvelle réalisation fait l'objet d'un article:

17 -



47217