← - Guía y ejemplos de Arduino. | Contenidos | Siguiente: Componente ESPHome →
Guía y Ejemplos de ESP-IDF
Biblioteca universal de atenuadores para ESP32. Guía y ejemplos del marco de trabajo ESP-IDF en C.
Antes de comenzar, lea la descripción general de la biblioteca: Biblioteca universal para ESP32
Requisitos y Compatibilidad
- Versión mínima de ESP-IDF: 5.3
- Chips soportados: ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6
- La biblioteca utiliza estándar
Kconfigpara la configuración de compilación (renombrado deKconfig.txten v2.0.0)
Novedades en v2.0.0
La versión 2.0.0 es una reescritura importante de la arquitectura interna. La API pública es totalmente compatible hacia atrás — no se requieren cambios en el código de su aplicación.
Mejoras internas:
- Arquitectura modular — el código se divide en 7 módulos internos (motor de fase, gestor de canales, tablas de curva, núcleo ISR, etc.). El único encabezado público
rbdimmerESP32.hsigue siendo el único que necesita incluir. - Todos los ISR utilizan
IRAM_ATTRy los temporizadores utilizan despachoESP_TIMER_ISRpara temporización determinística de submicrosegundos. - Puerta de ruido de cero cruzado — una ventana de rebote configurable rechaza el ruido eléctrico y disparos falsos en la entrada de cero cruzado. Predeterminado: 3000 us.
- ISR de dos pasos para sincronización multicanal — cuando múltiples canales comparten una fase, el ISR los preordena por retraso y dispara pulsos TRIAC en una sola pasada consolidada, eliminando la variabilidad de tiempo entre canales.
- Configuración de compilación
Kconfig— el archivo ahora se llamaKconfig(convención estándar de ESP-IDF; anteriormenteKconfig.txt).
Nuevos parámetros Kconfig:
| Parámetro | Predeterminado | Descripción |
|---|---|---|
CONFIG_RBDIMMER_ZC_DEBOUNCE_US |
3000 | Ventana de rebote de cero cruzado en microsegundos |
CONFIG_RBDIMMER_MIN_DELAY_US |
100 | Retardo mínimo de disparo TRIAC en microsegundos |
CONFIG_RBDIMMER_LEVEL_MIN |
3 | Nivel mínimo de atenuación (%). Los valores por debajo de este se tratan como APAGADO |
CONFIG_RBDIMMER_LEVEL_MAX |
99 | Nivel máximo de atenuación (%) |
Instalación
Uso de CMake con ESP-IDF
- Descargar la biblioteca
rbdimmerESP32del repositorio de GitHub:
git clone https://github.com/your-username/rbdimmerESP32 components/rbdimmer- Configure your project's
CMakeLists.txtto include the library:
# Main project CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(your_project_name)- Add component dependency in your application's
CMakeLists.txt:
# App CMakeLists.txt
idf_component_register(
SRCS "main.c"
INCLUDE_DIRS "."
REQUIRES rbdimmer
)- The library's
CMakeLists.txtandKconfigare included automatically when placed incomponents/rbdimmer/. The componentCMakeLists.txtregisters sources, includes, and dependencies:
# components/rbdimmer/CMakeLists.txt
idf_component_register(
SRCS "rbdimmerESP32.c"
INCLUDE_DIRS "include"
REQUIRES driver esp_timer freertos
)Kconfig (convención estándar de ESP-IDF). Si está actualizando desde una versión anterior que usaba Kconfig.txt, cámbielo a Kconfig.Configuración de Kconfig
La biblioteca expone parámetros de ajuste a través del sistema menuconfig de ESP-IDF. Para ajustarlos:
idf.py menuconfig
# Navigate to: Component config → RBDimmer ConfigurationOpciones disponibles:
- Rebote de Cero Cruzado (us) —
CONFIG_RBDIMMER_ZC_DEBOUNCE_US(predeterminado 3000). Aumente si ve disparos falsos de cero cruzado por ruido eléctrico. - Retardo Mínimo TRIAC (us) —
CONFIG_RBDIMMER_MIN_DELAY_US(predeterminado 100). Evita que el TRIAC dispare demasiado cerca del cero cruzado, lo que puede causar parpadeo a alto brillo. - Nivel Mín (%) —
CONFIG_RBDIMMER_LEVEL_MIN(predeterminado 3). Los niveles por debajo de este umbral se tratan como APAGADO para evitar comportamiento inestable de TRIAC. - Nivel Máx (%) —
CONFIG_RBDIMMER_LEVEL_MAX(predeterminado 99). Limita el ángulo de disparo máximo.
Conexión de Hardware
Instrucciones para conectar el atenuador al microcontrolador y la carga AC:
- Conecte el Pin de Cero Cruzado a cualquier GPIO que tenga funcionalidad ISR. Consulte la documentación de su chip ESP32
- Conecte el Pin del Atenuador a cualquier GPIO
- VCC a 3.3V (para ESP32, VCC = 3.3V)
- GND a GND
Ejemplo Básico (ESP-IDF / C)
#include
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "rbdimmerESP32.h"
static const char *TAG = "DIMMER_EXAMPLE";
// Pins
#define ZERO_CROSS_PIN 18 // Zero-Cross pin
#define DIMMER_PIN 19 // Dimming control pin
#define PHASE_NUM 0 // Phase N (0 for single phase)
// Global variables. Dimmer object
rbdimmer_channel_t* dimmer_channel = NULL;
void app_main(void)
{
ESP_LOGI(TAG, "AC Dimmer Test");
// Dimmer lib init
if (rbdimmer_init() != RBDIMMER_OK) {
ESP_LOGE(TAG, "Failed to initialize AC Dimmer library");
return;
}
// Zero-cross detector and phase registry
if (rbdimmer_register_zero_cross(ZERO_CROSS_PIN, PHASE_NUM, 0) != RBDIMMER_OK) {
ESP_LOGE(TAG, "Failed to register zero-cross detector");
return;
}
// Dimmer channel. Configuration data structure.
rbdimmer_config_t config_channel = {
.gpio_pin = DIMMER_PIN,
.phase = PHASE_NUM,
.initial_level = 50, // Initial dimming level 50%
.curve_type = RBDIMMER_CURVE_RMS // Level Curve Selection. RMS-curve
};
if (rbdimmer_create_channel(&config_channel, &dimmer_channel) != RBDIMMER_OK) {
ESP_LOGE(TAG, "Failed to create dimmer channel");
return;
}
ESP_LOGI(TAG, "AC Dimmer initialized successfully");
// Main loop
while (1) {
// dimming from 10% to 90% with step 10
for (int brightness = 10; brightness <= 90; brightness += 10) {
ESP_LOGI(TAG, "Setting brightness to %d%%", brightness);
rbdimmer_set_level(dimmer_channel, brightness);
vTaskDelay(2000 / portTICK_PERIOD_MS);
}
// Smooth transition from current level to 0 level in 5 sec
ESP_LOGI(TAG, "Smooth transition to 0%%");
rbdimmer_set_level_transition(dimmer_channel, 0, 5000);
vTaskDelay(6000 / portTICK_PERIOD_MS); // delay 6 sec
// Smooth transition from current level (0) to 100 level in 5 sec
ESP_LOGI(TAG, "Smooth transition to 100%%");
rbdimmer_set_level_transition(dimmer_channel, 100, 5000);
vTaskDelay(6000 / portTICK_PERIOD_MS); // delay 6 sec
}
} Referencia de API
Operación de Biblioteca
Preparación:
- Inicialice la biblioteca utilizando
rbdimmer_init() - Registre el detector de cruce por cero utilizando
rbdimmer_register_zero_cross() - Cree un canal de atenuador utilizando
rbdimmer_create_channel()
Control de Atenuación:
- Establezca el nivel de atenuación con
rbdimmer_set_level(). El nivel de atenuación se establece en el rango 0(APAGADO) ~ 100(ENCENDIDO) - Transición de atenuación suave con
rbdimmer_set_level_transition(). Transición suave del nivel actual al nivel establecido durante un período de tiempo (en milisegundos, 1s=1000ms)
Estructuras de Datos
rbdimmer_config_t
typedef struct {
uint8_t gpio_pin; // Dimmer GPIO
uint8_t phase; // Phase number
uint8_t initial_level; // Initial dimming level
rbdimmer_curve_t curve_type; // Level Curve type
} rbdimmer_config_t;Enumeraciones
rbdimmer_curve_t
Tipos de curvas de nivel:
typedef enum {
RBDIMMER_CURVE_LINEAR, // Linear curve
RBDIMMER_CURVE_RMS, // RMS-compensated curve (for incandescent bulbs)
RBDIMMER_CURVE_LOGARITHMIC // Logarithmic curve (for dimmable LED)
} rbdimmer_curve_t;rbdimmer_err_t
Respuestas de función de biblioteca:
typedef enum {
RBDIMMER_OK = 0, // Successful execution
RBDIMMER_ERR_INVALID_ARG, // Invalid argument
RBDIMMER_ERR_NO_MEMORY, // Not enough memory
RBDIMMER_ERR_NOT_FOUND, // Object not found
RBDIMMER_ERR_ALREADY_EXIST, // Object already exists
RBDIMMER_ERR_TIMER_FAILED, // Timer initialization error
RBDIMMER_ERR_GPIO_FAILED // GPIO initialization error
} rbdimmer_err_t;Constantes y Macros
En v2.0.0, la mayoría de los parámetros de ajuste se han movido a Kconfig (ver Configuración de Kconfig arriba). Las siguientes constantes permanecen en rbdimmerESP32.h:
#define RBDIMMER_MAX_PHASES 4 // Maximum number of phases
#define RBDIMMER_MAX_CHANNELS 8 // Maximum number of channels
#define RBDIMMER_DEFAULT_PULSE_WIDTH_US 50 // Pulse width (us)Los siguientes ahora se pueden configurar a través de idf.py menuconfig:
// Kconfig defaults (override via menuconfig):
// CONFIG_RBDIMMER_ZC_DEBOUNCE_US = 3000 // Zero-cross debounce (us)
// CONFIG_RBDIMMER_MIN_DELAY_US = 100 // Minimum TRIAC delay (us)
// CONFIG_RBDIMMER_LEVEL_MIN = 3 // Minimum level (%)
// CONFIG_RBDIMMER_LEVEL_MAX = 99 // Maximum level (%)RBDIMMER_DEFAULT_PULSE_WIDTH_US, ya que esto está relacionado con las características de hardware del atenuador.Funciones
Inicialización y Configuración
// Initialize the library
rbdimmer_err_t rbdimmer_init(void);
// Register a zero-cross detector
rbdimmer_err_t rbdimmer_register_zero_cross(uint8_t pin, uint8_t phase, uint16_t frequency);
// Create a dimmer channel
rbdimmer_err_t rbdimmer_create_channel(rbdimmer_config_t* config, rbdimmer_channel_t** channel);
// Set callback function for zero-cross events
rbdimmer_err_t rbdimmer_set_callback(uint8_t phase, void (*callback)(void*), void* user_data);Control de Atenuación
// Set dimming level
rbdimmer_err_t rbdimmer_set_level(rbdimmer_channel_t* channel, uint8_t level_percent);
// Set brightness with smooth transition
rbdimmer_err_t rbdimmer_set_level_transition(rbdimmer_channel_t* channel, uint8_t level_percent, uint32_t transition_ms);
// Set brightness curve type
rbdimmer_err_t rbdimmer_set_curve(rbdimmer_channel_t* channel, rbdimmer_curve_t curve_type);
// Activate/deactivate channel
rbdimmer_err_t rbdimmer_set_active(rbdimmer_channel_t* channel, bool active);Consultas de Información
// Get current channel brightness
uint8_t rbdimmer_get_level(rbdimmer_channel_t* channel);
// Get measured mains frequency for the specified phase
uint16_t rbdimmer_get_frequency(uint8_t phase);
// Check if channel is active
bool rbdimmer_is_active(rbdimmer_channel_t* channel);
// Get channel curve type
rbdimmer_curve_t rbdimmer_get_curve(rbdimmer_channel_t* channel);
// Get current channel delay
uint32_t rbdimmer_get_delay(rbdimmer_channel_t* channel);Guía Paso a Paso
Estructura de Proyecto
your_project/
├── CMakeLists.txt
├── main/
│ ├── CMakeLists.txt
│ └── main.c
└── components/
└── rbdimmer/
├── CMakeLists.txt
├── Kconfig
├── include/
│ └── rbdimmer.h
└── rbdimmerESP32.cPasos de Implementación
- Defina la biblioteca y los pines en su archivo
main.c:
#include "rbdimmer.h"
// Pins
#define ZERO_CROSS_PIN 18 // Zero-Cross pin
#define DIMMER_PIN 19 // Dimming control pin
#define PHASE_NUM 0 // Phase N (0 for single phase)- Cree un objeto atenuador (uno por cada atenuador):
rbdimmer_channel_t* dimmer_channel = NULL;- Inicialice la biblioteca del atenuador:
rbdimmer_init();- Registre el detector de cero cruzado y la fase:
rbdimmer_register_zero_cross(ZERO_CROSS_PIN, PHASE_NUM, 0);- Configure el canal del atenuador y créelo:
rbdimmer_config_t config_channel = {
.gpio_pin = DIMMER_PIN,
.phase = PHASE_NUM,
.initial_level = 50, // Initial dimming level 50%
.curve_type = RBDIMMER_CURVE_RMS // Level Curve Selection. RMS-curve
};
rbdimmer_create_channel(&config_channel, &dimmer_channel);- Controle la atenuación:
// Set specific level
rbdimmer_set_level(dimmer_channel, level);
// Smooth transition
rbdimmer_set_level_transition(dimmer_channel, 0, 5000);Ejemplos Avanzados
Sistemas Atenuadores Multicanal
#include
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "rbdimmer.h"
#define ZERO_CROSS_PIN 18
#define DIMMER_PIN_1 19
#define DIMMER_PIN_2 21
#define PHASE_NUM 0
static const char *TAG = "DIMMER_EXAMPLE";
rbdimmer_channel_t* channel1 = NULL;
rbdimmer_channel_t* channel2 = NULL;
void app_main(void)
{
// Initialize library
rbdimmer_init();
// Register zero-cross detector (one per phase)
rbdimmer_register_zero_cross(ZERO_CROSS_PIN, PHASE_NUM, 0);
// Create first channel (incandescent bulbs)
rbdimmer_config_t config1 = {
.gpio_pin = DIMMER_PIN_1,
.phase = PHASE_NUM,
.initial_level = 50,
.curve_type = RBDIMMER_CURVE_RMS
};
rbdimmer_create_channel(&config1, &channel1);
// Create second channel (dimmable LED lighting)
rbdimmer_config_t config2 = {
.gpio_pin = DIMMER_PIN_2,
.phase = PHASE_NUM,
.initial_level = 50,
.curve_type = RBDIMMER_CURVE_LOGARITHMIC
};
rbdimmer_create_channel(&config2, &channel2);
// Main control loop
while (1) {
// Control channels independently
rbdimmer_set_level(channel1, 75);
rbdimmer_set_level(channel2, 25);
vTaskDelay(2000 / portTICK_PERIOD_MS);
rbdimmer_set_level(channel1, 25);
rbdimmer_set_level(channel2, 75);
vTaskDelay(2000 / portTICK_PERIOD_MS);
}
} Utilización de Funciones de Devolución de Llamada de Interrupción de Cero Cruzado
#include
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "rbdimmer.h"
#define ZERO_CROSS_PIN 18
#define DIMMER_PIN 19
#define LED_PIN 2 // Built-in LED for zero-cross visualization
#define PHASE_NUM 0
static const char *TAG = "DIMMER_CALLBACK";
rbdimmer_channel_t* dimmer = NULL;
QueueHandle_t zero_cross_queue;
// Simple message for our queue
typedef struct {
uint32_t timestamp;
} ZeroCrossEvent_t;
// Callback function for zero-cross events
void zero_cross_callback(void* arg)
{
ZeroCrossEvent_t event;
event.timestamp = esp_timer_get_time() / 1000; // Current time in ms
// Send to queue from ISR
BaseType_t higher_priority_task_woken = pdFALSE;
xQueueSendFromISR(zero_cross_queue, &event, &higher_priority_task_woken);
if (higher_priority_task_woken) {
portYIELD_FROM_ISR();
}
}
// Task to process zero-cross events
void zero_cross_processing_task(void *pvParameters)
{
ZeroCrossEvent_t event;
while (1) {
if (xQueueReceive(zero_cross_queue, &event, portMAX_DELAY)) {
// Toggle LED to visualize zero-crossing
gpio_set_level(LED_PIN, !gpio_get_level(LED_PIN));
// Additional processing can be done here safely
ESP_LOGI(TAG, "Zero-cross event at time: %lu ms", event.timestamp);
}
}
}
void app_main(void)
{
// Setup LED
gpio_reset_pin(LED_PIN);
gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);
// Create the queue
zero_cross_queue = xQueueCreate(10, sizeof(ZeroCrossEvent_t));
if (zero_cross_queue == NULL) {
ESP_LOGE(TAG, "Failed to create queue");
return;
}
// Create the task to process zero-cross events
BaseType_t task_created = xTaskCreate(
zero_cross_processing_task,
"ZeroCrossTask",
2048,
NULL,
5,
NULL
);
if (task_created != pdPASS) {
ESP_LOGE(TAG, "Failed to create task");
return;
}
// Initialize dimmer
rbdimmer_init();
rbdimmer_register_zero_cross(ZERO_CROSS_PIN, PHASE_NUM, 0);
// Register callback
rbdimmer_set_callback(PHASE_NUM, zero_cross_callback, NULL);
// Create dimmer channel
rbdimmer_config_t config = {
.gpio_pin = DIMMER_PIN,
.phase = PHASE_NUM,
.initial_level = 60,
.curve_type = RBDIMMER_CURVE_RMS
};
rbdimmer_create_channel(&config, &dimmer);
ESP_LOGI(TAG, "Dimmer with callback initialized");
// Main loop - print frequency information
while (1) {
uint16_t frequency = rbdimmer_get_frequency(PHASE_NUM);
ESP_LOGI(TAG, "Detected frequency: %u Hz", frequency);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
} Sistemas Multifase
#include
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "rbdimmer.h"
#define ZERO_CROSS_PIN_PHASE_A 18
#define ZERO_CROSS_PIN_PHASE_B 19
#define ZERO_CROSS_PIN_PHASE_C 21
#define DIMMER_PIN_PHASE_A 22
#define DIMMER_PIN_PHASE_B 23
#define DIMMER_PIN_PHASE_C 25
#define PHASE_A 0
#define PHASE_B 1
#define PHASE_C 2
static const char *TAG = "DIMMER_MULTIPHASE";
rbdimmer_channel_t* channel_a = NULL;
rbdimmer_channel_t* channel_b = NULL;
rbdimmer_channel_t* channel_c = NULL;
void app_main(void)
{
// Initialize library
rbdimmer_init();
// Register zero-cross detectors for each phase
rbdimmer_register_zero_cross(ZERO_CROSS_PIN_PHASE_A, PHASE_A, 0);
rbdimmer_register_zero_cross(ZERO_CROSS_PIN_PHASE_B, PHASE_B, 0);
rbdimmer_register_zero_cross(ZERO_CROSS_PIN_PHASE_C, PHASE_C, 0);
// Create channels for each phase
rbdimmer_config_t config_a = {
.gpio_pin = DIMMER_PIN_PHASE_A,
.phase = PHASE_A,
.initial_level = 50,
.curve_type = RBDIMMER_CURVE_RMS
};
rbdimmer_create_channel(&config_a, &channel_a);
rbdimmer_config_t config_b = {
.gpio_pin = DIMMER_PIN_PHASE_B,
.phase = PHASE_B,
.initial_level = 50,
.curve_type = RBDIMMER_CURVE_RMS
};
rbdimmer_create_channel(&config_b, &channel_b);
rbdimmer_config_t config_c = {
.gpio_pin = DIMMER_PIN_PHASE_C,
.phase = PHASE_C,
.initial_level = 50,
.curve_type = RBDIMMER_CURVE_RMS
};
rbdimmer_create_channel(&config_c, &channel_c);
ESP_LOGI(TAG, "Multi-phase dimmer system initialized");
// Main control loop
while (1) {
// Control phases with different levels
ESP_LOGI(TAG, "Setting phase A: 75%%, phase B: 50%%, phase C: 25%%");
rbdimmer_set_level(channel_a, 75);
rbdimmer_set_level(channel_b, 50);
rbdimmer_set_level(channel_c, 25);
vTaskDelay(3000 / portTICK_PERIOD_MS);
ESP_LOGI(TAG, "Setting phase A: 25%%, phase B: 50%%, phase C: 75%%");
rbdimmer_set_level(channel_a, 25);
rbdimmer_set_level(channel_b, 50);
rbdimmer_set_level(channel_c, 75);
vTaskDelay(3000 / portTICK_PERIOD_MS);
}
} Monitoreo de Operación y Depuración
void print_dimmer_status(rbdimmer_channel_t* channel, uint8_t phase)
{
ESP_LOGI(TAG, "=== Dimmer Status ===");
ESP_LOGI(TAG, "Mains frequency: %d Hz", rbdimmer_get_frequency(phase));
ESP_LOGI(TAG, "Brightness: %d%%", rbdimmer_get_level(channel));
ESP_LOGI(TAG, "Active: %s", rbdimmer_is_active(channel) ? "Yes" : "No");
ESP_LOGI(TAG, "Curve type: %d", rbdimmer_get_curve(channel));
ESP_LOGI(TAG, "Delay: %d us", rbdimmer_get_delay(channel));
ESP_LOGI(TAG, "====================");
}Solución de Problemas
General
- If the dimmer doesn't work correctly, check your hardware connections, especially the zero-cross detector
- Asegúrese de que el pin de cero cruzado esté conectado a un GPIO que admita interrupciones
- Utilice funciones
ESP_LOGpara monitorear la operación en tiempo real - Para sistemas multicanal, asegúrese de que cada canal del atenuador tenga un pin GPIO separado
- La biblioteca soporta detección automática de frecuencia. Si conoce la frecuencia de red en su región (típicamente 50Hz o 60Hz), puede establecerla explícitamente para un mejor rendimiento inicial
Problemas de parpadeo e inestabilidad (correcciones en v2.0.0)
Parpadeo aleatorio o disparos falsos: La puerta de ruido de cero cruzado (CONFIG_RBDIMMER_ZC_DEBOUNCE_US, 3000 us por defecto) filtra el ruido eléctrico en la línea de cero cruzado. Si aún ve parpadeo aleatorio, intente aumentar el valor de rebote a través de idf.py menuconfig.
Parpadeo al 100% (brillo completo): El retardo TRIAC mínimo (CONFIG_RBDIMMER_MIN_DELAY_US, 100 us por defecto) evita que el TRIAC dispare demasiado cerca del borde de cero cruzado. El predeterminado v2.0.0 de 100 us resuelve el parpadeo que ocurría con el anterior predeterminado de 50 us.
Comportamiento inestable por debajo del 3%: Los niveles por debajo de CONFIG_RBDIMMER_LEVEL_MIN (predeterminado 3%) ahora se tratan como APAGADO. El TRIAC no puede mantener la conducción de manera confiable con ángulos de disparo muy bajos, por lo que la biblioteca se sujeta a apagado en lugar de producir resultados erráticos.
Variabilidad de múltiples canales: Cuando múltiples canales comparten la misma fase, v2.0.0 utiliza un ISR de dos pasos que preordena canales por retraso y dispara pulsos TRIAC en secuencia dentro de una sola interrupción. Esto elimina la variabilidad de tiempo que podría ocurrir cuando los canales tenían valores de retraso similares en versiones anteriores.
Integración Continua
La biblioteca se prueba en CI contra la siguiente matriz:
- Versiones de ESP-IDF: v5.3, v5.4, v5.5
- Chips objetivo: ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6
Esto asegura que cada envío se compile correctamente en el rango completo de configuraciones soportadas.
Historial de Cambios
Para obtener una lista completa de cambios, consulte CHANGELOG.md.
← - Guía y ejemplos de Arduino. | Contenidos | Siguiente: Componente ESPHome →