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.

4 Likes

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.

1 Like

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!