fbpixel
Etiquetas: ,
0
(0)

La Shield TFT se suele suministrar con un módulo de tarjeta SD para almacenar datos o imágenes. Pantallas LCD táctiles para la visualización de imágenes y la creación de interfaces gráficas de usuario. En este tutorial utilizamos el shield Kuman TFT de 2,8″ (muy similar al shield de 3,5″) y veremos cómo interactuar con la tarjeta microSD.

N.B.: Aunque el escudo es compatible con la placa Arduino Mega, el módulo SD no se puede utilizar directamente.

Material

  • Ordenador
  • Arduino UNO
  • Cable USB A macho
  • Pantalla LCD TFT con módulo SD
  • Carte microSD contenant une image bitmap au dimensions de l’écran

Esquema

El escudo se coloca directamente en una placa Arduino UNO o Mega. El escudo utiliza casi todos los pines del Arduino UNO. Asegúrese de no utilizar los mismos para otros módulos. El módulo de tarjeta SD del escudo TFT utiliza el bus SPI y selecciona el pin 10.

Código

Para utilizar el objeto TFT shield y en particular la comunicación con la tarjeta SD, utilizamos las librerías SD.h. En este ejemplo, recuperaremos la lista de archivos contenidos en la tarjeta SD y la mostraremos en la pantalla LCD

//Libraries
#include <SD.h>//https:
#include <Adafruit_GFX.h>//https://github.com/adafruit/Adafruit-GFX-Library
#include <MCUFRIEND_kbv.h>//https://github.com/prenticedavid/MCUFRIEND_kbv
#include <TouchScreen.h> //https://github.com/adafruit/Adafruit_TouchScreen
//Constants
#define SD_CS 10
#define BLACK 0
#define GREY 21845
#define BLUE 31
#define RED 63488
#define GREEN 2016
#define DARKGREEN 1472
#define CYAN 2047
#define MAGENTA 63519
#define YELLOW 65504
#define GOLD 56768
#define WHITE 65535
//Touch screen configuration
#define MINPRESSURE 200
#define MAXPRESSURE 1000
// ALL Touch panels and wiring is DIFFERENT
// copy-paste results from TouchScreen_Calibr_native.ino
//3.5 Parameters
//const int XP = 8, XM = A2, YP = A3, YM = 9; //320x480 ID=0x9486
//const int TS_LEFT = 144, TS_RT = 887, TS_TOP = 936, TS_BOT = 87;
//2.8 Parameters
const int XP = 8, XM = A2, YP = A3, YM = 9; //240x320 ID=0x9341
const int TS_LEFT = 907, TS_RT = 120, TS_TOP = 74, TS_BOT = 913;
TouchScreen ts = TouchScreen(XP, YP, XM, YM, 300);
TSPoint p;
bool down;
int pixel_x, pixel_y;     //Touch_getXY() updates global vars
//Variables
int currentPage  = 0, oldPage = -1;
int nbFiles = 0;
String fileList[10];
//Objects
MCUFRIEND_kbv tft;
// Button calibration
Adafruit_GFX_Button page1_btn, page2_btn;
int margin = 5;
int btnWidth = 100;
int btnHeight = 40;
int btnY = 200;
void setup() {
 //Init Serial USB
 Serial.begin(9600);
 Serial.println(F("Initialize System"));
 //Init tft screen
 uint16_t ID = tft.readID();
 if (ID == 0xD3D3) ID = 0x9486;  //for 3.5" TFT LCD Shield , 0x9341 for 2.8" TFT LCD Shield
 tft.begin(ID);
 tft.setRotation(1);//0-PORTRAIT 1-PAYSAGE 2-REVERSE PORTRAIT 3-REVERSE PAYSAGE
 //Uncomment if you are using SD
 if (!SD.begin(SD_CS)) {
   Serial.println(F("initialization failed!"));
   return;
 }
 currentPage = 0; // Indicates that we are at Home Screen
}
void loop() {
 down = Touch_getXY();
 switch (currentPage) {
   case 0:
     if (currentPage != oldPage) {
       Serial.println(F("Draw list"));
       drawList();
     }
     if (down) {
       currentPage = 1;
     }
     break;
   case 1:
     if (currentPage != oldPage) {
       tft.fillScreen(BLACK);
       delay(200);
       oldPage = currentPage;
     }
     currentPage = 0;
     break;
 }
}
void drawList() { /* function drawHomePage */
 getFilenames();
 tft.setRotation(1);
 tft.fillScreen(BLACK);
 //Title
 tft.setCursor(0, 10);
 tft.setTextSize(3);
 tft.setTextColor(WHITE, BLACK);
 tft.print("File list"); // Prints the string on the screen
 tft.drawLine(0, 32, 319, 32, DARKGREEN); // Draws the red line
 //text
 tft.setTextSize(2);
 tft.setTextColor(RED, BLACK);
 for (int i = 0; i < nbFiles; i++) {
   tft.setCursor(10, 40 + 20 * i);
   tft.print(fileList[i]);
 }
 oldPage = currentPage;
}
/************************************************************************************
    UTILITY FUNCTION
*************************************************************************************/
bool Touch_getXY(void)
{
 p = ts.getPoint();
 pinMode(YP, OUTPUT);      //restore shared pins
 pinMode(XM, OUTPUT);
 digitalWrite(YP, HIGH);
 digitalWrite(XM, HIGH);
 bool pressed = (p.z > MINPRESSURE && p.z < MAXPRESSURE);
 if (pressed) {
   if (tft.width() <= tft.height()) { //Portrait
     pixel_x = map(p.x, TS_LEFT, TS_RT, 0, tft.width()); //.kbv makes sense to me
     pixel_y = map(p.y, TS_TOP, TS_BOT, 0, tft.height());
   } else {
     pixel_x = map(p.y, TS_TOP, TS_BOT, 0, tft.width());
     pixel_y = map(p.x, TS_RT, TS_LEFT, 0, tft.height());
   }
 }
 return pressed;
}
File root;
void getFilenames(void ) { /* function printFilenames */
 ////find files in SD card
 root = SD.open("/");
 int i = 0;
 while (true) {
   File entry =  root.openNextFile();
   if (! entry) {
     break;// no more files
   }
   fileList[i] = entry.name();
   i++;
   Serial.println(entry.name());
   entry.close();
 }
 nbFiles = i;
}

Bonificación: Visualización de imágenes BMP desde la tarjeta SD

El objetivo principal del módulo MicroSD en el escudo TFT es almacenar imágenes para mostrarlas en la pantalla. Si no tienes una imagen de mapa de bits a mano, puedes descargar la que utilizamos en este ejemplo.

Para mostrar una imagen de mapa de bits, necesitamos utilizar una función especial que puedes reutilizar en tu código tal cual.

#define BUFFPIXEL 20           //Drawing speed, 20 is meant to be the best but you can use 60 altough it takes a lot of uno's RAM         
void bmpDraw(char *filename, int x, int y) {
 File     bmpFile;
 int      bmpWidth, bmpHeight;   // W+H in pixels
 uint8_t  bmpDepth;              // Bit depth (currently must be 24)
 uint32_t bmpImageoffset;        // Start of image data in file
 uint32_t rowSize;               // Not always = bmpWidth; may have padding
 uint8_t  sdbuffer[3*BUFFPIXEL]; // pixel in buffer (R+G+B per pixel)
 uint16_t lcdbuffer[BUFFPIXEL];  // pixel out buffer (16-bit per pixel)
 uint8_t  buffidx = sizeof(sdbuffer); // Current position in sdbuffer
 boolean  goodBmp = false;       // Set to true on valid header parse
 boolean  flip    = true;        // BMP is stored bottom-to-top
 int      w, h, row, col;
 uint8_t  r, g, b;
 uint32_t pos = 0, startTime = millis();
 uint8_t  lcdidx = 0;
 boolean  first = true;
 if((x >= tft.width()) || (y >= tft.height())) return;
 Serial.println();
 progmemPrint(PSTR("Loading image '"));
 Serial.print(filename);
 Serial.println('\'');
 // Open requested file on SD card
 if ((bmpFile = SD.open(filename)) == NULL) {
   progmemPrintln(PSTR("File not found"));
   return;
 }
 // Parse BMP header
 if(read16(bmpFile) == 0x4D42) { // BMP signature
   progmemPrint(PSTR("File size: ")); Serial.println(read32(bmpFile));
   (void)read32(bmpFile); // Read & ignore creator bytes
   bmpImageoffset = read32(bmpFile); // Start of image data
   progmemPrint(PSTR("Image Offset: ")); Serial.println(bmpImageoffset, DEC);
   // Read DIB header
   progmemPrint(PSTR("Header size: ")); Serial.println(read32(bmpFile));
   bmpWidth  = read32(bmpFile);
   bmpHeight = read32(bmpFile);
   if(read16(bmpFile) == 1) { // # planes -- must be '1'
     bmpDepth = read16(bmpFile); // bits per pixel
     progmemPrint(PSTR("Bit Depth: ")); Serial.println(bmpDepth);
     if((bmpDepth == 24) && (read32(bmpFile) == 0)) { // 0 = uncompressed
       goodBmp = true; // Supported BMP format -- proceed!
       progmemPrint(PSTR("Image size: "));
       Serial.print(bmpWidth);
       Serial.print('x');
       Serial.println(bmpHeight);
       // BMP rows are padded (if needed) to 4-byte boundary
       rowSize = (bmpWidth * 3 + 3) & ~3;
       // If bmpHeight is negative, image is in top-down order.
       // This is not canon but has been observed in the wild.
       if(bmpHeight < 0) {
         bmpHeight = -bmpHeight;
         flip      = false;
       }
       // Crop area to be loaded
       w = bmpWidth;
       h = bmpHeight;
       if((x+w-1) >= tft.width())  w = tft.width()  - x;
       if((y+h-1) >= tft.height()) h = tft.height() - y;
       // Set TFT address window to clipped image bounds
       tft.setAddrWindow(x, y, x+w-1, y+h-1);
       for (row=0; row<h; row++) { // For each scanline...
         // Seek to start of scan line.  It might seem labor-
         // intensive to be doing this on every line, but this
         // method covers a lot of gritty details like cropping
         // and scanline padding.  Also, the seek only takes
         // place if the file position actually needs to change
         // (avoids a lot of cluster math in SD library).
         if(flip) // Bitmap is stored bottom-to-top order (normal BMP)
           pos = bmpImageoffset + (bmpHeight - 1 - row) * rowSize;
         else     // Bitmap is stored top-to-bottom
           pos = bmpImageoffset + row * rowSize;
         if(bmpFile.position() != pos) { // Need seek?
           bmpFile.seek(pos);
           buffidx = sizeof(sdbuffer); // Force buffer reload
         }
         for (col=0; col<w; col++) { // For each column...
           // Time to read more pixel data?
           if (buffidx >= sizeof(sdbuffer)) { // Indeed
             // Push LCD buffer to the display first
             if(lcdidx > 0) {
               tft.pushColors(lcdbuffer, lcdidx, first);
               lcdidx = 0;
               first  = false;
             }
             bmpFile.read(sdbuffer, sizeof(sdbuffer));
             buffidx = 0; // Set index to beginning
           }
           // Convert pixel from BMP to TFT format
           b = sdbuffer[buffidx++];
           g = sdbuffer[buffidx++];
           r = sdbuffer[buffidx++];
           lcdbuffer[lcdidx++] = tft.color565(r,g,b);
         } // end pixel
       } // end scanline
       // Write any remaining data to LCD
       if(lcdidx > 0) {
         tft.pushColors(lcdbuffer, lcdidx, first);
       } 
       progmemPrint(PSTR("Loaded in "));
       Serial.print(millis() - startTime);
       Serial.println(" ms");
     } // end goodBmp
   }
 }
 bmpFile.close();
 if(!goodBmp) progmemPrintln(PSTR("BMP format not recognized."));
}
// These read 16- and 32-bit types from the SD card file.
// BMP data is stored little-endian, Arduino is little-endian too.
// May need to reverse subscript order if porting elsewhere.
uint16_t read16(File f) {
 uint16_t result;
 ((uint8_t *)&result)[0] = f.read(); // LSB
 ((uint8_t *)&result)[1] = f.read(); // MSB
 return result;
}
uint32_t read32(File f) {
 uint32_t result;
 ((uint8_t *)&result)[0] = f.read(); // LSB
 ((uint8_t *)&result)[1] = f.read();
 ((uint8_t *)&result)[2] = f.read();
 ((uint8_t *)&result)[3] = f.read(); // MSB
 return result;
}
// Copy string from flash to serial port
// Source string MUST be inside a PSTR() declaration!
void progmemPrint(const char *str) {
 char c;
 while(c = pgm_read_byte(str++)) Serial.print(c);
}
// Same as above, with trailing newline
void progmemPrintln(const char *str) {
 progmemPrint(str);
 Serial.println();
}

Crearemos un botón para cada archivo y utilizando la función anterior, mostraremos las imágenes contenidas en la tarjeta SD cuando pulsemos el botón correspondiente.

//Libraries
#include <SD.h>//https:
#include <Adafruit_GFX.h>//https://github.com/adafruit/Adafruit-GFX-Library
#include <MCUFRIEND_kbv.h>//https://github.com/prenticedavid/MCUFRIEND_kbv
#include <TouchScreen.h> //https://github.com/adafruit/Adafruit_TouchScreen
//Constants
#define SD_CS 10
#define BLACK 0
#define GREY 21845
#define BLUE 31
#define RED 63488
#define GREEN 2016
#define DARKGREEN 1472
#define CYAN 2047
#define MAGENTA 63519
#define YELLOW 65504
#define GOLD 56768
#define WHITE 65535
//Touch screen configuration
#define MINPRESSURE 200
#define MAXPRESSURE 1000
// ALL Touch panels and wiring is DIFFERENT
// copy-paste results from TouchScreen_Calibr_native.ino
//3.5 Parameters
//const int XP = 8, XM = A2, YP = A3, YM = 9; //320x480 ID=0x9486
//const int TS_LEFT = 144, TS_RT = 887, TS_TOP = 936, TS_BOT = 87;
//2.8 Parameters
const int XP = 8, XM = A2, YP = A3, YM = 9; //240x320 ID=0x9341
const int TS_LEFT = 907, TS_RT = 120, TS_TOP = 74, TS_BOT = 913;
TouchScreen ts = TouchScreen(XP, YP, XM, YM, 300);
TSPoint p;
bool down;
int pixel_x, pixel_y;     //Touch_getXY() updates global vars
//Variables
int currentPage  = 0, oldPage = -1;
int nbFiles = 0;
String fileList[10];
const int charBuff=50;
char charCopy[50];
//Objects
MCUFRIEND_kbv tft;
// Button calibration
Adafruit_GFX_Button fileBtn[10];
int margin = 5;
int btnWidth = 100;
int btnHeight = 40;
int btnY = 200;
void setup() {
 //Init Serial USB
 Serial.begin(9600);
 Serial.println(F("Initialize System"));
 //Init tft screen
 uint16_t ID = tft.readID();
 if (ID == 0xD3D3) ID = 0x9486;  //for 3.5" TFT LCD Shield , 0x9341 for 2.8" TFT LCD Shield
 tft.begin(ID);
 tft.setRotation(1);//0-PORTRAIT 1-PAYSAGE 2-REVERSE PORTRAIT 3-REVERSE PAYSAGE
 //Uncomment if you are using SD
 if (!SD.begin(SD_CS)) {
   Serial.println(F("initialization failed!"));
   return;
 }
 currentPage = 0; // Indicates that we are at Home Screen
}
void loop() {
 down = Touch_getXY();
 switch (currentPage) {
   case 0:
     if (currentPage != oldPage) {
       Serial.println(F("Draw list"));
       drawList();
     }
     oldPage = currentPage;
     for(int i=1;i<nbFiles;i++){
       fileBtn[i].press(down && fileBtn[i].contains(pixel_x, pixel_y));
       if (fileBtn[i].justReleased()) fileBtn[i].drawButton();
       if (fileBtn[i].justPressed()) {
         fileBtn[i].drawButton(true);
         fileList[i].toCharArray(charCopy,charBuff);
         bmpDraw(charCopy, 0, 0);
         currentPage = 1;
       }
     }
     
     
     break;
   case 1:
     oldPage=currentPage;
     if (down) {
       currentPage = 0;
     }
     break;
 }
}
void drawList() { /* function drawHomePage */
 getFilenames();
 tft.setRotation(1);
 tft.fillScreen(BLACK);
 //Title
 tft.setCursor(0, 10);
 tft.setTextSize(3);
 tft.setTextColor(WHITE, BLACK);
 tft.print("File list"); // Prints the string on the screen
 tft.drawLine(0, 32, 319, 32, DARKGREEN); // Draws the red line
 //text
 tft.setTextSize(2);
 tft.setTextColor(RED, BLACK);
 for (int i = 1; i < nbFiles; i++) {
   //tft.setCursor(10, 40 + 20 * i);
   //tft.print(fileList[i]);
   // Button
   fileList[i].toCharArray(charCopy,charBuff);
   fileBtn[i].initButton(&tft,  tft.width() / 2. , 45+ i*(20+5), 2 * 100, 20, WHITE, GREEN, BLACK, charCopy, 2);
   fileBtn[i].drawButton(false);
 }
}
/************************************************************************************
    UTILITY FUNCTION
*************************************************************************************/
bool Touch_getXY(void)
{
 p = ts.getPoint();
 pinMode(YP, OUTPUT);      //restore shared pins
 pinMode(XM, OUTPUT);
 digitalWrite(YP, HIGH);
 digitalWrite(XM, HIGH);
 bool pressed = (p.z > MINPRESSURE && p.z < MAXPRESSURE);
 if (pressed) {
   if (tft.width() <= tft.height()) { //Portrait
     pixel_x = map(p.x, TS_LEFT, TS_RT, 0, tft.width()); //.kbv makes sense to me
     pixel_y = map(p.y, TS_TOP, TS_BOT, 0, tft.height());
   } else {
     pixel_x = map(p.y, TS_TOP, TS_BOT, 0, tft.width());
     pixel_y = map(p.x, TS_RT, TS_LEFT, 0, tft.height());
   }
 }
 return pressed;
}
File root;
void getFilenames(void ) { /* function printFilenames */
 ////find files in SD card
 root = SD.open("/");
 int i = 0;
 while (true) {
   File entry =  root.openNextFile();
   if (! entry) {
     break;// no more files
   }
   fileList[i] = entry.name();
   i++;
   Serial.println(entry.name());
   entry.close();
 }
 nbFiles = i;
 root.close();
}
#define BUFFPIXEL 20           //Drawing speed, 20 is meant to be the best but you can use 60 altough it takes a lot of uno's RAM         
void bmpDraw(char *filename, int x, int y) {
 File     bmpFile;
 int      bmpWidth, bmpHeight;   // W+H in pixels
 uint8_t  bmpDepth;              // Bit depth (currently must be 24)
 uint32_t bmpImageoffset;        // Start of image data in file
 uint32_t rowSize;               // Not always = bmpWidth; may have padding
 uint8_t  sdbuffer[3*BUFFPIXEL]; // pixel in buffer (R+G+B per pixel)
 uint16_t lcdbuffer[BUFFPIXEL];  // pixel out buffer (16-bit per pixel)
 uint8_t  buffidx = sizeof(sdbuffer); // Current position in sdbuffer
 boolean  goodBmp = false;       // Set to true on valid header parse
 boolean  flip    = true;        // BMP is stored bottom-to-top
 int      w, h, row, col;
 uint8_t  r, g, b;
 uint32_t pos = 0, startTime = millis();
 uint8_t  lcdidx = 0;
 boolean  first = true;
 if((x >= tft.width()) || (y >= tft.height())) return;
 Serial.println();
 progmemPrint(PSTR("Loading image '"));
 Serial.print(filename);
 Serial.println('\'');
 // Open requested file on SD card
 if ((bmpFile = SD.open(filename)) == NULL) {
   progmemPrintln(PSTR("File not found"));
   return;
 }
 // Parse BMP header
 if(read16(bmpFile) == 0x4D42) { // BMP signature
   progmemPrint(PSTR("File size: ")); Serial.println(read32(bmpFile));
   (void)read32(bmpFile); // Read & ignore creator bytes
   bmpImageoffset = read32(bmpFile); // Start of image data
   progmemPrint(PSTR("Image Offset: ")); Serial.println(bmpImageoffset, DEC);
   // Read DIB header
   progmemPrint(PSTR("Header size: ")); Serial.println(read32(bmpFile));
   bmpWidth  = read32(bmpFile);
   bmpHeight = read32(bmpFile);
   if(read16(bmpFile) == 1) { // # planes -- must be '1'
     bmpDepth = read16(bmpFile); // bits per pixel
     progmemPrint(PSTR("Bit Depth: ")); Serial.println(bmpDepth);
     if((bmpDepth == 24) && (read32(bmpFile) == 0)) { // 0 = uncompressed
       goodBmp = true; // Supported BMP format -- proceed!
       progmemPrint(PSTR("Image size: "));
       Serial.print(bmpWidth);
       Serial.print('x');
       Serial.println(bmpHeight);
       // BMP rows are padded (if needed) to 4-byte boundary
       rowSize = (bmpWidth * 3 + 3) & ~3;
       // If bmpHeight is negative, image is in top-down order.
       // This is not canon but has been observed in the wild.
       if(bmpHeight < 0) {
         bmpHeight = -bmpHeight;
         flip      = false;
       }
       // Crop area to be loaded
       w = bmpWidth;
       h = bmpHeight;
       if((x+w-1) >= tft.width())  w = tft.width()  - x;
       if((y+h-1) >= tft.height()) h = tft.height() - y;
       // Set TFT address window to clipped image bounds
       tft.setAddrWindow(x, y, x+w-1, y+h-1);
       for (row=0; row<h; row++) { // For each scanline...
         // Seek to start of scan line.  It might seem labor-
         // intensive to be doing this on every line, but this
         // method covers a lot of gritty details like cropping
         // and scanline padding.  Also, the seek only takes
         // place if the file position actually needs to change
         // (avoids a lot of cluster math in SD library).
         if(flip) // Bitmap is stored bottom-to-top order (normal BMP)
           pos = bmpImageoffset + (bmpHeight - 1 - row) * rowSize;
         else     // Bitmap is stored top-to-bottom
           pos = bmpImageoffset + row * rowSize;
         if(bmpFile.position() != pos) { // Need seek?
           bmpFile.seek(pos);
           buffidx = sizeof(sdbuffer); // Force buffer reload
         }
         for (col=0; col<w; col++) { // For each column...
           // Time to read more pixel data?
           if (buffidx >= sizeof(sdbuffer)) { // Indeed
             // Push LCD buffer to the display first
             if(lcdidx > 0) {
               tft.pushColors(lcdbuffer, lcdidx, first);
               lcdidx = 0;
               first  = false;
             }
             bmpFile.read(sdbuffer, sizeof(sdbuffer));
             buffidx = 0; // Set index to beginning
           }
           // Convert pixel from BMP to TFT format
           b = sdbuffer[buffidx++];
           g = sdbuffer[buffidx++];
           r = sdbuffer[buffidx++];
           lcdbuffer[lcdidx++] = tft.color565(r,g,b);
         } // end pixel
       } // end scanline
       // Write any remaining data to LCD
       if(lcdidx > 0) {
         tft.pushColors(lcdbuffer, lcdidx, first);
       } 
       progmemPrint(PSTR("Loaded in "));
       Serial.print(millis() - startTime);
       Serial.println(" ms");
     } // end goodBmp
   }
 }
 bmpFile.close();
 if(!goodBmp) progmemPrintln(PSTR("BMP format not recognized."));
}
// These read 16- and 32-bit types from the SD card file.
// BMP data is stored little-endian, Arduino is little-endian too.
// May need to reverse subscript order if porting elsewhere.
uint16_t read16(File f) {
 uint16_t result;
 ((uint8_t *)&result)[0] = f.read(); // LSB
 ((uint8_t *)&result)[1] = f.read(); // MSB
 return result;
}
uint32_t read32(File f) {
 uint32_t result;
 ((uint8_t *)&result)[0] = f.read(); // LSB
 ((uint8_t *)&result)[1] = f.read();
 ((uint8_t *)&result)[2] = f.read();
 ((uint8_t *)&result)[3] = f.read(); // MSB
 return result;
}
// Copy string from flash to serial port
// Source string MUST be inside a PSTR() declaration!
void progmemPrint(const char *str) {
 char c;
 while(c = pgm_read_byte(str++)) Serial.print(c);
}
// Same as above, with trailing newline
void progmemPrintln(const char *str) {
 progmemPrint(str);
 Serial.println();
}

Una vez cargado el código, aparecerá un menú con un botón para cada archivo de la tarjeta SD. Al pulsar un botón, la imagen de mapa de bits correspondiente aparecerá en la pantalla. Si vuelve a pulsar la pantalla, volverá al menú principal.

Aplicaciones

  • Crear una interfaz gráfica con imágenes

Fuentes

¿De cuánta utilidad te ha parecido este contenido?

¡Haz clic en una estrella para puntuarlo!

Promedio de puntuación 0 / 5. Recuento de votos: 0

Hasta ahora, ¡no hay votos!. Sé el primero en puntuar este contenido.

Ya que has encontrado útil este contenido...

¡Sígueme en los medios sociales!

¡Siento que este contenido no te haya sido útil!

¡Déjame mejorar este contenido!

Dime, ¿cómo puedo mejorar este contenido?