/*
* thermocycler.cpp - OpenPCR control software.
* Copyright (C) 2010-2011 Josh Perfetto. All Rights Reserved.
*
* OpenPCR control software 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.
*
* OpenPCR control software 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
* the OpenPCR control software. If not, see .
*/
#include "pcr_includes.h"
#include "thermocycler.h"
#include "display.h"
#include "program.h"
#include "serialcontrol.h"
#include "../Wire/Wire.h"
#include
//constants
// in 0.1 Ohms
PROGMEM const unsigned long PLATE_RESISTANCE_TABLE[] = {
3364790, 3149040, 2948480, 2761940, 2588380, 2426810, 2276320, 2136100, 2005390, 1883490,
1769740, 1663560, 1564410, 1471770, 1385180, 1304210, 1228470, 1157590, 1091220, 1029060,
970810, 916210, 865010, 816980, 771900, 729570, 689820, 652460, 617360, 584340,
553290, 524070, 496560, 470660, 446260, 423270, 401590, 381150, 361870, 343680,
326500, 310290, 294980, 280520, 266850, 253920, 241700, 230130, 219180, 208820,
199010, 189710, 180900, 172550, 164630, 157120, 149990, 143230, 136810, 130720,
124930, 119420, 114190, 109220, 104500, 100000, 95720, 91650, 87770, 84080,
80570, 77220, 74020, 70980, 68080, 65310, 62670, 60150, 57750, 55450,
53260, 51170, 49170, 47250, 45430, 43680, 42010, 40410, 38880, 37420,
36020, 34680, 33400, 32170, 30990, 29860, 28780, 27740, 26750, 25790,
24880, 24000, 23160, 22350, 21570, 20830, 20110, 19420, 18760, 18130,
17520, 16930, 16370, 15820, 15300, 14800, 14320, 13850, 13400, 12970,
12550, 12150, 11770, 11400, 11040, 10700, 10370, 10050, 9738, 9441,
9155, 8878, 8612, 8354, 8106, 7866, 7635, 7412, 7196, 6987, 6786,
6591, 6403, 6222, 6046, 5876 };
// in Ohms
PROGMEM const unsigned int LID_RESISTANCE_TABLE[] = {
32919, 31270, 29715, 28246, 26858, 25547, 24307, 23135, 22026, 20977,
19987, 19044, 18154, 17310, 16510, 15752, 15034, 14352, 13705, 13090,
12507, 11953, 11427, 10927, 10452, 10000, 9570, 9161, 8771, 8401,
8048, 7712, 7391, 7086, 6795, 6518, 6254, 6001, 5761, 5531, 5311,
5102, 4902, 4710, 4528, 4353, 4186, 4026, 3874, 3728, 3588,
3454, 3326, 3203, 3085, 2973, 2865, 2761, 2662, 2567, 2476,
2388, 2304, 2223, 2146, 2072, 2000, 1932, 1866, 1803, 1742,
1684, 1627, 1573, 1521, 1471, 1423, 1377, 1332, 1289, 1248,
1208, 1170, 1133, 1097, 1063, 1030, 998, 968, 938, 909,
882, 855, 829, 805, 781, 758, 735, 714, 693, 673,
653, 635, 616, 599, 582, 565, 550, 534, 519, 505,
491, 478, 465, 452, 440, 428, 416, 405, 395, 384,
374, 364, 355, 345, 337 };
// I2C address for MCP3422 - base address for MCP3424
#define MCP3422_ADDRESS 0X68
#define MCP342X_RES_FIELD 0X0C // resolution/rate field
#define MCP342X_18_BIT 0X0C // 18-bit 3.75 SPS
#define MCP342X_BUSY 0X80 // read: output not ready
#define DATAOUT 11//MOSI
#define DATAIN 12//MISO
#define SPICLOCK 13//sck
#define SLAVESELECT 10//ss
#define CYCLE_START_TOLERANCE 0.2
#define LID_START_TOLERANCE 1.0
#define PLATE_PID_INC_P 1000
#define PLATE_PID_INC_I 250
#define PLATE_PID_INC_D 250
#define PLATE_PID_INC_LOW_THRESHOLD 40
#define PLATE_PID_INC_LOW_P 600
#define PLATE_PID_INC_LOW_I 200
#define PLATE_PID_INC_LOW_D 400
#define PLATE_PID_DEC_HIGH_THRESHOLD 70
#define PLATE_PID_DEC_HIGH_P 800
#define PLATE_PID_DEC_HIGH_I 700
#define PLATE_PID_DEC_HIGH_D 300
#define PLATE_PID_DEC_P 500
#define PLATE_PID_DEC_I 400
#define PLATE_PID_DEC_D 200
#define PLATE_PID_DEC_LOW_THRESHOLD 35
#define PLATE_PID_DEC_LOW_P 2000
#define PLATE_PID_DEC_LOW_I 100
#define PLATE_PID_DEC_LOW_D 200
#define LID_PID_P 100
#define LID_PID_I 50
#define LID_PID_D 50
#define PLATE_BANGBANG_THRESHOLD 2.0
#define LID_BANGBANG_THRESHOLD 2.0
#define MIN_PELTIER_PWM -1023
#define MAX_PELTIER_PWM 1023
#define MAX_LID_PWM 255
#define MIN_LID_PWM 0
#define STARTUP_DELAY 5000
//public
Thermocycler::Thermocycler(boolean restarted):
iRestarted(restarted),
ipDisplay(NULL),
ipProgram(NULL),
ipDisplayCycle(NULL),
ipSerialControl(NULL),
iProgramState(EOff),
ipCurrentStep(NULL),
iThermalDirection(OFF),
iPeltierPwm(0),
iLidPwm(0),
iPlateTemp(0.0),
iLidTemp(0.0),
iCycleStartTime(0),
iRamping(true),
iPlatePid(&iPlateTemp, &iPeltierPwm, &iTargetPlateTemp, PLATE_PID_INC_P, PLATE_PID_INC_I, PLATE_PID_INC_D, DIRECT),
iLidPid(&iLidTemp, &iLidPwm, &iTargetLidTemp, LID_PID_P, LID_PID_I, LID_PID_D, DIRECT),
iTargetLidTemp(0) {
ipDisplay = new Display();
ipSerialControl = new SerialControl(ipDisplay);
//init pins
pinMode(15, INPUT);
pinMode(2, OUTPUT);
pinMode(3, OUTPUT);
pinMode(4, OUTPUT);
pinMode(5, OUTPUT);
//spi pins
pinMode(DATAOUT, OUTPUT);
pinMode(DATAIN, INPUT);
pinMode(SPICLOCK,OUTPUT);
pinMode(SLAVESELECT,OUTPUT);
digitalWrite(SLAVESELECT,HIGH); //disable device
// SPCR = 01010000
//interrupt disabled,spi enabled,msb 1st,master,clk low when idle,
//sample on leading edge of clk,system clock/4 rate (fastest)
int clr;
SPCR = (1<GetNumCycles();
}
int Thermocycler::GetCurrentCycleNum() {
int numCycles = GetNumCycles();
return ipDisplayCycle->GetCurrentCycle() > numCycles ? numCycles : ipDisplayCycle->GetCurrentCycle();
}
Thermocycler::ThermalState Thermocycler::GetThermalState() {
if (iThermalDirection == EOff)
return EIdle;
if (iRamping) {
if (iThermalDirection == HEAT)
return EHeating;
else
return ECooling;
} else {
return EHolding;
}
}
// control
void Thermocycler::SetProgram(Cycle* pProgram, Cycle* pDisplayCycle, const char* szProgName, int lidTemp) {
Stop();
ipProgram = pProgram;
ipDisplayCycle = pDisplayCycle;
strcpy(iszProgName, szProgName);
SetLidTarget(lidTemp);
}
void Thermocycler::Stop() {
if (iProgramState != EOff)
iProgramState = EStopped;
ipProgram = NULL;
ipCurrentStep = NULL;
iStepPool.ResetPool();
iCyclePool.ResetPool();
ipDisplay->Clear();
}
PcrStatus Thermocycler::Start() {
if (ipProgram == NULL)
return ENoProgram;
if (iProgramState == EOff)
return ENoPower;
//advance to lid wait state
iProgramState = ELidWait;
return ESuccess;
}
// internal
void Thermocycler::Loop() {
CheckPower();
ReadPlateTemp();
ReadLidTemp();
switch (iProgramState) {
case EStartup:
if (millis() - iProgramStartTimeMs > STARTUP_DELAY) {
iProgramState = EStopped;
if (!iRestarted && !ipSerialControl->CommandReceived()) {
//check for stored program
SCommand command;
if (ProgramStore::RetrieveProgram(command, (char*)ipSerialControl->GetBuffer()))
ProcessCommand(command);
}
}
break;
case ELidWait:
if (iLidTemp >= iTargetLidTemp - LID_START_TOLERANCE) {
//advance to running state
//calculate program time params
ipProgram->BeginIteration();
Step* pStep;
double lastTemp = iPlateTemp;
iProgramHoldDurationS = 0;
iProgramRampDegrees = 0;
iElapsedRampDurationMs = 0;
iElapsedRampDegrees = 0;
iEstimatedTimeRemainingS = 0;
iHasCooled = false;
while ((pStep = ipProgram->GetNextStep()) && !pStep->IsFinal()) {
iProgramHoldDurationS += pStep->GetDuration();
if (lastTemp != pStep->GetTemp())
iProgramRampDegrees += absf(lastTemp - pStep->GetTemp()) - CYCLE_START_TOLERANCE;
lastTemp = pStep->GetTemp();
}
iProgramState = ERunning;
iThermalDirection = OFF;
iPeltierPwm = 0;
ipProgram->BeginIteration();
ipCurrentStep = ipProgram->GetNextStep();
SetPlateTarget(ipCurrentStep->GetTemp());
iRamping = true;
iProgramStartTimeMs = millis();
}
break;
case ERunning:
//update program
if (iProgramState == ERunning) {
if (iRamping && abs(ipCurrentStep->GetTemp() - iPlateTemp) <= CYCLE_START_TOLERANCE) {
//eta updates
iElapsedRampDegrees += absf(iPlateTemp - iRampStartTemp);
iElapsedRampDurationMs += millis() - iRampStartTime;
if (iRampStartTemp > iPlateTemp)
iHasCooled = true;
iRamping = false;
iCycleStartTime = millis();
} else if (!iRamping && !ipCurrentStep->IsFinal() && millis() - iCycleStartTime > (unsigned long)ipCurrentStep->GetDuration() * 1000) {
float prevTemp = ipCurrentStep->GetTemp();
ipCurrentStep = ipProgram->GetNextStep();
if (ipCurrentStep != NULL)
SetPlateTarget(ipCurrentStep->GetTemp());
//check for program completion
if (ipCurrentStep == NULL || ipCurrentStep->GetDuration() == 0)
iProgramState = EComplete;
}
}
break;
case EComplete:
if (iRamping && ipCurrentStep != NULL && abs(ipCurrentStep->GetTemp() - iPlateTemp) <= CYCLE_START_TOLERANCE)
iRamping = false;
break;
}
ControlPeltier();
ControlLid();
UpdateEta();
ipDisplay->Update();
ipSerialControl->Process();
}
void Thermocycler::CheckPower() {
float voltage = analogRead(0) * 5.0 / 1024 * 10 / 3; // 10/3 is for voltage divider
boolean externalPower = digitalRead(A0); //voltage > 7.0;
if (externalPower && iProgramState == EOff) {
iProgramState = EStartup;
iProgramStartTimeMs = millis();
} else if (!externalPower && iProgramState != EOff) {
Stop();
iProgramState = EOff;
}
}
//private
void Thermocycler::ReadLidTemp() {
unsigned long voltage_mv = (unsigned long)analogRead(1) * 5000 / 1024;
unsigned long resistance = voltage_mv * 2200 / (5000 - voltage_mv);
iLidTemp = TableLookup(LID_RESISTANCE_TABLE, sizeof(LID_RESISTANCE_TABLE) / sizeof(LID_RESISTANCE_TABLE[0]), 0, resistance);
}
char spi_transfer(volatile char data)
{
SPDR = data; // Start the transmission
while (!(SPSR & (1<> 7) & 0x01) + ((unsigned long)spiBuf[2] << 1) + ((unsigned long)spiBuf[1] << 9) + (((unsigned long)spiBuf[0] & 0x1F) << 17); //((spiBuf[0] & 0x1F) << 16) + (spiBuf[1] << 8) + spiBuf[2];
unsigned long adcDivisor = 0x1FFFFF;
float voltage = (float)conv * 5.0 / adcDivisor;
unsigned int convHigh = (conv >> 16);
digitalWrite(SLAVESELECT, HIGH);
unsigned long voltage_mv = voltage * 1000;
unsigned long resistance = voltage_mv * 22000 / (5000 - voltage_mv); // in hecto ohms
iPlateTemp = TableLookup(PLATE_RESISTANCE_TABLE, sizeof(PLATE_RESISTANCE_TABLE) / sizeof(PLATE_RESISTANCE_TABLE[0]), -40, resistance);
}
void Thermocycler::SetPlateTarget(double target) {
if (iTargetPlateTemp != target) {
iRamping = true;
iRampStartTime = millis();
iRampStartTemp = iPlateTemp;
} else {
iCycleStartTime = millis(); //next step starts immediately
}
iTargetPlateTemp = target;
if (absf(iTargetPlateTemp - iPlateTemp) >= PLATE_BANGBANG_THRESHOLD) {
iPlateControlMode = EBangBang;
iPlatePid.SetMode(MANUAL);
} else {
iPlateControlMode = EPID;
iPlatePid.SetMode(AUTOMATIC);
}
if (iRamping) {
if (iTargetPlateTemp >= iPlateTemp) {
iDecreasing = false;
if (iTargetPlateTemp < PLATE_PID_INC_LOW_THRESHOLD)
iPlatePid.SetTunings(PLATE_PID_INC_LOW_P, PLATE_PID_INC_LOW_I, PLATE_PID_INC_LOW_D);
else
iPlatePid.SetTunings(PLATE_PID_INC_P, PLATE_PID_INC_I, PLATE_PID_INC_D);
} else {
iDecreasing = true;
if (iTargetPlateTemp > PLATE_PID_DEC_HIGH_THRESHOLD)
iPlatePid.SetTunings(PLATE_PID_DEC_HIGH_P, PLATE_PID_DEC_HIGH_I, PLATE_PID_DEC_HIGH_D);
else if (iTargetPlateTemp < PLATE_PID_DEC_LOW_THRESHOLD)
iPlatePid.SetTunings(PLATE_PID_DEC_LOW_P, PLATE_PID_DEC_LOW_I, PLATE_PID_DEC_LOW_D);
else
iPlatePid.SetTunings(PLATE_PID_DEC_P, PLATE_PID_DEC_I, PLATE_PID_DEC_D);
}
}
}
void Thermocycler::SetLidTarget(double target) {
iTargetLidTemp = target;
if (absf(iTargetLidTemp - iLidTemp) >= LID_BANGBANG_THRESHOLD) {
iLidControlMode = EBangBang;
iLidPid.SetMode(MANUAL);
} else {
iLidControlMode = EPID;
iLidPid.SetMode(AUTOMATIC);
}
}
void Thermocycler::ControlPeltier() {
ThermalDirection newDirection = OFF;
if (iProgramState == ERunning || (iProgramState == EComplete && ipCurrentStep != NULL)) {
// Check whether we should switch to PID control
if (iPlateControlMode == EBangBang && absf(iTargetPlateTemp - iPlateTemp) < PLATE_BANGBANG_THRESHOLD) {
iPlateControlMode = EPID;
iPlatePid.SetMode(AUTOMATIC);
iPlatePid.ResetI();
}
// Apply control mode
if (iPlateControlMode == EBangBang) {
iPeltierPwm = iTargetPlateTemp > iPlateTemp ? MAX_PELTIER_PWM : MIN_PELTIER_PWM;
}
iPlatePid.Compute();
if (iDecreasing && iTargetPlateTemp > PLATE_PID_DEC_LOW_THRESHOLD) {
if (iTargetPlateTemp < iPlateTemp)
iPlatePid.ResetI();
else
iDecreasing = false;
}
if (iPeltierPwm > 0)
newDirection = HEAT;
else if (iPeltierPwm < 0)
newDirection = COOL;
else
newDirection = OFF;
} else {
iPeltierPwm = 0;
}
iThermalDirection = newDirection;
SetPeltier(newDirection, abs(iPeltierPwm));
}
void Thermocycler::ControlLid() {
double drive = 0;
if (iProgramState == ERunning || iProgramState == ELidWait) {
// Check whether we should switch to PID control
if (iLidControlMode == EBangBang && absf(iTargetLidTemp - iLidTemp) < LID_BANGBANG_THRESHOLD) {
iLidControlMode = EPID;
iLidPid.SetMode(AUTOMATIC);
iLidPid.ResetI();
}
if (iLidControlMode == EBangBang) {
iLidPwm = iTargetLidTemp > iLidTemp ? MAX_LID_PWM : MIN_LID_PWM;
}
iLidPid.Compute();
drive = iLidPwm;
} else {
iLidPwm = 0;
}
analogWrite(3, drive);
}
void Thermocycler::UpdateEta() {
if (iProgramState == ERunning) {
double secondPerDegree;
if (iElapsedRampDegrees == 0 || !iHasCooled)
secondPerDegree = 1.0;
else
secondPerDegree = iElapsedRampDurationMs / 1000 / iElapsedRampDegrees;
unsigned long estimatedDurationS = iProgramHoldDurationS + iProgramRampDegrees * secondPerDegree;
unsigned long elapsedTimeS = GetElapsedTimeS();
iEstimatedTimeRemainingS = estimatedDurationS > elapsedTimeS ? estimatedDurationS - elapsedTimeS : 0;
}
}
void Thermocycler::SetPeltier(ThermalDirection dir, int pwm) {
if (dir == COOL) {
digitalWrite(2, HIGH);
digitalWrite(4, LOW);
} else if (dir == HEAT) {
digitalWrite(2, LOW);
digitalWrite(4, HIGH);
} else {
digitalWrite(2, LOW);
digitalWrite(4, LOW);
}
analogWrite(9, pwm);
}
void Thermocycler::ProcessCommand(SCommand& command) {
if (command.command == SCommand::EStart) {
//find display cycle
Cycle* pProgram = command.pProgram;
Cycle* pDisplayCycle = pProgram;
int largestCycleCount = 0;
for (int i = 0; i < pProgram->GetNumComponents(); i++) {
ProgramComponent* pComp = pProgram->GetComponent(i);
if (pComp->GetType() == ProgramComponent::ECycle) {
Cycle* pCycle = (Cycle*)pComp;
if (pCycle->GetNumCycles() > largestCycleCount) {
largestCycleCount = pCycle->GetNumCycles();
pDisplayCycle = pCycle;
}
}
}
//start program by persisting and resetting device to overcome memory leak in C library
GetThermocycler().SetProgram(pProgram, pDisplayCycle, command.name, command.lidTemp);
GetThermocycler().Start();
} else if (command.command == SCommand::EStop) {
GetThermocycler().Stop(); //redundant as we already stopped during parsing
} else if (command.command == SCommand::EConfig) {
//update displayed
ipDisplay->SetContrast(command.contrast);
//update stored contrast
ProgramStore::StoreContrast(command.contrast);
}
}
uint8_t Thermocycler::mcp342xRead(int32_t &data)
{
// pointer used to form int32 data
uint8_t *p = (uint8_t *)&data;
// timeout - not really needed?
uint32_t start = millis();
do {
// assume 18-bit mode
Wire.requestFrom(MCP3422_ADDRESS, 4);
if (Wire.available() != 4) {
return false;
}
for (int8_t i = 2; i >= 0; i--) {
p[i] = Wire.receive();
}
// extend sign bits
p[3] = p[2] & 0X80 ? 0XFF : 0;
// read config/status byte
uint8_t s = Wire.receive();
if ((s & MCP342X_RES_FIELD) != MCP342X_18_BIT) {
// not 18 bits - shift bytes for 12, 14, or 16 bits
p[0] = p[1];
p[1] = p[2];
p[2] = p[3];
}
if ((s & MCP342X_BUSY) == 0) return true;
} while (millis() - start < 500); //allows rollover of millis()
return false;
}
//------------------------------------------------------------------------------
// write mcp342x configuration byte
uint8_t Thermocycler::mcp342xWrite(uint8_t config)
{
Wire.beginTransmission(MCP3422_ADDRESS);
Wire.send(config);
Wire.endTransmission();
}
//------------------------------------------------------------------------------
float Thermocycler::TableLookup(const unsigned long lookupTable[], unsigned int tableSize, int startValue, unsigned long searchValue) {
//simple linear search for now
int i;
for (i = 0; i < tableSize; i++) {
if (searchValue >= pgm_read_dword_near(lookupTable + i))
break;
}
if (i > 0) {
unsigned long high_val = pgm_read_dword_near(lookupTable + i - 1);
unsigned long low_val = pgm_read_dword_near(lookupTable + i);
return i + startValue - (float)(searchValue - low_val) / (float)(high_val - low_val);
} else {
return startValue;
}
}
//------------------------------------------------------------------------------
float Thermocycler::TableLookup(const unsigned int lookupTable[], unsigned int tableSize, int startValue, unsigned long searchValue) {
//simple linear search for now
int i;
for (i = 0; i < tableSize; i++) {
if (searchValue >= pgm_read_word_near(lookupTable + i))
break;
}
if (i > 0) {
unsigned long high_val = pgm_read_word_near(lookupTable + i - 1);
unsigned long low_val = pgm_read_word_near(lookupTable + i);
return i + startValue - (float)(searchValue - low_val) / (float)(high_val - low_val);
} else {
return startValue;
}
}