sábado, 7 de febrero de 2026

286 HARRIS 20MHz de pura nostalgia

Muy buenas a todos y todas!!!

Si estas atento al canal de Youtube, sabrás de mis últimas adquisiciones en ordenadores de los 90's.

En esta ocasión es un ordenador 286, una generación que estuvo activa desde 1982 hasta principios de los 90's empezando su declive en 1985 con la introducción de los 386 y sus poderosos 32bits.

Pero que esto no os confunda, sus auténticos rivales eran sus predecesores, los 8086/8080 a los cuales machacaba sin piedad.

La revolución del 286

Las mejoras del 286 son una autentica revolución a nivel hardware y se dividen en dos modos de operación principales:

  • Modo Real (Real Mode):
    Completamente compatible con el 8086/8088. Los programas escritos para el PC original funcionaban sin modificaciones.
    Sin embargo, en este modo, el 286 era mucho más rápido (a la misma frecuencia de reloj, unas 3-6 veces más rápido) gracias a mejoras en la microarquitectura, como un pipeline de 4 etapas y una unidad de ejecución más eficiente.

  • Modo Protegido (Protected Mode):
    Esta fue la gran innovación. Introdujo conceptos que definieron la informática moderna:
    • Espacio de Direccionamiento Ampliado:
      Acceso a hasta 16 MB de memoria física (frente a 1 MB del 8086).
    • Memoria Virtual:
      Podía direccionar hasta 1 GB de memoria virtual por tarea, utilizando un sistema de disco como extensión de la RAM.
    • Protección de Memoria:
      Aislamiento entre procesos y el sistema operativo mediante anillos de privilegio (0 para el SO, 3 para aplicaciones), evitando que una aplicación "colgada" colapsara toda la máquina.
    • Multitarea por Hardware:
      Soporte a nivel de hardware para cambiar rápidamente entre múltiples tareas.

Los componentes:

En este caso estamos frente a un PC totalmente clónico, es decir cada parte es de como decimos en España "de su padre y de su madre", son compatibles pero de marcas tamaños y formas diferentes. Aunque creo que eso son cosas que no cambian...

Empecemos con la placa base:


Placa base 286 Hsin Tech M209

Placa base Hsin Tech M209
Hsin Tech M209

Sin duda lo que más me sorprendió de todo el conjunto, y no precisamente por el procesador.

Esta placa con un socket de 40 pines admite procesadores con velocidades de 12, 16 y 20MHz y Co-procesadores 80287-8, 12 y 16.

Admitía un máximo de 4MB en módulos SIPP. Y esto es lo más raro de todo!!!. Nunca había ni visto ni escuchado hablar de este tipo de módulos, tienen 30 pines igual que las memorias SIMM de 30 contactos pero tienen soldadas unas "patitas" y el zócalo en el que las introducimos son sockets como los que se utilizan para poner los circuitos integrados, ¡curioso cuanto menos!

Cuenta con 6 puertos de expansión ISA 16bits y 1 puerto de expansión ISA de 8bits y puerto para teclados de tipo AT, El "gordo"

Procesador CS80C286-20 HARRIS

Esta bestia de 20MHz y pese a no contar con unidad de coma flotante, era capaz de manejar hasta 16MB de memoria RAM física y 1GB de memoria virtual, que no creo que en esa época aun usuario normal lo utilizase, pero poder, podía.

Ofrecía velocidades de 20MHz en su modo turbo y 12MHz para su modo "tortuga" haciendo compatibles software que no estaban optimizados para estas velocidades.

El Harris CS80C286-20 era particularmente valorado en aplicaciones embebidas, industriales y portátiles por su bajo consumo y rango de temperatura extendido, manteniendo compatibilidad total con el software x86 de la época.

Harris CS80C286-20 - Especificaciones Técnicas
Fabricante Harris Semiconductor (posteriormente Intersil)
Part Number CS80C286-20
Frecuencia 20 MHz
Tecnología CHMOS IV (1.2 µm = 1200 nm)
Arquitectura 16-bit x86 compatible
Bus direcciones 24 bits (16 MB físico)
Bus datos 16 bits
Transistores ~120,000-130,000
Voltaje +5V ±10%
Consumo típico 150 mA @ 20 MHz (750 mW)
Temperatura operativa -40°C a +85°C (Industrial)
Empaquetado PLCC-68, LCC-68, PGA-68

Memoria RAM: El formato SIPP

Como ya he comentado son memorias de tipo SIPP(Single In-line Pin Package) con una capacidad de 256KB, sumando los 4 módulos tenemos una cantidad de 1MB de RAM si, habéis leído bien, 1MB de RAM.

Módulo SIPP 256KB
Módulo SIPP 30 contactos
No descarto hacerme con unos módulos de 1MB de 30 contactos y probar si son compatibles, según el manual de la placa, sería posible...

Esta "escasa" cantidad de RAM sera suficiente para cualquier sistema MS-DOS como veremos más adelante.

Controladora de discos y puertos ISA 16bits

Controladora UMC UM82C86F
Controladora UMC UM82C86F
Aquí me lleve una pequeña alegría ya que funciono sin ningún tipo de inconveniente.

Con capacidad para 2 puertos COM y 2 puertos LPT es en sus puertos para disquetera de 34 pines en el que irá conectada nuestra querida disquetera Gotek y otro puerto IDE de 40 pines en el que he conectado un adaptador de tarjetas CF a IDE y una tarjeta con capacidad para 512MB (488MB reales).


Gráfica Trident TVGA-9000C Protac VC511TM6

Protac VC511TM6
Protac VC511TM6
Esta tarjeta del año 1991 cuanta con un bus ISA de 16 bits, una memoria de 512KB DRAM y 8bits de color.

Como casi todos los juegos son 2D o de tipo aventura gráfica tenemos más que suficiente.

Por otra parte como no voy a usar Windows en esta máquina no necesito drivers. Pero también están disponibles los drivers para Windows 3.0 en The Retro Web.


Sonido Sound Blaster Vibra 16bits CT4180

Sound Blaster 16 Value PNP CT4180
Sound Blaster 16 CT4180

Uno de los componentes que mejoraba la experiencia en cuanto a juegos se refiere es la tarjeta de sonido y en este caso viene de la mano de Creative y su gama Sound Blaster.
La Sound Blaster Vibra de 16bits.

Esta tarjeta, como todas las anteriores viene en formato ISA de 16bits y aunque es un estándar a nivel de sonido le da a esta máquina vida propia.


Unidades de almacenamiento:


Disco duro

Una de las partes más importantes de cualquier ordenador, además del poder de computación, es donde almacenar información. Por desgracia este ordenador no tenía disco duro.

Esto me hizo recurrir al uso de un adaptador de CF a IDE junto con una tarjeta de 512MB

Adaptador CF a IDE

Puede parecer poca memoria, pero hay que tener en cuenta que, de manera nativa, solo podemos disponer de 504MB, así que podemos decir que vamos sobrados de memoria.

Una de las cosas que más me gusta de este sistema es la ausencia de ruido. Estos equipos son especialmente ruidosos, así que al quitar el disco duro mecánico nos ahorramos unos cuandos dB's. No voy a negar que escuchar a un disco de esta época arrancar es todo un gustazo para los oídos, pero después de media hora ya empieza a cansar y más su tienes la caja abierta como suele ser mi caso.

Parámetros de Disco Duro BIOS (CHS)
Parámetro Valor Descripción
Cylinders (Cilindros) 933 Número de pistas por cara
Heads (Cabezas) 16 Número de superficies
de lectura/escritura
Sectors (Sectores) 63 Sectores por pista (estándar IDE)
Precomp (Precompensación) 65535 Valor de precompensación (opcional)
LZone (Landing Zone) 992 Cilindro donde se estacionan las cabezas (Opcional)
Tamaño Total 488 MB
(≈ 512,000,000 bytes)

Unidad de Disco

Gotek con FlasFloppy
Gotek con FlashFloppy
La unidad de disquetes de 1.44MB estaba prácticamente inservible. Sin pensarlo 2 veces me hice con una unidad Gotek.

Ya hemos hablado de la unidad Gotek en modo IBMpc en este blog, y es la navaja suiza para cualquier unidad de disco, sea de Alta o Baja densidad, con 720KB o 1.44MB respectivamente, tanto para unidades de 3 1/2 como unidades de 5 1/4.

Desde la Gotek cargaremos el sistema operativo e instalaremos programas y por supuesto, juegos.


El software

El sistema operativo


Aquí mi primera opción fue usar MS-DOS 6.22 (1994) pero la experiencia no fué muy buena, tener un 1MB no le gustaba mucho y aunque me consta que se puede ajustar, cambié a MS-DOS 5.0 (1991), un viejo conocido.

MS-DOS 5.0 fué mi primer contacto con un sistema operativo en el año 1992 en mi añorado 386SX.

Como tenía la misma cantidad de RAM sabía que no me iba a dar problemas, y así fué. Una vez se instaló arranco y apareció nuestro querido prompt: C:\.

Algunos de los primeros programas que tenía este sistema era el MS-DOS Shell (dosshell.com) un proto sistema operativo que además era bastante personalizable para su tiempo.

MS-DOS ya incluia el mítico editor de textos "edit.com" o "doskey.com", un potente administrador de la línea de comandos que añadía historial de comandos, edición de línea (con flechas) y la capacidad de crear macros. Esto Cambiaría completamente la experiencia del usuario en la consola.

También hubo mejoras en el instalador "setup.com" que hacia que la instalación del sistema fuera mucho más intuitivo.

Versiones de Windows 1.0 y 2.0

Ya que no había problemas de espacio en la unidad CF quise probar las versiones más tempranas de Windows.

Windows 1.0 y Windows 2.0:

Una de las aplicaciones que inicio una revolución en cuanto a sistemas gráficos y que perdura hasta el día de hoy.

Iniciando Windows 1.0 su andadura comercial en 1985 y terminando a finales de 1987 para dar paso a una versión superior Windows 2.0 en sus versiones 286 y 386.

Al no utilizar este tipo de herramientas, no me llamaron mucho la atención.

Sinceramente Dosshell se ve mucho mejor y es más rápido para probar programas y software de antaño.

Entretenimiento:

Al tener esta "locura" de almacenamiento, hay que tener en cuenta que el estándar era tener entre 40 y 105MB de almacenamiento, nos permite tener los suficientes juegos como para estar entretenidos una buenas sesiones.

Para tiempos de ocio y al contar con tarjeta de sonido, entretenerse con este ordenador es fácil, Juegos como Volfied, Duke Nukem2D o incluso Wolfenstein 3D pueden mantenerte durante horas pegado a la pantalla... veamos que juegos han funcionado y cuales no:

Funcionando:

  • Curse of Enchantia
  • Duke Nukem 2D
  • Indiana Jones y la última cruzada
  • Lemmings
  • Monkey Island 1
  • Monkey Island 2
  • Prince of Persia
  • Volfied
  • Wolfenstein 3D
  • Zool

No funcionan o necesitan equipo superior:

  • Alone in the Dark
  • Doom
  • Quake
  • Warcraft I y II

Todo el hardware y software esta disponible en los siguiente enlaces

viernes, 30 de enero de 2026

ESP32-C3 Super Mini + OLED SH1106

ESP32-C3 Super Mini
ESP32-C3 Super Mini

Muy buenas a todas y todos!!!

En la era de las pantallas táctiles y los comandos por voz, hay algo profundamente satisfactorio en la interacción física: el clic tangible de un botón, la rotación precisa de un encoder, el feedback táctil que confirma una acción.

Hoy exploramos cómo combinar el poder del ESP32-C3 con elementos de control clásicos para crear interfaces que no solo funcionan, sino que se sienten bien al usar.


ESP32-C3 Super Mini: Todo el poder de un ESP32 en su mínima expresión


SP32-C3 Super Mini pinout
ESP32-C3 Super Mini pinout

Esta es la primera toma de contacto de este poderoso microcontrolador en una placa con unas dimensiones realmente reducidas.

Y salvo por algunas consideraciones a la hora de programarlo con el Arduino IDE 2, es igual de sencillo que cualquiera de sus hermanos mayores

Características más destacadas:

  • Procesador RISC-V de 160MHz con 1 núcleo - Pese a tener un solo núcleo tiene potencia suficiente para interfaces complejas.
  • GPIOs configurables - Podemos adaptar cada pin a nuestras necesidades.
  • Tamaño minimalista - Ideal para proyectos de reducidas dimensiones.
  • Bajo consumo - Su consumo optimizado para IoT y transiciones de sleep más rápidas le da una relación potencia/consumo perfecto para proyectos portátiles

Visualización con OLED SH1106 1.3"

  • 128x64 píxeles - Espacio suficiente para mostrar menús e información de manera minimalista.
  • Un buen contraste - Legible en casi cualquier condición de luz.
  • Interfaz I2C - solo 2 cables necesarios
    Módulo encoder + pantalla Oled SH1106 1.3' frontal
Encoder + 3 Botones: Control Total
  • Encoder rotativo - navegación precisa e infinita
  • Botón del encoder - confirmación rápida
  • Botón Confirm/Back - navegación intuitiva tipo "OK/Cancel"

Función Pin GPIO
SDA (I²C Data) GPIO6
SCL (I²C Clock) GPIO7
Encoder A GPIO2
Encoder B GPIO3
BTN Encoder GPIO4
BTN Back GPIO5
BTN Conf GPIO8
Tabla de asignación de funciones a pines GPIO

Librería Adafruit SH110X para pantalla OLED 1.3"

Código Ejemplar: Sistema de Menús con Control Físico Completo

Aquí tienes un sistema completo que demuestra cómo integrar todos los componentes en una interfaz cohesiva:


#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>

// ================= CONFIGURACIÓN DE PINES =================
// Según tu configuración específica
#define PIN_CONFIRM    3   // Botón verde/confirmación
#define PIN_BACK       4   // Botón rojo/retroceso  
#define PIN_ROTARY_BTN 2   // Botón del encoder
#define PIN_ROTARY_A   20  // Encoder fase A
#define PIN_ROTARY_B   21  // Encoder fase B
#define OLED_SDA       6   // Datos I2C
#define OLED_SCL       7   // Reloj I2C

// ================= CONFIGURACIÓN OLED =================
#define SCREEN_WIDTH  128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
Adafruit_SH1106G display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// ================= VARIABLES GLOBALES =================
// Control del encoder
volatile int encoderPos = 0;
int lastEncoderPos = 0;
int rotaryAState;
int rotaryALastState;

// Estados de botones
bool btnConfirmPressed = false;
bool btnBackPressed = false;
bool btnRotaryPressed = false;
bool lastConfirmState = HIGH;
bool lastBackState = HIGH;
bool lastRotaryState = HIGH;

// Sistema de menús
int menuIndex = 0;
const int menuItems = 4;
String menuOptions[menuItems] = {
  "Ajustar Brillo",
  "Ver Contador",
  "Config. Sistema",
  "Info del Hardware"
};

// Variables de aplicación
int screenBrightness = 128; // 0-255
int counter = 0;
unsigned long lastActivity = 0;

// ================= INTERRUPCIÓN ENCODER =================
void IRAM_ATTR rotaryEncoderISR() {
  delayMicroseconds(500);
  rotaryAState = digitalRead(PIN_ROTARY_A);
  
  if (rotaryAState != rotaryALastState) {
    // Determinar dirección basándose en el estado de la fase B
    if (digitalRead(PIN_ROTARY_B) != rotaryAState) {
      encoderPos++;  // Rotación horaria
    } else {
      encoderPos--;  // Rotación antihoraria
    }
  }
  rotaryALastState = rotaryAState;
}

// ================= FUNCIÓN DE TEMPERATURA =================
float readChipTemperature() {
  // Simulación de temperatura basada en tiempo de actividad
  // Para medición real, conectar sensor DS18B20 o similar
  float baseTemp = 25.0;
  float heatingFactor = millis() / 600000.0;
  float tempC = baseTemp + fmin(heatingFactor, 10.0);
  
  // Pequeña variación aleatoria para simular lectura real
  static float lastTemp = baseTemp;
  float variation = (random(-10, 11) / 100.0); // ±0.1°C
  tempC = lastTemp + variation;
  tempC = constrain(tempC, 24.0, 36.0);
  lastTemp = tempC;
  
  return tempC;
}

// ================= CONFIGURACIÓN INICIAL =================
void setup() {
  Serial.begin(115200);
  Serial.println("Iniciando Sistema de Control Físico");
  
  // 1. Configurar I2C con pines personalizados
  Wire.begin(OLED_SDA, OLED_SCL);
  
  // 2. Inicializar pantalla OLED
  if(!display.begin(0x3C, true)) {
    Serial.println("Error al inicializar pantalla OLED");
    while(true); // Detener si la pantalla falla
  }
  
  Serial.println("Pantalla OLED SH1106 inicializada");
  
  // 3. Configurar pines de entrada
  pinMode(PIN_CONFIRM, INPUT_PULLUP);
  pinMode(PIN_BACK, INPUT_PULLUP);
  pinMode(PIN_ROTARY_BTN, INPUT_PULLUP);
  pinMode(PIN_ROTARY_A, INPUT_PULLUP);
  pinMode(PIN_ROTARY_B, INPUT_PULLUP);
  
  // 4. Configurar interrupción para encoder
  rotaryALastState = digitalRead(PIN_ROTARY_A);
  attachInterrupt(digitalPinToInterrupt(PIN_ROTARY_A), 
                  rotaryEncoderISR, CHANGE);
  
  // 5. Pantalla de bienvenida animada
  showWelcomeAnimation();
  
  Serial.println("Sistema listo para usar!");
}

// ================= BUCLE PRINCIPAL =================
void loop() {
  // 1. Leer y procesar botones
  readButtons();
  
  // 2. Actualizar menú según encoder
  updateMenuFromEncoder();
  
  // 3. Dibujar interfaz actual
  drawMainInterface();
  
  // 4. Manejar inactividad
  checkInactivity();
  
  delay(50); // Control de tasa de refresco
}

// ================= FUNCIONES DE CONTROL =================
void readButtons() {
  // Leer estados actuales
  bool confirmState = digitalRead(PIN_CONFIRM);
  bool backState = digitalRead(PIN_BACK);
  bool rotaryState = digitalRead(PIN_ROTARY_BTN);
  
  // Detectar flancos descendentes (pulsaciones)
  if (confirmState == LOW && lastConfirmState == HIGH) {
    btnConfirmPressed = true;
    lastActivity = millis();
    Serial.println("Botón CONFIRM pulsado");
  }
  if (backState == LOW && lastBackState == HIGH) {
    btnBackPressed = true;
    lastActivity = millis();
    Serial.println("Botón BACK pulsado");
  }
  if (rotaryState == LOW && lastRotaryState == HIGH) {
    btnRotaryPressed = true;
    lastActivity = millis();
    Serial.println("Botón ROTARY pulsado");
  }
  
  // Actualizar estados anteriores
  lastConfirmState = confirmState;
  lastBackState = backState;
  lastRotaryState = rotaryState;
  
  // Manejar acciones de botones
  handleButtonActions();
}

void updateMenuFromEncoder() {
  if (encoderPos != lastEncoderPos) {
    lastActivity = millis(); // Registrar actividad
    
    // Determinar dirección del cambio
    if (encoderPos > lastEncoderPos) {
      // Rotación hacia adelante
      menuIndex = (menuIndex + 1) % menuItems;
      Serial.print("Menú hacia adelante: ");
    } else {
      // Rotación hacia atrás
      menuIndex = (menuIndex - 1 + menuItems) % menuItems;
      Serial.print("Menú hacia atrás: ");
    }
    
    Serial.print(menuIndex);
    Serial.print(" - ");
    Serial.println(menuOptions[menuIndex]);
    
    lastEncoderPos = encoderPos;
  }
}

void handleButtonActions() {
  if (btnConfirmPressed || btnRotaryPressed) {
    // CONFIRM o ROTARY seleccionan la opción actual
    executeMenuAction(menuIndex);
    btnConfirmPressed = false;
    btnRotaryPressed = false;
  }
  
  if (btnBackPressed) {
    // BACK siempre vuelve al menú principal
    Serial.println("Volviendo al menú principal");
    btnBackPressed = false;
  }
}

// ================= INTERFAZ GRÁFICA =================
void drawMainInterface() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SH110X_WHITE);
  
  // 1. Encabezado
  display.setCursor(35, 0);
  display.println("MENU PRINCIPAL");
  display.drawLine(0, 10, 128, 10, SH110X_WHITE);
  
  // 2. Opciones del menú
  for (int i = 0; i < menuItems; i++) {
    display.setCursor(10, 12 + i * 12);
    
    if (i == menuIndex) {
      // Opción seleccionada (invertida)
      display.setTextColor(SH110X_BLACK, SH110X_WHITE);
      display.print("> ");
    } else {
      display.setTextColor(SH110X_WHITE);
      display.print("  ");
    }
    
    display.println(menuOptions[i]);
  }
  
  // 3. Pie de página informativo
  display.setTextColor(SH110X_WHITE);
  display.setCursor(0, 56);
  display.print("Pos: ");
  display.print(encoderPos);
  display.print(" | Sel: ");
  display.print(menuIndex + 1);
  display.print("/");
  display.print(menuItems);
  
  display.display();
}

// ================= ACCIONES DEL MENÚ =================
void executeMenuAction(int index) {
  Serial.print("Ejecutando acción: ");
  Serial.println(menuOptions[index]);
  
  switch(index) {
    case 0: // Ajustar Brillo
      adjustBrightness();
      break;
    case 1: // Ver Contador
      showCounter();
      break;
    case 2: // Config. Sistema
      showSystemConfig();
      break;
    case 3: // Info Hardware
      showHardwareInfo();
      break;
  }
  
  // Redibujar menú principal después de la acción
  display.clearDisplay();
}

// ================= MÓDULOS DE APLICACIÓN =================
void adjustBrightness() {
  Serial.println("Ajustando brillo...");
  int originalBrightness = screenBrightness;
  int lastBrightnessPos = encoderPos;
  
  bool exitAdjust = false;
  
  while(!exitAdjust) {
    // Actualizar brillo con encoder
    if (encoderPos != lastBrightnessPos) {
      int diff = encoderPos - lastBrightnessPos;
      screenBrightness += diff * 5;
      screenBrightness = constrain(screenBrightness, 0, 255);
      lastBrightnessPos = encoderPos;
      lastActivity = millis();
    }
    
    // Dibujar interfaz completa
    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(SH110X_WHITE);
    
    // Encabezado
    display.setCursor(20, 0);
    display.println("AJUSTAR BRILLO");
    display.drawLine(0, 10, 128, 10, SH110X_WHITE);
    
    // Valor numérico
    display.setCursor(40, 15);
    display.print("Valor: ");
    display.print(screenBrightness);
    
    // Barra de progreso visual
    display.drawRect(10, 30, 108, 12, SH110X_WHITE);
    int barWidth = (screenBrightness * 104) / 255;
    display.fillRect(12, 32, barWidth, 8, SH110X_WHITE);
    
    // Indicadores de extremos
    display.setCursor(10, 45);
    display.print("Min");
    display.setCursor(110, 45);
    display.print("Max");
    
    // Instrucciones
    display.setCursor(5, 55);
    display.print("Enc: +/-  BACK:Salir");
    
    display.display();
    
    // Verificar si presionan BACK para salir
    if (digitalRead(PIN_BACK) == LOW) {
      delay(200);
      exitAdjust = true;
      Serial.print("Brillo final: ");
      Serial.println(screenBrightness);
    }
    
    delay(50);
  }
}

void showCounter() {
  Serial.println("Mostrando contador...");
  
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SH110X_WHITE);
  
  display.setCursor(30, 0);
  display.println("CONTADOR");
  display.drawLine(0, 10, 128, 10, SH110X_WHITE);
  
  bool exitCounter = false;
  int lastCounterPos = encoderPos;
  
  while(!exitCounter) {
    // Actualizar contador con encoder
    if (encoderPos != lastCounterPos) {
      if (encoderPos > lastCounterPos) {
        counter++;
      } else {
        counter--;
      }
      lastCounterPos = encoderPos;
      lastActivity = millis();
    }
    
    // Mostrar contador grande
    display.setTextSize(3);
    display.setCursor(40, 18);
    display.println(counter);
    
    // Instrucciones
    display.setTextSize(1);
    display.setCursor(5, 45);
    display.print("Encoder: +/-");
    display.setCursor(5, 55);
    display.print("CONF:Reset BACK:Salir");
    
    display.display();
    display.clearDisplay();
    
    // Verificar botones
    if (digitalRead(PIN_BACK) == LOW) {
      delay(200);
      exitCounter = true;
    }
    
    if (digitalRead(PIN_CONFIRM) == LOW) {
      delay(200);
      counter = 0;
      Serial.println("Contador reseteado");
    }
    
    delay(50);
  }
}

void showSystemConfig() {
  Serial.println("Mostrando configuración...");
  
  bool exitConfig = false;
  int configOption = 0;
  String configOptions[] = {"Opcion A", "Opcion B", "Opcion C"};
  bool configStates[] = {false, false, false};
  
  while(!exitConfig) {
    // Leer encoder para navegar
    if (encoderPos != lastEncoderPos) {
      if (encoderPos > lastEncoderPos) {
        configOption = (configOption + 1) % 3;
      } else {
        configOption = (configOption - 1 + 3) % 3;
      }
      lastEncoderPos = encoderPos;
      lastActivity = millis();
    }
    
    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(SH110X_WHITE);
    
    display.setCursor(20, 0);
    display.println("CONFIGURACION");
    display.drawLine(0, 10, 128, 10, SH110X_WHITE);
    
    // Mostrar opciones
    for (int i = 0; i < 3; i++) {
      display.setCursor(10, 12 + i * 15);
      
      if (i == configOption) {
        display.setTextColor(SH110X_BLACK, SH110X_WHITE);
        display.print("> ");
      } else {
        display.setTextColor(SH110X_WHITE);
        display.print("  ");
      }
      
      display.print(configOptions[i]);
      display.print(": ");
      display.println(configStates[i] ? "ON" : "OFF");
    }
    
    // Instrucciones
    display.setTextColor(SH110X_WHITE);
    display.setCursor(0, 55);
    display.print("CONF:Tog  BACK:Salir");
    
    display.display();
    
    // Verificar botones
    if (digitalRead(PIN_BACK) == LOW) {
      delay(200);
      exitConfig = true;
    }
    
    if (digitalRead(PIN_CONFIRM) == LOW) {
      delay(200);
      configStates[configOption] = !configStates[configOption];
      Serial.print(configOptions[configOption]);
      Serial.print(" cambiado a: ");
      Serial.println(configStates[configOption] ? "ON" : "OFF");
    }
    
    delay(50);
  }
}

void showHardwareInfo() {
  Serial.println("Mostrando info hardware...");
  
  bool exitInfo = false;
  int infoPage = 0;
  
  while(!exitInfo) {
    if (encoderPos != lastEncoderPos) {
      if (encoderPos > lastEncoderPos) {
        infoPage = (infoPage + 1) % 2;
      } else {
        infoPage = (infoPage - 1 + 2) % 2;
      }
      lastEncoderPos = encoderPos;
      lastActivity = millis();
    }
    
    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(SH110X_WHITE);
    
    display.setCursor(30, 0);
    display.println("INFORMACION");
    display.drawLine(0, 10, 128, 10, SH110X_WHITE);
    
    if (infoPage == 0) {
      // Página 1: Info General
      display.setCursor(0, 15);
      display.println("Hardware: ESP32-C3");
      
      display.setCursor(0, 25);
      display.print("RAM Libre: ");
      display.print(ESP.getFreeHeap() / 1024);
      display.println(" KB");
      
      display.setCursor(0, 35);
      display.print("Flash Total: ");
      display.print(ESP.getFlashChipSize() / 1024 / 1024);
      display.println(" MB");
      
      display.setCursor(0, 45);
      display.print("Uptime: ");
      display.print(millis() / 1000);
      display.println(" seg");
      
      display.setCursor(0, 55);
      display.println("Enco:Temp BACK:salir");
    } else {
      // Página 2: Temperatura (NUEVO)
      float tempC = readChipTemperature();
      
      display.setCursor(10, 15);
      display.println("== TEMPERATURA ==");
      
      display.setCursor(25, 28);
      display.setTextSize(2);
      display.print(tempC, 1);
      display.print(" C");
      display.setTextSize(1);
      
      // Barra de temperatura
      display.setCursor(5, 46);
      display.print("[");
      int tempBar = map(constrain(tempC, 24, 36), 24, 36, 0, 20);
      for (int i = 0; i < 18; i++) {
        display.print(i < tempBar ? "#" : ".");
      }
      display.print("]");
      
      display.setCursor(10, 56);
      display.print("24C");
      display.setCursor(105, 56);
      display.print("36C");
    }
    
    display.display();
    
    if (digitalRead(PIN_BACK) == LOW) {
      delay(200);
      exitInfo = true;
    }
    
    delay(50);
  }
}

// ================= FUNCIONES AUXILIARES =================
void showWelcomeAnimation() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(SH110X_WHITE);
  
  // Animación de entrada
  for(int i = 0; i < 3; i++) {
    display.clearDisplay();
    display.setCursor(20, 20);
    display.println("SISTEMA");
    display.setCursor(25, 40);
    display.println("LISTO");
    display.display();
    delay(300);
    
    display.clearDisplay();
    delay(300);
  }
  
  // Dibujar marco final
  display.clearDisplay();
  display.drawRect(0, 0, 128, 64, SH110X_WHITE);
  display.setCursor(35, 28);
  display.println("READY");
  display.display();
  delay(1000);
}

void checkInactivity() {
  unsigned long inactivityTime = millis() - lastActivity;
  
  // Apagar pantalla después de 30 segundos de inactividad
  if (inactivityTime > 30000) {
    display.clearDisplay();
    display.setCursor(30, 28);
    display.println("Modo Sleep");
    display.display();
    
    // Esperar actividad
    while(millis() - lastActivity > 30000) {
      if (digitalRead(PIN_CONFIRM) == LOW || 
          digitalRead(PIN_BACK) == LOW || 
          digitalRead(PIN_ROTARY_BTN) == LOW ||
          encoderPos != lastEncoderPos) {
        lastActivity = millis();
        break;
      }
      delay(100);
    }
  }
}

Explicación Detallada del Código

Estructura Modular

El código está organizado en diferectes secciones:

  • Configuración de pines:
    Define cada conexión física entre el microcontrolador y el módulo.
  • Variables globales:
    Estado del sistema centralizado.
  • Interrupciones:
    Para una respuesta inmediata del encoder.
  • Funciones principal:
    Las clásicas funciones setup() y loop().
  • Módulos específicos:
    Funciones especificas para la pantalla y el menú.

2. Sistema de Menús Inteligente

  • Menú en módulo encoder + pantalla Oled SH1106 1.3'
    Navegación circular: El encoder rota de manera infinita por las opciones.
  • Selección visual:
    La opción actual se muestra invertida
  • Acciones contextuales:
    Cada menú tiene comportamiento único

3. Control de Botones con Debounce


// Detección de flancos (no estado sostenido)
if (confirmState == LOW && lastConfirmState == HIGH) {
    // Solo se activa al presionar, no al mantener
}

Con este código evitamos múltiples activaciones con una sola pulsación.

4. Módulos Interactivos Demostrativos

  • Ajuste de brillo: Control analógico con barra visual.
    Animación barra en módulo encoder + pantalla Oled SH1106 1.3'

  • Contador: Incremento/decremento con encoder
  • Configuración: Menú de opciones ON/OFF
  • Infornación del sistema: Datos del hardware en tiempo real.
    Información del sistema en módulo encoder + pantalla Oled SH1106 1.3'

    Menú en módulo encoder + pantalla Oled SH1106 1.3'

Conclusión: Una combinación casi perfecta

Este proyecto demuestra que incluso en la era digital, la interacción física sigue siendo relevante.

Con la combinación del microcontrolador ESP32-C3 + OLED + Encoder + Botones nos da:

  • Retroalimentación tangible:
    Sabes que has realizado una acción
  • Precisión:
    El encoder permite ajustes finos imposibles en pantallas táctiles
  • Confiabilidad:
    Botones físicos funcionan en cualquier condición
  • Experiencia de usuario:
    Hay placer en usar controles bien diseñados

Despedida: Tu Turno para Crear

Este código es solo el punto de partida. Imagina lo que puedes construir:

  • Un controlador MIDI para música electrónica
  • Una interfaz para una consola retro
  • Un panel de control para tu taller
  • Un sistema de menús para un robot

La belleza del ESP32-C3 Super Mini con OLED SH1106 está en su versatilidad. Es lo suficientemente potente para proyectos complejos, pero lo suficientemente accesible para principiantes.

¡Copia el código, conecta los componentes y siente la satisfacción de controlar algo físico! Comparte tus creaciones y variaciones - la comunidad maker crece cuando compartimos.

Puedes visitar los siguiente enlaces relacionados con pantallas y Arduino:

sábado, 25 de octubre de 2025

ESP8266 y GPS GY-NEO6MV2 y LittleFS

Muy buenas a todos y todas!!!

Seguimos investigando nuevas funciones para el módulo GPS GY-NEO6MV2, un módulo sencillo y económico que podemos implementar en cualquier microcontrolador, desde un Arduino UNO a un ESP32.

Pero como no siempre podemos tener un dispositivo conectado para leer los datos del GPS una solución sencilla es almacenarlos.

En este caso vamos el ESP8266 que será capaz de almacenar los datos relacionados con la posición, velocidad o altitud en su memoria interna, alrededor de 4MB. (Este valor puede variar según el modelo).

Para esta funcionalidad usaremos la libreía LittleFS de la que ya hemos hablado en "ESP8266 Como usar LittleFS para guardar información".

Los datos se almacenan en un archivo con formato JSON al ser uno de los formatos de intercambio de datos sencillo de escribir, ligero y eficiente. Ideal para microcontroladores.

Este archivo con los datos en formato JSON los descargaremos con un programa escrito en Python3 para poder procesarlos posteriormente a diferentes formatos como KML o GPX, muy utilizados por dispositivos deportivos, aplicaciones móviles como GAIA GPS o programas como Google Earth.



Componentes para GPS tracker con Wemos D1 Mini

Para este proyecto solo vamos a necesitar:

  • ESP8266 Wemos D1 Mini (clon)
  • Módulo GPS GY-NEO6MV2
  • Batería

La conexión de los dispositivos GY-NEO6MV2 y Wemos D1 Mini


Librerías necesarias para Wemos D1 Mini

Para programar el Wemos D1 usaremos el IDE de Arduino y las siguiente librerías necesarias para la instalación del sketch:

  • ESP8266Wifi
    Gestiona la conexión WiFi en modo AP o cliente del ESP8266. Para este proyecto usaremos el modo AP.

  • LittleFS
    Sistema de archivos para almacenar datos en la memoria flash (similar a SPIFFS).

  • TinyGPSPlus
    Decodifica datos NMEA del módulo GPS y proporciona métricas (lat, lng, alt, etc.).

  • ESP8266WebServer
    Crea un servidor HTTP para manejar peticiones REST (GET/DELETE).

  • SoftwareSerial
    Permite comunicación serial con hardware no nativo (GPS en este caso).

  • ArduinoJSON Serializa/deserializa datos JSON para la API REST.

Veamos el flujo de trabajo para el ESP8266 Wemos D1 Mini


🖥️ Procesamiento: Wemos D1 Mini

Para que todo el proyecto funcione correctamente necesitaremos algunas librerías externas:

  • Decodificación de datos GPS:
    Convierte el formato NMEA a valores útiles como latitud, longitud, altitud o el número de satélites usando la librería TinyGPSPlus.

  • Formato de datos JSON:
    Serialización y estructura de datos en formato JSON usando la librería ArduinoJson
    
        DynamicJsonDocument doc(200);  // Crea JSON de 200 bytes
        doc["sat"] = String(gps.satellites.value());  // Satélites visibles
        doc["lat"] = gps.location.lat();              // Latitud
        doc["lng"] = gps.location.lng();              // Longitud
        doc["tim"] = gps.time.value();                // Hora (formato hhmmsscc)
        doc["alt"] = gps.altitude.meters();           // Altitud en metros
        doc["spe"] = gps.speed.kmph();                // Velocidad en km/h
    	
        String response;
        serializeJson(doc, response);  // Convierte JSON a String
        

  • Almacenamiento con LittleFS:
    Guarda datos o elimina el archivo en formato JSON en la memoria interna haciendo uso de la librería LittleFS.
  • 
        LittleFS.open("/gps_data.json", "a");	// Abre el archivo "gps_data.json" en el directrorio raíz "/".
        LittleFS.remove("/gps_data.json");  // Elimina el archivo "gps_data.json" en el directrorio raíz "/".
    
      	
  • Servicio Web con 3 endpoints:
    Crearemos un servidor web con la librería ESP8266WebServer que se instala cuando instalamos la placa ESP8266.
    
        ESP8266WebServer server(80);	// Creamos el servidor en el puerto 80.
        server.on("/gps", HTTP_GET, handleGPSData);       // GET /gps → Datos actuales
        
        server.on("/download", HTTP_GET, handleDownload); // GET /download → Archivo JSON
        
        server.on("/clear", HTTP_DELETE, handleClear);    // DELETE /clear → Borra datos
        
        
    Los Endpoints son rutas URL con un metodo HTTP asignado a una función:
    • "/gps", HTTP_GET, handleGPSData:
      Con esta petición obtenemos los datos en tiempo real desde la función: handleGPSData.

    • "/download", HTTP_GET, handleDownload:
      Descarga el historial completo a un archivo con extensión JSON desde la función: handleDownload.

    • "/clear", HTTP_DELETE, handleClear:
      Borra el el archivo con el historial con la función asignada handleClear.

Para comprobar que todo esta correcto podemos abrir el monitor serial y tiene que aparecer una salida similar a la que se muestra a continuación:


Obtención de datos GY-NEO6MV2 desde el navegador

Esta opción es la más sencilla de todas si solo necesitas el archivo JSON. Solo tienes que conectarte con la IP generada por ESP8266, normalmente la 192.168.4.1.

Podemos usar cualquiera de los 3 Endpoints definidos:

  • http://192.168.4.1/gps

  • http://192.168.4.1/download

  • http://192.168.4.1/remove
    con este comando borraremos la memoria del Wemos Mini D1

Obtención de datos desde Python

Otra de las opciones para descargar los datos es el programa que tenemos a continuación:


import requests
import json

# Configuración
IP_WEMOS = "192.168.1.41"  # Reemplaza con la IP de tu Wemos

# 1. Obtener último dato (endpoint /gps)
response = requests.get(f"http://{IP_WEMOS}/gps")
if response.status_code == 200:
    data = response.json()
    print("Última posición:", data["lat"], data["lng"])

# 2. Descargar archivo completo (endpoint /download)
response = requests.get(f"http://{IP_WEMOS}/download")
if response.status_code == 200:
    with open("gps_data.json", "wb") as f:
        f.write(response.content)
    print("Archivo descargado!")


Sencillo pero efectivo, utiliza dos de los EndPoints, /gps y /download que ya hemos visto más arriba y nos proporcionara un archivo llamado gps_data.json.py.

Obtención y procesamiento de datos desde Python

Pero con los datos del JSON "en crudo" es difícil aplicarlos a otras aplicaciones ya que no es un estándar. Para tener una mayor compatibilidad con otros dispositivos existen otros formatos de intercambio de datos como KML y GPX

El formato KML (Keyhole Markup Language)

Es el formato nativo de Google Earth/Google Maps y es ideal para visualización en mapas interactivos y también incluye estilos, colores, iconos personalizables.

  • Excelente para visualización (Google Earth/Maps)
  • Muy usado en aplicaciones web
  • Menos compatible con dispositivos GPS físicos


<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
  <Document>
    <Placemark>
      <name>Zona de Descanso</name>
        <Polygon>
          <extrude>1</extrude>>
          <altitudeMode>absolute</altitudeMode>
          <outerBoundaryIs>
          <LinearRing>
            <coordinates>
              -3.579900,37.707300,1850.0
              -3.580000,37.707300,1850.0
              -3.580000,37.707400,1850.0
              -3.579900,37.707400,1850.0
              -3.579900,37.707300,1850.0
            </coordinates>
          </LinearRing>
        </outerBoundaryIs>
      </Polygon>
    </Placemark>
  </Document>
</kml>


El formato GPX (GPS Exchange Format)

Es el estándar universal para dispositivos GPS, creado específicamente para GPS (no adaptado como KML) y el formato nativo de la mayoría de dispositivos GPS

  • Metadata completa: elevación, tiempo, satélites, etc.
  • Múltiples tracks/routes en un archivo
  • Waypoints (puntos de interés)
  • Estándar abierto y bien documentado


<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="WEMOS D1 Mini" 
    xmlns="http://www.topografix.com/GPX/1/1"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
    
  <!-- Punto 1 -->
  <trkpt lat="37.707408" lon="-3.579952">
    <ele>1850.5</ele>
    <time>2024-01-15T08:30:00Z</time>
    <speed>2.5</speed>
    <sat8></sat>
    <extensions>
      <hr>125</hr>
      <cadence>85</cadence>
      <temp>15</temp>
    </extensions>
  </trkpt>
</gpx>


Esta conversión de formato JSON a KML o GPX la podemos realizar de la mano Python y un programa que reúne todo lo que se ha visto hasta ahora. Conexión, descarga, visualización y conversión.

Esta diseñado para su uso en terminal y con el que obtendremos el archivo .json y lo convertirá a uno de los dos formatos, KML y GPX de manera independiente, o a los dos formatos al mismo tiempo.

Estas son las opciones del menú:

  • 1. Descargar datos del WEMOS (JSON)
  • 2. Cargar datos desde archivo JSON
  • 3. Convertir a KML (Google Earth)
  • 4. Convertir a GPX (Dispositivos GPS)
  • 5. Convertir a AMBOS formatos
  • 6. Ver información del WEMOS
  • 0. Salir


Una vez terminado ya tenemos los archivos con las extensiones KML y GPX.



Vamos a utilizar el archivo generado en formato KML para mostrar la ruta en Google Maps:

Abrimos el navegador en la ruta earth.google.com y en la barra lateral izquierda nos aparece un sigo +.

Se abrirá un desplegable y seleccionamos "Importar KML/KMZ"



Y este es el resultado final de la importacion



Conclusión del proyecto:

La comunicación entre el ESP8266 WEMOS D1 Mini y el módulo GPS GY-NEO6MV2 ha demostrado ser una solución funcional y eficiente para la captura de datos de geolocalización.

El sistema constituye una base sólida para proyectos que requieran tracking GPS, pudiendo extenderse hacia aplicaciones de monitorización de flotas, seguimiento deportivo o recopilación de datos para análisis geográfico.

Es un ejemplo que podemos modular el diseño facilitando la incorporación de sensores adicionales como el ADXL345 o el MPU6050.

Archivos para descargar

Los archivos para descargar el ejemplo los podemos encontrar en GitHub en el repositorio ESP8266 GPS tracker KML converter

Espero que os guste el proyecto!! Saludos!!!


Aquí tienes otros enlaces de este blog relacionados con sensores y Arduino:

miércoles, 15 de octubre de 2025

Macro Keyboard ESP32 con TFT ILI9488 conexión paralelo 8bits

Muy buenas a todas y todos!!!

Seguramente tengas alguno de estos viejos modelos de pantalla que tienen la configuración en formato "sombrero" para los microcontroladores Arduino UNO .

Un display TFT 3,4" de 8 bits conectado en paralelo con el driver ILI9488 y táctil resistivo.

Esta manera de conectar los pines con 8bits más similitudes con un LCD 16x2 que con las modernas pantallas SPI que se manejan con menos hilos de control.

Es decir esta pantalla necesita 8 lineas para la transferencia de datos y otras 5 para la conexiones que manejan la pantalla TFT.




Librería TFT_eSPI

TFT_eSPI es una librería de alto rendimiento específicamente optimizada para controlar pantallas TFT a color con microcontroladores ESP32, ESP8266 y placas STM32.

Está diseñada desde cero para exprimir al máximo el hardware, utilizando SPI con DMA, transacciones asíncronas y manipulación directa de registros.

Su configuración manual permite un rendimiento máximo del display. 

A diferencia de librerías "plug-and-play", requiere que editemos un archivo de configuración "User_Setup.h" para especificar exactamente tu hardware: tipo de controlador, pines GPIO, frecuencia SPI, etc.

También dispone de configuraciones especificas según el tipo de microcontrolador ubicadas en la carpeta "/Users_Setups".

Es, sin duda, la librería más rápida para pantallas TFT con microcontroladores de tipo ESP32/ESP8266.

El uso directo de SPI DMA y transacciones optimizadas hace que la diferencia sea abismal frente a alternativas como Adafruit_GFX.

Aunque parece complejo, el sistema de archivos de configuración permite afinar hasta el último detalle de tu pantalla específica.

Dispone de un soporte masivo de controladores ya que es compatible con más de 50 controladores TFT diferentes. Desde los comunes ILI9341, ST7789, hasta modelos más exóticos.

Características Avanzadas de TFT_eSPI

  • Sprites en memoria para animaciones fluidas
  • Fuentes anti-aliadas con calidad profesional
  • Rotaciones eficientes de pantalla
  • Operaciones de dibujo aceleradas por hardware

ESP32 y TFT ILI9488 paralelo

La conexión de la pantalla con el ESP32 hay que seleccionar las lineas que se describen en la siguiente tabla:

Señal TFT GPIO ESP32
LCD_RST GPIO 15
LCD_CS GPIO 2
LCD_RS (DC) GPIO 32
LCD_WR GPIO 33
LCD_RD GPIO 4
LCD_D0 GPIO 12
LCD_D1 GPIO 13
LCD_D2 GPIO 26
LCD_D3 GPIO 25
LCD_D4 GPIO 17
LCD_D5 GPIO 16
LCD_D6 GPIO 27
LCD_D7 GPIO 14



Configuración de la librería TFT_eSPI

Hay que saber que esta librería tiene 2 archivos principales de configuración:

  • Setup70d_ILI9488_S3_Parallel.h

    Es el archivo de configuración ubicado en la carpeta "/Users_Setups" donde pondremos los pines personalizados tanto del ESP32 como de la pantalla con el controlador ILI9488 que hemos dispuesto en la tabla anterior:


    
        
    #define USER_SETUP_ID 146
    
    #define TFT_PARALLEL_8_BIT
    
    //#define ILI9341_DRIVER
    //#define ST7796_DRIVER
    #define ILI9488_DRIVER
    
    // ESP32 S3 pins used for the parallel interface TFT
    #define TFT_CS    2
    #define TFT_DC   32  // Data Command control pin - must use a GPIO in the range 0-31
    #define TFT_RST  15
    
    #define TFT_WR    33  // Write strobe control pin - must use a GPIO in the range 0-31
    #define TFT_RD    4
    
    #define TFT_D0   12  // Must use GPIO in the range 0-31 for the data bus
    #define TFT_D1   13  // so a single register write sets/clears all bits
    #define TFT_D2   26
    #define TFT_D3   25
    #define TFT_D4   17
    #define TFT_D5   16
    #define TFT_D6   27
    #define TFT_D7   14
    
    #define LOAD_GLCD
    #define LOAD_FONT2
    #define LOAD_FONT4
    #define LOAD_FONT6
    #define LOAD_FONT7
    #define LOAD_FONT8
    #define LOAD_GFXFF
    
    #define SMOOTH_FONT
    
    
  • User_Setup_Select.h
    Este es el archivo que maneja las configuraciones de la librería TFT_eSPI y que hará referencia al archivo "Setup70d_ILI9488_S3_Parallel.h" que acabamos de configurar.
    tenemos que comentar la linea donde esta la referencia al archivo "User_Setup.h":
    
        // Archivo User_Setup_Select.h
        
        // #include <User_Setup.h>  // Default setup is root library folder
        
        #include <User_Setups/Setup70d_ILI9488_S3_Parallel.h> // Setup file for ESP32 S3 with SPI ILI9488
    
        

Recuerda que una vez configuradas las conexiones correctamente, el rendimiento es óptimo porque la librería está compilada exactamente para tu hardware.

Ahora ya podemos probar el ejemplo que proporciona la misma librería como puede ser "Demo_3D_Cube.ino"

IMAGEN DEL CUBO 3D

Problemas con colores en pantalla TFT

Si se tienes problemas con los colores inserta esta línea en el setup() después de haber inicializado la pantalla (tft.begin())


tft.invertDisplay(true);

Con este comando se invierten los colores que se muestran en la pantalla TFT.


Otro problema que podemos encontrar es que no compile bien la librería, prueba a cambiar la versión de la placa antes de compilar. La versión de placa que estamos utilizando es la versión 2.0.17 de ESPressif Systems.

Librería LittleFS.

Sobre LittleFS ya hemos hablado antes en este blog pero enfocado al microcontroladores ESP8266.

Esta vez la usaremos para subir los iconos que se mostrarán posteriormente en la pantalla en formato .png.

Subir archivos a la memoria flash del ESP32 con Arduino IDE 2

Subiremos los archivos siguiendo unos pocos pasos:

  • Primero cerramos el IDE 2 de Arduino.

  • Después necesitamos descargar el archivo: arduino-littlefs-upload-1.x.x.vsix desde GitHub

  • Ahora nos situamos en la carpeta .ArduinoIDE y creamos un directorio llamado "plugins". Rcuerda que esta carpeta .ArduinoIDE puede estar oculta.

  • Finalmente solo tenemos que compiar el archivo "arduino-littlefs-upload-1.x.x.vsix" a la carpeta ".ArduinoIDE/plugins"

Abrimos el IDE de Arduino y pulsamos "Ctrl + Mayus + P" para buscar en la barra "upload", tal y como se muestra en la imagen:


Archivos PNG con librería LittleFS

Al pulsar sobre la opción "Upload LittleFS to Pico/ESP8266/ESP32" se copiarán todos los archivos que estén en la carpeta "data" dentro de la carpeta del sketch a la memoria flash del dispositivo ESP32 DOIT DEVKIT V1.

La memoria flash disponible es de 4MB, suficiente para unos cuantos archivos  con formato .png con un tamaño reducido.


Librería PNGdec

Esta librería fue una gran sorpresa, de uso sencillo y muy rápida pese a ser una pantalla tft con conexión en paralelo. Puedes encontrar la librería PNGdec en GitHub.

Como siempre no estaría bien que algo funcionará a la primera así que el una vez abierto el ejemplo de la librería llamado LittleFS_PNG.ino podemos compilar y nos aparecerá el siguiente error:


Compilation error: 
  invalid conversion from 'void (*)(PNGDRAW*)' {aka 'void (*)(png_draw_tag*)'} 
  to 'int (*)(PNGDRAW*)' {aka 'int (*)(png_draw_tag*)'} [-fpermissive]

Para solucionar este error solo hay que buscar la función "int pngDraw(PNGDRAW *pDraw)" y añadir "return 1; al final tal y como se muestra en el siguiente fragmento de código:


int pngDraw(PNGDRAW *pDraw) 
{
  uint16_t lineBuffer[MAX_IMAGE_WIDTH];
  png.getLineAsRGB565(pDraw, lineBuffer, PNG_RGB565_BIG_ENDIAN, 0xffffffff);
  tft.pushImage(xImage, yImage + pDraw->y, pDraw->iWidth, 1, lineBuffer);
  return 1;
}


Librería Adafruit TouchScreen

Esta ha sido la parte con la que más tiempo he perdido. Todo se ha complicado por no encontrar los pines que corresponden a XP y XM debido a que cada panel tiene una configuración de pines diferente.

También por que uno de los cabnles estaba en mal estado y, aunque permitia el encendido de la pantalla, no hací un buen contacto al hacer la lectura analógica.

Puedes descargar la librería Adafruit TouchScreen en GitHub.

La solución la encontré gracias a un Arduino UNO junto con la librería MCUFRIEND_kbv y el ejemplo: "TouchScreen_Calibr_native".

Con la pantalla en el Arduino UNO, cargamos el sketch TouchScreen_Calibr_native y esperamos a la salida serial


TouchScreen.h GFX Calibration
Making all control and bus pins INPUT_PULLUP
Typical 30k Analog pullup with corresponding pin
would read low when digital is written LOW
e.g. reads ~25 for 300R X direction
e.g. reads ~30 for 500R Y direction

Testing : (A2, D8) = 35
Testing : (A3, D9) = 25

ID = 0x9488

Nos interesan las lineas "Testing" con la que podemos realizar la siguiente conversión:

Arduino UNO TFT ESP32 Touch
A2 RS GPIO 32 YP
A3 CS GPIO 2 XM
D8 D0 GPIO 12 YM
D9 D1 GPIO 13 XP

Esto datos los pasaremos al programa principal en el apartado "Configuración TouchScreen":


// Configuración TouchScreen

#define YP 32  // Tiene que ser un pin analógico, usa la notación "ADC" 
#define XM 2   // Tiene que ser un pin analógico, usa la notación "ADC"

#define YM 12
#define XP 13


Ahora con la certeza de que el panel resistivo funcionaba, sustituimos los 4 cables que le corresponden al panel y volvemos a probar con el ESP32

Esta configuración puede cambiar

Otro de los problemas que surgió fue la lectura de la pulsación Z que corresponde a la variable "p.z" daba lecturas muy "extrañas" debido a la resolución de la lectura analógica del que dispone el ESP32 que es de 12bits frente a los 10bits que tiene, por ejemplo, el Arduino UNO.



Arduino UNO ESP32
Resolución ADC 10 bits 12 bits
Valores Posibles 0 - 1023
(2¹⁰ = 1024)
0 - 4095
(2¹² = 4096)

Una vez tenemos este dato, vamos al archivo "TouchScreen.cpp" que estará en la carpeta de las librerías /Adafruit_TouchScreen y buscamos las siguientes lineas en la función "TSPoint TouchScreen::getPoint(void)" y las sustituimos por esta:


// Función: TSPoint TouchScreen::getPoint(void)
// rtouch /= 1024;
rtouch /= 4096;

y

// z = (1023 - (z2 - z1));
z = (4095 - (z2 - z1));


Y aquí es donde daba el fallo al leer la . Para esta pantalla solo hay que cambiar estos parámetros en la función "TSPoint TouchScreen::getPoint(void)", pero para otras también cambiamos todos los valores 1024 y 1023 por 4095 y 4096 de todas las funciones en las que aparezcan dichos valores.

Ahora sí, la pantalla táctil funciona perfectamente y lo podemos comprobar en el monitor serial


Hasta aquí la primera parte para la configuración de la pantalla TFT y conexión paralela de 8bits con un microcontrolador ESP32, este es un pequeño resumen de lo que hemos realizado hasta ahora:

  • Configuración de la librería TFT_eSPI para pantalla de 8 bits.

  • Repaso a la librería LittleFS y la instalación de LittleFS upload en el IDE de Arduino.

  • Instalación de la librería PNGdec y la solución a posibles problemas.

  • Por último la librería Adafruit_TouchScreen y su configuración para el panel resistivo junto con pruebas con MCUFRIEND_kbv

Configuración para la lectura de teclas

Ahora tenemos que configurar el sistema para poder leer los comandos que nos manda el ESP32. La conexión con el PC sera realizada con el Bluetooth que nos proporciona el mismo microcontrolador ESP32.

Una vez que se realice la conexión el sistema leerá los comandos seriales y realizará las pulsaciones del el teclado o la llamada al programa que hayamos designado a dicha aplicación.

Comunicación Bluetooth entre ESP32 y PC

Aquí entran en juego las dos partes, la del microcontrolador y la conexión bluetooth que dispongamos en el PC. Como nota, podemos cambiar la conexión Bluetooth por la conexión serial por USB, pero eso lo dejamos para una actualización.

Librería BluetoothSerial en ESP32

Esta ĺibrería es interna y la proporciona el mismo IDE de Arduino así que la tenemos disponible desde el primer momento. Si necesitas más información este es la documentación de la librería BluetoothSerial.h


#include "BluetoothSerial.h"
void setup()
{
  SerialBT.begin("ESP32-ITB"); // Nombre del dispositivo Bluetooth
}

void loop()
{
  // Programa principal
  Lectura_Tecla_Pantalla()
}

void Lectura_Tecla_Pantalla()
{
  SerialBT.println("ACK: COMANDO");
}


Ahora que ya hemos visto como se conectará el microcontrolador vamos con la parte del PC de la mano, por supuesto, del lenguaje de programación Python.


Programa en Python para lectura del bluetooth

Python es la navaja suiza de los lenguajes de programación y gracias a esta versatilidad nos proporciona todas las librerías necesarias para este proyecto.

Estas son las librerías Python que vamos a utilizar.

Librerías externas

  • bluetooth
    Sera la encargada de comunicarnos con el ESP32. La puedes encontrar en GitHub como Pybluez

  • keyboard
    Esta es la librería encargada de transformar los datos seriales que proporciona el bluetooth a pulsaciones del teclado. Puedes consultar la librería keyboard de Boppreh en GitHub.

Librerías incluidas en Python

  • sys
    (System) Necesaria para interactuar con el intérprete de Python y el sistema como listar directorios o ejecutar comandos. Viene incluida con Python.

  • os
    (Operating System) Interfaz con el sistema operativo: archivos, directorios, procesos.

  • subprocess
    Ejecutar comandos externos y gestionar sus flujos de entrada/salida. Necesaria para la ejecución de los programas que designemos a las teclas.

  • time
    Realiza las pausas necesarias para no saturar la recepción de datos.

Comunicación ESP32 Bluetooth PC

Para diferenciar entre los comandos que vamos a recibir y el comando que vamos a ejecutar los vamos a almacenar en un diccionario de la siguiente manera:


# Mapeo de teclas (lo que escribes → lo que se simula)
COMANDOS = {
    'item1': 'inkscape',
    'item2': 'audacity',
    'item3': 'subl',
    'item4': 'spectacle',

    'item5': 'ctrl+z',         
    'item6': 'ctrl+x', 
    'item7': 'ctrl+c', 
    'item8': 'ctrl+v',
    
    'save': 'ctrl+s',
    'select all': 'ctrl+a',
    'find': 'ctrl+f'
    }

De esta manera podemos crear un diccionario con los atajos de teclado, comandos del sistema o aplicaciones del sistema. Así tenemos centralizados los comandos facilitando su modificación.

En la primera parte tenemos el Item que hemos seleccionado en el display. En la segunda parte del diccionario pondremos la aplicación o el atajo de teclado que queramos.

NOTA:

Depende del sistema que estes usando puede que el funcionamiento no sea el correcto. Para este ejemplo se ha utilizado una distribución basada en Debian.

Cargar ESP32 Macro keyborad en el inicio

Para cargar el programa para que se inicie al encender el ordenador tenemos que incluirlo en el crontab de usuario con el siguiente comando:


@reboot sleep 30 && sudo -u YOUR_USER /usr/bin/python3 /home/peyutron/PythonProjects/Comunicacion/Bluetooth/Bluetooth-HID-ESP32/main.py >> /home/YOUR_USER/esp32_cron.log 2>&1


Conclusión del proyecto

Hardware Especializado:

  1. Microcontrolador ESP32 con Bluetooth integrado.

  2. Pantalla TFT con interfaz táctil resistiva/capacitiva.

  3. Comunicación paralela 8 bits para control de pantalla.

Software de Control:

  1. TFT_eSPI - Librería optimizada para gráficos en ESP32.

  2. Protocolo Bluetooth RFCOMM - Comunicación serial inalámbrica.

  3. Sistema de comandos - Diccionario personalizable de acciones.

  4. Flexibilidad Total - Configuración ilimitada de comandos

  • Descargar los archivos para ESP32 y el progrma en Python3 de los que consta el proyectoTFT-eSPI 8bits ILI9488 Macro Keypad en GitHub
  • También es interesante echar un vistazo al datasheet del controlador ILI9488 para entender mejor su funcionamiento.

    Saludos!!!

    Puedes visitar los siguiente enlaces relacionados con pantallas y Arduino: