viernes, 30 de enero de 2026

ESP32-C3 Super Mini con OLED SH1106: Tutorial de menús y controles físicos

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.


🛠️ Componentes necesarios
ESP32 C3 SuperMini
ESP32 C3 SuperMini Placa de Desarrollo Inalámbrica WiFi ESP32 C3 SuperMini.
SH1106 OLED con módulo codificador rotatorio EC11 interfaz I2C
SH1106 OLED con módulo codificador rotatorio EC11 interfaz I2C" SH1106 OLED con módulo codificador rotatorio EC11 interfaz I2C.
Cables Dupont hembra hembra
Cables Dupont Kit de 40pin 10cm 2,54mm 1pin hembra a hembra .

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 GPIO20
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:

No hay comentarios :