Flaschenkühler - Programmversion 0.2
Diese Programmversion bindet das OLED-Display ein. Für MISO und SCLK werden die Hardwarepins verwendet. Es könnten auch andere digitale Pins verwendet werden, was sich aber negativ auf die Performanz auswirken soll. Die anderen Pins können ohne Einbußen bei der Performanz frei gewählt werden. Außerdem wurden die beiden Taster integriert, mit denen perspektivisch die Solltemperatur eingestellt können werden soll. Die eingestellte Solltemperatur wird im EEPROM gespeichert. Darüber hinaus wird die Drehzahl des Lüfters überwacht: Sollte der Lüfter sich drehen, dreht sich aber nicht, wird eine Fehlfunktion auf dem Display angegeben. Schließlich wurde noch ein Betriebsstundenzähler programmiert.
Der Code wurde hier und da optimiert, so dass er schneller ausgeführt wird. Beispielsweise wird der Thermistor nun nur noch alle 10 Millisekunden ausgelesen und die Isttemperatur berechnet. Außerdem werden nur die Bereiche auf dem Display neu aufgebaut, die sich geändert haben.
OLED-Display
Das OLED-Display ist an den folgenden Pins angeschlossen:
Arduino Nano Adafruit OLED Breakout Board
GND GND (G)
5V VIN (+)
D13 SCLK (CL)
D11 MOSI (SI)
D5 DC
D6 OLEDCS (OC) (besser bekannt als Chip Select (CS))
D7 RST (R)
Um das OLED zu testen werden verschiedene Variablen angezeigt: die Solltemeratur, die Isttemperatur am Thermistor und die Drehzahl des Lüfters. In der fertigen Programmversion wird freilich die Solltemperatur des Kühlbechers einstellbar sein, und das Peltier-Element entsprechend geregelt werden.
Hilfreiche Webseiten:
- Anleitung GTX-Bibliothek: https://learn.adafruit.com/adafruit-gfx-graphics-library/overview
Die Datenübertragung zum Display ist ziemlich langsam. Um die Ausführung des Codes zu beschleunigen, werden immer nur die Bildschirmbereiche aktualisiert, die sich geändert haben.
Betriebsstunden, Taster, EEPROM
Außerdem werden die Betriebsstunden viertelstundengenau gezählt und im EEPROM gespeichert. Die viertelstundengenaue Zählweise schont das EEPROM. Später soll eine Funktion programmiert werden, die z.B. alle 100 Stunden zur Reinigung des Geräts (insbesondere des Lüfters) auffordert.
Das Programm fragt einmal pro Schleife die beiden Taster ab. Diese Lösung ist gegenüber der Nutzung von Interrupts nicht die erste Wahl, weil der Arduino Nano jedoch nur zwei interruptfähige Pins hat (Pin 2 und Pin 3) und einer der beiden Pins (3) für die PWM-Ansteuerung des Peltier-Elements verwendet wird, fällt diese Variante aus. Die Taster sind entprellt und lösen erst beim Loslassen aus. Die Solltemperatur wird im EEPROM gespeichert.
Sowohl für den Betriebsstundenzähler als auch für die Speicherung der Solltemperatur müssen Variablen mit dem Dateityp float bzw. double im EEPROM gespeichert werden. Zu diesem Zweck wurde die Bibliothek EEPROMex eingebunden.
To dos:
- Es soll eine Sicherungsschaltung programmiert werden, die das Peltierelement ausschaltet, falls der Lüfter blockiert. Das erscheint mir sinnvoll, da vor dem Lüfter kein Schutzgitter montiert wird, um Luftgeräusche zu minimieren.
- Es soll zwischen verschiedenen Anzeigemodi gewechselt werden können, indem die Taster länger gedrückt werden.
// Flaschenkühler - Programmversion 0.2
// Diese Version steuert einen PC-Lüfter mit 4-Pin-Anschluss ...
// ... liest das Tachosignal aus ...
// ... berechnet die Drehzahl des Lüfters ...
// ... liest einen Thermistor aus und berechnet die Temperatur ...
// ... stellt über ein Poti die ZieltemperatureHotSidee ein ...
// ... regelt den Lüfter mit einem PID-Modul ...
// ... zeigt verschiedene Werte auf einem OLED-Display an ...
// ... zählt die Betriebsstunden (viertelstundengenau) und speichert sie im EEPROM ...
// ... pollt die Taster und speichert die Solltemperatur im EEPROM.
//------------------------- Eingebundene Bibliotheken ---------------------//
#include <Adafruit_GFX.h> // Grafik-Bibliothek für OLED-Display
#include <Adafruit_SSD1351.h> // Bibliothek für OLED-Display (Adafruit OLED Breakout Board 1.27")
#include <SPI.h> // Bibliothek Serial Peripheral Interface (SPI)
#include <PWM.h> // Bibliothek für Änderung der Frequenz der Timer
#include <RunningAverage.h> // Bibliothek für Berechnung von Mittelwerten
#include <PID_v1.h> // Bibliothek für PID-Regler
#include <EEPROMex.h> // Bibliothek für Lesen und Schreiben des EEPROMS
//------------------------- Definition der Inputs und Outputs ---------------------//
#define potiPin A0 // Input-Pin für den Lüfter
#define thermistorPin A1 // Input-Pin für den Thermistor
#define tachoPin 2 // Pin für Tachosignal des Lüfters
#define peltierPin 3 // PWM-Pin für Peltier-Element (hier zunächst nur als Funktionstest)
#define powerPin 4 // Schaltet den MOSFET für den Lüfter
#define dc 5 //
#define cs 6 // Chip Select
#define rst 7 // Reset
#define button1Pin 8 // Taster 1
#define fanPin 9 // PWM-Pin für Lüfter
#define button2Pin 12 // Taster 2
//------------------------- Definition der Farben ---------------------//
#define BLACK 0x0000
#define BLUE 0x001F
#define RED 0xF800
#define GREEN 0x07E0
#define CYAN 0x07FF
#define MAGENTA 0xF81F
#define YELLOW 0xFFE0
#define WHITE 0xFFFF
//------------------------- Definition der Variablen ---------------------//
int button1state = HIGH; // aktuelles Signal vom Tasterpin
int button1pressed = 0; // abfragen ob Taster gedrückt war
int button2state = HIGH; // aktuelles Signal vom Tasterpin
int button2pressed = 0; // abfragen ob Taster gedrückt war
int debouncetime = 5; // Zeit für Entprellung ggf. anpassen
unsigned long button1time = 0;
unsigned long button2time = 0;
float thermistorValue = 0; // Variable in der der Wert des Thermistors gespeichert wird
int potiValue = 0; // variable to store the value coming from the sensor
// PWM Frequenzen
uint16_t frequencyFan = 25000; // PWM-Frequenz für den Lüfter (in Hz)
uint16_t frequencyPeltier = 50000; // PWM-Frequenz für den Lüfter (in Hz)
// Lüfter
int tachoSignal; // Tachosignal
uint32_t pulseOn = 0; // Ansteigende Signalflanke (Variablenformat nicht verändern!)
uint32_t pulseOff = 0; // Abfallende Signalflanke (Variablenformat nicht verändern!)
uint32_t duration; // Zeit in Mikrosekunden zwischen ansteigender und abfallender Flanke (Variablenformat nicht verändern!)
bool high = false; // Statevariable
int rpm = 0; // Drehzahl des Lüfters in U/Min
float rpmAverage = 0; // Gemittelte Drehzahl des Lüfters in U/min
bool fanAlert = false; // Wird wahr, wenn der Lüfter blockiert ist
bool fanAlertState = true;
unsigned long fanAlertDelay = 2000;// Gibt die Verzögerung des Alarms "Lüfterfehlfunktion" in Millisekunden an
unsigned long previousMillis = 0;
// Temperaturen
float temperatureHotSide = 0;
// Betriebszeit
float operatingTime; // Betriebsstunden als Dezimalwert
long lastTime = 0;
// Adressen im EEPROM
int addrOperatingTime = 0; // Startadresse für eine Variable im Datentyp float (4 Byte!)
int addrTargetTemp = 4; // Startadresse für eine Variable im Datentyp double (8 Byte!)
// Definiert das OLED
Adafruit_SSD1351 tft = Adafruit_SSD1351(cs, dc, rst);
// Informationsanzeige
bool refreshPeltier = true;
bool refreshTargettemp = true;
//bool refreshActualtemp = true;
bool refreshFan = true;
// Instantiiert RunningAverage-Objekte zur Bechnung von Mittelwerten
RunningAverage averageRPM(25); // Mittelwert aus 25 Messungen
RunningAverage averageThermistor(10); // MIttelwert aus 10 Messungen
// Definiert den PID-Regler
double Setpoint, Input, Output;
double Kp=2, Ki=5, Kd=1;
PID fanPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, REVERSE);
// Definiert die Tracking-Variablen für die IF-Abfragen
unsigned long previousMillisCalculateTemperatures = 0;// Berechnung der Temperaturen
unsigned long previousMillisCalculateFanSpeed = 0; // Berechnung der Drehzahl des Lüfters
unsigned long previousMillisDisplayActualtemp = 0; // Ausgabe der Ist-Temperatur an das OLED-Display
unsigned long previousMillisDisplayFanSpeed = 0; // Ausgabe der gemittelten Lüfterdrehzahl an das OLED-Display
unsigned long previousMillisSerialPrint = 0; // Ausgabe an die serielle Schnittstelle
// Definiert die Intervalle für die IF-Abfragen in Millisekunden
const unsigned long intervalCalculateTemperatures = 10;
const unsigned long intervalCalculateFanSpeed = 100; // Berechnung der Drehzahl des Lüfters
const unsigned long intervalDisplayActualtemp = 500; // Ausgabe der Ist-Temperatur an das OLED-Display
const unsigned long intervalDisplayFanSpeed = 500; // Ausgabe der gemittelten Lüfterdrehzahl an das OLED-Display
const unsigned long intervalSerialPrint = 500; // Ausgabe an die serielle Schnittstelle
int loopCounter = 0;
//------------------------- Setup ---------------------//
void setup() {
Serial.begin(115200);
// Initialisiert das OLED-Dispaly
tft.begin();
// Definiert die Pins
pinMode(tachoPin, INPUT_PULLUP); // Tachosignal des Lüfters
pinMode(powerPin, OUTPUT); // Schaltet den MOSFET für den Lüfter
pinMode(fanPin, OUTPUT); // PWM-Signal für Lüfter
pinMode(peltierPin, OUTPUT); // PWM-Signal für Peltier-Element
pinMode(button1Pin, INPUT_PULLUP); // Intur Taster 1
pinMode(button2Pin, INPUT_PULLUP); // Input Taster 2
// Initialisiert timer1 und timer2 (timer0 bleibt unberührt)
InitTimersSafe();
// Definiert die Frequenzen für die angegebenen Pins
bool successFan = SetPinFrequencySafe(fanPin, frequencyFan);
bool successPeltier = SetPinFrequencySafe(peltierPin, frequencyPeltier);
/*
//if the pin frequency was set successfully, turn pin 13 on
if(successFan) {
pinMode(13, OUTPUT);
digitalWrite(13, HIGH);
Serial.print("PWM frequency PIN D9 and D10 is set to: "); Serial.println(frequencyFan);
}
if(successPeltier) {
Serial.print("PWM frequency PIN D3 and D11 is set to: "); Serial.println(frequencyPeltier);
}
*/
// Initialisiert die PID-Regler
fanPID.SetMode(AUTOMATIC);
// Liest die im EEPROM gespeicherten Variablen aus
operatingTime = EEPROM.readFloat(addrOperatingTime);
Setpoint = EEPROM.readDouble(addrTargetTemp);
// Stellt sicher, dass im ersten Durchlauf der Programmschleife nicht fälschlicherweise eine Lüfterfehlfunktion gemeldet wird
previousMillis = fanAlertDelay;
// Meldung "Klar zum Start!"
Serial.println("<Arduino is ready! Turn the potentiometer, please ...>");
Serial.print("<Operating Time of the System: "); Serial.print(operatingTime); Serial.println(" hours>");
// Startbildschirm
tft.fillScreen(BLACK);
tft.setCursor(25, 32);
tft.setTextColor(WHITE, BLACK);
tft.setTextSize(0);
tft.print("Flaschenk"); tft.print((char)154); tft.print("hler");
tft.setCursor(22, 60);
tft.print("Programmversion");
tft.setCursor(45, 75);
tft.setTextSize(2);
tft.print("0.2");
tft.setCursor(10, 108);
tft.setTextSize(0);
tft.print("Betriebsstunden:");
tft.setCursor(10, 120);
tft.print(operatingTime); tft.print(" Stunden");
delay(1000);
tft.fillScreen(BLACK);
}
//------------------------- Loop ---------------------//
void loop() {
// Aktuelle Zeit abfragen
unsigned long currentMillis = millis();
loopCounter++;
//------------------------- Abfrage der Taster ---------------------//
// Lesen und entprellen des Tasters
button1state = digitalRead(button1Pin);
button2state = digitalRead(button2Pin);
// Wenn der Taster 1 gedrückt ist...
if (button1state == LOW)
{
button1time = millis(); // aktualisiere tasterZeit
button1pressed = 1; // speichert, dass Taster gedrückt wurde
}
// Wenn die gewählte entprellZeit vergangen ist und der Taster gedrückt war...
if ((millis() - button1time > debouncetime) && button1pressed == 1) {
button1pressed = 0; // setzt gedrückten Taster zurück
Setpoint = Setpoint + 0.5;
EEPROM.updateDouble(addrTargetTemp, Setpoint);
Serial.print("Updated Setpoint: "); Serial.println(Setpoint);
refreshTargettemp = true;
if (Setpoint == 30) {
Setpoint = 30; // setzt die NeoPixel zurück
}
//Serial.print("Setpoint: "); Serial.print(Setpoint);
}
// Wenn der Taster 2 gedrückt ist...
if (button2state == LOW)
{
button2time = millis(); // aktualisiere tasterZeit
button2pressed = 1; // speichert, dass Taster gedrückt wurde
}
// Wenn die gewählte entprellZeit vergangen ist und der Taster gedrückt war...
if ((millis() - button2time > debouncetime) && button2pressed == 1) {
button2pressed = 0; // setzt gedrückten Taster zurück
Setpoint = Setpoint - 0.5;
EEPROM.updateDouble(addrTargetTemp, Setpoint);
Serial.print("Updated Setpoint: "); Serial.println(Setpoint);
refreshTargettemp = true;
if (Setpoint == 0) {
Setpoint = 0; // setzt die NeoPixel zurück
}
//Serial.print("Setpoint: "); Serial.print(Setpoint);
}
//------------------------- Auslesen der Thermistoren und Berechnung der Temperaturen ---------------------//
if ((unsigned long)(currentMillis - previousMillisCalculateTemperatures) >= intervalCalculateTemperatures) {
// Lese die analogen Inputs aus
potiValue = analogRead(potiPin); // Poti
thermistorValue = analogRead(thermistorPin); // Thermistor
averageThermistor.addValue(thermistorValue); // Wert wird an Running Average übergeben
// Berechnung der Temperatur
thermistorValue = averageThermistor.getAverage(); // Der Mittelwert wird eingelesen
thermistorValue = 1023 / thermistorValue - 1;
thermistorValue = 10000 / thermistorValue; // Der Wert wird in einen Widerstand umgerechnet
//float temperatureHotSide;
temperatureHotSide = thermistorValue / 10000; // (R/Ro)
temperatureHotSide = log(temperatureHotSide); // ln(R/Ro)
temperatureHotSide /= 3950; // 1/B * ln(R/Ro)
temperatureHotSide += 1.0 / (25 + 273.15); // + (1/To)
temperatureHotSide = 1.0 / temperatureHotSide; // Invert
temperatureHotSide -= 273.15; // convert to C
previousMillisCalculateTemperatures = currentMillis;
}
//------------------------- Regelung des Lüfters (TEST) ---------------------//
Input = temperatureHotSide; // Input ist die temperatureHotSide des Thermistors in *C
fanPID.Compute(); // PID-Regler wir aufgerufen
pwmWrite(fanPin, Output); // Gibt den Output des PID-Reglers an den Lüfter
// Wenn der Output des PID-Reglers Null ist, wird der Lüfter ausgeschaltet.
if (Output > 0) {
digitalWrite(powerPin, HIGH);
//Serial.print("HIGH"); Serial.print("; ");
}
else {
digitalWrite(powerPin, LOW);
//Serial.print("LOW"); Serial.print("; ");
}
//use this functions instead of analogWrite on 'initialized' pins
pwmWrite(peltierPin, potiValue / 4); // Nur zu Testzwecken
//------------------------- Tachosignal und Drehzahl des Lüfters ---------------------//
// Messung der Pulsweite des Tachosignals
tachoSignal = digitalRead(tachoPin);
if (tachoSignal == HIGH && high != true) { // Zeit in micros bei ansteigender Flanke
pulseOn = micros();
high = true;
}
else if (tachoSignal == LOW && high == true) {
pulseOff = micros(); // Zeit in micros bei fallender Flanke
high = false;
duration = pulseOff - pulseOn; // Aus der Differenz wir die Dauer berechnet, die das Tachosignal HIGH ist
if (duration > 7000 && duration < 150000) { // Liegt die Variable über bzw. unter den angegebenen Werten, liegt ein Messfehler vor
rpm = float(100000 * 2 * 60 / duration); // Berechnung der RPM
}
else {
rpm = 0;
}
//averageRPM.addValue(rpm);
}
// Berechnung der gemittelten Drehzahl des Lüfters
if ((unsigned long)(currentMillis - previousMillisCalculateFanSpeed) >= intervalCalculateFanSpeed) {
averageRPM.addValue(rpm); // Aktualisiert das RunningAverage-Objekt
// Wenn der Lüfter angehalten wird, soll die Drehzahl "0" angezeigt werden
if (Output == 0) {
rpm = 0;
averageRPM.addValue(rpm);
}
// Liest die durchschnittliche Drehzahl aus und speichert das Ergebnis in eine Variable
rpmAverage = averageRPM.getAverage();
// Meldet eine Fehlfunktion des Lüfters, wenn das Tachosignal "0" ist, obwohl der Lüfter sich drehen sollte
if (Output > 0) { // Wenn der Lüfter sich drehen sollte ...
if (rpmAverage == 0) { // ... aber die gemittelte Drehzahl gleich "0" ist ...
if (millis() - previousMillis > fanAlertDelay) { // ... und eine definierte Zeit verstrichen ist.
previousMillis = millis();
fanAlert = true;
fanAlertState = true;
Serial.println("Fan blocked!");
refreshFan = true;
}
}
else if (rpmAverage > 0) { // ... und die gemittelte Drehzahl größer "0" ist ...
fanAlert = false;
}
}
else if (Output == 0) { // Wenn der Lüfter sich nicht drehen soll kann nicht festgestellt werden, ob er blockiert ist.
fanAlert = false;
previousMillis = millis(); // Muss aktualisiert werden, damit
if (fanAlert != fanAlertState) {
refreshFan = true;
fanAlertState = false;
}
}
previousMillisCalculateFanSpeed = currentMillis;
}
//------------------------- Betriebsstunden ---------------------//
// Die Betriebsstunden werden alle 15 Minuten im EEPROM gespeichert. Die Einheit der Variable operatinTime ist also 0,25 Stunden.
if (millis() - lastTime >= 900000) {
operatingTime = operatingTime + 0.25;
lastTime = millis();
EEPROM.updateFloat(addrOperatingTime, operatingTime);
Serial.print("Updated operatingTime: "); Serial.println(operatingTime);
}
//------------------------- Ausgabe an das Display ---------------------//
// Anzeige Infobereich Peltier-Element
if (refreshPeltier == true) {
tft.setCursor(0, 32);
tft.setTextColor(WHITE, BLACK);
tft.setTextSize(0);
tft.print("Infos Peltier-Element");
refreshPeltier = false;
}
// Anzeige Infobereich Zieltemperatur
if (refreshTargettemp == true) {
tft.setCursor(15, 49);
tft.setTextColor(YELLOW, BLACK);
tft.setTextSize(0);
tft.print("Solltemperatur");
tft.setCursor(15, 62);
tft.setTextSize(2);
if (Setpoint < 10.00) {
tft.print("0");
}
tft.print(Setpoint); tft.print(" "); tft.print((char)247); tft.print("C");
tft.setTextColor(BLACK, BLACK);
tft.print((char)218);
refreshTargettemp = false;
}
// Anzeige Infobereich Isttemperatur
if ((unsigned long)(currentMillis - previousMillisDisplayActualtemp) >= intervalSerialPrint) {
tft.setCursor(15, 85);
tft.setTextColor(RED, BLACK);
tft.setTextSize(0);
tft.print("Isttemperatur");
tft.setCursor(15, 98);
tft.setTextSize(2);
if (temperatureHotSide < 10.00) {
tft.print("0");
}
tft.print(temperatureHotSide); tft.print(" "); tft.print((char)247); tft.print("C");
tft.setTextColor(BLACK, BLACK);
tft.print((char)218);
//refreshActualtemp = false;
previousMillisDisplayActualtemp = currentMillis;
}
// Anzeige Infobereich Lüfter
if (fanAlert == true) {
if (refreshFan == true) {
tft.setCursor(0, 120);
tft.setTextSize(0);
tft.setTextColor(RED, BLACK);
tft.print("L"); tft.print((char)154); tft.print("fterfehlfunktion!");
refreshFan = false;
}
}
else if (fanAlert == false) {
if (rpmAverage > 0) { // Wenn die Drehzahl größer "0" ist, aktualisiere den Anzeigebereich all 500 Millisekunden
if ((unsigned long)(currentMillis - previousMillisDisplayFanSpeed) >= intervalDisplayFanSpeed) {
tft.setCursor(0, 120);
tft.setTextSize(0);
tft.setTextColor(WHITE, BLACK);
tft.print("L"); tft.print((char)154); tft.print("fter "); tft.print(int(rpmAverage)); tft.print(" U/min");
tft.setTextColor(BLACK, BLACK);
for (int i=0; i<5; i++) { // Füllt die Zeile mit schwarzen Kästchen; i muss ggf. angepasst werden
tft.print((char)218);
}
refreshFan = true;
previousMillisDisplayFanSpeed = currentMillis;
}
}
else if (rpmAverage == 0) {
if (refreshFan == true) {
tft.setCursor(0, 120);
tft.setTextSize(0);
tft.setTextColor(WHITE, BLACK);
tft.print("L"); tft.print((char)154); tft.print("fter aus");
tft.setTextColor(BLACK, BLACK);
for (int i=0; i<12; i++) { // Füllt die Zeile mit schwarzen Kästchen; i muss ggf. angepasst werden
tft.print((char)218);
}
refreshFan = false;
}
}
}
//------------------------- Ausgabe an die serielle Schnittstelle ---------------------//
if ((unsigned long)(currentMillis - previousMillisSerialPrint) >= intervalSerialPrint) {
Serial.print("Setpoint: ");Serial.print(Setpoint);
Serial.print("; Input: ");Serial.print(Input);
Serial.print("; Output: ");Serial.print(Output);
//Serial.print("; pulseOn: ");Serial.print(pulseOn);
//Serial.print("; pulseOff: ");Serial.print(pulseOff);
//Serial.print("; Duration: "); Serial.print(duration);
Serial.print("; RPM: "); Serial.print(rpm);
Serial.print("; average RPM: "); Serial.print(rpmAverage);
Serial.print("; fanAlert: "); Serial.print(fanAlert);
//Serial.print("; temperatureHotSide: "); Serial.println(temperatureHotSide);
Serial.print("; operatingTime: "); Serial.print(operatingTime);
Serial.print("; Loops: "); Serial.println(loopCounter);
loopCounter = 0;
//Speichere die aktuelle Zeit in die zughörige Variable
previousMillisSerialPrint = currentMillis;
}
}
Der Sketch verwendet 19020 Bytes (61%) des Programmspeicherplatzes. Das Maximum sind 30720 Bytes. Globale Variablen verwenden 1054 Bytes (51%) des dynamischen Speichers, 994 Bytes für lokale Variablen verbleiben. Das Maximum sind 2048 Bytes.
Tags: #Arduino #Flaschenkühler #Nano #OLED