Caso práctico: Control PID de fermentación RS485 y Ethernet

Caso práctico: Control PID de fermentación RS485 y Ethernet — hero

Objetivo y caso de uso

Qué construirás: Un controlador de temperatura para fermentación utilizando Arduino Mega 2560, RS485 y Ethernet para la comunicación.

Para qué sirve

  • Controlar la temperatura de fermentación en tiempo real mediante un sensor PT100.
  • Transmitir datos de temperatura a través de RS485 para monitoreo a larga distancia.
  • Acceder a los datos de fermentación desde una interfaz web usando Ethernet.
  • Integrar alertas de temperatura fuera de rango mediante MQTT.

Resultado esperado

  • Latencia de respuesta del sistema de control menor a 200 ms.
  • Precisión de temperatura de ±0.5 °C en mediciones.
  • Capacidad de enviar datos a un servidor MQTT a una tasa de 1 paquete cada 5 segundos.
  • Monitoreo de temperatura con un rango de 0 a 100 °C.

Público objetivo: Ingenieros y desarrolladores de sistemas embebidos; Nivel: Avanzado

Arquitectura/flujo: Sensor PT100 → MAX31865 → Arduino Mega → RS485/Ethernet → Interfaz web/MQTT.

Nivel: Avanzado

Prerrequisitos

Sistema operativo y entorno recomendado

  • Linux: Ubuntu 22.04 LTS (jammy) 64-bit
  • Windows (alternativo): Windows 11 Pro 22H2 o superior
  • Red local con DHCP deshabilitado para la IP que asignaremos, o reserva DHCP si prefieres

Toolchain exacta

  • Arduino CLI 0.35.3
  • Núcleo (core) AVR: arduino:avr@1.8.6
  • Librerías (vía Library Manager):
  • Ethernet@2.0.2 (compatible con W5500)
  • Adafruit MAX31865 library@1.5.0
  • Adafruit BusIO@1.14.1
  • PID by Brett Beauregard@1.2.1

Notas:
– Usaremos estrictamente Arduino CLI (no GUI).
– FQBN: arduino:avr:mega (para Arduino Mega 2560).
– Velocidad de monitor serie: 115200 baudios.

Materiales

  • Arduino Mega 2560 R3 (ATmega2560-16AU).
  • W5500 Ethernet Shield (formato Arduino R3; SPI por cabecera ICSP, con CS en D10).
  • Módulo MAX485 RS485 TTL (5 V, con resistencias de polarización y terminación con jumpers).
  • Conversor RTD MAX31865 (placa “MAX31865 RTD” tipo Adafruit o compatible, para PT100 3 hilos).
  • Sonda RTD PT100 de 3 hilos (clase A o B; inmersión para líquidos).
  • Convertidor de nivel lógico bidireccional 4 canales para SPI (5 V ↔ 3.3 V). Importante: el MAX31865 típico trabaja a 3.3 V y no es tolerante a 5 V.
  • Cables Dupont, regleta o protoboard.
  • Fuente para actuador térmico:
  • Opción segura de laboratorio: pad calefactor de silicona 12 V y SSR DC-DC (entrada 3–32 VDC, salida 12 VDC, ≥10 A) o MOSFET DC.
  • Evita cargas de red/mains en prácticas de aula; si las usas, sigue normativa y aislamiento adecuados.
  • Relé de estado sólido (SSR) DC-DC para el pad calefactor (controlado por señal de 5 V).
  • Dongle USB–RS485 para el PC (validación del bus RS485).
  • Cable Ethernet CAT5e/CAT6.
  • Cable USB para el Mega.
  • Resistencias de terminación/bias si el módulo MAX485 no las incorpora (típicamente 120 Ω en extremos del bus; polarización con ~680 Ω–1 kΩ a Vcc y GND, aunque muchos módulos ya las incluyen).

Preparación y conexión

Consideraciones de bus y alimentación

  • El MAX485 funciona a 5 V (lado TTL) y con A/B diferencial al bus RS485. Asegura:
  • Terminación de 120 Ω SOLO en los extremos del bus.
  • Polarización (bias) de reposo en uno de los extremos, si no lo provee tu módulo.
  • El MAX31865, en su mayoría de placas, funciona a 3.3 V y NO tolera 5 V en las líneas SPI:
  • Alimenta el MAX31865 con 3.3 V desde el Mega (consumo típico bajo).
  • Usa conversión de nivel 5 V → 3.3 V para SCK, MOSI, CS y DRDY. MISO (3.3 V) hacia el Mega suele ser reconocido como alto; aun así, puedes pasarlo por el conversor para homogeneizar.

Asignación de pines y cableado

  • El W5500 usa SPI por ICSP en el shield y CS en D10 (no modificar). La SD del shield (si existe) suele usar D4; mantenla deshabilitada (D4 como salida en HIGH).
  • SPI adicional para el MAX31865 compartido (MISO/MOSI/SCK) con CS independiente (D9).

Tabla de pines y puertos

Subsistema Señal Arduino Mega 2560 Módulo Notas
Ethernet (W5500 Shield) SPI (MISO/MOSI/SCK) ICSP 6 pines W5500 El shield hace el ruteo por ICSP
Ethernet (W5500 Shield) CS D10 W5500 Reservado para W5500
SD (si presente en shield) CS D4 SD Poner D4 OUTPUT HIGH para deshabilitar
MAX31865 3.3 V 3.3 V VCC No usar 5 V
MAX31865 GND GND GND Común con Mega
MAX31865 SCK 52 (vía LV Shifter) SCLK 5 V → 3.3 V
MAX31865 MOSI 51 (vía LV Shifter) SDI 5 V → 3.3 V
MAX31865 MISO 50 (opcional LV Shifter) SDO 3.3 V → 5 V (aceptado)
MAX31865 CS D9 (vía LV Shifter) CS 5 V → 3.3 V
MAX31865 DRDY D48 (vía LV Shifter) DRDY 5 V → 3.3 V
RTD PT100 3 hilos A/B/C según manual MAX31865 Configurar jumpers para 3 hilos
RS485 (MAX485) VCC 5 V VCC 5 V
RS485 (MAX485) GND GND GND Común con Mega
RS485 (MAX485) DI D16 (TX2) DI UART2 TX
RS485 (MAX485) RO D17 (RX2) RO UART2 RX
RS485 (MAX485) DE y RE D22 (juntos) DE/RE Alto: transmitir; Bajo: recibir
RS485 (MAX485) A/B Bus A/B Diferencial, cable trenzado
Actuador térmico (SSR DC-DC) IN+ D6 SSR IN+ Control por PWM de periodo-ventana
Actuador térmico (SSR DC-DC) IN- GND SSR IN- Retorno a GND

Puntos clave:
– Asegura que D9 (CS MAX31865) y D10 (CS W5500) estén en HIGH por defecto; solo el dispositivo activo debe tener CS LOW.
– Configura el MAX31865 para 3 hilos (cambia el jumper/cableado según la placa).
– En el bus RS485, habilita terminación solo si estás en extremo; caso contrario, desactívala.

Código completo (Arduino C++ para Mega 2560)

El siguiente sketch implementa:
– Lectura de temperatura PT100 vía MAX31865 (SPI).
– Control PID de una resistencia calefactora mediante “time-proportional output” en ventana de 2 s.
– Servidor HTTP simple sobre W5500 con endpoints / y /status, y cambio de setpoint vía /set?sp=XX.X.
– Interfaz RS485 (Serial2) modo esclavo con protocolo ASCII simple: GET PV|SP|OUT|ALL, SET SP x.y, MODE AUTO|MAN, SET MAN x.

Explicación rápida:
– setup(): inicializa SPI, Ethernet, MAX31865, PID, RS485.
– loop(): lee temperatura, ejecuta PID, conmuta SSR, atiende RS485 y HTTP.
– RS485: línea por línea con ‘
‘; respuesta tras elevar DE/RE.
– Seguridad: si hay fallo del MAX31865, desactiva salida y reporta FAULT.

Crea la carpeta rs485-fermentation-pid-control y dentro el archivo rs485-fermentation-pid-control.ino con este contenido:

/*
  rs485-fermentation-pid-control
  Dispositivo: Arduino Mega 2560 + W5500 Ethernet Shield + MAX485 RS485 + MAX31865 RTD
  Toolchain:
    - Arduino CLI 0.35.3
    - Core arduino:avr@1.8.6
    - Ethernet@2.0.2
    - Adafruit MAX31865 library@1.5.0 (+ Adafruit BusIO@1.14.1)
    - PID@1.2.1
*/

#include <SPI.h>
#include <Ethernet.h>
#include <PID_v1.h>
#include <Adafruit_MAX31865.h>

// ---------------------- Configuración de hardware ----------------------
// W5500: CS en pin 10 (mantenido por el shield)
const uint8_t PIN_CS_ETH = 10;
const uint8_t PIN_CS_SD  = 4;   // si hay SD en el shield

// MAX31865 (SPI compartido)
const uint8_t PIN_CS_MAX31865 = 9;
const uint8_t PIN_DRDY        = 48; // opcional, pero útil

// RS485 con MAX485 en Serial2
const uint8_t PIN_RS485_DE_RE = 22; // DE y RE unidos
#define RS485_SERIAL Serial2

// SSR de calefacción (control por ventana temporal)
const uint8_t PIN_SSR = 6;

// Parámetros RTD
#define RREF      430.0   // resistencia de referencia (ver tu placa)
#define RNOMINAL  100.0   // PT100 nominal a 0 °C

// Ethernet (MAC e IP fijas)
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0x01 };
IPAddress ip(192, 168, 1, 77);
IPAddress dns(192, 168, 1, 1);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
EthernetServer server(80);

// MAX31865 objeto (3 hilos)
Adafruit_MAX31865 max31865 = Adafruit_MAX31865(PIN_CS_MAX31865);

// PID
double PV = 0.0;          // Process Variable (temperatura)
double SP = 20.0;         // Setpoint (°C)
double OUT = 0.0;         // Salida PID (0-100 %)
double MAN = 0.0;         // Mando manual (0-100 %)
PID pid(&PV, &OUT, &SP, 8.0, 0.6, 20.0, DIRECT); // Kp, Ki, Kd iniciales (ajustar en tu planta)

// Control por ventana
const unsigned long WINDOW_MS = 2000UL; // 2 s
unsigned long windowStart = 0;

// Filtros
const float alpha = 0.2f; // EMA para suavizar lectura
bool firstRead = true;

// RS485 buffer
static const size_t RS485_BUFSZ = 128;
char rs485_buf[RS485_BUFSZ];
size_t rs485_len = 0;

// Estado
enum Mode { MODE_AUTO, MODE_MANUAL };
Mode mode = MODE_AUTO;
bool max_fault = false;

// ---------------------- Utilidades ----------------------
void rs485SetTX(bool tx) {
  digitalWrite(PIN_RS485_DE_RE, tx ? HIGH : LOW);
}

void rs485SendLine(const char* s) {
  rs485SetTX(true);
  RS485_SERIAL.print(s);
  RS485_SERIAL.print("\r\n");
  RS485_SERIAL.flush();
  rs485SetTX(false);
}

void httpSendHeaderOK(EthernetClient& client, const char* mime) {
  client.println("HTTP/1.1 200 OK");
  client.print("Content-Type: ");
  client.println(mime);
  client.println("Connection: close");
  client.println();
}

void httpSend404(EthernetClient& client) {
  client.println("HTTP/1.1 404 Not Found");
  client.println("Connection: close");
  client.println();
  client.println("404");
}

String urlDecode(const String& s) {
  String r;
  for (uint16_t i = 0; i < s.length(); i++) {
    char c = s[i];
    if (c == '+') r += ' ';
    else if (c == '%' && i + 2 < s.length()) {
      char h1 = s[i+1], h2 = s[i+2];
      auto hex = [&](char h)->int { if (h>='0'&&h<='9') return h-'0'; if (h>='A'&&h<='F') return h-'A'+10; if (h>='a'&&h<='f') return h-'a'+10; return 0; };
      r += char((hex(h1) << 4) | hex(h2));
      i += 2;
    } else r += c;
  }
  return r;
}

// ---------------------- Inicialización ----------------------
void setup() {
  pinMode(PIN_SSR, OUTPUT);
  digitalWrite(PIN_SSR, LOW);

  pinMode(PIN_RS485_DE_RE, OUTPUT);
  rs485SetTX(false);

  pinMode(PIN_CS_ETH, OUTPUT);
  digitalWrite(PIN_CS_ETH, HIGH);
  pinMode(PIN_CS_SD, OUTPUT);     // deshabilita SD
  digitalWrite(PIN_CS_SD, HIGH);

  pinMode(PIN_CS_MAX31865, OUTPUT);
  digitalWrite(PIN_CS_MAX31865, HIGH);

  pinMode(PIN_DRDY, INPUT_PULLUP);

  Serial.begin(115200);
  RS485_SERIAL.begin(9600); // 8N1 por defecto

  // MAX31865
  if (!max31865.begin(MAX31865_3WIRE)) {
    Serial.println(F("[MAX31865] Error al inicializar (verifica conexiones y 3 hilos)."));
  }
  max31865.enable50Hz(true); // Rechazo 50 Hz (ajusta a tu red)

  // Ethernet
  Ethernet.init(PIN_CS_ETH);
  Ethernet.begin(mac, ip, dns, gateway, subnet);
  delay(1000);
  server.begin();
  Serial.print(F("[ETH] IP: ")); Serial.println(Ethernet.localIP());

  // PID
  pid.SetOutputLimits(0.0, 100.0); // 0-100 %
  pid.SetSampleTime(1000);         // 1 s
  pid.SetMode(AUTOMATIC);

  windowStart = millis();
  Serial.println(F("[BOOT] rs485-fermentation-pid-control listo."));
}

// ---------------------- Lectura de temperatura ----------------------
void readTemperature() {
  // Lectura y chequeo de fallos
  uint8_t fault = max31865.readFault();
  if (fault) {
    max_fault = true;
    Serial.print(F("[MAX31865] FAULT: 0x"));
    Serial.println(fault, HEX);
    max31865.clearFault();
    return;
  } else {
    max_fault = false;
  }

  float t = max31865.temperature(RNOMINAL, RREF);
  if (isnan(t) || t < -50.0 || t > 250.0) {
    max_fault = true;
    return;
  }

  if (firstRead) {
    PV = t;
    firstRead = false;
  } else {
    PV = alpha * t + (1.0f - alpha) * PV; // filtro EMA
  }
}

// ---------------------- Control de salida (SSR) ----------------------
void driveOutput() {
  unsigned long now = millis();
  if (now - windowStart >= WINDOW_MS) {
    windowStart += WINDOW_MS;
  }

  double duty = (mode == MODE_AUTO) ? OUT : MAN;
  if (max_fault) duty = 0.0; // seguridad

  unsigned long onTime = (unsigned long)( (duty / 100.0) * WINDOW_MS );
  if ((now - windowStart) < onTime) {
    digitalWrite(PIN_SSR, HIGH);
  } else {
    digitalWrite(PIN_SSR, LOW);
  }
}

// ---------------------- Protocolo RS485 ASCII ----------------------
void processRS485Line(char* line) {
  // Elimina CR/LF finales
  size_t n = strlen(line);
  while (n > 0 && (line[n-1] == '\r' || line[n-1] == '\n' || line[n-1] == ' ')) { line[--n] = 0; }
  // Convierte a mayúsculas para comparar (mantén valores numéricos)
  for (size_t i = 0; i < n; i++) if (line[i] >= 'a' && line[i] <= 'z') line[i] -= 32;

  if (strncmp(line, "GET ", 4) == 0) {
    const char* what = line + 4;
    if (strcmp(what, "PV") == 0) {
      char out[32]; snprintf(out, sizeof(out), "PV:%.2f", PV);
      rs485SendLine(out);
    } else if (strcmp(what, "SP") == 0) {
      char out[32]; snprintf(out, sizeof(out), "SP:%.2f", SP);
      rs485SendLine(out);
    } else if (strcmp(what, "OUT") == 0) {
      char out[32]; snprintf(out, sizeof(out), "OUT:%.1f MODE:%s FAULT:%d", (mode==MODE_AUTO?OUT:MAN), (mode==MODE_AUTO?"AUTO":"MAN"), max_fault?1:0);
      rs485SendLine(out);
    } else if (strcmp(what, "ALL") == 0) {
      char out[96];
      snprintf(out, sizeof(out), "PV:%.2f SP:%.2f OUT:%.1f MODE:%s FAULT:%d", PV, SP, (mode==MODE_AUTO?OUT:MAN), (mode==MODE_AUTO?"AUTO":"MAN"), max_fault?1:0);
      rs485SendLine(out);
    } else {
      rs485SendLine("ERR:UNKNOWN_GET");
    }
  } else if (strncmp(line, "SET ", 4) == 0) {
    const char* rest = line + 4;
    if (strncmp(rest, "SP ", 3) == 0) {
      double v = atof(rest + 3);
      if (v >= -10.0 && v <= 40.0) { // típico rango de fermentación
        SP = v;
        rs485SendLine("OK");
      } else {
        rs485SendLine("ERR:SP_RANGE");
      }
    } else if (strncmp(rest, "MAN ", 4) == 0) {
      double v = atof(rest + 4);
      if (v >= 0.0 && v <= 100.0) {
        MAN = v;
        rs485SendLine("OK");
      } else {
        rs485SendLine("ERR:MAN_RANGE");
      }
    } else {
      rs485SendLine("ERR:UNKNOWN_SET");
    }
  } else if (strncmp(line, "MODE ", 5) == 0) {
    const char* m = line + 5;
    if (strcmp(m, "AUTO") == 0) {
      mode = MODE_AUTO;
      pid.SetMode(AUTOMATIC);
      rs485SendLine("OK");
    } else if (strcmp(m, "MAN") == 0 || strcmp(m, "MANUAL") == 0) {
      mode = MODE_MANUAL;
      pid.SetMode(MANUAL);
      rs485SendLine("OK");
    } else {
      rs485SendLine("ERR:UNKNOWN_MODE");
    }
  } else {
    rs485SendLine("ERR:UNKNOWN_CMD");
  }
}

void pollRS485() {
  while (RS485_SERIAL.available()) {
    char c = (char)RS485_SERIAL.read();
    if (c == '\n') {
      rs485_buf[rs485_len] = 0;
      processRS485Line(rs485_buf);
      rs485_len = 0;
    } else if (rs485_len + 1 < RS485_BUFSZ) {
      rs485_buf[rs485_len++] = c;
    } else {
      rs485_len = 0; // overflow -> reset
    }
  }
}

// ---------------------- HTTP server ----------------------
void handleHttpClient(EthernetClient& client) {
  // Lee la primera línea del request
  String req = client.readStringUntil('\r');
  client.readStringUntil('\n'); // consume LF
  if (req.length() == 0) return;

  // Muy simple: parsea método y ruta
  int sp1 = req.indexOf(' ');
  int sp2 = req.indexOf(' ', sp1 + 1);
  if (sp1 < 0 || sp2 < 0) { httpSend404(client); return; }
  String method = req.substring(0, sp1);
  String path = req.substring(sp1 + 1, sp2);

  // Lee y descarta cabeceras restantes hasta línea vacía
  while (true) {
    String h = client.readStringUntil('\r');
    client.readStringUntil('\n');
    if (h.length() == 0) break;
  }

  if (method != "GET") {
    httpSend404(client);
    return;
  }

  if (path == "/" || path.startsWith("/index")) {
    httpSendHeaderOK(client, "text/html; charset=utf-8");
    client.println("<!DOCTYPE html><html><head><meta charset='utf-8'><title>Fermentation PID</title></head><body>");
    client.println("<h1>Fermentation PID (RS485 + W5500)</h1>");
    client.print("<p>PV: "); client.print(PV, 2); client.println(" &deg;C</p>");
    client.print("<p>SP: "); client.print(SP, 2); client.println(" &deg;C</p>");
    client.print("<p>OUT: "); client.print((mode==MODE_AUTO?OUT:MAN), 1); client.println(" %</p>");
    client.print("<p>MODE: "); client.print((mode==MODE_AUTO?"AUTO":"MAN")); client.println("</p>");
    client.print("<p>FAULT: "); client.print(max_fault ? "YES" : "NO"); client.println("</p>");
    client.println("<p>Endpoints: <code>/status</code>, <code>/set?sp=XX.X</code>, <code>/mode?m=AUTO|MAN</code>, <code>/man?v=0..100</code></p>");
    client.println("</body></html>");
    return;
  }

  if (path.startsWith("/status")) {
    httpSendHeaderOK(client, "text/plain; charset=utf-8");
    client.print("PV:"); client.println(PV, 2);
    client.print("SP:"); client.println(SP, 2);
    client.print("OUT:"); client.println((mode==MODE_AUTO?OUT:MAN), 1);
    client.print("MODE:"); client.println((mode==MODE_AUTO?"AUTO":"MAN"));
    client.print("FAULT:"); client.println(max_fault ? 1 : 0);
    return;
  }

  if (path.startsWith("/set")) {
    int q = path.indexOf('?');
    if (q >= 0) {
      String qs = path.substring(q + 1);
      if (qs.startsWith("sp=")) {
        String v = urlDecode(qs.substring(3));
        double spv = v.toFloat();
        if (spv >= -10.0 && spv <= 40.0) {
          SP = spv;
          httpSendHeaderOK(client, "text/plain; charset=utf-8");
          client.println("OK");
          return;
        }
      }
    }
    httpSend404(client);
    return;
  }

  if (path.startsWith("/mode")) {
    int q = path.indexOf('?');
    if (q >= 0) {
      String qs = path.substring(q + 1);
      if (qs.startsWith("m=")) {
        String m = urlDecode(qs.substring(2));
        if (m == "AUTO") {
          mode = MODE_AUTO;
          pid.SetMode(AUTOMATIC);
          httpSendHeaderOK(client, "text/plain; charset=utf-8");
          client.println("OK");
          return;
        } else if (m == "MAN") {
          mode = MODE_MANUAL;
          pid.SetMode(MANUAL);
          httpSendHeaderOK(client, "text/plain; charset=utf-8");
          client.println("OK");
          return;
        }
      }
    }
    httpSend404(client);
    return;
  }

  if (path.startsWith("/man")) {
    int q = path.indexOf('?');
    if (q >= 0) {
      String qs = path.substring(q + 1);
      if (qs.startsWith("v=")) {
        String s = urlDecode(qs.substring(2));
        double v = s.toFloat();
        if (v >= 0.0 && v <= 100.0) {
          MAN = v;
          httpSendHeaderOK(client, "text/plain; charset=utf-8");
          client.println("OK");
          return;
        }
      }
    }
    httpSend404(client);
    return;
  }

  httpSend404(client);
}

// ---------------------- Bucle principal ----------------------
void loop() {
  // 1) Medición
  readTemperature();

  // 2) Control
  if (!max_fault && mode == MODE_AUTO) {
    pid.Compute();
  }

  // 3) Actuación
  driveOutput();

  // 4) Comunicación RS485
  pollRS485();

  // 5) HTTP
  EthernetClient client = server.available();
  if (client) {
    handleHttpClient(client);
    delay(1);
    client.stop();
  }

  // 6) Debug periódico
  static unsigned long t0 = 0;
  unsigned long now = millis();
  if (now - t0 >= 5000) {
    t0 = now;
    Serial.print(F("[DBG] PV=")); Serial.print(PV, 2);
    Serial.print(F(" SP=")); Serial.print(SP, 2);
    Serial.print(F(" OUT=")); Serial.print((mode==MODE_AUTO?OUT:MAN), 1);
    Serial.print(F(" MODE=")); Serial.print((mode==MODE_AUTO?"AUTO":"MAN"));
    Serial.print(F(" FAULT=")); Serial.println(max_fault ? 1 : 0);
  }
}

Puntos clave a revisar en el código:
– MAX31865: ajusta RREF a tu placa (430 Ω es común en Adafruit). RNOMINAL=100 (PT100).
– Filtro: suaviza lecturas (EMA).
– PID: coeficientes iniciales para un proceso térmico lento; debes afinarlos en tu sistema.
– Salida: estrategia de ventana fija para controlar SSR DC (recomendado frente a PWM de alta frecuencia).
– RS485: protocolo ASCII minimalista, fácil de probar con un adaptador USB–RS485.
– HTTP: endpoints planos para integrar rápidamente con cURL o scripts.

Compilación, “flash” y ejecución

Instalación de Arduino CLI y core

Linux (Ubuntu 22.04):

# 1) Instalar Arduino CLI 0.35.3 en ~/bin
mkdir -p ~/bin
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh -s -- -b ~/bin 0.35.3
export PATH="$HOME/bin:$PATH"

# 2) Verificar versión
arduino-cli version
# Debería mostrar: arduino-cli Version: 0.35.3

# 3) Inicializar configuración (si es primera vez)
arduino-cli config init

# 4) Actualizar índice e instalar core AVR 1.8.6
arduino-cli core update-index
arduino-cli core install arduino:avr@1.8.6

# 5) Instalar librerías exactas
arduino-cli lib install "Ethernet@2.0.2" "Adafruit MAX31865 library@1.5.0" "Adafruit BusIO@1.14.1" "PID@1.2.1"

Windows (PowerShell) es similar; asegúrate de añadir arduino-cli.exe al PATH y usar las mismas versiones.

Compilación

Coloca el sketch en una carpeta con el mismo nombre del .ino:

# Estructura:
# rs485-fermentation-pid-control/
# └── rs485-fermentation-pid-control.ino

arduino-cli compile --fqbn arduino:avr:mega --warnings all --export-binaries ./rs485-fermentation-pid-control

Salida esperada: ruta a .hex en ./rs485-fermentation-pid-control/build/arduino.avr.mega/…

Subida (flash)

Identifica el puerto serie del Mega (ej.: /dev/ttyACM0 en Linux, COM5 en Windows).

# Linux
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:mega ./rs485-fermentation-pid-control

# Windows (ejemplo)
arduino-cli upload -p COM5 --fqbn arduino:avr:mega .\rs485-fermentation-pid-control

Ejecución y monitor serie

arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200

Deberías ver logs de arranque y depuración cada 5 s.

Validación paso a paso

1) Verificación de red (W5500)

  • Conecta el cable Ethernet al switch/router de tu laboratorio.
  • Ping:
  • Linux/Windows: ping 192.168.1.77
  • HTTP con cURL:
  • Estado: curl http://192.168.1.77/status
    • Debe devolver texto con PV, SP, OUT, MODE y FAULT.
  • Página: curl http://192.168.1.77/
    • Debe devolver HTML básico.
  • Cambiar setpoint: curl «http://192.168.1.77/set?sp=21.0» → OK
  • Cambiar modo: curl «http://192.168.1.77/mode?m=MAN» → OK
  • Salida manual: curl «http://192.168.1.77/man?v=25» → OK

Resultados esperados:
– /status: valores numéricos coherentes; FAULT=0; PV estable o lentamente cambiante.

2) Verificación del MAX31865 y RTD

  • A temperatura ambiente, PV ~ 18–25 °C (según sala).
  • Toca con la mano la sonda RTD: la temperatura debe subir algunos grados en ~10–20 s.
  • Si FAULT cambia a 1:
  • Revisa cableado 3 hilos, jumpers de 3-wire, continuidad de la sonda, y nivel lógico.

3) Verificación de RS485

  • Usa un adaptador USB–RS485 en el PC. Conecta A↔A, B↔B, GND común.
  • Configura terminación si tu tramo es corto y solo tienes 2 nodos: termina en ambos extremos (módulo del Mega y el dongle).
  • Abre un terminal serie a 9600 8N1 (por ejemplo, screen/minicom):
  • Linux: screen /dev/ttyUSB0 9600
  • Envía comandos:
  • GET PV
  • GET SP
  • SET SP 19.0
  • MODE MAN
  • SET MAN 30
  • GET ALL

Resultados esperados:
– Respuestas con “OK” o datos, p. ej.: PV:20.35, SP:19.00, OUT:xx.x MODE:MAN FAULT:0.

4) Validación del control PID (sin riesgo)

  • Usa un pad calefactor 12 V con SSR DC-DC y un recipiente aislado con agua (0.5–1 L).
  • Coloca la PT100 sumergida, evitando tocar directamente el pad/calefactor.
  • Modo AUTO:
  • Ajusta SP a 25.0 °C con curl o RS485.
  • Observa /status cada 30–60 s: PV debe aproximarse a SP sin oscilaciones grandes.
  • OUT tenderá a bajar conforme PV se acerque a SP.
  • Modo MAN:
  • Cambia a MAN y fija MAN=0, 20, 50, 100.
  • Observa que el SSR enciende durante una fracción de la ventana (0–100 % del tiempo de 2 s).

Criterios de éxito:
– El sistema responde a SP y mantiene PV en ±0.3–0.8 °C a entorno estable (depende de aislamiento y agitación).
– RS485 y HTTP devuelven valores consistentes con monitor serie.

Troubleshooting (5–8 errores típicos y soluciones)

1) Ethernet no responde (sin ping o /status no carga)
– Causas:
– MAC duplicada en la red.
– IP en subred distinta o conflicto de IP.
– CS de SD (D4) sin deshabilitar, interfiriendo con SPI.
– Soluciones:
– Cambia MAC a una diferente (p. ej., último byte).
– Ajusta IP/gateway/subnet a tu red.
– Asegura pinMode(4, OUTPUT) y digitalWrite(4, HIGH) para deshabilitar SD.

2) Lectura de temperatura errática o FAULT recurrente
– Causas:
– MAX31865 a 3.3 V sin conversión de nivel; SCK/MOSI/CS/DRDY recibiendo 5 V.
– Cableado 3 hilos incorrecto o jumper en modo 2/4 hilos.
– RREF no coincide con tu placa (error en cálculo).
– Soluciones:
– Usa level shifter para todas las líneas SPI y DRDY.
– Verifica configuración 3-wire en el MAX31865.
– Ajusta RREF (comúnmente 430.0 Ω en Adafruit; revisa tu módulo).

3) RS485 sin respuestas o caracteres basura
– Causas:
– A/B invertidos.
– DE/RE no controlados correctamente (siempre en RX o TX).
– Falta de terminación o exceso de terminaciones en tramo corto.
– Soluciones:
– Intercambia A/B y prueba de nuevo.
– Revisa PIN_RS485_DE_RE; debe ir HIGH al transmitir y LOW al recibir.
– Activa solo dos terminaciones (en extremos del bus).

4) El SSR no conmuta como se espera
– Causas:
– SSR incompatible (AC-AC siendo conducido con DC).
– Nivel lógico insuficiente en entrada SSR o polaridad invertida.
– Ventana de tiempo muy corta o SP/OUT en cero por FAULT.
– Soluciones:
– Usa SSR DC-DC para carga DC; verifica especificaciones.
– Comprueba D6→IN+ y GND→IN- y el LED del SSR (si lo tiene).
– Aumenta WINDOW_MS o revisa que max_fault sea 0.

5) Errores de compilación por librerías
– Causas:
– Nombres diferentes en Library Manager o versiones no coinciden.
– Soluciones:
– Reinstala con los nombres exactos: «Ethernet», «Adafruit MAX31865 library», «Adafruit BusIO», «PID».

6) Fallo al subir (upload) el sketch
– Causas:
– Puerto incorrecto o permisos (Linux).
– FQBN erróneo.
– Soluciones:
– Lista puertos: arduino-cli board list.
– Añade tu usuario a dialout: sudo usermod -aG dialout $USER; reinicia sesión.
– Usa –fqbn arduino:avr:mega.

7) Oscilaciones o sobreimpulso térmico excesivo
– Causas:
– Ganancias PID no sintonizadas para tu sistema térmico (inercia alta).
– Soluciones:
– Disminuye Kp, aumenta ligeramente Ki (cuidado con windup), ajusta Kd.
– Incrementa WINDOW_MS si la conmutación es muy rápida para tu carga.

8) Congestión SPI (W5500 y MAX31865)
– Causa:
– CS mal gestionados; dos dispositivos activos simultáneamente.
– Solución:
– Revisa que cada CS esté en HIGH salvo el dispositivo activo.
– Evita acceso concurrente en interrupciones (no usamos aquí).

Mejoras/variantes

  • Modbus RTU sobre RS485:
  • Sustituye protocolo ASCII por Modbus RTU (esclavo) usando una librería Modbus conforme. Expone holding registers: PV (scaled), SP, OUT, MODE, FAULT.
  • Registro de datos y MQTT:
  • Añade cliente MQTT por TCP en W5500 para publicar PV/SP/OUT hacia un broker local.
  • PID autotune:
  • Integra un algoritmo de autotuning (Ziegler-Nichols, Tyreus-Luyben) para obtener parámetros específicos de tu planta térmica.
  • Multipunto RS485:
  • Dirección de nodo y anti-colisión. Soporta múltiples controladores en un bus largo con direccionamiento.
  • Seguridad y enclavamientos:
  • Watchdog, corte por sobretemperatura, validación de sensor dual (RTD + termistor backup).
  • Interfaz web mejorada:
  • Página web con sliders y gráficos en tiempo real (sin depender de ArduinoJson si usas endpoints planos).
  • Redundancia de alimentación:
  • Separación de alimentación lógica y potencia para minimizar ruido en la medición.

Checklist de verificación

  • [ ] Ubuntu 22.04 o Windows 11 instalados y actualizados.
  • [ ] Arduino CLI 0.35.3 en PATH y verificado con arduino-cli version.
  • [ ] Core arduino:avr@1.8.6 instalado.
  • [ ] Librerías instaladas: Ethernet@2.0.2, Adafruit MAX31865 library@1.5.0, Adafruit BusIO@1.14.1, PID@1.2.1.
  • [ ] Cableado del W5500 Shield montado correctamente sobre el Mega (ICSP OK, CS en D10).
  • [ ] MAX31865 alimentado a 3.3 V y con conversión de nivel en SCK/MOSI/CS/DRDY.
  • [ ] RTD PT100 cableado en 3 hilos y jumpers de 3-wire configurados en el MAX31865.
  • [ ] RS485: DI→TX2 (D16), RO→RX2 (D17), DE/RE→D22, A/B al bus, terminación correcta.
  • [ ] SSR DC-DC comandado desde D6; carga de 12 V segura para pruebas.
  • [ ] Compilación exitosa con arduino-cli compile –fqbn arduino:avr:mega.
  • [ ] Subida exitosa a través del puerto correcto.
  • [ ] Respuesta a ping 192.168.1.77 y endpoints /, /status.
  • [ ] RS485 responde a GET/SET/MODE con OK/datos esperados.
  • [ ] Control PID estabiliza PV cerca de SP sin oscilaciones significativas.
  • [ ] FAULT=0 en operación normal; FAULT=1 ante desconexión de RTD y salida desactivada.

Apéndice: Script de validación RS485 (opcional)

Si prefieres automatizar la validación por RS485 desde tu PC con Python 3 y pyserial:

#!/usr/bin/env python3
import serial, time

PORT = "/dev/ttyUSB0"  # ajusta a tu dongle RS485
BAUD = 9600

def cmd(ser, s):
    ser.write((s+"\n").encode())
    ser.flush()
    line = ser.readline().decode().strip()
    print(f"> {s}\n< {line}")
    time.sleep(0.2)

with serial.Serial(PORT, BAUD, timeout=1) as ser:
    cmd(ser, "GET ALL")
    cmd(ser, "SET SP 20.0")
    cmd(ser, "GET SP")
    cmd(ser, "MODE MAN")
    cmd(ser, "SET MAN 30")
    cmd(ser, "GET OUT")
    cmd(ser, "MODE AUTO")
    cmd(ser, "GET ALL")

Con este caso práctico has desplegado un controlador PID de fermentación robusto sobre Arduino Mega 2560, integrando campo RS485 (MAX485) y monitoreo/control por Ethernet (W5500), con medición precisa de temperatura vía RTD PT100 y MAX31865. El flujo de trabajo, la toolchain exacta y el código son reproducibles y extensibles para usos industriales ligeros o de laboratorio avanzado.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a 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.

Quiz rápido

Pregunta 1: ¿Cuál es el sistema operativo recomendado para este proyecto?




Pregunta 2: ¿Qué versión de Arduino CLI se debe utilizar?




Pregunta 3: ¿Cuál es el FQBN correcto para Arduino Mega 2560?




Pregunta 4: ¿Qué librería es necesaria para el módulo MAX31865?




Pregunta 5: ¿Qué tipo de conexión utiliza el W5500 Ethernet Shield?




Pregunta 6: ¿Cuál es la velocidad de monitor serie recomendada?




Pregunta 7: ¿Qué tipo de sonda RTD se debe utilizar?




Pregunta 8: ¿Qué tipo de convertidor se necesita para la comunicación SPI entre 5 V y 3.3 V?




Pregunta 9: ¿Qué resistencias son necesarias para el módulo MAX485?




Pregunta 10: ¿Qué tipo de relé se recomienda para controlar el pad calefactor?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:
error: Contenido Protegido / Content is protected !!
Scroll to Top