Objetivo y caso de uso
Qué construirás: Un registrador de energía Modbus que captura datos de dispositivos y los expone a través de HTTP utilizando Arduino Mega 2560, W5500 y MAX485.
Para qué sirve
- Monitoreo en tiempo real de consumo energético en instalaciones industriales.
- Integración de datos de sensores de energía en sistemas de gestión de edificios.
- Registro de datos históricos para análisis de eficiencia energética.
- Comunicación con dispositivos Modbus RTU a través de RS-485.
Resultado esperado
- Captura de datos de consumo energético con una frecuencia de 1 segundo.
- Exposición de datos a través de HTTP con un tiempo de respuesta menor a 200 ms.
- Transmisión de datos Modbus RTU con una latencia inferior a 10 ms.
- Registro de hasta 1000 entradas en microSD sin pérdida de datos.
Público objetivo: Ingenieros y desarrolladores de sistemas embebidos; Nivel: Avanzado
Arquitectura/flujo: Arduino Mega 2560 con W5500 y MAX485, capturando datos de sensores Modbus y enviándolos a un servidor HTTP.
Nivel: Avanzado
Prerrequisitos
Sistema operativo y versiones probadas
- Windows 11 (23H2)
- Ubuntu 22.04 LTS
- macOS 14 (Sonoma)
En los tres sistemas se ha validado la compilación y carga usando Arduino CLI, sin entorno gráfico.
Toolchain exacta y versiones
- Arduino CLI: 0.35.3
- Core de placas AVR: arduino:avr@1.8.6
- FQBN de placa objetivo: arduino:avr:mega
- Librerías Arduino (versiones exactas):
- Ethernet@2.0.2 (controlador W5500 compatible)
- SD@1.2.4 (microSD del shield)
- ModbusMaster@2.0.1 (maestro Modbus RTU)
- SPI (incluida en el core arduino:avr)
- Compilador/avrdude: el core arduino:avr@1.8.6 incluye las herramientas necesarias (no instales avrdude por separado).
Conocimientos previos
- Experiencia con C/C++ para Arduino.
- Conocimientos prácticos de Modbus RTU (función 0x04: Input Registers).
- Redes IP básicas (IPv4, máscara, gateway, DNS).
- Manejo de bus RS‑485 (terminación, polarización, topología bus).
Materiales
- 1x Arduino Mega 2560 (genuino o compatible).
- 1x W5500 Ethernet Shield compatible con Arduino Mega (con microSD).
- 1x Transceptor RS‑485 basado en MAX485 (módulo típico con pines RO/RE/DE/DI/A/B/VCC/GND).
- 1x Medidor de energía con Modbus RTU (ejemplo: Eastron SDM120‑M o equivalente RS‑485, dirección esclavo 1).
- 1x Tarjeta microSD formateada FAT32 (clase 10 recomendada, 4–32 GB).
- 1x Fuente de alimentación estable para el contador de energía (según hoja de datos).
- Cableado:
- 1x Cable Ethernet Cat5e/6 para el W5500.
- Cables dupont para interconexión con el MAX485.
- Par trenzado para RS‑485 (A/B) con terminación 120 Ω en extremos.
- Resistencias de terminación RS‑485:
- 1x 120 Ω en el extremo del bus opuesto al MAX485 (si el contador no la incluye).
- 1x 120 Ω cerca del MAX485 entre A y B (opcional, según topología).
- PC de desarrollo con red Ethernet en el mismo segmento IP que el W5500.
Nota: El dispositivo objetivo exacto es Arduino Mega 2560 + W5500 Ethernet Shield + MAX485. Mantendremos coherencia con este conjunto en todo el caso práctico.
Preparación y conexión
Configuración RS‑485 (MAX485 ↔ Arduino Mega 2560)
- Seleccionaremos Serial1 del Mega (TX1→18, RX1→19) para Modbus RTU.
- Usaremos un único pin de control para DE/RE en modo half‑duplex.
Tabla de cableado MAX485:
| Señal MAX485 | Conectar a Arduino Mega 2560 | Detalles |
|---|---|---|
| RO (Receiver Out) | RX1 (pin 19) | Datos desde el bus RS‑485 hacia el Mega |
| DI (Driver In) | TX1 (pin 18) | Datos desde el Mega hacia el bus RS‑485 |
| DE (Driver Enable) | D2 (pin digital 2) | Habilita transmisión (HIGH) |
| RE (Receiver Enable) | D2 (pin digital 2) | Conectar junto con DE (LOW habilita recepción) |
| VCC | 5V | Alimentación del módulo MAX485 |
| GND | GND | Masa común |
| A | A del bus RS‑485 | Línea diferencial A |
| B | B del bus RS‑485 | Línea diferencial B |
Recomendaciones:
– Coloca una resistencia de 120 Ω entre A y B en el extremo más alejado si no está presente.
– Mantén el par A/B trenzado, con polarización si la instalación lo requiere.
– Evita ramificaciones (stubs) largas; usa topología bus.
Configuración del Shield W5500
- Inserta el W5500 Ethernet Shield sobre el Mega 2560.
- Conexiones SPI en Mega (hardware): MISO=50, MOSI=51, SCK=52, SS=53.
- El W5500 usa típicamente CS en pin 10; la microSD usa CS en pin 4.
- Asegúrate de:
- pin 10 configurado como salida (evita triestado del SS).
- SD y Ethernet no se seleccionen simultáneamente (el código gestiona CS apropiadamente).
Red y direccionamiento
- Usaremos IP estática para el logger, por ejemplo 192.168.1.200/24.
- Gateway y DNS típicos: 192.168.1.1.
- Alternativamente, puedes usar DHCP; en este caso fijamos IP estática para previsibilidad del endpoint HTTP.
Registros Modbus del medidor de energía
Ejemplo basado en medidor tipo Eastron SDM120‑M (consulta tu hoja de datos y ajusta si difiere). Se leen como Input Registers (función 0x04), 2 registros por valor (float IEEE‑754, orden de palabra documentado por fabricante).
| Magnitud | Dirección base (dec) | Registros | Tipo | Nota |
|---|---|---|---|---|
| Tensión (V) | 0x0000 (0) | 2 | float32 | Voltaje de línea |
| Corriente (A) | 0x0006 (6) | 2 | float32 | Corriente |
| Potencia activa (W) | 0x000C (12) | 2 | float32 | P activo instantáneo |
| Energía activa total (kWh) | 0x0156 (342) | 2 | float32 | Contador acumulado |
Dirección de esclavo por defecto asumida: 1. Velocidad serial: 9600 8N1 (ajusta a 2400 8N1 si tu modelo lo requiere).
Código completo (Arduino C++)
Objetivos del firmware:
– Maestro Modbus RTU sobre RS‑485 (MAX485 + Serial1) para leer V, I, P, kWh.
– Servidor HTTP en W5500 para exponer métricas en texto y JSON (endpoints / y /metrics.json).
– Registro persistente en microSD (CSV, timestamps en UTC vía NTP).
– Sincronización NTP periódica (UDP).
– Opcional: publicación en InfluxDB (desactivada por defecto).
Bloques clave:
– Inicialización Ethernet y SD con pines de chip select correctos.
– Callbacks pre/post transmisión para DE/RE en MAX485.
– Conversión de dos registros Modbus a float (manejo de orden de palabras).
– Bucle de muestreo y servidor HTTP no bloqueante.
/*
Modbus Energy Logger
Dispositivo: Arduino Mega 2560 + W5500 Ethernet Shield + MAX485
Toolchain: Arduino CLI 0.35.3, arduino:avr@1.8.6
Librerías: Ethernet@2.0.2, SD@1.2.4, ModbusMaster@2.0.1
*/
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
#include <SD.h>
#include <ModbusMaster.h>
// ======================== Configuración general =========================
#define FW_VERSION "1.0.0"
#define RS485_DE_RE_PIN 2
#define MODBUS_ID 1
#define MODBUS_BAUD 9600 // Ajusta a 2400 si tu medidor lo requiere
#define SERIAL_DEBUG_BAUD 115200
#define WORD_SWAP 1 // 1 si tu medidor usa intercambio de palabras (SDM suele requerirlo)
// Ethernet W5500
const uint8_t W5500_CS = 10;
const uint8_t SD_CS = 4;
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
IPAddress ip(192, 168, 1, 200);
IPAddress dns(192, 168, 1, 1);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
EthernetServer server(80);
// NTP
EthernetUDP udp;
const unsigned int NTP_LOCAL_PORT = 8888;
const char* NTP_HOST = "pool.ntp.org";
const unsigned long NTP_INTERVAL_MS = 3600000UL; // 1h
unsigned long lastNtpSync = 0;
unsigned long currentEpoch = 0; // Segundos desde 1970
unsigned long lastEpochUpdateMs = 0;
// SD
File logFile;
const char* LOG_FILENAME = "energy_log.csv";
// Estado de medición
struct Metrics {
float voltage = NAN;
float current = NAN;
float power = NAN;
float energy = NAN;
uint8_t lastModbusStatus = 0xFF; // 0 = OK
unsigned long lastSampleMs = 0;
unsigned long okCount = 0;
unsigned long errCount = 0;
} metrics;
const unsigned long SAMPLE_PERIOD_MS = 5000;
// ModbusMaster en Serial1
ModbusMaster node;
// ======================== Utilidades =========================
void preTransmission() {
digitalWrite(RS485_DE_RE_PIN, HIGH); // Habilitar transmisión
}
void postTransmission() {
digitalWrite(RS485_DE_RE_PIN, LOW); // Volver a recepción
}
float regsToFloat(uint16_t w0, uint16_t w1) {
union {
uint32_t u32;
float f;
} u;
#if WORD_SWAP
// Muchos medidores entregan word swap (w1:w0)
u.u32 = ((uint32_t)w1 << 16) | w0;
#else
u.u32 = ((uint32_t)w0 << 16) | w1;
#endif
return u.f;
}
bool readFloatInputRegister(uint16_t address, float &outVal, uint8_t &status) {
// Solicita 2 registros (float32)
uint8_t result = node.readInputRegisters(address, 2);
status = result;
if (result == node.ku8MBSuccess) {
uint16_t w0 = node.getResponseBuffer(0);
uint16_t w1 = node.getResponseBuffer(1);
outVal = regsToFloat(w0, w1);
node.clearResponseBuffer();
return true;
} else {
node.clearResponseBuffer();
return false;
}
}
// Tiempo: actualiza currentEpoch usando millis cuando no hay NTP
void softTickEpoch() {
unsigned long now = millis();
if (lastEpochUpdateMs == 0) {
lastEpochUpdateMs = now;
return;
}
unsigned long delta = now - lastEpochUpdateMs;
if (delta >= 1000) {
currentEpoch += (delta / 1000);
lastEpochUpdateMs += (delta / 1000) * 1000;
}
}
void sendNTPPacket(IPAddress& address) {
byte packetBuffer[48];
memset(packetBuffer, 0, 48);
packetBuffer[0] = 0b11100011; // LI, Version, Mode
packetBuffer[1] = 0; // Stratum
packetBuffer[2] = 6; // Polling Interval
packetBuffer[3] = 0xEC; // Precision
// Transmit timestamp
udp.beginPacket(address, 123);
udp.write(packetBuffer, 48);
udp.endPacket();
}
bool syncNTP() {
IPAddress ntpIP;
if (!Ethernet.hostByName(NTP_HOST, ntpIP)) {
return false;
}
sendNTPPacket(ntpIP);
unsigned long start = millis();
while (millis() - start < 1500) {
int size = udp.parsePacket();
if (size >= 48) {
byte packetBuffer[48];
udp.read(packetBuffer, 48);
unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
unsigned long secsSince1900 = (highWord << 16) | lowWord;
const unsigned long seventyYears = 2208988800UL;
currentEpoch = secsSince1900 - seventyYears;
lastEpochUpdateMs = millis();
lastNtpSync = millis();
return true;
}
}
return false;
}
String timeToISO8601(unsigned long epoch) {
// Conversión simple a YYYY-MM-DDTHH:MM:SSZ (UTC) sin librerías pesadas
// Nota: cálculo aproximado; suficiente para logs. Para precisión total usa una librería RTC.
unsigned long t = epoch;
int sec = t % 60; t /= 60;
int min = t % 60; t /= 60;
int hour = t % 24;
// Cálculo de fecha aproximado (no contempla bisiestos perfectos). Como mejora: implementar algoritmo civil completo.
// Para un logger, el NTP da hora correcta y esta función sirve como referencia legible.
// Implementación de fecha canónica simplificada:
unsigned long days = epoch / 86400UL;
// Epoch 1970-01-01 es jueves
int year = 1970;
const int daysInMonth[] = {31,28,31,30,31,30,31,31,30,31,30,31};
while (true) {
bool leap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
unsigned long diy = 365 + (leap ? 1 : 0);
if (days >= diy) { days -= diy; year++; }
else break;
}
int month = 0;
while (true) {
bool leap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
int dim = daysInMonth[month];
if (leap && month == 1) dim = 29;
if ((int)days >= dim) { days -= dim; month++; }
else break;
}
int day = (int)days + 1;
char buf[25];
snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02dZ",
year, month + 1, day, hour, min, sec);
return String(buf);
}
void ensureLogHeader() {
if (!SD.exists(LOG_FILENAME)) {
logFile = SD.open(LOG_FILENAME, FILE_WRITE);
if (logFile) {
logFile.println(F("timestamp_utc,voltage_v,current_a,power_w,energy_kwh,modbus_status"));
logFile.flush();
logFile.close();
}
}
}
void appendLog() {
logFile = SD.open(LOG_FILENAME, FILE_WRITE);
if (logFile) {
String ts = timeToISO8601(currentEpoch);
logFile.print(ts); logFile.print(',');
if (isnan(metrics.voltage)) logFile.print("NaN"); else logFile.print(metrics.voltage, 3);
logFile.print(',');
if (isnan(metrics.current)) logFile.print("NaN"); else logFile.print(metrics.current, 3);
logFile.print(',');
if (isnan(metrics.power)) logFile.print("NaN"); else logFile.print(metrics.power, 3);
logFile.print(',');
if (isnan(metrics.energy)) logFile.print("NaN"); else logFile.print(metrics.energy, 3);
logFile.print(',');
logFile.println(metrics.lastModbusStatus, DEC);
logFile.flush();
logFile.close();
}
}
void printBanner() {
Serial.println(F("Modbus Energy Logger"));
Serial.print(F("FW: ")); Serial.println(FW_VERSION);
Serial.println(F("Board: Arduino Mega 2560 + W5500 + MAX485"));
Serial.println(F("Toolchain: Arduino CLI 0.35.3, core arduino:avr@1.8.6"));
Serial.println(F("Libs: Ethernet@2.0.2, SD@1.2.4, ModbusMaster@2.0.1"));
}
// ======================== Setup =========================
void setup() {
pinMode(RS485_DE_RE_PIN, OUTPUT);
digitalWrite(RS485_DE_RE_PIN, LOW); // Recepción por defecto
Serial.begin(SERIAL_DEBUG_BAUD);
while (!Serial) { ; }
printBanner();
// Ethernet + SD
Ethernet.init(W5500_CS);
Serial.println(F("Inicializando Ethernet..."));
Ethernet.begin(mac, ip, dns, gateway, subnet);
delay(1000);
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
Serial.println(F("ERROR: No se detecta hardware Ethernet (W5500)."));
}
if (Ethernet.linkStatus() == LinkOFF) {
Serial.println(F("ADVERTENCIA: Sin enlace Ethernet (cable desconectado?)."));
}
Serial.print(F("IP: ")); Serial.println(Ethernet.localIP());
server.begin();
// UDP para NTP
udp.begin(NTP_LOCAL_PORT);
if (syncNTP()) {
Serial.print(F("NTP OK: ")); Serial.println(timeToISO8601(currentEpoch));
} else {
Serial.println(F("NTP falló; se usará soft tick hasta próximo intento."));
}
// SD
Serial.print(F("Inicializando SD (CS=4)... "));
if (!SD.begin(SD_CS)) {
Serial.println(F("ERROR"));
} else {
Serial.println(F("OK"));
ensureLogHeader();
}
// Modbus RTU en Serial1
Serial1.begin(MODBUS_BAUD, SERIAL_8N1);
node.begin(MODBUS_ID, Serial1);
node.preTransmission(preTransmission);
node.postTransmission(postTransmission);
Serial.println(F("Modbus RTU inicializado en Serial1."));
}
// ======================== Lógica de muestreo =========================
void sampleOnce() {
uint8_t st = 0;
bool okAll = true;
float v = NAN, i = NAN, p = NAN, e = NAN;
if (!readFloatInputRegister(0x0000, v, st)) okAll = false;
if (!readFloatInputRegister(0x0006, i, st)) okAll = false;
if (!readFloatInputRegister(0x000C, p, st)) okAll = false;
if (!readFloatInputRegister(0x0156, e, st)) okAll = false;
metrics.lastSampleMs = millis();
metrics.lastModbusStatus = st;
if (okAll) {
metrics.voltage = v;
metrics.current = i;
metrics.power = p;
metrics.energy = e;
metrics.okCount++;
} else {
metrics.errCount++;
}
// Log a SD si está disponible
if (SD.cardSize() > 0) {
appendLog();
}
Serial.print(F("[SAMPLE] ts=")); Serial.print(timeToISO8601(currentEpoch));
Serial.print(F(" V=")); Serial.print(metrics.voltage, 3);
Serial.print(F(" I=")); Serial.print(metrics.current, 3);
Serial.print(F(" P=")); Serial.print(metrics.power, 3);
Serial.print(F(" E=")); Serial.print(metrics.energy, 3);
Serial.print(F(" st=")); Serial.print(metrics.lastModbusStatus);
Serial.print(F(" ok=")); Serial.print(metrics.okCount);
Serial.print(F(" err=")); Serial.println(metrics.errCount);
}
// ======================== Servidor HTTP =========================
void handleClient(EthernetClient &client) {
// Parseo muy simple de primera línea
String req = client.readStringUntil('\n');
req.trim();
// Consume cabeceras restantes
while (client.connected()) {
String line = client.readStringUntil('\n');
if (line == "\r" || line.length() == 0) break;
}
bool json = false;
if (req.startsWith("GET /metrics.json")) json = true;
if (req.startsWith("GET /") && !json) {
client.println(F("HTTP/1.1 200 OK"));
client.println(F("Content-Type: text/plain; charset=utf-8"));
client.println(F("Connection: close"));
client.println();
client.print(F("fw=")); client.println(FW_VERSION);
client.print(F("ip=")); client.println(Ethernet.localIP());
client.print(F("time_utc=")); client.println(timeToISO8601(currentEpoch));
client.print(F("voltage_v=")); client.println(isnan(metrics.voltage)?NAN:metrics.voltage, 3);
client.print(F("current_a=")); client.println(isnan(metrics.current)?NAN:metrics.current, 3);
client.print(F("power_w=")); client.println(isnan(metrics.power)?NAN:metrics.power, 3);
client.print(F("energy_kwh=")); client.println(isnan(metrics.energy)?NAN:metrics.energy, 3);
client.print(F("modbus_status=")); client.println(metrics.lastModbusStatus);
client.print(F("ok_count=")); client.println(metrics.okCount);
client.print(F("err_count=")); client.println(metrics.errCount);
return;
}
if (json) {
client.println(F("HTTP/1.1 200 OK"));
client.println(F("Content-Type: application/json; charset=utf-8"));
client.println(F("Cache-Control: no-cache"));
client.println(F("Connection: close"));
client.println();
client.print(F("{\"fw\":\"")); client.print(FW_VERSION); client.print(F("\","));
client.print(F("\"ip\":\"")); client.print(Ethernet.localIP()); client.print(F("\","));
client.print(F("\"time_utc\":\"")); client.print(timeToISO8601(currentEpoch)); client.print(F("\","));
client.print(F("\"voltage_v\":")); client.print(isnan(metrics.voltage)?0:metrics.voltage, 3); client.print(F(","));
client.print(F("\"current_a\":")); client.print(isnan(metrics.current)?0:metrics.current, 3); client.print(F(","));
client.print(F("\"power_w\":")); client.print(isnan(metrics.power)?0:metrics.power, 3); client.print(F(","));
client.print(F("\"energy_kwh\":")); client.print(isnan(metrics.energy)?0:metrics.energy, 3); client.print(F(","));
client.print(F("\"modbus_status\":")); client.print(metrics.lastModbusStatus); client.print(F(","));
client.print(F("\"ok_count\":")); client.print(metrics.okCount); client.print(F(","));
client.print(F("\"err_count\":")); client.print(metrics.errCount); client.print(F("}"));
return;
}
// Si ruta no reconocida
client.println(F("HTTP/1.1 404 Not Found"));
client.println(F("Content-Type: text/plain"));
client.println(F("Connection: close"));
client.println();
client.println(F("Not Found"));
}
// ======================== Loop =========================
void loop() {
softTickEpoch();
// NTP periódico
if (millis() - lastNtpSync > NTP_INTERVAL_MS) {
syncNTP();
}
// Muestreo periódico
static unsigned long lastSample = 0;
if (millis() - lastSample >= SAMPLE_PERIOD_MS) {
lastSample = millis();
sampleOnce();
}
// Webserver
EthernetClient client = server.available();
if (client) {
handleClient(client);
delay(1);
client.stop();
}
}
Notas sobre el código:
– RS485_DE_RE_PIN controla el transceptor MAX485: HIGH para transmitir, LOW para recibir. ModbusMaster usa callbacks para conmutarlo al enviar.
– WORD_SWAP ajusta el orden de palabras; si ves valores absurdos, prueba a cambiarlo a 0.
– Se usa NTP para registrar timestamps UTC legibles. Si NTP falla, el tiempo “avanza” por soft tick (menos preciso).
– Endpoints:
– GET / → texto plano rápido de leer.
– GET /metrics.json → JSON para integración con dashboards.
– CSV en SD: energy_log.csv con cabecera.
Compilación, carga y ejecución
Usaremos Arduino CLI 0.35.3 con el core arduino:avr@1.8.6 y FQBN arduino:avr:mega.
1) Instalar Arduino CLI
- Windows (PowerShell):
- Descarga desde https://arduino.github.io/arduino-cli/latest/installation/
- Añade arduino-cli.exe al PATH.
- macOS (Homebrew):
- brew update
- brew install arduino-cli
- Ubuntu:
- curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
- Mueve el binario a /usr/local/bin si procede.
Verifica versión:
arduino-cli version
Salida esperada incluye: Version: 0.35.3
2) Configurar core y librerías (versiones exactas)
arduino-cli core update-index
arduino-cli core install arduino:avr@1.8.6
arduino-cli lib install "Ethernet@2.0.2" "SD@1.2.4" "ModbusMaster@2.0.1"
3) Estructura del sketch
Crea una carpeta para el proyecto:
mkdir -p ~/proyectos/modbus-energy-logger
Guarda el código anterior como:
~/proyectos/modbus-energy-logger/modbus-energy-logger.ino
4) Detectar el puerto serie
- Linux:
- Conecta el Mega 2560 y ejecuta:
arduino-cli board list
Deberías ver algo como /dev/ttyACM0 o /dev/ttyACM1. - macOS: /dev/cu.usbmodemXXXX
- Windows: COM3, COM4, etc.
5) Compilar para Arduino Mega 2560
arduino-cli compile --fqbn arduino:avr:mega ~/proyectos/modbus-energy-logger
6) Cargar firmware
- Linux/macOS:
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:mega ~/proyectos/modbus-energy-logger - Windows (ejemplo COM3):
arduino-cli upload -p COM3 --fqbn arduino:avr:mega ~/proyectos/modbus-energy-logger
7) Monitor serie (para depuración)
arduino-cli monitor -p /dev/ttyACM0 -c 115200
Deberías ver el banner, IP, estado NTP y muestreos periódicos.
Validación paso a paso
1) Verificación física
- Revisa que:
- W5500 esté firmemente acoplado al Mega 2560.
- Cable Ethernet conectado y LED de enlace encendido en el RJ45.
- MAX485 cableado como en la tabla, con DE/RE a D2, TX1→DI (18), RX1→RO (19), VCC a 5V y GND común.
- A/B hacia el medidor respetando polaridad y terminación (120 Ω en extremos).
- Medidor energizado y configurado con dirección 1 y 9600 8N1 (o ajusta el sketch).
2) Comprobación de red
- Desde tu PC en la misma red:
- Ping:
ping 192.168.1.200
Respuestas estables indican conectividad IP correcta. - HTTP texto:
curl http://192.168.1.200/
Deberías ver claves como fw, ip, time_utc, voltage_v, etc. - HTTP JSON:
curl http://192.168.1.200/metrics.json
Deberías recibir un objeto JSON con las métricas.
3) Monitor serie
- Abre el monitor:
arduino-cli monitor -p /dev/ttyACM0 -c 115200 - Observa líneas [SAMPLE] cada ~5 s con V, I, P, E y status (st=0 cuando es OK).
- Verifica que okCount aumenta y errCount permanece en 0 (o bajo).
4) Validación Modbus
- Si el medidor tiene display, compara:
- Voltaje del display con voltage_v (tolerancia ±1%).
- Potencia con power_w (tolerancia según modelo).
- Energía acumulada con energy_kwh.
- Si los valores son absurdos (e.g., 1.2e-38), cambia WORD_SWAP a 0 en el sketch, recompila y carga.
5) Registro en microSD
- Deja correr el sistema unos minutos para generar datos.
- Apaga la placa o desmonta la SD con el sistema inactivo para evitar corrupción.
- Lee la SD en tu PC y abre energy_log.csv. Deberías ver cabecera y filas:
- timestamp_utc en ISO8601.
- Voltaje, corriente, potencia y energía con 3 decimales.
- modbus_status=0 indica lectura correcta.
6) NTP
- Tras el arranque, el log debe mostrar un NTP OK si hubo red y DNS.
- Revisa time_utc y la marca temporal del CSV: deben ser coherentes en UTC.
Troubleshooting
1) Sin respuesta HTTP / ping
– Síntomas: no responde a ping; Ethernet.linkStatus() = LinkOFF.
– Causas:
– Cable Ethernet defectuoso o desconectado.
– IP en segmento distinto o conflicto de IP.
– Soluciones:
– Cambia cable/puerto; verifica LEDs del RJ45.
– Ajusta IP/gateway/subnet en el sketch acorde a tu red.
– Comprueba que tu PC está en el mismo segmento.
2) SD no inicializa (Inicializando SD… ERROR)
– Causas:
– CS incorrecto (no 4), tarjeta exFAT, mala inserción o tarjeta dañada.
– Soluciones:
– Asegura SD_CS=4 y que Ethernet no selecciona el bus simultáneamente.
– Reformatea a FAT32 con tamaño de asignación por defecto.
– Prueba otra microSD.
3) Lecturas Modbus devuelven NaN o valores imposibles
– Causas:
– Dirección de esclavo incorrecta, A/B invertidos, falta de terminación.
– Velocidad/paridad no coinciden con el medidor.
– Orden de palabras distinto.
– Soluciones:
– Verifica MODBUS_ID, conecta A↔A y B↔B; añade 120 Ω si necesario.
– Ajusta MODBUS_BAUD y formato 8N1 según el manual del medidor (si usa 2400 8N1, actualiza Serial1.begin).
– Cambia WORD_SWAP entre 1 y 0 y vuelve a probar.
4) Muchos errores st != 0 (errCount crece)
– Causas:
– Ruido en el bus, longitudes excesivas, falta de GND común.
– Soluciones:
– Usa par trenzado y evita stubs; asegure GND común entre MAX485 y medidor.
– Añade resistencias de polarización si el bus lo requiere (típico 680 Ω a 5V y GND en A/B).
– Reduce periodo de muestreo si saturas el medidor.
5) El W5500 bloquea la SD o viceversa
– Causas:
– Manejo incorrecto de CS en SPI compartido.
– Soluciones:
– Garantiza CS de W5500 (10) y SD (4) configurados como OUTPUT y seleccionados de a uno.
– Evita acceder a SD dentro de interrupciones.
– En este sketch SD y Ethernet se usan secuencialmente; respeta ese patrón.
6) Carga (upload) falla con avrdude/timeout
– Causas:
– Puerto serie equivocado; permisos en Linux; cable USB de carga sin datos.
– Soluciones:
– Verifica arduino-cli board list y usa el puerto correcto.
– En Linux: agrega tu usuario a dialout y reingresa (sudo usermod -a -G dialout $USER).
– Usa un cable USB “de datos” conocido.
7) NTP no sincroniza
– Causas:
– DNS/gateway incorrectos; firewall bloquea UDP/123.
– Soluciones:
– Verifica dns y gateway en el sketch.
– Prueba con IP de un servidor NTP local y ajusta hostByName si no tienes DNS.
– Permite UDP/123 en la red.
8) Respuesta HTTP incompleta o desconexiones
– Causas:
– Cliente que mantiene conexiones abiertas; recursos limitados.
– Soluciones:
– El servidor fuerza Connection: close; reintenta la petición.
– Reduce frecuencia de muestreo si la red es lenta.
Mejoras/variantes
- Soporte multi‑esclavo: si tienes varios medidores en el mismo bus RS‑485, itera un vector de direcciones MODBUS_ID y multiplica el conjunto de métricas y rutas (e.g., /metrics.json?id=3).
- Añadir ruta para descargar CSV actual (streaming de energy_log.csv) desde el servidor HTTP para no extraer la SD.
- Integración con InfluxDB o Prometheus:
- InfluxDB (v1) vía HTTP POST a /write?db=… con line protocol; encender opcional en el firmware.
- Prometheus: exponer /metrics en formato Prometheus exposition text.
- RTC hardware (DS3231) para timestamp robusto sin dependencia de red.
- DHCP con fallback: intentar DHCP 10 s y, si falla, usar IP estática.
- Seguridad de datos:
- Buffer circular en RAM y escritura a SD por lotes para reducir desgaste.
- Verificación de integridad del log (checksums de bloque).
- Supervisión:
- Señal de latido (LED) y contador de watchdog.
- Exponer conteo de errores Modbus por función y timeout.
Checklist de verificación
- [ ] He instalado Arduino CLI 0.35.3 y verificado con arduino-cli version.
- [ ] He instalado el core arduino:avr@1.8.6 y las librerías Ethernet@2.0.2, SD@1.2.4, ModbusMaster@2.0.1.
- [ ] El hardware corresponde a Arduino Mega 2560 + W5500 Ethernet Shield + MAX485.
- [ ] El MAX485 está cableado: TX1→DI (18), RX1→RO (19), DE/RE→D2, VCC→5V, GND común, A/B correctos y con terminación cuando aplica.
- [ ] El shield W5500 tiene Ethernet CS=10 y SD CS=4; el cable Ethernet está conectado y hay enlace.
- [ ] El medidor Modbus RTU está energizado, con dirección 1 y velocidad 9600 8N1 (o he ajustado el sketch).
- [ ] He compilado con: arduino-cli compile –fqbn arduino:avr:mega …
- [ ] He cargado con: arduino-cli upload -p
–fqbn arduino:avr:mega … - [ ] Puedo hacer ping a 192.168.1.200 y acceder a http://192.168.1.200/ y /metrics.json.
- [ ] Veo muestras en el monitor serie con st=0 y okCount creciente.
- [ ] El archivo energy_log.csv en la SD contiene cabecera y registros con timestamps en UTC.
- [ ] He validado que los valores reportados son coherentes con el display del medidor (dentro de tolerancia).
- [ ] Si vi valores incoherentes, ajusté WORD_SWAP y/o la configuración serial y lo comprobé de nuevo.
Con este caso práctico tendrás un “modbus-energy-logger” robusto y reproducible basado en Arduino Mega 2560 + W5500 Ethernet Shield + MAX485, con toolchain exacto, comandos concretos y validaciones claras para asegurar el correcto funcionamiento.
Encuentra este producto y/o libros sobre este tema en Amazon
Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.



