Okay. Here are the files.
Controller.ino is my main file.
Heater.h and Heater.cpp read the temperature and adjust the PWM to the heater (this is where I
think I need to write the temperature to the OLED).
Thermistor.h and Thermistor.cpp have a lookup table to convert the analog signal to a temperature.
PID.h and PID.cpp control how much to change the PWM output based on setpoint vs actual.
My approach was to "initialize" the OLED in setup(); (in my main program), and then write
the "current temperature" to the OLED from the Heater.cpp.
Thanks for being willing to look!
Main file (controller.ino)
#include <Bounce2.h>
/*
* Temperature control software for a Reflow Soldering Oven
* This firmware can be used on a ATtiny 45 µController
* For schematics of the hardware, please see
* http://www.pleasantsoftware.com/developer/3d/reflow
*
* (c) 2011 Eberhard Rensch, Pleasant Software
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#include <TinyWireM.h>
#include <Tiny4kOLED.h>
#include "Thermistor.h"
#include "Heater.h"
// Use these Pins on a ATtiny
uint8_t sensorPin = 3;
uint8_t heaterPin = 4;
uint8_t ledPin = 1;
uint8_t buttonPin= 6;
// These pins might be used for debugging with an Arduino instead
// uint8_t sensorPin = 3;
// uint8_t heaterPin = 2;
// uint8_t ledPin = 3;
// uint8_t buttonPin= 4;
Thermistor thermistor(sensorPin, 0);
Heater heater;
Bounce button = Bounce(buttonPin, 10);
void setup() {
// Send the initialization sequence to the oled. This leaves the display turned off
oled.begin();
// Two rotations are supported,
// The begin() method sets the rotation to 1.
//oled.setRotation(0);
// Some newer devices do not contain an external current reference.
// Older devices may also support using the internal curret reference,
// which provides more consistent brightness across devices.
// The internal current reference can be configured as either low current, or high current.
// Using true as the parameter value choses the high current internal current reference,
// resulting in a brighter display, and a more effective contrast setting.
//oled.setInternalIref(true);
// Two fonts are supplied with this library, FONT8X16 and FONT6X8
// Other fonts are available from the TinyOLED-Fonts library
oled.setFont(FONT8X16);
// Clear the memory before turning on the display
oled.clear();
// Turn on the display
oled.on();
// Switch the half of RAM that we are writing to, to be the half that is non currently displayed
oled.switchRenderFrame();
#if !defined(__AVR_ATtiny85__)
Serial.begin(115200);
Serial.println("Init");
#endif
pinMode(heaterPin, OUTPUT);
pinMode(ledPin, OUTPUT);
pinMode(buttonPin, INPUT);
digitalWrite(buttonPin, HIGH);
digitalWrite(heaterPin, LOW);
heater.set_target_temperature(0);
// Signal ready for duty
// for(uint8_t i=0;i<5;i++) {
// delay(100);
// digitalWrite(ledPin,HIGH);
// delay(150);
// digitalWrite(ledPin,LOW);
// }
}
uint8_t phase = 0;
uint8_t program = 0;
boolean running = false;
boolean programFinished = false;
uint8_t ledState=0;
uint8_t ledPhase = 0;
uint32_t blinkTimer=0L;
uint32_t tempManagerTimer=0L;
uint32_t currentTime=0L;
uint32_t nextPhaseChange=0;
boolean buttonHandled=true;
void loop()
{
boolean buttonChanged = button.update();
currentTime = millis();
if(running)
{
if(blinkTimer<currentTime)
{
ledPhase+=ledState;
ledState = 1-ledState;
digitalWrite(ledPin, ledState);
blinkTimer=currentTime+100UL;
if(ledPhase>(phase-1)/2) {
blinkTimer+=500UL;
ledPhase = 0;
}
}
if(tempManagerTimer<currentTime)
{
tempManagerTimer = currentTime+500UL;
heater.manage_temperature();
#if !defined(__AVR_ATtiny85__)
Serial.print("Phase ");
Serial.print(phase);
Serial.print(" SollTemp: ");
Serial.print(heater.get_target_temperature(),DEC);
Serial.print(" IstTemp: ");
Serial.print(thermistor.getTemperature(),DEC);
Serial.print(" nextTime: ");
Serial.print(nextPhaseChange/1000L,DEC);
Serial.print(" current Time: ");
Serial.println(currentTime/1000L,DEC);
#endif
}
if(buttonChanged && button.read()==LOW) {
heater.set_target_temperature(0);
digitalWrite(heaterPin, LOW);
digitalWrite(ledPin, LOW);
#if !defined(__AVR_ATtiny85__)
Serial.print("Program Abort");
#endif
running=false;
}
else {
if( (nextPhaseChange!=-1UL && nextPhaseChange<currentTime) || (nextPhaseChange==-1UL && heater.hasReachedTargetTemperature()) )
{
#if !defined(__AVR_ATtiny85__)
Serial.print("PhaseChange to ");
Serial.println(phase, DEC);
#endif
switch(phase++)
{
case 0:
heater.set_target_temperature(150);
nextPhaseChange = -1UL;
break;
case 1:
nextPhaseChange = currentTime + 40000L; // Hold Temperature for 40 Seconds
break;
case 2:
heater.set_target_temperature(190);
nextPhaseChange = -1UL;
break;
case 3:
nextPhaseChange = currentTime + 10000L; // Hold Temperature for 10 Seconds
break;
case 4:
heater.set_target_temperature(245);
nextPhaseChange = -1UL;
break;
case 5:
nextPhaseChange = currentTime + 10000L; // Hold Temperature for 20 Seconds
break;
case 10: // Always on
heater.set_target_temperature(500);
nextPhaseChange = -1UL; // Hold Temperature for 10 Seconds
// This phase never ends (500°C are never reached in a usual toaster oven!)
break;
case 20: // One hour 110°C
heater.set_target_temperature(110);
nextPhaseChange = currentTime + 3600000UL; // Hold Temperature for 1 hour
break;
default:
heater.set_target_temperature(0);
programFinished = true;
running=false;
#if !defined(__AVR_ATtiny85__)
Serial.println("Program End");
#endif
digitalWrite(heaterPin, LOW);
digitalWrite(ledPin, HIGH);
break;
}
}
}
}
else {
if(tempManagerTimer<currentTime)
{
tempManagerTimer = currentTime+1000L;
thermistor.update();
#if !defined(__AVR_ATtiny85__)
Serial.print("IstTemp: ");
Serial.println(thermistor.getTemperature());
#endif
if(programFinished && thermistor.getTemperature()<55)
resetProgram();
}
if( button.read()==LOW ) {
if(button.duration() > 1000) {
running = true;
programFinished = false;
switch(program)
{
case 0: phase=0;
break;
case 1: phase=20;
break;
case 2: phase=10;
break;
}
//phase = (program==0)?0:10; // 10 -> Always on!
nextPhaseChange = 0L;
buttonHandled=true;
#if !defined(__AVR_ATtiny85__)
Serial.println("Start Program");
#endif
}
else if(buttonChanged && programFinished)
{
resetProgram();
buttonHandled=true;
}
}
if(buttonChanged && button.read()==HIGH)
{
if(!buttonHandled) {
program = (program+1)%3;
for(uint8_t i=0;i<=program;i++)
{
delay(200);
digitalWrite(ledPin,HIGH);
delay(250);
digitalWrite(ledPin,LOW);
}
#if !defined(__AVR_ATtiny85__)
Serial.println("Change Program");
#endif
}
buttonHandled=false;
}
}
}
void resetProgram()
{
programFinished = false;
// digitalWrite(ledPin, LOW);
#if !defined(__AVR_ATtiny85__)
Serial.println("Reset Program");
#endif
}
Heater.h
/*
* 2011 Eberhard Rensch, Pleasant Software
*
* Based on parts of the Makerbot firmware
* Copyright 2010 by Adam Mayer <adam@makerbot.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#ifndef HEATER_H
#define HEATER_H
#include "Thermistor.h"
#include "PID.h"
class Heater
{
private:
int current_temperature;
PID pid;
public:
Heater();
void set_target_temperature(int temp);
int get_target_temperature();
bool hasReachedTargetTemperature();
// Call once each temperature interval
void manage_temperature();
// Reset to board-on state
void reset();
};
#endif // HEATER_H
Heater.cpp
type /*
* 2011 Eberhard Rensch, Pleasant Software
*
* Based on parts of the Makerbot firmware
* Copyright 2010 by Adam Mayer <adam@makerbot.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#include "Heater.h"
#include "Thermistor.h"
//#include "WProgram.h"
#include "Arduino.h"
#include <TinyWireM.h>
//#include <Tiny4kOLED.h>
// Offset to compensate for range clipping and bleed-off
#define HEATER_OFFSET_ADJUSTMENT 0
extern int heaterPin;
extern Thermistor thermistor;
Heater::Heater()
{
reset();
}
void Heater::reset() {
current_temperature = 0;
}
void Heater::set_target_temperature(int temp)
{
pid.setTarget(temp);
}
int Heater::get_target_temperature()
{
return pid.getTarget();
}
// We now define target hysteresis in absolute degrees. The original
// implementation (+/-5%) was giving us swings of 10% in either direction
// *before* any artifacts of process instability came in.
#define TARGET_HYSTERESIS 2
bool Heater::hasReachedTargetTemperature()
{
return (current_temperature >= (pid.getTarget() - TARGET_HYSTERESIS)) &&
(current_temperature <= (pid.getTarget() + TARGET_HYSTERESIS));
}
/**
* Samples the temperature and converts it to degrees celsius.
* Returns degrees celsius.
*/
/*!
Manages motor and heater based on measured temperature:
o If temp is too low, don't start the motor
o Adjust the heater power to keep the temperature at the target
*/
void Heater::manage_temperature()
{
thermistor.update();
// update the temperature reading.
current_temperature = thermistor.getTemperature();
oled.print(current_temperature);
int mv = pid.calculate(current_temperature);
// offset value to compensate for heat bleed-off.
// There are probably more elegant ways to do this,
// but this works pretty well.
mv += HEATER_OFFSET_ADJUSTMENT;
// clamp value
if (mv < 0) { mv = 0; }
if (mv >255) { mv = 255; }
digitalWrite(heaterPin,(mv>0));
}
Thermistor.h
/*
* 2011 Eberhard Rensch, Pleasant Software
*
* Based on parts of the Makerbot firmware
* Copyright 2010 by Adam Mayer <adam@makerbot.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#ifndef THERMISTOR_HH_
#define THERMISTOR_HH_
#include <stdint.h>
#define THERM_TABLE_SIZE 20
#define SAMPLE_COUNT 4
struct ThermTableEntry {
int16_t adc;
int16_t celsius;
} __attribute__ ((packed));
class Thermistor {
private:
uint16_t current_temp;
uint8_t analog_pin; // index of analog pin
const static int ADC_RANGE = 1024;
int16_t sample_buffer[SAMPLE_COUNT];
uint8_t next_sample;
const uint8_t table_index;
public:
Thermistor(uint8_t analog_pin, uint8_t table_index);
int16_t getTemperature() const { return current_temp; }
// True if update initiated, false otherwise
bool update();
};
#endif //THERMISTOR_H
Thermistor.cpp
/*
* 2011 Eberhard Rensch, Pleasant Software
*
* Based on parts of the Makerbot firmware
* Copyright 2010 by Adam Mayer <adam@makerbot.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#include "Thermistor.h"
//#include "WProgram.h"
#include "Arduino.h"
// Default thermistor table. If no thermistor table is loaded into eeprom,
// this will be copied in by the initTable() method.
//
// Thermistor lookup table for RepRap Temperature Sensor Boards (http://make.rrrf.org/ts)
// Made with createTemperatureLookup.py (http://svn.reprap.org/trunk/reprap/firmware/Arduino/utilities/createTemperatureLookup.py)
// ./createTemperatureLookup.py --r0=100000 --t0=25 --r1=0 --r2=4700 --beta=4066 --max-adc=1023
// r0: 100000
// t0: 25
// r1: 0
// r2: 4700
// beta: 4066
// max adc: 1023
typedef int16_t TempTable[THERM_TABLE_SIZE][2];
TempTable default_table = {
{1, 841},
{54, 255},
{107, 209},
{160, 184},
{213, 166},
{266, 153},
{319, 142},
{372, 132},
{425, 124},
{478, 116},
{531, 108},
{584, 101},
{637, 93},
{690, 86},
{743, 78},
{796, 70},
{849, 61},
{902, 50},
{955, 34},
{1008, 3}
};
int16_t thermistorToCelsius(int16_t reading, int8_t table_idx) {
int16_t celsius = 0;
int8_t i;
for (i=1; i<THERM_TABLE_SIZE; i++)
{
if (default_table[i][0] > reading)
{
celsius = default_table[i-1][1] +
(reading - default_table[i-1][0]) *
(default_table[i][1] - default_table[i-1][1]) /
(default_table[i][0] - default_table[i-1][0]);
if (celsius > 255)
celsius = 255;
break;
}
}
// Overflow: We just clamp to 255 degrees celsius to ensure
// that the heater gets shut down if something goes wrong.
if (i == THERM_TABLE_SIZE) {
celsius = 255;
}
return celsius;
}
Thermistor::Thermistor(uint8_t analog_pin_in, uint8_t table_index_in) :
analog_pin(analog_pin_in), next_sample(0), table_index(table_index_in) {
for (int i = 0; i < SAMPLE_COUNT; i++) { sample_buffer[i] = 0; }
}
bool Thermistor::update() {
int16_t temp;
temp = analogRead(analog_pin);
sample_buffer[next_sample] = temp;
next_sample = (next_sample+1) % SAMPLE_COUNT;
// average
int16_t cumulative = 0;
for (int i = 0; i < SAMPLE_COUNT; i++) {
cumulative += sample_buffer[i];
}
int16_t avg = cumulative / SAMPLE_COUNT;
//current_temp = thermistorToCelsius(avg,table_index);
current_temp = thermistorToCelsius(temp,table_index);
return true;
}
PID.h
/*
* 2011 Eberhard Rensch, Pleasant Software
*
* Based on parts of the Makerbot firmware
* Copyright 2010 by Adam Mayer <adam@makerbot.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
* This simplified PID controller was written with reference to:
* * The Heater.h implementation (lookup credits)
* * Brett Beauregard's Arduino PID implementation
* Created on: Feb 19, 2010
* Author: phooky
*/
#ifndef PID_HH_
#define PID_HH_
#include <stdint.h>
#define DELTA_SAMPLES 4
/// This simplified PID controller makes several assumptions:
/// * The output range is limited to 0-255.
class PID {
private:
// Data for approximating d (smoothing to handle discrete nature of sampling).
// See PID.cc for a description of why we do this.
int16_t delta_history[DELTA_SAMPLES];
float delta_summation;
uint8_t delta_idx;
int prev_error; // previous input for calculating next delta
int error_acc; // accumulated error, for calculating integral
int sp; // set point
public:
PID() { reset(); }
void setTarget(const int target) { sp = target; }
const int getTarget() const { return sp; }
/// Reset the PID to board-on values
void reset();
/// PV is the process value; that is, the measured value
/// Returns the new value of the manipulated value; that is, the output
int calculate(int pv);
};
#endif /* PID_HH_ */
PID.cpp
/*
* 2011 Eberhard Rensch, Pleasant Software
*
* Based on parts of the Makerbot firmware
* Copyright 2010 by Adam Mayer <adam@makerbot.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
* This simplified PID controller was written with reference to:
* * The Heater.h implementation (lookup credits)
* * Brett Beauregard's Arduino PID implementation
* Created on: Feb 19, 2010
* Author: phooky
*
*/
#include "PID.h"
#define ERR_ACC_MAX 256
#define ERR_ACC_MIN -ERR_ACC_MAX
#define P_GAIN (float)7.0
#define I_GAIN (float)0.325
#define D_GAIN (float)36.0
// scale the output term to account for our fixed-point bounds
#define OUTPUT_SCALE 2
void PID::reset() {
error_acc = 0;
prev_error = 0;
delta_idx = 0;
sp = 0;
for (delta_idx = 0; delta_idx < DELTA_SAMPLES; delta_idx++) {
delta_history[delta_idx] = 0;
}
delta_idx = 0;
delta_summation = 0;
}
// We're modifying the way we compute delta by averaging the deltas over a
// series of samples. This helps us get a reasonable delta despite the discrete
// nature of the samples; on average we will get a delta of maybe 1/deg/second,
// which will give us a delta impulse for that one calculation round and then
// the D term will immediately disappear. By averaging the last N deltas, we
// allow changes to be registered rather than get subsumed in the sampling noise.
int PID::calculate(const int pv) {
int e = sp - pv;
error_acc += e;
// Clamp the error accumulator at accepted values.
// This will help control overcorrection for accumulated error during the run-up
// and allow the I term to be integrated away more quickly as we approach the
// setpoint.
if (error_acc > ERR_ACC_MAX) {
error_acc = ERR_ACC_MAX;
}
if (error_acc < ERR_ACC_MIN) {
error_acc = ERR_ACC_MIN;
}
float p_term = (float)e * P_GAIN;
float i_term = (float)error_acc * I_GAIN;
int delta = e - prev_error;
// Add to delta history
delta_summation -= delta_history[delta_idx];
delta_history[delta_idx] = delta;
delta_summation += (float)delta;
delta_idx = (delta_idx+1) % DELTA_SAMPLES;
// Use the delta over the whole window
float d_term = delta_summation * D_GAIN;
prev_error = e;
return ((int)(p_term + i_term + d_term))*OUTPUT_SCALE;
}