├── .gitignore ├── Firmware ├── README.md ├── pcb_reflow_fw │ └── pcb_reflow_fw.ino ├── pcb_reflow_fw_PlatformIO │ ├── .gitignore │ ├── .vscode │ │ └── extensions.json │ ├── README.md │ ├── avrdude.conf │ ├── platformio.ini │ └── src │ │ └── main.cpp └── testing │ ├── analysis │ ├── LMT85 study.ipynb │ ├── True Bed Temp Estimation.ipynb │ └── data │ │ ├── thermal_analysis1.log │ │ ├── thermal_analysis2.log │ │ ├── thermal_analysis3.log │ │ ├── thermal_analysis_with_approximation1.log │ │ └── thermal_analysis_with_approximation2.log │ └── temperature_groundtruth_fw │ └── temperature_groundtruth_fw.ino ├── Heatplate_v1.0 ├── Heatplate_v1.0.kicad_pcb ├── Heatplate_v1.0.kicad_prl ├── Heatplate_v1.0.kicad_pro ├── Heatplate_v1.0.kicad_sch ├── Heatplate_v1.0.net ├── Heatplate_v1.0_Schematic.pdf ├── PERIPHERALS.kicad_sch ├── bom │ └── Heatplate_v1.0_interactiveBOM.html ├── fp-info-cache └── renders │ ├── Heatplate_v1.0_back.png │ └── Heatplate_v1.0_front.png ├── Heatplate_v1.1 ├── Heatplate_v1.1.kicad_pcb ├── Heatplate_v1.1.kicad_prl ├── Heatplate_v1.1.kicad_pro ├── Heatplate_v1.1.kicad_sch ├── Heatplate_v1.1.net ├── Heatplate_v1.1.pdf ├── PERIPHERALS.kicad_sch ├── bom │ └── ibom.html ├── fp-info-cache ├── gerbers │ └── gerbers_Heatplate_v1.1_JLCPCB.zip └── renders │ ├── Heatplate_v1.1_back.png │ └── Heatplate_v1.1_front.png ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # For PCBs designed using KiCad: http://www.kicad-pcb.org/ 2 | # Format documentation: http://kicad-pcb.org/help/file-formats/ 3 | 4 | # Temporary files 5 | *.000 6 | *.bak 7 | *.bck 8 | *.kicad_pcb-bak 9 | *.sch-bak 10 | *~ 11 | _autosave-* 12 | *.tmp 13 | *-save.pro 14 | *-save.kicad_pcb 15 | fp-info-cache 16 | 17 | # Netlist files (exported from Eeschema) 18 | *.net 19 | 20 | # Autorouter files (exported from Pcbnew) 21 | *.dsn 22 | *.ses 23 | 24 | # Exported BOM files 25 | *.xml 26 | *.csv 27 | 28 | # ipynb checkpoints 29 | .ipynb_checkpoints 30 | -------------------------------------------------------------------------------- /Firmware/README.md: -------------------------------------------------------------------------------- 1 | # PCB reflow firmware 2 | This firmware is for the PCB reflow hotplate designed by Spatz. 3 | 4 | It has the following features: 5 | - reflow profiles 6 | - OLED display 7 | - profile PID tracking 8 | - temperature setpoint tracking 9 | - safe voltage calculation 10 | - memory validation 11 | 12 | ## Build and load process 13 | to build and load, you need a UDPI programmer and core definitions for the Atmega4809. 14 | 15 | If you don't have a UDPI programmer you can use another arduino like in this link: 16 | https://github.com/ElTangas/jtag2updi 17 | 18 | You can download the Atmega4809 definitions here: 19 | https://github.com/MCUdude/MegaCoreX/tree/master 20 | 21 | Make sure the DallasTemperature and Adafruit display libraries are installed. 22 | 23 | build and load through the arduino IDE as normal. Select the Atmega4809 as the target 24 | and make sure the correct programmer is selected. 25 | 26 | ## TODO 27 | 28 | - Needs a good amount of cleanup and refactoring 29 | - Has a hack estimation for minimum PWM, need to find a way to improve this 30 | - should switch buttons to bounce2 31 | - if there's no current sense, write a function to approximate resistance based on temperature 32 | - validate temperature estimation function is correct across multiple boards 33 | -------------------------------------------------------------------------------- /Firmware/pcb_reflow_fw/pcb_reflow_fw.ino: -------------------------------------------------------------------------------- 1 | /* Solder Reflow Plate Sketch 2 | * H/W - Ver Spatz-1.0 3 | * S/W - Ver 0.35 4 | * by Chris Halsall and Nathan Heidt */ 5 | 6 | /* To prepare 7 | * 1) Install MiniCore in additional boards; (copy into File->Preferences->Additional Boards Manager 8 | * URLs) https://mcudude.github.io/MiniCore/package_MCUdude_MiniCore_index.json 2) Then add MiniCore 9 | * by searching and installing (Tools->Board->Board Manager) 3) Install Adafruit_GFX and 10 | * Adafruit_SSD1306 libraries (Tools->Manage Libraries) 11 | */ 12 | 13 | /* To program 14 | * 1) Select the following settings under (Tools) 15 | * Board->Minicore->Atmega328 16 | * Clock->Internal 8MHz 17 | * BOD->BOD 2.7V 18 | * EEPROM->EEPROM retained 19 | * Compiler LTO->LTO Disabled 20 | * Variant->328P / 328PA 21 | * Bootloader->No bootloader 22 | * 2) Set programmer of choice, e.g.'Arduino as ISP (MiniCore)', 'USB ASP', etc, and set correct 23 | * port. 3) Burn bootloader (to set fuses correctly) 4) Compile and upload 24 | * 25 | * 26 | * TODOS: 27 | * - add digital sensor setup routine if they are detected, but not setup 28 | * - figure out a method for how to use all the temperature sensors 29 | * - implement an observer/predictor for the temperature sensors. Kalman filter time?!? 30 | */ 31 | 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | 39 | // Version Definitions 40 | static const PROGMEM float hw = 0.9; 41 | static const PROGMEM float sw = 0.15; 42 | 43 | // Screen Definitions 44 | #define SCREEN_WIDTH 128 45 | #define SCREEN_HEIGHT 32 46 | #define SCREEN_ADDRESS 0x3C // I2C Address 47 | Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); // Create Display 48 | 49 | // Pin Definitions 50 | #define MOSFET_PIN PIN_PC3 51 | #define UPSW_PIN PIN_PF3 52 | #define DNSW_PIN PIN_PD4 53 | #define TEMP_PIN PIN_PF2 // A2 54 | #define VCC_PIN PIN_PF4 // A0 55 | #define LED_GREEN_PIN PIN_PC5 56 | #define LED_RED_PIN PIN_PC4 57 | #define ONE_WIRE_BUS PIN_PD5 58 | 59 | #define MOSFET_PIN_OFF 255 60 | 61 | enum menu_state_t { MENU_IDLE, MENU_SELECT_PROFILE, MENU_HEAT, MENU_INC_TEMP, MENU_DEC_TEMP }; 62 | enum buttons_state_t { BUTTONS_NO_PRESS, BUTTONS_BOTH_PRESS, BUTTONS_UP_PRESS, BUTTONS_DN_PRESS }; 63 | enum single_button_state_t { BUTTON_PRESSED, BUTTON_RELEASED, BUTTON_NO_ACTION }; 64 | 65 | // Button interrupt state 66 | volatile single_button_state_t up_button_state = BUTTON_NO_ACTION; 67 | volatile single_button_state_t dn_button_state = BUTTON_NO_ACTION; 68 | volatile unsigned long up_state_change_time = 0; 69 | volatile unsigned long down_state_change_time = 0; 70 | 71 | // Temperature Info 72 | byte max_temp_array[] = {140, 150, 160, 170, 180}; 73 | byte max_temp_index = 0; 74 | #define MAX_RESISTANCE 10.0 75 | float bed_resistance = 1.88; 76 | #define MAX_AMPERAGE 5.0 77 | #define PWM_VOLTAGE_SCALAR 2.0 78 | 79 | // These values were derived using a regression from real world data. 80 | // See the jupyter notebooks for more detail 81 | #define ANALOG_APPROXIMATION_SCALAR 1.612 82 | #define ANALOG_APPROXIMATION_OFFSET -20.517 83 | 84 | // EEPROM storage locations 85 | #define CRC_ADDR 0 86 | #define FIRSTTIME_BOOT_ADDR 4 87 | #define TEMP_INDEX_ADDR 5 88 | #define RESISTANCE_INDEX_ADDR 6 89 | #define DIGITAL_TEMP_ID_ADDR 10 90 | 91 | // Voltage Measurement Info 92 | #define VOLTAGE_REFERENCE 1.5 93 | 94 | 95 | // Solder Reflow Plate Logo 96 | static const uint8_t PROGMEM logo[] = { 97 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 98 | 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 99 | 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x1f, 0xc0, 0x00, 0x31, 0x80, 0x00, 0x00, 0x00, 0x00, 100 | 0x1f, 0xe0, 0x03, 0x01, 0x80, 0x00, 0x00, 0x30, 0x70, 0x00, 0x21, 0x80, 0x00, 0x00, 0x00, 0x00, 101 | 0x10, 0x20, 0x03, 0x00, 0xc7, 0x80, 0x00, 0x20, 0x18, 0xf0, 0x61, 0x80, 0x00, 0x00, 0x00, 0x00, 102 | 0x18, 0x00, 0x03, 0x3e, 0xcc, 0xc0, 0xc0, 0x04, 0x19, 0x98, 0x61, 0x80, 0x00, 0x00, 0x00, 0x00, 103 | 0x1c, 0x01, 0xf3, 0x77, 0xd8, 0xc7, 0xe0, 0x06, 0x33, 0x18, 0x61, 0x8f, 0x88, 0x00, 0x00, 0x00, 104 | 0x06, 0x03, 0x3b, 0x61, 0xd0, 0xc6, 0x00, 0x07, 0xe2, 0x18, 0x61, 0x98, 0xd8, 0x04, 0x00, 0x00, 105 | 0x01, 0xc6, 0x0b, 0x60, 0xd9, 0x86, 0x00, 0x06, 0x03, 0x30, 0xff, 0xb0, 0x78, 0x66, 0x00, 0x00, 106 | 0x40, 0xe4, 0x0f, 0x60, 0xdf, 0x06, 0x00, 0x07, 0x03, 0xe0, 0x31, 0xe0, 0x78, 0x62, 0x00, 0x00, 107 | 0x40, 0x3c, 0x0f, 0x61, 0xd8, 0x06, 0x00, 0x07, 0x83, 0x00, 0x31, 0xe0, 0x78, 0x63, 0x00, 0x00, 108 | 0x60, 0x36, 0x1b, 0x63, 0xc8, 0x02, 0x00, 0x02, 0xc1, 0x00, 0x18, 0xb0, 0xcc, 0xe2, 0x00, 0x00, 109 | 0x30, 0x33, 0x3b, 0x36, 0x4e, 0x03, 0x00, 0x02, 0x61, 0xc0, 0x0c, 0x99, 0xcd, 0xfe, 0x00, 0x00, 110 | 0x0f, 0xe1, 0xe1, 0x3c, 0x03, 0xf3, 0x00, 0x02, 0x38, 0x7e, 0x0c, 0x8f, 0x07, 0x9c, 0x00, 0x00, 111 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 112 | 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 113 | 0x7f, 0x84, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 114 | 0xc0, 0xe4, 0x00, 0x18, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 115 | 0x04, 0x3c, 0x3c, 0x18, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 116 | 0x04, 0x1e, 0x06, 0x7f, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 117 | 0x04, 0x3e, 0x03, 0x18, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 118 | 0x04, 0x36, 0x7f, 0x19, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 119 | 0x07, 0xe6, 0xc7, 0x19, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 120 | 0x06, 0x07, 0x83, 0x18, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 121 | 0x06, 0x07, 0x81, 0x18, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 122 | 0x06, 0x06, 0xc3, 0x98, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 123 | 0x02, 0x04, 0x7e, 0x08, 0x3f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 124 | static const uint8_t logo_width = 128; 125 | static const uint8_t logo_height = 27; 126 | 127 | // Heating Animation 128 | static const uint8_t PROGMEM heat_animate[] = { 129 | 0b00000001, 0b00000000, 0b00000001, 0b10000000, 0b00000001, 0b10000000, 0b00000001, 0b01000000, 130 | 0b00000010, 0b01000000, 0b00100010, 0b01000100, 0b00100100, 0b00100100, 0b01010101, 0b00100110, 131 | 0b01001001, 0b10010110, 0b10000010, 0b10001001, 0b10100100, 0b01000001, 0b10011000, 0b01010010, 132 | 0b01000100, 0b01100010, 0b00100011, 0b10000100, 0b00011000, 0b00011000, 0b00000111, 0b11100000}; 133 | static const uint8_t heat_animate_width = 16; 134 | static const uint8_t heat_animate_height = 16; 135 | 136 | // Tick 137 | static const uint8_t PROGMEM tick[] = { 138 | 0b00000000, 0b00000100, 0b00000000, 0b00001010, 0b00000000, 0b00010101, 0b00000000, 0b00101010, 139 | 0b00000000, 0b01010100, 0b00000000, 0b10101000, 0b00000001, 0b01010000, 0b00100010, 0b10100000, 140 | 0b01010101, 0b01000000, 0b10101010, 0b10000000, 0b01010101, 0b00000000, 0b00101010, 0b00000000, 141 | 0b00010100, 0b00000000, 0b00001000, 0b00000000, 0b01111111, 0b11100000}; 142 | static const uint8_t tick_width = 16; 143 | static const uint8_t tick_height = 15; 144 | 145 | // This needs to be specified or the compiler will fail as you can't initialize a 146 | // flexible array member in a nested context 147 | #define MAX_PROFILE_LENGTH 8 148 | 149 | // TODO(HEIDT) may need to switch away from floats for speed/sizeA 150 | struct solder_profile_t { 151 | uint8_t points; 152 | float seconds[MAX_PROFILE_LENGTH]; 153 | float fraction[MAX_PROFILE_LENGTH]; 154 | }; 155 | 156 | // TODO(HEIDT) how to adjust for environments where the board starts hot or cold? 157 | // profiles pulled from here: https://www.compuphase.com/electronics/reflowsolderprofiles.htm#_ 158 | #define NUM_PROFILES 2 159 | const static solder_profile_t profiles[NUM_PROFILES] = { 160 | {.points = 4, .seconds = {90, 180, 240, 260}, .fraction = {.65, .78, 1.00, 1.00}}, 161 | {.points = 2, .seconds = {162.0, 202.0}, .fraction = {.95, 1.00}}}; 162 | 163 | // temperature must be within this range to move on to next step 164 | #define TARGET_TEMP_THRESHOLD 2.5 165 | 166 | // PID values 167 | float kI = 0.2; 168 | float kD = 0.25; 169 | float kP = 8.0; 170 | float I_clip = 220; 171 | float error_I = 0; 172 | 173 | // Optional temperature sensor 174 | OneWire oneWire(ONE_WIRE_BUS); 175 | DallasTemperature sensors(&oneWire); 176 | int sensor_count = 0; 177 | DeviceAddress temp_addresses[3]; 178 | 179 | #define DEBUG 180 | 181 | #ifdef DEBUG 182 | #define debugprint(x) Serial.print(x); 183 | #define debugprintln(x) Serial.println(x); 184 | #else 185 | #define debugprint(x) 186 | #define debugprintln(x) 187 | #endif 188 | 189 | // -------------------- Function prototypes ----------------------------------- 190 | void inline heatAnimate(int &x, int &y, float v, float t, float target_temp); 191 | 192 | // -------------------- Function definitions ---------------------------------- 193 | 194 | void dnsw_change_isr() { 195 | dn_button_state = BUTTON_PRESSED; 196 | down_state_change_time = millis(); 197 | } 198 | 199 | void upsw_change_isr() { 200 | up_button_state = BUTTON_PRESSED; 201 | up_state_change_time = millis(); 202 | } 203 | 204 | void setup() { 205 | 206 | // Pin Direction control 207 | pinMode(MOSFET_PIN, OUTPUT); 208 | pinMode(UPSW_PIN, INPUT); 209 | pinMode(DNSW_PIN, INPUT); 210 | pinMode(TEMP_PIN, INPUT); 211 | pinMode(VCC_PIN, INPUT); 212 | pinMode(LED_GREEN_PIN, OUTPUT); 213 | 214 | digitalWrite(LED_GREEN_PIN, HIGH); 215 | analogWrite(MOSFET_PIN, 255); // VERY IMPORTANT, DONT CHANGE! 216 | 217 | attachInterrupt(DNSW_PIN, dnsw_change_isr, FALLING); 218 | attachInterrupt(UPSW_PIN, upsw_change_isr, FALLING); 219 | 220 | Serial.begin(9600); 221 | 222 | // Enable Fast PWM with no prescaler 223 | setFastPwm(); 224 | setVREF(); 225 | 226 | // Start-up Diplay 227 | debugprintln("Showing startup"); 228 | showLogo(); 229 | 230 | debugprintln("Checking sensors"); 231 | // check onewire TEMP_PIN sensors 232 | setupSensors(); 233 | 234 | debugprintln("Checking first boot"); 235 | if (isFirstBoot() || !validateCRC()) { 236 | doSetup(); 237 | } 238 | 239 | // Pull saved values from EEPROM 240 | max_temp_index = getMaxTempIndex(); 241 | bed_resistance = getResistance(); 242 | 243 | debugprintln("Entering main menu"); 244 | // Go to main menu 245 | mainMenu(); 246 | } 247 | 248 | void updateCRC() { 249 | uint32_t new_crc = eepromCRC(); 250 | setCRC(new_crc); 251 | } 252 | 253 | bool validateCRC() { 254 | uint32_t stored_crc; 255 | EEPROM.get(CRC_ADDR, stored_crc); 256 | uint32_t calculated_crc = eepromCRC(); 257 | debugprint("got CRCs, stored: "); 258 | debugprint(stored_crc); 259 | debugprint(", calculated: "); 260 | debugprintln(calculated_crc); 261 | return stored_crc == calculated_crc; 262 | } 263 | 264 | void setCRC(uint32_t new_crc) { EEPROM.put(CRC_ADDR, new_crc); } 265 | 266 | uint32_t eepromCRC(void) { 267 | static const uint32_t crc_table[16] = {0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 268 | 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, 269 | 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 270 | 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c}; 271 | uint32_t crc = ~0L; 272 | // Skip first 4 bytes of EEPROM as thats where we store the CRC 273 | for (int index = 4; index < EEPROM.length(); ++index) { 274 | crc = crc_table[(crc ^ EEPROM[index]) & 0x0f] ^ (crc >> 4); 275 | crc = crc_table[(crc ^ (EEPROM[index] >> 4)) & 0x0f] ^ (crc >> 4); 276 | crc = ~crc; 277 | } 278 | 279 | return crc; 280 | } 281 | 282 | inline void setupSensors() { 283 | sensors.begin(); 284 | sensor_count = sensors.getDeviceCount(); 285 | debugprint("Looking for sensors, found: "); 286 | debugprintln(sensor_count); 287 | for (int i = 0; i < min(sensor_count, sizeof(temp_addresses)); i++) { 288 | sensors.getAddress(temp_addresses[i], i); 289 | } 290 | } 291 | 292 | inline void setFastPwm() { analogWriteFrequency(64); } 293 | 294 | inline void setVREF() { analogReference(INTERNAL1V5); } 295 | 296 | inline bool isFirstBoot() { 297 | uint8_t first_boot = EEPROM.read(FIRSTTIME_BOOT_ADDR); 298 | debugprint("Got first boot flag: "); 299 | debugprintln(first_boot); 300 | return first_boot != 1; 301 | } 302 | 303 | inline void setFirstBoot() { 304 | EEPROM.write(FIRSTTIME_BOOT_ADDR, 1); 305 | updateCRC(); 306 | } 307 | 308 | inline float getResistance() { 309 | float f; 310 | return EEPROM.get(RESISTANCE_INDEX_ADDR, f); 311 | return f; 312 | } 313 | 314 | inline void setResistance(float resistance) { 315 | EEPROM.put(RESISTANCE_INDEX_ADDR, resistance); 316 | updateCRC(); 317 | } 318 | 319 | inline void setMaxTempIndex(int index) { 320 | EEPROM.update(TEMP_INDEX_ADDR, index); 321 | updateCRC(); 322 | } 323 | 324 | inline int getMaxTempIndex(void) { return EEPROM.read(TEMP_INDEX_ADDR) % sizeof(max_temp_array); } 325 | 326 | void showLogo() { 327 | unsigned long start_time = millis(); 328 | display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS); 329 | while (start_time + 2000 > millis()) { 330 | display.clearDisplay(); 331 | display.setTextSize(1); 332 | display.setTextColor(SSD1306_WHITE); 333 | display.setCursor(0, 0); 334 | display.drawBitmap(0, 0, logo, logo_width, logo_height, SSD1306_WHITE); 335 | display.setCursor(80, 16); 336 | display.print(F("S/W V")); 337 | display.print(sw, 1); 338 | display.setCursor(80, 24); 339 | display.print(F("H/W V")); 340 | display.print(hw, 1); 341 | display.display(); 342 | buttons_state_t cur_button = getButtonsState(); 343 | // If we press both buttons during boot, we'll enter the setup process 344 | if (cur_button == BUTTONS_BOTH_PRESS) { 345 | doSetup(); 346 | return; 347 | } 348 | } 349 | } 350 | 351 | inline void doSetup() { 352 | debugprintln("Performing setup"); 353 | // TODO(HEIDT) show an info screen if we're doing firstime setup or if memory is corrupted 354 | 355 | getResistanceFromUser(); 356 | // TODO(HEIDT) do a temperature module setup here 357 | 358 | setFirstBoot(); 359 | } 360 | 361 | inline void getResistanceFromUser() { 362 | float resistance = 1.88; 363 | while (1) { 364 | clearMainMenu(); 365 | display.setCursor(3, 4); 366 | display.print(F("Resistance")); 367 | display.drawLine(3, 12, 79, 12, SSD1306_WHITE); 368 | display.setCursor(3, 14); 369 | display.print(F("UP/DN: change")); 370 | display.setCursor(3, 22); 371 | display.print(F("BOTH: choose")); 372 | buttons_state_t button = getButtonsState(); 373 | if (button == BUTTONS_UP_PRESS) { 374 | resistance += 0.01; 375 | } else if (button == BUTTONS_DN_PRESS) { 376 | resistance -= 0.01; 377 | } else if (button == BUTTONS_BOTH_PRESS) { 378 | setResistance(resistance); 379 | return; 380 | } 381 | resistance = constrain(resistance, 0, MAX_RESISTANCE); 382 | 383 | display.setCursor(90, 12); 384 | display.print(resistance); 385 | display.display(); 386 | } 387 | } 388 | 389 | inline void mainMenu() { 390 | // Debounce 391 | menu_state_t cur_state = MENU_IDLE; 392 | 393 | int x = 0; // Display change counter 394 | int y = 200; // Display change max (modulused below) 395 | uint8_t profile_index = 0; 396 | 397 | while (1) { 398 | switch (cur_state) { 399 | case MENU_IDLE: { 400 | clearMainMenu(); 401 | buttons_state_t cur_button = getButtonsState(); 402 | 403 | if (cur_button == BUTTONS_BOTH_PRESS) { 404 | cur_state = MENU_SELECT_PROFILE; 405 | } else if (cur_button == BUTTONS_UP_PRESS) { 406 | cur_state = MENU_INC_TEMP; 407 | } else if (cur_button == BUTTONS_DN_PRESS) { 408 | cur_state = MENU_DEC_TEMP; 409 | } 410 | } break; 411 | case MENU_SELECT_PROFILE: { 412 | debugprintln("getting thermal profile"); 413 | profile_index = getProfile(); 414 | cur_state = MENU_HEAT; 415 | } break; 416 | case MENU_HEAT: { 417 | if (!heat(max_temp_array[max_temp_index], profile_index)) { 418 | cancelledPB(); 419 | coolDown(); 420 | } else { 421 | coolDown(); 422 | completed(); 423 | } 424 | cur_state = MENU_IDLE; 425 | } break; 426 | case MENU_INC_TEMP: { 427 | if (max_temp_index < sizeof(max_temp_array) - 1) { 428 | max_temp_index++; 429 | debugprintln("incrementing max temp"); 430 | setMaxTempIndex(max_temp_index); 431 | } 432 | cur_state = MENU_IDLE; 433 | } break; 434 | case MENU_DEC_TEMP: { 435 | if (max_temp_index > 0) { 436 | max_temp_index--; 437 | debugprintln("decrementing max temp"); 438 | setMaxTempIndex(max_temp_index); 439 | } 440 | cur_state = MENU_IDLE; 441 | } break; 442 | } 443 | 444 | // Change Display (left-side) 445 | showMainMenuLeft(x, y); 446 | 447 | // Update Display (right-side) 448 | showMainMenuRight(); 449 | } 450 | } 451 | 452 | #define BUTTON_PRESS_TIME 50 453 | buttons_state_t getButtonsState() { 454 | single_button_state_t button_dn; 455 | single_button_state_t button_up; 456 | unsigned long button_dn_time; 457 | unsigned long button_up_time; 458 | 459 | noInterrupts(); 460 | button_dn = dn_button_state; 461 | button_up = up_button_state; 462 | button_dn_time = down_state_change_time; 463 | button_up_time = up_state_change_time; 464 | interrupts(); 465 | 466 | unsigned long cur_time = millis(); 467 | buttons_state_t state = BUTTONS_NO_PRESS; 468 | 469 | if (button_dn == BUTTON_PRESSED && button_up == BUTTON_PRESSED && 470 | abs(button_dn_time - button_up_time) < BUTTON_PRESS_TIME) { 471 | if (cur_time - button_dn_time > BUTTON_PRESS_TIME && 472 | cur_time - button_up_time > BUTTON_PRESS_TIME) { 473 | state = BUTTONS_BOTH_PRESS; 474 | noInterrupts(); 475 | dn_button_state = BUTTON_NO_ACTION; 476 | up_button_state = BUTTON_NO_ACTION; 477 | interrupts(); 478 | } 479 | } else if (button_up == BUTTON_PRESSED && cur_time - button_up_time > BUTTON_PRESS_TIME) { 480 | state = BUTTONS_UP_PRESS; 481 | noInterrupts(); 482 | up_button_state = BUTTON_NO_ACTION; 483 | interrupts(); 484 | } else if (button_dn == BUTTON_PRESSED && cur_time - button_dn_time > BUTTON_PRESS_TIME) { 485 | state = BUTTONS_DN_PRESS; 486 | noInterrupts(); 487 | dn_button_state = BUTTON_NO_ACTION; 488 | interrupts(); 489 | } 490 | 491 | return state; 492 | } 493 | 494 | inline uint8_t getProfile() { 495 | uint8_t cur_profile = 0; 496 | while (1) { 497 | clearMainMenu(); 498 | display.setCursor(3, 4); 499 | display.print(F("Pick profile")); 500 | display.drawLine(3, 12, 79, 12, SSD1306_WHITE); 501 | display.setCursor(3, 14); 502 | display.print(F(" UP/DN: cycle")); 503 | display.setCursor(3, 22); 504 | display.print(F(" BOTH: choose")); 505 | buttons_state_t cur_button = getButtonsState(); 506 | if (cur_button == BUTTONS_BOTH_PRESS) { 507 | clearMainMenu(); 508 | return cur_profile; 509 | } else if (cur_button == BUTTONS_DN_PRESS) { 510 | cur_profile--; 511 | } else if (cur_button == BUTTONS_UP_PRESS) { 512 | cur_profile++; 513 | } 514 | cur_profile %= NUM_PROFILES; 515 | displayProfileRight(cur_profile); 516 | display.display(); 517 | } 518 | } 519 | 520 | inline void displayProfileRight(int8_t cur_profile) { 521 | int cur_x = 90; 522 | int cur_y = 30; 523 | // start at x=90, go to SCREEN_WIDTH-8, save 6 pixels for cooldown 524 | float x_dist = SCREEN_WIDTH - 90 - 8; 525 | display.setCursor(cur_x, cur_y); 526 | float total_seconds = (int)profiles[cur_profile].seconds[profiles[cur_profile].points - 1]; 527 | 528 | for (int i = 0; i < profiles[cur_profile].points; i++) { 529 | int x_next = (int)((profiles[cur_profile].seconds[i] / total_seconds) * x_dist) + 90; 530 | int y_next = 30 - (int)(profiles[cur_profile].fraction[i] * 28.0); 531 | display.drawLine(cur_x, cur_y, x_next, y_next, SSD1306_WHITE); 532 | cur_x = x_next; 533 | cur_y = y_next; 534 | } 535 | // draw down to finish TEMP_PIN 536 | display.drawLine(cur_x, cur_y, SCREEN_WIDTH - 2, 30, SSD1306_WHITE); 537 | } 538 | 539 | inline void clearMainMenu() { 540 | display.clearDisplay(); 541 | display.setTextSize(1); 542 | display.drawRoundRect(0, 0, 83, 32, 2, SSD1306_WHITE); 543 | } 544 | 545 | inline void showMainMenuLeft(int &x, int &y) { 546 | if (x < (y * 0.5)) { 547 | display.setCursor(3, 4); 548 | display.print(F("PRESS BUTTONS")); 549 | display.drawLine(3, 12, 79, 12, SSD1306_WHITE); 550 | display.setCursor(3, 14); 551 | display.print(F(" Change MAX")); 552 | display.setCursor(3, 22); 553 | display.print(F(" Temperature")); 554 | } else { 555 | display.setCursor(3, 4); 556 | display.print(F("HOLD BUTTONS")); 557 | display.drawLine(3, 12, 79, 12, SSD1306_WHITE); 558 | display.setCursor(3, 18); 559 | display.print(F("Begin Heating")); 560 | } 561 | x = (x + 1) % y; // Display change increment and modulus 562 | } 563 | 564 | inline void showMainMenuRight() { 565 | display.setCursor(95, 6); 566 | display.print(F("TEMP")); 567 | display.setCursor(95, 18); 568 | display.print(max_temp_array[max_temp_index]); 569 | display.print(F("C")); 570 | display.display(); 571 | } 572 | 573 | inline void showHeatMenu(byte max_temp) { 574 | display.clearDisplay(); 575 | display.setTextSize(2); 576 | display.setCursor(22, 4); 577 | display.print(F("HEATING")); 578 | display.setTextSize(1); 579 | display.setCursor(52, 24); 580 | display.print(max_temp); 581 | display.print(F("C")); 582 | display.display(); 583 | } 584 | 585 | bool heat(byte max_temp, int profile_index) { 586 | // Heating Display 587 | showHeatMenu(max_temp); 588 | delay(3000); 589 | 590 | float t; // Used to store current temperature 591 | float v; // Used to store current voltage 592 | 593 | unsigned long profile_max_time = millis() / 1000 + (8 * 60); 594 | unsigned long step_start_time = (millis() / 1000); 595 | int current_step = 0; 596 | 597 | // Other control variables 598 | int x = 0; // Heat Animate Counter 599 | int y = 80; // Heat Animate max (modulused below) 600 | 601 | float start_temp = getTemp(); 602 | float goal_temp = profiles[profile_index].fraction[0] * max_temp; 603 | float step_runtime = profiles[profile_index].seconds[0]; 604 | float last_time = 0; 605 | float last_temp = getTemp(); 606 | error_I = 0; 607 | 608 | while (1) { 609 | // Cancel heat, don't even wait for uppress so we don't risk missing it during the loop 610 | if (getButtonsState() != BUTTONS_NO_PRESS) { 611 | analogWrite(MOSFET_PIN, MOSFET_PIN_OFF); 612 | debugprintln("cancelled"); 613 | return 0; 614 | } 615 | 616 | // Check Heating not taken more than 8 minutes 617 | if (millis() / 1000 > profile_max_time) { 618 | analogWrite(MOSFET_PIN, MOSFET_PIN_OFF); 619 | debugprintln("exceeded time"); 620 | cancelledTimer(); 621 | return 0; 622 | } 623 | 624 | // Measure Values 625 | // TODO(HEIDT) getting the temperature from the digital sensors is by far the slowest part 626 | // of this loop. figure out an approach that allows control faster than sensing 627 | t = getTemp(); 628 | v = getVolts(); 629 | float max_possible_amperage = v / bed_resistance; 630 | // TODO(HEIDT) approximate true resistance based on cold resistance and temperature 631 | float vmax = (MAX_AMPERAGE * bed_resistance) * PWM_VOLTAGE_SCALAR; 632 | int min_PWM = 255 - ((vmax * 255.0) / v); 633 | min_PWM = constrain(min_PWM, 0, 255); 634 | debugprint("Min PWM: "); 635 | debugprintln(min_PWM); 636 | debugprintln(bed_resistance); 637 | 638 | // Determine what target temp is and PID to it 639 | float time_into_step = ((float)millis() / 1000.0) - (float)step_start_time; 640 | float target_temp = min( 641 | ((goal_temp - start_temp) * (time_into_step / step_runtime)) + start_temp, goal_temp); 642 | 643 | // TODO(HEIDT) PID for a ramp will always lag, other options may be better 644 | stepPID(target_temp, t, last_temp, time_into_step - last_time, min_PWM); 645 | last_time = time_into_step; 646 | 647 | // if we finish the step timewise 648 | if (time_into_step >= step_runtime) { 649 | // and if we're within the goal temperature of the step 650 | if (abs(t - goal_temp) < TARGET_TEMP_THRESHOLD) { 651 | // move onto the next step in the profile 652 | current_step++; 653 | // if that was the last step, we're done! 654 | if (current_step == profiles[profile_index].points) { 655 | analogWrite(MOSFET_PIN, MOSFET_PIN_OFF); 656 | return 1; 657 | } 658 | // otherwise, get the next goal temperature and runtime, and do the process again 659 | last_time = 0.0; 660 | start_temp = t; 661 | goal_temp = profiles[profile_index].fraction[current_step] * max_temp; 662 | step_runtime = profiles[profile_index].seconds[current_step] - 663 | profiles[profile_index].seconds[current_step - 1]; 664 | step_start_time = millis() / 1000.0; 665 | } 666 | } 667 | 668 | heatAnimate(x, y, v, t, target_temp); 669 | } 670 | } 671 | 672 | void evaluate_heat() { 673 | debugprintln("Starting thermal evaluation"); 674 | uint8_t duties[] = {255, 225, 200, 150, 100, 50, 0}; 675 | unsigned long runtime = 60*5; // run each for 5 minutes 676 | 677 | for(int i = 0; i < sizeof(duties); i++) { 678 | debugprint("Running to duty of: "); 679 | debugprintln(duties[i]); 680 | unsigned long start_time = millis(); 681 | analogWrite(MOSFET_PIN, duties[i]); 682 | float elapsed_time = (millis() - start_time)/1000.0; 683 | while(elapsed_time < runtime) { 684 | debugprint("elapsed time: "); 685 | debugprintln(elapsed_time); 686 | debugprint("runtime: "); 687 | debugprintln(runtime); 688 | elapsed_time = (millis() - start_time)/1000.0; 689 | float v = getVolts(); 690 | float t = getTemp(); 691 | delay(500); 692 | } 693 | } 694 | 695 | analogWrite(MOSFET_PIN, MOSFET_PIN_OFF); 696 | } 697 | 698 | void stepPID(float target_temp, float current_temp, float last_temp, float dt, int min_pwm) { 699 | float error = target_temp - current_temp; 700 | float D = (current_temp - last_temp) / dt; 701 | 702 | error_I += error * dt * kI; 703 | error_I = constrain(error_I, 0, I_clip); 704 | 705 | // PWM is inverted so 0 duty is 100% power 706 | float PWM = 255.0 - (error * kP + D * kD + error_I); 707 | PWM = constrain(PWM, min_pwm, 255); 708 | 709 | debugprintln("PID"); 710 | debugprintln(dt); 711 | debugprintln(error); 712 | debugprintln(error_I); 713 | debugprint("PWM: "); 714 | debugprintln(PWM); 715 | analogWrite(MOSFET_PIN, (int)PWM); 716 | } 717 | 718 | void inline heatAnimate(int &x, int &y, float v, float t, float target) { 719 | // Heat Animate Control 720 | display.clearDisplay(); 721 | display.drawBitmap(0, 3, heat_animate, heat_animate_width, heat_animate_height, SSD1306_WHITE); 722 | display.drawBitmap(112, 3, heat_animate, heat_animate_width, heat_animate_height, 723 | SSD1306_WHITE); 724 | display.fillRect(0, 3, heat_animate_width, heat_animate_height * (y - x) / y, SSD1306_BLACK); 725 | display.fillRect(112, 3, heat_animate_width, heat_animate_height * (y - x) / y, SSD1306_BLACK); 726 | x = (x + 1) % y; // Heat animate increment and modulus 727 | 728 | // Update display 729 | display.setTextSize(2); 730 | display.setCursor(22, 4); 731 | display.print(F("HEATING")); 732 | display.setTextSize(1); 733 | display.setCursor(20, 24); 734 | display.print(F("~")); 735 | display.print(v, 1); 736 | display.print(F("V")); 737 | if (t >= 100) { 738 | display.setCursor(63, 24); 739 | } else if (t >= 10) { 740 | display.setCursor(66, 24); 741 | } else { 742 | display.setCursor(69, 24); 743 | } 744 | display.print(F("~")); 745 | display.print(t, 0); 746 | display.print(F("C")); 747 | display.print(F("/")); 748 | display.print(target, 0); 749 | display.print(F("C")); 750 | display.display(); 751 | } 752 | 753 | void cancelledPB() { // Cancelled via push button 754 | // Update Display 755 | display.clearDisplay(); 756 | display.drawRoundRect(22, 0, 84, 32, 2, SSD1306_WHITE); 757 | display.setCursor(25, 4); 758 | display.print(F(" CANCELLED")); 759 | display.display(); 760 | delay(2000); 761 | } 762 | 763 | void cancelledTimer() { // Cancelled via 5 minute Time Limit 764 | // Initiate Swap Display 765 | int x = 0; // Display change counter 766 | int y = 150; // Display change max (modulused below) 767 | 768 | // Wait to return on any button press 769 | while (getButtonsState() == BUTTONS_NO_PRESS) { 770 | // Update Display 771 | display.clearDisplay(); 772 | display.drawRoundRect(22, 0, 84, 32, 2, SSD1306_WHITE); 773 | display.setCursor(25, 4); 774 | display.print(F(" TIMED OUT")); 775 | display.drawLine(25, 12, 103, 12, SSD1306_WHITE); 776 | 777 | // Swap Main Text 778 | if (x < (y * 0.3)) { 779 | display.setCursor(25, 14); 780 | display.println(" Took longer"); 781 | display.setCursor(25, 22); 782 | display.println(" than 5 mins"); 783 | } else if (x < (y * 0.6)) { 784 | display.setCursor(28, 14); 785 | display.println("Try a higher"); 786 | display.setCursor(25, 22); 787 | display.println(" current PSU"); 788 | } else { 789 | display.setCursor(25, 14); 790 | display.println(" Push button"); 791 | display.setCursor(25, 22); 792 | display.println(" to return"); 793 | } 794 | x = (x + 1) % y; // Display change increment and modulus 795 | 796 | display.setTextSize(3); 797 | display.setCursor(5, 4); 798 | display.print(F("!")); 799 | display.setTextSize(3); 800 | display.setCursor(108, 4); 801 | display.print(F("!")); 802 | display.setTextSize(1); 803 | display.display(); 804 | delay(50); 805 | } 806 | } 807 | 808 | void coolDown() { 809 | float t = getTemp(); // Used to store current temperature 810 | 811 | // Wait to return on any button press, or TEMP_PIN below threshold 812 | while (getButtonsState() == BUTTONS_NO_PRESS && t > 45.00) { 813 | display.clearDisplay(); 814 | display.drawRoundRect(22, 0, 84, 32, 2, SSD1306_WHITE); 815 | display.setCursor(25, 4); 816 | display.print(F(" COOL DOWN")); 817 | display.drawLine(25, 12, 103, 12, SSD1306_WHITE); 818 | display.setCursor(25, 14); 819 | display.println(" Still Hot"); 820 | t = getTemp(); 821 | if (t >= 100) { 822 | display.setCursor(49, 22); 823 | } else { 824 | display.setCursor(52, 22); 825 | } 826 | display.print(F("~")); 827 | display.print(t, 0); 828 | display.print(F("C")); 829 | display.setTextSize(3); 830 | display.setCursor(5, 4); 831 | display.print(F("!")); 832 | display.setTextSize(3); 833 | display.setCursor(108, 4); 834 | display.print(F("!")); 835 | display.setTextSize(1); 836 | display.display(); 837 | } 838 | } 839 | 840 | void completed() { 841 | // Update Display 842 | display.clearDisplay(); 843 | display.drawRoundRect(22, 0, 84, 32, 2, SSD1306_WHITE); 844 | display.setCursor(25, 4); 845 | display.print(F(" COMPLETED ")); 846 | display.drawLine(25, 12, 103, 12, SSD1306_WHITE); 847 | display.setCursor(25, 14); 848 | display.println(" Push button"); 849 | display.setCursor(25, 22); 850 | display.println(" to return"); 851 | display.drawBitmap(0, 9, tick, tick_width, tick_height, SSD1306_WHITE); 852 | display.drawBitmap(112, 9, tick, tick_width, tick_height, SSD1306_WHITE); 853 | display.display(); 854 | 855 | // Wait to return on any button press 856 | while (getButtonsState() == BUTTONS_NO_PRESS) { 857 | } 858 | } 859 | 860 | float getTemp() { 861 | debugprint("Temps: "); 862 | float t = 0; 863 | for (byte i = 0; i < 100; i++) { // Poll TEMP_PIN reading 100 times 864 | t = t + analogRead(TEMP_PIN); 865 | } 866 | t /= 100.0; // average 867 | t *= VOLTAGE_REFERENCE / 1024.0; // voltage 868 | // conversion to temp, consult datasheet: 869 | // https://www.ti.com/document-viewer/LMT85/datasheet/detailed-description#snis1681040 870 | // this is optimized for 25C to 150C 871 | // TODO(HEIDT) this is linearized and innacurate, could probably use the nonlinear 872 | // functions without much overhead. 873 | t = (t - 1.365) / ((.301 - 1.365) / (150.0 - 25.0)) + 25.0; 874 | 875 | // The analog sensor is too far from the bed for an accurate reading 876 | // this simple function estimates the true bed temperature based off the thermal 877 | // gradient 878 | float estimated_temp = t*ANALOG_APPROXIMATION_SCALAR + ANALOG_APPROXIMATION_OFFSET; 879 | debugprint(estimated_temp); 880 | debugprint(" "); 881 | 882 | sensors.requestTemperatures(); 883 | for (int i = 0; i < sensor_count; i++) { 884 | float temp_in = sensors.getTempC(temp_addresses[i]); 885 | debugprint(temp_in); 886 | debugprint(" "); 887 | } 888 | debugprintln(); 889 | 890 | 891 | return max(t, estimated_temp); 892 | } 893 | 894 | float getVolts() { 895 | float v = 0; 896 | for (byte i = 0; i < 20; i++) { // Poll Voltage reading 20 times 897 | v = v + analogRead(VCC_PIN); 898 | } 899 | v /= 20; 900 | 901 | float vin = (v / 1023.0) * 1.5; 902 | debugprint("voltage at term: "); 903 | debugprintln(vin); 904 | vin = (vin / 0.090981) + 0.3; 905 | return vin; 906 | } 907 | 908 | void loop() { 909 | // Not used 910 | } 911 | -------------------------------------------------------------------------------- /Firmware/pcb_reflow_fw_PlatformIO/.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /Firmware/pcb_reflow_fw_PlatformIO/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ], 7 | "unwantedRecommendations": [ 8 | "ms-vscode.cpptools-extension-pack" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /Firmware/pcb_reflow_fw_PlatformIO/README.md: -------------------------------------------------------------------------------- 1 | # Firmware 2 | 3 | This firmware project structure is based on [PlatformIO](https://platformio.org/). 4 | The flashing has been configured to use an Arduino as a programmer using the 5 | following project: https://github.com/ElTangas/jtag2updi. 6 | 7 | ## Todo 8 | 9 | - [ ] Split main file into multiple ones _(makes it easier to work on)_ 10 | - [ ] Better Instructions for flashing. -------------------------------------------------------------------------------- /Firmware/pcb_reflow_fw_PlatformIO/platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:ATmega4809] 12 | platform = atmelmegaavr 13 | board = ATmega4809 14 | framework = arduino 15 | upload_protocol = custom 16 | upload_flags = 17 | -C 18 | ${platformio.workspace_dir}/../avrdude.conf 19 | -p 20 | m4809 21 | -P 22 | $UPLOAD_PORT 23 | -b 24 | 115200 25 | -c 26 | jtag2updi 27 | upload_command = avrdude $UPLOAD_FLAGS -U flash:w:$SOURCE:i 28 | lib_deps = 29 | adafruit/Adafruit GFX Library@^1.11.2 30 | adafruit/Adafruit SSD1306@^2.5.4 31 | paulstoffregen/OneWire@^2.3.7 32 | milesburton/DallasTemperature@^3.10.0 33 | -------------------------------------------------------------------------------- /Firmware/pcb_reflow_fw_PlatformIO/src/main.cpp: -------------------------------------------------------------------------------- 1 | /* Solder Reflow Plate Sketch 2 | * H/W - Ver Spatz-1.0 3 | * S/W - Ver 0.35 4 | * by Chris Halsall and Nathan Heidt 5 | */ 6 | 7 | /* 8 | * TODOS: 9 | * - add digital sensor setup routine if they are detected, but not setup 10 | * - figure out a method for how to use all the temperature sensors 11 | * - implement an observer/predictor for the temperature sensors. Kalman filter 12 | * time?!? 13 | */ 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | // Version Definitions 23 | static const PROGMEM float hw = 0.9; 24 | static const PROGMEM float sw = 0.15; 25 | 26 | // Screen Definitions 27 | #define SCREEN_WIDTH 128 28 | #define SCREEN_HEIGHT 32 29 | #define SCREEN_ADDRESS 0x3C // I2C Address 30 | Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, 31 | -1); // Create Display 32 | 33 | // Pin Definitions 34 | #define MOSFET_PIN PIN_PC3 35 | #define UPSW_PIN PIN_PF3 36 | #define DNSW_PIN PIN_PD4 37 | #define TEMP_PIN PIN_PF2 // A2 38 | #define VCC_PIN PIN_PF4 // A0 39 | #define LED_GREEN_PIN PIN_PC5 40 | #define LED_RED_PIN PIN_PC4 41 | #define ONE_WIRE_BUS PIN_PD5 42 | 43 | #define MOSFET_PIN_OFF 255 44 | 45 | enum menu_state_t { 46 | MENU_IDLE, 47 | MENU_SELECT_PROFILE, 48 | MENU_HEAT, 49 | MENU_INC_TEMP, 50 | MENU_DEC_TEMP 51 | }; 52 | enum buttons_state_t { 53 | BUTTONS_NO_PRESS, 54 | BUTTONS_BOTH_PRESS, 55 | BUTTONS_UP_PRESS, 56 | BUTTONS_DN_PRESS 57 | }; 58 | enum single_button_state_t { 59 | BUTTON_PRESSED, 60 | BUTTON_RELEASED, 61 | BUTTON_NO_ACTION 62 | }; 63 | 64 | // Button interrupt state 65 | volatile single_button_state_t up_button_state = BUTTON_NO_ACTION; 66 | volatile single_button_state_t dn_button_state = BUTTON_NO_ACTION; 67 | volatile unsigned long up_state_change_time = 0; 68 | volatile unsigned long down_state_change_time = 0; 69 | 70 | // Temperature Info 71 | byte max_temp_array[] = {140, 150, 160, 170, 180}; 72 | byte max_temp_index = 0; 73 | #define MAX_RESISTANCE 10.0 74 | float bed_resistance = 1.88; 75 | #define MAX_AMPERAGE 5.0 76 | #define PWM_VOLTAGE_SCALAR 2.0 77 | 78 | // These values were derived using a regression from real world data. 79 | // See the jupyter notebooks for more detail 80 | #define ANALOG_APPROXIMATION_SCALAR 1.612 81 | #define ANALOG_APPROXIMATION_OFFSET -20.517 82 | 83 | // EEPROM storage locations 84 | #define CRC_ADDR 0 85 | #define FIRSTTIME_BOOT_ADDR 4 86 | #define TEMP_INDEX_ADDR 5 87 | #define RESISTANCE_INDEX_ADDR 6 88 | #define DIGITAL_TEMP_ID_ADDR 10 89 | 90 | // Voltage Measurement Info 91 | #define VOLTAGE_REFERENCE 1.5 92 | 93 | // Solder Reflow Plate Logo 94 | static const uint8_t PROGMEM logo[] = { 95 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 96 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x00, 97 | 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 98 | 0x80, 0x00, 0x00, 0x1f, 0xc0, 0x00, 0x31, 0x80, 0x00, 0x00, 0x00, 0x00, 99 | 0x1f, 0xe0, 0x03, 0x01, 0x80, 0x00, 0x00, 0x30, 0x70, 0x00, 0x21, 0x80, 100 | 0x00, 0x00, 0x00, 0x00, 0x10, 0x20, 0x03, 0x00, 0xc7, 0x80, 0x00, 0x20, 101 | 0x18, 0xf0, 0x61, 0x80, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x03, 0x3e, 102 | 0xcc, 0xc0, 0xc0, 0x04, 0x19, 0x98, 0x61, 0x80, 0x00, 0x00, 0x00, 0x00, 103 | 0x1c, 0x01, 0xf3, 0x77, 0xd8, 0xc7, 0xe0, 0x06, 0x33, 0x18, 0x61, 0x8f, 104 | 0x88, 0x00, 0x00, 0x00, 0x06, 0x03, 0x3b, 0x61, 0xd0, 0xc6, 0x00, 0x07, 105 | 0xe2, 0x18, 0x61, 0x98, 0xd8, 0x04, 0x00, 0x00, 0x01, 0xc6, 0x0b, 0x60, 106 | 0xd9, 0x86, 0x00, 0x06, 0x03, 0x30, 0xff, 0xb0, 0x78, 0x66, 0x00, 0x00, 107 | 0x40, 0xe4, 0x0f, 0x60, 0xdf, 0x06, 0x00, 0x07, 0x03, 0xe0, 0x31, 0xe0, 108 | 0x78, 0x62, 0x00, 0x00, 0x40, 0x3c, 0x0f, 0x61, 0xd8, 0x06, 0x00, 0x07, 109 | 0x83, 0x00, 0x31, 0xe0, 0x78, 0x63, 0x00, 0x00, 0x60, 0x36, 0x1b, 0x63, 110 | 0xc8, 0x02, 0x00, 0x02, 0xc1, 0x00, 0x18, 0xb0, 0xcc, 0xe2, 0x00, 0x00, 111 | 0x30, 0x33, 0x3b, 0x36, 0x4e, 0x03, 0x00, 0x02, 0x61, 0xc0, 0x0c, 0x99, 112 | 0xcd, 0xfe, 0x00, 0x00, 0x0f, 0xe1, 0xe1, 0x3c, 0x03, 0xf3, 0x00, 0x02, 113 | 0x38, 0x7e, 0x0c, 0x8f, 0x07, 0x9c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 114 | 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 115 | 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 116 | 0x00, 0x00, 0x00, 0x00, 0x7f, 0x84, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 117 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xe4, 0x00, 0x18, 118 | 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 119 | 0x04, 0x3c, 0x3c, 0x18, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 120 | 0x00, 0x00, 0x00, 0x00, 0x04, 0x1e, 0x06, 0x7f, 0xc6, 0x00, 0x00, 0x00, 121 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x3e, 0x03, 0x18, 122 | 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 123 | 0x04, 0x36, 0x7f, 0x19, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 124 | 0x00, 0x00, 0x00, 0x00, 0x07, 0xe6, 0xc7, 0x19, 0xf8, 0x00, 0x00, 0x00, 125 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x07, 0x83, 0x18, 126 | 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 127 | 0x06, 0x07, 0x81, 0x18, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 128 | 0x00, 0x00, 0x00, 0x00, 0x06, 0x06, 0xc3, 0x98, 0x70, 0x00, 0x00, 0x00, 129 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x7e, 0x08, 130 | 0x3f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 131 | static const uint8_t logo_width = 128; 132 | static const uint8_t logo_height = 27; 133 | 134 | // Heating Animation 135 | static const uint8_t PROGMEM heat_animate[] = { 136 | 0b00000001, 0b00000000, 0b00000001, 0b10000000, 0b00000001, 0b10000000, 137 | 0b00000001, 0b01000000, 0b00000010, 0b01000000, 0b00100010, 0b01000100, 138 | 0b00100100, 0b00100100, 0b01010101, 0b00100110, 0b01001001, 0b10010110, 139 | 0b10000010, 0b10001001, 0b10100100, 0b01000001, 0b10011000, 0b01010010, 140 | 0b01000100, 0b01100010, 0b00100011, 0b10000100, 0b00011000, 0b00011000, 141 | 0b00000111, 0b11100000}; 142 | static const uint8_t heat_animate_width = 16; 143 | static const uint8_t heat_animate_height = 16; 144 | 145 | // Tick 146 | static const uint8_t PROGMEM tick[] = { 147 | 0b00000000, 0b00000100, 0b00000000, 0b00001010, 0b00000000, 0b00010101, 148 | 0b00000000, 0b00101010, 0b00000000, 0b01010100, 0b00000000, 0b10101000, 149 | 0b00000001, 0b01010000, 0b00100010, 0b10100000, 0b01010101, 0b01000000, 150 | 0b10101010, 0b10000000, 0b01010101, 0b00000000, 0b00101010, 0b00000000, 151 | 0b00010100, 0b00000000, 0b00001000, 0b00000000, 0b01111111, 0b11100000}; 152 | static const uint8_t tick_width = 16; 153 | static const uint8_t tick_height = 15; 154 | 155 | // This needs to be specified or the compiler will fail as you can't initialize 156 | // a flexible array member in a nested context 157 | #define MAX_PROFILE_LENGTH 8 158 | 159 | // TODO(HEIDT) may need to switch away from floats for speed/sizeA 160 | struct solder_profile_t { 161 | uint8_t points; 162 | float seconds[MAX_PROFILE_LENGTH]; 163 | float fraction[MAX_PROFILE_LENGTH]; 164 | }; 165 | 166 | // TODO(HEIDT) how to adjust for environments where the board starts hot or 167 | // cold? profiles pulled from here: 168 | // https://www.compuphase.com/electronics/reflowsolderprofiles.htm#_ 169 | #define NUM_PROFILES 2 170 | const static solder_profile_t profiles[NUM_PROFILES] = { 171 | {.points = 4, 172 | .seconds = {90, 180, 240, 260}, 173 | .fraction = {.65, .78, 1.00, 1.00}}, 174 | {.points = 2, .seconds = {162.0, 202.0}, .fraction = {.95, 1.00}}}; 175 | 176 | // temperature must be within this range to move on to next step 177 | #define TARGET_TEMP_THRESHOLD 2.5 178 | 179 | // PID values 180 | float kI = 0.2; 181 | float kD = 0.25; 182 | float kP = 8.0; 183 | float I_clip = 220; 184 | float error_I = 0; 185 | 186 | // Optional temperature sensor 187 | OneWire oneWire(ONE_WIRE_BUS); 188 | DallasTemperature sensors(&oneWire); 189 | int sensor_count = 0; 190 | DeviceAddress temp_addresses[3]; 191 | 192 | #define DEBUG 193 | 194 | #ifdef DEBUG 195 | #define debugprint(x) Serial.print(x); 196 | #define debugprintln(x) Serial.println(x); 197 | #else 198 | #define debugprint(x) 199 | #define debugprintln(x) 200 | #endif 201 | 202 | // -------------------- Function prototypes ----------------------------------- 203 | void inline heatAnimate(int &x, int &y, float v, float t, float target_temp); 204 | 205 | // -------------------- General functions ---------------------------------- 206 | 207 | void dnsw_change_isr() { 208 | dn_button_state = BUTTON_PRESSED; 209 | down_state_change_time = millis(); 210 | } 211 | 212 | void upsw_change_isr() { 213 | up_button_state = BUTTON_PRESSED; 214 | up_state_change_time = millis(); 215 | } 216 | 217 | void setCRC(uint32_t new_crc) { EEPROM.put(CRC_ADDR, new_crc); } 218 | 219 | uint32_t eepromCRC(void) { 220 | static const uint32_t crc_table[16] = { 221 | 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 222 | 0x4db26158, 0x5005713c, 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 223 | 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c}; 224 | uint32_t crc = ~0L; 225 | // Skip first 4 bytes of EEPROM as thats where we store the CRC 226 | for (int index = 4; index < EEPROM.length(); ++index) { 227 | crc = crc_table[(crc ^ EEPROM[index]) & 0x0f] ^ (crc >> 4); 228 | crc = crc_table[(crc ^ (EEPROM[index] >> 4)) & 0x0f] ^ (crc >> 4); 229 | crc = ~crc; 230 | } 231 | 232 | return crc; 233 | } 234 | 235 | void updateCRC() { 236 | uint32_t new_crc = eepromCRC(); 237 | setCRC(new_crc); 238 | } 239 | 240 | bool validateCRC() { 241 | uint32_t stored_crc; 242 | EEPROM.get(CRC_ADDR, stored_crc); 243 | uint32_t calculated_crc = eepromCRC(); 244 | debugprint("got CRCs, stored: "); 245 | debugprint(stored_crc); 246 | debugprint(", calculated: "); 247 | debugprintln(calculated_crc); 248 | return stored_crc == calculated_crc; 249 | } 250 | 251 | inline void setupSensors() { 252 | sensors.begin(); 253 | sensor_count = sensors.getDeviceCount(); 254 | debugprint("Looking for sensors, found: "); 255 | debugprintln(sensor_count); 256 | for (int i = 0; i < min(sensor_count, sizeof(temp_addresses)); i++) { 257 | sensors.getAddress(temp_addresses[i], i); 258 | } 259 | } 260 | 261 | inline void setFastPwm() { analogWriteFrequency(64); } 262 | 263 | inline void setVREF() { analogReference(INTERNAL1V5); } 264 | 265 | inline bool isFirstBoot() { 266 | uint8_t first_boot = EEPROM.read(FIRSTTIME_BOOT_ADDR); 267 | debugprint("Got first boot flag: "); 268 | debugprintln(first_boot); 269 | return first_boot != 1; 270 | } 271 | 272 | inline void setFirstBoot() { 273 | EEPROM.write(FIRSTTIME_BOOT_ADDR, 1); 274 | updateCRC(); 275 | } 276 | 277 | inline float getResistance() { 278 | float f; 279 | return EEPROM.get(RESISTANCE_INDEX_ADDR, f); 280 | return f; 281 | } 282 | 283 | inline void setResistance(float resistance) { 284 | EEPROM.put(RESISTANCE_INDEX_ADDR, resistance); 285 | updateCRC(); 286 | } 287 | 288 | inline void setMaxTempIndex(int index) { 289 | EEPROM.update(TEMP_INDEX_ADDR, index); 290 | updateCRC(); 291 | } 292 | 293 | inline int getMaxTempIndex(void) { 294 | return EEPROM.read(TEMP_INDEX_ADDR) % sizeof(max_temp_array); 295 | } 296 | 297 | float getTemp() { 298 | debugprint("Temps: "); 299 | float t = 0; 300 | for (byte i = 0; i < 100; i++) { // Poll TEMP_PIN reading 100 times 301 | t = t + analogRead(TEMP_PIN); 302 | } 303 | t /= 100.0; // average 304 | t *= VOLTAGE_REFERENCE / 1024.0; // voltage 305 | // conversion to temp, consult datasheet: 306 | // https://www.ti.com/document-viewer/LMT85/datasheet/detailed-description#snis1681040 307 | // this is optimized for 25C to 150C 308 | // TODO(HEIDT) this is linearized and innacurate, could probably use the 309 | // nonlinear functions without much overhead. 310 | t = (t - 1.365) / ((.301 - 1.365) / (150.0 - 25.0)) + 25.0; 311 | 312 | // The analog sensor is too far from the bed for an accurate reading 313 | // this simple function estimates the true bed temperature based off the 314 | // thermal gradient 315 | float estimated_temp = 316 | t * ANALOG_APPROXIMATION_SCALAR + ANALOG_APPROXIMATION_OFFSET; 317 | debugprint(estimated_temp); 318 | debugprint(" "); 319 | 320 | sensors.requestTemperatures(); 321 | for (int i = 0; i < sensor_count; i++) { 322 | float temp_in = sensors.getTempC(temp_addresses[i]); 323 | debugprint(temp_in); 324 | debugprint(" "); 325 | } 326 | debugprintln(); 327 | 328 | return max(t, estimated_temp); 329 | } 330 | 331 | float getVolts() { 332 | float v = 0; 333 | for (byte i = 0; i < 20; i++) { // Poll Voltage reading 20 times 334 | v = v + analogRead(VCC_PIN); 335 | } 336 | v /= 20; 337 | 338 | float vin = (v / 1023.0) * 1.5; 339 | debugprint("voltage at term: "); 340 | debugprintln(vin); 341 | vin = (vin / 0.090981) + 0.3; 342 | return vin; 343 | } 344 | 345 | #define BUTTON_PRESS_TIME 50 346 | buttons_state_t getButtonsState() { 347 | single_button_state_t button_dn; 348 | single_button_state_t button_up; 349 | unsigned long button_dn_time; 350 | unsigned long button_up_time; 351 | 352 | noInterrupts(); 353 | button_dn = dn_button_state; 354 | button_up = up_button_state; 355 | button_dn_time = down_state_change_time; 356 | button_up_time = up_state_change_time; 357 | interrupts(); 358 | 359 | unsigned long cur_time = millis(); 360 | buttons_state_t state = BUTTONS_NO_PRESS; 361 | 362 | if (button_dn == BUTTON_PRESSED && button_up == BUTTON_PRESSED && 363 | abs(button_dn_time - button_up_time) < BUTTON_PRESS_TIME) { 364 | if (cur_time - button_dn_time > BUTTON_PRESS_TIME && 365 | cur_time - button_up_time > BUTTON_PRESS_TIME) { 366 | state = BUTTONS_BOTH_PRESS; 367 | noInterrupts(); 368 | dn_button_state = BUTTON_NO_ACTION; 369 | up_button_state = BUTTON_NO_ACTION; 370 | interrupts(); 371 | } 372 | } else if (button_up == BUTTON_PRESSED && 373 | cur_time - button_up_time > BUTTON_PRESS_TIME) { 374 | state = BUTTONS_UP_PRESS; 375 | noInterrupts(); 376 | up_button_state = BUTTON_NO_ACTION; 377 | interrupts(); 378 | } else if (button_dn == BUTTON_PRESSED && 379 | cur_time - button_dn_time > BUTTON_PRESS_TIME) { 380 | state = BUTTONS_DN_PRESS; 381 | noInterrupts(); 382 | dn_button_state = BUTTON_NO_ACTION; 383 | interrupts(); 384 | } 385 | 386 | return state; 387 | } 388 | 389 | // -------------------- UI Parts ----------------------------------- 390 | 391 | inline void clearMainMenu() { 392 | display.clearDisplay(); 393 | display.setTextSize(1); 394 | display.drawRoundRect(0, 0, 83, 32, 2, SSD1306_WHITE); 395 | } 396 | 397 | inline void getResistanceFromUser() { 398 | float resistance = 1.88; 399 | while (1) { 400 | clearMainMenu(); 401 | display.setCursor(3, 4); 402 | display.print(F("Resistance")); 403 | display.drawLine(3, 12, 79, 12, SSD1306_WHITE); 404 | display.setCursor(3, 14); 405 | display.print(F("UP/DN: change")); 406 | display.setCursor(3, 22); 407 | display.print(F("BOTH: choose")); 408 | buttons_state_t button = getButtonsState(); 409 | if (button == BUTTONS_UP_PRESS) { 410 | resistance += 0.01; 411 | } else if (button == BUTTONS_DN_PRESS) { 412 | resistance -= 0.01; 413 | } else if (button == BUTTONS_BOTH_PRESS) { 414 | setResistance(resistance); 415 | return; 416 | } 417 | resistance = constrain(resistance, 0, MAX_RESISTANCE); 418 | 419 | display.setCursor(90, 12); 420 | display.print(resistance); 421 | display.display(); 422 | } 423 | } 424 | 425 | inline void doSetup() { 426 | debugprintln("Performing setup"); 427 | // TODO(HEIDT) show an info screen if we're doing firstime setup or if memory 428 | // is corrupted 429 | 430 | getResistanceFromUser(); 431 | // TODO(HEIDT) do a temperature module setup here 432 | 433 | setFirstBoot(); 434 | } 435 | 436 | void showLogo() { 437 | unsigned long start_time = millis(); 438 | display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS); 439 | while (start_time + 2000 > millis()) { 440 | display.clearDisplay(); 441 | display.setTextSize(1); 442 | display.setTextColor(SSD1306_WHITE); 443 | display.setCursor(0, 0); 444 | display.drawBitmap(0, 0, logo, logo_width, logo_height, SSD1306_WHITE); 445 | display.setCursor(80, 16); 446 | display.print(F("S/W V")); 447 | display.print(sw, 1); 448 | display.setCursor(80, 24); 449 | display.print(F("H/W V")); 450 | display.print(hw, 1); 451 | display.display(); 452 | buttons_state_t cur_button = getButtonsState(); 453 | // If we press both buttons during boot, we'll enter the setup process 454 | if (cur_button == BUTTONS_BOTH_PRESS) { 455 | doSetup(); 456 | return; 457 | } 458 | } 459 | } 460 | 461 | inline void displayProfileRight(int8_t cur_profile) { 462 | int cur_x = 90; 463 | int cur_y = 30; 464 | // start at x=90, go to SCREEN_WIDTH-8, save 6 pixels for cooldown 465 | float x_dist = SCREEN_WIDTH - 90 - 8; 466 | display.setCursor(cur_x, cur_y); 467 | float total_seconds = 468 | (int)profiles[cur_profile].seconds[profiles[cur_profile].points - 1]; 469 | 470 | for (int i = 0; i < profiles[cur_profile].points; i++) { 471 | int x_next = 472 | (int)((profiles[cur_profile].seconds[i] / total_seconds) * x_dist) + 90; 473 | int y_next = 30 - (int)(profiles[cur_profile].fraction[i] * 28.0); 474 | display.drawLine(cur_x, cur_y, x_next, y_next, SSD1306_WHITE); 475 | cur_x = x_next; 476 | cur_y = y_next; 477 | } 478 | // draw down to finish TEMP_PIN 479 | display.drawLine(cur_x, cur_y, SCREEN_WIDTH - 2, 30, SSD1306_WHITE); 480 | } 481 | 482 | inline uint8_t getProfile() { 483 | uint8_t cur_profile = 0; 484 | while (1) { 485 | clearMainMenu(); 486 | display.setCursor(3, 4); 487 | display.print(F("Pick profile")); 488 | display.drawLine(3, 12, 79, 12, SSD1306_WHITE); 489 | display.setCursor(3, 14); 490 | display.print(F(" UP/DN: cycle")); 491 | display.setCursor(3, 22); 492 | display.print(F(" BOTH: choose")); 493 | buttons_state_t cur_button = getButtonsState(); 494 | if (cur_button == BUTTONS_BOTH_PRESS) { 495 | clearMainMenu(); 496 | return cur_profile; 497 | } else if (cur_button == BUTTONS_DN_PRESS) { 498 | cur_profile--; 499 | } else if (cur_button == BUTTONS_UP_PRESS) { 500 | cur_profile++; 501 | } 502 | cur_profile %= NUM_PROFILES; 503 | displayProfileRight(cur_profile); 504 | display.display(); 505 | } 506 | } 507 | 508 | void cancelledTimer() { // Cancelled via 5 minute Time Limit 509 | // Initiate Swap Display 510 | int x = 0; // Display change counter 511 | int y = 150; // Display change max (modulused below) 512 | 513 | // Wait to return on any button press 514 | while (getButtonsState() == BUTTONS_NO_PRESS) { 515 | // Update Display 516 | display.clearDisplay(); 517 | display.drawRoundRect(22, 0, 84, 32, 2, SSD1306_WHITE); 518 | display.setCursor(25, 4); 519 | display.print(F(" TIMED OUT")); 520 | display.drawLine(25, 12, 103, 12, SSD1306_WHITE); 521 | 522 | // Swap Main Text 523 | if (x < (y * 0.3)) { 524 | display.setCursor(25, 14); 525 | display.println(" Took longer"); 526 | display.setCursor(25, 22); 527 | display.println(" than 5 mins"); 528 | } else if (x < (y * 0.6)) { 529 | display.setCursor(28, 14); 530 | display.println("Try a higher"); 531 | display.setCursor(25, 22); 532 | display.println(" current PSU"); 533 | } else { 534 | display.setCursor(25, 14); 535 | display.println(" Push button"); 536 | display.setCursor(25, 22); 537 | display.println(" to return"); 538 | } 539 | x = (x + 1) % y; // Display change increment and modulus 540 | 541 | display.setTextSize(3); 542 | display.setCursor(5, 4); 543 | display.print(F("!")); 544 | display.setTextSize(3); 545 | display.setCursor(108, 4); 546 | display.print(F("!")); 547 | display.setTextSize(1); 548 | display.display(); 549 | delay(50); 550 | } 551 | } 552 | 553 | inline void showHeatMenu(byte max_temp) { 554 | display.clearDisplay(); 555 | display.setTextSize(2); 556 | display.setCursor(22, 4); 557 | display.print(F("HEATING")); 558 | display.setTextSize(1); 559 | display.setCursor(52, 24); 560 | display.print(max_temp); 561 | display.print(F("C")); 562 | display.display(); 563 | } 564 | 565 | void stepPID(float target_temp, float current_temp, float last_temp, float dt, 566 | int min_pwm) { 567 | float error = target_temp - current_temp; 568 | float D = (current_temp - last_temp) / dt; 569 | 570 | error_I += error * dt * kI; 571 | error_I = constrain(error_I, 0, I_clip); 572 | 573 | // PWM is inverted so 0 duty is 100% power 574 | float PWM = 255.0 - (error * kP + D * kD + error_I); 575 | PWM = constrain(PWM, min_pwm, 255); 576 | 577 | debugprintln("PID"); 578 | debugprintln(dt); 579 | debugprintln(error); 580 | debugprintln(error_I); 581 | debugprint("PWM: "); 582 | debugprintln(PWM); 583 | analogWrite(MOSFET_PIN, (int)PWM); 584 | } 585 | 586 | bool heat(byte max_temp, int profile_index) { 587 | // Heating Display 588 | showHeatMenu(max_temp); 589 | delay(3000); 590 | 591 | float t; // Used to store current temperature 592 | float v; // Used to store current voltage 593 | 594 | unsigned long profile_max_time = millis() / 1000 + (8 * 60); 595 | unsigned long step_start_time = (millis() / 1000); 596 | int current_step = 0; 597 | 598 | // Other control variables 599 | int x = 0; // Heat Animate Counter 600 | int y = 80; // Heat Animate max (modulused below) 601 | 602 | float start_temp = getTemp(); 603 | float goal_temp = profiles[profile_index].fraction[0] * max_temp; 604 | float step_runtime = profiles[profile_index].seconds[0]; 605 | float last_time = 0; 606 | float last_temp = getTemp(); 607 | error_I = 0; 608 | 609 | while (1) { 610 | // Cancel heat, don't even wait for uppress so we don't risk missing it 611 | // during the loop 612 | if (getButtonsState() != BUTTONS_NO_PRESS) { 613 | analogWrite(MOSFET_PIN, MOSFET_PIN_OFF); 614 | debugprintln("cancelled"); 615 | return 0; 616 | } 617 | 618 | // Check Heating not taken more than 8 minutes 619 | if (millis() / 1000 > profile_max_time) { 620 | analogWrite(MOSFET_PIN, MOSFET_PIN_OFF); 621 | debugprintln("exceeded time"); 622 | cancelledTimer(); 623 | return 0; 624 | } 625 | 626 | // Measure Values 627 | // TODO(HEIDT) getting the temperature from the digital sensors is by far 628 | // the slowest part of this loop. figure out an approach that allows control 629 | // faster than sensing 630 | t = getTemp(); 631 | v = getVolts(); 632 | float max_possible_amperage = v / bed_resistance; 633 | // TODO(HEIDT) approximate true resistance based on cold resistance and 634 | // temperature 635 | float vmax = (MAX_AMPERAGE * bed_resistance) * PWM_VOLTAGE_SCALAR; 636 | int min_PWM = 255 - ((vmax * 255.0) / v); 637 | min_PWM = constrain(min_PWM, 0, 255); 638 | debugprint("Min PWM: "); 639 | debugprintln(min_PWM); 640 | debugprintln(bed_resistance); 641 | 642 | // Determine what target temp is and PID to it 643 | float time_into_step = ((float)millis() / 1000.0) - (float)step_start_time; 644 | float target_temp = 645 | min(((goal_temp - start_temp) * (time_into_step / step_runtime)) + 646 | start_temp, 647 | goal_temp); 648 | 649 | // TODO(HEIDT) PID for a ramp will always lag, other options may be better 650 | stepPID(target_temp, t, last_temp, time_into_step - last_time, min_PWM); 651 | last_time = time_into_step; 652 | 653 | // if we finish the step timewise 654 | if (time_into_step >= step_runtime) { 655 | // and if we're within the goal temperature of the step 656 | if (abs(t - goal_temp) < TARGET_TEMP_THRESHOLD) { 657 | // move onto the next step in the profile 658 | current_step++; 659 | // if that was the last step, we're done! 660 | if (current_step == profiles[profile_index].points) { 661 | analogWrite(MOSFET_PIN, MOSFET_PIN_OFF); 662 | return 1; 663 | } 664 | // otherwise, get the next goal temperature and runtime, and do the 665 | // process again 666 | last_time = 0.0; 667 | start_temp = t; 668 | goal_temp = profiles[profile_index].fraction[current_step] * max_temp; 669 | step_runtime = profiles[profile_index].seconds[current_step] - 670 | profiles[profile_index].seconds[current_step - 1]; 671 | step_start_time = millis() / 1000.0; 672 | } 673 | } 674 | 675 | heatAnimate(x, y, v, t, target_temp); 676 | } 677 | } 678 | 679 | void evaluate_heat() { 680 | debugprintln("Starting thermal evaluation"); 681 | uint8_t duties[] = {255, 225, 200, 150, 100, 50, 0}; 682 | unsigned long runtime = 60 * 5; // run each for 5 minutes 683 | 684 | for (int i = 0; i < sizeof(duties); i++) { 685 | debugprint("Running to duty of: "); 686 | debugprintln(duties[i]); 687 | unsigned long start_time = millis(); 688 | analogWrite(MOSFET_PIN, duties[i]); 689 | float elapsed_time = (millis() - start_time) / 1000.0; 690 | while (elapsed_time < runtime) { 691 | debugprint("elapsed time: "); 692 | debugprintln(elapsed_time); 693 | debugprint("runtime: "); 694 | debugprintln(runtime); 695 | elapsed_time = (millis() - start_time) / 1000.0; 696 | float v = getVolts(); 697 | float t = getTemp(); 698 | delay(500); 699 | } 700 | } 701 | 702 | analogWrite(MOSFET_PIN, MOSFET_PIN_OFF); 703 | } 704 | 705 | void inline heatAnimate(int &x, int &y, float v, float t, float target) { 706 | // Heat Animate Control 707 | display.clearDisplay(); 708 | display.drawBitmap(0, 3, heat_animate, heat_animate_width, 709 | heat_animate_height, SSD1306_WHITE); 710 | display.drawBitmap(112, 3, heat_animate, heat_animate_width, 711 | heat_animate_height, SSD1306_WHITE); 712 | display.fillRect(0, 3, heat_animate_width, heat_animate_height * (y - x) / y, 713 | SSD1306_BLACK); 714 | display.fillRect(112, 3, heat_animate_width, 715 | heat_animate_height * (y - x) / y, SSD1306_BLACK); 716 | x = (x + 1) % y; // Heat animate increment and modulus 717 | 718 | // Update display 719 | display.setTextSize(2); 720 | display.setCursor(22, 4); 721 | display.print(F("HEATING")); 722 | display.setTextSize(1); 723 | display.setCursor(20, 24); 724 | display.print(F("~")); 725 | display.print(v, 1); 726 | display.print(F("V")); 727 | if (t >= 100) { 728 | display.setCursor(63, 24); 729 | } else if (t >= 10) { 730 | display.setCursor(66, 24); 731 | } else { 732 | display.setCursor(69, 24); 733 | } 734 | display.print(F("~")); 735 | display.print(t, 0); 736 | display.print(F("C")); 737 | display.print(F("/")); 738 | display.print(target, 0); 739 | display.print(F("C")); 740 | display.display(); 741 | } 742 | 743 | void cancelledPB() { // Cancelled via push button 744 | // Update Display 745 | display.clearDisplay(); 746 | display.drawRoundRect(22, 0, 84, 32, 2, SSD1306_WHITE); 747 | display.setCursor(25, 4); 748 | display.print(F(" CANCELLED")); 749 | display.display(); 750 | delay(2000); 751 | } 752 | 753 | void coolDown() { 754 | float t = getTemp(); // Used to store current temperature 755 | 756 | // Wait to return on any button press, or TEMP_PIN below threshold 757 | while (getButtonsState() == BUTTONS_NO_PRESS && t > 45.00) { 758 | display.clearDisplay(); 759 | display.drawRoundRect(22, 0, 84, 32, 2, SSD1306_WHITE); 760 | display.setCursor(25, 4); 761 | display.print(F(" COOL DOWN")); 762 | display.drawLine(25, 12, 103, 12, SSD1306_WHITE); 763 | display.setCursor(25, 14); 764 | display.println(" Still Hot"); 765 | t = getTemp(); 766 | if (t >= 100) { 767 | display.setCursor(49, 22); 768 | } else { 769 | display.setCursor(52, 22); 770 | } 771 | display.print(F("~")); 772 | display.print(t, 0); 773 | display.print(F("C")); 774 | display.setTextSize(3); 775 | display.setCursor(5, 4); 776 | display.print(F("!")); 777 | display.setTextSize(3); 778 | display.setCursor(108, 4); 779 | display.print(F("!")); 780 | display.setTextSize(1); 781 | display.display(); 782 | } 783 | } 784 | 785 | void completed() { 786 | // Update Display 787 | display.clearDisplay(); 788 | display.drawRoundRect(22, 0, 84, 32, 2, SSD1306_WHITE); 789 | display.setCursor(25, 4); 790 | display.print(F(" COMPLETED ")); 791 | display.drawLine(25, 12, 103, 12, SSD1306_WHITE); 792 | display.setCursor(25, 14); 793 | display.println(" Push button"); 794 | display.setCursor(25, 22); 795 | display.println(" to return"); 796 | display.drawBitmap(0, 9, tick, tick_width, tick_height, SSD1306_WHITE); 797 | display.drawBitmap(112, 9, tick, tick_width, tick_height, SSD1306_WHITE); 798 | display.display(); 799 | 800 | // Wait to return on any button press 801 | while (getButtonsState() == BUTTONS_NO_PRESS) { 802 | } 803 | } 804 | 805 | inline void showMainMenuLeft(int &x, int &y) { 806 | if (x < (y * 0.5)) { 807 | display.setCursor(3, 4); 808 | display.print(F("PRESS BUTTONS")); 809 | display.drawLine(3, 12, 79, 12, SSD1306_WHITE); 810 | display.setCursor(3, 14); 811 | display.print(F(" Change MAX")); 812 | display.setCursor(3, 22); 813 | display.print(F(" Temperature")); 814 | } else { 815 | display.setCursor(3, 4); 816 | display.print(F("HOLD BUTTONS")); 817 | display.drawLine(3, 12, 79, 12, SSD1306_WHITE); 818 | display.setCursor(3, 18); 819 | display.print(F("Begin Heating")); 820 | } 821 | x = (x + 1) % y; // Display change increment and modulus 822 | } 823 | 824 | inline void showMainMenuRight() { 825 | display.setCursor(95, 6); 826 | display.print(F("TEMP")); 827 | display.setCursor(95, 18); 828 | display.print(max_temp_array[max_temp_index]); 829 | display.print(F("C")); 830 | display.display(); 831 | } 832 | 833 | inline void mainMenu() { 834 | // Debounce 835 | menu_state_t cur_state = MENU_IDLE; 836 | 837 | int x = 0; // Display change counter 838 | int y = 200; // Display change max (modulused below) 839 | uint8_t profile_index = 0; 840 | 841 | while (1) { 842 | switch (cur_state) { 843 | case MENU_IDLE: { 844 | clearMainMenu(); 845 | buttons_state_t cur_button = getButtonsState(); 846 | 847 | if (cur_button == BUTTONS_BOTH_PRESS) { 848 | cur_state = MENU_SELECT_PROFILE; 849 | } else if (cur_button == BUTTONS_UP_PRESS) { 850 | cur_state = MENU_INC_TEMP; 851 | } else if (cur_button == BUTTONS_DN_PRESS) { 852 | cur_state = MENU_DEC_TEMP; 853 | } 854 | } break; 855 | case MENU_SELECT_PROFILE: { 856 | debugprintln("getting thermal profile"); 857 | profile_index = getProfile(); 858 | cur_state = MENU_HEAT; 859 | } break; 860 | case MENU_HEAT: { 861 | if (!heat(max_temp_array[max_temp_index], profile_index)) { 862 | cancelledPB(); 863 | coolDown(); 864 | } else { 865 | coolDown(); 866 | completed(); 867 | } 868 | cur_state = MENU_IDLE; 869 | } break; 870 | case MENU_INC_TEMP: { 871 | if (max_temp_index < sizeof(max_temp_array) - 1) { 872 | max_temp_index++; 873 | debugprintln("incrementing max temp"); 874 | setMaxTempIndex(max_temp_index); 875 | } 876 | cur_state = MENU_IDLE; 877 | } break; 878 | case MENU_DEC_TEMP: { 879 | if (max_temp_index > 0) { 880 | max_temp_index--; 881 | debugprintln("decrementing max temp"); 882 | setMaxTempIndex(max_temp_index); 883 | } 884 | cur_state = MENU_IDLE; 885 | } break; 886 | } 887 | 888 | // Change Display (left-side) 889 | showMainMenuLeft(x, y); 890 | 891 | // Update Display (right-side) 892 | showMainMenuRight(); 893 | } 894 | } 895 | 896 | // -------------------- Main Logic ----------------------------------- 897 | 898 | void setup() { 899 | // Pin Direction control 900 | pinMode(MOSFET_PIN, OUTPUT); 901 | pinMode(UPSW_PIN, INPUT); 902 | pinMode(DNSW_PIN, INPUT); 903 | pinMode(TEMP_PIN, INPUT); 904 | pinMode(VCC_PIN, INPUT); 905 | pinMode(LED_GREEN_PIN, OUTPUT); 906 | 907 | digitalWrite(LED_GREEN_PIN, HIGH); 908 | analogWrite(MOSFET_PIN, 255); // VERY IMPORTANT, DONT CHANGE! 909 | 910 | attachInterrupt(DNSW_PIN, dnsw_change_isr, FALLING); 911 | attachInterrupt(UPSW_PIN, upsw_change_isr, FALLING); 912 | 913 | Serial.begin(9600); 914 | 915 | // Enable Fast PWM with no prescaler 916 | setFastPwm(); 917 | setVREF(); 918 | 919 | // Start-up Diplay 920 | debugprintln("Showing startup"); 921 | showLogo(); 922 | 923 | debugprintln("Checking sensors"); 924 | // check onewire TEMP_PIN sensors 925 | setupSensors(); 926 | 927 | debugprintln("Checking first boot"); 928 | if (isFirstBoot() || !validateCRC()) { 929 | doSetup(); 930 | } 931 | 932 | // Pull saved values from EEPROM 933 | max_temp_index = getMaxTempIndex(); 934 | bed_resistance = getResistance(); 935 | 936 | debugprintln("Entering main menu"); 937 | // Go to main menu 938 | mainMenu(); 939 | } 940 | 941 | void loop() { 942 | // Not used 943 | } -------------------------------------------------------------------------------- /Firmware/testing/analysis/LMT85 study.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import matplotlib.pyplot as plt" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "metadata": {}, 16 | "source": [ 17 | "### LMT85 study\n", 18 | "A couple goals here. Determine if the linearization of the LMT85 response curve is sufficient" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 2, 24 | "metadata": {}, 25 | "outputs": [ 26 | { 27 | "data": { 28 | "image/png": "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\n", 29 | "text/plain": [ 30 | "
" 31 | ] 32 | }, 33 | "metadata": { 34 | "needs_background": "light" 35 | }, 36 | "output_type": "display_data" 37 | }, 38 | { 39 | "data": { 40 | "image/png": "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\n", 41 | "text/plain": [ 42 | "
" 43 | ] 44 | }, 45 | "metadata": { 46 | "needs_background": "light" 47 | }, 48 | "output_type": "display_data" 49 | } 50 | ], 51 | "source": [ 52 | "voltages = np.linspace(1365, 310, num=1000)\n", 53 | "\n", 54 | "def linearfunc(v):\n", 55 | " return (v - 1365.) / ((301. - 1365.)/(150. - 25.)) + 25.\n", 56 | "\n", 57 | "def nonlinearfunc(v):\n", 58 | " squared_section = np.sqrt(8.194**2.0 + 4*0.00262*(1324 - v))\n", 59 | " return (8.194 - squared_section)/(2*-0.00262) + 30\n", 60 | "\n", 61 | "x_linear = linearfunc(voltages)\n", 62 | "x_nonlinear = nonlinearfunc(voltages)\n", 63 | "error = x_nonlinear - x_linear\n", 64 | "\n", 65 | "plt.figure()\n", 66 | "plt.plot(voltages, x_linear, label=\"linear\")\n", 67 | "plt.plot(voltages, x_nonlinear, label=\"nonlinear\")\n", 68 | "plt.legend()\n", 69 | "plt.title(\"theoretical linear vs nonlinear graph\")\n", 70 | "plt.show()\n", 71 | "\n", 72 | "plt.figure()\n", 73 | "plt.plot(voltages, error, label=\"error\")\n", 74 | "plt.title(\"error between linear and nonlinear transfer functions\")\n", 75 | "plt.show()" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "metadata": {}, 81 | "source": [ 82 | "# Conclusion\n", 83 | "The conclusion is that the linear function is fine for this application. It has a maximum error of ~1.3C over the range" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": null, 89 | "metadata": {}, 90 | "outputs": [], 91 | "source": [] 92 | } 93 | ], 94 | "metadata": { 95 | "kernelspec": { 96 | "display_name": "Python 3", 97 | "language": "python", 98 | "name": "python3" 99 | }, 100 | "language_info": { 101 | "codemirror_mode": { 102 | "name": "ipython", 103 | "version": 3 104 | }, 105 | "file_extension": ".py", 106 | "mimetype": "text/x-python", 107 | "name": "python", 108 | "nbconvert_exporter": "python", 109 | "pygments_lexer": "ipython3", 110 | "version": "3.8.10" 111 | } 112 | }, 113 | "nbformat": 4, 114 | "nbformat_minor": 4 115 | } 116 | -------------------------------------------------------------------------------- /Firmware/testing/temperature_groundtruth_fw/temperature_groundtruth_fw.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Use software SPI: CS, DI, DO, CLK 4 | Adafruit_MAX31865 thermo = Adafruit_MAX31865(3, 4, 5, 6); 5 | 6 | // The value of the Rref resistor. Use 430.0 for PT100 and 4300.0 for PT1000 7 | #define RREF 430.0 8 | // The 'nominal' 0-degrees-C resistance of the sensor 9 | // 100.0 for PT100, 1000.0 for PT1000 10 | #define RNOMINAL 100.0 11 | 12 | 13 | void setup() { 14 | Serial.begin(115200); 15 | Serial1.begin(9600); 16 | thermo.begin(MAX31865_3WIRE); // set to 2WIRE or 4WIRE as necessary 17 | 18 | } 19 | 20 | void loop() { 21 | 22 | 23 | uint8_t fault = thermo.readFault(); 24 | if (fault) { 25 | Serial.print("Fault 0x"); Serial.println(fault, HEX); 26 | if (fault & MAX31865_FAULT_HIGHTHRESH) { 27 | Serial.println("RTD High Threshold"); 28 | } 29 | if (fault & MAX31865_FAULT_LOWTHRESH) { 30 | Serial.println("RTD Low Threshold"); 31 | } 32 | if (fault & MAX31865_FAULT_REFINLOW) { 33 | Serial.println("REFIN- > 0.85 x Bias"); 34 | } 35 | if (fault & MAX31865_FAULT_REFINHIGH) { 36 | Serial.println("REFIN- < 0.85 x Bias - FORCE- open"); 37 | } 38 | if (fault & MAX31865_FAULT_RTDINLOW) { 39 | Serial.println("RTDIN- < 0.85 x Bias - FORCE- open"); 40 | } 41 | if (fault & MAX31865_FAULT_OVUV) { 42 | Serial.println("Under/Over voltage"); 43 | } 44 | thermo.clearFault(); 45 | } 46 | 47 | String s; 48 | if (Serial1.available()) { 49 | s = Serial1.readStringUntil('\n'); 50 | Serial.println(s); 51 | } 52 | 53 | if (s.indexOf("Temps") >= 0) { 54 | Serial.print("Groundtruth "); 55 | Serial.println(thermo.temperature(RNOMINAL, RREF)); 56 | } 57 | } -------------------------------------------------------------------------------- /Heatplate_v1.0/Heatplate_v1.0.kicad_prl: -------------------------------------------------------------------------------- 1 | { 2 | "board": { 3 | "active_layer": 37, 4 | "active_layer_preset": "", 5 | "auto_track_width": false, 6 | "hidden_nets": [], 7 | "high_contrast_mode": 0, 8 | "net_color_mode": 1, 9 | "opacity": { 10 | "pads": 1.0, 11 | "tracks": 1.0, 12 | "vias": 1.0, 13 | "zones": 0.6 14 | }, 15 | "ratsnest_display_mode": 0, 16 | "selection_filter": { 17 | "dimensions": true, 18 | "footprints": true, 19 | "graphics": true, 20 | "keepouts": true, 21 | "lockedItems": true, 22 | "otherItems": true, 23 | "pads": true, 24 | "text": true, 25 | "tracks": true, 26 | "vias": true, 27 | "zones": true 28 | }, 29 | "visible_items": [ 30 | 0, 31 | 1, 32 | 2, 33 | 3, 34 | 4, 35 | 5, 36 | 8, 37 | 9, 38 | 10, 39 | 11, 40 | 12, 41 | 13, 42 | 14, 43 | 15, 44 | 16, 45 | 17, 46 | 18, 47 | 19, 48 | 20, 49 | 21, 50 | 22, 51 | 23, 52 | 24, 53 | 25, 54 | 26, 55 | 27, 56 | 28, 57 | 29, 58 | 30, 59 | 32, 60 | 33, 61 | 34, 62 | 35, 63 | 36 64 | ], 65 | "visible_layers": "000ffff_80000001", 66 | "zone_display_mode": 0 67 | }, 68 | "meta": { 69 | "filename": "Heatplate_v1.0.kicad_prl", 70 | "version": 3 71 | }, 72 | "project": { 73 | "files": [] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Heatplate_v1.0/Heatplate_v1.0.kicad_pro: -------------------------------------------------------------------------------- 1 | { 2 | "board": { 3 | "design_settings": { 4 | "defaults": { 5 | "board_outline_line_width": 0.049999999999999996, 6 | "copper_line_width": 0.19999999999999998, 7 | "copper_text_italic": false, 8 | "copper_text_size_h": 1.5, 9 | "copper_text_size_v": 1.5, 10 | "copper_text_thickness": 0.3, 11 | "copper_text_upright": false, 12 | "courtyard_line_width": 0.049999999999999996, 13 | "dimension_precision": 4, 14 | "dimension_units": 3, 15 | "dimensions": { 16 | "arrow_length": 1270000, 17 | "extension_offset": 500000, 18 | "keep_text_aligned": true, 19 | "suppress_zeroes": false, 20 | "text_position": 0, 21 | "units_format": 1 22 | }, 23 | "fab_line_width": 0.09999999999999999, 24 | "fab_text_italic": false, 25 | "fab_text_size_h": 1.0, 26 | "fab_text_size_v": 1.0, 27 | "fab_text_thickness": 0.15, 28 | "fab_text_upright": false, 29 | "other_line_width": 0.09999999999999999, 30 | "other_text_italic": false, 31 | "other_text_size_h": 1.0, 32 | "other_text_size_v": 1.0, 33 | "other_text_thickness": 0.15, 34 | "other_text_upright": false, 35 | "pads": { 36 | "drill": 0.0, 37 | "height": 1.1, 38 | "width": 1.5 39 | }, 40 | "silk_line_width": 0.12, 41 | "silk_text_italic": false, 42 | "silk_text_size_h": 1.0, 43 | "silk_text_size_v": 1.0, 44 | "silk_text_thickness": 0.15, 45 | "silk_text_upright": false, 46 | "zones": { 47 | "45_degree_only": false, 48 | "min_clearance": 0.15 49 | } 50 | }, 51 | "diff_pair_dimensions": [ 52 | { 53 | "gap": 0.0, 54 | "via_gap": 0.0, 55 | "width": 0.0 56 | } 57 | ], 58 | "drc_exclusions": [ 59 | "silk_over_copper|67107500|59950000|c801d42e-dd94-493e-bd2f-6c3ddad43f55|e4664c1b-a4cd-4eed-8c0f-efff7b6d7960", 60 | "silk_over_copper|67107500|60000000|c801d42e-dd94-493e-bd2f-6c3ddad43f55|03cb71dc-aa9f-4ef4-9b19-7d6989258e7e", 61 | "silk_over_copper|76307500|60000000|c801d42e-dd94-493e-bd2f-6c3ddad43f55|eae91cba-d07c-496a-a67b-cd0a2f6e08aa" 62 | ], 63 | "meta": { 64 | "version": 2 65 | }, 66 | "rule_severities": { 67 | "annular_width": "error", 68 | "clearance": "error", 69 | "copper_edge_clearance": "error", 70 | "courtyards_overlap": "error", 71 | "diff_pair_gap_out_of_range": "error", 72 | "diff_pair_uncoupled_length_too_long": "error", 73 | "drill_out_of_range": "error", 74 | "duplicate_footprints": "warning", 75 | "extra_footprint": "warning", 76 | "footprint_type_mismatch": "error", 77 | "hole_clearance": "error", 78 | "hole_near_hole": "error", 79 | "invalid_outline": "error", 80 | "item_on_disabled_layer": "error", 81 | "items_not_allowed": "error", 82 | "length_out_of_range": "error", 83 | "malformed_courtyard": "error", 84 | "microvia_drill_out_of_range": "error", 85 | "missing_courtyard": "ignore", 86 | "missing_footprint": "warning", 87 | "net_conflict": "warning", 88 | "npth_inside_courtyard": "ignore", 89 | "padstack": "error", 90 | "pth_inside_courtyard": "ignore", 91 | "shorting_items": "error", 92 | "silk_over_copper": "warning", 93 | "silk_overlap": "warning", 94 | "skew_out_of_range": "error", 95 | "through_hole_pad_without_hole": "error", 96 | "too_many_vias": "error", 97 | "track_dangling": "warning", 98 | "track_width": "error", 99 | "tracks_crossing": "error", 100 | "unconnected_items": "error", 101 | "unresolved_variable": "error", 102 | "via_dangling": "warning", 103 | "zone_has_empty_net": "error", 104 | "zones_intersect": "error" 105 | }, 106 | "rules": { 107 | "allow_blind_buried_vias": false, 108 | "allow_microvias": false, 109 | "max_error": 0.005, 110 | "min_clearance": 0.15, 111 | "min_copper_edge_clearance": 0.3, 112 | "min_hole_clearance": 0.25, 113 | "min_hole_to_hole": 0.25, 114 | "min_microvia_diameter": 0.19999999999999998, 115 | "min_microvia_drill": 0.09999999999999999, 116 | "min_silk_clearance": 0.0, 117 | "min_through_hole_diameter": 0.3, 118 | "min_track_width": 0.15, 119 | "min_via_annular_width": 0.15, 120 | "min_via_diameter": 0.6, 121 | "use_height_for_length_calcs": true 122 | }, 123 | "track_widths": [ 124 | 0.0, 125 | 0.3, 126 | 0.6, 127 | 1.25 128 | ], 129 | "via_dimensions": [ 130 | { 131 | "diameter": 0.0, 132 | "drill": 0.0 133 | } 134 | ], 135 | "zones_allow_external_fillets": true, 136 | "zones_use_no_outline": true 137 | }, 138 | "layer_presets": [] 139 | }, 140 | "boards": [], 141 | "cvpcb": { 142 | "equivalence_files": [] 143 | }, 144 | "erc": { 145 | "erc_exclusions": [], 146 | "meta": { 147 | "version": 0 148 | }, 149 | "pin_map": [ 150 | [ 151 | 0, 152 | 0, 153 | 0, 154 | 0, 155 | 0, 156 | 0, 157 | 1, 158 | 0, 159 | 0, 160 | 0, 161 | 0, 162 | 2 163 | ], 164 | [ 165 | 0, 166 | 2, 167 | 0, 168 | 1, 169 | 0, 170 | 0, 171 | 1, 172 | 0, 173 | 2, 174 | 2, 175 | 2, 176 | 2 177 | ], 178 | [ 179 | 0, 180 | 0, 181 | 0, 182 | 0, 183 | 0, 184 | 0, 185 | 1, 186 | 0, 187 | 1, 188 | 0, 189 | 1, 190 | 2 191 | ], 192 | [ 193 | 0, 194 | 1, 195 | 0, 196 | 0, 197 | 0, 198 | 0, 199 | 1, 200 | 1, 201 | 2, 202 | 1, 203 | 1, 204 | 2 205 | ], 206 | [ 207 | 0, 208 | 0, 209 | 0, 210 | 0, 211 | 0, 212 | 0, 213 | 1, 214 | 0, 215 | 0, 216 | 0, 217 | 0, 218 | 2 219 | ], 220 | [ 221 | 0, 222 | 0, 223 | 0, 224 | 0, 225 | 0, 226 | 0, 227 | 0, 228 | 0, 229 | 0, 230 | 0, 231 | 0, 232 | 2 233 | ], 234 | [ 235 | 1, 236 | 1, 237 | 1, 238 | 1, 239 | 1, 240 | 0, 241 | 1, 242 | 1, 243 | 1, 244 | 1, 245 | 1, 246 | 2 247 | ], 248 | [ 249 | 0, 250 | 0, 251 | 0, 252 | 1, 253 | 0, 254 | 0, 255 | 1, 256 | 0, 257 | 0, 258 | 0, 259 | 0, 260 | 2 261 | ], 262 | [ 263 | 0, 264 | 2, 265 | 1, 266 | 2, 267 | 0, 268 | 0, 269 | 1, 270 | 0, 271 | 2, 272 | 2, 273 | 2, 274 | 2 275 | ], 276 | [ 277 | 0, 278 | 2, 279 | 0, 280 | 1, 281 | 0, 282 | 0, 283 | 1, 284 | 0, 285 | 2, 286 | 0, 287 | 0, 288 | 2 289 | ], 290 | [ 291 | 0, 292 | 2, 293 | 1, 294 | 1, 295 | 0, 296 | 0, 297 | 1, 298 | 0, 299 | 2, 300 | 0, 301 | 0, 302 | 2 303 | ], 304 | [ 305 | 2, 306 | 2, 307 | 2, 308 | 2, 309 | 2, 310 | 2, 311 | 2, 312 | 2, 313 | 2, 314 | 2, 315 | 2, 316 | 2 317 | ] 318 | ], 319 | "rule_severities": { 320 | "bus_definition_conflict": "error", 321 | "bus_entry_needed": "error", 322 | "bus_label_syntax": "error", 323 | "bus_to_bus_conflict": "error", 324 | "bus_to_net_conflict": "error", 325 | "different_unit_footprint": "error", 326 | "different_unit_net": "error", 327 | "duplicate_reference": "error", 328 | "duplicate_sheet_names": "error", 329 | "extra_units": "error", 330 | "global_label_dangling": "warning", 331 | "hier_label_mismatch": "error", 332 | "label_dangling": "error", 333 | "lib_symbol_issues": "warning", 334 | "multiple_net_names": "warning", 335 | "net_not_bus_member": "warning", 336 | "no_connect_connected": "warning", 337 | "no_connect_dangling": "warning", 338 | "pin_not_connected": "error", 339 | "pin_not_driven": "error", 340 | "pin_to_pin": "warning", 341 | "power_pin_not_driven": "error", 342 | "similar_labels": "warning", 343 | "unannotated": "error", 344 | "unit_value_mismatch": "error", 345 | "unresolved_variable": "error", 346 | "wire_dangling": "error" 347 | } 348 | }, 349 | "libraries": { 350 | "pinned_footprint_libs": [], 351 | "pinned_symbol_libs": [] 352 | }, 353 | "meta": { 354 | "filename": "Heatplate_v1.0.kicad_pro", 355 | "version": 1 356 | }, 357 | "net_settings": { 358 | "classes": [ 359 | { 360 | "bus_width": 12.0, 361 | "clearance": 0.15, 362 | "diff_pair_gap": 0.25, 363 | "diff_pair_via_gap": 0.25, 364 | "diff_pair_width": 0.2, 365 | "line_style": 0, 366 | "microvia_diameter": 0.3, 367 | "microvia_drill": 0.1, 368 | "name": "Default", 369 | "pcb_color": "rgba(0, 0, 0, 0.000)", 370 | "schematic_color": "rgba(0, 0, 0, 0.000)", 371 | "track_width": 0.3, 372 | "via_diameter": 0.6, 373 | "via_drill": 0.3, 374 | "wire_width": 6.0 375 | }, 376 | { 377 | "bus_width": 12.0, 378 | "clearance": 0.4, 379 | "diff_pair_gap": 0.25, 380 | "diff_pair_via_gap": 0.25, 381 | "diff_pair_width": 0.2, 382 | "line_style": 0, 383 | "microvia_diameter": 0.3, 384 | "microvia_drill": 0.1, 385 | "name": "HEATBED", 386 | "nets": [ 387 | "HEATPLATE" 388 | ], 389 | "pcb_color": "rgba(0, 0, 0, 0.000)", 390 | "schematic_color": "rgba(0, 0, 0, 0.000)", 391 | "track_width": 1.15, 392 | "via_diameter": 0.6, 393 | "via_drill": 0.3, 394 | "wire_width": 6.0 395 | } 396 | ], 397 | "meta": { 398 | "version": 2 399 | }, 400 | "net_colors": null 401 | }, 402 | "pcbnew": { 403 | "last_paths": { 404 | "gencad": "", 405 | "idf": "", 406 | "netlist": "", 407 | "specctra_dsn": "", 408 | "step": "", 409 | "vrml": "" 410 | }, 411 | "page_layout_descr_file": "" 412 | }, 413 | "schematic": { 414 | "annotate_start_num": 0, 415 | "drawing": { 416 | "default_line_thickness": 6.0, 417 | "default_text_size": 50.0, 418 | "field_names": [], 419 | "intersheets_ref_own_page": false, 420 | "intersheets_ref_prefix": "", 421 | "intersheets_ref_short": false, 422 | "intersheets_ref_show": false, 423 | "intersheets_ref_suffix": "", 424 | "junction_size_choice": 3, 425 | "label_size_ratio": 0.375, 426 | "pin_symbol_size": 25.0, 427 | "text_offset_ratio": 0.15 428 | }, 429 | "legacy_lib_dir": "", 430 | "legacy_lib_list": [], 431 | "meta": { 432 | "version": 1 433 | }, 434 | "net_format_name": "", 435 | "ngspice": { 436 | "fix_include_paths": true, 437 | "fix_passive_vals": false, 438 | "meta": { 439 | "version": 0 440 | }, 441 | "model_mode": 0, 442 | "workbook_filename": "" 443 | }, 444 | "page_layout_descr_file": "", 445 | "plot_directory": "", 446 | "spice_adjust_passive_values": false, 447 | "spice_external_command": "spice \"%I\"", 448 | "subpart_first_id": 65, 449 | "subpart_id_separator": 0 450 | }, 451 | "sheets": [ 452 | [ 453 | "e63e39d7-6ac0-4ffd-8aa3-1841a4541b55", 454 | "" 455 | ], 456 | [ 457 | "049cfd7b-f956-4ae0-945f-6f6476ef0a46", 458 | "PERIPHERALS" 459 | ] 460 | ], 461 | "text_variables": {} 462 | } 463 | -------------------------------------------------------------------------------- /Heatplate_v1.0/Heatplate_v1.0.net: -------------------------------------------------------------------------------- 1 | (export (version "E") 2 | (design 3 | (source "C:\\Users\\Merlin\\Documents\\KiCad\\GitHub\\Solder-Reflow-Plate-main\\Kicad\\Original Version\\Original V3 - 70 50mm_Release_2022-03-04.kicad_sch") 4 | (date "3/9/2022 3:16:55 PM") 5 | (tool "Eeschema (6.0.1)") 6 | (sheet (number "1") (name "/") (tstamps "/") 7 | (title_block 8 | (title) 9 | (company) 10 | (rev) 11 | (date) 12 | (source "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch") 13 | (comment (number "1") (value "")) 14 | (comment (number "2") (value "")) 15 | (comment (number "3") (value "")) 16 | (comment (number "4") (value "")) 17 | (comment (number "5") (value "")) 18 | (comment (number "6") (value "")) 19 | (comment (number "7") (value "")) 20 | (comment (number "8") (value "")) 21 | (comment (number "9") (value ""))))) 22 | (components 23 | (comp (ref "C1") 24 | (value "100nF") 25 | (footprint "Capacitor_SMD:C_0603_1608Metric") 26 | (datasheet "~") 27 | (libsource (lib "Device") (part "C_Small") (description "Unpolarized capacitor, small symbol")) 28 | (property (name "Sheetname") (value "")) 29 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 30 | (sheetpath (names "/") (tstamps "/")) 31 | (tstamps "8a543a22-3f2e-4ef6-80ac-9ac69fbe4cf3")) 32 | (comp (ref "C2") 33 | (value "100uF") 34 | (datasheet "~") 35 | (libsource (lib "Device") (part "C_Polarized") (description "Polarized capacitor")) 36 | (property (name "Sheetname") (value "")) 37 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 38 | (sheetpath (names "/") (tstamps "/")) 39 | (tstamps "9b8a3f3b-1d5f-41e8-8e66-8a5056a0d9ec")) 40 | (comp (ref "C3") 41 | (value "22uF") 42 | (footprint "Capacitor_SMD:C_0805_2012Metric") 43 | (datasheet "~") 44 | (libsource (lib "Device") (part "C_Small") (description "Unpolarized capacitor, small symbol")) 45 | (property (name "Sheetname") (value "")) 46 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 47 | (sheetpath (names "/") (tstamps "/")) 48 | (tstamps "dc6ab44e-7bc8-415e-b086-adad4bb51674")) 49 | (comp (ref "C4") 50 | (value "100nF") 51 | (footprint "Capacitor_SMD:C_0603_1608Metric") 52 | (datasheet "~") 53 | (libsource (lib "Device") (part "C_Small") (description "Unpolarized capacitor, small symbol")) 54 | (property (name "Sheetname") (value "")) 55 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 56 | (sheetpath (names "/") (tstamps "/")) 57 | (tstamps "7b86bf56-1cfb-44a3-8bf8-6cc9b6893a0c")) 58 | (comp (ref "C5") 59 | (value "22uF") 60 | (footprint "Capacitor_SMD:C_0805_2012Metric") 61 | (datasheet "~") 62 | (libsource (lib "Device") (part "C_Small") (description "Unpolarized capacitor, small symbol")) 63 | (property (name "Sheetname") (value "")) 64 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 65 | (sheetpath (names "/") (tstamps "/")) 66 | (tstamps "a6aaa8c2-85aa-404f-9bd4-0ada02f358d5")) 67 | (comp (ref "C6") 68 | (value "100nF") 69 | (footprint "Capacitor_SMD:C_0603_1608Metric") 70 | (datasheet "~") 71 | (libsource (lib "Device") (part "C_Small") (description "Unpolarized capacitor, small symbol")) 72 | (property (name "Sheetname") (value "")) 73 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 74 | (sheetpath (names "/") (tstamps "/")) 75 | (tstamps "39fb441c-6279-4608-897d-9aff14221855")) 76 | (comp (ref "C7") 77 | (value "1nF") 78 | (footprint "Capacitor_SMD:C_0805_2012Metric") 79 | (datasheet "~") 80 | (libsource (lib "Device") (part "C_Small") (description "Unpolarized capacitor, small symbol")) 81 | (property (name "Sheetname") (value "")) 82 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 83 | (sheetpath (names "/") (tstamps "/")) 84 | (tstamps "d09dc482-f964-4132-80d1-ca5a924d088e")) 85 | (comp (ref "C8") 86 | (value "10nF") 87 | (footprint "Capacitor_SMD:C_0402_1005Metric") 88 | (datasheet "~") 89 | (libsource (lib "Device") (part "C_Small") (description "Unpolarized capacitor, small symbol")) 90 | (property (name "Sheetname") (value "")) 91 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 92 | (sheetpath (names "/") (tstamps "/")) 93 | (tstamps "75b50929-8934-44da-a8e3-152488d2dccc")) 94 | (comp (ref "C9") 95 | (value "100nF") 96 | (footprint "Capacitor_SMD:C_0603_1608Metric") 97 | (datasheet "~") 98 | (libsource (lib "Device") (part "C_Small") (description "Unpolarized capacitor, small symbol")) 99 | (property (name "Sheetname") (value "")) 100 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 101 | (sheetpath (names "/") (tstamps "/")) 102 | (tstamps "d50ebe24-1e09-4dab-a960-cc02295a3566")) 103 | (comp (ref "C10") 104 | (value "10nF") 105 | (footprint "Capacitor_SMD:C_0402_1005Metric") 106 | (datasheet "~") 107 | (libsource (lib "Device") (part "C_Small") (description "Unpolarized capacitor, small symbol")) 108 | (property (name "Sheetname") (value "")) 109 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 110 | (sheetpath (names "/") (tstamps "/")) 111 | (tstamps "1d357822-6e04-45da-93ee-8489926fbc2e")) 112 | (comp (ref "C12") 113 | (value "10nF") 114 | (footprint "Capacitor_SMD:C_0402_1005Metric") 115 | (datasheet "~") 116 | (libsource (lib "Device") (part "C_Small") (description "Unpolarized capacitor, small symbol")) 117 | (property (name "Sheetname") (value "")) 118 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 119 | (sheetpath (names "/") (tstamps "/")) 120 | (tstamps "363f0ac0-52f2-415a-ad61-24a8116beb65")) 121 | (comp (ref "C13") 122 | (value "10nF") 123 | (footprint "Capacitor_SMD:C_0402_1005Metric") 124 | (datasheet "~") 125 | (libsource (lib "Device") (part "C_Small") (description "Unpolarized capacitor, small symbol")) 126 | (property (name "Sheetname") (value "")) 127 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 128 | (sheetpath (names "/") (tstamps "/")) 129 | (tstamps "1f55dd3e-f947-4a8f-a03e-911f271aeb76")) 130 | (comp (ref "C14") 131 | (value "10nF") 132 | (footprint "Capacitor_SMD:C_0402_1005Metric") 133 | (datasheet "~") 134 | (libsource (lib "Device") (part "C_Small") (description "Unpolarized capacitor, small symbol")) 135 | (property (name "Sheetname") (value "")) 136 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 137 | (sheetpath (names "/") (tstamps "/")) 138 | (tstamps "be03984a-69ac-4e21-b2bb-218f81959184")) 139 | (comp (ref "C19") 140 | (value "100nF") 141 | (footprint "Capacitor_SMD:C_0603_1608Metric") 142 | (datasheet "~") 143 | (libsource (lib "Device") (part "C_Small") (description "Unpolarized capacitor, small symbol")) 144 | (property (name "Sheetname") (value "")) 145 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 146 | (sheetpath (names "/") (tstamps "/")) 147 | (tstamps "5822ca97-14ff-454f-997d-a9387e86a1c7")) 148 | (comp (ref "D1") 149 | (value "PESDMC2FD18VB") 150 | (datasheet "https://www.onsemi.com/pub/Collateral/ESD9B-D.PDF") 151 | (libsource (lib "Diode") (part "ESD9B3.3ST5G") (description "ESD protection diode, 3.3Vrwm, SOD-923")) 152 | (property (name "Sheetname") (value "")) 153 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 154 | (sheetpath (names "/") (tstamps "/")) 155 | (tstamps "0de4ee0f-bdc4-4a42-811d-d35d416be6d3")) 156 | (comp (ref "D2") 157 | (value "MM1Z24") 158 | (footprint "Diode_SMD:D_SOD-123") 159 | (datasheet "~") 160 | (libsource (lib "Device") (part "D_Zener") (description "Zener diode")) 161 | (property (name "Sheetname") (value "")) 162 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 163 | (sheetpath (names "/") (tstamps "/")) 164 | (tstamps "b2db9e19-9b16-47ee-932b-587d41810865")) 165 | (comp (ref "D3") 166 | (value "DSR1M") 167 | (footprint "Diode_SMD:D_SOD-123") 168 | (datasheet "~") 169 | (libsource (lib "Device") (part "D_Small") (description "Diode, small symbol")) 170 | (property (name "Sheetname") (value "")) 171 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 172 | (sheetpath (names "/") (tstamps "/")) 173 | (tstamps "cca18124-3066-4a02-a2fb-f018e68fe055")) 174 | (comp (ref "IC2") 175 | (value "ATMEGA4809-AFR") 176 | (footprint "Merlin:ATMEGA4809-AFR_QFP50P900X900X120-48N") 177 | (datasheet "https://datasheet.datasheetarchive.com/originals/distributors/DKDS41/DSANUWW0026740.pdf") 178 | (fields 179 | (field (name "Arrow Part Number") "ATMEGA4809-AFR") 180 | (field (name "Arrow Price/Stock") "https://www.arrow.com/en/products/atmega4809-afr/microchip-technology?region=nac") 181 | (field (name "Description") "MICROCHIP - ATMEGA4809-AFR - MCU, 8BIT, ATMEGA, 20MHZ, TQFP-48") 182 | (field (name "Height") "1.2") 183 | (field (name "Manufacturer_Name") "Microchip") 184 | (field (name "Manufacturer_Part_Number") "ATMEGA4809-AFR") 185 | (field (name "Mouser Part Number") "556-ATMEGA4809-AFR") 186 | (field (name "Mouser Price/Stock") "https://www.mouser.co.uk/ProductDetail/Microchip-Technology-Atmel/ATMEGA4809-AFR?qs=lYGu3FyN48fyX8GjkvcD6Q%3D%3D")) 187 | (libsource (lib "ATMEGA4809-AFR") (part "ATMEGA4809-AFR") (description "MICROCHIP - ATMEGA4809-AFR - MCU, 8BIT, ATMEGA, 20MHZ, TQFP-48")) 188 | (property (name "Description") (value "MICROCHIP - ATMEGA4809-AFR - MCU, 8BIT, ATMEGA, 20MHZ, TQFP-48")) 189 | (property (name "Height") (value "1.2")) 190 | (property (name "Manufacturer_Name") (value "Microchip")) 191 | (property (name "Manufacturer_Part_Number") (value "ATMEGA4809-AFR")) 192 | (property (name "Mouser Part Number") (value "556-ATMEGA4809-AFR")) 193 | (property (name "Mouser Price/Stock") (value "https://www.mouser.co.uk/ProductDetail/Microchip-Technology-Atmel/ATMEGA4809-AFR?qs=lYGu3FyN48fyX8GjkvcD6Q%3D%3D")) 194 | (property (name "Arrow Part Number") (value "ATMEGA4809-AFR")) 195 | (property (name "Arrow Price/Stock") (value "https://www.arrow.com/en/products/atmega4809-afr/microchip-technology?region=nac")) 196 | (property (name "Sheetname") (value "")) 197 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 198 | (sheetpath (names "/") (tstamps "/")) 199 | (tstamps "c5c6e125-3384-43c2-b0f0-3171448b2005")) 200 | (comp (ref "J1") 201 | (value "PJ-037A") 202 | (footprint "Merlin:PJ-037A") 203 | (fields 204 | (field (name "MANUFACTURER") "CUI INC") 205 | (field (name "STANDARD") "Manufacturer recommendations")) 206 | (libsource (lib "PJ-037A") (part "PJ-037A") (description "")) 207 | (property (name "MANUFACTURER") (value "CUI INC")) 208 | (property (name "STANDARD") (value "Manufacturer recommendations")) 209 | (property (name "Sheetname") (value "")) 210 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 211 | (sheetpath (names "/") (tstamps "/")) 212 | (tstamps "81aca804-0ab2-4ab6-a77d-a70cde13c06d")) 213 | (comp (ref "J2") 214 | (value "XT30PW-M") 215 | (footprint "Merlin:XT30PW-M") 216 | (fields 217 | (field (name "MANUFACTURER") "AMASS") 218 | (field (name "MAXIMUM_PACKAGE_HEIGHT") "8.4 mm") 219 | (field (name "PARTREV") "V1.2") 220 | (field (name "STANDARD") "Manufacturer recommendations")) 221 | (libsource (lib "XT60") (part "XT60PW-M") (description "")) 222 | (property (name "MAXIMUM_PACKAGE_HEIGHT") (value "8.4 mm")) 223 | (property (name "MANUFACTURER") (value "AMASS")) 224 | (property (name "PARTREV") (value "V1.2")) 225 | (property (name "STANDARD") (value "Manufacturer recommendations")) 226 | (property (name "Sheetname") (value "")) 227 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 228 | (sheetpath (names "/") (tstamps "/")) 229 | (tstamps "1090d9ae-1f9b-41b8-ae5f-6440f85dd625")) 230 | (comp (ref "J3") 231 | (value "XT30PW-F") 232 | (footprint "Merlin:XT30PW-F") 233 | (fields 234 | (field (name "MANUFACTURER") "AMASS") 235 | (field (name "MAXIMUM_PACKAGE_HEIGHT") "8.4 mm") 236 | (field (name "PARTREV") "V1.2") 237 | (field (name "STANDARD") "Manufacturer Recommendations")) 238 | (libsource (lib "XT60_Female") (part "XT60PW-F") (description "")) 239 | (property (name "MANUFACTURER") (value "AMASS")) 240 | (property (name "MAXIMUM_PACKAGE_HEIGHT") (value "8.4 mm")) 241 | (property (name "PARTREV") (value "V1.2")) 242 | (property (name "STANDARD") (value "Manufacturer Recommendations")) 243 | (property (name "Sheetname") (value "")) 244 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 245 | (sheetpath (names "/") (tstamps "/")) 246 | (tstamps "9e33a092-caae-4294-9a85-7b88d4962a54")) 247 | (comp (ref "MOSFET1") 248 | (value " ") 249 | (footprint "Package_TO_SOT_SMD:TO-252-2") 250 | (datasheet "http://www.diodes.com/assets/Datasheets/ZXMN3B01F.pdf") 251 | (libsource (lib "Transistor_FET") (part "ZXMN3B01F") (description "2A Id, 30V Vds, N-Channel MOSFET, SOT-23")) 252 | (property (name "Sheetname") (value "")) 253 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 254 | (sheetpath (names "/") (tstamps "/")) 255 | (tstamps "f4dff3fe-d4a7-43b3-a15c-383fce74d02c")) 256 | (comp (ref "OLED1") 257 | (value "SSD1306") 258 | (footprint "Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical") 259 | (libsource (lib "OLED SSD1306") (part "SSD1306") (description "SSD1306 OLED")) 260 | (property (name "Sheetname") (value "")) 261 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 262 | (sheetpath (names "/") (tstamps "/")) 263 | (tstamps "d03a1ad8-8895-479b-b962-8b35fa9c11ec")) 264 | (comp (ref "R1") 265 | (value "3k") 266 | (footprint "Resistor_SMD:R_0805_2012Metric") 267 | (datasheet "~") 268 | (libsource (lib "Device") (part "R_Small_US") (description "Resistor, small US symbol")) 269 | (property (name "Sheetname") (value "")) 270 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 271 | (sheetpath (names "/") (tstamps "/")) 272 | (tstamps "3284dcf0-6040-4022-ba5e-28f440433d63")) 273 | (comp (ref "R2") 274 | (value "1k") 275 | (footprint "Resistor_SMD:R_0805_2012Metric") 276 | (datasheet "~") 277 | (libsource (lib "Device") (part "R_Small_US") (description "Resistor, small US symbol")) 278 | (property (name "Sheetname") (value "")) 279 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 280 | (sheetpath (names "/") (tstamps "/")) 281 | (tstamps "bf31af8d-bc5a-4285-b8eb-fa758d4cbfd5")) 282 | (comp (ref "R4") 283 | (value "10K") 284 | (footprint "Resistor_SMD:R_0603_1608Metric") 285 | (datasheet "~") 286 | (libsource (lib "Device") (part "R_Small_US") (description "Resistor, small US symbol")) 287 | (property (name "Sheetname") (value "")) 288 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 289 | (sheetpath (names "/") (tstamps "/")) 290 | (tstamps "b35b5c60-686b-4003-a71f-2c90bae35525")) 291 | (comp (ref "R5") 292 | (value "10K") 293 | (footprint "Resistor_SMD:R_0603_1608Metric") 294 | (datasheet "~") 295 | (libsource (lib "Device") (part "R_Small_US") (description "Resistor, small US symbol")) 296 | (property (name "Sheetname") (value "")) 297 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 298 | (sheetpath (names "/") (tstamps "/")) 299 | (tstamps "3976ac40-fa7f-47be-b7ae-601607bec425")) 300 | (comp (ref "R6") 301 | (value "10K") 302 | (footprint "Resistor_SMD:R_0603_1608Metric") 303 | (datasheet "~") 304 | (libsource (lib "Device") (part "R_Small_US") (description "Resistor, small US symbol")) 305 | (property (name "Sheetname") (value "")) 306 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 307 | (sheetpath (names "/") (tstamps "/")) 308 | (tstamps "45c36899-6fd6-4650-adcb-a1f811fa3f0a")) 309 | (comp (ref "SW1") 310 | (value " ") 311 | (datasheet "~") 312 | (libsource (lib "Switch") (part "SW_Push") (description "Push button switch, generic, two pins")) 313 | (property (name "Sheetname") (value "")) 314 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 315 | (sheetpath (names "/") (tstamps "/")) 316 | (tstamps "9824ed65-9957-433f-b7c7-80916fd2f1a7")) 317 | (comp (ref "SW2") 318 | (value " ") 319 | (datasheet "~") 320 | (libsource (lib "Switch") (part "SW_Push") (description "Push button switch, generic, two pins")) 321 | (property (name "Sheetname") (value "")) 322 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 323 | (sheetpath (names "/") (tstamps "/")) 324 | (tstamps "25fcf6f7-0bfe-403f-a2d1-672a1d03dab8")) 325 | (comp (ref "TEMP1") 326 | (value "LMT85LP") 327 | (footprint "Package_TO_SOT_THT:TO-92_HandSolder") 328 | (datasheet "~") 329 | (libsource (lib "Connector_Generic") (part "Conn_01x03") (description "Generic connector, single row, 01x03, script generated (kicad-library-utils/schlib/autogen/connector/)")) 330 | (property (name "Sheetname") (value "")) 331 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 332 | (sheetpath (names "/") (tstamps "/")) 333 | (tstamps "bbe887ae-b60f-4c17-8405-434cc7e8f4c6")) 334 | (comp (ref "U1") 335 | (value "AMS1117-5.0") 336 | (footprint "Package_TO_SOT_SMD:SOT-89-3") 337 | (datasheet "http://www.advanced-monolithic.com/pdf/ds1117.pdf") 338 | (libsource (lib "Regulator_Linear") (part "AMS1117-5.0") (description "1A Low Dropout regulator, positive, 5.0V fixed output, SOT-223")) 339 | (property (name "Sheetname") (value "")) 340 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 341 | (sheetpath (names "/") (tstamps "/")) 342 | (tstamps "335f4d8c-c32f-4956-8614-15a2742272b4")) 343 | (comp (ref "UPDI1") 344 | (value " ") 345 | (footprint "Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical") 346 | (datasheet "~") 347 | (libsource (lib "Connector_Generic") (part "Conn_01x03") (description "Generic connector, single row, 01x03, script generated (kicad-library-utils/schlib/autogen/connector/)")) 348 | (property (name "Sheetname") (value "")) 349 | (property (name "Sheetfile") (value "Original V3 - 70 50mm_Release_2022-03-04.kicad_sch")) 350 | (sheetpath (names "/") (tstamps "/")) 351 | (tstamps "6c2d4dca-c0c7-4da9-9e99-ed14ed33d3cd"))) 352 | (libparts 353 | (libpart (lib "ATMEGA4809-AFR") (part "ATMEGA4809-AFR") 354 | (description "MICROCHIP - ATMEGA4809-AFR - MCU, 8BIT, ATMEGA, 20MHZ, TQFP-48") 355 | (docs "https://datasheet.datasheetarchive.com/originals/distributors/DKDS41/DSANUWW0026740.pdf") 356 | (fields 357 | (field (name "Reference") "IC") 358 | (field (name "Value") "ATMEGA4809-AFR") 359 | (field (name "Footprint") "QFP50P900X900X120-48N") 360 | (field (name "Datasheet") "https://datasheet.datasheetarchive.com/originals/distributors/DKDS41/DSANUWW0026740.pdf") 361 | (field (name "Description") "MICROCHIP - ATMEGA4809-AFR - MCU, 8BIT, ATMEGA, 20MHZ, TQFP-48") 362 | (field (name "Height") "1.2") 363 | (field (name "Manufacturer_Name") "Microchip") 364 | (field (name "Manufacturer_Part_Number") "ATMEGA4809-AFR") 365 | (field (name "Mouser Part Number") "556-ATMEGA4809-AFR") 366 | (field (name "Mouser Price/Stock") "https://www.mouser.co.uk/ProductDetail/Microchip-Technology-Atmel/ATMEGA4809-AFR?qs=lYGu3FyN48fyX8GjkvcD6Q%3D%3D") 367 | (field (name "Arrow Part Number") "ATMEGA4809-AFR") 368 | (field (name "Arrow Price/Stock") "https://www.arrow.com/en/products/atmega4809-afr/microchip-technology?region=nac")) 369 | (pins 370 | (pin (num "1") (name "PA5") (type "passive")) 371 | (pin (num "2") (name "PA6") (type "passive")) 372 | (pin (num "3") (name "PA7") (type "passive")) 373 | (pin (num "4") (name "PB0") (type "passive")) 374 | (pin (num "5") (name "PB1") (type "passive")) 375 | (pin (num "6") (name "PB2") (type "passive")) 376 | (pin (num "7") (name "PB3") (type "passive")) 377 | (pin (num "8") (name "PB4") (type "passive")) 378 | (pin (num "9") (name "PB5") (type "passive")) 379 | (pin (num "10") (name "PC0") (type "passive")) 380 | (pin (num "11") (name "PC1") (type "passive")) 381 | (pin (num "12") (name "PC2") (type "passive")) 382 | (pin (num "13") (name "PC3") (type "passive")) 383 | (pin (num "14") (name "VDD_1") (type "passive")) 384 | (pin (num "15") (name "GND_1") (type "passive")) 385 | (pin (num "16") (name "PC4") (type "passive")) 386 | (pin (num "17") (name "PC5") (type "passive")) 387 | (pin (num "18") (name "PC6") (type "passive")) 388 | (pin (num "19") (name "PC7") (type "passive")) 389 | (pin (num "20") (name "PD0") (type "passive")) 390 | (pin (num "21") (name "PD1") (type "passive")) 391 | (pin (num "22") (name "PD2") (type "passive")) 392 | (pin (num "23") (name "PD3") (type "passive")) 393 | (pin (num "24") (name "PD4") (type "passive")) 394 | (pin (num "25") (name "PD5") (type "passive")) 395 | (pin (num "26") (name "PD6") (type "passive")) 396 | (pin (num "27") (name "PD7") (type "passive")) 397 | (pin (num "28") (name "AVDD") (type "passive")) 398 | (pin (num "29") (name "GND_2") (type "passive")) 399 | (pin (num "30") (name "PE0") (type "passive")) 400 | (pin (num "31") (name "PE1") (type "passive")) 401 | (pin (num "32") (name "PE2") (type "passive")) 402 | (pin (num "33") (name "PE3") (type "passive")) 403 | (pin (num "34") (name "PF0_(TOSC1)") (type "passive")) 404 | (pin (num "35") (name "PF1_(TOSC2)") (type "passive")) 405 | (pin (num "36") (name "PF2") (type "passive")) 406 | (pin (num "37") (name "PF3") (type "passive")) 407 | (pin (num "38") (name "PF4") (type "passive")) 408 | (pin (num "39") (name "PF5") (type "passive")) 409 | (pin (num "40") (name "PF6") (type "passive")) 410 | (pin (num "41") (name "UPDI") (type "passive")) 411 | (pin (num "42") (name "VDD_2") (type "passive")) 412 | (pin (num "43") (name "GND_3") (type "passive")) 413 | (pin (num "44") (name "PA0_(EXTCLK)") (type "passive")) 414 | (pin (num "45") (name "PA1") (type "passive")) 415 | (pin (num "46") (name "PA2") (type "passive")) 416 | (pin (num "47") (name "PA3") (type "passive")) 417 | (pin (num "48") (name "PA4") (type "passive")))) 418 | (libpart (lib "Connector_Generic") (part "Conn_01x03") 419 | (description "Generic connector, single row, 01x03, script generated (kicad-library-utils/schlib/autogen/connector/)") 420 | (docs "~") 421 | (footprints 422 | (fp "Connector*:*_1x??_*")) 423 | (fields 424 | (field (name "Reference") "J") 425 | (field (name "Value") "Conn_01x03") 426 | (field (name "Datasheet") "~")) 427 | (pins 428 | (pin (num "1") (name "Pin_1") (type "passive")) 429 | (pin (num "2") (name "Pin_2") (type "passive")) 430 | (pin (num "3") (name "Pin_3") (type "passive")))) 431 | (libpart (lib "Device") (part "C_Polarized") 432 | (description "Polarized capacitor") 433 | (docs "~") 434 | (footprints 435 | (fp "CP_*")) 436 | (fields 437 | (field (name "Reference") "C") 438 | (field (name "Value") "C_Polarized") 439 | (field (name "Datasheet") "~")) 440 | (pins 441 | (pin (num "1") (name "") (type "passive")) 442 | (pin (num "2") (name "") (type "passive")))) 443 | (libpart (lib "Device") (part "C_Small") 444 | (description "Unpolarized capacitor, small symbol") 445 | (docs "~") 446 | (footprints 447 | (fp "C_*")) 448 | (fields 449 | (field (name "Reference") "C") 450 | (field (name "Value") "C_Small") 451 | (field (name "Datasheet") "~")) 452 | (pins 453 | (pin (num "1") (name "") (type "passive")) 454 | (pin (num "2") (name "") (type "passive")))) 455 | (libpart (lib "Device") (part "D_Small") 456 | (description "Diode, small symbol") 457 | (docs "~") 458 | (footprints 459 | (fp "TO-???*") 460 | (fp "*_Diode_*") 461 | (fp "*SingleDiode*") 462 | (fp "D_*")) 463 | (fields 464 | (field (name "Reference") "D") 465 | (field (name "Value") "D_Small") 466 | (field (name "Datasheet") "~")) 467 | (pins 468 | (pin (num "1") (name "K") (type "passive")) 469 | (pin (num "2") (name "A") (type "passive")))) 470 | (libpart (lib "Device") (part "D_Zener") 471 | (description "Zener diode") 472 | (docs "~") 473 | (footprints 474 | (fp "TO-???*") 475 | (fp "*_Diode_*") 476 | (fp "*SingleDiode*") 477 | (fp "D_*")) 478 | (fields 479 | (field (name "Reference") "D") 480 | (field (name "Value") "D_Zener") 481 | (field (name "Datasheet") "~")) 482 | (pins 483 | (pin (num "1") (name "K") (type "passive")) 484 | (pin (num "2") (name "A") (type "passive")))) 485 | (libpart (lib "Device") (part "R_Small_US") 486 | (description "Resistor, small US symbol") 487 | (docs "~") 488 | (footprints 489 | (fp "R_*")) 490 | (fields 491 | (field (name "Reference") "R") 492 | (field (name "Value") "R_Small_US") 493 | (field (name "Datasheet") "~")) 494 | (pins 495 | (pin (num "1") (name "") (type "passive")) 496 | (pin (num "2") (name "") (type "passive")))) 497 | (libpart (lib "Diode") (part "ESD9B3.3ST5G") 498 | (description "ESD protection diode, 3.3Vrwm, SOD-923") 499 | (docs "https://www.onsemi.com/pub/Collateral/ESD9B-D.PDF") 500 | (footprints 501 | (fp "D*SOD?923*")) 502 | (fields 503 | (field (name "Reference") "D") 504 | (field (name "Value") "ESD9B3.3ST5G") 505 | (field (name "Footprint") "Diode_SMD:D_SOD-923") 506 | (field (name "Datasheet") "https://www.onsemi.com/pub/Collateral/ESD9B-D.PDF")) 507 | (pins 508 | (pin (num "1") (name "A1") (type "passive")) 509 | (pin (num "2") (name "A2") (type "passive")))) 510 | (libpart (lib "OLED SSD1306") (part "SSD1306") 511 | (description "SSD1306 OLED") 512 | (footprints 513 | (fp "SSD1306-128x64_OLED:SSD1306")) 514 | (fields 515 | (field (name "Reference") "Brd") 516 | (field (name "Value") "SSD1306")) 517 | (pins 518 | (pin (num "1") (name "GND") (type "input")) 519 | (pin (num "2") (name "VCC") (type "input")) 520 | (pin (num "3") (name "SCL") (type "input")) 521 | (pin (num "4") (name "SDA") (type "input")))) 522 | (libpart (lib "PJ-037A") (part "PJ-037A") 523 | (fields 524 | (field (name "Reference") "J") 525 | (field (name "Value") "PJ-037A") 526 | (field (name "Footprint") "CUI_PJ-037A") 527 | (field (name "MANUFACTURER") "CUI INC") 528 | (field (name "STANDARD") "Manufacturer recommendations")) 529 | (pins 530 | (pin (num "1") (name "") (type "passive")) 531 | (pin (num "2") (name "") (type "passive")))) 532 | (libpart (lib "Regulator_Linear") (part "AMS1117-5.0") 533 | (description "1A Low Dropout regulator, positive, 5.0V fixed output, SOT-223") 534 | (docs "http://www.advanced-monolithic.com/pdf/ds1117.pdf") 535 | (footprints 536 | (fp "SOT?223*TabPin2*")) 537 | (fields 538 | (field (name "Reference") "U") 539 | (field (name "Value") "AMS1117-5.0") 540 | (field (name "Footprint") "Package_TO_SOT_SMD:SOT-223-3_TabPin2") 541 | (field (name "Datasheet") "http://www.advanced-monolithic.com/pdf/ds1117.pdf")) 542 | (pins 543 | (pin (num "1") (name "GND") (type "power_in")) 544 | (pin (num "2") (name "VO") (type "power_out")) 545 | (pin (num "3") (name "VI") (type "power_in")))) 546 | (libpart (lib "Switch") (part "SW_Push") 547 | (description "Push button switch, generic, two pins") 548 | (docs "~") 549 | (fields 550 | (field (name "Reference") "SW") 551 | (field (name "Value") "SW_Push") 552 | (field (name "Datasheet") "~")) 553 | (pins 554 | (pin (num "1") (name "1") (type "passive")) 555 | (pin (num "2") (name "2") (type "passive")))) 556 | (libpart (lib "Transistor_FET") (part "ZXMN3B01F") 557 | (description "2A Id, 30V Vds, N-Channel MOSFET, SOT-23") 558 | (docs "http://www.diodes.com/assets/Datasheets/ZXMN3B01F.pdf") 559 | (footprints 560 | (fp "SOT?23*")) 561 | (fields 562 | (field (name "Reference") "Q") 563 | (field (name "Value") "ZXMN3B01F") 564 | (field (name "Footprint") "Package_TO_SOT_SMD:SOT-23") 565 | (field (name "Datasheet") "http://www.diodes.com/assets/Datasheets/ZXMN3B01F.pdf")) 566 | (pins 567 | (pin (num "1") (name "G") (type "input")) 568 | (pin (num "2") (name "S") (type "passive")) 569 | (pin (num "3") (name "D") (type "passive")))) 570 | (libpart (lib "XT60") (part "XT60PW-M") 571 | (fields 572 | (field (name "Reference") "J") 573 | (field (name "Value") "XT60PW-M") 574 | (field (name "Footprint") "AMASS_XT60PW-M") 575 | (field (name "MAXIMUM_PACKAGE_HEIGHT") "8.4 mm") 576 | (field (name "MANUFACTURER") "AMASS") 577 | (field (name "PARTREV") "V1.2") 578 | (field (name "STANDARD") "Manufacturer recommendations")) 579 | (pins 580 | (pin (num "1") (name "+") (type "passive")) 581 | (pin (num "2") (name "-") (type "passive")))) 582 | (libpart (lib "XT60_Female") (part "XT60PW-F") 583 | (fields 584 | (field (name "Reference") "J") 585 | (field (name "Value") "XT60PW-F") 586 | (field (name "Footprint") "AMASS_XT60PW-F") 587 | (field (name "MANUFACTURER") "AMASS") 588 | (field (name "MAXIMUM_PACKAGE_HEIGHT") "8.4 mm") 589 | (field (name "PARTREV") "V1.2") 590 | (field (name "STANDARD") "Manufacturer Recommendations")) 591 | (pins 592 | (pin (num "1") (name "1") (type "passive")) 593 | (pin (num "2") (name "2") (type "passive")) 594 | (pin (num "S1") (name "SHIELD") (type "passive")) 595 | (pin (num "S2") (name "SHIELD") (type "passive"))))) 596 | (libraries 597 | (library (logical "ATMEGA4809-AFR") 598 | (uri "C:/Users/Merlin/Documents/KiCad/Library/LIB_ATMEGA4809-AFR/ATMEGA4809-AFR.lib")) 599 | (library (logical "Connector_Generic") 600 | (uri "C:\\Program Files\\KiCad\\6.0\\share\\kicad\\symbols\\/Connector_Generic.kicad_sym")) 601 | (library (logical "Device") 602 | (uri "C:\\Program Files\\KiCad\\6.0\\share\\kicad\\symbols\\/Device.kicad_sym")) 603 | (library (logical "Diode") 604 | (uri "C:\\Program Files\\KiCad\\6.0\\share\\kicad\\symbols\\/Diode.kicad_sym")) 605 | (library (logical "OLED SSD1306") 606 | (uri "C:/Users/Merlin/Documents/KiCad/Library/KiCad-SSD1306-128x64-master/library/SSD1306-128x64_OLED.lib")) 607 | (library (logical "PJ-037A") 608 | (uri "C:/Users/Merlin/Documents/KiCad/Library/PJ-037A/PJ-037A.lib")) 609 | (library (logical "Regulator_Linear") 610 | (uri "C:\\Program Files\\KiCad\\6.0\\share\\kicad\\symbols\\/Regulator_Linear.kicad_sym")) 611 | (library (logical "Switch") 612 | (uri "C:\\Program Files\\KiCad\\6.0\\share\\kicad\\symbols\\/Switch.kicad_sym")) 613 | (library (logical "Transistor_FET") 614 | (uri "C:\\Program Files\\KiCad\\6.0\\share\\kicad\\symbols\\/Transistor_FET.kicad_sym")) 615 | (library (logical "XT60") 616 | (uri "C:/Users/Merlin/Documents/KiCad/Library/USB C with PD/XT60PW-M/XT60PW-M.lib")) 617 | (library (logical "XT60_Female") 618 | (uri "C:/Users/Merlin/Documents/KiCad/Library/USB C with PD/XT60PW-F/XT60PW-F.lib"))) 619 | (nets 620 | (net (code "1") (name "+5V") 621 | (node (ref "C10") (pin "2") (pintype "passive")) 622 | (node (ref "C14") (pin "1") (pintype "passive")) 623 | (node (ref "C5") (pin "1") (pintype "passive")) 624 | (node (ref "C6") (pin "1") (pintype "passive")) 625 | (node (ref "C8") (pin "2") (pintype "passive")) 626 | (node (ref "C9") (pin "1") (pintype "passive")) 627 | (node (ref "IC2") (pin "14") (pinfunction "VDD_1") (pintype "passive")) 628 | (node (ref "IC2") (pin "28") (pinfunction "AVDD") (pintype "passive")) 629 | (node (ref "IC2") (pin "43") (pinfunction "GND_3") (pintype "passive")) 630 | (node (ref "OLED1") (pin "2") (pinfunction "VCC") (pintype "input")) 631 | (node (ref "SW1") (pin "1") (pinfunction "1") (pintype "passive")) 632 | (node (ref "SW2") (pin "1") (pinfunction "1") (pintype "passive")) 633 | (node (ref "TEMP1") (pin "1") (pinfunction "Pin_1") (pintype "passive")) 634 | (node (ref "U1") (pin "2") (pinfunction "VO") (pintype "power_out")) 635 | (node (ref "UPDI1") (pin "3") (pinfunction "Pin_3") (pintype "passive"))) 636 | (net (code "2") (name "DNSW") 637 | (node (ref "C12") (pin "2") (pintype "passive")) 638 | (node (ref "IC2") (pin "20") (pinfunction "PD0") (pintype "passive")) 639 | (node (ref "R5") (pin "2") (pintype "passive")) 640 | (node (ref "SW2") (pin "2") (pinfunction "2") (pintype "passive"))) 641 | (net (code "3") (name "GND") 642 | (node (ref "C1") (pin "1") (pintype "passive")) 643 | (node (ref "C10") (pin "1") (pintype "passive")) 644 | (node (ref "C12") (pin "1") (pintype "passive")) 645 | (node (ref "C13") (pin "1") (pintype "passive")) 646 | (node (ref "C14") (pin "2") (pintype "passive")) 647 | (node (ref "C19") (pin "2") (pintype "passive")) 648 | (node (ref "C2") (pin "2") (pintype "passive")) 649 | (node (ref "C3") (pin "2") (pintype "passive")) 650 | (node (ref "C4") (pin "2") (pintype "passive")) 651 | (node (ref "C5") (pin "2") (pintype "passive")) 652 | (node (ref "C6") (pin "2") (pintype "passive")) 653 | (node (ref "C8") (pin "1") (pintype "passive")) 654 | (node (ref "C9") (pin "2") (pintype "passive")) 655 | (node (ref "D1") (pin "1") (pinfunction "A1") (pintype "passive")) 656 | (node (ref "IC2") (pin "15") (pinfunction "GND_1") (pintype "passive")) 657 | (node (ref "IC2") (pin "29") (pinfunction "GND_2") (pintype "passive")) 658 | (node (ref "IC2") (pin "42") (pinfunction "VDD_2") (pintype "passive")) 659 | (node (ref "J1") (pin "2") (pintype "passive")) 660 | (node (ref "MOSFET1") (pin "3") (pinfunction "D") (pintype "passive")) 661 | (node (ref "OLED1") (pin "1") (pinfunction "GND") (pintype "input")) 662 | (node (ref "R2") (pin "1") (pintype "passive")) 663 | (node (ref "R4") (pin "1") (pintype "passive")) 664 | (node (ref "R5") (pin "1") (pintype "passive")) 665 | (node (ref "R6") (pin "1") (pintype "passive")) 666 | (node (ref "TEMP1") (pin "3") (pinfunction "Pin_3") (pintype "passive")) 667 | (node (ref "U1") (pin "1") (pinfunction "GND") (pintype "power_in")) 668 | (node (ref "UPDI1") (pin "1") (pinfunction "Pin_1") (pintype "passive"))) 669 | (net (code "4") (name "Gate") 670 | (node (ref "IC2") (pin "13") (pinfunction "PC3") (pintype "passive")) 671 | (node (ref "MOSFET1") (pin "1") (pinfunction "G") (pintype "input")) 672 | (node (ref "R4") (pin "2") (pintype "passive"))) 673 | (net (code "5") (name "Net-(C7-Pad1)") 674 | (node (ref "C7") (pin "1") (pintype "passive")) 675 | (node (ref "D2") (pin "2") (pinfunction "A") (pintype "passive")) 676 | (node (ref "J2") (pin "1") (pinfunction "+") (pintype "passive")) 677 | (node (ref "MOSFET1") (pin "2") (pinfunction "S") (pintype "passive"))) 678 | (net (code "6") (name "Net-(J3-PadS1)") 679 | (node (ref "J3") (pin "S1") (pinfunction "SHIELD") (pintype "passive")) 680 | (node (ref "J3") (pin "S2") (pinfunction "SHIELD") (pintype "passive"))) 681 | (net (code "7") (name "Plate") 682 | (node (ref "J3") (pin "1") (pinfunction "1") (pintype "passive")) 683 | (node (ref "J3") (pin "2") (pinfunction "2") (pintype "passive"))) 684 | (net (code "8") (name "SCKOLED") 685 | (node (ref "IC2") (pin "47") (pinfunction "PA3") (pintype "passive")) 686 | (node (ref "OLED1") (pin "3") (pinfunction "SCL") (pintype "input"))) 687 | (net (code "9") (name "SDAOLED") 688 | (node (ref "IC2") (pin "46") (pinfunction "PA2") (pintype "passive")) 689 | (node (ref "OLED1") (pin "4") (pinfunction "SDA") (pintype "input"))) 690 | (net (code "10") (name "TData") 691 | (node (ref "IC2") (pin "39") (pinfunction "PF5") (pintype "passive")) 692 | (node (ref "TEMP1") (pin "2") (pinfunction "Pin_2") (pintype "passive"))) 693 | (net (code "11") (name "UPDI") 694 | (node (ref "IC2") (pin "41") (pinfunction "UPDI") (pintype "passive")) 695 | (node (ref "UPDI1") (pin "2") (pinfunction "Pin_2") (pintype "passive"))) 696 | (net (code "12") (name "UPSW") 697 | (node (ref "C13") (pin "2") (pintype "passive")) 698 | (node (ref "IC2") (pin "21") (pinfunction "PD1") (pintype "passive")) 699 | (node (ref "R6") (pin "2") (pintype "passive")) 700 | (node (ref "SW1") (pin "2") (pinfunction "2") (pintype "passive"))) 701 | (net (code "13") (name "VBUS") 702 | (node (ref "C1") (pin "2") (pintype "passive")) 703 | (node (ref "C2") (pin "1") (pintype "passive")) 704 | (node (ref "C7") (pin "2") (pintype "passive")) 705 | (node (ref "D1") (pin "2") (pinfunction "A2") (pintype "passive")) 706 | (node (ref "D2") (pin "1") (pinfunction "K") (pintype "passive")) 707 | (node (ref "D3") (pin "1") (pinfunction "K") (pintype "passive")) 708 | (node (ref "J1") (pin "1") (pintype "passive")) 709 | (node (ref "J2") (pin "2") (pinfunction "-") (pintype "passive"))) 710 | (net (code "14") (name "VCC-Div") 711 | (node (ref "R1") (pin "2") (pintype "passive")) 712 | (node (ref "R2") (pin "2") (pintype "passive"))) 713 | (net (code "15") (name "VIN-D") 714 | (node (ref "C19") (pin "1") (pintype "passive")) 715 | (node (ref "C3") (pin "1") (pintype "passive")) 716 | (node (ref "C4") (pin "1") (pintype "passive")) 717 | (node (ref "D3") (pin "2") (pinfunction "A") (pintype "passive")) 718 | (node (ref "IC2") (pin "37") (pinfunction "PF3") (pintype "passive")) 719 | (node (ref "R1") (pin "1") (pintype "passive")) 720 | (node (ref "U1") (pin "3") (pinfunction "VI") (pintype "power_in"))) 721 | (net (code "16") (name "unconnected-(IC2-Pad1)") 722 | (node (ref "IC2") (pin "1") (pinfunction "PA5") (pintype "passive+no_connect"))) 723 | (net (code "17") (name "unconnected-(IC2-Pad2)") 724 | (node (ref "IC2") (pin "2") (pinfunction "PA6") (pintype "passive+no_connect"))) 725 | (net (code "18") (name "unconnected-(IC2-Pad3)") 726 | (node (ref "IC2") (pin "3") (pinfunction "PA7") (pintype "passive+no_connect"))) 727 | (net (code "19") (name "unconnected-(IC2-Pad4)") 728 | (node (ref "IC2") (pin "4") (pinfunction "PB0") (pintype "passive+no_connect"))) 729 | (net (code "20") (name "unconnected-(IC2-Pad5)") 730 | (node (ref "IC2") (pin "5") (pinfunction "PB1") (pintype "passive+no_connect"))) 731 | (net (code "21") (name "unconnected-(IC2-Pad6)") 732 | (node (ref "IC2") (pin "6") (pinfunction "PB2") (pintype "passive+no_connect"))) 733 | (net (code "22") (name "unconnected-(IC2-Pad7)") 734 | (node (ref "IC2") (pin "7") (pinfunction "PB3") (pintype "passive+no_connect"))) 735 | (net (code "23") (name "unconnected-(IC2-Pad8)") 736 | (node (ref "IC2") (pin "8") (pinfunction "PB4") (pintype "passive+no_connect"))) 737 | (net (code "24") (name "unconnected-(IC2-Pad9)") 738 | (node (ref "IC2") (pin "9") (pinfunction "PB5") (pintype "passive+no_connect"))) 739 | (net (code "25") (name "unconnected-(IC2-Pad10)") 740 | (node (ref "IC2") (pin "10") (pinfunction "PC0") (pintype "passive+no_connect"))) 741 | (net (code "26") (name "unconnected-(IC2-Pad11)") 742 | (node (ref "IC2") (pin "11") (pinfunction "PC1") (pintype "passive+no_connect"))) 743 | (net (code "27") (name "unconnected-(IC2-Pad12)") 744 | (node (ref "IC2") (pin "12") (pinfunction "PC2") (pintype "passive+no_connect"))) 745 | (net (code "28") (name "unconnected-(IC2-Pad16)") 746 | (node (ref "IC2") (pin "16") (pinfunction "PC4") (pintype "passive+no_connect"))) 747 | (net (code "29") (name "unconnected-(IC2-Pad17)") 748 | (node (ref "IC2") (pin "17") (pinfunction "PC5") (pintype "passive+no_connect"))) 749 | (net (code "30") (name "unconnected-(IC2-Pad18)") 750 | (node (ref "IC2") (pin "18") (pinfunction "PC6") (pintype "passive+no_connect"))) 751 | (net (code "31") (name "unconnected-(IC2-Pad19)") 752 | (node (ref "IC2") (pin "19") (pinfunction "PC7") (pintype "passive+no_connect"))) 753 | (net (code "32") (name "unconnected-(IC2-Pad22)") 754 | (node (ref "IC2") (pin "22") (pinfunction "PD2") (pintype "passive"))) 755 | (net (code "33") (name "unconnected-(IC2-Pad23)") 756 | (node (ref "IC2") (pin "23") (pinfunction "PD3") (pintype "passive"))) 757 | (net (code "34") (name "unconnected-(IC2-Pad24)") 758 | (node (ref "IC2") (pin "24") (pinfunction "PD4") (pintype "passive"))) 759 | (net (code "35") (name "unconnected-(IC2-Pad25)") 760 | (node (ref "IC2") (pin "25") (pinfunction "PD5") (pintype "passive+no_connect"))) 761 | (net (code "36") (name "unconnected-(IC2-Pad26)") 762 | (node (ref "IC2") (pin "26") (pinfunction "PD6") (pintype "passive+no_connect"))) 763 | (net (code "37") (name "unconnected-(IC2-Pad27)") 764 | (node (ref "IC2") (pin "27") (pinfunction "PD7") (pintype "passive+no_connect"))) 765 | (net (code "38") (name "unconnected-(IC2-Pad30)") 766 | (node (ref "IC2") (pin "30") (pinfunction "PE0") (pintype "passive+no_connect"))) 767 | (net (code "39") (name "unconnected-(IC2-Pad31)") 768 | (node (ref "IC2") (pin "31") (pinfunction "PE1") (pintype "passive+no_connect"))) 769 | (net (code "40") (name "unconnected-(IC2-Pad32)") 770 | (node (ref "IC2") (pin "32") (pinfunction "PE2") (pintype "passive+no_connect"))) 771 | (net (code "41") (name "unconnected-(IC2-Pad33)") 772 | (node (ref "IC2") (pin "33") (pinfunction "PE3") (pintype "passive+no_connect"))) 773 | (net (code "42") (name "unconnected-(IC2-Pad34)") 774 | (node (ref "IC2") (pin "34") (pinfunction "PF0_(TOSC1)") (pintype "passive+no_connect"))) 775 | (net (code "43") (name "unconnected-(IC2-Pad35)") 776 | (node (ref "IC2") (pin "35") (pinfunction "PF1_(TOSC2)") (pintype "passive+no_connect"))) 777 | (net (code "44") (name "unconnected-(IC2-Pad36)") 778 | (node (ref "IC2") (pin "36") (pinfunction "PF2") (pintype "passive+no_connect"))) 779 | (net (code "45") (name "unconnected-(IC2-Pad38)") 780 | (node (ref "IC2") (pin "38") (pinfunction "PF4") (pintype "passive+no_connect"))) 781 | (net (code "46") (name "unconnected-(IC2-Pad40)") 782 | (node (ref "IC2") (pin "40") (pinfunction "PF6") (pintype "passive+no_connect"))) 783 | (net (code "47") (name "unconnected-(IC2-Pad44)") 784 | (node (ref "IC2") (pin "44") (pinfunction "PA0_(EXTCLK)") (pintype "passive+no_connect"))) 785 | (net (code "48") (name "unconnected-(IC2-Pad45)") 786 | (node (ref "IC2") (pin "45") (pinfunction "PA1") (pintype "passive+no_connect"))) 787 | (net (code "49") (name "unconnected-(IC2-Pad48)") 788 | (node (ref "IC2") (pin "48") (pinfunction "PA4") (pintype "passive+no_connect"))))) -------------------------------------------------------------------------------- /Heatplate_v1.0/Heatplate_v1.0_Schematic.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerSpatz/PCB-reflow-solder-heat-plate/ff15e48973843f93387d2c7b3f7c796a69bbe7f2/Heatplate_v1.0/Heatplate_v1.0_Schematic.pdf -------------------------------------------------------------------------------- /Heatplate_v1.0/renders/Heatplate_v1.0_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerSpatz/PCB-reflow-solder-heat-plate/ff15e48973843f93387d2c7b3f7c796a69bbe7f2/Heatplate_v1.0/renders/Heatplate_v1.0_back.png -------------------------------------------------------------------------------- /Heatplate_v1.0/renders/Heatplate_v1.0_front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerSpatz/PCB-reflow-solder-heat-plate/ff15e48973843f93387d2c7b3f7c796a69bbe7f2/Heatplate_v1.0/renders/Heatplate_v1.0_front.png -------------------------------------------------------------------------------- /Heatplate_v1.1/Heatplate_v1.1.kicad_prl: -------------------------------------------------------------------------------- 1 | { 2 | "board": { 3 | "active_layer": 0, 4 | "active_layer_preset": "", 5 | "auto_track_width": true, 6 | "hidden_nets": [], 7 | "high_contrast_mode": 0, 8 | "net_color_mode": 1, 9 | "opacity": { 10 | "pads": 1.0, 11 | "tracks": 1.0, 12 | "vias": 1.0, 13 | "zones": 0.6 14 | }, 15 | "ratsnest_display_mode": 0, 16 | "selection_filter": { 17 | "dimensions": true, 18 | "footprints": true, 19 | "graphics": true, 20 | "keepouts": true, 21 | "lockedItems": true, 22 | "otherItems": true, 23 | "pads": true, 24 | "text": true, 25 | "tracks": true, 26 | "vias": true, 27 | "zones": true 28 | }, 29 | "visible_items": [ 30 | 0, 31 | 1, 32 | 2, 33 | 3, 34 | 4, 35 | 5, 36 | 8, 37 | 9, 38 | 10, 39 | 11, 40 | 12, 41 | 13, 42 | 14, 43 | 15, 44 | 16, 45 | 17, 46 | 18, 47 | 19, 48 | 20, 49 | 21, 50 | 22, 51 | 23, 52 | 24, 53 | 25, 54 | 26, 55 | 27, 56 | 28, 57 | 29, 58 | 30, 59 | 32, 60 | 33, 61 | 34, 62 | 35, 63 | 36 64 | ], 65 | "visible_layers": "002ffff_80000001", 66 | "zone_display_mode": 0 67 | }, 68 | "meta": { 69 | "filename": "Heatplate_v1.1.kicad_prl", 70 | "version": 3 71 | }, 72 | "project": { 73 | "files": [] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Heatplate_v1.1/Heatplate_v1.1.kicad_pro: -------------------------------------------------------------------------------- 1 | { 2 | "board": { 3 | "design_settings": { 4 | "defaults": { 5 | "board_outline_line_width": 0.049999999999999996, 6 | "copper_line_width": 0.19999999999999998, 7 | "copper_text_italic": false, 8 | "copper_text_size_h": 1.5, 9 | "copper_text_size_v": 1.5, 10 | "copper_text_thickness": 0.3, 11 | "copper_text_upright": false, 12 | "courtyard_line_width": 0.049999999999999996, 13 | "dimension_precision": 4, 14 | "dimension_units": 3, 15 | "dimensions": { 16 | "arrow_length": 1270000, 17 | "extension_offset": 500000, 18 | "keep_text_aligned": true, 19 | "suppress_zeroes": false, 20 | "text_position": 0, 21 | "units_format": 1 22 | }, 23 | "fab_line_width": 0.09999999999999999, 24 | "fab_text_italic": false, 25 | "fab_text_size_h": 1.0, 26 | "fab_text_size_v": 1.0, 27 | "fab_text_thickness": 0.15, 28 | "fab_text_upright": false, 29 | "other_line_width": 0.09999999999999999, 30 | "other_text_italic": false, 31 | "other_text_size_h": 1.0, 32 | "other_text_size_v": 1.0, 33 | "other_text_thickness": 0.15, 34 | "other_text_upright": false, 35 | "pads": { 36 | "drill": 0.0, 37 | "height": 1.45, 38 | "width": 1.0 39 | }, 40 | "silk_line_width": 0.12, 41 | "silk_text_italic": false, 42 | "silk_text_size_h": 1.0, 43 | "silk_text_size_v": 1.0, 44 | "silk_text_thickness": 0.15, 45 | "silk_text_upright": false, 46 | "zones": { 47 | "45_degree_only": false, 48 | "min_clearance": 0.15 49 | } 50 | }, 51 | "diff_pair_dimensions": [ 52 | { 53 | "gap": 0.0, 54 | "via_gap": 0.0, 55 | "width": 0.0 56 | } 57 | ], 58 | "drc_exclusions": [ 59 | "silk_over_copper|67107500|59950000|c801d42e-dd94-493e-bd2f-6c3ddad43f55|e4664c1b-a4cd-4eed-8c0f-efff7b6d7960", 60 | "silk_over_copper|67107500|60000000|c801d42e-dd94-493e-bd2f-6c3ddad43f55|03cb71dc-aa9f-4ef4-9b19-7d6989258e7e", 61 | "silk_over_copper|76307500|60000000|c801d42e-dd94-493e-bd2f-6c3ddad43f55|eae91cba-d07c-496a-a67b-cd0a2f6e08aa" 62 | ], 63 | "meta": { 64 | "version": 2 65 | }, 66 | "rule_severities": { 67 | "annular_width": "error", 68 | "clearance": "error", 69 | "copper_edge_clearance": "error", 70 | "courtyards_overlap": "error", 71 | "diff_pair_gap_out_of_range": "error", 72 | "diff_pair_uncoupled_length_too_long": "error", 73 | "drill_out_of_range": "error", 74 | "duplicate_footprints": "warning", 75 | "extra_footprint": "warning", 76 | "footprint_type_mismatch": "error", 77 | "hole_clearance": "error", 78 | "hole_near_hole": "error", 79 | "invalid_outline": "error", 80 | "item_on_disabled_layer": "error", 81 | "items_not_allowed": "error", 82 | "length_out_of_range": "error", 83 | "malformed_courtyard": "error", 84 | "microvia_drill_out_of_range": "error", 85 | "missing_courtyard": "ignore", 86 | "missing_footprint": "warning", 87 | "net_conflict": "warning", 88 | "npth_inside_courtyard": "ignore", 89 | "padstack": "error", 90 | "pth_inside_courtyard": "ignore", 91 | "shorting_items": "error", 92 | "silk_over_copper": "warning", 93 | "silk_overlap": "warning", 94 | "skew_out_of_range": "error", 95 | "through_hole_pad_without_hole": "error", 96 | "too_many_vias": "error", 97 | "track_dangling": "warning", 98 | "track_width": "error", 99 | "tracks_crossing": "error", 100 | "unconnected_items": "error", 101 | "unresolved_variable": "error", 102 | "via_dangling": "warning", 103 | "zone_has_empty_net": "error", 104 | "zones_intersect": "error" 105 | }, 106 | "rules": { 107 | "allow_blind_buried_vias": false, 108 | "allow_microvias": false, 109 | "max_error": 0.005, 110 | "min_clearance": 0.15, 111 | "min_copper_edge_clearance": 0.3, 112 | "min_hole_clearance": 0.25, 113 | "min_hole_to_hole": 0.25, 114 | "min_microvia_diameter": 0.19999999999999998, 115 | "min_microvia_drill": 0.09999999999999999, 116 | "min_silk_clearance": 0.0, 117 | "min_through_hole_diameter": 0.3, 118 | "min_track_width": 0.15, 119 | "min_via_annular_width": 0.15, 120 | "min_via_diameter": 0.6, 121 | "use_height_for_length_calcs": true 122 | }, 123 | "track_widths": [ 124 | 0.0, 125 | 0.3, 126 | 0.6, 127 | 1.25 128 | ], 129 | "via_dimensions": [ 130 | { 131 | "diameter": 0.0, 132 | "drill": 0.0 133 | } 134 | ], 135 | "zones_allow_external_fillets": true, 136 | "zones_use_no_outline": true 137 | }, 138 | "layer_presets": [] 139 | }, 140 | "boards": [], 141 | "cvpcb": { 142 | "equivalence_files": [] 143 | }, 144 | "erc": { 145 | "erc_exclusions": [], 146 | "meta": { 147 | "version": 0 148 | }, 149 | "pin_map": [ 150 | [ 151 | 0, 152 | 0, 153 | 0, 154 | 0, 155 | 0, 156 | 0, 157 | 1, 158 | 0, 159 | 0, 160 | 0, 161 | 0, 162 | 2 163 | ], 164 | [ 165 | 0, 166 | 2, 167 | 0, 168 | 1, 169 | 0, 170 | 0, 171 | 1, 172 | 0, 173 | 2, 174 | 2, 175 | 2, 176 | 2 177 | ], 178 | [ 179 | 0, 180 | 0, 181 | 0, 182 | 0, 183 | 0, 184 | 0, 185 | 1, 186 | 0, 187 | 1, 188 | 0, 189 | 1, 190 | 2 191 | ], 192 | [ 193 | 0, 194 | 1, 195 | 0, 196 | 0, 197 | 0, 198 | 0, 199 | 1, 200 | 1, 201 | 2, 202 | 1, 203 | 1, 204 | 2 205 | ], 206 | [ 207 | 0, 208 | 0, 209 | 0, 210 | 0, 211 | 0, 212 | 0, 213 | 1, 214 | 0, 215 | 0, 216 | 0, 217 | 0, 218 | 2 219 | ], 220 | [ 221 | 0, 222 | 0, 223 | 0, 224 | 0, 225 | 0, 226 | 0, 227 | 0, 228 | 0, 229 | 0, 230 | 0, 231 | 0, 232 | 2 233 | ], 234 | [ 235 | 1, 236 | 1, 237 | 1, 238 | 1, 239 | 1, 240 | 0, 241 | 1, 242 | 1, 243 | 1, 244 | 1, 245 | 1, 246 | 2 247 | ], 248 | [ 249 | 0, 250 | 0, 251 | 0, 252 | 1, 253 | 0, 254 | 0, 255 | 1, 256 | 0, 257 | 0, 258 | 0, 259 | 0, 260 | 2 261 | ], 262 | [ 263 | 0, 264 | 2, 265 | 1, 266 | 2, 267 | 0, 268 | 0, 269 | 1, 270 | 0, 271 | 2, 272 | 2, 273 | 2, 274 | 2 275 | ], 276 | [ 277 | 0, 278 | 2, 279 | 0, 280 | 1, 281 | 0, 282 | 0, 283 | 1, 284 | 0, 285 | 2, 286 | 0, 287 | 0, 288 | 2 289 | ], 290 | [ 291 | 0, 292 | 2, 293 | 1, 294 | 1, 295 | 0, 296 | 0, 297 | 1, 298 | 0, 299 | 2, 300 | 0, 301 | 0, 302 | 2 303 | ], 304 | [ 305 | 2, 306 | 2, 307 | 2, 308 | 2, 309 | 2, 310 | 2, 311 | 2, 312 | 2, 313 | 2, 314 | 2, 315 | 2, 316 | 2 317 | ] 318 | ], 319 | "rule_severities": { 320 | "bus_definition_conflict": "error", 321 | "bus_entry_needed": "error", 322 | "bus_label_syntax": "error", 323 | "bus_to_bus_conflict": "error", 324 | "bus_to_net_conflict": "error", 325 | "different_unit_footprint": "error", 326 | "different_unit_net": "error", 327 | "duplicate_reference": "error", 328 | "duplicate_sheet_names": "error", 329 | "extra_units": "error", 330 | "global_label_dangling": "warning", 331 | "hier_label_mismatch": "error", 332 | "label_dangling": "error", 333 | "lib_symbol_issues": "warning", 334 | "multiple_net_names": "warning", 335 | "net_not_bus_member": "warning", 336 | "no_connect_connected": "warning", 337 | "no_connect_dangling": "warning", 338 | "pin_not_connected": "error", 339 | "pin_not_driven": "error", 340 | "pin_to_pin": "warning", 341 | "power_pin_not_driven": "error", 342 | "similar_labels": "warning", 343 | "unannotated": "error", 344 | "unit_value_mismatch": "error", 345 | "unresolved_variable": "error", 346 | "wire_dangling": "error" 347 | } 348 | }, 349 | "libraries": { 350 | "pinned_footprint_libs": [], 351 | "pinned_symbol_libs": [] 352 | }, 353 | "meta": { 354 | "filename": "Heatplate_v1.1.kicad_pro", 355 | "version": 1 356 | }, 357 | "net_settings": { 358 | "classes": [ 359 | { 360 | "bus_width": 12.0, 361 | "clearance": 0.15, 362 | "diff_pair_gap": 0.25, 363 | "diff_pair_via_gap": 0.25, 364 | "diff_pair_width": 0.2, 365 | "line_style": 0, 366 | "microvia_diameter": 0.3, 367 | "microvia_drill": 0.1, 368 | "name": "Default", 369 | "pcb_color": "rgba(0, 0, 0, 0.000)", 370 | "schematic_color": "rgba(0, 0, 0, 0.000)", 371 | "track_width": 0.3, 372 | "via_diameter": 0.6, 373 | "via_drill": 0.3, 374 | "wire_width": 6.0 375 | }, 376 | { 377 | "bus_width": 12.0, 378 | "clearance": 0.4, 379 | "diff_pair_gap": 0.25, 380 | "diff_pair_via_gap": 0.25, 381 | "diff_pair_width": 0.2, 382 | "line_style": 0, 383 | "microvia_diameter": 0.3, 384 | "microvia_drill": 0.1, 385 | "name": "HEATBED", 386 | "nets": [ 387 | "HEATPLATE" 388 | ], 389 | "pcb_color": "rgba(0, 0, 0, 0.000)", 390 | "schematic_color": "rgba(0, 0, 0, 0.000)", 391 | "track_width": 1.3, 392 | "via_diameter": 0.6, 393 | "via_drill": 0.3, 394 | "wire_width": 6.0 395 | } 396 | ], 397 | "meta": { 398 | "version": 2 399 | }, 400 | "net_colors": null 401 | }, 402 | "pcbnew": { 403 | "last_paths": { 404 | "gencad": "", 405 | "idf": "", 406 | "netlist": "", 407 | "specctra_dsn": "", 408 | "step": "", 409 | "vrml": "" 410 | }, 411 | "page_layout_descr_file": "" 412 | }, 413 | "schematic": { 414 | "annotate_start_num": 0, 415 | "drawing": { 416 | "default_line_thickness": 6.0, 417 | "default_text_size": 50.0, 418 | "field_names": [], 419 | "intersheets_ref_own_page": false, 420 | "intersheets_ref_prefix": "", 421 | "intersheets_ref_short": false, 422 | "intersheets_ref_show": false, 423 | "intersheets_ref_suffix": "", 424 | "junction_size_choice": 3, 425 | "label_size_ratio": 0.375, 426 | "pin_symbol_size": 25.0, 427 | "text_offset_ratio": 0.15 428 | }, 429 | "legacy_lib_dir": "", 430 | "legacy_lib_list": [], 431 | "meta": { 432 | "version": 1 433 | }, 434 | "net_format_name": "", 435 | "ngspice": { 436 | "fix_include_paths": true, 437 | "fix_passive_vals": false, 438 | "meta": { 439 | "version": 0 440 | }, 441 | "model_mode": 0, 442 | "workbook_filename": "" 443 | }, 444 | "page_layout_descr_file": "", 445 | "plot_directory": "./", 446 | "spice_adjust_passive_values": false, 447 | "spice_external_command": "spice \"%I\"", 448 | "subpart_first_id": 65, 449 | "subpart_id_separator": 0 450 | }, 451 | "sheets": [ 452 | [ 453 | "e63e39d7-6ac0-4ffd-8aa3-1841a4541b55", 454 | "" 455 | ], 456 | [ 457 | "049cfd7b-f956-4ae0-945f-6f6476ef0a46", 458 | "PERIPHERALS" 459 | ] 460 | ], 461 | "text_variables": {} 462 | } 463 | -------------------------------------------------------------------------------- /Heatplate_v1.1/Heatplate_v1.1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerSpatz/PCB-reflow-solder-heat-plate/ff15e48973843f93387d2c7b3f7c796a69bbe7f2/Heatplate_v1.1/Heatplate_v1.1.pdf -------------------------------------------------------------------------------- /Heatplate_v1.1/gerbers/gerbers_Heatplate_v1.1_JLCPCB.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerSpatz/PCB-reflow-solder-heat-plate/ff15e48973843f93387d2c7b3f7c796a69bbe7f2/Heatplate_v1.1/gerbers/gerbers_Heatplate_v1.1_JLCPCB.zip -------------------------------------------------------------------------------- /Heatplate_v1.1/renders/Heatplate_v1.1_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerSpatz/PCB-reflow-solder-heat-plate/ff15e48973843f93387d2c7b3f7c796a69bbe7f2/Heatplate_v1.1/renders/Heatplate_v1.1_back.png -------------------------------------------------------------------------------- /Heatplate_v1.1/renders/Heatplate_v1.1_front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DerSpatz/PCB-reflow-solder-heat-plate/ff15e48973843f93387d2c7b3f7c796a69bbe7f2/Heatplate_v1.1/renders/Heatplate_v1.1_front.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bastian Mohing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PCB reflow solder heat plate 2 | 3 | ## Introduction 4 | 5 | The PCB reflow solder heat plate can be used to solder small PCBs with SMD parts. The user can control the maximum temperature and heating curve. 6 | 7 | ## Hardware design 8 | 9 | The idea for this design comes from https://github.com/AfterEarthLTD/Solder-Reflow-Plate. But when I checked the original design, I found some issues that could cause safety problems and other design choices that are not optimal. 10 | 11 | ### Feature list 12 | 13 | - PCB size 100x100mm, heat plate size 81x69mm 14 | - DC barrel plug (5.5x2.5mm), runs with 12V/5A power supply 15 | - resettable 5A PTC fuse 16 | - inrush current limiter to protect power supply when capacitors are discharged 17 | - 2.7mF input power filter for smooth current draw 18 | - capacitor discharging resistors 19 | - ATMEGA4809 MCU, can be programmed via UPDI header 20 | - high power NMOS with gate driver circuit 21 | - flyback diode connected to the heat plate 22 | - AMS1117 LDO 23 | - additional diodes for protection 24 | - dual buttons 25 | - status LED 26 | - 0.91" OLED display 27 | - LMT85 analog temperature sensor as main sensor 28 | - option to add additional digital sensors for calibration purposes 29 | 30 | ### Building your own 31 | 32 | You can order almost all parts needed from LCSC/JLCPCB. Only the OLED display and the power supply need to be sourced elsewhere. The BOM includes LCSC part numbers for hand soldering and their automated SMT manufacturing. Minimal part size is 0805 and there's enough space between all solder pads, so hand soldering should be no problem if you have a steady hand. Hardest part will be the MCU, but this is hand solderable, too. 33 | 34 | ### Safety measures compared to the original design 35 | 36 | The original design had a too low heat plate resistance, which caused very high current spikes when the heat plate was turned on. This should be filtered by PWM and the input capacitors, but the input capacitor in the original design was far too small (100µF). Because of this, the fuse always blew when the device was turned on, so eventually the fuse was removed again. 37 | In my design I made the input capacitors 27 times larger to have a smoother current draw. I also added a fuse, and an inrush current limiter. After disconnecting the power supply, the output capacitors are quickly discharged to GND. 38 | The original also had no gate driver, so the NMOS was only driven with logic levels. I added a simple NPN gate driver. 39 | Switching inductive loads (like the long heat plate trace) causes voltage spikes. In the original design, the filtering was done with an ESD diode and a small capacitor. The usual way to filter these voltage spikes is a flyback diode. 40 | The resistance of the heat plate was very low, so the current draw with 100% PWM duty cycle was very high. If the MCUs hangs up and the power MOSFET stays active, the original design could cause too much stress for the power supply. Furthermore, even with a hot heat plate, the resistance would still be so low that the board would draw more than 5A. 41 | In my design, the heat plate resistance was chosen in a way that a hot heat plate will draw less than 5A, so it is much safer to run the new design, as it will automatically reach electrically safe working conditions. 42 | 43 | ## Firmware 44 | 45 | The original firmware was reworked by Nathan Heidt (https://github.com/heidtn) and works mostly, only the temperature sensing is incorrect (as the firmware was written with an earlier board revision). So for now, you just need to unplug the device when the PCB that needed soldering is done. Also, the dual color LED is not yet included into the code, for now it just lights green to show power. And the buttons sometimes don't react properly. 46 | 47 | Daniel Oltmanns (https://github.com/oltdaniel) is doing a rework of the firmware using platformio. 48 | 49 | ### To-do list for software: 50 | - use of the dual color LED (green = power, orange = heating, red = hot) 51 | - better temperature sensing 52 | - better working buttons 53 | - calibration function for the analog temperature sensor by taping a digital probe directly to the heat plate 54 | 55 | Please note that I can only do hardware design, so I can't contribute anything to the software. 56 | 57 | ### Programming the MCU 58 | 59 | The MCU can be programmed with JTAG2UPDI (https://github.com/ElTangas/jtag2updi). For programming, you need an Arduino with ATMEGA328p (Uno or Nano), some wires, a 4.7k resistor and a 10µF capacitor or 120 Ohm resistor to disable the auto-reset. 60 | 61 | JCM from the Discord explained the process pretty good: 62 | 63 | > 1. Download/Clone this project: https://github.com/ElTangas/jtag2updi and rename the folder "source" to "jtag2updi" (otherwise the Arduino IDE won't like it) 64 | > 2. Open jtag2updi/jtag2updi.ino in your Arduino IDE 65 | > 3. Configure the flasher options for your Arduino Nano and flash it 66 | > 4. Connect D6 of your Arduino Nano over the 4.7kOhm resistor to the UPDI pin of the board and 5V to 5V and GND to 0V 67 | > 5. Add the MegaCoreX hardware package to the Ardunio IDE (see https://github.com/MCUdude/MegaCoreX#how-to-install) 68 | > 6. Install the Adafruit_GFX, Adafruit_SSD1306, DallasTemperature and Debounce2 libraries with the Library Manager (you might not need all of them depending on which firmware you plan to use) 69 | > 7. Download and open the ino you want to upload to the ATMEGA4809 (https://github.com/DerSpatz/PCB-reflow-solder-heat-plate/blob/main/Firmware/pcb_reflow_fw/pcb_reflow_fw.ino) 70 | > 8. Select the options for the programmer (Board: ATmega4809, Clock: Internal 16 MHz, BOD: 2.6V or 2.7V, EEPROM: retained, Pinout: 48 pin standard, Reset pin: Reset, no Bootloader) and select the port of your Ardunio Nano as Port 71 | > 9. Select Burn Bootloader and see if it runs through 72 | > 10. Temporarily disable auto reset for the Arduino Nano: https://playground.arduino.cc/Main/DisablingAutoResetOnSerialConnection/ (not sure if it's needed for the Nano, it was for my Mega) 73 | > 11. Select Sketch > Upload using Programmer (normal Upload will not work) 74 | 75 | ## Thanks 76 | 77 | Special Thanks go out to: 78 | 79 | Nathan Heidt for writing the software and testing 80 | 81 | Merlin Shaw (www.facebook.com/GeekIslandGaming) for ordering the first batch of prototypes and testing 82 | 83 | ![heatplate PCB front](https://github.com/DerSpatz/PCB-reflow-solder-heat-plate/blob/main/Heatplate_v1.1/renders/Heatplate_v1.1_front.png) 84 | ![heatplate PCB back](https://github.com/DerSpatz/PCB-reflow-solder-heat-plate/blob/main/Heatplate_v1.1/renders/Heatplate_v1.1_back.png) 85 | --------------------------------------------------------------------------------