Mostrando entradas con la etiqueta Oled. Mostrar todas las entradas
Mostrando entradas con la etiqueta Oled. Mostrar todas las entradas

viernes, 31 de julio de 2026

Ejemplos Arduino: LEDC (LED Control) en el ESP32

Muy buenas a todos y todas a la vuelta al canal!!

El periférico LEDC está diseñado principalmente para controlar la intensidad de LEDs, aunque también pero también es muy útil para generar señales PWM para otros propósitos tales como controlar motores DC.

El ESP32-C3 cuenta con 6 canales que pueden generar formas de onda independientes, ideales para controlar LEDs o dispositivos que necesiten PWM tan solo cambiando algunos parámetros de la configuración como el temporizador,la confirmación del canal y el cambio de la señal PWM.


¿Cuando se implementó ledc?

La API de LEDC para Arduino-ESP32 está documentada y disponible como parte del núcleo (core) de ESP32 en Arduino.
La versión actual (a partir de la 3.0.0) incluye cambios importantes en la sintaxis, como la sustitución de:


ledcAttachPin() // V < 3.0.0

por


ledcAttach() // Versión > 3.0.0

Para la versión 3.0.0 del core de ESP32 para Arduino, ya no se usa la función ledcSetup() ni la función ledcAttachPin(), sino que se recomienda utilizar la función ledcAttach(pin, freq, resolution)
ESP32 C3 Mini 0.42 Pinout
ESP32 C3 mini 0.42" pinout

Funciones y definiciones relacionadas

1. ledcAttach()

Función:


bool ledcAttach(uint8_t pin, uint32_t freq, uint8_t resolution);

La función ledcAttach configura un pin para PWM con la frecuencia (uint32_t freq) y resolución (uint8_t resolution)indicadas.
El canal LEDC se selecciona automáticamente.

Devuelve true si la configuración es exitosa.


2. ledcAttachChannel()

Función:


bool ledcAttachChannel(uint8_t pin, uint32_t freq, uint8_t resolution, uint8_t channel);

Con ledcAttachChannel configuramos un pin para PWM con la frecuencia, resolución y canal específicos.

Devuelve true si la configuración es exitosa.


3. ledcWrite()

Función:


void ledcWrite(uint8_t pin, uint32_t duty);

Establece el ciclo de trabajo (duty cycle) para el pin LEDC.

El valor de duty depende de la resolución: para 10 bits, el rango es de 0 a 1023.


4. ledcRead()

Función:


uint32_t ledcRead(uint8_t pin);

Lee el ciclo de trabajo configurado para el pin.


5. ledcReadFreq()

Función:


uint32_t ledcReadFreq(uint8_t pin);

Con ledcReadFreq lee la frecuencia configurada para el pin que hemos seleccionado.


Relación entre frecuencia y resolución


6. ledcDetach()

Función:


bool ledcDetach(uint8_t pin);

Desvincula el pin del periférico LEDC para usarlo como GPIO digital.


7. ledcChangeFrequency()

Función:


uint32_t ledcChangeFrequency(uint8_t pin, uint32_t freq, uint8_t resolution);

Cambia la frecuencia y resolución de un pin LEDC ya configurado.


8. ledcFade()

Función:


bool ledcFade(uint8_t pin, uint32_t start_duty, uint32_t target_duty, int max_fade_time_ms);

Configura e inicia un desvanecimiento (fade) automático por hardware desde un valor inicial hasta un valor objetivo.

Devuelve true si se inicia correctamente.


9. ledcWriteTone()

Función:


uint32_t ledcWriteTone(uint8_t pin, uint32_t freq);

Genera un tono PWM al 50% en la frecuencia especificada.

10. ledcWriteNote()

Función:


uint32_t ledcWriteNote(uint8_t pin, note_t note, uint8_t octave);

Por último tenemos ledcWriteNote que genera un tono a partir de una nota musical y una octava.


LEDC ¿es mejor o peor que analogWrite?

En ESP32, analogWrite() es una capa de abstracción sobre LEDC.
La principal diferencia es que analogWrite() simplifica el uso, pero limita el control.


Característica analogWrite() LEDC nativo
Resolución Fija a 8 bits (0-255) Configurable (1-14 bits en ESP32-C3)
Frecuencia Fija (~5 kHz) Totalmente configurable
Gestión de canales Automática, puede causar conflictos Manual, 6 canales independientes
Funciones avanzadas No disponible Fade por hardware, interrupciones
Complejidad de código Muy baja Media

Conclusión: analogWrite() es perfecto para prototipos rápidos y proyectos simples.
LEDC es necesario cuando se necesita precisión, frecuencias personalizadas o múltiples canales PWM sin interferencias .


Relación entre frecuencia y resolución

En el ESP32-C3, la frecuencia y la resolución están interrelacionadas. A mayor frecuencia, menor resolución máxima disponible, y viceversa.

Ejemplos prácticos:

  • 5 kHz con 13 bits: 8192 niveles de intensidad.
  • 40 MHz con 1 bit: Solo 50% fijo.

Si se intenta configurar una combinación no soportada, la API mostrará un error:


E (196) ledc: requested frequency and duty resolution cannot be achieved, try reducing freq_hz or duty_resolution. div_param=128 


Ejemplos para ESP32-C3 con pantalla OLED 0.42"

La placa "ESP32-C3 OLED 0.42" tiene la pantalla conectada por I2C: en el pin GPIO5 corresponde a SDA y el pin GPIO6 para SCL.


Ejemplo 1: Uso de analogWrite (simplificado)

Primero veamos un ejemplo clásico programa con analogWrite()



#include <Wire.h> 
#include <Adafruit_SSD1306.h>

#define OLED_SDA 5
#define OLED_SCL 6
#define OLED_RESET -1
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

#define LED_PIN 8

void setup() {
  Wire.begin(OLED_SDA, OLED_SCL);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  for (int brightness = 0; brightness <= 255; brightness += 5) {
    analogWrite(LED_PIN, brightness);

    display.clearDisplay();
    display.setCursor(30, 30);
    display.print("AnalogWrite: ");
    display.println(brightness);

    int porcentaje = map(brightness, 0, 255, 0, 100);
    display.setCursor(30, 40);
    display.print("Inten.: ");
    display.print(porcentaje);
    display.println("%");

    display.display();
    delay(30);
  }
  for (int brightness = 255; brightness >= 0; brightness -= 5) {
    analogWrite(LED_PIN, brightness);

    display.clearDisplay();
    display.setCursor(30, 30);
    display.print("AnalogWrite: ");
    display.println(brightness);

    int porcentaje = map(brightness, 0, 255, 0, 100);
    display.setCursor(30, 40);
	display.print("Inten.: ");
    display.print(porcentaje);
    display.println("%");

    display.display();
    delay(30);
  }
}


Ejemplo 2: Ledc PWM en LED y mostrar valor en OLED (I2C)

En este ejemplo tendremos la salida del valor Ledc en la pantalla OLED 0.42". El pin 8 del LED builtin el estado de Ledc.

NOTA: el encendido del LED builtin esta invertido, es decir cuando la señal Ledc esta en 0 el LED builtin estará a su máxima intensidad y apagado en su valor más alto.



 

#include <Wire.h> 
#include <Adafruit_SSD1306.h>

#define OLED_SDA 5
#define OLED_SCL 6
#define OLED_RESET -1
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

#define LED_PIN 8
#define PWM_FREQ 5000
#define PWM_RES 10
#define MAX_DUTY ((1 << PWM_RES) - 1) // 1023 para 10 bits

int duty = 0;
int step = 5;

void setup() {
  Wire.begin(OLED_SDA, OLED_SCL);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  ledcAttach(LED_PIN, PWM_FREQ, PWM_RES);
  ledcWrite(LED_PIN, 0);
}

void loop() {
  duty += step;
  if (duty >= MAX_DUTY || duty <= 0) step = -step;

  ledcWrite(LED_PIN, duty);

  display.clearDisplay();
  display.setCursor(30, 30);
  display.print("Duty: ");
  display.setCursor(30, 40);
  display.print(duty);
  display.print(" / ");
  display.println(MAX_DUTY);

  int porcentaje = map(duty, 0, MAX_DUTY, 0, 100);
  display.setCursor(30, 50);
  display.print("Intensidad: ");
  display.print(porcentaje);
  display.println("%");

  display.display();
  delay(20);
}


Ejemplo 3: Ledc fade automático por hardware y mostrar estado


 
#include <Wire.h> 
#include <Adafruit_SSD1306.h>

#define OLED_SDA 5
#define OLED_SCL 6
#define OLED_RESET -1
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

#define LED_PIN 4
#define PWM_CHANNEL 0
#define PWM_FREQ 5000
#define PWM_RES 10
#define MAX_DUTY ((1 << PWM_RES) - 1)

void setup() {
  Wire.begin(OLED_SDA, OLED_SCL);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  ledcAttachChannel(LED_PIN, PWM_FREQ, PWM_RES, PWM_CHANNEL);
 ledcWrite(LED_PIN, 0);

  display.display();
  delay(1000);
}

void loop() {
  display.clearDisplay();
  display.setCursor(30, 30);
  display.println("Fade IN");
  display.display();

  ledcFade(LED_PIN, 0, MAX_DUTY, 2000);
  delay(2500);

  display.clearDisplay();
  display.setCursor(30, 30);
  display.println("Fade OUT");
  display.display();

  ledcFade(LED_PIN, MAX_DUTY, 0, 2000);
  delay(2500);
}


Ejemplo 4: Ledc PWM para ESC de motor brushed

Para controlar un ESC, se necesita una señal PWM de 50 Hz con pulsos entre 1000 µs y 2000 µs.



#define ESC_PIN 8 
#define PWM_FREQ 50 
#define PWM_RES 14 
#define MAX_DUTY ((1 << PWM_RES) - 1)

// Para 14 bits: 16384 = 100% (20 ms)
// 1000 µs = 1 ms → (1/20)16384 = 819
// 2000 µs = 2 ms → (2/20)16384 = 1638

void setup() {
  ledcAttach(ESC_PIN, PWM_FREQ, PWM_RES);
  ledcWrite(ESC_PIN, 819); // Neutro
  delay(3000);
}

void loop() {
  ledcWrite(ESC_PIN, 819); // Neutro
  delay(2000);
  ledcWrite(ESC_PIN, 1638); // Máximo
  delay(2000);
}


También puedes revisar el repositorio supermini_esp32c3_sketches para ejemplos muy variados y completos.


Conclusión sobre LEDC en el ESP32-C3

El periférico LEDC del ESP32-C3 es una herramienta extremadamente versátil y potente para generar señales PWM, muy superior a la función analogWrite() que muchos conocen de Arduino.

Mientras que analogWrite() está limitada a 8 bits de resolución y frecuencias fijas, LEDC ofrece un control total con resoluciones de hasta 14 bits y frecuencias completamente configurables.
Esto hace que Ledc sea ideal para aplicaciones que requieren precisión, como el control de motores DC, servomotores, atenuación de LEDs profesionales o incluso generación de tonos y notas musicales.

Lo bueno:

El ESP32-C3 dispone de 6 canales independientes, lo que permite manejar múltiples dispositivos PWM simultáneamente sin interferencias.
Una de sus características más destacadas es la capacidad de realizar desvanecimientos (fade) automáticos por hardware, lo que descarga al procesador de esta tarea y permite transiciones suaves sin necesidad de interrupciones constantes.


Lo malo:

Sin embargo, hay que tener en cuenta algunas limitaciones importantes. El ESP32-C3 solo funciona en modo "low speed", a diferencia de otros ESP32 que también tienen modo "high speed".
Además, existe una relación de compromiso entre frecuencia y resolución: a mayor frecuencia, menor resolución disponible, y viceversa. Por ejemplo, a 5 kHz se puede alcanzar 13 bits de resolución, pero a 40 MHz apenas se obtiene 1 bit.

Aquí tienes otros enlaces de este blog relacionados con el microcontrolador ESP32 y Arduino:


martes, 12 de mayo de 2026

ESP32-C3 con pantalla Oled 0.42" con ejemplos

ESP32 C3 Oled 0.42 logo

Muy buenas a todas y todos!!!

Después de la buena experiencia programando el ESP32-C3 mini junto con el módulo de la pantalla SH1106 y el encoder, y otro proyecto que aún estoy probando.

Este pequeño no comparte los mismos pines que el ESP32C3 mini así que si ya tienes un montaje realizado para el mini no te servirá, pero eso no quita que sus pines GPIO sean igual de versátiles, veamos las características y los pines de esta pequeña placa de desarrollo:



Hardware e interfaces

  • 22 pines GPIO programables.
  • Comunicación vía UART, SPI, I2C, I2S y USB Serie/JTAG.
  • Funciones analógicas: dos SAR de 12 bits Convertidores analógico-digitales (ADC), sensor de temperatura integrado.

Procesador y memoria

ESP32 C3 Oled 0.42 chip
  • Procesador: ESP32-C3FN4/FH4, RISC-V de 32 bits de un solo núcleo, hasta 160 MHz.
  • Memoria flash: 4 MB.
  • Pantalla: OLED de 0,42 pulgadas para información visual directa.
  • Conectividad: Wi-Fi de modo dual (802.11b/g/n) y Bluetooth 5 (LE, malla).
  • Programación: Fácil vía USB, en este caso USB-C.
  • Antena: Antena cerámica integrada.


Funciones de Wi-Fi y Bluetooth

El ESP32 integra conectividad dual Wi-Fi y Bluetooth 5 LE, optimizada para aplicaciones que requieren velocidad y eficiencia. Estas son sus capacidades más relevantes:

  • Wi-Fi con velocidades de datos de hasta 150 Mbps, compatible con ancho de banda de 20/40 MHz.
  • Bluetooth 5 LE con alta potencia de transmisión de hasta 20 dBm y red mallada.
  • Coexistencia interna entre Wi-Fi y Bluetooth para el uso de antena compartida.
  • Compatible con múltiples modos Wi-Fi: Estación, SoftAP y modo promiscuo.

Bajo consumo de energía y seguridad

El ESP32 ha sido diseñado para ofrecer un rendimiento óptimo en aplicaciones IoT sin sacrificar la eficiencia energética ni la protección de los datos.
A continuación, se detallan sus características más destacadas en este ámbito:

  • Cuatro modos de ahorro de energía, incluyendo un modo de suspensión profunda con un consumo de corriente de tan solo 5 µA.
  • Funciones de seguridad como arranque seguro y cifrado de memoria flash.
  • Aceleración criptográfica por hardware: AES, SHA, RSA y más.

Aplicaciones

Gracias a su bajo consumo de energía y su versátil conectividad, esta placa es perfecta para numerosas aplicaciones como:

  • Dispositivos para el hogar inteligente
  • Automatización industrial
  • Atención médica
  • Electrónica de consumo
  • Agricultura inteligente
  • Robots de servicio
  • Sensores IoT de bajo consumo y registradores de datos


La placa ESP32-C3 con pantalla OLED de 0,42".


Esta placa IoT compacta se basa en el chip ESP32-C3 (RISC-V) e incorpora una pantalla OLED de 0,42 pulgadas. Como la distribución de pines y el controlador de pantalla no son iguales que la de los módulos ESP32 estándar.



A continuación os dejamos la información necesaria para el correcto funcionamiento de la placa.

Especificaciones del hardware

  • Chip: ESP32-C3FN4 con una Memoria Flash de 4 MB, Wi-Fi y Bluetooth 5.0 BLE.
  • Conexión USB: USB nativo USB-C Para Habilitar la comunicación con el monitor serial active en el IDE de Arduino "USB CDC al arrancar".
  • Pantalla OLED: 0,42 pulgadas con una resolución de 72 x 40 píxeles.
  • Dimensiones: Es muy compacta aproximadamente 20 x 25 mm.


Asignación de pines y conexiones

ESP32 C3 Oled 0.42 cubo

La documentación en línea suele ser confusa. Utilice la siguiente asignación de pines para esta placa específica:

  • I2C OLED SDA: GPIO 5 (aunque generalmente se indica el 8.
  • I2C OLED SCL: GPIO 6.
  • LED Integrado: GPIO 8.
  • Boton de reset: GPIO 9.
  • Tx; GPIO 21.
  • Rx: GPIO 20.

Configurando del IDE de Arduino

Para programar placa correctamente vamos seleccionar la siguiente configuración en el IDE de Arduino:

  • Placa: Módulo de desarrollo ESP32C3 Dev Module.
  • USB CDC al arrancar: Habilitado (Para salida de Serial.print).
  • Modo de flasheo: DIO (predeterminado).

Algunas veces podemos experimentar problemas con la carga del programa, para solucionarlo, mantenga presionado el botón BOOT, presione RESET brevemente y luego suelte BOOT. Esto fuerza a la placa al entrar modo de programación.

Códigos de ejemplo

Comprobación de pines: Parpadeo del LED integrado

Usa este sencillo bucle en el pin 8 que controla el LED integrado.

// GPIO 8 es el LED integrado (LOW = ON)
#define LED_PIN 8 

void setup() {
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_PIN, LOW);  // LED On
  delay(500);
  digitalWrite(LED_PIN, HIGH); // LED Off
  delay(500);
}

Control de la pantalla OLED

La pantalla utiliza un controlador SSD1306, pero tiene una resolución física es de solo 72x40 píxeles.
Sin embargo, el búfer interno es de 128x64.

Para solucionar este problema tenemos que aplicar un desplazamiento en los ejes X e Y para que el texto sea visible.

Proporción Oled 0.42 con Oled 0.96

Necesitamos tener instalada la biblioteca U8g2 (de olikraus) a través del Administrador de bibliotecas de Arduino o desde el Repositorio de GitHub.


#include <U8g2lib.h>
#include <Wire.h>

#define SCL 6
#define SDA 5

// Iniciando un ESP32-C3 con I2C en los pines 5 SDA y 6 SCL.
// Usaremos el driver estandar de 128x64 pero solo una parte de la pantalla.
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, SCL, SDA);

// Los Offsets se requieren para centrar la salida dentro del buffer 128x64.
const int VISIBLE_X0 = 28;
const int VISIBLE_Y0 = 24;

//configuramos la pantalla de 0.42 pulgadas 
const int VISIBLE_W  = 72;
const int VISIBLE_H  = 40;

void setup(void) {
  delay(1000); // Una pausa para estabilizar el ESP
  
  u8g2.begin();
  u8g2.setContrast(255);      // Máximo brillo
  u8g2.setBusClock(400000);   // 400kHz velocidad del I2C
  u8g2.setFont(u8g2_font_ncenB10_tr); // Elegimos el tipo de letra
}

void loop(void) {
  u8g2.clearBuffer(); // Limpiamos el buffer interno
  
  // Dibuja un marco para probar el área visible
  u8g2.drawFrame(VISIBLE_X0, VISIBLE_Y0, VISIBLE_W, VISIBLE_H);
  
  // Dibujar texto teniendo en cuenta el desplazamiento en x y el desplazamiento en y
  u8g2.setCursor(VISIBLE_X0 + 6, VISIBLE_Y0 + 15); 
  u8g2.print("ESP32-C3");
  u8g2.setCursor(VISIBLE_X0 + 25, VISIBLE_Y0 + 30); 
  u8g2.print("ITB");
  
  u8g2.sendBuffer(); // Enviar datos a la pantalla
  delay(1000);
}


Código: Texto con scroll lateral

El texto se mueve horizontalmente dentro de los 72 píxeles visibles y se dibuja un marco opcional para recordar los límites.
Puedes ajustar delay(30) para cambiar velocidad.

ESP32 C3 Oled 0.42 scroll lateral

#include <U8g2lib.h>

// Configuración de la pantalla (driver SSD1306, 128x64, I2C hardware)
// Pines: SDA = 6, SCL = 5 (ajusta según tu conexión)
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, 6, 5);

// --- Constantes de área visible real (72x40 con offset 28,24) ---
const int VISIBLE_X0 = 28;
const int VISIBLE_Y0 = 24;
const int VISIBLE_W  = 72;
const int VISIBLE_H  = 40;

// Texto a desplazar
const char* texto = "  Infotronikblog desplazamiento lateral  ";
int desplazamiento = 0;

// Función para dibujar texto teniendo en cuenta el recorte (clipping manual)
void dibujarTextoScroll(int offsetX) {
  // El área de dibujo está limitada a [VISIBLE_X0, VISIBLE_X0+VISIBLE_W]
  // Usamos setFont y drawStr con coordenadas absolutas (incluyendo offset)
  u8g2.setFont(u8g2_font_ncenB08_tr);
  int anchoTexto = u8g2.getStrWidth(texto);
  
  // Dibujamos el texto varias veces para efecto continuo
  int x = VISIBLE_X0 + offsetX;
  int y = VISIBLE_Y0 + 20;  // centrado vertical aprox.
  
  u8g2.drawStr(x, y, texto);
  // Si el texto se sale por la derecha, dibujamos continuación
  if (x + anchoTexto < VISIBLE_X0 + VISIBLE_W) {
    u8g2.drawStr(x + anchoTexto, y, texto);
  }
  // Si se sale por la izquierda, dibujamos la parte que falta
  if (x < VISIBLE_X0) {
    u8g2.drawStr(x + anchoTexto, y, texto);
  }
}

void setup() {
  u8g2.begin();
  u8g2.setFontMode(1);          // Modo transparente
  u8g2.setFontDirection(0);
}

void loop() {
  u8g2.firstPage();
  do {
    // Limpia toda la pantalla (128x64) pero solo se verá el área activa
    u8g2.clearBuffer();
    
    // Dibujar un marco alrededor del área visible (opcional, para referencia)
    u8g2.drawFrame(VISIBLE_X0 - 1, VISIBLE_Y0 - 1, VISIBLE_W + 2, VISIBLE_H + 2);
    
    // Dibujar el texto en scroll
    dibujarTextoScroll(desplazamiento);
    
  } while (u8g2.nextPage());
  
  // Actualizar desplazamiento (velocidad ajustable)
  desplazamiento--;
  if (desplazamiento < -u8g2.getStrWidth(texto)) {
    desplazamiento = VISIBLE_W;
  }
  
  delay(30);  // 30 ms entre frames
}

Código: Cubo 3D + rebotes en los bordes

Un pequeño cubo 3D que se mueve con velocidades vx y vy iniciales semi-aleatorias.
Con rebotes realistas: al tocar cualquier borde del área visible (72×40 con offsets), invierte la componente de velocidad correspondiente, quedando siempre dentro de los límites considerando el medio lado del cubo para que no se salga.

ESP32 C3 Oled 0.42 ejemplo cubo

#include <U8g2lib.h>
#include <math.h>

U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, 6, 5);

// Área visible real de tu pantalla (offset 28,24)
const int X0 = 28;
const int Y0 = 24;
const int W  = 72;
const int H  = 40;

// Tamaño del cubo (lado en píxeles)
const int LADO = 14;   // Ajustado a 14 como pediste

// Centro del cubo (coordenadas absolutas) - posición inicial en el centro del área visible
float cx = X0 + W/2.0;
float cy = Y0 + H/2.0;

// Velocidades de desplazamiento (píxeles por frame)
float vx = 1.2;
float vy = 0.9;

// Ángulo de rotación (radianes)
float anguloY = 0.0;

// Límites de rebote: el cubo no debe salirse del rectángulo visible
// Como el cubo se dibuja centrado en (cx,cy), el medio lado se extiende a cada lado.
float limiteIzq  = X0 + LADO/2.0;
float limiteDer  = X0 + W - LADO/2.0;
float limiteSup  = Y0 + LADO/2.0;
float limiteInf  = Y0 + H - LADO/2.0;

// Proyección isométrica con rotación alrededor del eje Y
void projectPoint(float x, float y, float z, float angY, int &sx, int &sy) {
  // Rotación en Y
  float x1 = x * cos(angY) + z * sin(angY);
  float z1 = -x * sin(angY) + z * cos(angY);
  // Proyección simple (sin perspectiva, solo desplazamiento)
  sx = (int)(cx + x1);
  sy = (int)(cy - y + z1 * 0.3);   // ligero efecto de profundidad
}

void drawCubo(float angY) {
  // Vértices relativos al centro del cubo (coordenadas modelo)
  int vertices[8][3] = {
    {-LADO/2, -LADO/2, -LADO/2}, // 0
    { LADO/2, -LADO/2, -LADO/2}, // 1
    { LADO/2, -LADO/2,  LADO/2}, // 2
    {-LADO/2, -LADO/2,  LADO/2}, // 3
    {-LADO/2,  LADO/2, -LADO/2}, // 4
    { LADO/2,  LADO/2, -LADO/2}, // 5
    { LADO/2,  LADO/2,  LADO/2}, // 6
    {-LADO/2,  LADO/2,  LADO/2}  // 7
  };
  
  int projX[8], projY[8];
  for (int i = 0; i < 8; i++) {
    projectPoint(vertices[i][0], vertices[i][1], vertices[i][2], angY, projX[i], projY[i]);
  }
  
  // Aristas (12 líneas)
  int edges[12][2] = {
    {0,1},{1,2},{2,3},{3,0}, // base inferior
    {4,5},{5,6},{6,7},{7,4}, // base superior
    {0,4},{1,5},{2,6},{3,7}  // verticales
  };
  
  for (int i = 0; i < 12; i++) {
    u8g2.drawLine(projX[edges[i][0]], projY[edges[i][0]],
                  projX[edges[i][1]], projY[edges[i][1]]);
  }
}

void setup() {
  u8g2.begin();
  randomSeed(analogRead(0));
  
  // Velocidades iniciales aleatorias (para dar variedad)
  vx = (random(80, 160) / 100.0) * 1.5;
  vy = (random(60, 140) / 100.0) * 1.5;
  if (random(2) == 0) vx = -vx;
  if (random(2) == 0) vy = -vy;
  
  // Asegurar que la posición inicial esté dentro de los límites
  if (cx < limiteIzq) cx = limiteIzq;
  if (cx > limiteDer) cx = limiteDer;
  if (cy < limiteSup) cy = limiteSup;
  if (cy > limiteInf) cy = limiteInf;
}

void loop() {
  // 1. Actualizar posición del cubo
  cx += vx;
  cy += vy;
  
  // 2. Rebotes en los bordes
  if (cx < limiteIzq) { cx = limiteIzq; vx = -vx; }
  if (cx > limiteDer) { cx = limiteDer; vx = -vx; }
  if (cy < limiteSup) { cy = limiteSup; vy = -vy; }
  if (cy > limiteInf) { cy = limiteInf; vy = -vy; }
  
  // 3. Rotación continua
  anguloY += 0.06;   // velocidad angular (ajústala a gusto)
  if (anguloY > 2 * M_PI) anguloY -= 2 * M_PI;
  
  // 4. Dibujar en la pantalla
  u8g2.firstPage();
  do {
    u8g2.clearBuffer();
    
    // (Opcional) marco del área visible - puedes comentarlo si prefieres sin marco
    u8g2.drawFrame(X0, Y0, W, H);
    
    // Dibujar el cubo rotando
    drawCubo(anguloY);
    
  } while (u8g2.nextPage());
  
  delay(30);  // controla la velocidad de animación
}

Puedes visitar los siguiente enlaces relacionados con el microcontrolador ESP32 y Arduino:

viernes, 20 de febrero de 2026

Ojos animados en OLED con Raspberry Pi: Tutorial Python con expresiones

Muy buenas a todos y todas!!!

Ya hemos visto como configurar una pantalla OLED de 128x64 en nuestra Raspberry Pi junto con la librería Adafruit.

Y como veía mi cluster Pi un poco "triste" decidí animarlo un poco con una pantalla OLED de tipo I2c y algunas animaciones programadas en Python que la hacen más "friendly".


Expresiones y dirección de la mirada

Podemos alternar entre las diferentes expresiones:

  • Normal
  • Contento
  • Triste
  • Sorpresa
  • Dormido

También podemos dirigir la mirada en las siguientes direcciones:

  • Izquierda
  • Centro
  • Derecha



Pero también podemos llamar de manera independiente a cada una de las expresiones con la función "crear_ojos" y "animar_parpadeo"

def crear_ojos(expresion="normal", direccion_mirada="centro", estado_parpadeo=1.0):
    """
    Ojos con sistema de parpadeo realista
    estado_parpadeo: 1.0 = totalmente abierto, 0.0 = totalmente cerrado
    """

Para crear la animación de parpadeo:


def animar_parpadeo(expresion="normal", direccion_mirada="centro"):
    """Animación completa de parpadeo"""
    

Programa completo en python:



# Simulación de ojos con expresiones en OLED 128x64

import time
import random
import board
import adafruit_ssd1306
from PIL import Image, ImageDraw, ImageFont

# Use for I2C.
i2c = board.I2C()  # uses board.SCL and board.SDA

try:
        disp = adafruit_ssd1306.SSD1306_I2C(128, 64, i2c, addr=0x3C)
        print("\nAdafruit SSD1306 ok")

except Exception as errors:
        print("\nError Adafruit SSD1306:",str(errors), "\nPlease check Raspi-config\n")
        exit()


def crear_ojos(expresion="normal", direccion_mirada="centro", estado_parpadeo=1.0):
    """
    Ojos con sistema de parpadeo realista
    estado_parpadeo: 1.0 = totalmente abierto, 0.0 = totalmente cerrado
    """
    # Crear imagen evitando la franja amarilla superior
    img = Image.new('1', (128, 64), 0)
    draw = ImageDraw.Draw(img)
    
    # Posiciones fijas
    centro_y = 40
    ojo_izq_x = 45
    ojo_der_x = 83
    
    # Tamaño máximo reducido a 40
    max_tam = 40
    
    # Radio de redondeo
    radio = 15
    
    # Determinar tamaños según dirección de mirada
    if direccion_mirada == "izquierda":
        ancho_izq, alto_izq = max_tam, max_tam
        ancho_der, alto_der = 30, 30
    elif direccion_mirada == "derecha":
        ancho_izq, alto_izq = 30, 30
        ancho_der, alto_der = max_tam, max_tam
    else:  # centro
        ancho_izq, alto_izq = 35, 40
        ancho_der, alto_der = 35, 40
    
    def dibujar_ojo(x, y, ancho, alto, radio, apertura=1.0):
        """Dibuja un ojo con sistema de parpadeo"""
        if apertura >= 1.0:
            # Ojo totalmente abierto
            draw.rounded_rectangle([
                (x - ancho//2, y - alto//2),
                (x + ancho//2, y + alto//2)
            ], radius=radio, outline=1, fill=1)
        elif apertura <= 0.0:
            # Ojo totalmente cerrado (línea)
            draw.line([
                (x - ancho//2, y),
                (x + ancho//2, y)
            ], fill=1, width=3)
        else:
            # Ojo parcialmente cerrado - calcular altura visible
            alto_visible = int(alto * apertura)
            if alto_visible < 2:
                alto_visible = 2
            
            # Dibujar el ojo recortado según la apertura
            draw.rounded_rectangle([
                (x - ancho//2, y - alto_visible//2),
                (x + ancho//2, y + alto_visible//2)
            ], radius=min(radio, alto_visible//2), outline=1, fill=1)
    
    # Aplicar expresiones (modifican la apertura base)
    apertura_base = estado_parpadeo
    
    if expresion == "normal":
        apertura = apertura_base
    elif expresion == "contento":
        apertura = apertura_base * 0.7  # Ojos más cerrados
    elif expresion == "triste":
        apertura = apertura_base * 0.9  # Ojos casi normales pero caídos
    elif expresion == "sorpresa":
        apertura = min(apertura_base * 1.2, 1.0)  # Ojos más abiertos
    elif expresion == "dormido":
        apertura = apertura_base * 0.3  # Ojos casi cerrados
    
    # Dibujar ojos con la apertura calculada
    dibujar_ojo(ojo_izq_x, centro_y, ancho_izq, alto_izq, radio, apertura)
    dibujar_ojo(ojo_der_x, centro_y, ancho_der, alto_der, radio, apertura)
    
    return img

def animar_parpadeo(expresion="normal", direccion_mirada="centro"):
    """Animación completa de parpadeo"""
    frames = []
    
    # Fases del parpadeo (apertura de 1.0 a 0.0 y vuelta)
    fases = [1.0, 0.8, 0.5, 0.2, 0.0, 0.2, 0.5, 0.8, 1.0]
    
    for apertura in fases:
        frame = crear_ojos(expresion, direccion_mirada, apertura)
        frames.append(frame)
    
    return frames


# Bucle principal 
if __name__ == "__main__":
    expresiones = ["normal", "contento", "triste", "sorpresa", "dormido"]
    direcciones = ["izquierda", "centro", "derecha"]

    print("Mostrando expresiones sencillas...")
    print("Presiona Ctrl+C para detener")

    try:
        while True:
            for expresion in expresiones:
                for direccion in direcciones:

                    print(f"Expresión: {expresion} dirección: {direccion}")

                    img = crear_ojos(expresion, direccion)
                    disp.image(img)
                    disp.show()
                    time.sleep(5)
                                        # 30% de probabilidad de guiño entre animaciones
                    if random.random() < 0.2:
                        #lado_guiño = random.choice(["izquierdo", "derecho"])
                        print(f"¡papadeo {expresion}!")
                        frames_parpadeo = animar_parpadeo(expresion, direccion)

                        for frame in frames_parpadeo:
                            disp.image(frame)
                            disp.show()

    except KeyboardInterrupt:
        print("\nBucle detenido")

Es un programa que podemos ampliar con otras expresiones o incluso hacer llamadas desde otros programas por ejemplo conectandolo a una Raspi cam para simular que la mirada nos sigue.

Otro idea que se me ocurre es que las llamadas sean según diferentes eventos, como que despierte con el acceso por ssh o utilizar el sensor de temperatura para que nos "mire mal" si nuestra Raspberry Pi se pone lujuriosa.

Añadir programa al iniciar Raspberry PI

Lo ideal es que este programa se inicie al encender la Raspberry pi. Es un buen indicador para saber que la Raspberry Pi arranco sin problemas.

esto lo podemos hacer editando el crontab con el comando "crontab -e" (no hace falta sudo).


@reboot sleep 30 && python3 ~/Eyes_simulation/eyes_simulation.py > ~/Eyes_simulation/eyes.log 2>&1

Recuerda poner "@reboot sleep 30" para darle un tiempo prudente a la Raspberry para inicializar el puerto I2c.

Y siempre puedes ver si todo esta bien o hay algún fallo consultando el archivo que se genera en el inicio llamado "eyes.log".


Conclusión:

Es un programa sencillo que le dará a nuestra maquina un poco más de carisma (por si le faltaba un poco) y que deje de ser un puñado de lucecitas encima del escritorio.

Antes de montar el cluster con las 3 Raspberry Pi, la que máquina que usaba para estas cosas empezó a dar problemas con la tarjeta y lo pude detectar ya que no me miraba igual que siempre...

¿Le puedo poner nombre?


Aquí tienes otros enlaces de este blog relacionados con Raspberry Pi:

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.


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:

viernes, 25 de octubre de 2024

ESP32 con OLED 128x32: Velocímetro y dashboard para vehículos RC

Muy buenas a todos Arduineros y Arduineras!!!

ESP32 Engine sound oled 128x32 I2c_adafruit
Librería Adafruit
Ya os presente uno de los mejores proyectos para Radio Control el ESP32 Engine sound. En este proyecto una de las últimas opciones que implementaron fue la pantalla con velocímetro o dashboard

En ese apartado, se sincroniza una pequeña pantalla de 160x80 con conexión SPI. Simplemente genial.

¿Cual es mi problema con esto? Pues en principio ninguno, pero no disponía de una pantalla SPI, pero si una OLED de 128x32.


conexión I2C

ESP32 Engine sound con librería U8GLib
Librería U8g2Lib

El panel original viene un frok del ESP32 Engine Sound original al que como hemos comentado, se le añadió la pantalla con cuatro relojes, Velocidad, RPM, Gasolina y ADblue y los iconos de las funciones más comunes en todos los vehículos.

Como me pareció sencillo, decidí probar si ese código se podría portar a una pantalla I2C. Para la comunicación I2C con la pantalla he probado las 2 librerías más famosas para la pantalla SSD1306, la librería U8g2Lib y la librería Adafruit_SSD1306 siendo esta última la que me mejor a funcionado.


El proyecto lo he dividido en 2 partes, la primera es para usarlo de manera independiente y la segunda es con la pantalla ya integrada en el sistema ESP32 Engine Sound.

Los componentes que se han utilizado para el proyecto son los siguientes:

  • ESP32 WROOM Dev Kit V1

  • Oled SSD1306 128x32 I2C

Para no liar con lo que ya esta hecho, primero he renombrado los archivos "dashboardc.cpp" y "dashboard.h" a "dashboardi2c.cpp" y "dashboardi2c.h". y borrar todo lo que no esta relacionado con la pantalla "Garimaldi"

Para el manejo de la pantalla he usado la librería de Adafruit_SSD1306.h y Adafrit_GFX.h. Esto ha llevado cambios en la inicialización del programa y los tamaños de las esferas. Estos valores los cambiamos en el archivo dashboardi2c.h

En este archivo encontraremos los valores predefinidos como el radio de las esferas, iconos, colores o la posición de los iconos en la pantalla. Después de las definiciones, esta la clase "Dashboard" sustituida por "DashboardI2c" junto con sus funciones, menos la función "drawLogo()" que no afecta al funcionamiento normal del programa.

Puedes encontrar los archivos para su descarga al final de este articulo.

Una vez que ya he probado las funciones y parece que todo funciona bien, añado un puerto para el I2c en la placa con el programa RC Engine Sound ESP32 y copio los archivos a la carpeta "RC_Engine_Sound_ESP32/src/src/"

En el archivo 9_Dashboard.h añado la siguiente linea para poder activar o desactivar la pantalla:


#define I2C_DASHBOARD 


Con la definición creada ya podemos añadir los cambios en el archivo "src.ino". Primero declaramos el objeto dashboard:


// Other objects -----
// Dashboard

#ifdef I2C_DASHBOARD
  DashboardI2c dashboard;
#else
  Dashboard dashboard;
#endif


En la función void setup() tenemos que desactivar los pines que correspondes al puerto I2C que en la placa DOIT ESP32 DEVKIT V1 corresponden a los pines GPIO21 (SCL) y GPIO22(SDA) y en el programa a las funciones "Beacon1" y "Cab light":


// Added for I2c dashboard
#if not defined I2C_DASHBOARD
  beaconLight1.begin(BEACON_LIGHT1_PIN, 9, 20000);  // Timer 9, 20kHz
  cabLight.begin(CABLIGHT_PIN, 12, 20000); // Timer 12, 20kHz
#endif

Un poco más abajo en la misma función setup() ponemos un mensaje para mostrar en el monitor serial que la pantalla esta activada:


#if defined I2C_DASHBOARD
  // Dashboard setup
  Serial.printf("I2C_DASHBOARD enabled. Pins 21 (beacon1), 22 (cablights) not usable!\n");
  Serial.printf("-------------------------------------\n");
  dashboard.init(dashRotation);
#endif


Como la tasa de refresco de la pantalla es bastante baja, el tiempo en el que se muestra tiene que ser mayor, si no gastará mucho tiempo "dibujando" y el programa ira excesivamente lento, para esto añadimos en la función "updateDashboard()" lo siguiente:


#ifdef I2C_DASHBOARD
  if (millis() - lastFrameTime > 300) // We need to save processing time!
#else
  if (millis() - lastFrameTime > 40) // 40) // We need to save processing time!
#endif
  

Y por último actualizamos la pantalla en la función loop():


#ifdef I2C_DASHBOARD
    updateDashboard();
#endif


Por supuesto todo esto ha sido para poder implementar la pantalla junto con su código en el programa original RC Engine Sound, pero no solo de código vive el hombre, así que este es el diagrama de conexiones que utilizo actualmente:



Al conservar los nombres que le dieron a las funciones en la clase Dashboard original, solo con unos pocos cambios podemos hacer compatibles esta pantalla I2C con el sistema RC Engine Sound ESP32. Si bien no dispone de todo el colorido y la rapidez de la pantalla SPI. Quedará muy bien en cualquier vehículo RC.

Como comente al principio, la librería U8G2Lib que, aunque si que dibuja la pantalla, la tasa de refresco es muy inferior a la tasa de refresco de la librería Adafruit. Esto hace que la pantalla no muestre los datos de una manera fluida e incluso produzca cuelgues en el ESP32.



Tenéis más información sobre el sistema en el artículo:Man Kat con Rc Engine Sound esp32.

Los archivos, tanto para la librería Adafruit como para U8g2Lib, para el velocímetro los puedes descargar en:

Descargar ESP32 oled i2c 128x32_dashboard desde GitHub

El programa original, en ingles, lo podéis encontrar en el siguiente enlace:

Repositorio oficial de RC Engine Sound ESP32 en GitHub

Puedes hacer las placas para RC Engine Sound ESP32 de una manera profesional desde el repositorio PCBway.com de "Thediyguy

Espero que os guste esta pequeña modificación para este excelente programa y para los que os gusta trastear, esta es una manera más clara de aprender a como dibujar en una pantalla de estas características.

Saludos a todos y todas!!!

Otros artículos los ESP8266 y ESP32 que te pueden interesar: