
A ideia de construir este projeto surgiu da tentativa de reproduzir, em uma placa considerada simples perto dos computadores de voo reais, o funcionamento de um dos instrumentos mais conhecidos da aviação: o horizonte artificial. Em um voo, conhecer a atitude da aeronave é fundamental, visto que o piloto precisa saber não apenas para onde a aeronave está se deslocando, mas também como ela está orientada no espaço. Uma aeronave pode, por exemplo, estar fazendo uma curva inclinada ou estar com o nariz acima ou abaixo do horizonte sem que isso seja imediatamente perceptível visualmente. O horizonte artificial resolve esse problema apresentando uma representação gráfica da atitude da aeronave. Em uma implementação eletrônica, essa informação pode ser obtida através de sensores inerciais e processada por um microcontrolador.
Neste projeto utilizaremos um ESP32, um MPU6050 e uma TFT LCD de 3,5 polegadas para construir uma versão experimental desse instrumento. O MPU6050 será responsável por medir os movimentos da plataforma através de um acelerômetro e um giroscópio. O ESP32 realizará a leitura desses dados, calculará os ângulos de orientação e controlará a interface gráfica exibida na tela.
O resultado será uma representação semelhante a um horizonte artificial, na qual o horizonte se movimenta conforme a placa é inclinada. Além de reproduzir visualmente o instrumento, o projeto permite explorar conceitos bastante interessantes de sistemas embarcados, como comunicação entre dispositivos, leitura de sensores, processamento de sinais, trigonometria, filtragem e representação gráfica. Esse projeto poderá ser usado e adaptado para construção de drones e aeromodelos, nos quais ter um plano conhecimento da atitude do aparelho, pode ser a diferença entre um voo perfeito ou uma colisão e destruição do projeto.
Também será necessário um computador com a Arduino IDE instalada.
No software utilizaremos principalmente:
TFT_eSPI;MPU6050_light;Wire;SPI;TFT_eSPI, SPI, Wire e MPU6050_light.
Esse artigo a seguir poderá lhe ajudar:
Antes de partir para a montagem, é importante entender o que estamos tentando reproduzir. O horizonte artificial indica principalmente dois movimentos da aeronave: Pitch e Roll.
O Pitch representa a inclinação longitudinal (arfagem). Em termos práticos, indica se o nariz da aeronave está apontando para cima ou para baixo.
O Roll representa a inclinação lateral da aeronave (rolagem), ou seja, quanto ela está inclinada para a direita ou para a esquerda.
Outra métrica existente nas aeronaves é o Yaw, que indica se a aeronave está virando para a direita ou para a esquerda (guinada), porém não será utilizada nesse projeto, em razão de algumas interferências causadas no projeto final.
Em nosso projeto, o ESP32 fará essa transformação: os dados numéricos provenientes do MPU6050 serão convertidos em coordenadas gráficas utilizadas para desenhar o horizonte na tela.
O MPU6050 é uma IMU (Inertial Measurement Unit, Unidade de Medição Inercial em português) bastante utilizada em projetos de robótica, drones e sistemas embarcados.
O módulo reúne dois sensores diferentes no mesmo circuito integrado:
Por isso, é frequentemente descrito como um sensor de 6 graus de liberdade (6 DOF).
O acelerômetro mede a aceleração nos eixos X, Y e Z. Quando o módulo está parado, uma das principais referências disponíveis para o sensor é a gravidade. A partir da distribuição dessa aceleração entre os três eixos podemos estimar a inclinação da placa.
O giroscópio trabalha de maneira diferente. Ele mede a velocidade angular, normalmente em graus por segundo. Se o sensor estiver girando, por exemplo, a 30 °/s, podemos utilizar essa informação juntamente com o intervalo de tempo entre duas leituras para calcular a alteração do ângulo.
Os dois sensores apresentam vantagens diferentes:
O acelerômetro fornece uma referência estável para a orientação em relação à gravidade, mas pode sofrer interferência quando o dispositivo está acelerando ou vibrando.
O giroscópio responde muito rapidamente às rotações, mas possui um pequeno erro que, quando integrado continuamente, provoca o chamado drift.
Por isso, não queremos simplesmente escolher um dos sensores. Queremos combinar os dois.
É justamente por apresentarem características diferentes que acelerômetro e giroscópio podem trabalhar melhor quando utilizados em conjunto.
O acelerômetro oferece uma boa referência de longo prazo, mas é bastante sensível a vibrações e outras acelerações.
O giroscópio apresenta boa resposta aos movimentos rápidos, porém sofre com o acúmulo de erro ao longo do tempo.
Neste projeto utilizamos a biblioteca MPU6050_light. Ela realiza o processamento dos dados do acelerômetro e do giroscópio utilizando um filtro complementar para estimar os ângulos de inclinação nos eixos X e Y. Isso é parte da própria biblioteca, e não uma equação que precisamos implementar manualmente no sketch.
O princípio de um filtro complementar pode ser representado de forma simplificada por:ângulo = α × (ângulo anterior + velocidade angular × Δt) + (1 − α) × ângulo do acelerômetro
Um valor típico para α seria algo próximo de 0,98, embora esse parâmetro possa ser ajustado durante os testes. Dessa maneira, o sistema não depende exclusivamente de um único sensor. Essa técnica é particularmente interessante para o projeto porque pode ser implementada diretamente no ESP32, sem exigir um processador adicional ou algoritmos muito complexos. Na prática, o giroscópio recebe maior peso nas mudanças rápidas, enquanto o acelerômetro atua corrigindo lentamente o erro acumulado. Dessa forma, obtemos uma leitura mais adequada para movimentar o horizonte na tela do que obteríamos utilizando somente um dos sensores.
A comunicação entre o ESP32 e o MPU6050 é feita através do barramento I²C. O I²C utiliza principalmente duas linhas:
Uma vantagem do ESP32 é que podemos definir por software quais GPIOs serão utilizados para essas funções. No código do projeto, a configuração ativa utiliza:
const int GPIO_SDA = 33;
const int GPIO_SCL = 32;
Posteriormente, o barramento é iniciado através de:
Wire.begin(GPIO_SDA, GPIO_SCL, 100000);
O valor 100000 indica uma frequência de comunicação de 100 kHz. O MPU6050 normalmente responde pelo endereço I²C 0x68, que também é o endereço esperado pelo código utilizado neste projeto.
Antes de começar a utilizar os ângulos fornecidos pelo sensor, o programa realiza sua calibração. No código, essa etapa aparece através da função:
mpu.calcOffsets();
Ela calcula os offsets necessários para o acelerômetro e para o giroscópio. Durante esse processo o MPU6050 deve permanecer completamente parado. Isso acontece porque o programa precisa determinar qual parcela das leituras corresponde ao erro natural dos sensores. Se movimentarmos o módulo durante a calibração, parte desse movimento poderá ser interpretada incorretamente como offset. Depois da calibração, o programa passa a atualizar continuamente os dados do MPU6050 com:
mpu.update();
e utiliza os ângulos fornecidos pela biblioteca para controlar o horizonte.
Uma parte importante deste projeto é a tela utilizada para representar o horizonte artificial. Como precisamos exibir uma interface gráfica que se movimenta continuamente, um display apenas de caracteres não seria suficiente. Por isso, utilizaremos uma TFT LCD de 3,5 polegadas, capaz de controlar individualmente milhares de pixels e exibir gráficos coloridos.
Embora os termos TFT e LCD apareçam frequentemente juntos, eles representam coisas diferentes.
LCD significa Liquid Crystal Display, ou display de cristal líquido. Os cristais líquidos controlam a passagem da luz em cada região da tela, formando a imagem que observamos.
Já TFT, ou Thin-Film Transistor, está relacionado à forma como os pixels são controlados. Em uma tela TFT, pequenos transistores são associados aos pixels da matriz, permitindo um controle mais rápido e preciso da imagem.
Por isso, esse tipo de display é adequado para interfaces que precisam ser atualizadas continuamente, como medidores, painéis de instrumentos, interfaces gráficas e, no nosso caso, um horizonte artificial.
Diferentemente de um display OLED, no qual os próprios pixels emitem luz, uma TFT LCD normalmente utiliza uma iluminação traseira, ou backlight. Os cristais líquidos controlam a quantidade de luz que atravessa cada pixel, enquanto filtros de cor formam os componentes vermelho, verde e azul da imagem.
Para este projeto, uma vantagem importante da TFT é a área disponível para desenhar o instrumento. O código utilizado trabalha com uma interface de 480 × 320 pixels, fornecendo espaço suficiente para representar o céu, o solo, a linha do horizonte, as marcações de Pitch e Roll e o símbolo central da aeronave.
Apesar de visualmente semelhantes, duas telas TFT de 3,5 polegadas podem utilizar circuitos controladores completamente diferentes.
O controlador funciona como um intermediário entre o ESP32 e a matriz de pixels.
Em vez de o microcontrolador controlar eletricamente cada pixel da tela de forma direta, ele envia comandos para esse circuito controlador. O controlador interpreta esses comandos e realiza as operações necessárias para modificar a imagem.
Existem diversos controladores utilizados em telas TFT, como:
Por esse motivo, apenas saber que uma tela possui 3,5 polegadas não é suficiente para determinar sua configuração.
É necessário identificar qual controlador está presente no módulo utilizado.
Isso é importante porque a biblioteca precisa saber qual conjunto de comandos deverá enviar para inicializar e controlar corretamente o display.
Uma configuração incorreta pode fazer com que a tela:
Em nosso projeto essa configuração será feita pela biblioteca TFT_eSPI.
Para controlar a tela utilizaremos a biblioteca TFT_eSPI, desenvolvida especialmente para microcontroladores como ESP32 e ESP8266 e otimizada para operações gráficas rápidas.
O código inclui a biblioteca através de:
#include <TFT_eSPI.h>
e cria o objeto responsável pelo display:
TFT_eSPI TFT_SCREEN = TFT_eSPI();
Essas definições já estão presentes no programa utilizado no projeto.
Uma característica importante da TFT_eSPI é que informações específicas do hardware não precisam ficar espalhadas pelo código principal.
A biblioteca mantém essas configurações separadas.
Entre elas estão:
Essa separação é interessante porque permite manter o programa do horizonte artificial praticamente independente do modelo físico da tela.
Em outras palavras, temos duas partes diferentes:
Programa do horizonte
↓
TFT_eSPI
↓
Configuração da tela
↓
Controlador da TFT
↓
Display
O código do horizonte apenas solicita operações como desenhar uma linha ou preencher uma região. É a biblioteca que transforma essas instruções nos comandos adequados ao controlador.
A documentação atual da TFT_eSPI também deixa claro que oUser_Setup.h concentra configurações como driver, pinos e método de comunicação da tela.
Antes de compilar o programa, precisamos instalar a biblioteca.
Na Arduino IDE:
TFT_eSPI;Depois disso, a biblioteca será adicionada à pasta de bibliotecas da Arduino IDE.
Entretanto, instalar a biblioteca não é suficiente.
Precisamos informar à TFT_eSPI qual display estamos utilizando.
Dentro da pasta da biblioteca existe um arquivo chamado:
User_Setup.h
Esse arquivo contém as principais configurações do display.
É nele que podemos selecionar o controlador correto e determinar os pinos utilizados para comunicação com o ESP32.
A própria biblioteca explica que, quando esse arquivo é configurado corretamente, os exemplos gráficos podem funcionar sem que cada sketch precise repetir toda a configuração de hardware.
No código que estamos utilizando também existe uma observação indicando que a configuração da TFT_eSPI deve ser realizada previamente através desse arquivo.
O primeiro passo é verificar qual controlador existe na sua TFT de 3,5 polegadas.
Depois disso, no User_Setup.h, deve permanecer habilitada apenas a definição correspondente ao controlador utilizado.
Por exemplo, a TFT_eSPI possui opções para controladores como:
#define ILI9341_DRIVER
#define ILI9486_DRIVER
#define ILI9488_DRIVER
#define ST7796_DRIVER
entre diversos outros.
A biblioteca atualmente possui suporte explícito para esses controladores.
Não devemos escolher uma dessas opções apenas pelo tamanho físico da tela. O correto é utilizar o driver correspondente ao controlador real do módulo.
Pode parecer estranho modificar um arquivo da biblioteca em vez de colocar todos os pinos no início do programa, mas essa é justamente a arquitetura adotada pela TFT_eSPI.
O programa pode continuar utilizando:
TFT_SCREEN.init();
sem precisar informar novamente o controlador e todos os pinos sempre que a tela é inicializada.
A biblioteca consulta sua configuração e realiza a inicialização adequada.
Isso é particularmente útil em um projeto como este, que já possui uma quantidade considerável de funções responsáveis por desenhar e movimentar o horizonte.
Assim, deixamos o código principal concentrado na lógica do instrumento e a configuração física da tela fica separada.
Antes de enviar o código completo do horizonte artificial, é recomendável verificar se a configuração da TFT está correta.
A biblioteca TFT_eSPI possui vários exemplos próprios.
Depois de instalar e configurar a biblioteca, podemos abrir um exemplo simples e verificar:
A biblioteca também possui arquivos de configuração de exemplo na pasta User_Setups, que podem servir como referência para diferentes combinações de microcontrolador e display.
Esse teste separado é útil porque permite descobrir problemas de configuração antes de envolver o MPU6050 e toda a lógica do horizonte artificial.
Se um exemplo básico da TFT não funcionar, o problema provavelmente está relacionado à tela, ao controlador, aos pinos ou à configuração da biblioteca, e não ao cálculo de Pitch e Roll.
No programa do horizonte artificial são utilizadas duas constantes para representar as dimensões da área gráfica:
const int _DX = 480;
const int _DY = 320;
Ou seja, o programa trabalha considerando uma área de 480 pixels na horizontal por 320 pixels na vertical.
A quantidade total de pixels disponíveis é:
480 × 320 = 153.600 pixels
Cada um desses pixels pode assumir uma determinada cor.
Essa resolução é importante porque várias dimensões do instrumento são calculadas proporcionalmente ao tamanho da tela.
Por exemplo, o centro do horizonte é definido utilizando metade das dimensões:
HA_x0 = _DX / 2;
HA_y0 = _DY / 2;
Portanto, para 480 × 320:
X = 240 pixels
Y = 160 pixels
Esse ponto funciona como referência para diversos elementos gráficos.
O programa também utiliza:
TFT_SCREEN.setRotation(3);
para definir a orientação da TFT.
A rotação é importante porque o mesmo display pode ser utilizado em modo retrato ou paisagem.
Para o horizonte artificial, utilizaremos a tela horizontalmente, permitindo aproveitar melhor sua largura para representar a linha do horizonte e as escalas laterais.
Outro conceito interessante utilizado no projeto é o RGB565.
Uma imagem colorida precisa representar diferentes intensidades de vermelho, verde e azul para cada pixel.
Entretanto, armazenar 8 bits completos para cada uma dessas cores exigiria 24 bits por pixel.
Em sistemas embarcados é comum utilizar um formato mais compacto.
No RGB565, cada pixel utiliza 16 bits:
RRRRR GGGGGG BBBBB
5 6 5
São:
Isso permite representar:
2¹⁶ = 65.536 cores
utilizando apenas dois bytes por pixel.
O código do horizonte trabalha com cores nesse formato e possui inclusive uma função de conversão para RGB565.
No nosso horizonte, isso permite definir diferentes regiões visuais.
Uma cor azul representa o céu, enquanto outra tonalidade representa o solo. Linhas, escalas e indicadores utilizam cores contrastantes para permanecer visíveis independentemente da posição do horizonte.
Talvez o maior desafio de utilizar a TFT neste projeto seja que o horizonte não é uma imagem estática.
O MPU6050 é atualizado continuamente.
Consequentemente, os ângulos também mudam continuamente e o desenho precisa acompanhar essas mudanças.
Imagine que o Roll passe de:
0°
para:
25°
A divisão entre céu e solo precisa girar.
Se o Pitch também mudar, além da rotação haverá deslocamento da imagem.
Isso significa que o ESP32 precisa realizar várias operações gráficas repetidamente.
Em uma implementação simples, poderíamos apagar toda a tela e desenhar novamente.
Porém, isso poderia produzir um efeito desagradável de piscar, conhecido como flicker, principalmente quando a atualização ocorre muitas vezes por segundo.
É por isso que o código utiliza técnicas um pouco mais elaboradas para atualizar apenas o necessário.
A biblioteca TFT_eSPI oferece o recurso chamado Sprite.
Um sprite pode ser entendido como uma pequena tela temporária criada na memória do ESP32.
Em vez de desenhar diretamente no display:
ESP32
↓
TFT
podemos fazer:
ESP32
↓
Sprite na memória
↓
Imagem pronta
↓
TFT
O código cria vários objetos desse tipo:
TFT_eSprite SPR_HA = TFT_eSprite(&TFT_SCREEN);
além de outros sprites utilizados pela interface.
O sprite SPR_HA é especialmente importante porque está relacionado à região móvel do horizonte.
O programa cria uma imagem contendo as regiões correspondentes ao céu e ao solo e define um ponto de pivô.
A partir desse pivô, a imagem pode ser rotacionada.
Esse mecanismo é muito útil para representar o Roll.
Quando o sensor é inclinado, não precisamos calcular individualmente uma nova posição para cada pixel do céu e do solo. Podemos rotacionar a região gráfica utilizando os recursos da biblioteca.
A posição da linha que separa céu e solo depende dos ângulos medidos pelo MPU6050.
O código determina um ponto de pivô e utiliza o sprite do horizonte para realizar a rotação da imagem.
Assim, conforme o valor de Roll aumenta ou diminui, a divisão entre céu e solo acompanha esse ângulo.
O Pitch também influencia a posição do pivô, fazendo com que o horizonte possa se deslocar enquanto gira.
Visualmente, temos algo parecido com:
MPU6050 inclina
↓
Roll muda
↓
ESP32 calcula nova posição
↓
Sprite é rotacionado
↓
TFT apresenta novo horizonte
Essa é uma das partes que tornam o projeto mais interessante do que simplesmente apresentar os valores numéricos dos sensores.
Estamos convertendo uma grandeza física diretamente em uma transformação gráfica.
O código também utiliza funções da TFT_eSPI como:
readRect()
e:
pushRect()
Essas funções permitem ler uma região da tela para um buffer e posteriormente escrevê-la novamente.
Isso pode ser utilizado para movimentar linhas ou indicadores sem precisar reconstruir toda a imagem existente atrás deles.
O programa mantém buffers de pixels de linhas horizontais e verticais exatamente para essa finalidade.
Em outras palavras, antes de desenhar um elemento móvel, o programa pode guardar o que havia naquela região.
Quando o elemento muda de posição, a imagem anterior é restaurada.
Isso evita deixar rastros na tela durante a movimentação.
Portanto, nesse projeto a TFT não funciona apenas como um dispositivo de saída simples.
Ela participa diretamente da representação da informação fornecida pelo MPU6050.
Todo o caminho pode ser resumido da seguinte maneira:
Movimento físico
↓
MPU6050
↓
Acelerômetro + giroscópio
↓
Cálculo da orientação
↓
Pitch e Roll
↓
Coordenadas e rotação
↓
Sprites e funções gráficas
↓
TFT LCD
↓
Horizonte artificial
Essa ligação entre sensor, processamento matemático e representação gráfica é justamente o principal objetivo do projeto.
A TFT transforma os valores abstratos de inclinação em uma informação que pode ser compreendida praticamente de forma instantânea, da mesma maneira que ocorre em um indicador de atitude convencional.
O circuito utiliza o ESP32 como unidade central, conectado à TFT LCD de 3,5" e ao MPU6050. A tela possui várias conexões porque, além dos pinos de alimentação, utiliza um barramento paralelo de 8 bits (LCD_D0 a LCD_D7) para transferência dos dados gráficos. Também são conectados os sinais de controle LCD_RD, LCD_WR, LCD_RS, LCD_CS e LCD_RST, responsáveis por comandar a comunicação e a atualização do display.
A shield também possui os pinos destinados ao cartão microSD (SD_SCK, SD_DO, SD_DI e SD_SS). Eles aparecem conectados no diagrama, embora o uso do cartão SD não seja necessário para o funcionamento básico do horizonte artificial.
O MPU6050 possui uma conexão bem mais simples. Ele é alimentado pelo ESP32 e se comunica através do barramento I²C, utilizando apenas as linhas SDA e SCL, além de VCC e GND. No projeto, os pinos SDA e SCL utilizados não são os padrões do ESP32, por isso devem seguir os GPIOs definidos no código.
Assim, o funcionamento do circuito pode ser resumido como:
MPU6050
│
│ I²C
▼
ESP32
│
│ Barramento paralelo + sinais de controle
▼
TFT LCD 3,5"
O MPU6050 envia os dados de orientação ao ESP32, que realiza o processamento e utiliza a TFT para apresentar graficamente o movimento do horizonte artificial.
Observação: como a TFT utiliza muitos GPIOs do ESP32, é importante seguir exatamente o esquema de ligação apresentado, principalmente nos pinos LCD_D0–D7 e nos sinais de controle.
A montagem é bem simples, sendo necessário apenas jumpers para fazer as conexões. No meu caso, para facilitar minha vida, resolvi usar uma PCB (placa de circuito impressa), mas o resultado final não se alterou, apenas a aparência do projeto que ficou mais limpa e apresentável.
Vamos agora ao cérebro do projeto. Com a TFT e o MPU6050 configurados, podemos partir para o código responsável pelo funcionamento do horizonte artificial. Apesar de relativamente extenso, o programa pode ser dividido em três partes principais: leitura da orientação pelo MPU6050, tratamento dos valores de Pitch e Roll e geração da interface gráfica na TFT.
/*
Horizonte artificial
*/
/* ******** IMPORTANTE SOBRE O DISPLAY TFT ************************
Adicionar ao arquivo User_Setup.h (em ~/Arduino/libraries/TFT_eSPI/):
#define ILI9341_2_DRIVER
****************************************************************************/
String version="3.0";
uint8_t fond_blanc = 0;
#include <stdint.h>
#include <TFT_eSPI.h> // Hardware-specific library
#include "SPI.h"
#include "Free_Fonts.h"
#include "FS.h"
#include "SD.h"
TFT_eSPI TFT_SCREEN = TFT_eSPI(); // Configure previamente o arquivo User_Setup.h da biblioteca TFT_eSPI
#include "Wire.h"
#include <MPU6050_light.h>
/*
Uma varredura do barramento I2C deve mostrar no monitor serial:
Scanning...
I2C device found at address 0x68
done
*/
//***************************** IMPORTANT !!!*******************************
// Descomente as 4 linhas correspondentes ao display utilizado e comente as outras 4
// Use também o arquivo User_Setup.h correto em ~/Arduino/libraries/TFT_eSPI/
//***************************************************************************
// ------------------------------
const int _DX = 480;
const int _DY = 320;
const int GPIO_SDA = 33;
const int GPIO_SCL = 32;
// ------------------------------
// ------------------------------
// const int _DX = 320
// const int _DY = 240
// const int GPIO_SDA = 27;
// const int GPIO_SCL = 22;
// ------------------------------
//***************************************************************************
// Armazena os pixels de 2 linhas horizontais e 2 linhas verticais
// Permite mover um retângulo sobre a imagem sem danificar o desenho anterior
uint16_t data_L1[_DX]; // pixels de uma linha horizontal
uint16_t data_L2[_DX]; // pixels de outra linha horizontal
uint16_t data_C1[_DY]; // pixels de uma linha vertical (C de coluna)
uint16_t data_C2[_DY]; // pixels de outra linha vertical
uint16_t x_1; // posição recebida do módulo positionneur_XY
uint16_t x_2; // posição recebida do módulo positionneur_XY
uint16_t y_1;
uint16_t y_2;
uint16_t memo_x1;
uint16_t memo_y1; // posição da linha
uint16_t memo_x2;
uint16_t memo_y2;
uint16_t memo_x_pivot;
uint16_t memo_y_pivot;
float AngleX;
float AngleY;
float AngleZ;
char var_array32[10];// 10 caracteres + terminador nulo para envio por WiFi (2^32 - 1 = 4294967295)
// =====================================================================
#define _pi 3.141592653
float raddeg =_pi/180.0;
float roulis;
float memo_roulis;
float R_2, T_2;
float memo_R_2, memo_T_2;
float tangage;
float memo_tangage;
float cap;
// ferramenta online útil para compor cores RGB565
// https://rgbcolorpicker.com/565
#define NOIR 0x0000
#define MARRON 0x3920
#define ROUGE 0xF800
#define ROSE 0xFBDD
#define ORANGE 0xFBC0
#define JAUNE 0xFFE0
#define JAUNE_PALE 0xF7F4
#define VERT 0x07E0
#define VERT_FONCE 0x02E2
#define OLIVE 0x05A3
#define CYAN 0x07FF
#define BLEU_CLAIR 0x455F
#define AZUR 0x1BF9
#define BLEU 0x001F
#define MAGENTA 0xF81F
#define VIOLET1 0x781A
#define VIOLET_2 0xECBE
#define GRIS_TRES_CLAIR 0xDEFB
#define GRIS_CLAIR 0xA534
#define GRIS 0x8410
#define GRIS_FONCE 0x5ACB
#define GRIS_TRES_FONCE 0x2124
#define BLANC 0xFFFF
#define GRIS_AF 0x51C5 // 0x3985
#define HA_CIEL 0x33FE
uint16_t HA_SOL; // definida no setup
// Width and height of sprite
#define SPR_W 25
#define SPR_H 16
uint16_t couleur_txt = BLANC;
uint16_t couleur_fond = GRIS_TRES_FONCE; //GRIS_TRES_FONCE;
uint16_t couleur_fond_txt = VERT_FONCE;
uint16_t couleur_fond_gradu = HA_CIEL;
TFT_eSprite SPR_HA = TFT_eSprite(&TFT_SCREEN);
TFT_eSprite SPR_10 = TFT_eSprite(&TFT_SCREEN);
TFT_eSprite SPR_10_ciel = TFT_eSprite(&TFT_SCREEN);
TFT_eSprite SPR_10_sol = TFT_eSprite(&TFT_SCREEN);
// posição e dimensões do horizonte artificial
uint16_t HA_x0 = _DX /2;
uint16_t HA_y0 = _DY /2;
uint16_t HA_w = _DX * 1.5; // mais largo que _DX para cobrir a diagonal nos cantos
#define HA_h 24
MPU6050 mpu(Wire);
//unsigned long timer = 0;
uint8_t flag_SDcardOk=0;
uint8_t flag_1er_passage =1;
uint8_t TEST_AFFI;
uint32_t compte=0;
float degTOrad(float angle)
{
return (angle * M_PI / 180.0);
}
uint8_t decToBcd( int val )
{
return (uint8_t) ((val / 10 * 16) + (val % 10));
}
uint16_t Color_To_565(uint8_t r, uint8_t g, uint8_t b)
{
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3);
}
void RGB565_to_888(uint16_t color565, uint8_t *R, uint8_t *G, uint8_t *B)
{
*R=(color565 & 0xFFFFF800) >> 8;
*G=(color565 & 0x7E0) >> 3;
*B=(color565 & 0x1F) << 3 ;
}
void init_SDcard()
{
String s1;
TFT_SCREEN.fillRect(0, 0, _DX, _DY, NOIR); // limpa
TFT_SCREEN.setTextColor(BLANC, NOIR);
TFT_SCREEN.setFreeFont(FF0);
uint16_t y=0;
y+=20;
s1="version " + version;
TFT_SCREEN.drawString(s1, 0, y);
y+=40;
TFT_SCREEN.setTextColor(VERT, NOIR);
TFT_SCREEN.drawString("Init SDcard", 0, y);
y+=20;
if(!SD.begin())
{
TFT_SCREEN.drawString("Card Mount Failed", 0, y);
delay (2000);
TFT_SCREEN.fillRect(0, 0, _DX, _DY, NOIR); // limpa
return;
}
uint8_t cardType = SD.cardType();
if(cardType == CARD_NONE)
{
TFT_SCREEN.drawString("No SDcard", 0, y);
delay (2000);
TFT_SCREEN.fillRect(0, 0, _DX, _DY, NOIR); // limpa
return;
}
flag_SDcardOk=1;
TFT_SCREEN.drawString("SDcard Type: ", 0, y);
if(cardType == CARD_SD) {TFT_SCREEN.drawString("SDSC", 150, y);}
else if(cardType == CARD_SDHC) {TFT_SCREEN.drawString("SDHC", 150, y);}
y+=20;
uint32_t cardSize = SD.cardSize() / (1024 * 1024);
s1=(String)cardSize + " GB";
TFT_SCREEN.drawString("SDcard size: ", 0, y);
TFT_SCREEN.drawString(s1, 150, y);
// listDir(SD, "/", 0);
//Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
//Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));
delay (1000);
TFT_SCREEN.fillRect(0, 0, _DX, _DY, NOIR); // limpa
}
/** -----------------------------------------------------------------------------------
CAPTURA DE TELA PARA O CARTÃO SD
/** ----------------------------------------------------------------------------------- */
void write_TFT_on_SDcard() // salva o arquivo .bmp
{
//TFT_SCREEN.setTextColor(VERT, NOIR);
//TFT_SCREEN.drawString("CP", 450, 300);
if (flag_SDcardOk==0) {return;}
String s1;
uint16_t ys=200;
TFT_SCREEN.setFreeFont(FF0);
TFT_SCREEN.setTextColor(JAUNE, NOIR);
uint16_t x, y;
uint16_t color565;
uint16_t bmp_color;
uint8_t R, G, B;
if( ! SD.exists("/bmp/capture2.bmp"))
{
TFT_SCREEN.fillRect(0, 0, _DX, _DY, NOIR); // limpa
TFT_SCREEN.setTextColor(ROUGE, NOIR);
TFT_SCREEN.drawString("NO /bmp/capture2.bmp !", 100, ys);
delay(300);
TFT_SCREEN.fillRect(100, ys, 220, 20, NOIR); // limpa
return;
}
File File1 = SD.open("/bmp/capture2.bmp", FILE_WRITE); // abre o arquivo binário vazio para escrita
if (File1)
{
/*
Imagens BMP888 em cores reais utilizam 24 bits por pixel.
São necessários 3 bytes para cada pixel, na ordem azul, verde e vermelho.
*/
uint16_t bmp_offset = 138;
File1.seek(bmp_offset);
TFT_SCREEN.setTextColor(VERT, NOIR);;
for (y = _DY; y>0; y--)
{
for (x=0; x < _DX; x++)
{
color565=TFT_SCREEN.readPixel(x, y);
RGB565_to_888(color565, &R, &G, &B);
File1.write(B); //G
File1.write(G); //R
File1.write(R); //B
}
s1=(String) (y/10);
TFT_SCREEN.fillRect(_DX -30, _DY -30, 20, 20, NOIR);
TFT_SCREEN.drawString(s1, _DY -30, 300);// exibe a contagem regressiva
}
File1.close(); // fecha o arquivo
TFT_SCREEN.fillRect(_DX -30, _DY -30, 20, 20, NOIR); // limpa a contagem regressiva
}
}
/** ----------------------------------------------------------------------------------- */
void Draw_arc_elliptique(uint16_t x0, uint16_t y0, int16_t dx, int16_t dy, float alpha1, float alpha2, uint16_t couleur)
// alpha1 e alpha2 em radianos
{
/*
OBSERVAÇÕES:
- Esta função também pode desenhar um arco de círculo ou um círculo completo quando dx = dy.
- dx e dy são do tipo int e podem ser negativos ou zero.
- alpha1 e alpha2 são os ângulos, em radianos, das extremidades do arco.
*/
uint16_t n;
float i;
float x,y;
i=alpha1;
while(i<alpha2)
{
x=x0+dx*cos(i);
y=y0+dy*cos(i+M_PI/2.0);
TFT_SCREEN.drawPixel(x,y, couleur);
i+=0.01; // radians
}
}
void affi_rayon2(uint16_t x0, uint16_t y0, float r1, float R_2, float angle_i, uint16_t couleur_i)
{
// desenha uma parte de um raio entre as distâncias r1 e R_2 a partir do centro
// angle_i em graus decimais, sentido trigonométrico
float angle = degTOrad(angle_i);
int16_t x1, x2;
int16_t y1, y2;
x1=x0+int16_t(r1* cos(angle));
y1=y0-int16_t(r1* sin(angle));
x2=x0+int16_t(R_2* cos(angle));
y2=y0-int16_t(R_2* sin(angle));
if ((x1>0) && (x2>0) && (y1>0) && (y2>0) && (x1<_DX) && (x2<_DX) && (y1<_DY) && (y2<_DY) )
{
TFT_SCREEN.drawLine(x1, y1, x2, y2, couleur_i);
}
}
void affi_pointe(uint16_t x0, uint16_t y0, uint16_t r, uint16_t dr, double angle_i, float taille, uint16_t couleur_i)
{
// desenha uma ponta de seta sobre um círculo de raio r
// angle_i em graus decimais, sentido trigonométrico
float angle = degTOrad(angle_i);
int16_t x1, x2, x3;
int16_t y1, y2, y3;
x1=x0+r* cos(angle); // ponta
y1=y0-r* sin(angle); // ponta
x2=x0+(r-dr)* cos(angle-taille); // base A
y2=y0-(r-dr)* sin(angle-taille); // base A
x3=x0+(r-dr)* cos(angle+taille); // base B
y3=y0-(r-dr)* sin(angle+taille); // base B
TFT_SCREEN.fillTriangle(x1, y1, x2, y2, x3, y3, couleur_i);
}
void affi_base(uint16_t x0, uint16_t y0, float r, float angle_i, float delta_angle_i, uint16_t couleur_i)
{
// desenha uma linha tangente a um círculo fictício de raio r
// angle_i em graus decimais, sentido trigonométrico
float angle =angle_i / 57.3; // (57.3 ~ 180/pi)
float delta_angle = delta_angle_i / 57.3;
int16_t x2, x3;
int16_t y2, y3;
x2=x0+ r * cos(angle-delta_angle); // x do ponto A da base do triângulo
y2=y0- r * sin(angle-delta_angle); // y
x3=x0+ r * cos(angle+delta_angle); // x do ponto B da base do triângulo
y3=y0- r * sin(angle+delta_angle); // y
TFT_SCREEN.drawLine(x2, y2, x3, y3, couleur_i);
}
void init_sprites()
{
SPR_HA.createSprite(HA_w, HA_h);
SPR_HA.setPivot(HA_w/2, HA_h/2);
SPR_HA.fillSprite(NOIR); // para teste de rotação --> AZUL
SPR_HA.fillRect(0, 0, HA_w, HA_h/2, HA_CIEL);
SPR_HA.fillRect(0, HA_h/2, HA_w, HA_h/2, HA_SOL);
// sprite que representa o número 10 sobre fundo preto
SPR_10.createSprite(SPR_W, SPR_H);
SPR_10.setFreeFont(FF0); // FF5
SPR_10_ciel.setTextColor(BLANC, NOIR);
SPR_10.fillSprite(NOIR);
SPR_10.drawString("10", 2, 2 );
SPR_10.setPivot(SPR_W/2, SPR_H/2); // Set pivot relative to top left corner of Sprite
// sprite que representa o número 10 sobre fundo azul
SPR_10_ciel.createSprite(SPR_W, SPR_H);
SPR_10_ciel.setFreeFont(FF0); // FF5
SPR_10_ciel.setTextColor(BLANC, HA_CIEL);
SPR_10_ciel.fillSprite(HA_CIEL);
SPR_10_ciel.setPivot(SPR_W/2, SPR_H/2); // Set pivot relative to top left corner of Sprite
SPR_10_ciel.drawString("10", 2, 2 );
// sprite que representa o número 10 sobre fundo marrom
SPR_10_sol.createSprite(SPR_W, SPR_H);
SPR_10_sol.setFreeFont(FF0); // FF5
SPR_10_ciel.setTextColor(BLANC, HA_SOL);
SPR_10_sol.fillSprite(HA_SOL);
SPR_10_sol.setPivot(SPR_W/2, SPR_H/2); // Set pivot relative to top left corner of Sprite
SPR_10_sol.drawString("10", 2, 2 );
}
void affi_HA(float R_in, float T_in) // Display de navegação (círculo principal com os diferentes elementos gráficos)
{
//float angle1;
int16_t x_pivot, y_pivot;
int16_t x0= _DX/2;
int16_t y0= _DY/2;
x_pivot = x0 + T_in * sin(degTOrad(R_in));
y_pivot = y0 + T_in * cos(degTOrad(R_in));
TFT_SCREEN.setPivot(x_pivot, y_pivot);
SPR_HA.pushRotated(-R_in); // exibe a linha de separação entre céu e solo
dessine_avion();
// graduações
for (int n=1; n<=10; n++)
{
float r = 10.0;
float ech = (float)_DX/480.0; // resulta em 1.0 para display 320x480 e 0.6 para display 240x320
float delta_angle = 10.0;
if (n==1) {delta_angle = 30.0; r = -20 * ech;}
if (n==2) {delta_angle = 30.0; r = -40 * ech;}
if (n==3) {delta_angle = 10.0; r = -53 * ech;}
if (n==4) {delta_angle = 30.0; r = -80 * ech;}
if (n==5) {delta_angle = 7.0; r = -90 * ech;}
if (n==6) {delta_angle = 30.0; r = 20 * ech;}
if (n==7) {delta_angle = 30.0; r = 40 * ech;}
if (n==8) {delta_angle = 10.0; r = 53 * ech;}
if (n==9) {delta_angle = 30.0; r = 80 * ech;}
if (n==10){delta_angle = 7.0; r = 90 * ech;}
if (r > T_in) {couleur_fond_gradu = HA_SOL;} else {couleur_fond_gradu = HA_CIEL;}
//TFT_SCREEN.fillRect(452, 90+r, 5, 5, couleur_fond_gradu); // para teste
affi_base(x0, y0, -r, memo_roulis + 90, delta_angle, couleur_fond_gradu); // limpa
affi_base(x0, y0, -r, R_in + 90, delta_angle, BLANC); // desenha
}
// exibe a marcação 10 na escala
x_pivot = x0 - 60 * sin(degTOrad(R_2-38)); // 90
y_pivot = y0 - 60 * cos(degTOrad(R_2-38));
uint16_t couleur1;
int16_t limite = -70;
if (T_2 < limite){couleur1 = HA_SOL;} else {couleur1 = HA_CIEL;}
TFT_SCREEN.fillRect(memo_x_pivot-16, memo_y_pivot-12, 32, 22, couleur1); // limpa
TFT_SCREEN.setPivot(x_pivot, y_pivot);
SPR_10.pushRotated(-R_2, NOIR);
//dessine_avion();
memo_x_pivot = x_pivot;
memo_y_pivot = y_pivot;
//void affi_pointe(uint16_t x0, uint16_t y0, uint16_t r, uint16_t dr, double angle_i, float taille, uint16_t couleur_i)
affi_pointe(_DX/2, _DY/2, _DY/2 -40, 12, memo_roulis+90, 0.05, HA_CIEL);
affi_pointe(_DX/2, _DY/2, _DY/2 -40, 12, R_in+90, 0.05, BLANC);
float alpha;
uint8_t z;
// graduações 10 20 30 45 60
affi_graduation_fixe();
memo_roulis = R_in;
}
void dessine_avion() // formado por cantos horizontais pretos contornados de branco
{
// asa esquerda
TFT_SCREEN.fillRect(HA_x0-102, HA_y0-3, 60, 10, BLANC); // H com contorno branco
TFT_SCREEN.fillRect(HA_x0-42, HA_y0-3, 10, 19, BLANC); //V
TFT_SCREEN.fillRect(HA_x0-100, HA_y0-1, 60, 5, NOIR); //H
TFT_SCREEN.fillRect(HA_x0-40, HA_y0-1, 5, 15, NOIR); //V
// asa direita
TFT_SCREEN.fillRect(HA_x0+28, HA_y0-3, 64, 10, BLANC); // H com contorno branco
TFT_SCREEN.fillRect(HA_x0+28, HA_y0-3, 10, 19, BLANC); //V
TFT_SCREEN.fillRect(HA_x0+30, HA_y0-1, 60, 5, NOIR); //H
TFT_SCREEN.fillRect(HA_x0+30, HA_y0-1, 5, 15, NOIR); //V
// quadrado branco no centro
TFT_SCREEN.fillRect(HA_x0-4, HA_y0-3, 8, 2, BLANC);
TFT_SCREEN.fillRect(HA_x0-4, HA_y0-3, 2, 8, BLANC);
TFT_SCREEN.fillRect(HA_x0-4, HA_y0+3, 10, 2, BLANC);
TFT_SCREEN.fillRect(HA_x0+4, HA_y0-3, 2, 8, BLANC);
}
void affi_ligne1_V(uint16_t x)
{
/** DOC: (source : "TFT_eSPI.h")
// The next functions can be used as a pair to copy screen blocks (or horizontal/vertical lines) to another location
// Read a block of pixels to a data buffer, buffer is 16 bit and the size must be at least w * h
void readRect(int32_t x, int32_t y, int32_t w, int32_t h, uint16_t *data);
// Write a block of pixels to the screen which have been read by readRect()
void pushRect(int32_t x, int32_t y, int32_t w, int32_t h, uint16_t *data);
**/
TFT_SCREEN.pushRect(memo_x1, 0, 1, _DY, data_C1); // apaga a linha restaurando a imagem anterior
memo_x1=x;
TFT_SCREEN.readRect(x, 0, 1, _DY, data_C1); // armazena a linha antes de desenhar sobre ela
//TFT_SCREEN.drawFastVLine(x, 0, _DY, ROUGE);
TFT_SCREEN.drawFastVLine(x, y_1, y_2-y_1, JAUNE);
}
void affi_ligne2_V(uint16_t x)
{
TFT_SCREEN.pushRect(memo_x2, 0, 1, _DY, data_C2); // apaga a linha restaurando a imagem anterior
memo_x2=x;
TFT_SCREEN.readRect(x, 0, 1, _DY, data_C2); // armazena a linha antes de desenhar sobre ela
//TFT_SCREEN.drawFastVLine(x, 0, _DY, ROUGE);
TFT_SCREEN.drawFastVLine(x, y_1, y_2-y_1, JAUNE);
}
void affi_ligne1_H(uint16_t y)
{
TFT_SCREEN.pushRect(0, memo_y1, _DX, 1, data_L1); // apaga a linha restaurando a imagem anterior
memo_y1=y;
TFT_SCREEN.readRect(0, y, _DX, 1, data_L1); // armazena a linha antes de desenhar sobre ela
//TFT_SCREEN.drawFastHLine(0, y, 480, ROUGE);
TFT_SCREEN.drawFastHLine(x_1, y, x_2-x_1, JAUNE);
}
void affi_ligne2_H(uint16_t y)
{
TFT_SCREEN.pushRect(0, memo_y2, _DX, 1, data_L2); // apaga a linha restaurando a imagem anterior
memo_y2=y;
TFT_SCREEN.readRect(0, y, _DX, 1, data_L2); // armazena a linha antes de desenhar sobre ela
//TFT_SCREEN.drawFastHLine(0, y, 480, ROUGE);
TFT_SCREEN.drawFastHLine(x_1, y, x_2-x_1, JAUNE);
}
void setup()
{
Serial.begin(115200);
Wire.begin(GPIO_SDA, GPIO_SCL, 100000); // OK (source: https://randomnerdtutorials.com/esp32-i2c-communication-arduino-ide/ )
// portanto, conecte o MPU6050 via I2C aos GPIOs indicados na configuração acima
Serial.println("display.init()");
if(_DX == 480) {HA_SOL = 0xAA81;}
if(_DX == 320) {HA_SOL = 0x3920;}
TFT_SCREEN.init();
TFT_SCREEN.setRotation(3); // valores de 0 a 3, conforme a orientação física do display
TFT_SCREEN.fillScreen(NOIR);
TFT_SCREEN.setTextColor(BLANC, NOIR);
TFT_SCREEN.setFreeFont(FF0);
uint16_t y=0;
Serial.println("Horizon artificiel");
delay(300);
TFT_SCREEN.drawString("Horizon artificiel", 0, y);
y+=20;
String s1="version " + version;
TFT_SCREEN.drawString(s1, 0, y);
y+=20;
delay(300);
byte status = mpu.begin();
s1="MPU6050 status:" + String(status);
TFT_SCREEN.setTextColor(JAUNE, NOIR);
TFT_SCREEN.drawString(s1, 0, y);
y+=20;
////while(status!=0){ } // stop everything if could not connect to MPU6050
TFT_SCREEN.setTextColor(BLANC, NOIR);
s1="Calcul offsets, do not move MPU6050";
Serial.println(s1);
TFT_SCREEN.drawString(s1, 0, y);
y+=20;
delay(1000);
// mpu.upsideDownMounting = true; // uncomment this line if the MPU6050 is mounted upside-down
mpu.calcOffsets(); // gyro and acceler.
TFT_SCREEN.setTextColor(VERT, NOIR);
s1="OK!\n";
TFT_SCREEN.drawString(s1, 0, y);
y+=20;
delay(1000);
TFT_SCREEN.setTextColor(TFT_BLUE, TFT_BLACK);
//TFT_SCREEN.setTextFont(1);
TFT_SCREEN.setCursor(0, 40, 1);
TFT_SCREEN.fillScreen(couleur_fond);
//init_SDcard();
init_sprites();
TFT_SCREEN.fillRect(0, 0, _DX, _DY/2, HA_CIEL);
TFT_SCREEN.fillRect(0, _DY/2, _DX, _DY, HA_SOL);
R_2=0;
T_2=0;
affi_HA(R_2,T_2);
x_1=0;
y_1=0;
mpu.update();
tangage = 0;
roulis = 0;
TEST_AFFI=0;
for(int n=0; n<200; n++)
{
mpu.update(); // estabiliza as primeiras leituras
}
///delay(1000);
///write_TFT_on_SDcard();
Serial.println("fin du setup");
}
void affi_graduation_fixe() // seguindo um arco de círculo na parte superior da tela
{
int16_t x0= _DX /2;
int16_t y0= _DY /2;
affi_rayon2(x0, y0, _DY/2 -35, _DY/2 -20, 90, BLANC);
affi_rayon2(x0, y0, _DY/2 -35, _DY/2 -30, 90-10, BLANC);
affi_rayon2(x0, y0, _DY/2 -35, _DY/2 -30, 90-20, BLANC);
affi_rayon2(x0, y0, _DY/2 -35, _DY/2 -20, 90-30, BLANC);
affi_rayon2(x0, y0, _DY/2 -35, _DY/2 -20, 90-45, BLANC);
affi_rayon2(x0, y0, _DY/2 -35, _DY/2 -20, 90-60, BLANC);
affi_rayon2(x0, y0, _DY/2 -35, _DY/2 -30, 90+10, BLANC);
affi_rayon2(x0, y0, _DY/2 -35, _DY/2 -30, 90+20, BLANC);
affi_rayon2(x0, y0, _DY/2 -35, _DY/2 -20, 90+30, BLANC);
affi_rayon2(x0, y0, _DY/2 -35, _DY/2 -20, 90+45, BLANC);
affi_rayon2(x0, y0, _DY/2 -35, _DY/2 -20, 90+60, BLANC);
TFT_SCREEN.setFreeFont(FF0);
TFT_SCREEN.setTextColor(BLANC, HA_CIEL);
TFT_SCREEN.drawString("30", x0+50, 23);
TFT_SCREEN.drawString("30", x0-60, 23);
TFT_SCREEN.drawString("60", x0+100, 60); // 125
TFT_SCREEN.drawString("60", x0-110, 60); // 150
}
void affichages()
{
// as linhas seguintes fazem a rotação avançar passo a passo
//if (R_2<roulis-10) { affi_HA(R_2, T_2); R_2+=2.5; }
if (R_2<roulis-3) { affi_HA(R_2, T_2); R_2+=2; }
else if (R_2<roulis) { affi_HA(R_2, T_2); R_2++; }
//if (R_2>roulis+10) { affi_HA(R_2, T_2); R_2-=2.5; }
if (R_2>roulis+3) { affi_HA(R_2, T_2); R_2-=2; }
else if (R_2>roulis) { affi_HA(R_2, T_2); R_2--; }
if (T_2<tangage-3) { affi_HA(R_2, T_2); T_2+=2; }
else if (T_2<tangage) { affi_HA(R_2, T_2); T_2++; }
if (T_2>tangage+3) { affi_HA(R_2, T_2); T_2-=2; }
else if (T_2>tangage) { affi_HA(R_2, T_2); T_2--;}
}
float t=0;
float dt=2;
void loop()
{
if(TEST_AFFI==1) // use 1 para testar apenas a exibição gráfica
{
roulis = t/2;
tangage = 0; // 60.0*sin(t/134);
//roulis= 25.0;
//tangage = 0.0;
affichages();
t += dt; // 3*dt ...
if ((t==60)||(t== -60)) {dt = -dt;}
}
else
{
mpu.update();
AngleX=mpu.getAngleX();
AngleY=mpu.getAngleY();
AngleZ=mpu.getAngleZ();
//timer = millis();
tangage = -5.0*AngleX;
roulis = -1.0*AngleY;
affichages();
compte++;
delay(1);
// if (compte==100) {write_TFT_on_SDcard();}
}
}
Logo no início são carregadas as bibliotecas necessárias para controlar a tela, utilizar o barramento I²C e se comunicar com o MPU6050. Entre elas estão TFT_eSPI, SPI, Wire e MPU6050_light.
O objeto responsável pela tela é criado através de:
TFT_eSPI TFT_SCREEN = TFT_eSPI();
enquanto o MPU6050 é associado ao barramento I²C por:
MPU6050 mpu(Wire);
Além das funções normais de desenho da TFT_eSPI, o código utiliza sprites, que permitem criar pequenas áreas gráficas na memória antes de transferi-las para a tela. O sprite SPR_HA, por exemplo, contém a região que representa a divisão entre céu e solo e posteriormente pode ser deslocado e rotacionado conforme a atitude medida.
A função init_sprites() é responsável pela criação desses elementos gráficos. Nela são definidas as regiões do céu e do solo, além de sprites utilizados nas marcações do instrumento.
Já a função affi_HA() concentra uma parte importante da representação gráfica. Ela recebe dois valores correspondentes à orientação, calcula o novo ponto de pivô e utiliza pushRotated() para movimentar o horizonte. Dessa maneira, o mesmo elemento gráfico pode ser simultaneamente deslocado pelo Pitch e rotacionado pelo Roll.
No centro da tela permanece fixa uma representação da aeronave, construída com retângulos e linhas. Como o símbolo não acompanha o movimento do horizonte, torna-se fácil visualizar a posição relativa entre a aeronave e a linha que divide céu e solo.
Dentro de setup(), primeiro são iniciadas a comunicação serial e a comunicação I²C:
Serial.begin(115200);
Wire.begin(GPIO_SDA, GPIO_SCL, 100000);
Em seguida, o display é inicializado e colocado na orientação escolhida:
TFT_SCREEN.init();
TFT_SCREEN.setRotation(3);
Depois é a vez do MPU6050. O programa verifica a comunicação com:
byte status = mpu.begin();
e realiza a calibração dos offsets:
mpu.calcOffsets();
Durante essa etapa, o sensor deve permanecer parado para evitar que um movimento seja interpretado como erro do acelerômetro ou do giroscópio.
Depois da calibração, os sprites são criados e a primeira imagem do horizonte é desenhada.
A operação normal ocorre dentro de loop().
A função:
mpu.update();
atualiza os dados da IMU. Em seguida, são obtidos os três ângulos:
AngleX = mpu.getAngleX();
AngleY = mpu.getAngleY();
AngleZ = mpu.getAngleZ();
Neste projeto, os ângulos X e Y são utilizados para controlar o horizonte:
tangage = -5.0 * AngleX;
roulis = -1.0 * AngleY;
roulis corresponde ao Roll, enquanto tangage corresponde ao Pitch. O fator -5.0 aplicado ao Pitch não significa que o sensor esteja multiplicando fisicamente a inclinação por cinco. Ele funciona como uma escala gráfica, convertendo a variação angular em um deslocamento suficientemente grande para ser perceptível na tela.
Por fim, a função:
affichages();
atualiza a representação gráfica.
Em vez de fazer a imagem saltar diretamente de um ângulo para outro, essa função modifica progressivamente os valores intermediários, criando uma movimentação mais suave do horizonte.
Existem alguns detalhes no código que merecem atenção e podem ser fonte de dúvidas.
A biblioteca TFT_eSPI precisa estar configurada corretamente através do User_Setup.h antes da compilação. O próprio código chama atenção para isso logo no início.
Se o controlador ou os pinos estiverem configurados incorretamente, o programa pode compilar normalmente e ainda assim a tela permanecer branca ou apresentar uma imagem incorreta.
A configuração ativa do código utiliza:
const int _DX = 480;
const int _DY = 320;
Isso deve estar de acordo com a resolução e a orientação utilizadas na TFT, pois grande parte da geometria do instrumento depende dessas duas constantes.
Na configuração ativa encontramos:
GPIO_SDA = 33;
GPIO_SCL = 32;
Para evitar confusão durante a montagem, devem ser considerados os valores efetivamente definidos nas constantes utilizadas pelo código.
Existe uma variável chamada:
TEST_AFFI
No setup(), ela recebe o valor 0.
Com TEST_AFFI == 0, o horizonte utiliza os dados reais do MPU6050.
Caso seja alterada para 1, o programa entra em um modo de teste gráfico e passa a gerar artificialmente valores para movimentar o horizonte. Isso pode ser útil para verificar a tela mesmo sem movimentar o sensor.
O código também possui funções para utilizar um cartão SD e até salvar uma captura da TFT em formato BMP.
Entretanto, no funcionamento normal do horizonte essa parte está desativada:
//init_SDcard();
Portanto, o cartão SD não é necessário para reproduzir o projeto principal.
Após a montagem do circuito, configuração das bibliotecas e envio do código para o ESP32, o horizonte artificial passa a responder aos movimentos detectados pelo MPU6050.
Ao inclinar o sensor lateralmente, o valor de Roll é alterado e a representação do horizonte acompanha esse movimento através da rotação da divisão entre céu e solo. Já ao inclinar o conjunto para frente ou para trás, o Pitch provoca o deslocamento vertical do horizonte.
A imagem abaixo mostra o projeto já montado e em funcionamento.

Durante o funcionamento, o símbolo que representa a aeronave permanece fixo no centro da tela, enquanto o horizonte e suas marcações se movimentam ao redor dele. Essa escolha facilita a interpretação da atitude, seguindo o mesmo princípio visual de um horizonte artificial aeronáutico.
No vídeo a seguir é possível observar melhor a resposta do sistema às mudanças de inclinação.
Um detalhe perceptível durante os testes é que o movimento apresentado na TFT não ocorre simplesmente por saltos entre uma posição e outra. O próprio código atualiza gradualmente os valores utilizados para desenhar o horizonte, produzindo uma transição visual mais suave.
A leitura do MPU6050 também é atualizada continuamente dentro do loop(). Os ângulos dos eixos X e Y são utilizados para determinar respectivamente o deslocamento de Pitch e a rotação de Roll apresentados na tela.
O resultado final demonstra como as informações obtidas por uma IMU podem deixar de ser apenas valores numéricos e ser transformadas em uma interface gráfica de interpretação imediata. Além da aplicação apresentada, o mesmo princípio pode ser aproveitado em projetos como plataformas estabilizadas, robôs, aeromodelos e sistemas de monitoramento de orientação.
É importante lembrar que este projeto possui finalidade experimental e educacional. Apesar de reproduzir visualmente o funcionamento de um indicador de atitude, não se trata de um instrumento aeronáutico certificado.
|
Aprenda a criar um horizonte artificial com ESP32, MPU6050 e display TFT LCD de 3,5″. O projeto mostra como ler os dados do acelerômetro e giroscópio, processar Pitch e Roll e representar a orientação em tempo real na tela.
Encontre tudo na Loja Eletrogate com frete grátis para compras acima de R$ 200