jueves, 29 de enero de 2026

Separando archivos en Arduino y ESP: .ino, .h y .cpp

Cuando uno empieza a programar en Arduino o ESP, lo más común es trabajar con un único archivo .ino.

Eso funciona bien para ejemplos chicos y pruebas rápidas. El problema aparece cuando el proyecto empieza a crecer. “Modularizar” el código no solo es una buena práctica, sino que además nos da la gran ventaja de poder reutilizarlo de manera muy sencilla.

Para introducir este tema me pareció un buen ejemplo la conexión a WiFi, dado que es algo que utilizaremos bastante, sobre todo con los ESP.

1. El problema del .ino único

Un sketch típico suele empezar así:

#include <WiFi.h>

const char* ssid = "MiWiFi";
const char* password = "MiPassword";

void setup() {
  Serial.begin(115200);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("Conectado!");
}

void loop() {
  // código principal
}

Al principio es claro y fácil de entender. El problema es que, con el tiempo, empiezan a aparecer:

  • más configuraciones
  • más funciones
  • más lógica

Y el archivo .ino termina teniendo:

  • código de WiFi
  • código de red
  • lógica de la aplicación
  • configuraciones globales

Y todo esto puede dificultar la lectura del sketch, no escalar bien a medida que el proyecto crece e incluso hacer que reutilizar código termine siendo más costoso que volver a escribirlo.

2. El rol de cada archivo: .ino, .h y .cpp


Archivo .ino

  • Punto de entrada
  • Contiene setup() y loop()
  • Orquesta el flujo general

Archivo .h

  • Declara qué ofrece un módulo
  • Define funciones, constantes y estructuras públicas
  • Actúa como contrato

Archivo .cpp

  • Implementa la lógica real
  • Contiene los detalles internos

Captura1

Resumen

El programa principal (.ino) tiene el programa, pero no las funciones que usa. Esas se definen en otro lado. El archivo .h trae las definiciones de las funciones y los parámetros que recibe y que devuelve, (pero no el código propiamente dicho) y nos permite ver que funciones tenemos disponibles para trabajar: Nos da la información precisa para usar las funciones del [archivo] .cpp - El fichero .cpp, tiene el código final de las funciones y aquí es donde podemos añadir o modificar funciones, siempre y cuando las definamos en el fichero .h

Fuente: https://www.prometec.net/organizando-tus-programas-arduino/

Archivo Responsabilidad
.ino Orquestación
.h Interfaz
.cpp Implementación

3. WiFi como módulo reutilizable

Para este ejemplo estaré usando un ESP32 (Wemos D1 R32)

La estructura de archivos será la siguiente:

/
├── main.ino
├── config.h
├── wifi_connect.h
└── wifi_connect.cpp

config.h

#pragma once

// WiFi
const char* WIFI_SSID = "TU_WIFI_AQUI";
const char* WIFI_PASSWORD = "TU_PASSWORD_AQUI";

// mDNS
const char* MDNS_NAME = "NOMBRE_DE_RED_AQUI";

En los lenguajes de programación C y C++, #pragma once es una directiva del preprocesador no estándar pero con un extenso soporte. Está diseñado para asegurar que el código fuente que lo invoca sea incluido una única vez. [...] Usando #pragma once en lugar de la protección de macros (también visto como protección de inclusiones (include) ) generalmente aumentará la velocidad de compilación puesto que es un mecanismo de nivel más alto.

Fuente: https://es.wikipedia.org/wiki/Pragma_once

wifi_connect.h

#pragma once
void conectarWiFi();

wifi_connect.cpp

#include <WiFi.h>
#include <ESPmDNS.h>
#include "config.h"
#include "wifi_connect.h"

void conectarWiFi() {
  Serial.println();
  Serial.print("Conectando a: ");
  Serial.println(WIFI_SSID);

  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  // Bucle mientras se conecta
  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
    Serial.print(".");
  }

  // Conexión exitosa -> mostramos los datos de conexión
  Serial.println("");
  Serial.println("WiFi Conectado!");
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());
  Serial.print("IP asignada: ");
  Serial.println(WiFi.localIP());

  // mDNS
  if (!MDNS.begin(MDNS_NAME)) {  // Seteamos el hostname 
    Serial.println("Error iniciando mDNS");
    return;
  }

  // Mostramos los datos de mDNS
  Serial.print("mDNS iniciado: http://");
  Serial.print(MDNS_NAME);
  Serial.println(".local");
}

main.ino

#include "wifi_connect.h"

void setup() {
  Serial.begin(115200);
  // Llamamos a la función para conectar a WiFi
  conectarWiFi();
}

void loop() {}

4. Credenciales, .gitignore y repositorios públicos

Como vimos en el ejemplo de arriba, el archivo config.h contendrá las credenciales para la conexión (entre otras cosas) y debemos tener cuidado de no subir este archivo a nuestro repositorio.

captura2

Debemos siempre agregarlo al archivo .gitignore al crear nuestro repo. En su lugar podemos crear un archivo config_example.h por ejemplo y aclarar que el mismo debe renombrarse antes de subirse al microcontrolador.

// config_example.h
#pragma once

/* Renombrar este archivo a config.h
y completar con tus credenciales*/

// WiFi
const char* WIFI_SSID = "TU_WIFI_AQUI";
const char* WIFI_PASSWORD = "TU_PASSWORD_AQUI";

// mDNS
const char * MDNS_NAME = "NOMBRE_DE_RED_AQUI";

En conclusión, separar el código desde el inicio tiene sus ventajas:

  • mejora la organización
  • facilita el mantenimiento
  • permite reutilizar módulos

Es una buena práctica a adquirir (ya sé que nunca lo hice hasta ahora...) e intentaré mantenerla para proyectos futuros.

Espero que este post les haya interesado. Como siempre, todos los comentarios son bienvenidos. Y si quieren, pueden invitarme un cafecito!

Invitame un café en cafecito.app