DIY Filamentextruder – Teil 2: Temperaturregelung

Ich versuche mir einen eigenen Filamentextruder zu bauen. Es gibt ja schon einige tolle Projekte auf Instructables und dergleichen. Den besten, den ich bisher finden konnte, könnt ihr auf Thingiverse ansehen. Es geht um den Lyman Extruder V5 mit Ramps-Steuerung und Firmware von Mulier.

Doch das Projekt hat für mich eine Schwachstelle: Den Filamentdurchmesser durch Differenz Geschwindigkeit einzustellen, halte ich für eine sehr gute Idee, jedoch ist der Sensor für die meisten Maker suboptimal, da hier platinen gebraucht werden, die es nicht zu kaufen gibt. Daher versuche ich das ganze über einen gehackten digitalen Messschieber umzusetzen. Das ist recht einfach und die gibt es günstig im Netz.

Außerdem versuche ich eine Firmware auf ESP32 Basis zu schreiben, damit man den Filamentextruder, ähnlich wie mit Octoprint, direkt vom Rechner, Handy oder Tablet steuern kann.

Letztendlich muss der Extruder soweit automatisiert werden, dass kein Eingreifen erforderlich ist. Man drückt auf Start und die Logik soll den Prozess steuern. Ich werde nach und nach weitere Teile ergänzen.

Teil 2: Temperaturregelung

Wir machen direkt da weiter wo wir in Teil 1 aufgehört haben. Daher ist die Teileliste identisch.

Teileliste

  • 1x 18mm Spiralbohrer
  • 1x 1/2″ Rohr (gleiche Länge etwa wie der Bohrer)
  • 2x 1/2″ Bodenflansch
  • 1x 1/2″ Gewindekappe
  • 4x M10 x 150 Maschinenschrauben
  • 4x M10 Muttern (zb. Selbstsichernd)
  • 1x Thermalbarriere aus Holz
  • 1x Axialkugellager
  • 1x 12V DC Getriebemotor
  • 1x L298N Motortreiber
  • 1x 3D-Druck Kupplung
  • 1x Heizband 230V / 25×25 mm
  • 1x SSR 25A / 3-32V DC / 24-380V AC
  • 1x Max6675 SPI Wandler
  • 1x K-Type Thermistor WRNT01
  • 1x Schaltnetzteil 12V / 30A

Wenn wir die Schnecke aus Teil 1 aufgebaut haben, der Motor soweit läuft und die Schnecke sich gut im Rohr dreht, können wir nun zur Temperaturregelung übergehen.

Heizelement

Als Heizelement nutze ich ein 230V Heizband mit 25mm Durchmesser, wie dieses <hier/>. Über ein Solid State Relais schalte ich den Strom. Hier nutze ich dieses <hier/> von Reichelt. Wichtig ist, dass es induktive Lasten verträgt. Zuvor hatte ich eines von Amazon, welches nicht funktionierte. Das liegt an der Funktionsweise von SSRs. Es gibt verschiedene Nutzungstypen, sogenannte Gebrauchskategorien. Diese muss zur Art des Relais passen. Für die unterschiedlichen Kategorien sind angaben zur maximalen Spannung und Stromstärke auf dem Relais angegeben.

Unsere Heizmanschette hat 360W, das ergibt ca. 1,5A bei 230V, also sollte die maximale Last von 5A nicht erreicht werden.

Temperaturmessung

Für die Temperaturmessung nutze ich einen NTC – Thermistor mit Spannungsteiler. Dazu nehme ich einen 10k Ohm Referenzwiderstand. Mein Temperaturfühler kann 0 bis 600°C messen, das ist für unsere Zwecke mehr als genug.

Alternativ funktioniert ein Set aus Temperaturfühler (K-Typ Thermoelement) und Wandler Platine wie dieses <hier/>. So erhalten wir ein SPI Signal für den ESP32. Das gute am MAX6675 (der Wandler) ist, dass sie die Platinen Temperatur mit einbezieht und dadurch den korrekten Temperaturwert ausgibt.

Nozzle

Für die Nozzle nutze ich eine 1/2 Zoll Rohrkappe in die ich ein 4mm Loch gebohrt habe. Zwischen Rohr und Kappe ist ein 1/2″ Fitting aus Rotguss. Auf diesem sitzt das Heizelement und der Temperaturfühler. Letzterer wird zuerst mit CaptonTape aufgeklebt, danach kommt das Heizelement darüber.

Netzteil / Spannungsversorgung

Da ich 230V, 12V und 5V brauche, habe ich ein Schaltnetzteil besorgt. Hier nutze ich dieses <hier/>. Dort greife ich direkt am Eingang die 230V für das Heizelement ab, 12V für den DC Motor und habe einen DC-Stepdown für die 5V für den ESP32.

VORSICHT BEIM ARBEITEN AN 230V- Zieht immer den Stecker bevor ihr was macht!!!

Aufbau

Zunächst müssen wir alles korrekt verkabeln. Fangen wir also einfach erstmal mit der Temperaturmessung an. Als Hilfestellung habe ich den folgenden Schaltplan für euch erstellt:

Code

Hier ist der Code um den NTC-Thermistor auszulesen:

#define DEBUGGING                     // debugging needed?

#ifdef DEBUGGING
#define DEBUG_B(...) Serial.begin(__VA_ARGS__)
#define DEBUG_P(...) Serial.println(__VA_ARGS__)
#define DEBUG_F(...) Serial.printf(__VA_ARGS__)
#else
#define DEBUG_B(...)
#define DEBUG_P(...)
#define DEBUG_F(...)
#endif

#define NUMSAMPLES 10
#define THERMISTOR_PIN 34

double adcMax, Vs;

double R1 = 10000.0;   // voltage divider resistor value
double Beta = 3950.0;  // Beta value
double To = 298.15;    // Temperature in Kelvin for 25 degree Celsius
double Ro = 10000.0;   // Resistance of Thermistor at 25 degree Celsius

int samples[NUMSAMPLES];

//--- Timer ---
unsigned long previousMillisMes; //alte Zeit
const unsigned long messIntervall = 2000;

void setupNTC(){
  DEBUG_P("Function: setupNTC started...");
    //  analogReference(EXTERNAL);

  adcMax = 4095.0; // ADC resolution 12-bit (0-4095)
  Vs = 3.3;        // supply voltage
  pinMode(THERMISTOR_PIN, INPUT);
}

String readNTC(){
  DEBUG_P("Function: readNTC started...");

  //Working Vars in local Space
  float Vout, Rt = 0;
  float T, Tc, averageTc, Tf = 0;
  uint8_t i;
  float average;

  //Measure a few times to calculate average
  for (i = 0; i < NUMSAMPLES; i++) {
    samples[i] = analogRead(THERMISTOR_PIN);
    delay(10);
  }
  
  //calculate average
  average = 0;
  for (i = 0; i < NUMSAMPLES; i++) {
    average += samples[i];
  }
  average /= NUMSAMPLES;
  DEBUG_F("Average analog reading: ");
  DEBUG_P(String(average));

  average = adcMax / average;
  average = R1 / average;
  DEBUG_F("Thermistor resistance: ");
  DEBUG_P(String(average));
  
  //This is the Math to Calculate the correct Temperature
  float steinhart;
  steinhart = average / Ro;     // (R/Ro)
  steinhart = log(steinhart);                  // ln(R/Ro)
  steinhart /= Beta;                   // 1/B * ln(R/Ro)
  steinhart += 1.0 / (To); // + (1/To)
  steinhart = 1.0 / steinhart;                 // Invert
  steinhart -= 273.15;                         // convert absolute temp to C

  DEBUG_F("Calculated Temperature: ");
  DEBUG_P(String(steinhart));
  DEBUG_P(" *C");
  //delay(2000);
  
  return String(steinhart);
}

void setup() {
  // Serial port for debugging purposes
  DEBUG_B(115200);
  

  //Initialise NTC Stuff (See NTC Tab)
  setupNTC();
  
}

//--- LOOP ---
void loop() {

  //we want to measure the current nozzle Temperature and Diameter in a certain interval -> this is only for the Charts, for control we measure more often.
  if (millis() - previousMillisMes >= messIntervall) {
    previousMillisMes = millis();
    DEBUG_P(readNTC());
  }
}

Wundert euch nicht, der ist aus dem finalen Hauptprogramm ein Auszug. Hier habe ich den Webserver-Part weggelassen. Nun sollte etwa alle zwei Sekunden ein neuer Messwert (inklusive Zwischenschritte) auf dem Seriellen Monitor ausgegeben werden.

Wenn der Wert -273,15 °C beträgt, dann ist der Sensor nicht korrekt angeschlossen. Bei mir wird bei 16°C Raumtemperatur (ich arbeite im Keller) ein Wert von 41°C angezeigt. Also habe ich eine Korrektur um 25°C eingebaut.

Step by Step

Zunächst erstellen wir uns einen Debugging Modus. Wenn der part mit

#define DEBUGGING

auskommentiert ist, wird der #ifdef Teil nicht mit den Serial.print Befehlen verknüpft. So können wir am Ende den Seriellen Monitor aus unserem Programm ausklammern.

Timer

//--- Timer ---
unsigned long previousMillisMes; //alte Zeit
const unsigned long messIntervall = 2000;

Damit wir nicht mit Delay arbeiten müssen, nutzen wir die millis() Funktion und zwei Timer Variablen. Die millis() funktion gibt die aktuelle Laufzeit aus, diese speichern wir in eine Variable. Das nächstmal wenn wir unseren Messintervall erreichen, wird der Code erneut ausgeführt, aber wir können in der Zwischenzeit andere Dinge erledigen.

Defines

#define NUMSAMPLES 10
#define THERMISTOR_PIN 34

Defines sind eine Platzsparende Methode konstanten im Programm zu hinterlegen. Da dies eine reine Compiler-Anweisung ist. Im Prinzip ist es ein Suchen und Ersetzen Befehl. Überall wo NUMSAMPLES auftaucht, wird eine 10 gesetzt. Wir müssen also keinen Platz auf dem Microcontroller dafür verschwenden.

NUMSAMPLES steht dabei für 10 Messwerte, aus denen wir einen durchschnittlichen Messwert bilden. So können wir Schwankungen minimieren. Ihr könnt ja mal testen wie sehr euer Messwert schwankt, wenn ihr direkt messt.

THERMISTOR_PIN 34 ist offensichtlich der Pin an dem wir messen.

Variablen

double adcMax, Vs;

double R1 = 10000.0;   // voltage divider resistor value
double Beta = 3950.0;  // Beta value
double To = 298.15;    // Temperature in Kelvin for 25 degree Celsius
double Ro = 10000.0;   // Resistance of Thermistor at 25 degree Celsius

int samples[NUMSAMPLES];

adcMax und Vs sind Variablen mit denen wir später vom gemessen Wert auf die Temperatur umrechnen. ADC steht für Analog Digital Converter. Da der Wert Vs zwischen 0 und 3,3V (beim ESP) liegt und digitalen Stufen von 0 bis 4095 zugeordnet wird, müssen wir unser Max festlegen. Bei anderen Arduino-Platinen ist das maximum 5V. Oder auch nur 1024 Stufen. V Analog-Stufen Digital. -> ADC

R1 ist der Referenzwiderstand im Spannungsteiler, siehe Schaltbild oben.

Beta entspricht der Materialkonstante. Da der Widerstand sich pro Energieeinehit ändert und der Startpunkt bei Ro/To liegt, brauchen wir Beta um von hier die aktuelle Temperatur berechnen zu können.

To entspricht der Nenntemperatur in Kelvin

Ro ist der Nennwiderstand des NTC-Thermistors bei 25°C Raumtemperatur (To).

setupNTC

void setupNTC(){
  DEBUG_P("Function: setupNTC started...");

  adcMax = 4095.0; // ADC resolution 12-bit (0-4095)
  Vs = 3.3;        // supply voltage
  pinMode(THERMISTOR_PIN, INPUT);
}

Hier legen wir die Werte für ADC und VS tatsächlich fest. Das müsst ihr ggf. an euer Board anpassen. Danach sagen wir dem Arduino, dass wir am Pin lesen möchten.

readNTC

String readNTC(){
  DEBUG_P("Function: readNTC started...");

  //Working Vars in local Space
  float Vout, Rt = 0;
  float T, Tc, averageTc, Tf = 0;
  uint8_t i;
  float average;

Diese Funktion ist hier als String angelegt, da wir später Websockets und JSON Strings nutzen. Ein Typ float würde auch funktionieren, aber ich war zu faul den Code zu ändern.

Dann haben wir ein paar Arbeitsvariablen im lokalen Speicher. Wir unterscheiden ja lokale und globale Variablen. Globale sind jederzeit vorhanden, lokale Variablen werden nur für die Laufzeit der Unterfunktion (hier readNTC) erzeugt.

Messen

//Measure a few times to calculate average
  for (i = 0; i < NUMSAMPLES; i++) {
    samples[i] = analogRead(THERMISTOR_PIN);
    delay(10);
  }
  
  //calculate average
  average = 0;
  for (i = 0; i < NUMSAMPLES; i++) {
    average += samples[i];
  }
  average /= NUMSAMPLES;
  DEBUG_F("Average analog reading: ");
  DEBUG_P(String(average));

Wir nutzen eine for-Schleife um (hier 10) Messwerte zu sammeln Dazwischen warten wir immer 10 Millisekunden mit dem delay Befehl. Diese Werte speichern wir in ein Array. Danach summieren wir alle in einer weiteren for Schleife auf und Teilen durch die Anzahl der Werte um einen einfachen Durchschnitt zu berechnen. Wenn wir nur mit analogRead() messen würden, würde der Messwert stark schwanken.

average = adcMax / average;
  average = R1 / average;
  DEBUG_F("Thermistor resistance: ");
  DEBUG_P(String(average));

Dann können wir den durchschnittlichen Messwert in einen Widerstand umrechnen, da wir R1 am Spannungsteiler und die Auflösung des ADC kennen.

//This is the Math to Calculate the correct Temperature
  float steinhart;
  steinhart = average / Ro;     // (R/Ro)
  steinhart = log(steinhart);                  // ln(R/Ro)
  steinhart /= Beta;                   // 1/B * ln(R/Ro)
  steinhart += 1.0 / (To); // + (1/To)
  steinhart = 1.0 / steinhart;                 // Invert
  steinhart -= 273.15;                         // convert absolute temp to C

  DEBUG_F("Calculated Temperature: ");
  DEBUG_P(String(steinhart));
  DEBUG_P(" *C");

Die Methode zur Berechnung der wirklichen Temperatur wird nach der Steinhart-Hart-Methode durchgeführt. Lest beim verlinkten Artikel nach, sollte das unklar sein.

return String(steinhart);
}

Hier geben wir den fertigen Messwert am Ende der Funktion zurück.

Heizen

Wenn wir die Heizmanschette direkt an 230V hängen würden, würde diese immer heißer werden, bis sie Durchbrennt. Das ist auch für unseren Filamentextruder ungünstig, da wir eine stabile Temperatur im Schmelzbereich des Kunststoffes brauchen. Wir müssen also die echte Temperatur kontinuierlich mit der gewünschten Temperatur vergleichen und auf Basis davon, das Heizelement an und aus schalten.

Dazu müssen wir aber erstmal wieder alles anschließen. Hier ist der Schalptlan:

Das Solid State Relais wird an Pin 23 und GND angeschlossen. Achtet hier unbedingt auf korrekte Polung und die Beschriftung auf eurem Relais. Das SSR hat einen Spannungsbereich als Eingang – wir brauchen weniger als 3V, damit es unser High Signal erkennen kann. Auf der Ausgangsseite ist es egal, an welche Klemme ihr das Heizelement hängt. Wichtig ist nur, immer Stromfrei zu arbeiten, wenn ihr am SSR arbeitet. Und es ist besser die L-Ader zu schalten als die N-Ader, da so keine Spannung am Heizelement anliegt, wenn das SSR aus ist.

PID Regelung

Um den Sollwert zu Regeln empfiehlt sich ein PID Regler. Dazu gibt es für den Arduino eine schöne Bibliothek, direkt im Playground. Diese installieren wir über die Bibliothekverwaltung in der Arduino IDE. Wir suchen dort nach PID und installieren PID von Brett Beauregard. Falls ihr eine andere nehmen möchtet geht das natürlich auch.

Dann können wir den Sketch schreiben. Der folgende Auszug ist nur der Tab für den PID Regler. Ich empfehle für die Firmware eine Untergliederung in Tabs. So bleibt alles schön übersichtlicht.

Wir erstellen also eine setupHeater funktion. Diese rufen wir dann einfach in der void setup auf.

//Include the PID LIB
#include <PID_v1.h>

//Define some Pins
#define HEATER_PIN 23

//this is the tolerance in between we want to be to give the heater okay signal and to unlock auger control
int tempToleranceMax = 5;
int tempToleranceMin = 5;

float lastErrorTime;
float errorTimeFrame = 30000; // for checking if we are in tolerance for atleast 30 seconds

//Define Vars for the PID controller
double Setpoint, Input, Output;

double Kp = 2.0;
double Ki = 0.2;
double Kd = 0.0;

unsigned long WindowSize = 5000; //this is our Control-Window in MS

PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT); //PID Controller constructor

//--- Function to connect Pins and stuff
void setupHeater() {
  DEBUG_P("Function: setupHeater...");
  //Defining the Outputs
  pinMode(HEATER_PIN, OUTPUT);
  digitalWrite(HEATER_PIN, LOW);

  //initialise the PID
  Setpoint = nozzleTempSP + nozzleTempOffsetSP; //calculate the real setpoint from UI
  DEBUG_P("Calculated Setpoint...");
  myPID.SetOutputLimits(0, WindowSize);         //this makes the pid lib recalculate the output value to our timeframe -> basically a chart mapping
  //turn the PID on
  myPID.SetMode(AUTOMATIC);                     //there are some other mode but no clue of that.
  DEBUG_P("Initialised PID...");
}

//starts the preheat procedure
void preheat() {                          //after the user startet this we fire up the heater
  DEBUG_P("Function: preheat");
  heaterOn = true; //if this is true, in the main arduino loop the function heatNozzle will be called every loop -> this is the next step in program logic
}

void cooldown() {
  DEBUG_P("Function: cooldown");
  heaterOn = false; //we just set this to false to exclude the heatNozzle from the loop again
  digitalWrite(HEATER_PIN, LOW);  //and this is just a safety call to make sure heater is low
}

void heatNozzle() { //if heaterOn Boolean is true, this function is called in main loop on every loop -> this controls the nozzle temperature
  DEBUG_P("Function: heatNozzle");

  if (millis() % 1000 == 0) { //We do this part every second to get the current Temperature -> mostly for debugging
    unsigned long zeit = (millis() / 1000);
    Input = readNTC().toInt();
    DEBUG_P(String(zeit));
    DEBUG_F("; ");
    DEBUG_P(String(Input));
    DEBUG_F("; ");
    DEBUG_P(String(Output));
    DEBUG_F(" ms");
  }
  // Input = readNTC(); //uncomment if you need every loop sensor reading
  myPID.Compute(); //now we let the PID LIB do its magic
  sps(Output);    //and call the controller routine to actually turn on off the heater in a time window manner
}

/************************************************
   turn the output pin on/off based on pid output
 ************************************************/

void sps(double hz) {
  //https://de.wikipedia.org/wiki/Schwingungspaketsteuerung

  static unsigned long szl;
  static unsigned long szh;               //the first low time is the window time minus the output from the pid
  unsigned long lzl = WindowSize - hz;
  unsigned long lzh = hz;

  if (!digitalRead(HEATER_PIN) && millis() - szl >= lzl ) { //if heaterpin is low and the current time - start time low is bigger than last time low
    szh = millis();                                         //start time high is current time
    digitalWrite(HEATER_PIN, HIGH);                         //heater on!
  }
  if (digitalRead(HEATER_PIN) && millis() - szh >= lzh ) { // if heaterpin is on and the current time minus start time high is bigger than last time low
    szl = millis();
    digitalWrite(HEATER_PIN, LOW);                        //heater on
  }
}
//nice, now we have our temperature controlled by the PID which is controlling our SSR to turn on / off the heater.
//next, when we are close enough to setpoint and did not exit the tolerance for a decent amount of time, we can set a bool to a okay state

Damit wir ein wenig steuern können was passiert, sollten wir preheat und cooldown gezielt ansteuern können. Dazu können wir zunächst den seriellen Monitor nutzen. Wir müssen also den Haupt-Tab anpassen und können dabei gleich die Temperaturmessung in einen eigenen Tab auslagern.

Haupttab

#define DEBUGGING                     // debugging needed?

#ifdef DEBUGGING
#define DEBUG_B(...) Serial.begin(__VA_ARGS__)
#define DEBUG_P(...) Serial.println(__VA_ARGS__)
#define DEBUG_F(...) Serial.printf(__VA_ARGS__)
#else
#define DEBUG_B(...)
#define DEBUG_P(...)
#define DEBUG_F(...)
#endif

//--- Timer ---
unsigned long previousMillisMes; //alte Zeit
const unsigned long messIntervall = 2000;

//some working variables
int nozzleTempSP = 100;
int nozzleTempOffsetSP = 0;

//status
bool heaterOn = false; // when the heater is turned on

char rx_byte = 0;

void setup() {
  // Serial port for debugging purposes
  DEBUG_B(115200);
  

  //Initialise NTC Stuff (See NTC Tab)
  setupNTC();

  //Initialise PID
  setupHeater();
  
}

//--- LOOP ---
void loop() {
  if (Serial.available() > 0) {    // is a character available?
    rx_byte = Serial.read();       // get the character
  
    // check if a number was received
    if ((rx_byte >= '0') && (rx_byte <= '9')) {
      Serial.print("Number received: ");
      Serial.println(rx_byte);
      if (rx_byte == '1'){
        preheat();
      }
      if (rx_byte == '2'){
        cooldown();
      }
    }
    else {
      Serial.println("Not a number.");
    }
  } // end:

  //we want to measure the current nozzle Temperature and Diameter in a certain interval -> this is only for the Charts, for control we measure more often.
  if (millis() - previousMillisMes >= messIntervall) {
    previousMillisMes = millis();
    DEBUG_P(readNTC());
  }

  if (heaterOn) {
    heatNozzle();
  }
}

NTC Tab

#define NUMSAMPLES 10
#define THERMISTOR_PIN 34

double adcMax, Vs;

double R1 = 10000.0;   // voltage divider resistor value
double Beta = 3950.0;  // Beta value
double To = 298.15;    // Temperature in Kelvin for 25 degree Celsius
double Ro = 10000.0;   // Resistance of Thermistor at 25 degree Celsius

int samples[NUMSAMPLES];

void setupNTC(){
  DEBUG_P("Function: setupNTC started...");
    //  analogReference(EXTERNAL);

  adcMax = 4095.0; // ADC resolution 12-bit (0-4095)
  Vs = 3.3;        // supply voltage
  pinMode(THERMISTOR_PIN, INPUT);
}

String readNTC(){
  DEBUG_P("Function: readNTC started...");

  //Working Vars in local Space
  float Vout, Rt = 0;
  float T, Tc, averageTc, Tf = 0;
  uint8_t i;
  float average;

  //Measure a few times to calculate average
  for (i = 0; i < NUMSAMPLES; i++) {
    samples[i] = analogRead(THERMISTOR_PIN);
    delay(10);
  }
  
  //calculate average
  average = 0;
  for (i = 0; i < NUMSAMPLES; i++) {
    average += samples[i];
  }
  average /= NUMSAMPLES;
  DEBUG_F("Average analog reading: ");
  DEBUG_P(String(average));

  average = adcMax / average;
  average = R1 / average;
  DEBUG_F("Thermistor resistance: ");
  DEBUG_P(String(average));
  
  //This is the Math to Calculate the correct Temperature
  float steinhart;
  steinhart = average / Ro;     // (R/Ro)
  steinhart = log(steinhart);                  // ln(R/Ro)
  steinhart /= Beta;                   // 1/B * ln(R/Ro)
  steinhart += 1.0 / (To); // + (1/To)
  steinhart = 1.0 / steinhart;                 // Invert
  steinhart -= 273.15;                         // convert absolute temp to C
  steinhart -= 25;

  DEBUG_F("Calculated Temperature: ");
  DEBUG_P(String(steinhart));
  DEBUG_P(" *C");
  //delay(2000);
  
  return String(steinhart);
}

Step by Step – Main Tab

Als erstes haben wir wieder die defines und includes. Ich setze die Includes für die jeweiligen Funktionen gerne in den entsprechenden Tab. Man kann aber auch alle im Haupt-Tab an den Anfang setzen. Variablen, die wir global benötigen, sollten immer im Haupt-Tab stehen, da diese sonst ggf. zu spät deklariert werden. Hinzugekommen sind:

Setpoints

//some working variables
int nozzleTempSP = 100;
int nozzleTempOffsetSP = 0;

Natürlich brauchen wir eine Zieltemperatur. Diese müssen wir später, je nach Materialtyp festlegen. Sollte man eine kleine Abweichung brauchen, kommt die Offset Variable ins Spiel. Jetzt ist hier einfach 100°C festgelegt, da wir erstmal nur Testen möchten.

Status

//status
bool heaterOn = false; // when the heater is turned on

Diese nutzen wir um später die Zustände der Programmlogik zu kontrollieren. Nur wenn die Düse heiß ist, darf die Schnecke sich drehen, etc. Hier nutzen wir die Variable heaterOn um im loop den Code für die PID Regelung auszuführen. So können wir leicht An und Aus schalten. Dazu gleich mehr beim entsprechenden Code-Abschnitt.

char rx_byte = 0;

In diese Variable speichern wir unsere Eingabe im Seriellen Monitor, um das Heizen An und Aus zu schalten.

setup

void setup() {
  // Serial port for debugging purposes
  DEBUG_B(115200);
  

  //Initialise NTC Stuff (See NTC Tab)
  setupNTC();

  //Initialise PID
  setupHeater();
  
}

Wir haben die void Setup stark entschlackt und starten hier nur noch den Seriellen Monitor und rufen die zwei definierten SetUp funktionen auf.

Loop

//--- LOOP ---
void loop() {
  if (Serial.available() > 0) {    // is a character available?
    rx_byte = Serial.read();       // get the character

Wenn wir eine Eingabe durch den Seriellen Monitor erhalten, wird diese in rx_byte gespeichert. Es geht nur um ein Zeichen.

/ check if a number was received
    if ((rx_byte >= '0') && (rx_byte <= '9')) {
      Serial.print("Number received: ");
      Serial.println(rx_byte);
      if (rx_byte == '1'){
        preheat();
      }
      if (rx_byte == '2'){
        cooldown();
      }
    }
    else {
      Serial.println("Not a number.");
    }
}

Dann prüfen wir ob dieses Zeichen eine Nummer ist. Falls wir eine 1 Eingeben, rufen wir die Funktion preheat auf. Falls eine 2, cooldown.

Falls keine Nummer eingetippt wurde, gibt es eine Fehlermeldung.

//we want to measure the current nozzle Temperature and Diameter in a certain interval -> this is only for the Charts, for control we measure more often.
  if (millis() - previousMillisMes >= messIntervall) {
    previousMillisMes = millis();
    DEBUG_P(readNTC());
  }

  if (heaterOn) {
    heatNozzle();
  }
}

Hier ist wieder der Messintervall alle 2 Sekunden. Außerdem prüfen wir ob die bool heaterOn wahr ist, und wenn dem so ist, starten wir die Funktion heatNozzle.

PID-Tab

//Include the PID LIB
#include <PID_v1.h>

Wir brauchen natürlich die oben erwähnte Bibliothek.

//Define some Pins
#define HEATER_PIN 23

Das Relais schalten wir an Pin 23. Wenn wir den Pin auf High setzen, werden die 230V zum Heizelement durchgelassen. Das SSR achtet dabei sogar auf Phasendurchgänge.

//Define Vars for the PID controller
double Setpoint, Input, Output;

double Kp = 2.0;
double Ki = 0.2;
double Kd = 0.0;

unsigned long WindowSize = 5000; //this is our Control-Window in MS

Als nächstes brauchen wir die Arbeitsvariablen für den PID Regler. Wir haben einen Sollwert – den Setpoint, einen Messwert, den Input und einen Stellwert, den Output. Außerdem haben wir die drei Regelparameter Kp, Ki und Kd.

Hintergrund PID Regelung

Man unterscheidet zwischen Steuerung und Regelung. Eine Steuerung ist blind, d.h. wir würden das Heizelement für eine bestimmte Dauer anschalten und dann wieder aus. Die Temperatur würde nicht gemessen. Besser ist eine Regelung. Hier erhalten wir direktes Feedback (nach einer gewissen Zeit, genannt Regelstrecke) mit dem wir unsere Regelung wieder verbessern können.

P-Anteil

Wenn der Messwert weit vom Sollwert weg ist, brauchen wir einen großen Stellwert, wenn er nah dran ist, einen kleinen. Diese Änderung ist proportional. In den Variablen sehen wir den Kp Wert. Hier wird die Konstante für die proportionale Änderung eingetragen. Je höher dieser Wert gewählt wird, desto aggressiver wird die Regelung. Der Sollwert kann zwar schneller erreicht werden, jedoch haben wir ggf. einen Überschwinger.

Mathematisch ausgedrückt: Wenn die Regeldifferenz (e = Sollwert – Istwert) groß ist, muss der Stellwert y durch den Faktor Kp verstärkt werden: y = e * Kp

Leider hat diese P-Regelung den Nachteil, dass der Sollwert nie genau erreicht werden kann. Er wird immer etwas Abweichen und um den Sollwert herum schwingen.

I-Anteil

Eine andere Variante ist der I-Regler. Dazu kann man sich die bisherigen Werte der Regeldifferenz e ansehen. Man nutzt die Integralrechnung um den Grad der Abweichung zu berechnen. So wird es Möglich die Regeldifferenz auf 0 zu Reduzieren. Das Wiederum bedeutet, der Istwert entspricht dem Sollwert.

Konkret ist beim Integral des Sollwertes die einzige Variable die Zeit. Je mehr Zeit verstreicht, desto größer wird die Fläche unter der Kurve. Wenn wir nun das Integral des Istwertes betrachten, müssen wir uns dem des Sollwertes annähern. Die Flächen sollen gleich werden.

Mathematisch: y = Ki * (integral 0 -> t) * e

Leider ist dieser Regler aber nur sehr langsam.

D-Anteil

Zu guter letzt gibt es die Möglichkeit, einen Trend zu analysieren und einen Faktor für diesen Einzuführen. D Steht hierbei für Differential.

Mehr Informationen zu PID Reglern findet man sehr gut im Netz.

Constructor

PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT); //PID Controller constructor

Hier wird der PID Konstruktor gebaut. Wir übergeben alle Werte an die Bibliothek.

setupHeater

//--- Function to connect Pins and stuff
void setupHeater() {
  DEBUG_P("Function: setupHeater...");
  //Defining the Outputs
  pinMode(HEATER_PIN, OUTPUT);
  digitalWrite(HEATER_PIN, LOW);

  //initialise the PID
  Setpoint = nozzleTempSP + nozzleTempOffsetSP; //calculate the real setpoint from UI
  DEBUG_P("Calculated Setpoint...");
  myPID.SetOutputLimits(0, WindowSize);         //this makes the pid lib recalculate the output value to our timeframe -> basically a chart mapping
  //turn the PID on
  myPID.SetMode(AUTOMATIC);                     //there are some other mode but no clue of that.
  DEBUG_P("Initialised PID...");
}

Wir sagen der IDE wieder, welch die pinModes sind und stellen hier zusätzlich Sicher, dass der Ausgang auch auf Low steht. Dann berechnen wir den Sollwert . Der Abschnitt myPID.SetOutputLimits() sorgt letztendlich nur für ein ReMapping des Stellwertes. Denn wir nutzen eine bestimmte Art, das Relais zu steuern. Dazu aber gleich mehr. Danach setzen wir den Modus auf Automatik, ich habe mich aber nicht gut genug mit der Bibliothek beschäftigt um genau sagen zu können was das macht.

preheat

void preheat() {                          //after the user startet this we fire up the heater
  DEBUG_P("Function: preheat");
  heaterOn = true; //if this is true, in the main arduino loop the function heatNozzle will be called every loop -> this is the next step in program logic
}

Die Funktion preheat wird von uns durch den seriellen Monitor aufgerufen. Im Prinzip schalten wir hier nur die bool heaterOn auf True, damit der code zum Heizen in der loop berücksichtigt wird.

cooldown

void cooldown() {
  DEBUG_P("Function: cooldown");
  heaterOn = false; //we just set this to false to exclude the heatNozzle from the loop again
  digitalWrite(HEATER_PIN, LOW);  //and this is just a safety call to make sure heater is low
}

Die Funktion cooldown schaltet die bool heaterOn wieder aus. Zusätzlich stellen wir sicher, dass das Heizelement wirklich aus ist, in dem einmal den Low Befehl setzen. Wir könnten auch wenn die bool false ist, in der loop kontinuierlich das low Signal setzen.

heatNozzle

void heatNozzle() { //if heaterOn Boolean is true, this function is called in main loop on every loop -> this controls the nozzle temperature
  DEBUG_P("Function: heatNozzle");

  if (millis() % 1000 == 0) { //We do this part every second to get the current Temperature -> mostly for debugging
    unsigned long zeit = (millis() / 1000);
    Input = readNTC().toInt();
    DEBUG_P(String(zeit));
    DEBUG_F("; ");
    DEBUG_P(String(Input));
    DEBUG_F("; ");
    DEBUG_P(String(Output));
    DEBUG_F(" ms");
  }
  // Input = readNTC(); //uncomment if you need every loop sensor reading
  myPID.Compute(); //now we let the PID LIB do its magic
  sps(Output);    //and call the controller routine to actually turn on off the heater in a time window manner
}

In der Funktion heatNozzle lesen wir bei jedem Durchgang (oder jeder Sekunde, je nachdem was ihr möchtet) die aktuelle Ist-Temperatur und übergeben diese an den PID Regler. Dieser berechnet dann den Stellwert und startet damit die Funktion sps. SPS steht für Schwingungspaketsteuerung und ist eine einfache Methode um träge Stellhardware zu schalten.

Schwingungspaketsteuerung

void sps(double hz) {
  //https://de.wikipedia.org/wiki/Schwingungspaketsteuerung

  static unsigned long szl;
  static unsigned long szh;               //the first low time is the window time minus the output from the pid
  unsigned long lzl = WindowSize - hz;
  unsigned long lzh = hz;

  if (!digitalRead(HEATER_PIN) && millis() - szl >= lzl ) { //if heaterpin is low and the current time - start time low is bigger than last time low
    szh = millis();                                         //start time high is current time
    digitalWrite(HEATER_PIN, HIGH);                         //heater on!
  }
  if (digitalRead(HEATER_PIN) && millis() - szh >= lzh ) { // if heaterpin is on and the current time minus start time high is bigger than last time low
    szl = millis();
    digitalWrite(HEATER_PIN, LOW);                        //heater on
  }
}

Würden wir beispielsweise einen Durchfluss regeln, müssten wir als Stellwert ein Ventil auf einen bestimmten Winkel (0 bis 90°) einstellen. Hier brauchen wir eine Leistung von 0 bis 100%. Da wir aber nur an und aus schalten können, legen wir ein Zeitfenster fest. Wenn wir beispielsweise 5 Sekunden als Zeitfenster nehmen und eine Sekunde lang das Heizelement schalten, liegen wir bei einer Leistung von 20%.

Genau das setzten wir in der Funktion sps um. Wir haben jeweils eine Startzeit (StartZeitLoW = szl, StartZeitHigh = szh) und eine Laufzeit (lzl, lzh). Damit wir nicht mit delays Arbeiten müssen, nutzen wir wieder millis und Timer.

Im Detail prüfen wir zunächst den Stellwert. Wenn wir diesen vom Zeitfenster abziehen, erhalten wir die LaufZeitLow. Dann prüfen wir in welchem Zustand das Heizelement gerade ist. Wenn wir aktuell Heizen und die Marke für die LaufzeitLow passieren, schalten wir aus. Dann wird die neue StartzeitHigh festgelegt. Wenn wir nicht heizen und die LaufzeitHigh passieren, schalten wir ein und setzen eine neue StartZeitlow. Die aktuelle Zeit – der Startzeit ergibt ja immer ein Zeitfenster zwischen 0 (zur Startzeit) und größer werdend je mehr Zeit verstrichen ist. Wenn wir dann die größe des Zeitfensters erreichen, wird es Zeit umzuschalten.

Ich hoffe das ist halbwegs verständlich erklärt :S

So haben wir sozusagen ein langsames PWM Signal erzeugt um die Heizmanschette gezielt steuern zu können. Wir modulieren nur nicht mit Pulsweiten, sondern mit Schwingungspaketen. Ein Paket entspricht jeweils der Heizzeit im Zeitfenster.

Als nächstes bauen wir unseren Webserver in Teil 3. Danach optimieren wir die PID Werte in Teil 4.

3 Kommentare

Moin Daniel,

also auf GitHub kannst du dir den gesamte Code ziehen, die Firmware funktioniert auch schon halbwegs.

Ich hatte dann aber Schwierigkeiten gescheite PID-Werte zu bekommen, dabei ein Heizelement durchgebrannt und dann bin ich umgezogen. Das Projekt wartet aktuell in einem Karton auf eine Wiederbelebung, aber es gibt ein paar sehr sehr coole Filament-Extruder Projekte an denen man sich sehr gut orientieren kann.

Code: https://github.com/partofthecurse/axTruder

Schreibe einen Kommentar

Geb mir einen aus :)

Wenn du das Zeug hier magst, denk doch über eine Spende nach um Server und Domain zu finanzieren.

$ Die mit einem $ gekennzeichneten Links, sind Affiliate Links. Wenn du über diese in den Shop gelangst und etwas kaufst, bekomme ich eine kleine Provision.

Suche & Filter