Funk Grillthermometer (433 MHz)

Über den Beitrag

Normalerweise sind meine Beiträge ja eher grundsätzlicher Natur, aber hier möchte ich einmal ein ganz konkretes Projekt von mir vorstellen, und zwar ein Funk Grillthermometer mit grafischer Anzeige des Temperaturverlaufs.

Ich empfinde es im Winter als besonders schön, ein gutes Stück Fleisch auf dem Grill zu haben, während ich drinnen sitze, schon mal einen Wein trinke und die Entwicklung der Kerntemperatur des Grillgutes verfolge. Und wenn man das dann noch auf seinem selbstgebauten Gerät tun kann, macht es besonders viel Spaß. Braucht man eine grafische Anzeige? Nein, natürlich nicht, aber Spaß macht es schon und angeben kann man damit auch!

Fühler für das Funk Grillthermometer

Um ein Grillthermometer zu bauen, benötigt man zunächst einmal einen geeigneten Temperaturfühler. So etwas bekommt ihr unter dem Suchbegriff „Ersatzfühler“ oder „Ersatzsonde“ für 5 bis 15 Euro in Onlineshops wie zum Beispiel Amazon.

Verschiedene Temperaturfühler für das Funk Grillthermometer
Verschiedene Temperaturfühler

Meistens haben die Fühler einen 2.5 oder 3.5 mm Klinkenstecker als Anschluss. Ob der Stecker zwei oder drei Pole hat, spielt keine Rolle, denn im Regelfall sind nur zwei davon aktiv. Passend dazu gibt es entsprechende Buchsen, die ihr dann in das Gehäuse der Sendeeinheit eures Thermometers einbauen könnt.

Die Fühler basieren auf einem temperaturabhängigen Widerstand. Leider ändert sich der Widerstandswert nicht linear mit der Temperatur und außerdem ist jedes Modell anders. Eine Kalibrierung ist also unumgänglich. Dazu nehmt ihr mit einem bekannten, zuverlässigen Thermometer eine Temperatur-Widerstandskurve auf. Ein erster Test mit meinem Multimeter ergab, dass der Widerstand bei 0 °C bei 330 kΩ lag. Bei Raumtemperatur betrug er um die 120 kΩ und bei 100 °C fiel er auf 6.3 kΩ. Das ist weit entfernt von jeglicher Linearität.

Messung der Thermometerwiderstände

Theorie

Im Prinzip könnt ihr die Widerstände für die Temperatur-Widerstandskurve direkt mit einem Multimeter messen. Praktisch ist es besser, die Widerstände so zu messen, wie ihr es auch später in eurer Schaltung macht. Der Vorteil ist, dass ihr Fehler, wie z.B. eine potenzielle nicht-Linearität eures A/D-Wandlers „wegkalibriert“.

Die Grundlage für die Bestimmung des Widerstandes ist das Ohmsche Gesetz:

U = R\cdot I\;\;\;\;\;\text{bzw.:}\;\;\;\;\;\frac{U}{R}=I

Spannungen könnt ihr bequem mit A/D-Wandlern messen, Ströme sind komplizierter, zumal sie bei diesen Widerstandsgrößen überaus gering sind. Deshalb behelfen wir uns mit einem zweiten, bekannten Widerstand (Referenz), den wir mit dem Thermometer in Reihe schalten. Da der Strom durch beide Widerstände fließt, gilt:

\frac{U_{\text{Thermometer}}}{R_{\text{Thermometer}}}= I = \frac{U_{\text{Referenz}}}{R_{\text{Referenz}}}\;\;\;\;\;\;\;\Rightarrow\;\;\;\;\;\;\;R_{\text{Thermometer}}=\frac{U_{\text{Thermometer}}}{U_{\text{Referenz}}}\cdot R_{\text{Referenz}}

Um bei den Temperaturmessungen eine gute Auflösung über den ganzen Bereich zu erzielen, sollte die Größe des Referenzwiderstandes dem Thermometerwiderstand in der Mitte des Temperaturmessbereiches entsprechen. Ich habe 33 kΩ ausgewählt.

Praxis

Ich wollte möglichst genau messen und habe deswegen mit dem ADS1115 einen guten, externen A/D-Wandler gewählt. Vor ein paar Jahren habe ich schon einmal eine Version 1.0 eines Grillthermometers gebaut und dabei den A/D-Wandler eines Arduino Nano verwendet. Damit konnte ich eine Genauigkeit von +/- 1 Grad erzielen. Das geht also auch und ist später weniger zu löten – eure Entscheidung.

Die folgende Schaltung habe ich zur Aufnahme der Kalibrierkurve verwendet:

Ich messe hier also die Gesamtspannung und den Spannungsabfall über dem Referenzwiderstand. UThermometer ist die Differenz.

Den Temperaturfühler wird im Schaltplan durch die Anschlussbuchse repräsentiert. Denkt ihn euch dazu. Und hier nun der Sketch:

#include<ADS1115_WE.h> 
#include<Wire.h>
#define I2C_ADDRESS 0x48
const float constResistorVal = 32.8; // 32.8 kOhm

ADS1115_WE adc = ADS1115_WE(I2C_ADDRESS);

void setup() {
  Wire.begin();
  Serial.begin(115200);
  if(!adc.init()){
    Serial.println("ADS1115 not connected!");
  }
  adc.setMeasureMode(ADS1115_CONTINUOUS); // Continuous Mode
  adc.setPermanentAutoRangeMode(true);  // Autorange Mode
}

void loop(){
  float supplyVoltage = 0.0;
  float resistorVoltage = 0.0;
  float thermometerVoltage = 0.0;
  float thermometerResistance = 0.0;

  Serial.print("Supply Voltage [V]: ");
  supplyVoltage = readChannel(ADS1115_COMP_0_GND);
  Serial.println(supplyVoltage);
  
  Serial.print("Resistor Voltage [V]: ");
  resistorVoltage = readChannel(ADS1115_COMP_1_GND);
  Serial.println(resistorVoltage);

  Serial.print("ThermometerVoltage [V]: ");
  thermometerVoltage = supplyVoltage - resistorVoltage;
  Serial.println(thermometerVoltage);

  Serial.print("Thermometer Resistance [kOhm]: ");
  thermometerResistance = thermometerVoltage/resistorVoltage * constResistorVal;
  Serial.println(thermometerResistance);
  
  Serial.println();
  Serial.println("-------------------------------");
  Serial.println();
  delay(500);
}

float readChannel(ADS1115_MUX channel) {
  float volts = 0.0;
  adc.setCompareChannels(channel);
  for(int i=0; i<10; i++){
    volts += adc.getResult_V();
  } 
  return volts/10;
}

 

Wenn ihr den internen A/D-Wandler eures Boards verwenden wollt, müsst ihr den Sketch entsprechend modifizieren.

Für die Messung habe ich einen Topf mit Wasser zum Kochen gebracht und dann beim Abkühlen Widerstand und Temperatur gemessen. Im Topf hat man schnell Temperaturzonen und sollte deswegen rühren. Außerdem sollten die Thermometerspitzen nicht – so wie auf dem Foto – auf dem Boden aufliegen. Ich hatte nur nicht genügend Hände beim Fotografieren.

Aufnahme der Kalibrierkure für das Funk Grillthermometer
Aufnahme der Kalibrierkure für das Funk Grillthermometer

Am Anfang fällt die Temperatur schnell, dann immer langsamer. Als es zu langsam ging, habe ich portionsweise kaltes Wasser zugefügt. Für den Bereich von Raumtemperatur bis 0 Grad kamen dann Eiswürfel zum Einsatz.

Die Kalibrierung kann maximal so gut sein wie euer Vergleichsthermometer. Wie ihr auf dem Foto oben seht, weichen die beiden Grillthermometer um ein Grad voneinander ab. Glaubt also nicht jeder Digitalanzeige!

Auswertung der Kalibrierkurve für das Funk Grillthermometer

Die Messwertepaare aus Temperatur und Widerstand habe ich in Excel eingetippt und damit ein Diagramm erzeugt. Dann habe ich versucht, eine Ausgleichskurve durch die Werte zu legen. Im Excel heißen Ausgleichskurven „Trendlinien“. Um Trendlinien anzuzeigen, macht einen Linksklick auf einen Datenpunkt, dann einen Rechtsklick und wählt „Trendlinie hinzufügen“. Dann könnt ihr wählen, welche Art von Funktion ihr verwenden wollt. Und macht einen Haken bei „Formel im Diagramm anzeigen“.

Weder eine logarithmische Funktion, noch ein Ausgleichspolynom passten wirklich gut:

Kalibrierkurve 0 -100 °C mit Polynom vierter Ordnung als Trendlinie

Erst als ich die Kurve in drei Stücke geteilt habe, war ich mit den Trendlinien zufrieden. Die Diagramme sind etwas klein. Klickt auf sie, um sie groß anzuzeigen:

0 – 40°C
40 – 80°C
80 -100°C

Hier noch ein Hinweis: übernehmt die Trendlinien nicht blind in eure Sketche. Gerade bei Polynomen höherer Ordnungen werden bei den Koeffizienten oft nicht genügend Stellen angezeigt (so wie oben in der Kalibrierkurve 0-100°C).  Klickt auf die Formel im Diagramm und wählt rechts bei den Optionen die Säulen (Beschriftungsoptionen) aus. Dort legt ihr als Rubrik „Wissenschaftlich“ fest und wählt eine angemessene Anzahl Dezimalstellen.

Zur Sicherheit könntet ihr probeweise Widerstandswerte in die Funktionen einsetzen und die Ergebnisse mit euren Messwerten vergleichen. So sah mein Prüfsketch aus:

float r = 331.0; // resistance
float temperature = 0.0;

void setup() {
  Serial.begin(115200);
  for(int i=0; i<140; i++){
    r = r - 2.0;
    temperature =  8.731*pow(r,4)*pow(10,-9) - 9.025*pow(r,3)*pow(10,-6)+ 0.00354*pow(r,2)- 0.709*r + 69.094; 
    Serial.print("R: ");
    Serial.print(r);
    Serial.print(",   T:");
    Serial.println(temperature);  
  }
  Serial.println("********************");
  r = 60.0;
  for(int i=0; i<49; i++){
    r = r - 1.0;
    temperature = -26.74*log(r) + 146.08;
    Serial.print("R: ");
    Serial.print(r);
    Serial.print(",   T:");
    Serial.println(temperature);  
  }
  Serial.println("********************");
  r = 13.0;
  for(int i=0; i<15; i++){
    r = r - 0.5;
    temperature = -0.041*pow(r,3) + 1.3525*pow(r,2) - 17.51*r + 166.68;
    Serial.print("R: ");
    Serial.print(r);
    Serial.print(",   T:");
    Serial.println(temperature);  
  }
  Serial.println("********************");
}

void loop() {
}

 

Wenn alles OK ist, dann habt ihr damit das Kernstück für das Funk Grillthermometer fertiggestellt.

Die Sendeeinheit

Konzept der Sendeeinheit

Neben der Temperaturmessung soll die Sendeeinheit folgendes können:

  • die Temperatur auf einem Display ausgeben,
  • der Empfängereinheit die Temperatur alle paar Sekunden per Funk senden,
  • die Batteriespannung überwachen.

Als Display habe ich ein kleines 128×32 px OLED Modell gewählt, das über I2C angesteuert wird.

Für die Funkübertragung fiel meine Wahl auf das 433 MHz Modul HC-12, das ich im Detail hier beschrieben habe. Es zeichnet sich durch eine hohe Reichweite und eine überaus einfache Ansteuerung durch Software Serial aus.

Zur Spannungsversorgung verwende ich zwei hintereinander geschaltete 3.7 Volt Lithium-Ionen-Akkus an VIN des Arduino Nano. Da der A/D Wandler nur bis zu 5 Volt erfasst, messe ich die Spannung über einen Spannungsteiler (2 x 10 kΩ).

Eine LED leuchtet durchgehend, wenn die Batteriespannung noch OK ist (>= 7.2 Volt), anderenfalls blinkt sie.

Hier die Schaltung:

Schaltung der Sendeeinheit für das Funk Grillthermometer
Schaltung der Sendeeinheit

Zum Stromversorgungskonzept für das Funk Grillthermometer

Lithium-Ionen-Akkus in AA Bauform (14500) sind etwas ungewöhnlich. Der Vorteil an ihnen ist, dass ihr dafür Batteriegehäuse mit Schalter bekommt. Ein Nachteil ist hingegen, dass ihr ein spezielles Ladegerät braucht. Ferner muss man aufpassen, dass man sie streng von seinen Ni-Mh Akkus trennt. Denn legt ihr sie in die falschen Geräte ein, könnt ihr diese zerstören. Und bei der Anzeige der Kapazitäten übertreiben die Anbieter gnadenlos.

Für die Empfängereinheit (um das vorwegzunehmen), die wegen des größeren Displays mehr Strom verbraucht, habe ich die großen Lithium-Ionen-Akkus der Bauform 18650 verwendet. Diese halten eher, was hinsichtlich ihrer Kapazität versprochen wird. Sie brauchen natürlich auch ein spezielles Ladegerät. Und Batteriegehäuse sind etwas schwieriger zu bekommen.

18650 und 14500 (AA) Li-Ionen Akkus mit Batteriegehäusen für das Funk Grillthermometer
18650 und 14500 (AA) Li-Ionen-Akkus mit Batteriegehäusen

In meinem Funk Grillthermometer Version 1.0 hatte ich drei Ni-Mh Akkus und einen Step-Up Konverter verwendet – das geht auch. Weitere Alternativen sind 9 V Block Akkus oder ein 6er-Pack Ni-Mh Akkus.

Noch ein Hinweis: der VIN Anschluss nimmt euch eine Verpolung sehr übel. Der Arduino Nano stirbt einen sofortigen Tod. Gerade bei 9 Volt Blöcken kann das leicht passieren. Ein kurzer Kontakt reicht. Zur Sicherheit könntet ihr noch eine Diode einbauen.

Sketch der Sendeeinheit

Der Sketch für die Sendeeinheit des Funk Grillthermometers ist nicht sonderlich komplex. Allerdings nutzt er eine Reihe von Bibliotheken, auf die ich nicht im Detail eingehen kann. Ich glaube aber, dass der Sketch trotzdem einigermaßen verständlich ist.

#include <ADS1115_WE.h> 
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <SoftwareSerial.h>

#define OLED_RESET 7 // we don't have a reset, but the constructor expects it 
#define I2C_ADDRESS 0x48
const int ledPin = 8;
const float constResistorVal = 32.8; 

SoftwareSerial hc12(10,11);
Adafruit_SSD1306 display(OLED_RESET);
ADS1115_WE adc = ADS1115_WE(I2C_ADDRESS);

void setup() {
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, HIGH);
  Wire.begin();
  Serial.begin(115200);
  hc12.begin(9600);
  if(!adc.init()){
    Serial.println("ADS1115 not connected!");
  }
  adc.setMeasureMode(ADS1115_CONTINUOUS);
  adc.setPermanentAutoRangeMode(true);

  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  // initialize with the I2C addr 0x3C (for the 128x64)
  display.clearDisplay();
  display.setTextSize(3);
  display.setTextColor(WHITE);
  display.setCursor(10,4);
  display.println("Wait");
  display.display();
}

void loop(){
  float supplyVoltage = 0.0;
  float resistorVoltage = 0.0;
  float thermometerVoltage = 0.0;
  float thermometerResistance = 0.0;
  float batteryVoltage = 0.0;

  Serial.print("Supply Voltage [V]: ");
  supplyVoltage = readChannel(ADS1115_COMP_0_GND);
  Serial.println(supplyVoltage);
  
  Serial.print("Resistor Voltage [V]: ");
  resistorVoltage = readChannel(ADS1115_COMP_1_GND);
  Serial.println(resistorVoltage);

  Serial.print("ThermometerVoltage [V]: ");
  thermometerVoltage = supplyVoltage - resistorVoltage;
  Serial.println(thermometerVoltage);

  Serial.print("Thermometer Resistance [kOhm]: ");
  thermometerResistance = thermometerVoltage/resistorVoltage * constResistorVal;
  Serial.println(thermometerResistance);

  Serial.print("Battery Voltage [V]: ");
  batteryVoltage = readChannel(ADS1115_COMP_2_GND) * 2;
  Serial.println(batteryVoltage);

  Serial.print("Temperature [°C]: ");
  float temperature = calcTemp(thermometerResistance);
  Serial.println(temperature);

  displayAndSendTemperature(temperature);
  if(batteryVoltage <= 7.2){
    digitalWrite(ledPin, LOW);
    delay(500);
    digitalWrite(ledPin, HIGH);
  }
  Serial.println();
  Serial.println("-------------------------------");
  Serial.println();
  delay(2500);
}

void displayAndSendTemperature(float tC){
  String tCString = "";
  tCString = String(tC,1) + " ";
  hc12.print(tCString);
  tCString += char(247);  // ASCII No 247 = "°"
  tCString += "C";
  display.clearDisplay();
  display.setCursor(0,11);
  display.println(tCString);
  display.display();
}

float calcTemp(float r){
  float tC = 0.0; // temperature Celsius
  if(r < 11.5){
    tC = -0.041*pow(r,3) + 1.3525*pow(r,2) - 17.51*r + 166.68;
  }
  else if(r < 55.0){
    tC = -26.74*log(r) + 146.08; 
  }
  else{
    tC = 8.731*pow(r,4)*pow(10,-9) - 9.025*pow(r,3)*pow(10,-6)+ 0.00354*pow(r,2)- 0.709*r + 69.094;  
  }
  return tC;
}

float readChannel(ADS1115_MUX channel) {
  float volts = 0.0;
  adc.setCompareChannels(channel);
  for(int i=0; i<10; i++){
    volts += adc.getResult_V();
  } 
  return volts/10;
}

 

Ein paar Erläuterungen:

  • Der A/D-Wandler ADS1115 misst die Spannungen, aus denen der Thermometerwiderstand berechnet wird.
  • Außerdem misst der ADS1115 die Akkuspannung.
  • Die Funktion calcTemp() berechnet mithilfe der Kalibrierfunktionen die Temperatur.
  • Die Temperatur wird in einen String umgewandelt und auf dem Display ausgegeben.
  • Der HC-12 sendet die Temperatur an die Empfängereinheit (ganz einfach über hc12.print())
  • Fällt die Akkuspannung unter 7.2 Volt, dann geht die LED für eine halbe Sekunde aus.
  • Nach 2.5 Sekunden Pause beginnt der Zyklus von vorne.

Bau der Sendeeinheit

Die Sendeeinheit habe ich in einem Elektronikgehäuse untergebracht, welches ich vielleicht auch eine Nummer kleiner hätte wählen können. Der A/D-Wandler, der Arduino Nano, die Widerstände und das HC-12 Modul nahmen auf einer Lochrasterplatine Platz, die wiederum mit Abstandshaltern auf der Bodenplatte befestigt wurde.

Das Batteriegehäuse habe ich angeschraubt und die Kabel für die Versorgungsspannung nach innen verlegt. Durch ein zusätzliches Loch bekamen die Kabel einen Zugang zum „Hauptgehäuse“ und sind so von außen nicht mehr sichtbar.

Für die LED, die Buchse für den Temperaturfühler und die Pinleiste des Displays musste ich weitere Öffnungen schaffen. Plastik ist nicht das schönste Material zum Basteln, weil es beim Bohren an den Kontaktstellen schmilzt. Aber es geht. Etwas schwieriger war es, die Öffnung für die Pinleiste des Display zu realisieren. Hier hat ein Dremelwerkzeug gute Dienste geleistet.

Das Funk Grillthermometer (Sendeeinheit) von Innen
Das Funk Grillthermometer (Sendeeinheit) von Innen
Befestigung der Platine - Sendeeinheit Funk Grillthermometer.
Befestigung der Platine

Nun könnt ihr, falls ihr das Projekt nachbaut, mit einem zweiten Microcontroller und einem weiteren HC-12 Modul und/oder einer LED prüfen, ob die Übertragung funktioniert oder auch die Reichweite überprüfen.

void setup() {
  Serial.begin(115200);
  hc12.begin(9600);
  pinMode(ledPin, OUTPUT);
  Serial.println("BBQ Receive Test");
}

void loop(){ 
  String message = "";
  if(hc12.available()) {
    message = hc12.readString();
    Serial.println(message);
    if(message.indexOf(".")){  // sense check
      digitalWrite(ledPin, HIGH);
      delay(500);
      digitalWrite(ledPin, LOW);
    }
  }
}

Damit ist die Sendeeinheit fertig. Auch ohne Empfängereinheit habt ihr damit schon ein funktionierendes, digitales Grillthermometer.

Die Empfängereinheit für das Funk Grillthermometer

Konzept der Empfängereinheit

Die Empfängereinheit soll Folgendes können:

  • die Messwerte der Sendeeinheit empfangen und als Wert anzeigen
  • den Temperaturverlauf grafisch darstellen
  • bei einer wählbaren Zieltemperatur eine akustische Warnung ausgeben
  • mithilfe einer LED anzeigen, wenn die Akkuspannung unter 7.2 Volt fällt

Als Mikrocontroller habe ich wieder einen Arduino Nano gewählt.

Beim Display habe ich mich für ein 128×160 px TFT Display mit integriertem ST7735 Controller entschieden. Es wird über SPI angesteuert. Da der Controller nicht 5 Volt kompatibel ist, wird bei Verwendung eines AVR basierten Arduinos ein Levelshifter benötigt. Ein Pinout Schema für das von mir verwendete Display findet ihr hier. Eine Einführung dazu gibt es hier, es wird dort allerdings die Nicht-Kompatibilität mit 5 Volt ignoriert. Als Levelshifter habe ich den 8-Kanal TXB0108 verwendet.

Der Temperaturalarm wird mit einem günstigen 5 Volt Buzzer ausgegeben. Die Teile sind nicht übermäßig laut, aber günstig in der Anschaffung und direkt über einen Arduino Pin ansteuerbar.

Die Einstellung des Temperaturalarms erfolgt über zwei Taster. Mein Wahl fiel auf eine einschraubbare Bauform, die u.a. bei Modelleisenbahnen zum Einsatz kommt.

Die Stromversorgung hatte ich schon weiter oben behandelt.

 

Fritzing Schaltplan der Empfängereinheit für das Funk Grillthermometer.
Fritzing Schaltplan der Empfängereinheit

Sketch der Empfängereinheit

Der Sketch für die Empfängereinheit ist komplexer als der für die Sendeeinheit. Er ist auch zu lang, als dass ich ihn Zeile für Zeile erklären könnte. Es folgen noch ein paar Erläuterungen, aber hier zunächst der Sketch:

#include <TFT.h> 
#include <SoftwareSerial.h>

// TFT settings(Arduino Nano): SDA - 11, SCK - 13
#define CS 10 // Chip Select
#define DC 8 
#define RESET 9
#define ALERTPIN 5
#define LED_PIN 2
#define CC_X_BEGIN 15
#define CC_X_END   125
#define CC_Y_BEGIN 10
#define CC_Y_END   120

enum tftColor{RED=1, GREEN, YELLOW, BLUE, LIGHTBLUE, GREY, BLACK};

TFT myScreen = TFT(CS, DC, RESET);
SoftwareSerial hc12(6,7);

float pixelsPerDegree = CC_Y_END/90.0; // Scaling of the T-axis (10.0 - 100.0 °C) -> constant
unsigned int secsPerXPixel = 6;  // Scaling of the t-axis -> changes over time
int targetT = 55;        // target temperature
int valCounter = 0;      // counter for measured values
byte coordPair[105][2];  // coordinates time/temperature in pixels
bool alert = true;       // alert, when target temperature is reached

void setup(){
  pinMode(ALERTPIN,OUTPUT);
  pinMode(LED_PIN, OUTPUT); 
  Serial.begin(115200);
  hc12.begin(9600);
  myScreen.begin();  
  myScreen.setRotation(0); // portrait view
  myScreen.background(0, 0, 0);  // clear the screen with black
  delay(500);
  setTargetTemperature();
  displayCoordinateCross();   
}

void loop(){
  static float currentTemp = 0.0;      // current Temperature
  static unsigned int prevUpdate = 0;  // time of the last temperature update
  targetTempCheck(currentTemp);
  currentTemp = waitForTemperatureUpdate(); // Wait for a message from sender
  Serial.println(currentTemp);
  
  unsigned int currentTime = (unsigned int)(millis() / 1000); // current time in seconds
  if(((currentTime-prevUpdate) > secsPerXPixel) && (currentTemp > 0.0)){  // time for update?  
    displayCurrentTemp(currentTemp); 
    prevUpdate += secsPerXPixel;  //neues Intervall beginnt
    valCounter++;
    myColor(YELLOW);
    coordPair[valCounter][1] = CC_X_BEGIN + valCounter; // x-Wert / Zeit 
    coordPair[valCounter][2] = CC_Y_END - (pixelsPerDegree * (currentTemp - 10.0)); // y-Wert (Temperatur)
    
    if(valCounter > 1){ //Verbinde den vorherigen Punkt mit dem aktuellen
      myScreen.line(coordPair[valCounter-1][1], coordPair[valCounter-1][2], coordPair[valCounter][1], coordPair[valCounter][2]);
    }
    else myScreen.point(coordPair[valCounter][1], coordPair[valCounter][2]); // first point
    
    if(valCounter>99) rescaleCoordinateCross();
   }
   if(analogRead(A0) <  655){ // battery voltage < 7.2 V (voltage divider!)
     digitalWrite(LED_PIN, HIGH);
   }
   else digitalWrite(LED_PIN, LOW);
}

void setTargetTemperature(){
  const int keyPin1 = 3;
  const int keyPin2 = 4;
  unsigned int currentTime = 0;
  bool newTargetT = false;
  char targetTChar[3];     // target temperature as char array
  pinMode(keyPin1, INPUT);
  pinMode(keyPin2, INPUT);
  itoa(targetT,targetTChar,10); 
  
  myColor(RED);
  myScreen.text("Target Temperature: ", 10, 30);
  myScreen.setTextSize(2);
  myScreen.text(targetTChar, 53, 60);  
  myScreen.setTextSize(1);
  myScreen.text("Left:  set", 20, 100);
  myScreen.text("Right: accept", 20, 120);
  
  while((digitalRead(keyPin1) == LOW) && (digitalRead(keyPin2) == LOW)){/*Wait*/}; 
  if(digitalRead(keyPin1) == HIGH){  // set new target temperature 
    delay(300); // debounce 
    myColor(BLACK);
    myScreen.text("Left:  set", 20, 100);     // write in black -> delete
    myScreen.text("Right: accept", 20, 120);
    myColor(LIGHTBLUE);
    myScreen.setTextSize(2);
    myScreen.text(targetTChar, 53, 60);  
    myScreen.setTextSize(1);
    myColor(RED);
    myScreen.text("Left: +10", 30, 100);
    myScreen.text("Right: +1", 30, 120);
    currentTime = millis();
    
    while((millis() - currentTime) < 4000){ // 4s without key pressed = accept
      if(digitalRead(keyPin1) == HIGH){ // left key: increase by ten degrees
        delay(300); // debounce
        targetT = targetT + 10;
        newTargetT = true;
        if(targetT > 100){
          targetT = targetT - 60; // e.g. 96 + 10 --> 46
        } 
        currentTime = millis(); // reset 4s period
      }
      if(digitalRead(keyPin2) == HIGH){ // right key: increase by 1 degree
        delay(300); // debounce 
        targetT = targetT + 1;
        if((targetT%10) == 0){  // e.g. 59 + 1 --> back to 50
          targetT = targetT - 10;
        }
        newTargetT=true;
        currentTime = millis();  // // reset 4s period
      }
      if(newTargetT){ // display new target Temp
        myColor(BLACK);
        myScreen.fill(0,0,0);
        itoa(targetT, targetTChar, 10);
        myScreen.rect(53, 60, 40, 40);
        myColor(LIGHTBLUE);
        myScreen.setTextSize(2);
        myScreen.text(targetTChar, 53, 60);  
        myScreen.setTextSize(1);
        newTargetT = false;
      }
    }
  }
  myScreen.background(0,0,0); // setting target temperature is completed
}
 
void displayCoordinateCross(){
 int yPos;
 char targetTChar[5];  // target temperature as char array
 myColor(RED);
 myScreen.line(CC_X_BEGIN,     CC_Y_BEGIN, CC_X_BEGIN,     CC_Y_END); // x-axis
 myScreen.line(CC_X_BEGIN,     CC_Y_END,   CC_X_END,       CC_Y_END); // y-axis
 myScreen.line(CC_X_BEGIN,     CC_Y_BEGIN, CC_X_BEGIN-3,   CC_Y_BEGIN+5); // arrowheads
 myScreen.line(CC_X_BEGIN,     CC_Y_BEGIN, CC_X_BEGIN+3,   CC_Y_BEGIN+5); 
 myScreen.line(CC_X_END,       CC_Y_END,   CC_X_END-5,     CC_Y_END-3);
 myScreen.line(CC_X_END,       CC_Y_END,   CC_X_END-5,     CC_Y_END+3);
 myScreen.line(CC_X_BEGIN,     CC_Y_END,   CC_X_BEGIN,     CC_Y_END+3); //x-axis ticks at 0, 50, 100
 myScreen.line(CC_X_BEGIN+50,  CC_Y_END,   CC_X_BEGIN+50,  CC_Y_END+3);
 myScreen.line(CC_X_BEGIN+100, CC_Y_END,   CC_X_BEGIN+100, CC_Y_END+3);
 myColor(GREY);
 yPos = CC_Y_END - (pixelsPerDegree * (targetT - 10)); // target temp line
 myScreen.line(CC_X_BEGIN+1, yPos, CC_X_END, yPos);
 myColor(RED);
 myScreen.setTextSize(1);
 myScreen.text("T", 5, 5); // y-axis label
 myScreen.text("t", 120, 107);  // x-axis label
 myColor(GREEN);
 itoa(targetT, targetTChar, 10); // target temperature as char array
 myScreen.text("(", 98, 147);
 myScreen.text(targetTChar, 105, 147);
 myScreen.text(")", 118, 147);
 myColor(RED);
 for(int i = 1; i < 5; i++){ // y-axis labels at 20, 40, 60, 80 degrees
  char degreesT[3];
  yPos = CC_Y_END - (pixelsPerDegree * ((i * 20.0)-10));
  itoa(i*20, degreesT,10);
  myScreen.text(degreesT, 0, yPos-3);
  myScreen.line(CC_X_BEGIN-2, yPos, CC_X_BEGIN, yPos);
 }
 myScreen.text("0", CC_X_BEGIN-3, CC_Y_END+7);  // x-axis labeling 
 for(int i = 1; i < 3; i++){
  int xPos;
  char timeMinutes[3];
  xPos = CC_X_BEGIN + (i * 50);
  itoa(i * 5 * secsPerXPixel/6, timeMinutes, 10);
  if((i * 5 * secsPerXPixel / 6) < 100) myScreen.text(timeMinutes, xPos-3, CC_Y_END+7);
  else myScreen.text(timeMinutes, xPos-8, CC_Y_END+7);
 }
}

void rescaleCoordinateCross(){
  delay(100);
  myScreen.background(0,0,0);
  delay(1000);
  secsPerXPixel = secsPerXPixel * 2;
  displayCoordinateCross();
  valCounter = 49; // after rescale, continue at 50th value
  myColor(YELLOW);
  for(int i=1; i<50; i++){
    coordPair[i][2] = coordPair[2*i][2]; // the values 1-100 are rescaled to 1-50, every second value is deleted
    if(i > 1){
      myScreen.line(coordPair[i-1][1], coordPair[i-1][2], coordPair[i][1], coordPair[i][2]);
    }
    else myScreen.point(coordPair[i][1], coordPair[i][2]);
  }
  myColor(RED);
}

void displayCurrentTemp(float T_float){ // display the current temperature
  char* T_arr;
  String T_string = String(T_float, 1); // make a string using one decimal
  T_arr = &T_string[0]; // String to char array
  
  myScreen.setTextSize(2); 
  myColor(RED);
  myScreen.text("T= ", 5, 140);
  myScreen.stroke(0, 255, 0);
  myScreen.fill(0 ,0, 0); 
  myColor(BLACK);
  myScreen.rect(40, 140, 50, 20); // black rectangle -> deletes an area
  myColor(LIGHTBLUE);
  myScreen.text(T_arr, 40, 140);
  myColor(RED);
}

void myColor(int color){ // definition of some colors
  switch(color){
    case RED:
      myScreen.stroke(0, 0, 255); break;
    case GREEN:
      myScreen.stroke(0, 255, 0); break;
    case YELLOW:
      myScreen.stroke(0, 255, 255); break;
    case BLUE:
      myScreen.stroke(255, 0, 0); break;
    case LIGHTBLUE:
      myScreen.stroke(255, 128, 128);break;
    case GREY:
      myScreen.stroke(16, 16, 16);break;
    case BLACK:
      myScreen.stroke(0, 0, 0); break;
    default:
      myScreen.stroke(0, 0, 255);  
  }
}

float waitForTemperatureUpdate(){
  String message = "";
  while(message.length()<2){
    if(hc12.available()){
      message = hc12.readString();
      Serial.println(message);
      if((message.indexOf(".")) < 1){ //sense check
        message = "";
      }
    }
  }
  return message.toFloat();
}

void targetTempCheck(float currentTemperature){
  const int keyPin1 = 3;
  if(currentTemperature >= (targetT) && (alert==true)){
    digitalWrite(ALERTPIN, HIGH);
    if(digitalRead(keyPin1) == HIGH){
      digitalWrite(ALERTPIN, LOW);
      alert = false;
    }
  }
}

 

Erläuterungen zum Empfängersketch

Ich gehe auf drei Teile bzw. Aspekte des Sketches ein:

  • Die Einstellung der Zieltemperatur
  • Das Koordinatenkreuz
  • Zeitliche Abfolge

Einstellung der Zieltemperatur

Wenn ihr den Sketch startet, könnt ihr eine Zieltemperatur einstellen. Das macht die Funktion setTargetTemperature(). Der Sketch wartet, bis ihr die Voreinstellung 55 °C akzeptiert („accept“) oder auf Einstellen geht („set“). Wählt ihr Letzteres, ändert sich die Schriftfarbe der Temperatur. Mit dem linken Taster erhöht ihr um 10 Grad, mit dem rechten Taster um 1 Grad.  Die Zehner springen nach der neun auf vier, die Einer springen nach der neun auf null. Ihr könnt also Zieltemperaturen von 40 bis 99 °C einstellen. Nach vier Sekunden ohne Tasterdruck wird die Temperatur übernommen.

Zieltemperatureinstellung am Funk Grillthermometer
Zieltemperatureinstellung am Funk Grillthermometer

Das Koordinatenkreuz

Das TFT Display hat seinen Ursprung (0,0) oben links. Ihr müsst also etwas umdenken. Das Temperatur-Zeit Koordinatenkreuz hat seinen Ursprung bei (0 min, 10°C) = (CC_X_BEGIN, CC_Y_END) = (15, 120). Eigentlich gehören Einheiten an die Achsen. Wegen des begrenzten Platzes habe ich darauf verzichtet.

Die Temperaturachse ist festgelegt, die Zeitachse ändert mit der Zeit. Zu Beginn entspricht jeder x-Pixel sechs Sekunden (secsPerXPixel). 100 Pixel sind also 600 Sekunden = 10 Minuten. Nach 10 Minuten wird einfach jeder zweite Wert gelöscht und die x-Achse auf 0 bis 20 Minuten „reskaliert“. Die nächste Änderung des Maßstabes erfolgt dann nach 20 min auf 0 bis 40 Minuten usw. Die Temperaturachse umfasst 10 bis 100 °C (oberer Displayrand). Dafür stehen 120 Pixel zur Verfügung. Die Auflösung beträgt also 1.3 Pixel pro Grad.

Die Zieltemperatur ist unten rechts in Grün angezeigt und außerdem als blaue Linie im Diagramm. Alle verwendeten Farben habe ich in der Funktion myColor() definiert und greife darauf über ein enum zu.

Zeitliche Abfolge

In waitForTemperatureUpdate() wird auf einen neuen Messwert der Sendeeinheit gewartet, der ungefähr alle 2.5 Sekunden oder 3 Sekunden bei „low battery“ eintrifft. Die Funktion gibt den Messwert als Float zurück. Ist die Zeit secsPerXPixel ( = 6, 12, 24 … Sekunden) vergangen, wird der Messwert in einen Koordinatenpunkt umgerechnet und angezeigt. Nach dem einhundertsten Messwert wird der Maßstab der Zeitachse verdoppelt und der Messwertzähler auf 50 zurückgesetzt.

Weil der Empfänger bis zu 3 Sekunden auf einen neuen Messwert wartet, ist der Abstand zwischen zwei angezeigten Messwerten nicht unbedingt genau secsPerXPixel. Allerdings ist über prevUpdate += secsPerXPixel sichergestellt, dass die Abweichungen beim jeweils nächsten Messwert ausgeglichen werden.

Wenn der Messwert die Zieltemperatur erreicht oder überschreitet, ertönt das akustische Warnsignal. Durch Drücken des linken Tasters wird der Alarm ausgeschaltet. Da ich den Ablauf sequenziell gestaltet habe, müsst ihr bis zu 3 Sekunden lang drücken. Wenn euch das stört, könntet ihr das Ausschalten des Alarms über einen Interrupt steuern.

Bau der Empfängereinheit

Hinsichtlich des Gehäuses, der Platine und des Batteriegehäuses bin ich wie bei der Sendeeinheit vorgegangen. Durch die vielen Pins des Displays und den Levelshifter war das Löten recht aufwendig.

Empfängereinheit - Innenansicht
Empfängereinheit – Innenansicht

Zum Verbinden des Displays habe ich Kabel an die Pins gelötet und die Lötstellen mit Schrumpfschlauch geschützt.

Verkabelung des Displays
Verkabelung des Displays

Und so sieht dann das Ergebnis aus:

Das fertige Funk Grillthermometer.
Das fertige Funk Grillthermometer

Funk Grillthermometer – Verbesserungspotential / Varianten

Es gibt sehr viele Möglichkeiten, dieses Projekt noch zu modifizieren, beispielsweise:

  • Verkleinern durch:
    • kleinere Gehäuse (ggf. 3D-Druck), und
    • angepasste Platinen.
  • Anschlüsse für weitere Temperaturfühler.
  • Auswahl der Zieltemperatur aus einer Liste, z.B. „Rind, medium: 57 °C“.
  • 3.3 Volt MCU verwenden, um sich den Levelshifter zu ersparen.
  • Das Display an der Sendeeinheit weglassen.
  • Stromsparende MCUs verwenden, Schlafmodi einsetzen.
  • Verschönern durch Anschrauben der Displays von innen.

Insbesondere der hohe Bastelaufwand wird den einen oder anderen wahrscheinlich vom Nachbau abhalten. Deshalb werde ich noch eine sehr viel einfachere Variante bauen und darüber im nächsten Beitrag als Fortsetzung berichten:

  • Sendeeinheit ohne Display und auf Basis eines ESP32.
  • Übertragung per WLAN.
  • Smartphone / Browser als Empfänger.

Danksagung

Das Bild von den Steaks auf dem Grill, das die Grundlage für mein Beitragsbild ist, habe ich David Butler auf Pixabay zu verdanken.

4 thoughts on “Funk Grillthermometer (433 MHz)

  1. Hallo Wolle,
    tolles Projekt! Habe mir vor einem Jahr eine ähnliche Aufgabe gestellt und aus einem ‚gebrauchten‘ NTC aus einem defekten Notebook Akku und einem Tiny85 ein Funkthermometer gebastelt.
    Um den NTC zu kalibrieren habe ich einen Dallas OneWire DS18B20 und den NTC an den Tiny angeschlossen und mit einem Hilfsprogramm die Temperatur des DS18B20 und den ADC Wert über Serial ausgegeben.
    An drei Punkten die Werte aufgenommen (ca -20 im Gefrierschrank, bei Raumtemperatur und bei 75° im Backofen. Den Vorwiderstand des NTCs so gewählt, dass er etwa dem NTC Wert in der Mitte des gewünschten Messbereichs entspricht. Bei der ADC Wandlung im Tiny hift mitteln ungemein um stabile Werte zu bekommen. 64 x ADC aufaddieren geht perfekt in einen 16Bit uint…
    Da sich die Versorgungsspannung/Referenzspannung des ADC sowieso rauskürzt, läuft das fertige Thermometer ohne Spannungsregner direkt an 3 AA Batterien.
    Mit der ntc-steinhart_and_hart_calculator.xls Kalibrierung komme ich auf besser 0,2°C Abweichung zum DS18B20 über den Bereich zwischen meinen Ecktemperaturen – mit dem internen ADC.
    Ach ja, um die Erwärmung des NTCs durch den Spannungsteilerstrom zu reduzieren und die Batterielebensdauer auf rechnerisch über 20Jahre zu bringen, hängt der Spannungsteiler an einem Portpin, der erst 1ms vor der 64xADC Wandlung aktiviert wird und danach gleich wieder ausgeschaltet wird.
    https://edwardmallon.files.wordpress.com/2017/04/ntc-steinhart_and_hart_calculator.xls

    Viele Grüße, Wolfgang

    PS: ich hätte das Projekt auch gleich mit dem DS18B20 lösen können, aber ich wollte ja was über NTCs lernen

  2. Hallo Wolle,

    danke für Deinen Beitrag. Vielleicht ein Zusatz von mir, da ich mich vor Jahren schon einmal mit der genauen Errechung der Temperaturkurve bei NTCs beschäftigt habe. Dabei bin ich auch das Steinhart-Hardt-Polynom gestoßen. Ich habe dazu auch einen kleinen Blog-Eintrag geschrieben. Vielleicht ist er ja nützlich für Dich.

    Viele Grüße

    David

      1. Hallo Wolfgang,

        auch eine schöne Idee! Über NTCs und RTCs werde ich übrigens auch berichten (wahrscheinlich übernächster Beitrag). Danke für deinen Kommentar!

        VG, Wolle

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert