#include <Wire.h>
#include <EEPROM.h>
// --- Pin Definitions ---
const int PIN_NTC = A0;
const int PIN_RELAY = 2;
const int PIN_BTN_MODE = 3;
const int PIN_BTN_UP = 9;
const int PIN_BTN_DOWN = 10;
const int PIN_LED_LOW = 11;
const int PIN_LED_HIGH = 12;
// --- Minimal I2C LCD Driver ---
#define LCD_ADDR 0x27
void lcd_send(uint8_t val, uint8_t mode) {
uint8_t high_nibble = val & 0xF0;
uint8_t low_nibble = (val << 4) & 0xF0;
uint8_t data[4];
data[0] = high_nibble | mode | 0x04;
data[1] = high_nibble | mode & ~0x04;
data[2] = low_nibble | mode | 0x04;
data[3] = low_nibble | mode & ~0x04;
Wire.beginTransmission(LCD_ADDR);
Wire.write(data, 4);
Wire.endTransmission();
}
void lcd_cmd(uint8_t cmd) { lcd_send(cmd, 0x08); }
void lcd_data(uint8_t data) { lcd_send(data, 0x09); }
void lcd_init() {
Wire.begin();
delay(50);
lcd_cmd(0x33);
lcd_cmd(0x32);
lcd_cmd(0x28);
lcd_cmd(0x0C);
lcd_cmd(0x01);
delay(2);
}
void lcd_set_cursor(uint8_t col, uint8_t row) {
uint8_t row_offsets[] = { 0x00, 0x40 };
lcd_cmd(0x80 | (col + row_offsets[row]));
}
void lcd_print(const char* str) {
while (*str) lcd_data(*str++);
}
void lcd_print_val(int32_t val, bool is_tenths) {
if (val < 0) {
lcd_data('-');
val = -val;
}
int32_t whole = is_tenths ? (val / 10) : val;
uint8_t frac = is_tenths ? (val % 10) : 0;
char buf[10];
int idx = 0;
if (whole == 0) {
buf[idx++] = '0';
} else {
while (whole > 0) {
buf[idx++] = (whole % 10) + '0';
whole /= 10;
}
}
for (int i = idx - 1; i >= 0; i--) {
lcd_data(buf[i]);
}
if (is_tenths) {
lcd_data('.');
lcd_data(frac + '0');
}
}
// --- Menu States ---
enum MenuState { SET_SV, SET_AL, SET_P, SET_I, SET_D, SET_T, RUN_MODE };
MenuState currentState = RUN_MODE;
// --- Controller Parameters (Scaled by 10) ---
int32_t PV_10x = 0;
int32_t SV_10x = 500;
int32_t AL_dev_10x = 20;
int32_t P_band_10x = 100;
int32_t I_time = 0;
int32_t D_time = 0;
int32_t T_period_10x = 200;
// --- EEPROM Management ---
// A unique signature to tell if the board has been flashed/saved before
const uint32_t EEPROM_MAGIC = 0x7C450001;
void saveSettings() {
EEPROM.put(0, EEPROM_MAGIC);
EEPROM.put(4, SV_10x);
EEPROM.put(8, AL_dev_10x);
EEPROM.put(12, P_band_10x);
EEPROM.put(16, I_time);
EEPROM.put(20, D_time);
EEPROM.put(24, T_period_10x);
}
void loadSettings() {
uint32_t magic;
EEPROM.get(0, magic);
// If the signature matches, load the saved data
if (magic == EEPROM_MAGIC) {
EEPROM.get(4, SV_10x);
EEPROM.get(8, AL_dev_10x);
EEPROM.get(12, P_band_10x);
EEPROM.get(16, I_time);
EEPROM.get(20, D_time);
EEPROM.get(24, T_period_10x);
} else {
// First time running on a new board!
// Save the hardcoded defaults so they exist next time.
saveSettings();
}
}
// --- PID Variables ---
int32_t integral_halves = 0;
int32_t last_error_10x = 0;
unsigned long lastPIDTime = 0;
int32_t MV_10x = 0;
// --- Relay PWM Variables ---
unsigned long periodStartTime = 0;
// --- Button Debouncing ---
unsigned long lastButtonPress = 0;
bool needsDisplayUpdate = true;
void setup() {
pinMode(PIN_RELAY, OUTPUT);
pinMode(PIN_LED_LOW, OUTPUT);
pinMode(PIN_LED_HIGH, OUTPUT);
pinMode(PIN_BTN_MODE, INPUT_PULLUP);
pinMode(PIN_BTN_UP, INPUT_PULLUP);
pinMode(PIN_BTN_DOWN, INPUT_PULLUP);
lcd_init();
loadSettings(); // Load variables from memory on startup
}
void loop() {
handleButtons();
readTemperature();
computePID();
driveRelay();
checkAlarms();
updateDisplay();
}
void handleButtons() {
static unsigned long modePressTime = 0;
static bool modeHandled = false;
// --- Mode Button Logic (Short = Cycle Menu, Long = Exit & Save) ---
if (digitalRead(PIN_BTN_MODE) == LOW) {
if (modePressTime == 0) {
modePressTime = millis(); // Start timing the press
modeHandled = false;
} else if (!modeHandled && (millis() - modePressTime >= 2000)) {
// Long press (>2 seconds) triggered while holding
modeHandled = true;
if (currentState != RUN_MODE) {
saveSettings(); // Save immediately
currentState = RUN_MODE; // Escape to default screen
lcd_cmd(0x01); // Clear LCD
delay(2);
needsDisplayUpdate = true;
}
}
} else {
// Mode Button released
if (modePressTime > 0) {
unsigned long duration = millis() - modePressTime;
// Short press (Debounced > 50ms, but less than 2000ms)
if (!modeHandled && duration > 50) {
// Custom cycling logic that prevents returning to RUN_MODE
if (currentState == RUN_MODE) {
currentState = SET_SV; // Enter menu from default screen
} else if (currentState == SET_T) {
currentState = SET_SV; // Reached the end, loop back to the start of the menu!
} else {
// Normal cycling to the next state
currentState = static_cast<MenuState>(currentState + 1);
}
lcd_cmd(0x01);
delay(2);
needsDisplayUpdate = true;
}
modePressTime = 0; // Reset for the next press
}
}
// --- Up and Down Button Logic ---
if (millis() - lastButtonPress < 200) return;
int32_t increment = 0;
if (digitalRead(PIN_BTN_UP) == LOW) increment = 1;
if (digitalRead(PIN_BTN_DOWN) == LOW) increment = -1;
if (increment != 0 && currentState != RUN_MODE) {
switch (currentState) {
case SET_SV: SV_10x += increment * 10; break;
case SET_AL: AL_dev_10x += increment * 10; if(AL_dev_10x < 0) AL_dev_10x = 0; if(AL_dev_10x > 500) AL_dev_10x = 500; break;
case SET_P: P_band_10x += increment; if(P_band_10x < 1) P_band_10x = 1; if(P_band_10x > 9999) P_band_10x = 9999; break;
case SET_I: I_time += increment; if(I_time < 0) I_time = 0; if(I_time > 9999) I_time = 9999; break;
case SET_D: D_time += increment; if(D_time < 0) D_time = 0; if(D_time > 9999) D_time = 9999; break;
case SET_T: T_period_10x += increment * 5; if(T_period_10x < 5) T_period_10x = 5; if(T_period_10x > 1200) T_period_10x = 1200; break;
default: break;
}
lastButtonPress = millis();
needsDisplayUpdate = true;
}
}
void readTemperature() {
const int16_t ntc_lut[17] = {
1500, 1015, 762, 621, 518, 438, 369, 307,
249, 194, 139, 82, 21, -48, -133, -258, -400
};
int32_t adc = analogRead(PIN_NTC);
if (adc > 1023) adc = 1023;
int index = adc / 64;
int remainder = adc % 64;
if (index >= 16) {
PV_10x = ntc_lut[16];
} else {
int32_t val1 = ntc_lut[index];
int32_t val2 = ntc_lut[index + 1];
PV_10x = val1 + ((val2 - val1) * remainder) / 64;
}
}
void checkAlarms() {
if (PV_10x > (SV_10x + AL_dev_10x)) {
digitalWrite(PIN_LED_HIGH, HIGH);
digitalWrite(PIN_LED_LOW, LOW);
} else if (PV_10x < (SV_10x - AL_dev_10x)) {
digitalWrite(PIN_LED_HIGH, LOW);
digitalWrite(PIN_LED_LOW, HIGH);
} else {
digitalWrite(PIN_LED_HIGH, LOW);
digitalWrite(PIN_LED_LOW, LOW);
}
}
void computePID() {
unsigned long now = millis();
if (now - lastPIDTime >= 500) {
int32_t error_10x = SV_10x - PV_10x;
int32_t P_out_10x = (1000L * error_10x) / P_band_10x;
int32_t I_out_10x = 0;
if (I_time > 0) {
integral_halves += error_10x;
int32_t max_accum = (1000L * P_band_10x * I_time) / 500L;
if (integral_halves > max_accum) integral_halves = max_accum;
if (integral_halves < -max_accum) integral_halves = -max_accum;
I_out_10x = (500L * integral_halves) / (P_band_10x * I_time);
} else {
integral_halves = 0;
}
int32_t diff_10x = error_10x - last_error_10x;
int32_t D_out_10x = (2000L * D_time * diff_10x) / P_band_10x;
MV_10x = P_out_10x + I_out_10x + D_out_10x;
if (MV_10x < 0) MV_10x = 0;
if (MV_10x > 1000) MV_10x = 1000;
last_error_10x = error_10x;
lastPIDTime = now;
needsDisplayUpdate = true;
}
}
void driveRelay() {
unsigned long now = millis();
unsigned long periodMillis = T_period_10x * 100;
if (now - periodStartTime >= periodMillis) {
periodStartTime = now;
}
unsigned long onTime = (MV_10x * periodMillis) / 1000;
if (now - periodStartTime < onTime) {
digitalWrite(PIN_RELAY, HIGH);
} else {
digitalWrite(PIN_RELAY, LOW);
}
}
void updateDisplay() {
if (!needsDisplayUpdate) return;
lcd_set_cursor(0, 0);
switch (currentState) {
case RUN_MODE:
lcd_print("PV: "); lcd_print_val(PV_10x, true); lcd_print(" C ");
lcd_set_cursor(0, 1);
lcd_print("SV: "); lcd_print_val(SV_10x, true); lcd_print(" C ");
break;
case SET_SV:
lcd_print("Set SV: ");
lcd_set_cursor(0, 1); lcd_print_val(SV_10x, true); lcd_print(" C ");
break;
case SET_AL:
lcd_print("Set Alarm Dev: ");
lcd_set_cursor(0, 1); lcd_print_val(AL_dev_10x, true); lcd_print(" C ");
break;
case SET_P:
lcd_print("Set P Band: ");
lcd_set_cursor(0, 1); lcd_print_val(P_band_10x, true); lcd_print(" C ");
break;
case SET_I:
lcd_print("Set I Time: ");
lcd_set_cursor(0, 1); lcd_print_val(I_time, false); lcd_print(" s ");
break;
case SET_D:
lcd_print("Set D Time: ");
lcd_set_cursor(0, 1); lcd_print_val(D_time, false); lcd_print(" s ");
break;
case SET_T:
lcd_print("Set T Period: ");
lcd_set_cursor(0, 1); lcd_print_val(T_period_10x, true); lcd_print(" s ");
break;
}
needsDisplayUpdate = false;
}Mode
Up
Down