PID Smoker Controller

My first Spark project is a PID charcoal smoker controller. With the help of these great forums I must say that it is a huge success. I have written an android app with MIT app inventor and am using google docs to graph the results.

I am using a FQP30N06L logic level MOSFET for PWM signals to a 12v blower fan. The lcd has a cheap I2C backpack from Ebay and works flawlessly so far. The temp probes are from my Maverick et732.

awesome. i was halfway through building the exact same thing, and got distracted!
do you mind sharing your schematic and code? I have a few maverick ET732’s sitting on my desk for this exact purpose!

// This #include statement was automatically added by the Spark IDE.
#include "LiquidCrystal_I2C.h"

// This #include statement was automatically added by the Spark IDE.
#include "PID_v1.h"
#include "math.h"



#define PWM_FREQ 20000 // in Hertz (SET YOUR FREQUENCY)   for analogWrite2
#define pitPin A6
#define meatPin A7
#define downPin D2
#define upPin D3
#define motorPin A0
 
uint16_t TIM_ARR = (uint16_t)(24000000 / PWM_FREQ) - 1; // Don't change! Calc's period.
char resultstr[64]; 
int val = 0; // variable to store the read ADC value
double Setpoint, Output, Input;
int meatTemp, pitTemp, P, I, D,motor,set;
unsigned long downTime=0;
    unsigned long upTime=0;
    unsigned long Time;
LiquidCrystal_I2C   *lcd;
PID myPID(&Input, &Output, &Setpoint,10,0.05,2, DIRECT);
Servo fanServo;
void setup() {
    Spark.variable("Input",&pitTemp,INT);
    Spark.variable("Setpoint",&set,INT);
    Spark.variable("Output",&motor,INT);
    Spark.variable("meatTemp",&meatTemp,INT);
    Spark.variable("P",&P,INT);
    Spark.variable("I",&I,INT);
    Spark.variable("D",&D,INT);
    Spark.function("webControl",webControl);
    Spark.variable("result", &resultstr, STRING); 
    pinMode(downPin, INPUT_PULLDOWN);
    pinMode(upPin, INPUT_PULLDOWN);
    pinMode(A0,OUTPUT);
    Input = thermistor_temp(analogRead(pitPin));
    fanServo.attach(A4);
  Setpoint = 225;

  myPID.SetMode(AUTOMATIC);
  myPID.SetOutputLimits(75,255);      //80 is the lowest value that the fan would kick on. 79 will be off
    myPID.SetSampleTime(1000);     //slows pid sample time due to slow moving system
    lcd = new LiquidCrystal_I2C(0x27, 16, 2);  // set the LCD address to 0x20 for a 16x2 //SparkCore bug, address is actually 27 but shifted (0x27*2)
    lcd->init();                      // initialize the lcd
    lcd->backlight();
    lcd->clear();
    lcd->setCursor(0,0);
    lcd->print("Pit:");
    lcd->setCursor(0,1);
    lcd->print("Meat:");
    lcd->setCursor(8,0);
    lcd->print("Set:");
    lcd->setCursor(4,0);
   // lcd->print(thermister_temp(analogRead(6)));


}

void loop() {
Input = thermistor_temp(analogRead(pitPin));    
myPID.Compute();
pitTemp = Input;
meatTemp = thermistor_temp(analogRead(meatPin));
motor = map(Output,75,255,0,100);
if ((Setpoint-Input > 10) or (Input-Setpoint > 10)){
    myPID.SetTunings(myPID.GetKp(),0,myPID.GetKd());
}
else myPID.SetTunings(myPID.GetKp(),0.05,myPID.GetKd());
P = myPID.GetKp();
I = myPID.GetKi();
D = myPID.GetKd();
set = Setpoint;
  if (Output <= 79) analogWrite2(motorPin,0);     //Turn fan off at low point of range
  else  analogWrite2(A0,Output);
  if (Output == 75) fanServo.write(10);
  else fanServo.write(85);
  
  
  if (digitalRead (D2) == HIGH){       //Listen for button press
      Time = millis();
      if (Time-upTime > 150){           //debounce and slow increment
          Setpoint++;
          upTime = millis();
      }
    }
    if (digitalRead(D3) == HIGH){
       Time = millis();
       if (Time-downTime>150){
        Setpoint--;
        downTime = millis();
       }
    }
  lcdPrint();   //calls print function
  sprintf(resultstr, "{\"pitTemp\":%d,\"set\":%d, \"motor\":%d}", pitTemp, set, motor);   
}

int webControl(String command){             //allow app to interface through API
    
    if (command == "setUp") Setpoint++;
    if (command == "setDown") Setpoint--;
    if (command.startsWith("S") == true){     // change paramaters via web/app
        Setpoint = double(command.substring(1,4).toInt());
        
        }
    if (command.startsWith("P") == true){     // change paramaters via web/app
        String holder = command.substring(1,4);
        myPID.SetTunings(double(holder.toInt()),myPID.GetKi(),myPID.GetKd());
        P = myPID.GetKp();
    }
    if (command.startsWith("I") == true){
       String holder = command.substring(1,4);
       myPID.SetTunings(myPID.GetKp(),double(holder.toInt()),myPID.GetKd());
       I = myPID.GetKi();
    }    
    if (command.startsWith("D") == true){
       String holder = command.substring(1,4);
       myPID.SetTunings(myPID.GetKp(),myPID.GetKi(),double(holder.toInt()));
       D = myPID.GetKd();
    }    
  
    return 1;
}

void lcdPrint(){                        // update lcd
    if (Input < 0){
        lcd->setCursor(4,0);
        lcd->print("XXX");
    }
    else{
    lcd->setCursor(4,0);
  lcd->print((int)(Input));
  lcd->print((char)223);
  if (Input < 100){
      lcd->setCursor(8,1);
      lcd->print(" ");
  }
    }
    if (meatTemp < 0){
        lcd->setCursor(5,1);
        lcd->print("XXX");
    }
    else{
  lcd->setCursor(5,1);
  lcd->print(meatTemp);
  lcd->print((char)223);
  if (meatTemp < 100){
      lcd->setCursor(7,0);
      lcd->print(" ");
  }
    }
  
  lcd->setCursor(12,0);
  lcd->print((int)(Setpoint));
  lcd->print((char)223);
  if(Setpoint < 100){
      lcd->setCursor(15,0);
      lcd->print(" ");
  }
  lcd->setCursor(12,1);
  lcd->print((int)(map(Output,75,255,0,100)));
 // lcd->print("%");
  
  if (map(Output,79,255,0,100) == 100) {
       // lcd->setCursor(14,1);
        lcd->print("%");
  }
  else if (map(Output,79,255,0,100) < 100){
      lcd->print("% ");
  }
  else if (map(Output,79,255,0,100) < 10){
      //lcd->setCursor(13,1);
      lcd->print("%   ");
  }
  
}
int thermistor_temp(int aval) {                           //Function to do theristor calculations
	double R, T;
	R = log((1 / ((4095 / (double) aval) - 1)) * (double) 20800);
   	T = (1 / ((2.3067434E-4) + (2.3696596E-4) * R + (1.2636414E-7) * R * R * R)) - 273.25;
	// return degrees F
	return ((int) ((T * 9.0) / 5.0 + 32.0));
}

void analogWrite2(uint16_t pin, uint8_t value) {         //analogWrite function allowing 20khz pwm credit bdub from Sark forums
  TIM_OCInitTypeDef TIM_OCInitStructure;
 
  if (pin >= TOTAL_PINS || PIN_MAP[pin].timer_peripheral == NULL) {
    return;
  }
  // SPI safety check
  if (SPI.isEnabled() == true && (pin == SCK || pin == MOSI || pin == MISO)) {
    return;
  }
  // I2C safety check
  if (Wire.isEnabled() == true && (pin == SCL || pin == SDA)) {
    return;
  }
  // Serial1 safety check
  if (Serial1.isEnabled() == true && (pin == RX || pin == TX)) {
    return;
  }
  if (PIN_MAP[pin].pin_mode != OUTPUT && PIN_MAP[pin].pin_mode != AF_OUTPUT_PUSHPULL) {
    return;
  }
  // Don't re-init PWM and cause a glitch if already setup, just update duty cycle and return.
  if (PIN_MAP[pin].pin_mode == AF_OUTPUT_PUSHPULL) {
    TIM_OCInitStructure.TIM_Pulse = (uint16_t)(value * (TIM_ARR + 1) / 255);
    if (PIN_MAP[pin].timer_ch == TIM_Channel_1) {
      PIN_MAP[pin].timer_peripheral-> CCR1 = TIM_OCInitStructure.TIM_Pulse;
    } else if (PIN_MAP[pin].timer_ch == TIM_Channel_2) {
      PIN_MAP[pin].timer_peripheral-> CCR2 = TIM_OCInitStructure.TIM_Pulse;
    } else if (PIN_MAP[pin].timer_ch == TIM_Channel_3) {
      PIN_MAP[pin].timer_peripheral-> CCR3 = TIM_OCInitStructure.TIM_Pulse;
    } else if (PIN_MAP[pin].timer_ch == TIM_Channel_4) {
      PIN_MAP[pin].timer_peripheral-> CCR4 = TIM_OCInitStructure.TIM_Pulse;
    }
    return;
  }
 
  TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
 
  //PWM Frequency : PWM_FREQ (Hz)
  uint16_t TIM_Prescaler = (uint16_t)(SystemCoreClock / 24000000) - 1; //TIM Counter clock = 24MHz
 
  // TIM Channel Duty Cycle(%) = (TIM_CCR / TIM_ARR + 1) * 100
  uint16_t TIM_CCR = (uint16_t)(value * (TIM_ARR + 1) / 255);
 
  // AFIO clock enable
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
 
  pinMode(pin, AF_OUTPUT_PUSHPULL);
 
  // TIM clock enable
  if (PIN_MAP[pin].timer_peripheral == TIM2)
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
  else if (PIN_MAP[pin].timer_peripheral == TIM3)
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
  else if (PIN_MAP[pin].timer_peripheral == TIM4)
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
 
  // Time base configuration
  TIM_TimeBaseStructure.TIM_Period = TIM_ARR;
  TIM_TimeBaseStructure.TIM_Prescaler = TIM_Prescaler;
  TIM_TimeBaseStructure.TIM_ClockDivision = 0;
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
 
  TIM_TimeBaseInit(PIN_MAP[pin].timer_peripheral, & TIM_TimeBaseStructure);
 
  // PWM1 Mode configuration
  TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
  TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
  TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
  TIM_OCInitStructure.TIM_Pulse = TIM_CCR;
 
  if (PIN_MAP[pin].timer_ch == TIM_Channel_1) {
    // PWM1 Mode configuration: Channel1
    TIM_OC1Init(PIN_MAP[pin].timer_peripheral, & TIM_OCInitStructure);
    TIM_OC1PreloadConfig(PIN_MAP[pin].timer_peripheral, TIM_OCPreload_Enable);
  } else if (PIN_MAP[pin].timer_ch == TIM_Channel_2) {
    // PWM1 Mode configuration: Channel2
    TIM_OC2Init(PIN_MAP[pin].timer_peripheral, & TIM_OCInitStructure);
    TIM_OC2PreloadConfig(PIN_MAP[pin].timer_peripheral, TIM_OCPreload_Enable);
  } else if (PIN_MAP[pin].timer_ch == TIM_Channel_3) {
    // PWM1 Mode configuration: Channel3
    TIM_OC3Init(PIN_MAP[pin].timer_peripheral, & TIM_OCInitStructure);
    TIM_OC3PreloadConfig(PIN_MAP[pin].timer_peripheral, TIM_OCPreload_Enable);
  } else if (PIN_MAP[pin].timer_ch == TIM_Channel_4) {
    // PWM1 Mode configuration: Channel4
    TIM_OC4Init(PIN_MAP[pin].timer_peripheral, & TIM_OCInitStructure);
    TIM_OC4PreloadConfig(PIN_MAP[pin].timer_peripheral, TIM_OCPreload_Enable);
  }
 
  TIM_ARRPreloadConfig(PIN_MAP[pin].timer_peripheral, ENABLE);
 
  // TIM enable counter
  TIM_Cmd(PIN_MAP[pin].timer_peripheral, ENABLE);
}

The code is pretty messy so I would welcome any suggestions. I will post the schematic as soon as I finish I’m pretty new to fritzing.

David

Which PID library did you use?
I am looking to port my arduino sous vide machine to Spark, I used the standard Arduino one link
Did you need to make many changes?

I like your app - do you know of an iphone equivalent easy to sue app creator?

@gorstj I used the PID library that you linked with no modification. I don’t know that there is an equivalent app builder for iOS, but you should checkout @bdub’s webapp thread under project share. I think that’s the route I will take for iOS and Spark. Be sure and post your sous vide when its up and wifi’d. That might be my next project.

Nice work! I popped a web interface on top of Bob Hruska’s tempmon project a while back and am now planning to port it over to use a Spark Core. Bob wrote some great math to calculate the ramp up blower control to “ease” in to a set point. Worked great on my Ugly Drum Smoker. Anyway I noticed you have a servo mounted on that blower (fanServo). What is the purpose of it? To close the blower off?

@szac Thanks! Yes the servo is to close the blower to choke the coals if the temp gets to high. It is working pretty well.

do you have a wiring diagram for this?

looking for help i have a QMaster-jr bbq temp controller that i have a 225 set point but once the temp gets to 150 fan turns off do you think i need to adjust the P right now the P=30 seconds I=240 seconds D=60 seconds

hey @Dave1955 i dont think this will be the best forum to get help with the QMaster....but what you will find here are some awesome people building a REPLACEMENT for your Qmaster.

keep an eye our for "SmartBBQ". a few people here are already working on the prototype.

Hi al…

Do you have the code to read the maverick probe adc values?, how do you hook it up? I’m trying to find a way to implement something similar to this!!

This is really cool! I’m thinking of doing something similar. Can you share your list of parts? Do you have a project log someplace?

Thanks!

this thread has an awesome walk through

How do you handle getting the graph data exactly? Is it stored on the web page, or on a server somewhere? I’m working on a similar project, except for an electric smoker. I made a rough prototype using a Beaglebone that worked well, but I came upon the Spark and think it will be a much more lean solution. On the Beaglebone I could do full HTTP requests, so I just sent temperatures to a Parse server. But this isn’t possible with the Spark Core ars far as I know.

hi, great project!
Are you able to share schematics? Along with the smartbbq and heatermeter projects there are a number of good code starting points, but finding the wiring is largely start from scratch or buy a PCB and assemble. if you happened to have a basic wiring setup that would be very interesting for me!

Thanks!