Practical case: PID fermentation control via RS485/Ethernet

Practical case: PID fermentation control via RS485/Ethernet — hero

Objective and use case

What you’ll build: A robust fermentation temperature controller using Arduino Mega 2560, RS485, and Ethernet to manage and monitor fermentation processes effectively.

Why it matters / Use cases

  • Automated fermentation control for breweries to maintain optimal temperatures, ensuring consistent product quality.
  • Remote monitoring of fermentation parameters via Ethernet, allowing for quick adjustments and oversight from anywhere.
  • Integration with existing industrial systems using RS485 Modbus for seamless communication and control.
  • Real-time data logging of temperature and PID tuning for analysis and optimization of fermentation processes.
  • Enhanced safety and efficiency in fermentation by automating temperature adjustments based on sensor feedback.

Expected outcome

  • Temperature control accuracy within ±0.5°C, ensuring precise fermentation conditions.
  • Response time of the system to temperature changes under 5 seconds, allowing for timely adjustments.
  • Data transmission rates of 9600 baud over RS485, ensuring reliable communication with minimal latency.
  • HTTP JSON status updates every 10 seconds, providing real-time insights into fermentation conditions.
  • Successful PID tuning leading to reduced overshoot and settling time, improving overall process stability.

Audience: Intermediate to advanced users; Level: Advanced project implementation.

Architecture/flow: Arduino Mega 2560 with W5500 Ethernet Shield, MAX485 for RS485 communication, and MAX31865 for RTD temperature sensing.

Hands‑on Practical Case: RS485 Fermentation PID Control on Arduino Mega 2560 + W5500 + MAX485 + MAX31865

This advanced project builds a robust fermentation temperature controller using a PT100 RTD sensor, a time‑proportioning PID for a heater, RS485 Modbus‑RTU for supervision/control, and an Ethernet REST status endpoint. You’ll deploy firmware on an Arduino Mega 2560 with a W5500 Ethernet Shield, a MAX485 transceiver for RS485, and a MAX31865 RTD amplifier.

The goal: an industrial‑style “rs485‑fermentation‑pid‑control” that exposes setpoints, tunings, and process values over RS485 (Modbus‑RTU), uses a reliable RTD temperature front‑end, and provides an auxiliary HTTP JSON status on Ethernet.

You’ll find precise wiring (text and table), full code, deterministic build/flash/run commands with Arduino CLI, step‑by‑step validation (sensor, PID actuation, RS485, Ethernet), troubleshooting, and improvements.


Prerequisites

  • Familiarity with:
  • SPI devices on shared buses and chip‑select discipline.
  • UART/RS485 half‑duplex with transceiver direction control.
  • PID control concepts and time‑proportioning (SSR windows).
  • Arduino CLI workflow on Linux/macOS/Windows.
  • Tools:
  • A computer with Arduino CLI v0.32.0 or newer (tested with v0.35.x).
  • A USB cable for Arduino Mega 2560.
  • For RS485 validation: a USB‑RS485 adapter and the mbpoll tool.
  • For HTTP validation: curl.
  • Basic hand tools and a multimeter.

Safety note: If you drive a mains heater with an SSR, follow electrical codes, use proper enclosures and fusing, and isolate high voltage wiring. The controller side remains low voltage; never touch mains terminals when powered.


Materials (exact models)

  • Microcontroller: Arduino Mega 2560 (ATmega2560‑16AU)
  • Ethernet: Arduino W5500 Ethernet Shield (compatible with Arduino Ethernet library)
  • RS485: MAX485 module (5V TTL, RO/RE/DE/DI pinout; typical small PCB module)
  • RTD Front‑end: MAX31865 breakout (3.3V, PT100/PT1000 compatible; Adafruit MAX31865 or equivalent with level shifting)
  • Temperature sensor: PT100 (Class B or better), 2‑wire or 3‑wire (preferable), stainless probe
  • Solid State Relay (SSR): Zero‑cross AC SSR rated appropriately for your heater
  • Power: 5V from Arduino; 3.3V for MAX31865
  • Cabling and termination: 120 Ω terminator for RS485 bus end
  • Optional: USB‑RS485 dongle for testing Modbus‑RTU

Setup / Connection

The Arduino Mega 2560 hosts three peripherals:

  • W5500 Ethernet shield: SPI device using the ICSP header (hardware SPI), CS on D10.
  • MAX31865 RTD amplifier: SPI device on the same bus, dedicated CS on D9.
  • MAX485 RS485 transceiver: Half‑duplex UART using Serial1 (TX1/RX1 on pins 18/19), direction control pin on D2.

We also control a heater via an SSR on D6 using a time‑proportion window (on/off within a fixed time window to approximate duty cycle).

SPI considerations

  • On the Mega, hardware SPI is on the 6‑pin ICSP header (not pins 11/12/13 as on the Uno).
  • The W5500 shield already routes MISO/MOSI/SCK through ICSP.
  • The MAX31865 must share SPI lines and use a distinct CS pin. Ensure the non‑active CS stays HIGH to avoid bus contention.

RS485 considerations

  • Use Serial1 for RS485. Connect:
  • Mega TX1 (pin 18) → MAX485 DI
  • Mega RX1 (pin 19) → MAX485 RO
  • DE and RE (active‑high) tied together → Mega D2 (direction control)
  • MAX485 VCC 5V and GND to Mega 5V/GND
  • RS485 A/B lines to twisted pair; 120 Ω termination at the far ends; bias resistors as recommended (many modules include bias).

PT100 wiring to MAX31865

  • Choose 3‑wire PT100 if available; it compensates for lead resistance.
  • Follow your MAX31865 board’s 2/3/4‑wire jumpers.
  • Typical Adafruit MAX31865 has solder jumpers for 2‑wire (bridge) or 3‑wire mode.
  • Reference resistor (Rref) on most PT100 boards is 430.0 Ω. Use the exact value marked on your board for best accuracy.

SSR (heater) wiring

  • Mega D6 → SSR input “+”
  • Mega GND → SSR input “−”
  • SSR load side in series with the heater and mains (performed by a qualified person). Use zero‑cross SSR for resistive heaters.

Pin/Signal Mapping Table

Function Device/Pin Arduino Mega 2560 Pin Notes
SPI SCK W5500 + MAX31865 ICSP‑3 Shared SPI via ICSP
SPI MISO W5500 + MAX31865 ICSP‑1 Shared
SPI MOSI W5500 + MAX31865 ICSP‑4 Shared
W5500 CS W5500 Ethernet Shield D10 Reserved for Ethernet
SD card CS (on shield) microSD (unused) D4 Set HIGH as OUTPUT to deselect
MAX31865 CS MAX31865 D9 Choose any free digital pin
MAX31865 VIN MAX31865 3.3V Prefer 3.3V; ensure board is 5V‑tolerant on logic
MAX31865 GND MAX31865 GND Common ground
RS485 DI MAX485 Mega TX1 (D18) Serial1 TX
RS485 RO MAX485 Mega RX1 (D19) Serial1 RX
RS485 DE+RE MAX485 D2 Tie DE and RE together to D2
RS485 VCC MAX485 5V Power for MAX485
RS485 GND MAX485 GND Common ground
Heater SSR control SSR (input +) D6 Time‑proportion window output
Heater SSR return SSR (input −) GND Return

Notes:
– If your MAX31865 breakout isn’t 5V‑tolerant on logic, keep SPI at 3.3V via level shifting. Many brand‑name boards include level shifting and tolerate 5V logic; check docs.
– Keep SPI CS lines configured as OUTPUT and set HIGH when not selected. We explicitly set D4 HIGH (microSD CS) to avoid interference.


Full Code (Arduino Mega 2560)

Create a folder “fermenter‑pid‑rs485” and save the following as “fermenter‑pid‑rs485/fermenter‑pid‑rs485.ino”.

/*
  Fermentation PID Controller with RS485 (Modbus-RTU) and Ethernet status
  Hardware: Arduino Mega 2560 + W5500 Ethernet Shield + MAX485 + MAX31865 (PT100)
  Features:
    - PT100 via MAX31865 (SPI)
    - PID (time-proportioning window) for heater SSR on D6
    - Modbus-RTU slave on RS485 (Serial1), DE/RE on D2
    - HTTP JSON status on Ethernet (W5500) at /status
*/

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

// ---------------- Hardware configuration ----------------
static const uint8_t PIN_ETHERNET_CS = 10;  // W5500 CS (from shield)
static const uint8_t PIN_SD_CS       = 4;   // SD CS (unused)
static const uint8_t PIN_MAX31865_CS = 9;   // MAX31865 CS
static const uint8_t PIN_RS485_DIR   = 2;   // MAX485 DE+RE
static const uint8_t PIN_HEATER_SSR  = 6;   // Heater SSR control

// MAX31865 object
Adafruit_MAX31865 rtd = Adafruit_MAX31865(PIN_MAX31865_CS);

// RTD/board constants (adjust to your board’s exact Rref and RTD type)
#define RREF      430.0   // Ohms for PT100 board (typical Adafruit: 430.0)
#define RNOMINAL  100.0   // Ohms for PT100

// Ethernet configuration
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; // Use a unique MAC on your LAN
IPAddress ip(192, 168, 1, 50);
IPAddress dns(192, 168, 1, 1);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);

EthernetServer server(80);

// ---------------- PID configuration ----------------
double setpointC = 20.00;    // default 20.00 C
double inputC = 0.0;
double outputPct = 0.0;      // 0..100 %
double Kp = 30.0, Ki = 0.5, Kd = 200.0;  // conservative starting point (tune in process)

PID pid(&inputC, &outputPct, &setpointC, Kp, Ki, Kd, REVERSE);

// Time-proportioning window (ms)
volatile unsigned long windowSizeMs = 2000UL;
unsigned long windowStart = 0;

// Enable/disable control
volatile bool controlEnabled = true;

// ---------------- Modbus RTU (slave) on RS485 ----------------
ModbusRTU mb;

// Modbus unit id
static const uint8_t MODBUS_ID = 1;

// Holding Register map (16-bit word registers)
// Scale strategy: temperatures in centi-degC, PID terms scaled by x100, output in 0..1000 = 0..100.0%
enum {
  HR_SETPOINT_Cx100 = 0, // write/read
  HR_TEMP_Cx100     = 1, // read
  HR_OUTPUT_x10     = 2, // read (0..1000)
  HR_WINDOW_MS_L    = 3, // write/read low 16 bits
  HR_WINDOW_MS_H    = 4, // write/read high 16 bits
  HR_KP_x100        = 5, // write/read
  HR_KI_x100        = 6, // write/read
  HR_KD_x100        = 7, // write/read
  HR_ENABLE         = 8, // write/read (0=off, 1=on)
  HR_FAULT          = 9, // read (fault bitmap from MAX31865)
  HR_COUNT          = 10
};

uint16_t hregs[HR_COUNT] = {0};

// ---------------- Helpers ----------------
void applyModbusWrites() {
  // Apply changes from hregs to runtime variables
  setpointC = ((int16_t)hregs[HR_SETPOINT_Cx100]) / 100.0;
  uint32_t ws = (uint32_t)hregs[HR_WINDOW_MS_L] | ((uint32_t)hregs[HR_WINDOW_MS_H] << 16);
  if (ws >= 250 && ws <= 60000) {  // limit reasonable window 0.25..60 s
    windowSizeMs = ws;
  }
  bool newEnable = (hregs[HR_ENABLE] != 0);
  controlEnabled = newEnable;
  // Update tunings if changed
  Kp = ((int16_t)hregs[HR_KP_x100]) / 100.0;
  Ki = ((int16_t)hregs[HR_KI_x100]) / 100.0;
  Kd = ((int16_t)hregs[HR_KD_x100]) / 100.0;
  pid.SetTunings(Kp, Ki, Kd);
}

void refreshModbusRegs() {
  // Write runtime values back to holding registers
  int16_t tempCx100 = (int16_t)(inputC * 100.0);
  hregs[HR_TEMP_Cx100] = (uint16_t)tempCx100;
  uint16_t outx10 = (uint16_t)(outputPct * 10.0); // 0..1000
  hregs[HR_OUTPUT_x10] = outx10;
  hregs[HR_WINDOW_MS_L] = (uint16_t)(windowSizeMs & 0xFFFF);
  hregs[HR_WINDOW_MS_H] = (uint16_t)(windowSizeMs >> 16);
  hregs[HR_SETPOINT_Cx100] = (int16_t)(setpointC * 100.0);
  hregs[HR_KP_x100] = (int16_t)(Kp * 100.0);
  hregs[HR_KI_x100] = (int16_t)(Ki * 100.0);
  hregs[HR_KD_x100] = (int16_t)(Kd * 100.0);
  hregs[HR_ENABLE] = controlEnabled ? 1 : 0;
  // Fault register is handled in the sensor read step
}

bool cbReadHreg(TRegister* reg, uint16_t val) {
  // Called after each read; nothing special needed
  (void)reg; (void)val;
  return true;
}

bool cbWriteHreg(TRegister* reg, uint16_t val) {
  // Called on writes; update local mirror and apply changes
  (void)val;
  // Mirror all HREGs from Modbus stack to hregs[]
  for (uint16_t i = 0; i < HR_COUNT; i++) {
    hregs[i] = mb.Hreg(i);
  }
  applyModbusWrites();
  return true;
}

void setupModbus() {
  Serial1.begin(19200, SERIAL_8N1); // RS485 bus speed
  mb.begin(&Serial1, PIN_RS485_DIR);
  mb.slave(MODBUS_ID);

  // Add holding registers
  for (uint16_t i = 0; i < HR_COUNT; i++) {
    mb.addHreg(i, 0, 1); // 1 = allow write (we'll gate at application level)
  }
  // Initialize with defaults
  hregs[HR_SETPOINT_Cx100] = (int16_t)(setpointC * 100.0);
  hregs[HR_WINDOW_MS_L] = (uint16_t)(windowSizeMs & 0xFFFF);
  hregs[HR_WINDOW_MS_H] = (uint16_t)(windowSizeMs >> 16);
  hregs[HR_KP_x100] = (int16_t)(Kp * 100.0);
  hregs[HR_KI_x100] = (int16_t)(Ki * 100.0);
  hregs[HR_KD_x100] = (int16_t)(Kd * 100.0);
  hregs[HR_ENABLE] = controlEnabled ? 1 : 0;

  // Push initial values to stack
  for (uint16_t i = 0; i < HR_COUNT; i++) {
    mb.Hreg(i, hregs[i]);
  }

  // Register callbacks
  mb.onGetHreg(cbReadHreg);
  mb.onSetHreg(cbWriteHreg);
}

void setupEthernet() {
  pinMode(PIN_ETHERNET_CS, OUTPUT);
  digitalWrite(PIN_ETHERNET_CS, HIGH);
  pinMode(PIN_SD_CS, OUTPUT);
  digitalWrite(PIN_SD_CS, HIGH); // Deselect SD
  Ethernet.init(PIN_ETHERNET_CS);
  Ethernet.begin(mac, ip, dns, gateway, subnet);
  delay(1000);
  server.begin();
}

void setupMAX31865() {
  if (!rtd.begin(MAX31865_3WIRE)) { // set to MAX31865_2WIRE, _3WIRE, or _4WIRE
    // If begin fails, continue; faults will be reported
  }
}

void setupPID() {
  pid.SetOutputLimits(0.0, 100.0); // 0..100 %
  pid.SetMode(AUTOMATIC);
  pid.SetSampleTime(1000); // 1 s sample time
  windowStart = millis();
}

// Read temperature and record fault flags
uint8_t lastFault = 0;
void readTemperature() {
  lastFault = rtd.readFault(); // Check before conversion to clear stale flags
  if (lastFault) {
    rtd.clearFault();
  }
  inputC = rtd.temperature(RNOMINAL, RREF);
  // Fault capture for Modbus
  lastFault = rtd.readFault();
  hregs[HR_FAULT] = lastFault;
}

// Apply time-proportioning control to SSR
void applySSR() {
  unsigned long now = millis();
  if (now - windowStart >= windowSizeMs) {
    windowStart += windowSizeMs; // Avoid drift from millis() wrap
  }
  bool heaterOn = false;
  if (controlEnabled) {
    unsigned long onTime = (unsigned long)(outputPct * windowSizeMs / 100.0);
    heaterOn = ((now - windowStart) < onTime);
  } else {
    heaterOn = false;
  }
  digitalWrite(PIN_HEATER_SSR, heaterOn ? HIGH : LOW);
}

void handleHTTP() {
  EthernetClient client = server.available();
  if (!client) return;

  // Very simple HTTP 1.0 parser
  String req = client.readStringUntil('\r');
  client.readStringUntil('\n'); // consume newline
  // Only handle GET /status
  bool ok = req.startsWith("GET /status");
  while (client.available()) client.read(); // drain

  if (!ok) {
    client.println("HTTP/1.0 404 Not Found");
    client.println("Content-Type: text/plain");
    client.println("Connection: close");
    client.println();
    client.println("Not Found");
    client.stop();
    return;
  }

  // Build JSON
  client.println("HTTP/1.0 200 OK");
  client.println("Content-Type: application/json");
  client.println("Cache-Control: no-store");
  client.println("Connection: close");
  client.println();
  client.print("{\"ip\":\"");
  client.print(Ethernet.localIP());
  client.print("\",\"setpoint_c\":");
  client.print(setpointC, 2);
  client.print(",\"temp_c\":");
  client.print(inputC, 2);
  client.print(",\"output_pct\":");
  client.print(outputPct, 1);
  client.print(",\"window_ms\":");
  client.print(windowSizeMs);
  client.print(",\"enabled\":");
  client.print(controlEnabled ? "true" : "false");
  client.print(",\"kp\":");
  client.print(Kp, 2);
  client.print(",\"ki\":");
  client.print(Ki, 2);
  client.print(",\"kd\":");
  client.print(Kd, 2);
  client.print(",\"fault\":");
  client.print(lastFault);
  client.println("}");
  client.stop();
}

void setup() {
  // IO basics
  pinMode(PIN_HEATER_SSR, OUTPUT);
  digitalWrite(PIN_HEATER_SSR, LOW);
  pinMode(PIN_RS485_DIR, OUTPUT);
  digitalWrite(PIN_RS485_DIR, LOW); // receive by default

  // Subsystems
  setupEthernet();
  setupMAX31865();
  setupPID();
  setupModbus();
}

unsigned long lastCompute = 0;
void loop() {
  // 1 s loop for PID; sensor read first
  unsigned long now = millis();
  if (now - lastCompute >= 1000) {
    lastCompute = now;
    readTemperature();
    if (controlEnabled && !hregs[HR_FAULT]) {
      pid.Compute();
    } else {
      outputPct = 0.0;
    }
    refreshModbusRegs();
    // Push HREG mirrors to stack for Modbus clients to read
    for (uint16_t i = 0; i < HR_COUNT; i++) {
      mb.Hreg(i, hregs[i]);
    }
  }

  // Apply SSR time proportioning continuously
  applySSR();

  // Service Modbus RTU
  mb.task();

  // Service HTTP requests
  handleHTTP();
}

Key notes on the code:

  • The Modbus holding registers are scaled to keep integer 16‑bit values:
  • Temperatures are centi‑degrees (e.g., 2000 = 20.00 C).
  • PID tunings multiplied by 100.
  • Output multiplied by 10 (0..1000 equals 0.0..100.0%).
  • Window size stored as a 32‑bit value across two HREGs.
  • PID is “REVERSE” action: when the temperature is below setpoint, the output increases.
  • Time‑proportioning SSR window defaults to 2 s; adjust via Modbus.
  • An HTTP GET /status returns a JSON snapshot for quick integration.

Build / Flash / Run (Arduino CLI)

We use Arduino CLI on the Arduino Mega 2560 (AVR). Commands below are explicit and reproducible. Replace the serial port with your actual port.

Create the project directory and place the .ino as instructed:
– Project path: ~/projects/fermenter-pid-rs485/fermenter-pid-rs485.ino

Install Arduino CLI and libraries:

arduino-cli version

# Update core indices and install AVR core
arduino-cli core update-index
arduino-cli core install arduino:avr

# Verify the board is detected and note the port (e.g., /dev/ttyACM0 or COM5)
arduino-cli board list

# Install required libraries with explicit versions
arduino-cli lib install "Ethernet@2.0.2"
arduino-cli lib install "Adafruit MAX31865 library@1.4.5"
arduino-cli lib install "PID@1.2.1"
arduino-cli lib install "ModbusRTU@2.3.8"

Compile and upload for Arduino Mega 2560:

# Compile (FQBN for Mega 2560)
arduino-cli compile --fqbn arduino:avr:mega ~/projects/fermenter-pid-rs485

# Upload (replace port as detected)
arduino-cli upload -p /dev/ttyACM0 --fqbn arduino:avr:mega ~/projects/fermenter-pid-rs485

Run and monitor serial logs if you add prints (this sketch is quiet on serial by design):

# Optional: serial monitor at 115200 if you add debug messages
arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200

Network sanity checks after the board reboots:

# Check reachability
ping -c 3 192.168.1.50

# Fetch JSON status
curl -s http://192.168.1.50/status | jq .

Step‑by‑step Validation

Follow these steps systematically after wiring is complete and firmware is flashed.

1) SPI bus integrity and device selection

  • Power up the system with the W5500 shield attached.
  • Ensure D4 is set HIGH (our code does this) so the SD card is deselected.
  • Confirm the MAX31865 CS line idles HIGH and toggles when reading.
  • If you have a logic probe or LED, verify activity on D9 when the board runs.

Expected: Ethernet responds to pings, HTTP status is reachable, temperature field shows a plausible value.

2) Temperature sensing via MAX31865

  • Run:
  • curl -s http://192.168.1.50/status
  • Verify fields: "temp_c" is near ambient (e.g., 20‑28 C). Touch the PT100 probe: temperature should climb slowly.
  • Fault handling:
  • Disconnect the RTD: fault should become non‑zero (open circuit flags). Reconnect to clear.
  • If temperature reads unrealistically high/low or fault persists, recheck MAX31865 wiring and jumper configuration (2/3/4‑wire).

3) PID output and SSR actuation

  • The heater SSR output is on D6. Without a heater connected, you can still observe LED on SSR modules (if present).
  • Temporarily set setpoint below current temp to force 0% output:
  • Default setpoint is 20.00 C; if ambient is ~23 C, output should be 0.
  • Raise the setpoint via Modbus to 28.00 C and confirm the output rises (see steps in RS485 section).
  • Observe SSR behavior:
  • The SSR will toggle within a 2 s window.
  • At 50% output, it should be ON ~1 s and OFF ~1 s within each window.

4) RS485 Modbus‑RTU: read/write registers

Use a USB‑RS485 adapter and connect it to the RS485 A/B lines with proper polarity and a 120 Ω terminator at the far end of the bus (your controller can be one end, the USB dongle the other).

Install mbpoll (Linux/macOS; Windows builds are also available):

# Debian/Ubuntu
sudo apt-get update && sudo apt-get install -y mbpoll

# macOS (Homebrew)
brew install mbpoll

Reading status registers (unit id 1, 19200 8N1, no parity):

# Read 10 holding registers starting at 0
mbpoll -m rtu -a 1 -b 19200 -P none -t 4 -r 0 -c 10 /dev/ttyUSB0
  • -t 4 selects holding registers.
  • Expect values:
  • Reg0 (setpoint) ~ 2000 for 20.00 C
  • Reg1 (temp) varies around ambient
  • Reg2 (output) 0..1000
  • Reg3/4 (window L/H)
  • Reg5/6/7 for Kp/Ki/Kd x100
  • Reg8 enable (0/1)
  • Reg9 fault bitmap

Write a new setpoint to 28.00 C (2800 in centi‑C):

# Write single register (HR 0) value 2800
mbpoll -m rtu -a 1 -b 19200 -P none -t 4 -r 0 -1 2800 /dev/ttyUSB0

Enable control (HR 8 = 1):

mbpoll -m rtu -a 1 -b 19200 -P none -t 4 -r 8 -1 1 /dev/ttyUSB0

Change PID tunings (example Kp=35.00, Ki=0.80, Kd=250.00), scale x100:

mbpoll -m rtu -a 1 -b 19200 -P none -t 4 -r 5 -1 3500 /dev/ttyUSB0
mbpoll -m rtu -a 1 -b 19200 -P none -t 4 -r 6 -1 80   /dev/ttyUSB0
mbpoll -m rtu -a 1 -b 19200 -P none -t 4 -r 7 -1 25000 /dev/ttyUSB0

Confirm that:
– The output (HR 2) rises when the setpoint exceeds the current temperature.
– The HTTP /status JSON reflects the same variables you wrote via Modbus.

5) Ethernet status endpoint

  • Check link lights on the Ethernet shield.
  • Verify IP: ping 192.168.1.50
  • Retrieve JSON:
    bash
    curl -s http://192.168.1.50/status | jq .
  • Fields to verify:
  • setpoint_c, temp_c, output_pct, window_ms, enabled, kp/ki/kd, fault.
  • Cross‑compare with Modbus values for consistency.

6) Closed‑loop test

  • With the heater connected and the probe placed in a water bath or fermentation vessel (safer: a bench test in a cup of water):
  • Start from ambient; set setpoint +2 C above ambient.
  • Observe temperature rising; expect overshoot if Kd is low or Ki is high.
  • Adjust Kp, Ki, Kd via Modbus in small increments:
    • Increase Kp for faster response (watch for oscillation).
    • Increase Kd to reduce overshoot.
    • Adjust Ki to remove steady‑state error (avoid integral windup).
  • Verify that at steady state, output_pct hovers near a value that compensates heat losses.

Troubleshooting

  • No temperature change or fault flagged:
  • Check MAX31865 wiring, power at 3.3V, and that your board is configured for the correct wire count (2/3/4‑wire).
  • Verify Rref value; if your board uses 400 Ω vs 430 Ω, adjust #define RREF.
  • Ensure CS pins: D9 for MAX31865 must be OUTPUT and default HIGH (the library manages this).
  • Ethernet not responding:
  • Ensure D4 (SD CS) is set to OUTPUT and HIGH (our setupEthernet() does this).
  • Check for IP conflicts; change the IP to a free address.
  • Verify cabling and link LEDs.
  • RS485 communication errors (timeouts, CRC):
  • Confirm DE/RE tied together to D2 and that your library is controlling direction (ModbusRTU handles this).
  • Check A/B polarity; swap if needed (A↔B reversed yields no comms).
  • Use 120 Ω terminators only at physical ends of the bus; add bias resistors if your modules don’t include them (typical ~680 Ω–1 kΩ pull‑up on A and pull‑down on B, or follow application note).
  • Match serial settings exactly: 19200, 8N1, no parity.
  • PID seems ineffective:
  • Verify controlEnabled (HR 8) is set to 1.
  • Confirm SSR output on D6 toggles within windows; a DMM won’t show duty—use an LED SSR indicator or scope.
  • Increase window size if your heater is large/inertial (e.g., 5–10 s) to reduce relay chatter and allow heat to integrate smoothly.
  • Temperature noisy or drifting:
  • Improve RTD cabling (twisted pair, shielded).
  • Use 3‑wire RTD to compensate lead resistance.
  • Add simple filtering (moving average) in software if needed (beware of added lag).
  • Library conflicts on SPI:
  • Ensure only one CS is LOW at a time. If adding more SPI devices, define unique CS pins and set them HIGH by default.

Improvements

  • Add cooling output: implement split‑range with two SSRs (heat and cool) and map output above 50% to heat and below 50% to cool, or run two PIDs with interlocks.
  • Auto‑tuning: integrate a relay auto‑tuner to estimate process gain/time constants, then compute PID parameters (e.g., Ziegler–Nichols or Tyreus–Luyben).
  • Modbus‑TCP server: serve the same registers on Ethernet (W5500) for SCADA systems that use TCP instead of RTU.
  • Data logging: publish CSV/JSON to an SD card or a remote InfluxDB via UDP/TCP; add timestamps via NTP.
  • Safety interlocks: add high‑temp cutout, sensor plausibility checks, and watchdog resets.
  • Calibration: implement a two‑point calibration for the RTD path to compensate systematic offset from wiring and Rref tolerances.
  • Multi‑vessel scaling: use the Mega’s resources to manage multiple MAX31865 channels and SSRs, each with its own PID loop and Modbus register block.

Final Checklist

  • Materials
  • Arduino Mega 2560, W5500 shield, MAX485, MAX31865, PT100, SSR, cables, 120 Ω terminator.
  • Wiring
  • SPI: W5500 via ICSP, MAX31865 on CS D9, SD CS D4 pulled HIGH.
  • RS485: TX1→DI, RX1→RO, DE/RE→D2, A/B wired with termination.
  • SSR: D6→SSR(+), GND→SSR(−); mains side wired safely.
  • Firmware
  • Code placed at ~/projects/fermenter-pid-rs485/fermenter-pid-rs485.ino.
  • Libraries installed: Ethernet@2.0.2, Adafruit MAX31865 library@1.4.5, PID@1.2.1, ModbusRTU@2.3.8.
  • Built and uploaded with:
    • arduino-cli core install arduino:avr
    • arduino-cli compile --fqbn arduino:avr:mega
    • arduino-cli upload -p <PORT> --fqbn arduino:avr:mega
  • Validation
  • HTTP: curl http://192.168.1.50/status returns JSON with plausible temps.
  • RS485: mbpoll reads/writes HREGs; setpoint/tunings take effect.
  • PID/SSR: output windowing toggles D6; heater responds in process.
  • Faults: MAX31865 fault bit non‑zero when RTD is disconnected; returns to zero when restored.
  • Tuning
  • Set window_ms to match actuator; tune Kp/Ki/Kd for minimal overshoot and steady control.
  • Documentation
  • Record your Modbus map, IP settings, and PID tunings for future reproducibility.

With this build, you have a field‑ready fermentation controller that speaks Modbus‑RTU over RS485 for integration into supervisory systems and provides a convenient Ethernet status endpoint for quick checks and dashboards.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the primary purpose of the project described in the article?




Question 2: Which microcontroller is used in the project?




Question 3: What type of sensor is utilized for temperature measurement?




Question 4: What communication protocol is employed for supervision/control?




Question 5: Which tool is mentioned for HTTP validation?




Question 6: What is the function of the MAX485 component in the project?




Question 7: What is a prerequisite for this project regarding PID control?




Question 8: What type of connection does the W5500 Ethernet Shield provide?




Question 9: Which version of Arduino CLI is recommended for this project?




Question 10: What safety precaution is mentioned for driving a mains heater?




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

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:
error: Contenido Protegido / Content is protected !!
Scroll to Top