Grill Temp Control

I am trying to add some temperature control to my grill (but also fits on the stove top for oil temp for instance). I am using an ESP32 DEV board with a break out board. The movement is a couple of 28BYJ-48 5V stepper motors driven by two ULN2003 driver boards. A MAX6675 Module + K Type Thermocouple is used for temperature readings. I started with one stepper on ESP pins 13-12-14-27. It worked perfectly but I needed to add a second burner to reach my desired set point (at least in winter). I added the second motor on pins 32-33-25-26 but it doesn't move. The motor #1 ULN2003 board all leds light up for every movement, which is multiple steps. Motor #2 ULN2003 has only two leds illuminate alternately. I've swapped motors, driver boards, breakout boards, ESP32 boards, and power supplies with the same results. I am left with either an issue with the code that I can't figure out or some choice of pin issue that I can't find. Any help would be appreciated.

#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <Stepper.h>
#include <max6675.h>
#include <PID_v1.h>
#include <vector> // Include the vector library

// Access Point credentials
const char* ssid = "ESP32_Grill_Control";
const char* password = "12345678";

// Network Config (Optional static IP setup)
IPAddress local_IP(192, 168, 1, 1);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);

// Create AsyncWebServer instance
AsyncWebServer server(80);

// MAX6675 pins
#define SCK 18 // Clock pin
#define CS 5 // Chip select pin
#define MISO 19 // Master in, slave out pin

// Stepper motor pins and settings
#define IN1 13 // Motor input pin 1
#define IN2 12 // Motor input pin 2
#define IN3 14 // Motor input pin 3
#define IN4 27 // Motor input pin 4
#define IN1_B 32  // Second stepper input pin 1
#define IN2_B 33  // Second stepper input pin 2
#define IN3_B 25  // Second stepper input pin 3
#define IN4_B 26  // Second stepper input pin 4
const int stepsPerRevolution = 2048; // Number of steps per revolution for the stepper
Stepper myStepper(stepsPerRevolution, IN1, IN3, IN2, IN4);
Stepper myStepperB(stepsPerRevolution, IN1_B, IN3_B, IN2_B, IN4_B);

// PID variables
double Setpoint, Input, Output; // Setpoint, input (current temperature), and output (control signal)
double Kp = 2.0, Ki = 5.0, Kd = 1.0; // Initial PID constants
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT); // Create PID controller instance

// Thermocouple and stepper variables
MAX6675 thermocouple(SCK, CS, MISO); // Thermocouple sensor instance
int maxSteps = 4606; // Maximum stepper position (steps)
int stepperPosition = 0; // Current stepper position in steps

// Create a vector to store temperature log
std::vector<String> tempLog;

// HTML for the web page
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML>
<html>
<head>
  <title>ESP32 PID Controller</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <h2>PID-Controlled Stepper Motor</h2>
  <!-- Form to set the temperature -->
  <form id="setpointForm">
    <label for="temp">Set Temperature (&deg;F): </label>
    <input type="number" id="temp" name="temp">
    <input type="submit" value="Set">
  </form>
  <!-- Form to update PID constants -->
  <form id="pidForm">
    <div>
      <label for="kp">Kp: </label>
      <input type="number" step="0.1" id="kp" name="kp" value="2.0">
    </div>
    <div>
      <label for="ki">Ki: </label>
      <input type="number" step="0.1" id="ki" name="ki" value="5.0">
    </div>
    <div>
      <label for="kd">Kd: </label>
      <input type="number" step="0.1" id="kd" name="kd" value="1.0">
    </div>
    <input type="submit" value="Update PID">
  </form>
  <p id="message"></p>
  <p>Current Temperature: <span id="currentTemp">--</span>&deg;F</p>
  <p>Stepper Position: <span id="stepPosition">--</span>%</p>
  <p><a href="/download">Download Temperature Log</a></p> <!-- Link to download the log -->
  <!-- JavaScript to handle form submissions and periodic updates -->
  <script>
    document.getElementById('setpointForm').addEventListener('submit', function(event) {
      event.preventDefault();
      const temp = document.getElementById('temp').value;
      fetch(`/setpoint?temp=${temp}`)
        .then(response => response.text())
        .then(data => {
          document.getElementById('message').innerText = data;
        });
    });

    document.getElementById('pidForm').addEventListener('submit', function(event) {
      event.preventDefault();
      const kp = document.getElementById('kp').value;
      const ki = document.getElementById('ki').value;
      const kd = document.getElementById('kd').value;
      fetch(`/pid?kp=${kp}&ki=${ki}&kd=${kd}`)
        .then(response => response.text())
        .then(data => {
          document.getElementById('message').innerText = data;
        });
    });

    setInterval(() => {
      fetch('/status')
        .then(response => response.json())
        .then(data => {
          document.getElementById('currentTemp').innerText = data.temperature;
          document.getElementById('stepPosition').innerText = data.stepperPosition;
        });
    }, 1000);
  </script>
</body>
</html>
)rawliteral";

// Function to set stepper pins to high impedance mode (reduce power consumption)
void setPinsToHighImpedance() {
  pinMode(IN1, INPUT); // Set pin 1 (13) to high impedance
  pinMode(IN2, INPUT); // Set pin 2 (12) to high impedance
  pinMode(IN3, INPUT); // Set pin 3 (14) to high impedance
  pinMode(IN4, INPUT); // Set pin 4 (27) to high impedance
  pinMode(IN1_B, INPUT); // Set pin 1_B (32) to high impedance
  pinMode(IN2_B, INPUT); // Set pin 2_B (33) to high impedance
  pinMode(IN3_B, INPUT); // Set pin 3_B (25) to high impedance
  pinMode(IN4_B, INPUT); // Set pin 4_B (26) to high impedance
}

// Function to update stepper position based on PID output
void updateStepperPosition() {
  int targetSteps = map(constrain(Output, 0, 100), 0, 100, 0, maxSteps); // Map PID output to stepper range
  int stepsToMove = targetSteps - stepperPosition; // Calculate steps to move

  if (stepsToMove != 0) {
    // Activate stepper motor by setting pins to output for motor 1
    pinMode(IN1, OUTPUT);
    pinMode(IN2, OUTPUT);
    pinMode(IN3, OUTPUT);
    pinMode(IN4, OUTPUT);
    // Activate stepper motor by setting pins to output for motor 2
    pinMode(IN1_B, OUTPUT);
    pinMode(IN2_B, OUTPUT);
    pinMode(IN3_B, OUTPUT);
    pinMode(IN4_B, OUTPUT);  

    myStepper.step(stepsToMove); // Move stepper 1 by the required steps
    myStepperB.step(stepsToMove); // Move stepper 1 by the required steps
    stepperPosition += stepsToMove; // Update stepper position


    // Set both steppers back to high impedance
    setPinsToHighImpedance();
  }
}

// Handle temperature setpoint update
void handleSetpoint(AsyncWebServerRequest *request) {
  if (request->hasParam("temp")) {
    // Convert Fahrenheit input to Celsius for the PID
    double tempF = request->getParam("temp")->value().toDouble();
    Setpoint = (tempF - 32) * 5.0 / 9.0; // Convert Fahrenheit to Celsius
    request->send(200, "text/plain", "Setpoint updated to " + String(tempF) + "F");
  } else {
    request->send(400, "text/plain", "Invalid request");
  }
}

// Handle PID constant update
void handlePID(AsyncWebServerRequest *request) {
  if (request->hasParam("kp") && request->hasParam("ki") && request->hasParam("kd")) {
    // Retrieve PID constants from the request
    Kp = request->getParam("kp")->value().toDouble();
    Ki = request->getParam("ki")->value().toDouble();
    Kd = request->getParam("kd")->value().toDouble();
    myPID.SetTunings(Kp, Ki, Kd); // Update PID constants
    request->send(200, "text/plain", "PID constants updated: Kp=" + String(Kp) + ", Ki=" + String(Ki) + ", Kd=" + String(Kd));
  } else {
    request->send(400, "text/plain", "Invalid request");
  }
}

// Handle status request
void handleStatus(AsyncWebServerRequest *request) {
  // Convert temperature to Fahrenheit for display
  double tempF = Input * 9.0 / 5.0 + 32.0;
  // Send temperature and stepper position as JSON
  String json = "{\"temperature\":" + String(tempF) + ",\"stepperPosition\":" + String((stepperPosition * 100) / maxSteps) + "}";
  request->send(200, "application/json", json);
}

// Handle log download request
void handleDownload(AsyncWebServerRequest *request) {
  String logContent;
  for (const auto& entry : tempLog) {
    logContent += entry + "\n";
  }
  request->send(200, "text/plain", logContent);
}

void setup() {
  Serial.begin(115200); // Initialize serial communication

  // Initialize thermocouple
  Input = thermocouple.readCelsius();

  // Initialize PID
  myPID.SetMode(AUTOMATIC); // Set PID mode to automatic
  myPID.SetOutputLimits(0, 100); // Limit PID output to 0-100%

  // Initialize stepper
  myStepper.setSpeed(10); // Set stepper speed
  setPinsToHighImpedance(); // Reduce power consumption by setting pins to high impedance

  // Set up Access Point mode
  WiFi.softAPConfig(local_IP, gateway, subnet); // Optional: Configure static IP
  WiFi.softAP(ssid, password);

  // Setup server routes
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
    request->send_P(200, "text/html", index_html); // Serve the main HTML page
  });
  server.on("/setpoint", HTTP_GET, handleSetpoint); // Handle temperature setpoint update
  server.on("/pid", HTTP_GET, handlePID); // Handle PID constant update
  server.on("/status", HTTP_GET, handleStatus); // Handle status requests
  server.on("/download", HTTP_GET, handleDownload); // Handle log download requests

  // Start server
  server.begin();
}

unsigned long lastLogTime = 0; // Variable to store the last log time

void loop() {
  // Read the current temperature from the thermocouple
  Input = thermocouple.readCelsius();
  if (isnan(Input)) {
    Serial.println("Failed to read temperature"); // Log error if temperature read fails
    return;
  }

  // Log the current temperature in Fahrenheit every 10 seconds
  if (millis() - lastLogTime >= 10000) { // Check if 10 seconds have passed
    double tempF = Input * 9.0 / 5.0 + 32.0;
    String logEntry = String(millis() / 1000) + "," + String(tempF);
    tempLog.push_back(logEntry);
    lastLogTime = millis(); // Update the last log time
  }

  // Compute the PID output
  myPID.Compute();

  // Update the stepper motor position
  updateStepperPosition();

  // Add debugging output (optional)
  Serial.print("Input Temp (C): ");
  Serial.print(Input);
  Serial.print(" | Setpoint (C): ");
  Serial.print(Setpoint);
  Serial.print(" | PID Output: ");
  Serial.print(Output);
  Serial.print(" | Stepper Position: ");
  Serial.println(stepperPosition);

  // Delay for stability
  delay(2000); // 2-second delay
}

Posting schematics is worth trying.

2 Likes

I put this together in Cirkit.

Please post real schematiks. Much is missing in that Fritzing, f ex powering.

1 Like

Perhaps it's time to begin debugging using the serial.Print() to show messages that you add to the code to indicate the actual steps your logic is taking.

1 Like

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.