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(" °C</p>");
client.print("<p>SP: "); client.print(SP, 2); client.println(" °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
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.



