diff --git a/Firmware/pcb_reflow_fw/pcb_reflow_fw.ino b/Firmware/pcb_reflow_fw/pcb_reflow_fw.ino new file mode 100644 index 0000000..48c50a8 --- /dev/null +++ b/Firmware/pcb_reflow_fw/pcb_reflow_fw.ino @@ -0,0 +1,910 @@ +/* Solder Reflow Plate Sketch + * H/W - Ver Spatz-1.0 + * S/W - Ver 0.35 + * by Chris Halsall and Nathan Heidt */ + +/* To prepare + * 1) Install MiniCore in additional boards; (copy into File->Preferences->Additional Boards Manager + * URLs) https://mcudude.github.io/MiniCore/package_MCUdude_MiniCore_index.json 2) Then add MiniCore + * by searching and installing (Tools->Board->Board Manager) 3) Install Adafruit_GFX and + * Adafruit_SSD1306 libraries (Tools->Manage Libraries) + */ + +/* To program + * 1) Select the following settings under (Tools) + * Board->Minicore->Atmega328 + * Clock->Internal 8MHz + * BOD->BOD 2.7V + * EEPROM->EEPROM retained + * Compiler LTO->LTO Disabled + * Variant->328P / 328PA + * Bootloader->No bootloader + * 2) Set programmer of choice, e.g.'Arduino as ISP (MiniCore)', 'USB ASP', etc, and set correct + * port. 3) Burn bootloader (to set fuses correctly) 4) Compile and upload + * + * + * TODOS: + * - add digital sensor setup routine if they are detected, but not setup + * - figure out a method for how to use all the temperature sensors + * - implement an observer/predictor for the temperature sensors. Kalman filter time?!? + */ + +#include +#include +#include +#include +#include +#include + +// Version Definitions +static const PROGMEM float hw = 0.9; +static const PROGMEM float sw = 0.15; + +// Screen Definitions +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 32 +#define SCREEN_ADDRESS 0x3C // I2C Address +Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); // Create Display + +// Pin Definitions +#define MOSFET_PIN PIN_PC3 +#define UPSW_PIN PIN_PF3 +#define DNSW_PIN PIN_PD4 +#define TEMP_PIN PIN_PF2 // A2 +#define VCC_PIN PIN_PF4 // A0 +#define LED_GREEN_PIN PIN_PC5 +#define LED_RED_PIN PIN_PC4 +#define ONE_WIRE_BUS PIN_PD5 + +#define MOSFET_PIN_OFF 255 + +enum menu_state_t { MENU_IDLE, MENU_SELECT_PROFILE, MENU_HEAT, MENU_INC_TEMP, MENU_DEC_TEMP }; +enum buttons_state_t { BUTTONS_NO_PRESS, BUTTONS_BOTH_PRESS, BUTTONS_UP_PRESS, BUTTONS_DN_PRESS }; +enum single_button_state_t { BUTTON_PRESSED, BUTTON_RELEASED, BUTTON_NO_ACTION }; + +// Button interrupt state +volatile single_button_state_t up_button_state = BUTTON_NO_ACTION; +volatile single_button_state_t dn_button_state = BUTTON_NO_ACTION; +volatile unsigned long up_state_change_time = 0; +volatile unsigned long down_state_change_time = 0; + +// Temperature Info +byte max_temp_array[] = {140, 150, 160, 170, 180}; +byte max_temp_index = 0; +#define MAX_RESISTANCE 10.0 +float bed_resistance = 1.88; +#define MAX_AMPERAGE 5.0 +#define PWM_VOLTAGE_SCALAR 2.0 + +// These values were derived using a regression from real world data. +// See the jupyter notebooks for more detail +#define ANALOG_APPROXIMATION_SCALAR 1.612 +#define ANALOG_APPROXIMATION_OFFSET -20.517 + +// EEPROM storage locations +#define CRC_ADDR 0 +#define FIRSTTIME_BOOT_ADDR 4 +#define TEMP_INDEX_ADDR 5 +#define RESISTANCE_INDEX_ADDR 6 +#define DIGITAL_TEMP_ID_ADDR 10 + +// Voltage Measurement Info +#define VOLTAGE_REFERENCE 1.5 + + +// Solder Reflow Plate Logo +static const uint8_t PROGMEM logo[] = { + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x1f, 0xc0, 0x00, 0x31, 0x80, 0x00, 0x00, 0x00, 0x00, + 0x1f, 0xe0, 0x03, 0x01, 0x80, 0x00, 0x00, 0x30, 0x70, 0x00, 0x21, 0x80, 0x00, 0x00, 0x00, 0x00, + 0x10, 0x20, 0x03, 0x00, 0xc7, 0x80, 0x00, 0x20, 0x18, 0xf0, 0x61, 0x80, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x03, 0x3e, 0xcc, 0xc0, 0xc0, 0x04, 0x19, 0x98, 0x61, 0x80, 0x00, 0x00, 0x00, 0x00, + 0x1c, 0x01, 0xf3, 0x77, 0xd8, 0xc7, 0xe0, 0x06, 0x33, 0x18, 0x61, 0x8f, 0x88, 0x00, 0x00, 0x00, + 0x06, 0x03, 0x3b, 0x61, 0xd0, 0xc6, 0x00, 0x07, 0xe2, 0x18, 0x61, 0x98, 0xd8, 0x04, 0x00, 0x00, + 0x01, 0xc6, 0x0b, 0x60, 0xd9, 0x86, 0x00, 0x06, 0x03, 0x30, 0xff, 0xb0, 0x78, 0x66, 0x00, 0x00, + 0x40, 0xe4, 0x0f, 0x60, 0xdf, 0x06, 0x00, 0x07, 0x03, 0xe0, 0x31, 0xe0, 0x78, 0x62, 0x00, 0x00, + 0x40, 0x3c, 0x0f, 0x61, 0xd8, 0x06, 0x00, 0x07, 0x83, 0x00, 0x31, 0xe0, 0x78, 0x63, 0x00, 0x00, + 0x60, 0x36, 0x1b, 0x63, 0xc8, 0x02, 0x00, 0x02, 0xc1, 0x00, 0x18, 0xb0, 0xcc, 0xe2, 0x00, 0x00, + 0x30, 0x33, 0x3b, 0x36, 0x4e, 0x03, 0x00, 0x02, 0x61, 0xc0, 0x0c, 0x99, 0xcd, 0xfe, 0x00, 0x00, + 0x0f, 0xe1, 0xe1, 0x3c, 0x03, 0xf3, 0x00, 0x02, 0x38, 0x7e, 0x0c, 0x8f, 0x07, 0x9c, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7f, 0x84, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xc0, 0xe4, 0x00, 0x18, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x3c, 0x3c, 0x18, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x1e, 0x06, 0x7f, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x3e, 0x03, 0x18, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x36, 0x7f, 0x19, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x07, 0xe6, 0xc7, 0x19, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x07, 0x83, 0x18, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x07, 0x81, 0x18, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x06, 0xc3, 0x98, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x04, 0x7e, 0x08, 0x3f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +static const uint8_t logo_width = 128; +static const uint8_t logo_height = 27; + +// Heating Animation +static const uint8_t PROGMEM heat_animate[] = { + 0b00000001, 0b00000000, 0b00000001, 0b10000000, 0b00000001, 0b10000000, 0b00000001, 0b01000000, + 0b00000010, 0b01000000, 0b00100010, 0b01000100, 0b00100100, 0b00100100, 0b01010101, 0b00100110, + 0b01001001, 0b10010110, 0b10000010, 0b10001001, 0b10100100, 0b01000001, 0b10011000, 0b01010010, + 0b01000100, 0b01100010, 0b00100011, 0b10000100, 0b00011000, 0b00011000, 0b00000111, 0b11100000}; +static const uint8_t heat_animate_width = 16; +static const uint8_t heat_animate_height = 16; + +// Tick +static const uint8_t PROGMEM tick[] = { + 0b00000000, 0b00000100, 0b00000000, 0b00001010, 0b00000000, 0b00010101, 0b00000000, 0b00101010, + 0b00000000, 0b01010100, 0b00000000, 0b10101000, 0b00000001, 0b01010000, 0b00100010, 0b10100000, + 0b01010101, 0b01000000, 0b10101010, 0b10000000, 0b01010101, 0b00000000, 0b00101010, 0b00000000, + 0b00010100, 0b00000000, 0b00001000, 0b00000000, 0b01111111, 0b11100000}; +static const uint8_t tick_width = 16; +static const uint8_t tick_height = 15; + +// This needs to be specified or the compiler will fail as you can't initialize a +// flexible array member in a nested context +#define MAX_PROFILE_LENGTH 8 + +// TODO(HEIDT) may need to switch away from floats for speed/sizeA +struct solder_profile_t { + uint8_t points; + float seconds[MAX_PROFILE_LENGTH]; + float fraction[MAX_PROFILE_LENGTH]; +}; + +// TODO(HEIDT) how to adjust for environments where the board starts hot or cold? +// profiles pulled from here: https://www.compuphase.com/electronics/reflowsolderprofiles.htm#_ +#define NUM_PROFILES 2 +const static solder_profile_t profiles[NUM_PROFILES] = { + {.points = 4, .seconds = {90, 180, 240, 260}, .fraction = {.65, .78, 1.00, 1.00}}, + {.points = 2, .seconds = {162.0, 202.0}, .fraction = {.95, 1.00}}}; + +// temperature must be within this range to move on to next step +#define TARGET_TEMP_THRESHOLD 2.5 + +// PID values +float kI = 0.2; +float kD = 0.25; +float kP = 8.0; +float I_clip = 220; +float error_I = 0; + +// Optional temperature sensor +OneWire oneWire(ONE_WIRE_BUS); +DallasTemperature sensors(&oneWire); +int sensor_count = 0; +DeviceAddress temp_addresses[3]; + +#define DEBUG + +#ifdef DEBUG +#define debugprint(x) Serial.print(x); +#define debugprintln(x) Serial.println(x); +#else +#define debugprint(x) +#define debugprintln(x) +#endif + +// -------------------- Function prototypes ----------------------------------- +void inline heatAnimate(int &x, int &y, float v, float t, float target_temp); + +// -------------------- Function definitions ---------------------------------- + +void dnsw_change_isr() { + dn_button_state = BUTTON_PRESSED; + down_state_change_time = millis(); +} + +void upsw_change_isr() { + up_button_state = BUTTON_PRESSED; + up_state_change_time = millis(); +} + +void setup() { + + // Pin Direction control + pinMode(MOSFET_PIN, OUTPUT); + pinMode(UPSW_PIN, INPUT); + pinMode(DNSW_PIN, INPUT); + pinMode(TEMP_PIN, INPUT); + pinMode(VCC_PIN, INPUT); + pinMode(LED_GREEN_PIN, OUTPUT); + + digitalWrite(LED_GREEN_PIN, HIGH); + analogWrite(MOSFET_PIN, 255); // VERY IMPORTANT, DONT CHANGE! + + attachInterrupt(DNSW_PIN, dnsw_change_isr, FALLING); + attachInterrupt(UPSW_PIN, upsw_change_isr, FALLING); + + Serial.begin(9600); + + // Enable Fast PWM with no prescaler + setFastPwm(); + setVREF(); + + // Start-up Diplay + debugprintln("Showing startup"); + showLogo(); + + debugprintln("Checking sensors"); + // check onewire TEMP_PIN sensors + setupSensors(); + + debugprintln("Checking first boot"); + if (isFirstBoot() || !validateCRC()) { + doSetup(); + } + + // Pull saved values from EEPROM + max_temp_index = getMaxTempIndex(); + bed_resistance = getResistance(); + + debugprintln("Entering main menu"); + // Go to main menu + mainMenu(); +} + +void updateCRC() { + uint32_t new_crc = eepromCRC(); + setCRC(new_crc); +} + +bool validateCRC() { + uint32_t stored_crc; + EEPROM.get(CRC_ADDR, stored_crc); + uint32_t calculated_crc = eepromCRC(); + debugprint("got CRCs, stored: "); + debugprint(stored_crc); + debugprint(", calculated: "); + debugprintln(calculated_crc); + return stored_crc == calculated_crc; +} + +void setCRC(uint32_t new_crc) { EEPROM.put(CRC_ADDR, new_crc); } + +uint32_t eepromCRC(void) { + static const uint32_t crc_table[16] = {0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, + 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, + 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, + 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c}; + uint32_t crc = ~0L; + // Skip first 4 bytes of EEPROM as thats where we store the CRC + for (int index = 4; index < EEPROM.length(); ++index) { + crc = crc_table[(crc ^ EEPROM[index]) & 0x0f] ^ (crc >> 4); + crc = crc_table[(crc ^ (EEPROM[index] >> 4)) & 0x0f] ^ (crc >> 4); + crc = ~crc; + } + + return crc; +} + +inline void setupSensors() { + sensors.begin(); + sensor_count = sensors.getDeviceCount(); + debugprint("Looking for sensors, found: "); + debugprintln(sensor_count); + for (int i = 0; i < min(sensor_count, sizeof(temp_addresses)); i++) { + sensors.getAddress(temp_addresses[i], i); + } +} + +inline void setFastPwm() { analogWriteFrequency(64); } + +inline void setVREF() { analogReference(INTERNAL1V5); } + +inline bool isFirstBoot() { + uint8_t first_boot = EEPROM.read(FIRSTTIME_BOOT_ADDR); + debugprint("Got first boot flag: "); + debugprintln(first_boot); + return first_boot != 1; +} + +inline void setFirstBoot() { + EEPROM.write(FIRSTTIME_BOOT_ADDR, 1); + updateCRC(); +} + +inline float getResistance() { + float f; + return EEPROM.get(RESISTANCE_INDEX_ADDR, f); + return f; +} + +inline void setResistance(float resistance) { + EEPROM.put(RESISTANCE_INDEX_ADDR, resistance); + updateCRC(); +} + +inline void setMaxTempIndex(int index) { + EEPROM.update(TEMP_INDEX_ADDR, index); + updateCRC(); +} + +inline int getMaxTempIndex(void) { return EEPROM.read(TEMP_INDEX_ADDR) % sizeof(max_temp_array); } + +void showLogo() { + unsigned long start_time = millis(); + display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS); + while (start_time + 2000 > millis()) { + display.clearDisplay(); + display.setTextSize(1); + display.setTextColor(SSD1306_WHITE); + display.setCursor(0, 0); + display.drawBitmap(0, 0, logo, logo_width, logo_height, SSD1306_WHITE); + display.setCursor(80, 16); + display.print(F("S/W V")); + display.print(sw, 1); + display.setCursor(80, 24); + display.print(F("H/W V")); + display.print(hw, 1); + display.display(); + buttons_state_t cur_button = getButtonsState(); + // If we press both buttons during boot, we'll enter the setup process + if (cur_button == BUTTONS_BOTH_PRESS) { + doSetup(); + return; + } + } +} + +inline void doSetup() { + debugprintln("Performing setup"); + // TODO(HEIDT) show an info screen if we're doing firstime setup or if memory is corrupted + + getResistanceFromUser(); + // TODO(HEIDT) do a temperature module setup here + + setFirstBoot(); +} + +inline void getResistanceFromUser() { + float resistance = 1.88; + while (1) { + clearMainMenu(); + display.setCursor(3, 4); + display.print(F("Resistance")); + display.drawLine(3, 12, 79, 12, SSD1306_WHITE); + display.setCursor(3, 14); + display.print(F("UP/DN: change")); + display.setCursor(3, 22); + display.print(F("BOTH: choose")); + buttons_state_t button = getButtonsState(); + if (button == BUTTONS_UP_PRESS) { + resistance += 0.01; + } else if (button == BUTTONS_DN_PRESS) { + resistance -= 0.01; + } else if (button == BUTTONS_BOTH_PRESS) { + setResistance(resistance); + return; + } + resistance = constrain(resistance, 0, MAX_RESISTANCE); + + display.setCursor(90, 12); + display.print(resistance); + display.display(); + } +} + +inline void mainMenu() { + // Debounce + menu_state_t cur_state = MENU_IDLE; + + int x = 0; // Display change counter + int y = 200; // Display change max (modulused below) + uint8_t profile_index = 0; + + while (1) { + switch (cur_state) { + case MENU_IDLE: { + clearMainMenu(); + buttons_state_t cur_button = getButtonsState(); + + if (cur_button == BUTTONS_BOTH_PRESS) { + cur_state = MENU_SELECT_PROFILE; + } else if (cur_button == BUTTONS_UP_PRESS) { + cur_state = MENU_INC_TEMP; + } else if (cur_button == BUTTONS_DN_PRESS) { + cur_state = MENU_DEC_TEMP; + } + } break; + case MENU_SELECT_PROFILE: { + debugprintln("getting thermal profile"); + profile_index = getProfile(); + cur_state = MENU_HEAT; + } break; + case MENU_HEAT: { + if (!heat(max_temp_array[max_temp_index], profile_index)) { + cancelledPB(); + coolDown(); + } else { + coolDown(); + completed(); + } + cur_state = MENU_IDLE; + } break; + case MENU_INC_TEMP: { + if (max_temp_index < sizeof(max_temp_array) - 1) { + max_temp_index++; + debugprintln("incrementing max temp"); + setMaxTempIndex(max_temp_index); + } + cur_state = MENU_IDLE; + } break; + case MENU_DEC_TEMP: { + if (max_temp_index > 0) { + max_temp_index--; + debugprintln("decrementing max temp"); + setMaxTempIndex(max_temp_index); + } + cur_state = MENU_IDLE; + } break; + } + + // Change Display (left-side) + showMainMenuLeft(x, y); + + // Update Display (right-side) + showMainMenuRight(); + } +} + +#define BUTTON_PRESS_TIME 50 +buttons_state_t getButtonsState() { + single_button_state_t button_dn; + single_button_state_t button_up; + unsigned long button_dn_time; + unsigned long button_up_time; + + noInterrupts(); + button_dn = dn_button_state; + button_up = up_button_state; + button_dn_time = down_state_change_time; + button_up_time = up_state_change_time; + interrupts(); + + unsigned long cur_time = millis(); + buttons_state_t state = BUTTONS_NO_PRESS; + + if (button_dn == BUTTON_PRESSED && button_up == BUTTON_PRESSED && + abs(button_dn_time - button_up_time) < BUTTON_PRESS_TIME) { + if (cur_time - button_dn_time > BUTTON_PRESS_TIME && + cur_time - button_up_time > BUTTON_PRESS_TIME) { + state = BUTTONS_BOTH_PRESS; + noInterrupts(); + dn_button_state = BUTTON_NO_ACTION; + up_button_state = BUTTON_NO_ACTION; + interrupts(); + } + } else if (button_up == BUTTON_PRESSED && cur_time - button_up_time > BUTTON_PRESS_TIME) { + state = BUTTONS_UP_PRESS; + noInterrupts(); + up_button_state = BUTTON_NO_ACTION; + interrupts(); + } else if (button_dn == BUTTON_PRESSED && cur_time - button_dn_time > BUTTON_PRESS_TIME) { + state = BUTTONS_DN_PRESS; + noInterrupts(); + dn_button_state = BUTTON_NO_ACTION; + interrupts(); + } + + return state; +} + +inline uint8_t getProfile() { + uint8_t cur_profile = 0; + while (1) { + clearMainMenu(); + display.setCursor(3, 4); + display.print(F("Pick profile")); + display.drawLine(3, 12, 79, 12, SSD1306_WHITE); + display.setCursor(3, 14); + display.print(F(" UP/DN: cycle")); + display.setCursor(3, 22); + display.print(F(" BOTH: choose")); + buttons_state_t cur_button = getButtonsState(); + if (cur_button == BUTTONS_BOTH_PRESS) { + clearMainMenu(); + return cur_profile; + } else if (cur_button == BUTTONS_DN_PRESS) { + cur_profile--; + } else if (cur_button == BUTTONS_UP_PRESS) { + cur_profile++; + } + cur_profile %= NUM_PROFILES; + displayProfileRight(cur_profile); + display.display(); + } +} + +inline void displayProfileRight(int8_t cur_profile) { + int cur_x = 90; + int cur_y = 30; + // start at x=90, go to SCREEN_WIDTH-8, save 6 pixels for cooldown + float x_dist = SCREEN_WIDTH - 90 - 8; + display.setCursor(cur_x, cur_y); + float total_seconds = (int)profiles[cur_profile].seconds[profiles[cur_profile].points - 1]; + + for (int i = 0; i < profiles[cur_profile].points; i++) { + int x_next = (int)((profiles[cur_profile].seconds[i] / total_seconds) * x_dist) + 90; + int y_next = 30 - (int)(profiles[cur_profile].fraction[i] * 28.0); + display.drawLine(cur_x, cur_y, x_next, y_next, SSD1306_WHITE); + cur_x = x_next; + cur_y = y_next; + } + // draw down to finish TEMP_PIN + display.drawLine(cur_x, cur_y, SCREEN_WIDTH - 2, 30, SSD1306_WHITE); +} + +inline void clearMainMenu() { + display.clearDisplay(); + display.setTextSize(1); + display.drawRoundRect(0, 0, 83, 32, 2, SSD1306_WHITE); +} + +inline void showMainMenuLeft(int &x, int &y) { + if (x < (y * 0.5)) { + display.setCursor(3, 4); + display.print(F("PRESS BUTTONS")); + display.drawLine(3, 12, 79, 12, SSD1306_WHITE); + display.setCursor(3, 14); + display.print(F(" Change MAX")); + display.setCursor(3, 22); + display.print(F(" Temperature")); + } else { + display.setCursor(3, 4); + display.print(F("HOLD BUTTONS")); + display.drawLine(3, 12, 79, 12, SSD1306_WHITE); + display.setCursor(3, 18); + display.print(F("Begin Heating")); + } + x = (x + 1) % y; // Display change increment and modulus +} + +inline void showMainMenuRight() { + display.setCursor(95, 6); + display.print(F("TEMP")); + display.setCursor(95, 18); + display.print(max_temp_array[max_temp_index]); + display.print(F("C")); + display.display(); +} + +inline void showHeatMenu(byte max_temp) { + display.clearDisplay(); + display.setTextSize(2); + display.setCursor(22, 4); + display.print(F("HEATING")); + display.setTextSize(1); + display.setCursor(52, 24); + display.print(max_temp); + display.print(F("C")); + display.display(); +} + +bool heat(byte max_temp, int profile_index) { + // Heating Display + showHeatMenu(max_temp); + delay(3000); + + float t; // Used to store current temperature + float v; // Used to store current voltage + + unsigned long profile_max_time = millis() / 1000 + (8 * 60); + unsigned long step_start_time = (millis() / 1000); + int current_step = 0; + + // Other control variables + int x = 0; // Heat Animate Counter + int y = 80; // Heat Animate max (modulused below) + + float start_temp = getTemp(); + float goal_temp = profiles[profile_index].fraction[0] * max_temp; + float step_runtime = profiles[profile_index].seconds[0]; + float last_time = 0; + float last_temp = getTemp(); + error_I = 0; + + while (1) { + // Cancel heat, don't even wait for uppress so we don't risk missing it during the loop + if (getButtonsState() != BUTTONS_NO_PRESS) { + analogWrite(MOSFET_PIN, MOSFET_PIN_OFF); + debugprintln("cancelled"); + return 0; + } + + // Check Heating not taken more than 8 minutes + if (millis() / 1000 > profile_max_time) { + analogWrite(MOSFET_PIN, MOSFET_PIN_OFF); + debugprintln("exceeded time"); + cancelledTimer(); + return 0; + } + + // Measure Values + // TODO(HEIDT) getting the temperature from the digital sensors is by far the slowest part + // of this loop. figure out an approach that allows control faster than sensing + t = getTemp(); + v = getVolts(); + float max_possible_amperage = v / bed_resistance; + // TODO(HEIDT) approximate true resistance based on cold resistance and temperature + float vmax = (MAX_AMPERAGE * bed_resistance) * PWM_VOLTAGE_SCALAR; + int min_PWM = 255 - ((vmax * 255.0) / v); + min_PWM = constrain(min_PWM, 0, 255); + debugprint("Min PWM: "); + debugprintln(min_PWM); + debugprintln(bed_resistance); + + // Determine what target temp is and PID to it + float time_into_step = ((float)millis() / 1000.0) - (float)step_start_time; + float target_temp = min( + ((goal_temp - start_temp) * (time_into_step / step_runtime)) + start_temp, goal_temp); + + // TODO(HEIDT) PID for a ramp will always lag, other options may be better + stepPID(target_temp, t, last_temp, time_into_step - last_time, min_PWM); + last_time = time_into_step; + + // if we finish the step timewise + if (time_into_step >= step_runtime) { + // and if we're within the goal temperature of the step + if (abs(t - goal_temp) < TARGET_TEMP_THRESHOLD) { + // move onto the next step in the profile + current_step++; + // if that was the last step, we're done! + if (current_step == profiles[profile_index].points) { + analogWrite(MOSFET_PIN, MOSFET_PIN_OFF); + return 1; + } + // otherwise, get the next goal temperature and runtime, and do the process again + last_time = 0.0; + start_temp = t; + goal_temp = profiles[profile_index].fraction[current_step] * max_temp; + step_runtime = profiles[profile_index].seconds[current_step] - + profiles[profile_index].seconds[current_step - 1]; + step_start_time = millis() / 1000.0; + } + } + + heatAnimate(x, y, v, t, target_temp); + } +} + +void evaluate_heat() { + debugprintln("Starting thermal evaluation"); + uint8_t duties[] = {255, 225, 200, 150, 100, 50, 0}; + unsigned long runtime = 60*5; // run each for 5 minutes + + for(int i = 0; i < sizeof(duties); i++) { + debugprint("Running to duty of: "); + debugprintln(duties[i]); + unsigned long start_time = millis(); + analogWrite(MOSFET_PIN, duties[i]); + float elapsed_time = (millis() - start_time)/1000.0; + while(elapsed_time < runtime) { + debugprint("elapsed time: "); + debugprintln(elapsed_time); + debugprint("runtime: "); + debugprintln(runtime); + elapsed_time = (millis() - start_time)/1000.0; + float v = getVolts(); + float t = getTemp(); + delay(500); + } + } + + analogWrite(MOSFET_PIN, MOSFET_PIN_OFF); +} + +void stepPID(float target_temp, float current_temp, float last_temp, float dt, int min_pwm) { + float error = target_temp - current_temp; + float D = (current_temp - last_temp) / dt; + + error_I += error * dt * kI; + error_I = constrain(error_I, 0, I_clip); + + // PWM is inverted so 0 duty is 100% power + float PWM = 255.0 - (error * kP + D * kD + error_I); + PWM = constrain(PWM, min_pwm, 255); + + debugprintln("PID"); + debugprintln(dt); + debugprintln(error); + debugprintln(error_I); + debugprint("PWM: "); + debugprintln(PWM); + analogWrite(MOSFET_PIN, (int)PWM); +} + +void inline heatAnimate(int &x, int &y, float v, float t, float target) { + // Heat Animate Control + display.clearDisplay(); + display.drawBitmap(0, 3, heat_animate, heat_animate_width, heat_animate_height, SSD1306_WHITE); + display.drawBitmap(112, 3, heat_animate, heat_animate_width, heat_animate_height, + SSD1306_WHITE); + display.fillRect(0, 3, heat_animate_width, heat_animate_height * (y - x) / y, SSD1306_BLACK); + display.fillRect(112, 3, heat_animate_width, heat_animate_height * (y - x) / y, SSD1306_BLACK); + x = (x + 1) % y; // Heat animate increment and modulus + + // Update display + display.setTextSize(2); + display.setCursor(22, 4); + display.print(F("HEATING")); + display.setTextSize(1); + display.setCursor(20, 24); + display.print(F("~")); + display.print(v, 1); + display.print(F("V")); + if (t >= 100) { + display.setCursor(63, 24); + } else if (t >= 10) { + display.setCursor(66, 24); + } else { + display.setCursor(69, 24); + } + display.print(F("~")); + display.print(t, 0); + display.print(F("C")); + display.print(F("/")); + display.print(target, 0); + display.print(F("C")); + display.display(); +} + +void cancelledPB() { // Cancelled via push button + // Update Display + display.clearDisplay(); + display.drawRoundRect(22, 0, 84, 32, 2, SSD1306_WHITE); + display.setCursor(25, 4); + display.print(F(" CANCELLED")); + display.display(); + delay(2000); +} + +void cancelledTimer() { // Cancelled via 5 minute Time Limit + // Initiate Swap Display + int x = 0; // Display change counter + int y = 150; // Display change max (modulused below) + + // Wait to return on any button press + while (getButtonsState() == BUTTONS_NO_PRESS) { + // Update Display + display.clearDisplay(); + display.drawRoundRect(22, 0, 84, 32, 2, SSD1306_WHITE); + display.setCursor(25, 4); + display.print(F(" TIMED OUT")); + display.drawLine(25, 12, 103, 12, SSD1306_WHITE); + + // Swap Main Text + if (x < (y * 0.3)) { + display.setCursor(25, 14); + display.println(" Took longer"); + display.setCursor(25, 22); + display.println(" than 5 mins"); + } else if (x < (y * 0.6)) { + display.setCursor(28, 14); + display.println("Try a higher"); + display.setCursor(25, 22); + display.println(" current PSU"); + } else { + display.setCursor(25, 14); + display.println(" Push button"); + display.setCursor(25, 22); + display.println(" to return"); + } + x = (x + 1) % y; // Display change increment and modulus + + display.setTextSize(3); + display.setCursor(5, 4); + display.print(F("!")); + display.setTextSize(3); + display.setCursor(108, 4); + display.print(F("!")); + display.setTextSize(1); + display.display(); + delay(50); + } +} + +void coolDown() { + float t = getTemp(); // Used to store current temperature + + // Wait to return on any button press, or TEMP_PIN below threshold + while (getButtonsState() == BUTTONS_NO_PRESS && t > 45.00) { + display.clearDisplay(); + display.drawRoundRect(22, 0, 84, 32, 2, SSD1306_WHITE); + display.setCursor(25, 4); + display.print(F(" COOL DOWN")); + display.drawLine(25, 12, 103, 12, SSD1306_WHITE); + display.setCursor(25, 14); + display.println(" Still Hot"); + t = getTemp(); + if (t >= 100) { + display.setCursor(49, 22); + } else { + display.setCursor(52, 22); + } + display.print(F("~")); + display.print(t, 0); + display.print(F("C")); + display.setTextSize(3); + display.setCursor(5, 4); + display.print(F("!")); + display.setTextSize(3); + display.setCursor(108, 4); + display.print(F("!")); + display.setTextSize(1); + display.display(); + } +} + +void completed() { + // Update Display + display.clearDisplay(); + display.drawRoundRect(22, 0, 84, 32, 2, SSD1306_WHITE); + display.setCursor(25, 4); + display.print(F(" COMPLETED ")); + display.drawLine(25, 12, 103, 12, SSD1306_WHITE); + display.setCursor(25, 14); + display.println(" Push button"); + display.setCursor(25, 22); + display.println(" to return"); + display.drawBitmap(0, 9, tick, tick_width, tick_height, SSD1306_WHITE); + display.drawBitmap(112, 9, tick, tick_width, tick_height, SSD1306_WHITE); + display.display(); + + // Wait to return on any button press + while (getButtonsState() == BUTTONS_NO_PRESS) { + } +} + +float getTemp() { + debugprint("Temps: "); + float t = 0; + for (byte i = 0; i < 100; i++) { // Poll TEMP_PIN reading 100 times + t = t + analogRead(TEMP_PIN); + } + t /= 100.0; // average + t *= VOLTAGE_REFERENCE / 1024.0; // voltage + // conversion to temp, consult datasheet: + // https://www.ti.com/document-viewer/LMT85/datasheet/detailed-description#snis1681040 + // this is optimized for 25C to 150C + // TODO(HEIDT) this is linearized and innacurate, could probably use the nonlinear + // functions without much overhead. + t = (t - 1.365) / ((.301 - 1.365) / (150.0 - 25.0)) + 25.0; + + // The analog sensor is too far from the bed for an accurate reading + // this simple function estimates the true bed temperature based off the thermal + // gradient + float estimated_temp = t*ANALOG_APPROXIMATION_SCALAR + ANALOG_APPROXIMATION_OFFSET; + debugprint(estimated_temp); + debugprint(" "); + + sensors.requestTemperatures(); + for (int i = 0; i < sensor_count; i++) { + float temp_in = sensors.getTempC(temp_addresses[i]); + debugprint(temp_in); + debugprint(" "); + } + debugprintln(); + + + return max(t, estimated_temp); +} + +float getVolts() { + float v = 0; + for (byte i = 0; i < 20; i++) { // Poll Voltage reading 20 times + v = v + analogRead(VCC_PIN); + } + v /= 20; + + float vin = (v / 1023.0) * 1.5; + debugprint("voltage at term: "); + debugprintln(vin); + vin = (vin / 0.090981) + 0.3; + return vin; +} + +void loop() { + // Not used +} diff --git a/Firmware/pcb_reflow_fw/.gitignore b/Firmware/pcb_reflow_fw_PlatformIO/.gitignore similarity index 100% rename from Firmware/pcb_reflow_fw/.gitignore rename to Firmware/pcb_reflow_fw_PlatformIO/.gitignore diff --git a/Firmware/pcb_reflow_fw/.vscode/extensions.json b/Firmware/pcb_reflow_fw_PlatformIO/.vscode/extensions.json similarity index 100% rename from Firmware/pcb_reflow_fw/.vscode/extensions.json rename to Firmware/pcb_reflow_fw_PlatformIO/.vscode/extensions.json diff --git a/Firmware/pcb_reflow_fw/README.md b/Firmware/pcb_reflow_fw_PlatformIO/README.md similarity index 100% rename from Firmware/pcb_reflow_fw/README.md rename to Firmware/pcb_reflow_fw_PlatformIO/README.md diff --git a/Firmware/pcb_reflow_fw/avrdude.conf b/Firmware/pcb_reflow_fw_PlatformIO/avrdude.conf similarity index 100% rename from Firmware/pcb_reflow_fw/avrdude.conf rename to Firmware/pcb_reflow_fw_PlatformIO/avrdude.conf diff --git a/Firmware/pcb_reflow_fw/platformio.ini b/Firmware/pcb_reflow_fw_PlatformIO/platformio.ini similarity index 96% rename from Firmware/pcb_reflow_fw/platformio.ini rename to Firmware/pcb_reflow_fw_PlatformIO/platformio.ini index 2e8cfc1..3cfab46 100644 --- a/Firmware/pcb_reflow_fw/platformio.ini +++ b/Firmware/pcb_reflow_fw_PlatformIO/platformio.ini @@ -13,7 +13,6 @@ platform = atmelmegaavr board = ATmega4809 framework = arduino upload_protocol = custom -upload_port = /dev/ttyUSB0 upload_flags = -C ${platformio.workspace_dir}/../avrdude.conf diff --git a/Firmware/pcb_reflow_fw/src/main.cpp b/Firmware/pcb_reflow_fw_PlatformIO/src/main.cpp similarity index 100% rename from Firmware/pcb_reflow_fw/src/main.cpp rename to Firmware/pcb_reflow_fw_PlatformIO/src/main.cpp