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

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

Pourquoi filmer devant un fond vert plutôt que devant un vrai paysage (surtout que j'habite pas loin ni de la mer ni de la montagne ? En fait j'ai essayé de le faire et je me suis vite rendu compte que la luminosité d'un afficheur TFT devant un tel arrière-plan c'est pas top : on obtient soit un affichage lisible devant un fond tout blanc, soit un magnifique paysage avec un afficheur tout noir au centre ! avec un personnage, on peut encore l'éclairer avec un spot, mais si on éclaire un afficheur... ça ne le rend pas d'avantage lisible (sauf pour un écran e-ink d'une liseuse par exemple).

8 Version pour la carte ESP32 Cheap Yellow Display

Il s'agit de la carte ESP32 Cheap Yellow Display (ESP32-2432S028R) J'ai découvert récemment cette carte qui associe une platine ESP32 + un écran 2.8 inch (320x240). Elle existe aussi en 3.5". Vous trouverez le code spécifique au bas de cet article.

9 Test, en vidéo de la carte jaune 320x240


10 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 suivant 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 que 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)

11 Documents

Code source en C++
27 mars 2025 : J'ai ajouté une version du code source adaptée à l’éditeur VScode/PlatformIO
L'avantage par rapport à l'EDI Arduino c'est que ça encapsule toutes les dépendances et les bibliothèques nécessaires et dans la bonne version.

12 -

Liens...

4484