Slow PWM - Pseudo PID output for thermostat

Guy's

I'm new to coding for Arduino, but currently having fun developing a product I use with my reptiles, and need some advice.

In 2009 with the help ( a lot of help) of some friends on the PicBASIC Pro forum I developed a vivarium controller that was basically four independent pulse proportional thermostats that use DS18B20's to monitor and control the four outputs that drive ceramic heaters via SSR's. The code uses an interrupt driven PID routines, and has a frequency of around 0.75 Hz. All four outputs come on at the same time, but are turned off based on the individual "duty cycle" of that pulse within the frequency period. The "duty cycle" (or PID period) is based on difference between the set temperature and the real temperature as read by the DS18B20, so when cold the "duty cycle" is 100% and the output is constantly on, then as the real temperature gets closer to the set temperature the duty cycle is reduced. If it overshoots the duty cycle is 0% and thus the heater is turned off. When tends to happen is that it then becomes stable and the duty cycle averages out around 50% to provide a constant heat at the set temperature (or within 0.5C of it).

I wanted to play around with a TFT screen, and regretfully this isn't directly supported in PBP, so after a lot of badgering from a friend I agreed to use a mega to play with. Over the past couple of weeks it's now developed into the basis of a MKV version of this project. So far we've got the screen layout right, we have it reading four individual DS18B20 on their own pins, and have four DHT11's on their own pins, with all the temps and humidity displayed on the 2.8" TFT.

So now we're at the stage where we need to look at the heater control. I came across a website which allows a pwm pin to have a low frequency and a simple way of setting the duty cycle Slow PWM. However it uses a delay statement which is not not ideal.

So is there a simple way (I say simple - I'm still learning the syntax and format of C++) to obtain an interrupt driven PWM routine that will replicate the existing controllers output, and will run in the background whilst the main lop keeps the data on the screen up to date ?

Thanks

Malcolm

Malc-C:
four individual DS18B20 on their own pins

Why not a single pin for all of them? They're designed to be used like that. That's the beauty of this One Wire bus.

So is there a simple way (I say simple - I'm still learning the syntax and format of C++) to obtain an interrupt driven PWM routine

What's the interrupt for?

analogWrite() produces a PWM output.

Not as slow as yours (default is 1000 Hz or so - this can be changed) but for a heater the actual frequency normally doesn't matter.

Set up the PID library - provide it the setpoint and the actual temperature, and it outputs a value that you in turn use to set your PWM output.

If you really want arduinos PWM, U'll have to scale the timer(s) for <1Hz or so.

It's not clear to me that with your application you can not use millis() based software timers which run in the background. You are correct to avoid delay, but there are techniques for software pwm which will not block the rest of the program.

Take a look at the basic example "blink without delay" and the tutorial on millis() based timers in
"How to do several things at the same time" Demonstration code for several things at the same time - Project Guidance - Arduino Forum and "Using millis() for timing" https://forum.arduino.cc/index.php?topic=503368.0

For software-based slow PWM, using a timer interrupt would make most sense to me. It's much more precise than millis() based timing, which is detected some time after the period has passed, while interrupts fire at the actually desired moment which is what you want for PWM.

If you only need accuracy of several milliseconds then loop() ought to be enough to poll for time instants,
as nothing should normally be hogging the processor.

Thanks for the replies, most of which went right over my head !

To try and answer a couple of questions raised. I've used individual pins is to prevent reordering of the sensors based on their address. If I needed to replace a sensor at some stage in the future I would need to find its address, then reprogram the mega with the address. It could also mean a rewire of the sensors location if the address didn't fit in the same location as the faulty one. Having individual pins means that I didn't need to hard code the addresses and error trapping for a fault was simpler.

Here's a video of the previous update to the controller in 2015 Vivarium Controller update - YouTube - it isn't the final build as I used a different controller for the GLCD. Details here http://micro-heli.co.uk/Thermostat2.htm

I'll take a look at the PID library suggested.

Thanks

Malcolm

I'll take a look at the PID library suggested.

The Arduino pid library PID_v1.h includes an example for relay control which manages an on/off period in a time window. Your window size will be 750ms.

Your challenge will be creating four instances of the controller and using arrays to pass values in and out.

/********************************************************
   PID RelayOutput Example
   Same as basic example, except that this time, the output
   is going to a digital pin which (we presume) is controlling
   a relay.  The pid is designed to output an analog value,
   but the relay can only be On/Off.

     To connect them together we use "time proportioning
   control"  Tt's essentially a really slow version of PWM.
   First we decide on a window size (5000mS say.) We then
   set the pid to adjust its output between 0 and that window
   size.  Lastly, we add some logic that translates the PID
   output into "Relay On Time" with the remainder of the
   window being "Relay Off Time"
 ********************************************************/

#include <PID_v1.h>
#define RelayPin 6

//Define Variables we'll be connecting to
double Setpoint, Input, Output;

//Specify the links and initial tuning parameters
PID myPID(&Input, &Output, &Setpoint, 2, 5, 1, DIRECT);

int WindowSize = 5000;
unsigned long windowStartTime;
void setup()
{
  pinMode(RelayPin, OUTPUT);

  windowStartTime = millis();

  //initialize the variables we're linked to
  Setpoint = 100;

  //tell the PID to range between 0 and the full window size
  myPID.SetOutputLimits(0, WindowSize);

  //turn the PID on
  myPID.SetMode(AUTOMATIC);
}

void loop()
{
  Input = analogRead(0);
  myPID.Compute();

  /************************************************
     turn the output pin on/off based on pid output
   ************************************************/
  unsigned long now = millis();
  if (now - windowStartTime > WindowSize)
  { //time to shift the Relay Window
    windowStartTime += WindowSize;
  }
  if (Output > now - windowStartTime) digitalWrite(RelayPin, HIGH);
  else digitalWrite(RelayPin, LOW);
}

Many thanks for the details. It will give me (well my friend) something to work with. One thing that is puzzling me is how to get the four instances to start at the same time rather than sequentially as per the video.

Steep learning curve :slight_smile:

Malcolm

I've been testing various options and for my application using a 150w ceramic heater found the low frequency PWM to work the best. What I need now is a simple means without loads of IF statements of varying the duty cycle between 1 and 0, with .5 = the setpoint.

EG:
if the actual temperature is 2C or more lower than the set point the duty cycle = 1. If the actual temperature equals the set temperature the duty cycle = 0.5. If the actual temperature is 2C or more than the set point then the duty cycle =0. But it needs to be some formula that fills in the gaps in between so there is better resolution.

The actual variables etc are:

float dutyCycle1 = .15; // Represents a duty cycle of 15%
byte setpoint1 = 30;
float temperature1;

The current check routine is

 if (dutyCycle1 > 0.0) {
    digitalWrite(output1, HIGH);
  }
  else {
    digitalWrite(output1, LOW);
  }
  int on = frequency * dutyCycle1;
  int off = frequency - on;
  delay(on);
  
  if (off > 0) {
    digitalWrite(output1, LOW);
    delay(off);
  }

Any suggestions ?

TIA

Malcolm

Here is some sample code implementing a non-blocking slowPWM. It will give you some ideas.
There is linear variation of the duty cycle from 0 to 100% through a four degree proportional band with 50% duty cycle at the set point (mid point of band). The constrain() and map() functions manage the high and low limits and the proportional values.
I assume that the temperature values are to tenths of a degree, but work with them in the program with a 10x multiplier to avoid floating point operations.
I have used a five second period for the pwm to help visualize the led flash, but it can be set to your 750ms.
I would think that you would want to take your temperature readings at some timed interval, and pass a new value to the slow PWM if the temperature changes.

//all values *10 to convert one decimal point temperature readings to integers
//measured temperatures from sensor in tenths of degree
float tempSensorReading;
int workingTemperature = tempSensorReading *10;
int setPoint = 30.0 * 10;
int difference;
byte dutyCycle;
// four degree proportional band 100% to 0% duty cycle linear transition
//use even number for symmetry around setPoint
byte proportionalBand = 4 * 10; 

void setup()
{}
void loop()
{
  tempSensorReading = 30.0;//some temperature from sensor in tenths of degree
  workingTemperature = 10 * tempSensorReading;
  difference = setPoint - workingTemperature;
  difference = constrain(difference, -20, 20);
  dutyCycle = map(difference, -20, 20, 0, 100);

  //call slowPWM function with linear proportional dutyCycle
  slowPWM(dutyCycle, 5000); // %duty cycle, period milliseconds
}

void slowPWM(byte dutyCycle, unsigned long period)
{
  const byte outputPin = 13;//  LED pin for visualization
  pinMode(outputPin, OUTPUT);
  static byte outputState = LOW;
  static unsigned long lastSwitchTime = 0;

  unsigned long onTime = (dutyCycle * period) / 100;
  unsigned long offTime = period - onTime;

  unsigned long currentTime = millis();

  if (outputState == HIGH && (currentTime - lastSwitchTime >= onTime))
  {
    lastSwitchTime = currentTime;
    outputState = LOW;
  }
  if (outputState == LOW && (currentTime - lastSwitchTime >= offTime))
  {
    lastSwitchTime = currentTime;
    outputState = HIGH;
  }
  digitalWrite(outputPin, outputState);
}

EDIT: There are still some "magic numbers" in the program with map() and constrain() which should be made more general and calculated from the proportional band.

Many thanks, I'll have a play