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:

No hay comentarios :

Publicar un comentario