Grafica deslizante para presentar datos en el TFT
Aprovechando la velocidad del teensy 4 (nada menos que 600 mHz!!! en condiciones normales, con posibilidad de 1 GHz con un disipador y un ventilador adecuado)
Visualizar datos en un TFT en el entorno arduino resulta algo complejo de conseguir con las pantallas basadas en librerías estilo Adafruit/GFX: ILI9341, ILI9488, ILI9325, ST7735, etc. Como no disponen de memoria dedicada, todo el trabajo lo realiza el MCU. Es posible agregar algún shield basado en chips F103X, con la idea de que gestione los gráficos, pero la electrónica requerida nos queda fuera de alcance. Existen algunos ejemplos en la red pero es tal la cantidad de subrutinas y de código C rebuscado, que tan solo tratar de aislar el marco de la gráfica es todo un triunfo.
Después de muchos intentos y luego de la llegada de estas diminutas ST7735 de 1.8" táctiles (sigo sin salir del asombro!), decidí retomar el tema.
La aproximación la conseguí con la primitiva más simple: borrar y luego dibujar un pixel. Para borrar un pixel en una determinada posición en el TFT, lo podemos conseguir colocando en esa posición un pixel con el color del fondo.
Otro concepto tiene que ver con los arrays de datos. Podemos usar un array para almacenar los datos actuales de algún sensor y en otro array podemos almacenar los datos previos de ese sensor. Aquí está la clave: los datos previos serán el conjunto de pixeles que al dibujarlos con el color del fondo, nos ayudarán a borrar los pixeles. Aquí todo muy bien como concepto, pero escribirlo en el código podría parecer complejo...
Al escribir el código traté de mantenerlo de forma modular, lo mas simple posible.
Primero debemos dibujar el marco para graficar. Esta función es fija
//posición X, posición Y, número de marcadores en X, número de marcadores en Y, color del marco
void MarcoG(int XM, int YM, int NDX, int NDY, uint16_t color)
{
int Pitch = 10; //espaciado fijo de 10 pixeles
//Marco
tft.drawRect(XM, YM, Pitch*NDX+1, Pitch*NDY+1, color);
//Divisores eje X
int altoMarcaX = 4; //Número de pixeles de cada marcador
for (int i=XM; i<=XM+Pitch*NDX; i+=Pitch)
tft.drawLine(i, YM+Pitch*NDY, i, YM+Pitch*NDY+altoMarcaX, color);
// Divisores eje Y
int largoMarcaY = altoMarcaX;
for (int i=YM; i<=YM+Pitch*NDY+1; i+=Pitch)
tft.drawLine((XM-largoMarcaY), i, (XM-1), i, color);
}
Para fines prácticos, el marco estará formado por arreglos cuadrados de 10 pixeles por lado, por lo que cada celda representa 10 unidades en el eje X y 10 en el eje Y.
En el eje X se representa el número de datos y el en eje Y los datos del sensor, normalizados a marcadores de 10 unidades
Antes de dibujar algún pixel, debemos tener datos como línea base. Esta función es fija
void lineabaseG1(int BaseD)
{
for (int i=0; i<maxlecturas; i++)
{
//AdquiereDatosG1();
lecturaG1[i] = BaseD;
}
}
Para efectos prácticos, el array principal lo designé como lecturaG1[], almacenará los datos actuales y los datos previos estarán en lecturapreviaG1[], la línea base llena el array de datos actuales con algún valor dentro del rango de los datos del sensor.
Los datos los obtendremos con la función AdquiereDatosG1. En ella podemos colocar la rutina con la que adquirimos datos del sensor, en este caso coloqué alguna función simple para simular un termómetro con escala Celcius, con lecturas en el rango de 1 a 25. Cabe señalar que se debe agregar una operación de mapeo, escalado o factorización, con la finalidad de obtener valores que se puedan dibujar en el espacio de pixeles del TFT.
Si el sensor arroja valores en un rango de 0 a 10000, podemos dividir los datos del sensor entre 100, para que la lectura se adapte a la escala de la pantalla. En este ejemplo los valores corresponden 1 a 1. Esta función puede ser adaptada por el usuario
float velTCG1 = 1;
void AdquiereDatosG1()
{
//TempCG1=random(20,30);
TempCG1= TempCG1 + velTCG1;
if (TempCG1>=25){velTCG1=-1;}
if (TempCG1<=1){velTCG1=1;}
if (TempCG1<=0){TempCG1=0;}
LecturaTAG1 = TempCG1;
// Truco para conservar 2 decimales en la presentación de la temperatura
//TAG1=TempCG1*100;
TAG1=TempCG1*1;
}
Para efectos prácticos, TempCG1 es el dato actual del sensor, LecturaTAG1 y TAG1, son valores de respaldo que podemos manipular para efectos de presentación en pantalla, como señalamos previamente, nos permitirán escalar los valores del sensor para poder manejarlos dentro del marco de la gráfica, además nos permitirán conservar valores decimales por ejemplo para la impresión de datos en pantalla.
La rutina que da vida a la gráfica continua de datos es esta (función fija)
long previousMillisG1 = 0;
long intervalG1 = 100; //7000
int jG1; // contador para recorrer los datos de la lista actual
void LineaDatosG1(int xinicial, int ybase, int NDX, int NDY, const int maxlecturas, uint16_t color) //permite ajustar el numero máximo de datos de las bases lecturaprevia y lectura
{
int escala = 1;
int yDatoTXT = ybase;
ybase = ybase + 10*NDY;
unsigned long currentMillisG1= micros();
if(currentMillisG1 - previousMillisG1 > intervalG1)
{
previousMillisG1 = currentMillisG1;
lecturapreviaG1[jG1]=lecturaG1[jG1]; // almacena el dato actual
// Recorre una posición. Los datos de la lista se recorren de adelante hacia atrás, para dejar libre el último espacio de la lista
lecturaG1[jG1] = lecturaG1[jG1+1];
tft.drawPixel(jG1+xinicial, ybase-escala*lecturapreviaG1[jG1], ST77XX_BLUE); //borra el pixel previo
tft.drawPixel(jG1+xinicial, ybase-escala*lecturaG1[jG1], color); //gráfica el pixel nuevo
jG1++;
// Continua recorriendo los datos hasta llegar al último de la lista, en el que se colocará la nueva lectura de datos
if (jG1==maxlecturas-1)
{
AdquiereDatosG1();
lecturapreviaG1[jG1]=lecturaG1[jG1];
lecturaG1[jG1] = TempCG1;
tft.drawPixel(jG1+xinicial, ybase-escala*lecturapreviaG1[jG1], ST77XX_BLUE); //borra el pixel previo
tft.drawPixel(jG1+xinicial, ybase-escala*lecturaG1[jG1], color); //gráfica el pixel nuevo
tft.setCursor(xinicial+1, yDatoTXT+2); tft.setTextColor(color, ST77XX_BLUE); sprintf(TXP,"%2d", TAG1); tft.println(TXP);
jG1=0;
}
}
}
Se aceptan sugerencias de mejora.
Solo falta: lo que va en el encabezado, la parte modificable por el usuario de la gráfica dentro del sketch y la parte de presentación de la gráfica en el TFT.
Encabezado del sketch
En el encabezado para cada sensor debemos agregar estas líneas, el ejemplo muestra como se pueden manejar dos sensores de temperatura
char TXP[50];
const int maxlecturas = 139; // 139 // Variable que permite manipular el tamaño de todos los arrays de datos, esta relacinado con el largo del eje X, provisional
//sensor 1
float TempCG1 =20; // dato decimal
int TAG1, LecturaTAG1; // para manejar los decimales (experimental no usado aún
float lecturaG1[maxlecturas]; // base de datos actual
float lecturapreviaG1[maxlecturas]; // base de datos previa
//sensor 1
//sensor 2
float TempCG2 =20; // dato decimal
int TAG2, LecturaTAG2; // para manejar los decimales (experimental no usado aún
float lecturaG2[maxlecturas]; // base de datos actual
float lecturapreviaG2[maxlecturas]; // base de datos previa
//sensor 2
Para agregar un tercer sensor bastaría agregar estas líneas:
//sensor 3
float TempCG3 =20; // dato decimal
int TAG3, LecturaTAG3; // para manejar los decimales (experimental no usado aún
float lecturaG3[maxlecturas]; // base de datos actual
float lecturapreviaG3[maxlecturas]; // base de datos previa
//sensor 3
Las funciones que vimos:
void LineaDatosG1()
void lineabaseG1()
void AdquiereDatosG1()
Deben copiarse y modificarse para cada sensor que agreguemos, solo hay que sustituir el índice al final del nombre. En el interior de las funciones también se debe modificar el índice en cada variable.
Mas adelante podría ser factible simplificar a solo tres funciones para n sensores, sin tener que copiar, pero aun es un trabajo en progreso, posiblemente involucrará un array para el ID de los sensores.