/* -------------------------------------------------------------------------
RADIO FM - pour ESP32
par Silicium628
--------------------------------------------------------
CONCERNANT L'AFFICHAGE TFT :
Pensez à configurer le fichier User_Setup.h de la bibliothèque ~/Arduino/libraries/TFT_eSPI/ au préalable
voici les paramètres OK pour cette réalisation :
#define ST7789_DRIVER // Full configuration option, define additional parameters below for this display
#define TFT_RGB_ORDER TFT_BGR // Colour order Blue-Green-Red
#define TFT_WIDTH 240 // ST7789 240 x 240 and 240 x 320
#define TFT_HEIGHT 240 // ST7789 240 x 240
#define TFT_MISO 19
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS 15 // Chip select control pin
#define TFT_DC 2 // Data Command control pin
#define TFT_RST 4 // Reset pin (could connect to RST pin)
explications ici : https://www.youtube.com/watch?v=HoZhgNcJjNA
Remerciements à :
- big12boy 2017 - Licence: GNU GPL
--------------------------------------------------------
CONCERNANT LA TRANSFORMEE DE FOURIER :
FFT par Silicium628
logiciel libre, OPEN SOURCE
pour l'explication des calculs voir mes pages :
http://www.silicium628.fr/article_i.php?id=123 -> La transformée de Fourier Etude analytique
http://www.silicium628.fr/article_i.php?id=126 -> Transformée de Fourier rapide
http://www.silicium628.fr/article_i.php?id=31 -> Transformée de Fourier sur ATmega32 et Arduino Mega2560
------------------------------------------------------------------------- */
String version="2.0";
#include <SPI.h>
#include <TEA5767.h>
#include <TFT_eSPI.h> // Hardware-specific library
#include "complexe628.h"
#include "Free_Fonts.h"
TEA5767 radio = TEA5767();
TFT_eSPI tft = TFT_eSPI(); // Configurer le fichier User_Setup.h de la bibliothèque TFT_eSPI au préalable
typedef int16_t complexe_f[2]; // entier signé pour calculs (rapides) en virgule fixe
Complexe ech[480]; // (480 = nb_ech echantillons)
Complexe tab_X[480]; // nb_ech valeurs traitées
Complexe tab_W[240]; // nb_ech/2
uint nb_etapes=8;
uint nb_ech = pow (2,nb_etapes); // nombre d'échantillons = 2 puissance(nb_etapes)
struct boite
{
uint16_t x;
uint16_t y;
uint16_t w;
uint16_t h;
};
boite boite_FFT;
#define TFT_GREY 0x5AEB
float ltx = 0; // Saved x coord of bottom of aiguille
uint16_t osx = 120, osy = 120; // Saved x & y coords
const int bouton1 = 12; // GPIO12
const int bouton2 = 14; // GPIO14
const int bouton3 = 27; // GPIO27
//const int led1 = 13; // GPIO13
bool bouton1_etat;
bool bouton2_etat;
bool bouton3_etat;
bool memo_bouton1_etat;
bool memo_bouton2_etat;
bool memo_bouton3_etat;
uint8_t mode_boutons;
uint8_t mode_affi;
uint8_t memo_mode_affi;
uint8_t num_station;
uint8_t nb_stations;
uint16_t frq_Int;
float frq_out; // ex: 89.4 (MHz)
uint16_t compteur1=0;
struct station
{
uint16_t frq;
char nom[20+1];
};
struct station liste_stations[20]; // 0..19
void init_stations()
{
nb_stations = 12;
strcpy(liste_stations[0].nom , "France Inter");
liste_stations[0].frq = 894;
strcpy(liste_stations[1].nom , "France Musique");
liste_stations[1].frq = 929;
strcpy(liste_stations[2].nom , "RFM");
liste_stations[2].frq = 956;
strcpy(liste_stations[3].nom , "France Culture");
liste_stations[3].frq = 978;
strcpy(liste_stations[4].nom , "Radio France Herault");
liste_stations[4].frq = 1011;
strcpy(liste_stations[5].nom , "Fun Radio");
liste_stations[5].frq = 918;
strcpy(liste_stations[6].nom , "Radio Clapas");
liste_stations[6].frq = 935;
strcpy(liste_stations[7].nom , "Cherie FM");
liste_stations[7].frq = 969;
strcpy(liste_stations[8].nom , "FIP");
liste_stations[8].frq = 997;
strcpy(liste_stations[9].nom , "Nostalgie");
liste_stations[9].frq = 1039;
strcpy(liste_stations[10].nom , "France Info");
liste_stations[10].frq = 1051;
strcpy(liste_stations[11].nom , "Radio Classique");
liste_stations[11].frq = 1073;
}
void affiche_Signal_Level()
{
short level = radio.getSignalLevel(); //Get Signal Level
plotAiguille(100*level/15);
}
// ================== FONCTIONS FFT ==============================
void effacer_trace()
{
tft.fillRect(boite_FFT.x+1, boite_FFT.y+1, boite_FFT.w-2, boite_FFT.h-2, TFT_BLACK);
}
void RAZ_tableau_echantillons()
{
uint n;
for(n=0; n < nb_ech; n++)
{
ech[n].a = 0; // partie reelle
ech[n].b = 0; // partie imaginaire
}
}
uint16_t bit_reversal(uint16_t num, uint8_t nb_bits)
{
uint r = 0, i, s;
if ( num > (1<< nb_bits)) { return 0; }
for (i=0; i<nb_bits; i++)
{
s = (num & (1 << i));
if(s) { r |= (1 << ((nb_bits - 1) - i)); }
}
return r;
}
void bit_reverse_tableau_X()
{
// recopie les échantillons en les classant dans l'ordre 'bit reverse'
uint n,r;
for(n=0; n < nb_ech; n++) // nb d'échantillons
{
r=bit_reversal(n,nb_etapes);
tab_X[n] = ech[r];
}
}
void acquisition()
{
// source = signal analogique en entrée du CAN.
// destination -> tableau des échantillons complexes
float x;
for(int n=0; n<nb_ech; n++)
{
x= analogRead(25)/30.0; // GPIO 25; le /30.0 détermine la sensibilité (atténuateur sur l'amplitude)
ech[n].a = x; // partie reelle
ech[n].b = 0; // partie imaginaire
}
}
/**
pour l'explication des calculs suivants voir mes pages :
http://www.silicium628.fr/article_i.php?id=123 -> La transformée de Fourier Etude analytique
http://www.silicium628.fr/article_i.php?id=126 -> Transformée de Fourier rapide
http://www.silicium628.fr/article_i.php?id=31 -> Transformée de Fourier sur ATmega32 et Arduino Mega2560
**/
void calcul_tableau_W()
{
// calcul et memorisation dans un tableau des twiddle factors
uint n;
float x;
for(n=0; n<(nb_ech/2-1); n++)
{
x=2.0*M_PI * n / nb_ech;
tab_W[n].a = cos(x); // partie reelle
tab_W[n].b = -sin(x); // partie imaginaire
}
}
void calcul_fourier()
{
Complexe produit; // voir la classe "Complexe" : complexe.h et complexe.ccp
uint etape, e1, e2, li, w, ci;
uint li_max=nb_ech;
e2=1;
for (etape=1; etape<=nb_etapes; etape++)
{
e1=e2; //(e1 evite d'effectuer des divisions e2/2 plus bas)
e2=e2+e2;
for (li=0; li<li_max; li+=1)
{
ci=li & (e2-1); // ET bit à bit
if (ci>(e1-1))
{
w=li_max/e2*(li & (e1 -1)); // ET bit à bit calcul du numéro du facteur de tripatouillages W
produit = tab_W[w] * tab_X[li]; // le twiddle factor est lu en memoire; le produit est une mutiplication de nb complexes. Voir "complexe.cpp"
tab_X[li]=tab_X[li-e1]-produit; // concerne la ligne basse du croisillon; soustraction complexe; Voir "complexe.cpp"
tab_X[li-e1]=tab_X[li-e1] + produit; // concerne la ligne haute du croisillon; addition complexe; Voir "complexe.cpp"
}
}
}
}
// couleurs RGB24 :
//couleur1= 255; // bleu
//couleur1= 255<<1; // bleu-clair
//couleur1= 255<<2; // bleu très clair
//couleur1= 255<<3; // blanc bleuté
//couleur1= 255<<4; // cyan
//couleur1= 255<<5; // vert-vert-jaune
//couleur1= 255<<6; // vert-jaune
//couleur1= 255<<7; // vert-jaune
//couleur1= 255<<8; // jaune
//couleur1= 255<<9; // jaune orangé
//couleur1= 255<<10; // orange
//couleur1= 255<<11; // rouge
void affi_FFT()
{
float TF;
uint8_t offset_y = boite_FFT.y + boite_FFT.h -1;
float echelle_x = 8.0;
int n;
float x, y, memo_x, memo_y;
float z= 0.5;
uint32_t couleur1;
x=0;
y=offset_y;
uint16_t n_max = nb_ech/2;
for(n=1; n< n_max; n++)
{
memo_x = x;
memo_y = y;
x = boite_FFT.x + echelle_x * n;
if (x<(boite_FFT.w-10))
{
TF = z * sqrt( tab_X[n].a * tab_X[n].a + tab_X[n].b * tab_X[n].b ); // racine de A²+B²
if(TF>100) {TF=100;}
y = offset_y - TF;
//tft.drawLine(memo_x ,memo_y, x, y, TFT_YELLOW); // courbe continue
//tft.drawLine(x+1 ,offset_y, x+1, y, TFT_YELLOW);// barres verticales
//tft.drawFastVLine(x, y, TF, TFT_YELLOW); // barres verticales, tarcé rapide
int decalage = 12 - (int16_t)x/21.0;
if (decalage < 0) {decalage = 0;}
couleur1= (255 << decalage);
tft.fillRect(x+1, y, 6, TF, couleur1); // barres verticales larges
}
}
}
// ===============================================================
void setup(void)
{
pinMode(bouton1, INPUT_PULLUP);
pinMode(bouton2, INPUT_PULLUP);
pinMode(bouton3, INPUT_PULLUP);
//pinMode(led1, OUTPUT);
tft.init();
tft.setRotation(3); // 0..3 à vous de voir, suivant disposition de l'afficheur
// Serial.begin(57600);
tft.fillScreen(TFT_BLACK);
//Setup I2C
Wire.begin();
dessine_VuMetre();
//dessine_gamme_FM();
mode_affi=0;
mode_boutons=0;
init_stations();
num_station = 0;
if (mode_boutons==0) {frq_Int = liste_stations[num_station].frq;}
if (mode_boutons==1) {frq_Int=880;}
frq_out = (float)frq_Int / 10;
MAJ_frq();
affiche_numero_station(num_station);
delay(20);
affiche_Signal_Level();
bouton1_etat = digitalRead(bouton1);
memo_bouton1_etat = bouton1_etat;
bouton2_etat = digitalRead(bouton2);
memo_bouton2_etat = bouton2_etat;
bouton3_etat = digitalRead(bouton3);
memo_bouton3_etat = bouton3_etat;
calcul_tableau_W(); // (twiddle factors)
boite_FFT.x = 0;
boite_FFT.y = 0;
boite_FFT.w = 239;
boite_FFT.h = 120;
tft.drawRect(boite_FFT.x, boite_FFT.y, boite_FFT.w, boite_FFT.h, TFT_CYAN);
//updateTime = millis(); // Next update time
}
void affiche_numero_station(uint8_t n_i)
{
if (mode_boutons==0)
{
tft.fillRect(0, 138, 25, 25, TFT_BLUE);
String s1= (String) (n_i + 1);
tft.setTextColor(TFT_WHITE);
tft.drawString(s1, 5, 145, 2);
}
if (mode_boutons==1)
{
tft.fillRect(0, 138, 25, 25, TFT_GREY);
}
}
void efface_nom_station()
{
tft.fillRect(0, 180, 239, 30, TFT_BLACK);
}
void affiche_nom_station(uint8_t n)
{
tft.setTextColor(TFT_YELLOW);
String s3= liste_stations[n].nom;
tft.drawString(s3, 0, 180, 4);
}
void affiche_frq_out()
{
tft.fillRect(25, 135, 239, 30, TFT_BLACK); //TFT_BLACK
String s2= (String) frq_out;
s2+=" MHz";
if (mode_boutons==0) {tft.setTextColor(TFT_CYAN);}
if (mode_boutons==1) {tft.setTextColor(TFT_GREEN);}
tft.setFreeFont(FF19);
tft.drawString(s2, 35, 135, GFXFF);
tft.fillRect(0, 180, 239, 30, TFT_BLACK);
tft.setTextColor(TFT_YELLOW);
}
void MAJ_frq()
{
radio.setFrequency(frq_out);
affiche_frq_out();
affiche_nom_station(num_station);
dessine_gamme_FM();
delay(20);
affiche_Signal_Level();
}
void inc_frq_int()
{
if ( frq_Int < 1080) {frq_Int += 1;}
if ( frq_Int > 1080) {frq_Int = 1080; }
}
void dec_frq_int()
{
if ( frq_Int > 880) {frq_Int -= 1;}
}
uint8_t detect_num_station(uint16_t frq_Int_i)
{
for (int i=0; i<nb_stations; i++)
{
if (frq_Int_i == liste_stations[i].frq) return i;
}
return 255;
}
void loop()
{
memo_mode_affi = mode_affi;
uint16_t compte=0;
bouton1_etat = digitalRead(bouton1);
bouton2_etat = digitalRead(bouton2);
if (mode_boutons == 0)
{
if (bouton1_etat != memo_bouton1_etat)
{
memo_bouton1_etat = bouton1_etat;
if (bouton1_etat == 0)
{
mode_affi=0;
if ( num_station < nb_stations) {num_station += 1;}
if ( num_station > (nb_stations-1)) {num_station = nb_stations-1; }
frq_Int = liste_stations[num_station].frq;
frq_out = (float)frq_Int / 10;
}
affiche_numero_station(num_station);
MAJ_frq();
}
if (bouton2_etat != memo_bouton2_etat)
{
memo_bouton2_etat = bouton2_etat;
if (bouton2_etat == 0)
{
mode_affi=0;
if ( num_station > 0) {num_station -= 1;}
frq_Int = liste_stations[num_station].frq;
frq_out = (float)frq_Int / 10;
}
affiche_numero_station(num_station);
MAJ_frq();
}
}
if (mode_boutons == 1)
{
bouton1_etat = digitalRead(bouton1);
bouton2_etat = digitalRead(bouton2);
if (bouton1_etat != memo_bouton1_etat)
{
memo_bouton1_etat = bouton1_etat;
if (bouton1_etat == 0) // suite à un appui bref, on avance de 1 pas
{
mode_affi=0;
inc_frq_int();
frq_out = (float)frq_Int / 10;
radio.setFrequency(frq_out);
affiche_frq_out();
dessine_gamme_FM();
affiche_Signal_Level();
}
//MAJ_frq();
bouton1_etat = digitalRead(bouton1);
while(bouton1_etat == 0) // pui on détecte un éventuel appui long (500ms)
{
bouton1_etat = digitalRead(bouton1);
compte++;
if (compte>=50) // l'appui long est advenu
{
compte=0;
bouton1_etat = digitalRead(bouton1);
while(bouton1_etat == 0) // tant que le bouton reste appuyé, on incrémente rapidement les pas
{
bouton1_etat = digitalRead(bouton1);
inc_frq_int();
frq_out = (float)frq_Int / 10;
radio.setFrequency(frq_out);
affiche_frq_out();
dessine_gamme_FM();
affiche_Signal_Level();
delay(50);
}
}
delay(10);
}
efface_nom_station();
uint8_t num1= detect_num_station(frq_Int);
if (num1 !=255) {affiche_nom_station(num1);}
}
if (bouton2_etat != memo_bouton2_etat)
{
memo_bouton2_etat = bouton2_etat;
if (bouton2_etat == 0) // suite à un appui bref, on avance de 1 pas
{
mode_affi=0;
dec_frq_int();
frq_out = (float)frq_Int / 10;
radio.setFrequency(frq_out);
affiche_frq_out();
dessine_gamme_FM();
affiche_Signal_Level();
}
//MAJ_frq();
bouton2_etat = digitalRead(bouton2);
while(bouton2_etat == 0) // puis on détecte un éventuel appui long (500ms)
{
bouton2_etat = digitalRead(bouton2);
compte++;
if (compte>=50) // l'appui long est advenu
{
compte=0;
bouton2_etat = digitalRead(bouton2);
while(bouton2_etat == 0) // tant que le bouton reste appuyé, on incrémente rapidement les pas
{
bouton2_etat = digitalRead(bouton2);
dec_frq_int();
frq_out = (float)frq_Int / 10;
radio.setFrequency(frq_out);
affiche_frq_out();
dessine_gamme_FM();
affiche_Signal_Level();
delay(50);
}
}
delay(10);
}
efface_nom_station();
uint8_t num1= detect_num_station(frq_Int);
if (num1 !=255) {affiche_nom_station(num1);}
}
}
bouton3_etat = digitalRead(bouton3);
if (bouton3_etat != memo_bouton3_etat)
{
memo_bouton3_etat = bouton3_etat;
if (bouton3_etat == 0)
{
mode_boutons=1-mode_boutons;
affiche_frq_out();
delay(20);
}
if(mode_boutons==0){tft.fillRect(0, 138, 25, 25, TFT_BLUE);}
if(mode_boutons==1){tft.fillRect(0, 138, 25, 25, TFT_GREY);}
}
if(memo_mode_affi != mode_affi)
{
if(mode_affi==0)
{
tft.drawRect(boite_FFT.x, boite_FFT.y, boite_FFT.w, boite_FFT.h, TFT_CYAN);
dessine_VuMetre();
}
}
if (mode_affi==0) {compteur1++;}
if (compteur1>=100)
{
compteur1=0;
mode_affi=1;
tft.fillRect(0, 0, 239, 126, TFT_GREY);
}
if(mode_affi==1)
{
// ----------- FFT ---------------
effacer_trace();
RAZ_tableau_echantillons();
acquisition();
//tracer_signal();
bit_reverse_tableau_X();
calcul_fourier();
affi_FFT();
}
delay(20);
}
void dessine_gamme_FM()
{
// cadre rectangulaire
tft.fillRect(0, 210, 239, 28, 0x040004);
tft.drawRect(0, 212, 239, 28, TFT_WHITE);
tft.drawLine(20, 224, 215, 224, TFT_WHITE); // TFT_DARKGREEN
uint16_t x=20;
for(int n=0; n<20; n++)
{
tft.drawLine(3+x, 215, 3+x, 230, TFT_WHITE); // TFT_DARKGREEN
x+=10;
}
tft.setTextColor(TFT_YELLOW); //TFT_GREEN
tft.setFreeFont(FSS9);
tft.drawString("88", 4, 218, GFXFF);
tft.setFreeFont(FSSB9);
tft.drawString("108", 208, 218, GFXFF);
uint16_t frq1 = 15 + (( frq_out) - 88) * 10;
tft.fillCircle(frq1, 224, 5, TFT_GREEN);
}
// -------------------------------------------------------------------------
void dessine_VuMetre()
{
// cadre rectangulaire
tft.fillRect(0, 0, 239, 126, TFT_GREY);
tft.fillRect(5, 3, 230, 119, TFT_WHITE);
tft.setTextColor(TFT_BLACK);
// graduation chaque 5 deg entre -50 et +50 deg
for (int i = -50; i < 51; i += 5)
{
int tl = 15; // tiret plus long
// Coordonnées du tiret à dessiner
float sx = cos((i - 90) * 0.0174532925);
float sy = sin((i - 90) * 0.0174532925);
uint16_t x0 = sx * (100 + tl) + 120;
uint16_t y0 = sy * (100 + tl) + 140;
uint16_t x1 = sx * 100 + 120;
uint16_t y1 = sy * 100 + 140;
// Coordonnées of next tick for zone fill
float sx2 = cos((i + 5 - 90) * 0.0174532925);
float sy2 = sin((i + 5 - 90) * 0.0174532925);
int x2 = sx2 * (100 + tl) + 120;
int y2 = sy2 * (100 + tl) + 140;
int x3 = sx2 * 100 + 120;
int y3 = sy2 * 100 + 140;
// zone verte
if (i >= 0 && i < 25)
{
tft.fillTriangle(x0, y0, x1, y1, x2, y2, TFT_GREEN);
tft.fillTriangle(x1, y1, x2, y2, x3, y3, TFT_GREEN);
}
// zone orange
if (i >= 25 && i < 50)
{
tft.fillTriangle(x0, y0, x1, y1, x2, y2, TFT_ORANGE);
tft.fillTriangle(x1, y1, x2, y2, x3, y3, TFT_ORANGE);
}
if (i % 25 != 0) tl = 8; // Short scale tick length
// Recalcule coords in case tick lenght changed
x0 = sx * (100 + tl) + 120;
y0 = sy * (100 + tl) + 140;
x1 = sx * 100 + 120;
y1 = sy * 100 + 140;
// Draw tick
tft.drawLine(x0, y0, x1, y1, TFT_BLACK);
// Check if labels should be drawn, with position tweaks
if (i % 25 == 0)
{
// Calculate label positions
x0 = sx * (100 + tl + 10) + 120;
y0 = sy * (100 + tl + 10) + 140;
switch (i / 25)
{
case -2: tft.drawCentreString("0", x0, y0 - 12, 2); break;
case -1: tft.drawCentreString("25", x0, y0 - 9, 2); break;
case 0: tft.drawCentreString("50", x0, y0 - 6, 2); break;
case 1: tft.drawCentreString("75", x0, y0 - 9, 2); break;
case 2: tft.drawCentreString("100", x0, y0 - 12, 2); break;
}
}
// draw the arc of the scale
sx = cos((i + 5 - 90) * 0.0174532925);
sy = sin((i + 5 - 90) * 0.0174532925);
x0 = sx * 100 + 120;
y0 = sy * 100 + 140;
// Draw scale arc, don't draw the last part
if (i < 50) {tft.drawLine(x0, y0, x1, y1, TFT_BLACK);}
}
tft.drawRect(5, 3, 230, 119, TFT_BLACK); // Draw bezel line
}
void plotAiguille(int value)
{
tft.setTextColor(TFT_BLACK, TFT_WHITE);
char buf[8]; dtostrf(value, 4, 0, buf);
if (value < -10) value = -10; // Limit value to emulate aiguille end stops
if (value > 110) value = 110;
float sdeg = map(value, -10, 110, -150, -30); // Map value to angle
// Calcul tip of aiguille coords
float sx = cos(sdeg * 0.0174532925);
float sy = sin(sdeg * 0.0174532925);
// Calcul x delta of aiguille start (does not start at pivot point)
float tx = tan((sdeg + 90) * 0.0174532925);
// Erase old aiguille image
tft.drawLine(120 + 20 * ltx - 1, 140 - 20, osx - 1, osy, TFT_WHITE);
tft.drawLine(120 + 20 * ltx, 140 - 20, osx, osy, TFT_WHITE);
tft.drawLine(120 + 20 * ltx + 1, 140 - 20, osx + 1, osy, TFT_WHITE);
// Re-plot texte sous l'aiguille
tft.fillRect(100, 70, 40, 25, TFT_WHITE);
tft.setTextColor(TFT_BLACK);
tft.drawCentreString((String)value, 120, 70, 4);
// Store new aiguille end coords for next erase
ltx = tx;
osx = sx * 98 + 120;
osy = sy * 98 + 140;
// Draw the aiguille in the new postion, magenta makes aiguille a bit bolder
// draws 3 lines to thicken aiguille
tft.drawLine(120 + 20 * ltx - 1, 140 - 20, osx - 1, osy, TFT_RED);
tft.drawLine(120 + 20 * ltx, 140 - 20, osx, osy, TFT_MAGENTA);
tft.drawLine(120 + 20 * ltx + 1, 140 - 20, osx + 1, osy, TFT_RED);
}