CYTHEIA AIOpen the editor

Greenhouse Climate Controller

A complete controller: it senses, decides and acts at once, using non-blocking timers, hysteresis, proportional fan speed, an interrupt-driven mode button and a live display.

Open this circuit and run it

Opens in your browser with the parts, wiring and code already in place. Press Run to watch it work, then change anything you like.

How it works

This is a complete controller, bringing together most of what the other templates teach. A temperature sensor (A0) and a potentiometer (A1, the target temperature, from 15 to 35 °C) are the inputs, and a button on pin 2 changes mode. The outputs are an LED on pin 13 standing in for a heater, a DC motor on pin 5 as a fan, and an OLED that shows the state. As on a real board, the fan would really need a transistor or driver rather than being wired straight to the pin.

Nothing uses delay(). Instead loop() looks at millis() and runs three jobs on their own timers: every 100 milliseconds it senses and decides, every 250 it redraws the OLED, and every 2 seconds it prints a report. That keeps the controller responsive to the button and the serial port the whole time. Deciding uses two ideas. Hysteresis: the heater turns on only when the temperature is more than 1 degree below the target and off only when it is more than 1 degree above, so it does not flick on and off right at the target. And a proportional fan: idle until half a degree over target, then a PWM speed that starts near 90 and rises with each extra degree up to 255.

The Mode button uses an interrupt (see the Interrupt Button Counter) to cycle AUTO, COOL ONLY and OFF immediately, ignoring bounces within 200 milliseconds. The sensor is read eight times and averaged to smooth out noise. Typing ? in the serial monitor prints a status line on demand.

Parts

Wiring

Arduino code

// Greenhouse Climate Controller
// The knob sets the target temperature. Below it the heater LED switches on;
// above it the fan spins faster the hotter it gets. The Mode button cycles
// AUTO -> COOL ONLY -> OFF. Drag the temperature slider while it runs to
// heat or chill the greenhouse. Type ? in the serial monitor for a status line.
//
// Nothing here uses delay(): three timers run side by side, so the controller
// keeps reacting to the button and the serial port while it waits.
#include <Wire.h>
#include <Adafruit_SSD1306.h>

#define TEMP_PIN A0
#define SETPOINT_PIN A1
#define BUTTON_PIN 2
#define FAN_PIN 5
#define HEATER_PIN 13

#define MODE_AUTO 0
#define MODE_COOL_ONLY 1
#define MODE_OFF 2

Adafruit_SSD1306 display(128, 64, &Wire, -1);

const unsigned long CONTROL_EVERY = 100;   // sense and decide
const unsigned long DISPLAY_EVERY = 250;   // redraw the OLED
const unsigned long REPORT_EVERY = 2000;   // log to the serial monitor
const float HYSTERESIS = 1.0;              // degrees of dead band around the target

volatile int mode = MODE_AUTO;
volatile unsigned long lastPress = 0;

unsigned long lastControl = 0;
unsigned long lastDisplay = 0;
unsigned long lastReport = 0;

float tempC = 0;
float setpointC = 25;
bool heaterOn = false;
int fanSpeed = 0;

// Runs the instant the button goes HIGH, even in the middle of other work
void onModeButton() {
  unsigned long now = millis();
  if (lastPress == 0 || now - lastPress > 200) {   // ignore contact bounce
    mode = (mode + 1) % 3;
    lastPress = now;
  }
}

// The sensor outputs 10 mV per degree. Averaging a few readings smooths noise.
float readTemperature() {
  long total = 0;
  for (int i = 0; i < 8; i++) {
    total = total + analogRead(TEMP_PIN);
  }
  float voltage = (total / 8.0) * 5.0 / 1024.0;
  return voltage * 100.0;
}

// The knob covers 15 to 35 degrees
float readSetpoint() {
  return 15.0 + analogRead(SETPOINT_PIN) * 20.0 / 1023.0;
}

void control() {
  if (mode == MODE_OFF) {
    heaterOn = false;
    fanSpeed = 0;
  } else {
    // Hysteresis: only change the heater once the temperature has moved
    // clearly past the target, so it doesn't chatter on and off at the line.
    if (mode == MODE_AUTO) {
      if (tempC < setpointC - HYSTERESIS) {
        heaterOn = true;
      } else if (tempC > setpointC + HYSTERESIS) {
        heaterOn = false;
      }
    } else {
      heaterOn = false;
    }

    // Proportional fan: idle until half a degree over, then 90 to 255 PWM
    float over = tempC - setpointC;
    if (over > 0.5) {
      fanSpeed = constrain(round(90 + (over - 0.5) * 33), 90, 255);
    } else {
      fanSpeed = 0;
    }
  }
  digitalWrite(HEATER_PIN, heaterOn ? HIGH : LOW);
  analogWrite(FAN_PIN, fanSpeed);
}

void drawDisplay() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print("Temp:   ");
  display.print(tempC, 1);
  display.println(" C");
  display.print("Target: ");
  display.print(setpointC, 1);
  display.println(" C");
  display.print("Mode:   ");
  if (mode == MODE_AUTO) display.println("AUTO");
  else if (mode == MODE_COOL_ONLY) display.println("COOL ONLY");
  else display.println("OFF");
  display.print("Heater: ");
  display.println(heaterOn ? "ON" : "off");
  display.print("Fan:    ");
  display.println(fanSpeed);
  display.display();
}

void report() {
  Serial.print("T=");
  Serial.print(tempC, 1);
  Serial.print("C target=");
  Serial.print(setpointC, 1);
  Serial.print("C mode=");
  Serial.print(mode);
  Serial.print(" heater=");
  Serial.print(heaterOn ? "ON" : "off");
  Serial.print(" fan=");
  Serial.println(fanSpeed);
}

void setup() {
  Serial.begin(9600);
  pinMode(BUTTON_PIN, INPUT);
  pinMode(FAN_PIN, OUTPUT);
  pinMode(HEATER_PIN, OUTPUT);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onModeButton, RISING);
  Serial.println("Greenhouse controller ready. Type ? for status.");
}

void loop() {
  unsigned long now = millis();

  if (now - lastControl >= CONTROL_EVERY) {
    lastControl = now;
    tempC = readTemperature();
    setpointC = readSetpoint();
    control();
  }
  if (now - lastDisplay >= DISPLAY_EVERY) {
    lastDisplay = now;
    drawDisplay();
  }
  if (now - lastReport >= REPORT_EVERY) {
    lastReport = now;
    report();
  }
  if (Serial.available() > 0) {
    char c = Serial.read();
    if (c == '?') report();
  }
}

Try changing it

Open this circuit and run it

More projects in Sensors