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é...

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

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...

2063