├── README.md └── Sous_Viduino.ino /README.md: -------------------------------------------------------------------------------- 1 | Sous-Viduino 2 | ============ 3 | Sous-vide powered by Arduino - The SousViduino! 4 | 5 | This is the code from Bill Earl for the Adafruit Sous-vide tutorial using an Arduino. Check it out at http://learn.adafruit.com/sous-vide-powered-by-arduino-the-sous-viduino -------------------------------------------------------------------------------- /Sous_Viduino.ino: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------- 2 | // 3 | // Sous Vide Controller 4 | // Bill Earl - for Adafruit Industries 5 | // 6 | // Based on the Arduino PID and PID AutoTune Libraries 7 | // by Brett Beauregard 8 | //------------------------------------------------------------------ 9 | 10 | // PID Library 11 | #include 12 | #include 13 | 14 | // Libraries for the Adafruit RGB/LCD Shield 15 | #include 16 | #include 17 | 18 | // Libraries for the DS18B20 Temperature Sensor 19 | #include 20 | #include 21 | 22 | // So we can save and retrieve settings 23 | #include 24 | 25 | // ************************************************ 26 | // Pin definitions 27 | // ************************************************ 28 | 29 | // Output Relay 30 | #define RelayPin 7 31 | 32 | // One-Wire Temperature Sensor 33 | // (Use GPIO pins for power/ground to simplify the wiring) 34 | #define ONE_WIRE_BUS 2 35 | #define ONE_WIRE_PWR 3 36 | #define ONE_WIRE_GND 4 37 | 38 | // ************************************************ 39 | // PID Variables and constants 40 | // ************************************************ 41 | 42 | //Define Variables we'll be connecting to 43 | double Setpoint; 44 | double Input; 45 | double Output; 46 | 47 | volatile long onTime = 0; 48 | 49 | // pid tuning parameters 50 | double Kp; 51 | double Ki; 52 | double Kd; 53 | 54 | // EEPROM addresses for persisted data 55 | const int SpAddress = 0; 56 | const int KpAddress = 8; 57 | const int KiAddress = 16; 58 | const int KdAddress = 24; 59 | 60 | //Specify the links and initial tuning parameters 61 | PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT); 62 | 63 | // 10 second Time Proportional Output window 64 | int WindowSize = 10000; 65 | unsigned long windowStartTime; 66 | 67 | // ************************************************ 68 | // Auto Tune Variables and constants 69 | // ************************************************ 70 | byte ATuneModeRemember=2; 71 | 72 | double aTuneStep=500; 73 | double aTuneNoise=1; 74 | unsigned int aTuneLookBack=20; 75 | 76 | boolean tuning = false; 77 | 78 | PID_ATune aTune(&Input, &Output); 79 | 80 | // ************************************************ 81 | // DiSplay Variables and constants 82 | // ************************************************ 83 | 84 | Adafruit_RGBLCDShield lcd = Adafruit_RGBLCDShield(); 85 | // These #defines make it easy to set the backlight color 86 | #define RED 0x1 87 | #define YELLOW 0x3 88 | #define GREEN 0x2 89 | #define TEAL 0x6 90 | #define BLUE 0x4 91 | #define VIOLET 0x5 92 | #define WHITE 0x7 93 | 94 | #define BUTTON_SHIFT BUTTON_SELECT 95 | 96 | unsigned long lastInput = 0; // last button press 97 | 98 | byte degree[8] = // define the degree symbol 99 | { 100 | B00110, 101 | B01001, 102 | B01001, 103 | B00110, 104 | B00000, 105 | B00000, 106 | B00000, 107 | B00000 108 | }; 109 | 110 | const int logInterval = 10000; // log every 10 seconds 111 | long lastLogTime = 0; 112 | 113 | // ************************************************ 114 | // States for state machine 115 | // ************************************************ 116 | enum operatingState { OFF = 0, SETP, RUN, TUNE_P, TUNE_I, TUNE_D, AUTO}; 117 | operatingState opState = OFF; 118 | 119 | // ************************************************ 120 | // Sensor Variables and constants 121 | // Data wire is plugged into port 2 on the Arduino 122 | 123 | // Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs) 124 | OneWire oneWire(ONE_WIRE_BUS); 125 | 126 | // Pass our oneWire reference to Dallas Temperature. 127 | DallasTemperature sensors(&oneWire); 128 | 129 | // arrays to hold device address 130 | DeviceAddress tempSensor; 131 | 132 | // ************************************************ 133 | // Setup and diSplay initial screen 134 | // ************************************************ 135 | void setup() 136 | { 137 | Serial.begin(9600); 138 | 139 | // Initialize Relay Control: 140 | 141 | pinMode(RelayPin, OUTPUT); // Output mode to drive relay 142 | digitalWrite(RelayPin, LOW); // make sure it is off to start 143 | 144 | // Set up Ground & Power for the sensor from GPIO pins 145 | 146 | pinMode(ONE_WIRE_GND, OUTPUT); 147 | digitalWrite(ONE_WIRE_GND, LOW); 148 | 149 | pinMode(ONE_WIRE_PWR, OUTPUT); 150 | digitalWrite(ONE_WIRE_PWR, HIGH); 151 | 152 | // Initialize LCD DiSplay 153 | 154 | lcd.begin(16, 2); 155 | lcd.createChar(1, degree); // create degree symbol from the binary 156 | 157 | lcd.setBacklight(VIOLET); 158 | lcd.print(F(" Adafruit")); 159 | lcd.setCursor(0, 1); 160 | lcd.print(F(" Sous Vide!")); 161 | 162 | // Start up the DS18B20 One Wire Temperature Sensor 163 | 164 | sensors.begin(); 165 | if (!sensors.getAddress(tempSensor, 0)) 166 | { 167 | lcd.setCursor(0, 1); 168 | lcd.print(F("Sensor Error")); 169 | } 170 | sensors.setResolution(tempSensor, 12); 171 | sensors.setWaitForConversion(false); 172 | 173 | delay(3000); // Splash screen 174 | 175 | // Initialize the PID and related variables 176 | LoadParameters(); 177 | myPID.SetTunings(Kp,Ki,Kd); 178 | 179 | myPID.SetSampleTime(1000); 180 | myPID.SetOutputLimits(0, WindowSize); 181 | 182 | // Run timer2 interrupt every 15 ms 183 | TCCR2A = 0; 184 | TCCR2B = 1< 3000) // return to RUN after 3 seconds idle 309 | { 310 | opState = RUN; 311 | return; 312 | } 313 | lcd.setCursor(0,1); 314 | lcd.print(Setpoint); 315 | lcd.print(" "); 316 | DoControl(); 317 | } 318 | } 319 | 320 | // ************************************************ 321 | // Proportional Tuning State 322 | // UP/DOWN to change Kp 323 | // RIGHT for Ki 324 | // LEFT for setpoint 325 | // SHIFT for 10x tuning 326 | // ************************************************ 327 | void TuneP() 328 | { 329 | lcd.setBacklight(VIOLET); 330 | lcd.print(F("Set Kp")); 331 | 332 | uint8_t buttons = 0; 333 | while(true) 334 | { 335 | buttons = ReadButtons(); 336 | 337 | float increment = 1.0; 338 | if (buttons & BUTTON_SHIFT) 339 | { 340 | increment *= 10; 341 | } 342 | if (buttons & BUTTON_LEFT) 343 | { 344 | opState = SETP; 345 | return; 346 | } 347 | if (buttons & BUTTON_RIGHT) 348 | { 349 | opState = TUNE_I; 350 | return; 351 | } 352 | if (buttons & BUTTON_UP) 353 | { 354 | Kp += increment; 355 | delay(200); 356 | } 357 | if (buttons & BUTTON_DOWN) 358 | { 359 | Kp -= increment; 360 | delay(200); 361 | } 362 | if ((millis() - lastInput) > 3000) // return to RUN after 3 seconds idle 363 | { 364 | opState = RUN; 365 | return; 366 | } 367 | lcd.setCursor(0,1); 368 | lcd.print(Kp); 369 | lcd.print(" "); 370 | DoControl(); 371 | } 372 | } 373 | 374 | // ************************************************ 375 | // Integral Tuning State 376 | // UP/DOWN to change Ki 377 | // RIGHT for Kd 378 | // LEFT for Kp 379 | // SHIFT for 10x tuning 380 | // ************************************************ 381 | void TuneI() 382 | { 383 | lcd.setBacklight(VIOLET); 384 | lcd.print(F("Set Ki")); 385 | 386 | uint8_t buttons = 0; 387 | while(true) 388 | { 389 | buttons = ReadButtons(); 390 | 391 | float increment = 0.01; 392 | if (buttons & BUTTON_SHIFT) 393 | { 394 | increment *= 10; 395 | } 396 | if (buttons & BUTTON_LEFT) 397 | { 398 | opState = TUNE_P; 399 | return; 400 | } 401 | if (buttons & BUTTON_RIGHT) 402 | { 403 | opState = TUNE_D; 404 | return; 405 | } 406 | if (buttons & BUTTON_UP) 407 | { 408 | Ki += increment; 409 | delay(200); 410 | } 411 | if (buttons & BUTTON_DOWN) 412 | { 413 | Ki -= increment; 414 | delay(200); 415 | } 416 | if ((millis() - lastInput) > 3000) // return to RUN after 3 seconds idle 417 | { 418 | opState = RUN; 419 | return; 420 | } 421 | lcd.setCursor(0,1); 422 | lcd.print(Ki); 423 | lcd.print(" "); 424 | DoControl(); 425 | } 426 | } 427 | 428 | // ************************************************ 429 | // Derivative Tuning State 430 | // UP/DOWN to change Kd 431 | // RIGHT for setpoint 432 | // LEFT for Ki 433 | // SHIFT for 10x tuning 434 | // ************************************************ 435 | void TuneD() 436 | { 437 | lcd.setBacklight(VIOLET); 438 | lcd.print(F("Set Kd")); 439 | 440 | uint8_t buttons = 0; 441 | while(true) 442 | { 443 | buttons = ReadButtons(); 444 | float increment = 0.01; 445 | if (buttons & BUTTON_SHIFT) 446 | { 447 | increment *= 10; 448 | } 449 | if (buttons & BUTTON_LEFT) 450 | { 451 | opState = TUNE_I; 452 | return; 453 | } 454 | if (buttons & BUTTON_RIGHT) 455 | { 456 | opState = RUN; 457 | return; 458 | } 459 | if (buttons & BUTTON_UP) 460 | { 461 | Kd += increment; 462 | delay(200); 463 | } 464 | if (buttons & BUTTON_DOWN) 465 | { 466 | Kd -= increment; 467 | delay(200); 468 | } 469 | if ((millis() - lastInput) > 3000) // return to RUN after 3 seconds idle 470 | { 471 | opState = RUN; 472 | return; 473 | } 474 | lcd.setCursor(0,1); 475 | lcd.print(Kd); 476 | lcd.print(" "); 477 | DoControl(); 478 | } 479 | } 480 | 481 | // ************************************************ 482 | // PID COntrol State 483 | // SHIFT and RIGHT for autotune 484 | // RIGHT - Setpoint 485 | // LEFT - OFF 486 | // ************************************************ 487 | void Run() 488 | { 489 | // set up the LCD's number of rows and columns: 490 | lcd.print(F("Sp: ")); 491 | lcd.print(Setpoint); 492 | lcd.write(1); 493 | lcd.print(F("C : ")); 494 | 495 | SaveParameters(); 496 | myPID.SetTunings(Kp,Ki,Kd); 497 | 498 | uint8_t buttons = 0; 499 | while(true) 500 | { 501 | setBacklight(); // set backlight based on state 502 | 503 | buttons = ReadButtons(); 504 | if ((buttons & BUTTON_SHIFT) 505 | && (buttons & BUTTON_RIGHT) 506 | && (abs(Input - Setpoint) < 0.5)) // Should be at steady-state 507 | { 508 | StartAutoTune(); 509 | } 510 | else if (buttons & BUTTON_RIGHT) 511 | { 512 | opState = SETP; 513 | return; 514 | } 515 | else if (buttons & BUTTON_LEFT) 516 | { 517 | opState = OFF; 518 | return; 519 | } 520 | 521 | DoControl(); 522 | 523 | lcd.setCursor(0,1); 524 | lcd.print(Input); 525 | lcd.write(1); 526 | lcd.print(F("C : ")); 527 | 528 | float pct = map(Output, 0, WindowSize, 0, 1000); 529 | lcd.setCursor(10,1); 530 | lcd.print(F(" ")); 531 | lcd.setCursor(10,1); 532 | lcd.print(pct/10); 533 | //lcd.print(Output); 534 | lcd.print("%"); 535 | 536 | lcd.setCursor(15,0); 537 | if (tuning) 538 | { 539 | lcd.print("T"); 540 | } 541 | else 542 | { 543 | lcd.print(" "); 544 | } 545 | 546 | // periodically log to serial port in csv format 547 | if (millis() - lastLogTime > logInterval) 548 | { 549 | lastLogTime = millis(); 550 | Serial.print(Input); 551 | Serial.print(","); 552 | Serial.println(Output); 553 | } 554 | 555 | delay(100); 556 | } 557 | } 558 | 559 | // ************************************************ 560 | // Execute the control loop 561 | // ************************************************ 562 | void DoControl() 563 | { 564 | // Read the input: 565 | if (sensors.isConversionAvailable(0)) 566 | { 567 | Input = sensors.getTempC(tempSensor); 568 | sensors.requestTemperatures(); // prime the pump for the next one - but don't wait 569 | } 570 | 571 | if (tuning) // run the auto-tuner 572 | { 573 | if (aTune.Runtime()) // returns 'true' when done 574 | { 575 | FinishAutoTune(); 576 | } 577 | } 578 | else // Execute control algorithm 579 | { 580 | myPID.Compute(); 581 | } 582 | 583 | // Time Proportional relay state is updated regularly via timer interrupt. 584 | onTime = Output; 585 | } 586 | 587 | // ************************************************ 588 | // Called by ISR every 15ms to drive the output 589 | // ************************************************ 590 | void DriveOutput() 591 | { 592 | long now = millis(); 593 | // Set the output 594 | // "on time" is proportional to the PID output 595 | if(now - windowStartTime>WindowSize) 596 | { //time to shift the Relay Window 597 | windowStartTime += WindowSize; 598 | } 599 | if((onTime > 100) && (onTime > (now - windowStartTime))) 600 | { 601 | digitalWrite(RelayPin,HIGH); 602 | } 603 | else 604 | { 605 | digitalWrite(RelayPin,LOW); 606 | } 607 | } 608 | 609 | // ************************************************ 610 | // Set Backlight based on the state of control 611 | // ************************************************ 612 | void setBacklight() 613 | { 614 | if (tuning) 615 | { 616 | lcd.setBacklight(VIOLET); // Tuning Mode 617 | } 618 | else if (abs(Input - Setpoint) > 1.0) 619 | { 620 | lcd.setBacklight(RED); // High Alarm - off by more than 1 degree 621 | } 622 | else if (abs(Input - Setpoint) > 0.2) 623 | { 624 | lcd.setBacklight(YELLOW); // Low Alarm - off by more than 0.2 degrees 625 | } 626 | else 627 | { 628 | lcd.setBacklight(WHITE); // We're on target! 629 | } 630 | } 631 | 632 | // ************************************************ 633 | // Start the Auto-Tuning cycle 634 | // ************************************************ 635 | 636 | void StartAutoTune() 637 | { 638 | // REmember the mode we were in 639 | ATuneModeRemember = myPID.GetMode(); 640 | 641 | // set up the auto-tune parameters 642 | aTune.SetNoiseBand(aTuneNoise); 643 | aTune.SetOutputStep(aTuneStep); 644 | aTune.SetLookbackSec((int)aTuneLookBack); 645 | tuning = true; 646 | } 647 | 648 | // ************************************************ 649 | // Return to normal control 650 | // ************************************************ 651 | void FinishAutoTune() 652 | { 653 | tuning = false; 654 | 655 | // Extract the auto-tune calculated parameters 656 | Kp = aTune.GetKp(); 657 | Ki = aTune.GetKi(); 658 | Kd = aTune.GetKd(); 659 | 660 | // Re-tune the PID and revert to normal control mode 661 | myPID.SetTunings(Kp,Ki,Kd); 662 | myPID.SetMode(ATuneModeRemember); 663 | 664 | // Persist any changed parameters to EEPROM 665 | SaveParameters(); 666 | } 667 | 668 | // ************************************************ 669 | // Check buttons and time-stamp the last press 670 | // ************************************************ 671 | uint8_t ReadButtons() 672 | { 673 | uint8_t buttons = lcd.readButtons(); 674 | if (buttons != 0) 675 | { 676 | lastInput = millis(); 677 | } 678 | return buttons; 679 | } 680 | 681 | // ************************************************ 682 | // Save any parameter changes to EEPROM 683 | // ************************************************ 684 | void SaveParameters() 685 | { 686 | if (Setpoint != EEPROM_readDouble(SpAddress)) 687 | { 688 | EEPROM_writeDouble(SpAddress, Setpoint); 689 | } 690 | if (Kp != EEPROM_readDouble(KpAddress)) 691 | { 692 | EEPROM_writeDouble(KpAddress, Kp); 693 | } 694 | if (Ki != EEPROM_readDouble(KiAddress)) 695 | { 696 | EEPROM_writeDouble(KiAddress, Ki); 697 | } 698 | if (Kd != EEPROM_readDouble(KdAddress)) 699 | { 700 | EEPROM_writeDouble(KdAddress, Kd); 701 | } 702 | } 703 | 704 | // ************************************************ 705 | // Load parameters from EEPROM 706 | // ************************************************ 707 | void LoadParameters() 708 | { 709 | // Load from EEPROM 710 | Setpoint = EEPROM_readDouble(SpAddress); 711 | Kp = EEPROM_readDouble(KpAddress); 712 | Ki = EEPROM_readDouble(KiAddress); 713 | Kd = EEPROM_readDouble(KdAddress); 714 | 715 | // Use defaults if EEPROM values are invalid 716 | if (isnan(Setpoint)) 717 | { 718 | Setpoint = 60; 719 | } 720 | if (isnan(Kp)) 721 | { 722 | Kp = 850; 723 | } 724 | if (isnan(Ki)) 725 | { 726 | Ki = 0.5; 727 | } 728 | if (isnan(Kd)) 729 | { 730 | Kd = 0.1; 731 | } 732 | } 733 | 734 | 735 | // ************************************************ 736 | // Write floating point values to EEPROM 737 | // ************************************************ 738 | void EEPROM_writeDouble(int address, double value) 739 | { 740 | byte* p = (byte*)(void*)&value; 741 | for (int i = 0; i < sizeof(value); i++) 742 | { 743 | EEPROM.write(address++, *p++); 744 | } 745 | } 746 | 747 | // ************************************************ 748 | // Read floating point values from EEPROM 749 | // ************************************************ 750 | double EEPROM_readDouble(int address) 751 | { 752 | double value = 0.0; 753 | byte* p = (byte*)(void*)&value; 754 | for (int i = 0; i < sizeof(value); i++) 755 | { 756 | *p++ = EEPROM.read(address++); 757 | } 758 | return value; 759 | } 760 | --------------------------------------------------------------------------------