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 ?
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.
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.
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.
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);
}
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.