Horizon artificiel - ESP32 - MPU6050 - TFT480x320

Petit appareil autonome, super réactif, basé sur un minuscule capteur gyroscopique MPU6050. Commence à ressembler furieusement à un "vrai". Toutefois je dois préciser que ce projet n'est pas destiné à être utilisé sur un avion réel ni même un ULM. Il faudrait pour cela obtenir des certifications dont il est totalement dépourvu. Éventuellement pour faire joli sur votre moto ? Pas sûr non plus que ce soit toléré...

Je fais de fréquentes mises à jour des pages. Ce bouton permet de raffraichir la version mise en cache dans votre navigateur (et rien d'autre !!!)

1 L'afficheur 480x320px de 3.5

Ce n'est pas une photo, mais une copie d'écran de l'afficheur

2 Photos

Voici la photo vue de face...

et vue de dos. sur la droite, fixée perpendiculairement à l'afficheur, la petite carte du gyroscope MPU6050.

3 Photos2

La carte ESP32 (30pins)

Les interconnexions sont assurées par un petit circuit imprimé. J'ai tout monté sur supports parce que c'est le prototype. On peut donc, en les supprimant, en soudant directement l'ESP32 et l'afficheur sur le circuit imprimé gagner énormément en épaisseur. Une sorte de smartphone donc...

4 Le contrôle qualité

C'est OK !

5 Le schéma

L'ensemble ne comporte que trois composants :
  • La carte ESP32
  • La carte avec le capteur MPU6050
  • l'afficheur LCD

6 Le code source pour l'ESP32

Vous aimez les math ? ça tombe bien ! Vous aimez la trigo ?? Vous allez adorer !! Il est aussi question de Sprites, de couleurs RGB565, de copie d'écran sur la TFcard... Que des bonnes choses.

ET tenez, je vous dévoile l'astuce principale qui permet d'obtenir la réactivité de l'affichage : Lorsque la ligne d'horizon tourne (roulis) ou se déplace (tangage), seule une fine bande de 24 pixels de large sur l'afficheur sont actualisés (plus la graduation mobile), et pas toute la surface. Gain de vitesse environ 30x !!! En effet, l'ESP32 est hyper rapide mais l'afficheur beaucoup moins.

CODE SOURCE en C++
  1. /*
  2. Horizon artificiel ()
  3.  
  4. pour ESP32 Wroom + afficheur 3.5" TFT 480x320
  5.  
  6. par Silicium628
  7.  
  8. */
  9.  
  10.  
  11. /*=====================================================================================================
  12. CONCERNANT L'AFFICHAGE TFT: connexion:
  13.  
  14. (Pensez à configurer le fichier User_Setup.h de la bibliothèque ~/Arduino/libraries/TFT_eSPI/ )
  15.  
  16. les lignes qui suivent ne sont qu'un commentaire pour vous indiquer la config à utiliser
  17. placée ici, elle ne sont pas fonctionnelles
  18. Il FAUT modifier le fichier User_Setup.h installé par le système Arduino dans ~/Arduino/libraries/TFT_eSPI/
  19.  
  20. // ESP32 pins used for the parallel interface TFT
  21. #define TFT_CS 27 // Chip select control pin
  22. #define TFT_DC 14 // Data Command control pin - must use a pin in the range 0-31
  23. #define TFT_RST 26 // Reset pin
  24.  
  25. #define TFT_WR 12 // Write strobe control pin - must use a pin in the range 0-31
  26. #define TFT_RD 13
  27.  
  28. #define TFT_D0 16 // Must use pins in the range 0-31 for the data bus
  29. #define TFT_D1 4 // so a single register write sets/clears all bits
  30. #define TFT_D2 2 // 23
  31. #define TFT_D3 22
  32. #define TFT_D4 21
  33. #define TFT_D5 15 // 19
  34. #define TFT_D6 25 // 18
  35. #define TFT_D7 17
  36. =====================================================================================================*/
  37.  
  38.  
  39. String version="1.0";
  40.  
  41. uint8_t fond_blanc = 0;
  42.  
  43.  
  44. #include <stdint.h>
  45. #include <TFT_eSPI.h> // Hardware-specific library
  46. #include "SPI.h"
  47. #include "Free_Fonts.h"
  48.  
  49. #include "FS.h"
  50. #include "SD.h"
  51.  
  52. TFT_eSPI TFT480 = TFT_eSPI(); // Configurer le fichier User_Setup.h de la bibliothèque TFT480_eSPI au préalable
  53.  
  54. //#include <WiFi.h> // Pour un serveur WiFi
  55. //#include "ESPAsyncWebServer.h"
  56.  
  57. #include "Wire.h"
  58. #include <MPU6050_light.h>
  59. /*
  60. un scan du bus i2c doit donner ceci (dans le moniteur série):
  61. Scanning...
  62. I2C device found at address 0x68
  63. done
  64. */
  65.  
  66.  
  67. const char* ssid = "PFD_srv";
  68. const char* password = "72r4TsJ28";
  69.  
  70. //AsyncWebServer server(80); // Create AsyncWebServer object on port 80
  71.  
  72. String argument_recu1;
  73. String argument_recu2;
  74. String argument_recu3;
  75.  
  76. // =====================================================================
  77. //mémorisation dex pixels deux lignes H et de deux lignes V
  78. //ce qui permet d'afficher un rectangle mobile sur l'image sans l'abimer
  79.  
  80. uint16_t data_L1[480]; // pixels d'une ligne Horizontale
  81. uint16_t data_L2[480]; // pixels d'une autre ligne Horizontale
  82. uint16_t data_C1[320]; // pixels d'une ligne Verticale ('C' comme colonne)
  83. uint16_t data_C2[320]; // pixels d'une autre ligne Verticale
  84.  
  85. uint16_t x_1; // position reçu du module positionneur_XY
  86. uint16_t x_2; // position reçu du module positionneur_XY
  87. uint16_t y_1;
  88. uint16_t y_2;
  89.  
  90. uint16_t memo_x1;
  91. uint16_t memo_y1; // position de la ligne
  92. uint16_t memo_x2;
  93. uint16_t memo_y2;
  94.  
  95. uint16_t memo_x_pivot;
  96. uint16_t memo_y_pivot;
  97.  
  98. float AngleX;
  99. float AngleY;
  100. float AngleZ;
  101.  
  102. char var_array32[10];// 10 char + zero terminal - pour envoi par WiFi (because 2^32 -1 = 4294967295 -> 10 caractères)
  103.  
  104.  
  105. // =====================================================================
  106.  
  107. #define _pi 3.141592653
  108. float raddeg =_pi/180.0;
  109.  
  110.  
  111. float roulis;
  112. float memo_roulis;
  113.  
  114. float R_2, T_2;
  115. float memo_R_2, memo_T_2;
  116.  
  117. float tangage;
  118. float memo_tangage;
  119.  
  120. float cap;
  121.  
  122. #define NOIR 0x0000
  123. #define MARRON 0x9240
  124. #define ROUGE 0xF800
  125. #define ROSE 0xFBDD
  126. #define ORANGE 0xFBC0
  127. #define JAUNE 0xFFE0
  128. #define JAUNE_PALE 0xF7F4
  129. #define VERT 0x07E0
  130. #define VERT_FONCE 0x02E2
  131. #define OLIVE 0x05A3
  132. #define CYAN 0x07FF
  133. #define BLEU_CLAIR 0x455F
  134. #define AZUR 0x1BF9
  135. #define BLEU 0x001F
  136. #define MAGENTA 0xF81F
  137. #define VIOLET1 0x781A
  138. #define VIOLET_2 0xECBE
  139. #define GRIS_TRES_CLAIR 0xDEFB
  140. #define GRIS_CLAIR 0xA534
  141. #define GRIS 0x8410
  142. #define GRIS_FONCE 0x5ACB
  143. #define GRIS_TRES_FONCE 0x2124
  144. #define BLANC 0xFFFF
  145.  
  146. #define GRIS_AF 0x51C5 // 0x3985
  147.  
  148. #define HA_CIEL 0x33FE
  149. #define HA_SOL 0xAA81 //0xDB60
  150.  
  151. // Width and height of sprite
  152. #define SPR_W 25
  153. #define SPR_H 16
  154.  
  155. uint16_t couleur_txt = BLANC;
  156. uint16_t couleur_fond = GRIS_TRES_FONCE; //GRIS_TRES_FONCE;
  157. uint16_t couleur_fond_txt = VERT_FONCE;
  158. uint16_t couleur_fond_gradu = HA_CIEL;
  159.  
  160. TFT_eSprite SPR_HA = TFT_eSprite(&TFT480);
  161.  
  162. TFT_eSprite SPR_10 = TFT_eSprite(&TFT480);
  163. TFT_eSprite SPR_10_ciel = TFT_eSprite(&TFT480);
  164. TFT_eSprite SPR_10_sol = TFT_eSprite(&TFT480);
  165.  
  166. //position et dimensions de l'horizon artificiel
  167. #define HA_x0 240
  168. #define HA_y0 160
  169.  
  170. #define HA_w 680
  171. #define HA_h 24
  172.  
  173. MPU6050 mpu(Wire);
  174. //unsigned long timer = 0;
  175.  
  176. uint8_t flag_SDcardOk=0;
  177. uint8_t flag_1er_passage =1;
  178. uint8_t TEST_AFFI;
  179.  
  180. uint32_t compte=0;
  181.  
  182.  
  183.  
  184. float degTOrad(float angle)
  185. {
  186. return (angle * M_PI / 180.0);
  187. }
  188.  
  189.  
  190. uint8_t decToBcd( int val )
  191. {
  192. return (uint8_t) ((val / 10 * 16) + (val % 10));
  193. }
  194.  
  195.  
  196. uint16_t Color_To_565(uint8_t r, uint8_t g, uint8_t b)
  197. {
  198. return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3);
  199. }
  200.  
  201.  
  202. void RGB565_to_888(uint16_t color565, uint8_t *R, uint8_t *G, uint8_t *B)
  203. {
  204. *R=(color565 & 0xFFFFF800) >> 8;
  205. *G=(color565 & 0x7E0) >> 3;
  206. *B=(color565 & 0x1F) << 3 ;
  207. }
  208.  
  209.  
  210. void init_SDcard()
  211. {
  212. String s1;
  213.  
  214. TFT480.fillRect(0, 0, 480, 320, NOIR); // efface
  215. TFT480.setTextColor(BLANC, NOIR);
  216. TFT480.setFreeFont(FF1);
  217.  
  218. uint16_t y=0;
  219.  
  220. TFT480.drawString("PRIMARY FLIGHT DISPLAY", 0, y);
  221. y+=20;
  222.  
  223. s1="version " + version;
  224. TFT480.drawString(s1, 0, y);
  225.  
  226. y+=40;
  227. TFT480.setTextColor(VERT, NOIR);
  228. TFT480.drawString("Init SDcard", 0, y);
  229. y+=20;
  230.  
  231.  
  232. if(!SD.begin())
  233. {
  234. TFT480.drawString("Card Mount Failed", 0, y);
  235. delay (2000);
  236. TFT480.fillRect(0, 0, 480, 320, NOIR); // efface
  237. return;
  238. }
  239.  
  240.  
  241. uint8_t cardType = SD.cardType();
  242.  
  243. if(cardType == CARD_NONE)
  244. {
  245. TFT480.drawString("No SDcard", 0, y);
  246. delay (2000);
  247. TFT480.fillRect(0, 0, 480, 320, NOIR); // efface
  248. return;
  249. }
  250.  
  251. flag_SDcardOk=1;
  252.  
  253. TFT480.drawString("SDcard Type: ", 0, y);
  254. if(cardType == CARD_SD) {TFT480.drawString("SDSC", 150, y);}
  255. else if(cardType == CARD_SDHC) {TFT480.drawString("SDHC", 150, y);}
  256.  
  257. y+=20;
  258.  
  259. uint32_t cardSize = SD.cardSize() / (1024 * 1024);
  260. s1=(String)cardSize + " GB";
  261. TFT480.drawString("SDcard size: ", 0, y);
  262. TFT480.drawString(s1, 150, y);
  263.  
  264. // listDir(SD, "/", 0);
  265.  
  266. //Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
  267. //Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));
  268.  
  269. delay (1000);
  270. TFT480.fillRect(0, 0, 480, 320, NOIR); // efface
  271.  
  272. }
  273.  
  274.  
  275.  
  276. /** -----------------------------------------------------------------------------------
  277. CAPTURE D'ECRAN vers SDcard
  278. /** ----------------------------------------------------------------------------------- */
  279.  
  280. void write_TFT_on_SDcard() // enregistre le fichier .bmp
  281. {
  282.  
  283. //TFT480.setTextColor(VERT, NOIR);
  284. //TFT480.drawString("CP", 450, 300);
  285.  
  286. if (flag_SDcardOk==0) {return;}
  287.  
  288. String s1;
  289. uint16_t ys=200;
  290. TFT480.setFreeFont(FF1);
  291. TFT480.setTextColor(JAUNE, NOIR);
  292.  
  293. uint16_t x, y;
  294. uint16_t color565;
  295. uint16_t bmp_color;
  296. uint8_t R, G, B;
  297.  
  298. if( ! SD.exists("/bmp/capture2.bmp"))
  299. {
  300. TFT480.fillRect(0, 0, 480, 320, NOIR); // efface
  301. TFT480.setTextColor(ROUGE, NOIR);
  302. TFT480.drawString("NO /bmp/capture2.bmp !", 100, ys);
  303. delay(300);
  304. TFT480.fillRect(100, ys, 220, 20, NOIR); // efface
  305. return;
  306. }
  307.  
  308.  
  309. File File1 = SD.open("/bmp/capture2.bmp", FILE_WRITE); // ouverture du fichier binaire (vierge) en écriture
  310. if (File1)
  311. {
  312. /*
  313. Les images en couleurs réelles BMP888 utilisent 24 bits par pixel:
  314. Il faut 3 octets pour coder chaque pixel, en respectant l'ordre de l'alternance bleu, vert et rouge.
  315. */
  316. uint16_t bmp_offset = 138;
  317. File1.seek(bmp_offset);
  318.  
  319.  
  320. TFT480.setTextColor(VERT, NOIR);;
  321.  
  322. for (y=320; y>0; y--)
  323. {
  324. for (x=0; x<480; x++)
  325. {
  326. color565=TFT480.readPixel(x, y);
  327.  
  328. RGB565_to_888(color565, &R, &G, &B);
  329.  
  330. File1.write(B); //G
  331. File1.write(G); //R
  332. File1.write(R); //B
  333. }
  334.  
  335. s1=(String) (y/10);
  336. TFT480.fillRect(450, 300, 20, 20, NOIR);
  337. TFT480.drawString(s1, 450, 300);// affi compte à rebour
  338. }
  339.  
  340. File1.close(); // referme le fichier
  341. TFT480.fillRect(450, 300, 20, 20, NOIR); // efface le compte à rebour
  342. }
  343. }
  344.  
  345. /** ----------------------------------------------------------------------------------- */
  346.  
  347.  
  348.  
  349.  
  350. void Draw_arc_elliptique(uint16_t x0, uint16_t y0, int16_t dx, int16_t dy, float alpha1, float alpha2, uint16_t couleur)
  351. // alpha1 et alpha2 en radians
  352. {
  353. /*
  354. REMARQUES :
  355. -cette fonction permet également de dessiner un arc de cercle (si dx=dy), voire le cercle complet
  356. - dx et dy sont du type int (et pas uint) et peuvent êtres négafifs, ou nuls.
  357. -alpha1 et alpha2 sont les angles (en radians) des caps des extrémités de l'arc
  358. */
  359. uint16_t n;
  360. float i;
  361. float x,y;
  362.  
  363. i=alpha1;
  364. while(i<alpha2)
  365. {
  366. x=x0+dx*cos(i);
  367. y=y0+dy*cos(i+M_PI/2.0);
  368. TFT480.drawPixel(x,y, couleur);
  369. i+=0.01; // radians
  370. }
  371. }
  372.  
  373.  
  374. void affi_rayon2(uint16_t x0, uint16_t y0, float r1, float R_2, float angle_i, uint16_t couleur_i)
  375. {
  376. // trace une portion de rayon de cercle entre les distances r1 et R_2 du centre
  377. // angle_i en degrés décimaux - sens trigo
  378.  
  379. float angle = degTOrad(angle_i);
  380. int16_t x1, x2;
  381. int16_t y1, y2;
  382.  
  383. x1=x0+int16_t(r1* cos(angle));
  384. y1=y0-int16_t(r1* sin(angle));
  385.  
  386. x2=x0+int16_t(R_2* cos(angle));
  387. y2=y0-int16_t(R_2* sin(angle));
  388.  
  389. if ((x1>0) && (x2>0) && (y1>0) && (y2>0) && (x1<480) && (x2<480) && (y1<320) && (y2<320) )
  390. {
  391. TFT480.drawLine(x1, y1, x2, y2, couleur_i);
  392. }
  393. }
  394.  
  395.  
  396. void affi_pointe(uint16_t x0, uint16_t y0, uint16_t r, uint16_t dr, double angle_i, float taille, uint16_t couleur_i)
  397. {
  398. // trace une pointe de flèche sur un cercle de rayon r
  399. // angle_i en degrés décimaux - sens trigo
  400.  
  401. float angle = degTOrad(angle_i);
  402. int16_t x1, x2, x3;
  403. int16_t y1, y2, y3;
  404.  
  405. x1=x0+r* cos(angle); // pointe
  406. y1=y0-r* sin(angle); // pointe
  407.  
  408. x2=x0+(r-dr)* cos(angle-taille); // base A
  409. y2=y0-(r-dr)* sin(angle-taille); // base A
  410.  
  411. x3=x0+(r-dr)* cos(angle+taille); // base B
  412. y3=y0-(r-dr)* sin(angle+taille); // base B
  413.  
  414. TFT480.fillTriangle(x1, y1, x2, y2, x3, y3, couleur_i);
  415. }
  416.  
  417.  
  418. void affi_base(uint16_t x0, uint16_t y0, float r, float angle_i, float delta_angle_i, uint16_t couleur_i)
  419. {
  420. // trace un trait tangent sur un cercle fictif de rayon r
  421. // angle_i en degrés décimaux, sens trigo
  422.  
  423. float angle =angle_i / 57.3; // (57.3 ~ 180/pi)
  424. float delta_angle = delta_angle_i / 57.3;
  425. int16_t x2, x3;
  426. int16_t y2, y3;
  427.  
  428. x2=x0+ r * cos(angle-delta_angle); // x du point A de la base du triangle
  429. y2=y0- r * sin(angle-delta_angle); // y
  430.  
  431. x3=x0+ r * cos(angle+delta_angle); // x du point B de la base du triangle
  432. y3=y0- r * sin(angle+delta_angle); // y
  433.  
  434. TFT480.drawLine(x2, y2, x3, y3, couleur_i);
  435. }
  436.  
  437.  
  438.  
  439. void init_sprites()
  440. {
  441. SPR_HA.createSprite(HA_w, HA_h);
  442. SPR_HA.setPivot(HA_w/2, HA_h/2);
  443. SPR_HA.fillSprite(NOIR); // pour test en rotation --> BLEU
  444. SPR_HA.fillRect(0, 0, HA_w, HA_h/2, HA_CIEL);
  445. SPR_HA.fillRect(0, HA_h/2, HA_w, HA_h/2, HA_SOL);
  446.  
  447. // sprite représentant le nombre '10' sur fond noir
  448. SPR_10.createSprite(SPR_W, SPR_H);
  449. SPR_10.setFreeFont(FF5);
  450. SPR_10_ciel.setTextColor(BLANC, NOIR);
  451. SPR_10.fillSprite(NOIR);
  452. SPR_10.drawString("10", 2, 2 );
  453. SPR_10.setPivot(SPR_W/2, SPR_H/2); // Set pivot relative to top left corner of Sprite
  454.  
  455. // sprite représentant le nombre '10' sur fond bleu
  456. SPR_10_ciel.createSprite(SPR_W, SPR_H);
  457. SPR_10_ciel.setFreeFont(FF5);
  458. SPR_10_ciel.setTextColor(BLANC, HA_CIEL);
  459. SPR_10_ciel.fillSprite(HA_CIEL);
  460. SPR_10_ciel.setPivot(SPR_W/2, SPR_H/2); // Set pivot relative to top left corner of Sprite
  461. SPR_10_ciel.drawString("10", 2, 2 );
  462.  
  463.  
  464. // sprite représentant le nombre '10' sur fond marron
  465. SPR_10_sol.createSprite(SPR_W, SPR_H);
  466. SPR_10_sol.setFreeFont(FF5);
  467. SPR_10_ciel.setTextColor(BLANC, HA_SOL);
  468. SPR_10_sol.fillSprite(HA_SOL);
  469. SPR_10_sol.setPivot(SPR_W/2, SPR_H/2); // Set pivot relative to top left corner of Sprite
  470. SPR_10_sol.drawString("10", 2, 2 );
  471.  
  472. }
  473.  
  474.  
  475.  
  476. void affi_HA(float R_in, float T_in) // Navigation Display (le grand cercle à gauche avec les différents affichages dessus)
  477. {
  478. //float angle1;
  479.  
  480. int16_t x_pivot, y_pivot;
  481. int16_t x0= 240;
  482. int16_t y0= 160;
  483.  
  484. x_pivot = x0 + T_in * sin(degTOrad(R_in));
  485. y_pivot = y0 + T_in * cos(degTOrad(R_in));
  486. TFT480.setPivot(x_pivot, y_pivot);
  487. SPR_HA.pushRotated(-R_in); // affiche la ligne de séparation ciel/sol
  488.  
  489. dessine_avion();
  490.  
  491. // graduations
  492.  
  493. for (int n=1; n<=10; n++)
  494. {
  495. float r = 10;
  496. float delta_angle = 10.0;
  497.  
  498. if (n==1) {delta_angle = 30.0; r = -20;}
  499. if (n==2) {delta_angle = 30.0; r = -40;}
  500. if (n==3) {delta_angle = 10.0; r = -53;}
  501. if (n==4) {delta_angle = 30.0; r = -80;}
  502. if (n==5) {delta_angle = 7.0; r = -90;}
  503.  
  504. if (n==6) {delta_angle = 30.0; r = 20;}
  505. if (n==7) {delta_angle = 30.0; r = 40;}
  506. if (n==8) {delta_angle = 10.0; r = 53;}
  507. if (n==9) {delta_angle = 30.0; r = 80;}
  508. if (n==10){delta_angle = 7.0; r = 90;}
  509.  
  510. if (r > T_in) {couleur_fond_gradu = HA_SOL;} else {couleur_fond_gradu = HA_CIEL;}
  511.  
  512. //TFT480.fillRect(452, 90+r, 5, 5, couleur_fond_gradu); // pour test
  513.  
  514. affi_base(x0, y0, -r, memo_roulis+90.0, delta_angle, couleur_fond_gradu); // efface
  515. affi_base(x0, y0, -r, R_in+90.0, delta_angle, BLANC); // trace
  516. }
  517.  
  518. // affichage de l'étiquette '10' sur la graduation
  519.  
  520. x_pivot = x0 - 90 * sin(degTOrad(R_2-38));
  521. y_pivot = y0 - 90 * cos(degTOrad(R_2-38));
  522.  
  523. uint16_t couleur1;
  524. int16_t limite = -70;
  525.  
  526. if (T_2 < limite){couleur1 = HA_SOL;} else {couleur1 = HA_CIEL;}
  527.  
  528. TFT480.fillRect(memo_x_pivot-16, memo_y_pivot-12, 32, 22, couleur1); // efface
  529. TFT480.setPivot(x_pivot, y_pivot);
  530. SPR_10.pushRotated(-R_2, NOIR);
  531.  
  532. //dessine_avion();
  533.  
  534. memo_x_pivot = x_pivot;
  535. memo_y_pivot = y_pivot;
  536.  
  537. //void affi_pointe(uint16_t x0, uint16_t y0, uint16_t r, uint16_t dr, double angle_i, float taille, uint16_t couleur_i)
  538.  
  539. affi_pointe(240, 160, 120, 12, memo_roulis+90, 0.05, HA_CIEL);
  540. affi_pointe(240, 160, 120, 12, R_in+90, 0.05, BLANC);
  541.  
  542. float alpha;
  543. uint8_t z;
  544.  
  545.  
  546. // graduations 10 20 30 45 60
  547. affi_graduation_fixe();
  548.  
  549. memo_roulis = R_in;
  550. }
  551.  
  552.  
  553.  
  554. void dessine_avion() // sous forme d'équerres horizontales noires entourées de blanc
  555. {
  556. // aile gauche
  557. TFT480.fillRect(HA_x0-102, HA_y0-3, 60, 10, BLANC); //H contour en blanc
  558. TFT480.fillRect(HA_x0-42, HA_y0-3, 10, 19, BLANC); //V
  559.  
  560. TFT480.fillRect(HA_x0-100, HA_y0-1, 60, 5, NOIR); //H
  561. TFT480.fillRect(HA_x0-40, HA_y0-1, 5, 15, NOIR); //V
  562.  
  563.  
  564. // aile droite
  565. TFT480.fillRect(HA_x0+28, HA_y0-3, 64, 10, BLANC); //H contour en blanc
  566. TFT480.fillRect(HA_x0+28, HA_y0-3, 10, 19, BLANC); //V
  567.  
  568. TFT480.fillRect(HA_x0+30, HA_y0-1, 60, 5, NOIR); //H
  569. TFT480.fillRect(HA_x0+30, HA_y0-1, 5, 15, NOIR); //V
  570.  
  571. //carré blanc au centre
  572.  
  573. TFT480.fillRect(HA_x0-4, HA_y0-3, 8, 2, BLANC);
  574. TFT480.fillRect(HA_x0-4, HA_y0-3, 2, 8, BLANC);
  575.  
  576. TFT480.fillRect(HA_x0-4, HA_y0+3, 10, 2, BLANC);
  577. TFT480.fillRect(HA_x0+4, HA_y0-3, 2, 8, BLANC);
  578.  
  579. }
  580.  
  581.  
  582.  
  583.  
  584.  
  585. void affi_ligne1_V(uint16_t x)
  586. {
  587. /** DOC: (source : "TFT_eSPI.h")
  588. // The next functions can be used as a pair to copy screen blocks (or horizontal/vertical lines) to another location
  589.  
  590. // Read a block of pixels to a data buffer, buffer is 16 bit and the size must be at least w * h
  591. void readRect(int32_t x, int32_t y, int32_t w, int32_t h, uint16_t *data);
  592.  
  593. // Write a block of pixels to the screen which have been read by readRect()
  594. void pushRect(int32_t x, int32_t y, int32_t w, int32_t h, uint16_t *data);
  595. **/
  596.  
  597. TFT480.pushRect(memo_x1, 0, 1, 320, data_C1); // efface la ligne en replaçant l'image
  598. memo_x1=x;
  599. TFT480.readRect(x, 0, 1, 320, data_C1); // memorisation de la ligne avant de tracer dessus
  600. //TFT480.drawFastVLine(x, 0, 320, ROUGE);
  601. TFT480.drawFastVLine(x, y_1, y_2-y_1, JAUNE);
  602. }
  603.  
  604.  
  605.  
  606. void affi_ligne2_V(uint16_t x)
  607. {
  608. TFT480.pushRect(memo_x2, 0, 1, 320, data_C2); // efface la ligne en replaçant l'image
  609. memo_x2=x;
  610. TFT480.readRect(x, 0, 1, 320, data_C2); // memorisation de la ligne avant de tracer dessus
  611. //TFT480.drawFastVLine(x, 0, 320, ROUGE);
  612. TFT480.drawFastVLine(x, y_1, y_2-y_1, JAUNE);
  613. }
  614.  
  615.  
  616. void affi_ligne1_H(uint16_t y)
  617. {
  618. TFT480.pushRect(0, memo_y1, 480, 1, data_L1); // efface la ligne en replaçant l'image
  619. memo_y1=y;
  620. TFT480.readRect(0, y, 480, 1, data_L1); // memorisation de la ligne avant de tracer dessus
  621. //TFT480.drawFastHLine(0, y, 480, ROUGE);
  622. TFT480.drawFastHLine(x_1, y, x_2-x_1, JAUNE);
  623. }
  624.  
  625.  
  626. void affi_ligne2_H(uint16_t y)
  627. {
  628. TFT480.pushRect(0, memo_y2, 480, 1, data_L2); // efface la ligne en replaçant l'image
  629. memo_y2=y;
  630.  
  631. TFT480.readRect(0, y, 480, 1, data_L2); // memorisation de la ligne avant de tracer dessus
  632. //TFT480.drawFastHLine(0, y, 480, ROUGE);
  633. TFT480.drawFastHLine(x_1, y, x_2-x_1, JAUNE);
  634. }
  635.  
  636.  
  637.  
  638. void setup()
  639. {
  640.  
  641. //Serial.begin(115200);
  642.  
  643. //Wire.begin(21,22, 100000); // choisir d'autre pins pour compatibilité avec l'afficheur LCD que je n'ai pas pu déplacer
  644.  
  645. uint8_t GPIO_SDA = 33;
  646. uint8_t GPIO_SCL = 32;
  647.  
  648. Wire.begin(GPIO_SDA, GPIO_SCL, 100000); // OK (source: https://randomnerdtutorials.com/esp32-i2c-communication-arduino-ide/ )
  649. // en conséquece câbler le MPU6050 en i2C sur les GPIO 32 et GPIO 33 de l'ESP32 (à la place de 21, 22 par défaut)
  650.  
  651. //Serial.println("display.init()");
  652.  
  653. TFT480.init();
  654.  
  655. TFT480.setRotation(3); // 0..3 à voir, suivant disposition de l'afficheur et sa disposition
  656.  
  657. TFT480.fillScreen(NOIR);
  658. TFT480.setTextColor(BLANC, NOIR);
  659.  
  660. TFT480.setFreeFont(FF1);
  661. uint16_t y=0;
  662. TFT480.drawString("Horizon artificiel", 0, y);
  663. y+=20;
  664. String s1="version " + version;
  665. TFT480.drawString(s1, 0, y);
  666. y+=20;
  667.  
  668.  
  669. delay(300);
  670.  
  671. byte status = mpu.begin();
  672.  
  673. s1="MPU6050 status:" + String(status);
  674. TFT480.setTextColor(JAUNE, NOIR);
  675. TFT480.drawString(s1, 0, y);
  676. y+=20;
  677.  
  678. ////while(status!=0){ } // stop everything if could not connect to MPU6050
  679. TFT480.setTextColor(BLANC, NOIR);
  680. s1="Calcul offsets, do not move MPU6050";
  681. TFT480.drawString(s1, 0, y);
  682. y+=20;
  683.  
  684. delay(1000);
  685. // mpu.upsideDownMounting = true; // uncomment this line if the MPU6050 is mounted upside-down
  686. mpu.calcOffsets(); // gyro and acceler
  687.  
  688. TFT480.setTextColor(VERT, NOIR);
  689. s1="OK!\n";
  690. TFT480.drawString(s1, 0, y);
  691. y+=20;
  692.  
  693. delay(1000);
  694.  
  695. TFT480.setTextColor(TFT_BLUE, TFT_BLACK);
  696. //TFT480.setTextFont(1);
  697. TFT480.setCursor(0, 40, 1);
  698. TFT480.fillScreen(couleur_fond);
  699.  
  700. init_SDcard();
  701.  
  702. init_sprites();
  703.  
  704. TFT480.fillRect(0, 0, 480, 160, HA_CIEL);
  705. TFT480.fillRect(0, 160, 480, 320, HA_SOL);
  706.  
  707. R_2=0;
  708. T_2=0;
  709. affi_HA(R_2,T_2);
  710.  
  711. x_1=0;
  712. y_1=0;
  713.  
  714. mpu.update();
  715.  
  716. tangage = 0;
  717. roulis = 0;
  718.  
  719. TEST_AFFI=0;
  720.  
  721. for(int n=0; n<200; n++)
  722. {
  723. mpu.update(); // calme la bête !
  724. }
  725.  
  726. ///delay(1000);
  727. ///write_TFT_on_SDcard();
  728.  
  729. }
  730.  
  731.  
  732. void affi_graduation_fixe()
  733. {
  734. int16_t x0= 240;
  735. int16_t y0= 160;
  736.  
  737. affi_rayon2(x0, y0, 125, 140, 90, BLANC);
  738.  
  739. affi_rayon2(x0, y0, 125, 130, 90-10, BLANC);
  740. affi_rayon2(x0, y0, 125, 130, 90-20, BLANC);
  741. affi_rayon2(x0, y0, 125, 140, 90-30, BLANC);
  742. affi_rayon2(x0, y0, 125, 140, 90-45, BLANC);
  743. affi_rayon2(x0, y0, 125, 140, 90-60, BLANC);
  744.  
  745. affi_rayon2(x0, y0, 125, 130, 90+10, BLANC);
  746. affi_rayon2(x0, y0, 125, 130, 90+20, BLANC);
  747. affi_rayon2(x0, y0, 125, 140, 90+30, BLANC);
  748. affi_rayon2(x0, y0, 125, 140, 90+45, BLANC);
  749. affi_rayon2(x0, y0, 125, 140, 90+60, BLANC);
  750.  
  751. TFT480.setFreeFont(FF5);
  752. TFT480.setTextColor(BLANC, HA_CIEL);
  753. TFT480.drawString("30", x0+70, 23);
  754. TFT480.drawString("30", x0-90, 23);
  755.  
  756. TFT480.drawString("60", x0+125, 75);
  757. TFT480.drawString("60", x0-150, 75);
  758. }
  759.  
  760.  
  761.  
  762. void affichages()
  763. {
  764.  
  765. //les lignes suivantes obligent la rotation pas à pas
  766.  
  767. //if (R_2<roulis-10) { affi_HA(R_2, T_2); R_2+=2.5; }
  768. if (R_2<roulis-3) { affi_HA(R_2, T_2); R_2+=2; }
  769. else if (R_2<roulis) { affi_HA(R_2, T_2); R_2++; }
  770.  
  771. //if (R_2>roulis+10) { affi_HA(R_2, T_2); R_2-=2.5; }
  772. if (R_2>roulis+3) { affi_HA(R_2, T_2); R_2-=2; }
  773. else if (R_2>roulis) { affi_HA(R_2, T_2); R_2--; }
  774.  
  775.  
  776.  
  777. if (T_2<tangage-3) { affi_HA(R_2, T_2); T_2+=2; }
  778. else if (T_2<tangage) { affi_HA(R_2, T_2); T_2++; }
  779.  
  780. if (T_2>tangage+3) { affi_HA(R_2, T_2); T_2-=2; }
  781. else if (T_2>tangage) { affi_HA(R_2, T_2); T_2--;}
  782.  
  783. }
  784.  
  785.  
  786.  
  787. float t=0;
  788. float dt=2;
  789.  
  790. void loop()
  791. {
  792.  
  793. if(TEST_AFFI==1) // pour test des affichages:
  794. {
  795. roulis = t;
  796. tangage = 60.0*sin(t/134);
  797.  
  798. affichages();
  799. t += dt; // 3*dt ...
  800. if ((t==120)||(t== -120)) {dt = -dt;}
  801. //delay(1);
  802. }
  803.  
  804.  
  805. else
  806. {
  807. mpu.update();
  808.  
  809. AngleX=mpu.getAngleX();
  810. AngleY=mpu.getAngleY();
  811. AngleZ=mpu.getAngleZ();
  812. //timer = millis();
  813.  
  814. tangage = -5.0*AngleX;
  815. roulis = -1.0*AngleY;
  816.  
  817. affichages();
  818. compte++;
  819. delay(1);
  820.  
  821. // if (compte==100) {write_TFT_on_SDcard();}
  822. }
  823. }
  824.  
  825.  
  826.  

7 Test en vidéo


Voici le fonctionnement en temps réel de l'appareil. J'ai incrusté cette vidéo dans un paysage maritime mais je vous assure que la prise de vue a été faite avec le camescope fixé horizontalement sur un pied, et donc immobile (devant un fond vert. La réaction rapide de l'affichage est donc bien réelle. Si votre avion s’agitait de cette façon, il y aurait du soucis à se faire !!

8 Pourquoi des gyroscopes et pas un simple fil à plomb ?

-Un corps matériel soumis à aucune force se déplace en ligne droite à vitesse constante (qui peut être nulle).
-Un avion qui vole en ligne droite à vitesse et altitude constante n'est soumis à aucune force, plus précisément les différentes forces qui s'appliquent sur lui s'annulent:
  • la portance annule le poids.
  • la traction du moteur annule la traînée.
La résultante des forces aérodynamiques qui s’appliquent sur l’aile, et qui résulte du déplacement du profil de l’aile dans l’air est perpendiculaire à la surface de l’aile. Elle se décompose vectoriellement en :
  • une force verticale, vers le haut -> la portance.
  • une force horizontale, vers l’arrière -> la traînée.
Pour obliger l’avion à prendre un virage à altitude constante, il faut en permanence le tirer vers le centre de la trajectoire circulaire.
On y parvient en inclinant l’aile sur le côté (en roulis ) ce qui incline également la force résultante sur l’aile, dont la décomposition vectorielle fait apparaître une nouvelle composante horizontale perpendiculaire à la trajectoire, vers le centre de la trajectoire, ce que l’on souhaite, mais diminue la composante verticale, la portance. Il faut alors, pour éviter que l’avion ne descende, augmenter cette portance en augmentant les gaz → augmentation de la vitesse → augmentation du module de la résultante des forces aérodynamiques → augmentation de la portance (et de la traînée).

Lorsque l’avion se déplaçait une ligne droite en vitesse et altitude constantes, le pilote ne ressentait que son poids qui est le produit de sa masse (en kg) par l’accélération de la pesanteur (dirigée vers le centre de la Terre) suivant la formule P=mG. (plus exactement il ressentait la force que la structure de l'avion (le siège) exerce vers le haut sur son postérieur et qui contrebalance son poids, en effet, en chute libre on ne ressent rien du tout (sauf le vent relatif), et en particulier on ne ressent plus son poids).

Lorsque l’avion suit une trajectoire qui tourne dans le plan horizontal, en vitesse et altitude constantes, le pilote ressent maintenant une force égale au produit de sa masse (en kg) par l’accélération résultante (plus exactement... voir plus haut).

Cette accélération résultante et la somme vectorielle de l’accélération de la pesanteur qui n’a pas changé et de l’accélération centripète. Oui centripète, pas centrifuge !! Un corps qui se déplace à vitesse linéaire constante suivant une trajectoire circulaire voit sa vitesse changer constamment. Pas le module de sa vitesse, qui par hypothèse est constante, mais la direction de sa vitesse qui tourne en permanence. Et une force qui varie c’est une accélération.

Voici la démonstration mathématique (faire dérouler le PDF...):



  • Cette démonstration en pdf -> 01.pdf (au cas où elle ne s'afficherait pas dans le cadre ci-dessus, par exemple sur certains navigateurs Androïd)

Pour que le pilote suive la même trajectoire que l’avion, qu’il ne continue pas en ligne droite ( !) il faut le contraindre à effectuer cette accélération centripète (gamma) en lui appliquant une force (F en N) horizontale dirigée vers le centre de la courbe, c.a.d perpendiculairement à la trajectoire. L'accélération centripète (gamma en m/s²) n'est pas la cause, c'est la conséquence. La cause c'est la force centripète (en N) appliquée par la structure de l'avion sur la masse du pilote. Si l'avion était une sorte d'hologramme immatériel, n'agissant pas sur le pilote, ce dernier continuerait effectivement en ligne droite dans le plan horizontal. (et descendante dans le plan vertical !)

La deuxième loi de Newton, dite aussi "principe fondamental de la dynamique" (en abréviation, PFD, tiens, tiens...) dit que gamma = F/m avec gamma et F alignés et dans le même sens.

Nous obtenons donc comme conséquence de la force appliquée latéralement par l'avion sur la masse du pilote -> une accélération gamma = F/m dirigée vers le centre de la trajectoire courbe.

Deux choses concernent donc le pilote :
  • son poids = au produit de sa masse (en kg) par l'accélération de la pesanteur (9.81 m/s²) dirigée vers le centre de la Terre.
  • et notre force F latérale.
Ce que ressent le pilote c'est la résultante (la somme vectorielle) entre :
  • La force que la structure de l'avion (le siège) exerce vers le haut sur son postérieur et qui contrebalance son poids.
  • et notre force latérale.
Remarque : Vous ai-je parlé de "force centrifuge" ? NON parce que ça n'existe pas ! C'est un truc sans doute inventé par les gangsters qui tirent par les portières des voitures en virage (enfin c'est comme ça au cinéma)... Si vous faites tourner une pierre au bout d'une ficelle et que la ficelle se casse, la pierre continuera avec une trajectoire rectiligne TANGENTE au cercle (et pas radiale ! TANGENTE !!! Ok ?) Cette résultante est un peu plus grande en module que le poids seul, mais surtout elle est dirigée vers le haut, mais inclinée vers le centre de la trajectoire.

..et cette inclinaison est égale à celle de l'avion de sorte que pilote ne ressent pas de force latérale, il se sent juste un peu plus lourd. Mais ça, cette augmentation du poids, lorsqu’on est bien calé sur son siège, ça passe presque inaperçu. (Plus exactement c'est le cas lorsque l'avion est correctement piloté, voir plus bas).

C’est la raison pour laquelle, lorsque l’avion tourne de jour par beau temps, la vue de l’horizon réel ne permet aucun doute sur le fait qu’on tourne. MAIS sans visibilité (dans le brouillard, les nuages, ou de nuit sans lune au dessus de la mer loin des côtes …) on peut très bien se trouver en virage sans s’en rendre compte, ce qui est extrêmement dangereux du point de vue de la perte de l’orientation et de la trajectoire mais également du fait que l’augmentation des forces sur la structure de l’avion peut occasionner des dégâts.

Mais alors, pour détecter le fait qu’on est incliné et qu'on tourne, un simple fil à plomb ne suffit-il pas ? Eh bien non justement. Le fil à plomb va bien s’incliner mais suivant la somme vectorielle de l'accélération de la pesanteur et l'accélération latérale. Et cela donne le même angle de l’inclinaison de l’avion et de son pilote. Il restera de ce fait perpendiculaire au plancher de l’avion (pas celui des vaches !!). C'est vrai si l'avion est bien piloté, et d'ailleurs il existe un instrument de bord, la bille, qui permet de vérifier cela (C'est l'équivalent d'un fil à plomb). Oui un avion peut voler avec une résultante des forces aérodynamiques qui ne soit par strictement perpendiculaire à l'aile, du fait de la surface de son fuselage et des empennages, mais c'est la chose à éviter en temps normal. (Toutefois la bille seule ne dit rien sur l'assiette latérale de l'avion par rapport à l'horizon).

Un horizon artificiel ne peut donc pas être basé sur une simple masselotte qui se dirigerait vers "le bas". Un horizon artificiel doit donc garder la mémoire de l’orientation de la verticale terrestre (mémorisée au sol avant de décoller) durant tout le vol. Et ça, les gyroscopes savent le faire.

Un gyroscope mécanique est composé d’une masse en rotation rapide (toupie qui garde une orientation constante) montée sur double cardan. Je vous fait grâce de la démonstration mathématique et de la force de Coriolis :)

La puce électronique MPU6050 comprend des accéléromètres et gyroscopes en micro-mécanique (MEMS) de précision nanométrique. Un bijou de technologie ! Toutefois, pour la fonction gyroscopique, il n’y a pas de pièce en rotation rapide et continue. La puce détecte des accélérations circulaires, reste à en déduire l’angle de rotation par intégration mathématique.

"MPU6050 Gyroscopes do NOT report angles, they report the speed at which the device is turning, or angular velocity. In order to get the angle position you have to integrate it over time. " (voir les liens ci-dessous)

9 Documents

Code source en C++

10 -

Liens...



926