![]() |
| 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
![]() |
| 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

- 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"
Librería Adafruit SH110X para pantalla OLED 1.3"
Código Ejemplar: Sistema de Menús con Control Físico CompletoAquí 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
- 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.
- Contador: Incremento/decremento con encoder
- Configuración: Menú de opciones ON/OFF
- Infornación del sistema: Datos del hardware en tiempo real.
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