ESP32 Heater Controller
U1)Table
| Function | GPIO / Module Pin | Net | Connected To | Direction | Notes |
|---|---|---|---|---|---|
| USB D- | GPIO19 / IO19 | USB_D_N | J1 USB-C, U5 ESD | USB | Native USB programming/data |
| USB D+ | GPIO20 / IO20 | USB_D_P | J1 USB-C, U5 ESD | USB | Native USB programming/data |
| I2C SCL | GPIO8 / IO8 | I2C_SCL | U2 STUSB4500, J6 Qwiic, R7 4.7k | Open-drain | 3.3 V bus |
| I2C SDA | GPIO9 / IO9 | I2C_SDA | U2 STUSB4500, J6 Qwiic, R6 4.7k | Open-drain | 3.3 V bus |
| STUSB ALERT | GPIO38 / IO38 | PD_ALERT | U2 ALERT, R8 4.7k pull-up | Input | Active-low/open-drain alert |
| Heater 1 PWM | GPIO10 / IO10 | HEATER1_PWM | R20 100R -> Q1 gate | Output | Low-side MOSFET driver |
| Heater 2 PWM | GPIO11 / IO11 | HEATER2_PWM | R21 100R -> Q2 gate | Output | Low-side MOSFET driver |
| Heater 3 PWM | GPIO12 / IO12 | HEATER3_PWM | R22 100R -> Q3 gate | Output | Low-side MOSFET driver |
| Heater 4 PWM | GPIO13 / IO13 | HEATER4_PWM | R23 100R -> Q4 gate | Output | Low-side MOSFET driver |
| Heater safety NTC 1 | GPIO1 / IO1 | TEMP_HEATER1_ADC | RT1 + R30 10k pull-up | ADC input | 10k NTC divider to GND |
| Heater safety NTC 2 | GPIO2 / IO2 | TEMP_HEATER2_ADC | RT2 + R31 10k pull-up | ADC input | 10k NTC divider to GND |
| Heater safety NTC 3 | GPIO3 / IO3 | TEMP_HEATER3_ADC | RT3 + R32 10k pull-up | ADC input | 10k NTC divider to GND |
| Heater safety NTC 4 | GPIO4 / IO4 | TEMP_HEATER4_ADC | RT4 + R33 10k pull-up | ADC input | 10k NTC divider to GND |
| Ambient NTC A | GPIO5 / IO5 | TEMP_AMBIENT_A_ADC | RT5 + R34 10k pull-up | ADC input | 10k NTC divider to GND |
| Ambient NTC B | GPIO6 / IO6 | TEMP_AMBIENT_B_ADC | RT6 + R35 10k pull-up | ADC input | 10k NTC divider to GND |
| Encoder A | GPIO17 / IO17 | ENC_A | SW1 A, R50 10k pull-up | Input | Debounced/interrupt-capable |
| Encoder B | GPIO18 / IO18 | ENC_B | SW1 B, R51 10k pull-up | Input | Debounced/interrupt-capable |
| Encoder push | GPIO21 / IO21 | ENC_BUTTON | SW1 switch, R52 10k pull-up | Input | Active low |
| RGB red | GPIO14 / IO14 | RGB_RED_DRV | R40 560R -> D2 cathode | Output | Active low, LED anode to 3V3 |
| RGB green | GPIO15 / IO15 | RGB_GREEN_DRV | R41 220R -> D3 cathode | Output | Active low, LED anode to 3V3 |
| RGB blue | GPIO16 / IO16 | RGB_BLUE_DRV | R42 220R -> D4 cathode | Output | Active low, LED anode to 3V3 |
| BOOT | GPIO0 / IO0 | ESP_BOOT | S1, R4 10k pull-up | Input | Hold low during reset for bootloader |
| EN / reset | EN | ESP_EN | S2, R3 10k pull-up, C11 | Input | Reset button |
| UART TX fallback | TXD0 | UART_TXD0 | J7 pin 3 | Output | 3.3 V UART |
| UART RX fallback | RXD0 | UART_RXD0 | J7 pin 4 | Input | 3.3 V UART |
platformio.ini:Ini
[env:esp32-s3-heater-controller] platform = espressif32@6.6.0 board = esp32-s3-devkitc-1 framework = arduino monitor_speed = 115200 upload_speed = 921600 board_build.mcu = esp32s3 board_build.f_cpu = 240000000L build_flags = -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 lib_deps = adafruit/Adafruit SSD1306@^2.5.10 adafruit/Adafruit GFX Library@^1.11.9
src/main.cpp.Cpp
#include <Arduino.h> #include <Wire.h> #include <WiFi.h> #include <esp_task_wdt.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include <math.h> // ----------------------------------------------------------------------------- // Pin definitions from schematic // ----------------------------------------------------------------------------- constexpr int PIN_I2C_SCL = 8; constexpr int PIN_I2C_SDA = 9; constexpr int PIN_PD_ALERT = 38; constexpr int PIN_HEATER_PWM[4] = {10, 11, 12, 13}; constexpr int PIN_TEMP_HEATER[4] = {1, 2, 3, 4}; constexpr int PIN_TEMP_AMBIENT[2] = {5, 6}; constexpr int PIN_ENC_A = 17; constexpr int PIN_ENC_B = 18; constexpr int PIN_ENC_BUTTON = 21; constexpr int PIN_LED_RED = 14; // active low constexpr int PIN_LED_GREEN = 15; // active low constexpr int PIN_LED_BLUE = 16; // active low // ----------------------------------------------------------------------------- // Electrical constants // ----------------------------------------------------------------------------- constexpr float ADC_REF_MV = 3300.0f; constexpr float NTC_PULLUP_OHMS = 10000.0f; constexpr float NTC_R25_OHMS = 10000.0f; constexpr float NTC_BETA = 3950.0f; // Verify against final populated NTC MPN. constexpr float TEMP_K_25C = 298.15f; constexpr float HEATER_TARGET_MAX_C = 38.0f; constexpr float HEATER_SAFETY_SHUTDOWN_C = 45.0f; // Conservative first firmware threshold; tune after thermal tests. constexpr float SENSOR_OPEN_C = -40.0f; constexpr float SENSOR_SHORT_C = 125.0f; constexpr uint8_t STUSB4500_ADDR = 0x28; // ADDR0/ADDR1 grounded in schematic. constexpr uint8_t OLED_ADDR = 0x3C; // Common Qwiic OLED default; change if final OLED differs. constexpr int OLED_W = 128; constexpr int OLED_H = 64; constexpr int PWM_FREQ_HZ = 1000; constexpr int PWM_RES_BITS = 10; constexpr int PWM_MAX = (1 << PWM_RES_BITS) - 1; // WiFi placeholders; firmware must remain safe if WiFi is absent. const char *WIFI_SSID = "YOUR_SSID"; const char *WIFI_PASSWORD = "YOUR_PASSWORD"; Adafruit_SSD1306 display(OLED_W, OLED_H, &Wire, -1); volatile int32_t encoderTicks = 0; volatile uint8_t lastEncoderState = 0; float heaterTempsC[4] = {NAN, NAN, NAN, NAN}; float ambientTempsC[2] = {NAN, NAN}; bool heatersAllowed = false; uint16_t heaterDuty[4] = {0, 0, 0, 0}; uint32_t lastSensorMs = 0; uint32_t lastDisplayMs = 0; uint32_t lastWifiMs = 0; void setLed(bool red, bool green, bool blue) { digitalWrite(PIN_LED_RED, red ? LOW : HIGH); digitalWrite(PIN_LED_GREEN, green ? LOW : HIGH); digitalWrite(PIN_LED_BLUE, blue ? LOW : HIGH); } void setAllHeatersOff() { for (int i = 0; i < 4; i++) { heaterDuty[i] = 0; ledcWrite(i, 0); } } void setHeaterDuty(int channel, uint16_t duty10bit) { if (channel < 0 || channel >= 4) return; if (!heatersAllowed) duty10bit = 0; if (duty10bit > PWM_MAX) duty10bit = PWM_MAX; heaterDuty[channel] = duty10bit; ledcWrite(channel, duty10bit); } float readNtcC(int pin) { // Use millivolts when supported by ESP32 Arduino for calibrated ADC behavior. uint32_t mv = analogReadMilliVolts(pin); if (mv < 50) return SENSOR_SHORT_C; // NTC nearly shorted to GND / very hot equivalent. if (mv > 3250) return SENSOR_OPEN_C; // Open NTC or missing ground path. float v = static_cast<float>(mv); float rNtc = NTC_PULLUP_OHMS * v / (ADC_REF_MV - v); float invT = (1.0f / TEMP_K_25C) + (logf(rNtc / NTC_R25_OHMS) / NTC_BETA); float tempK = 1.0f / invT; return tempK - 273.15f; } bool sensorsHealthy() { for (float t : heaterTempsC) { if (!isfinite(t) || t <= SENSOR_OPEN_C + 0.1f || t >= SENSOR_SHORT_C - 0.1f) return false; } for (float t : ambientTempsC) { if (!isfinite(t) || t <= SENSOR_OPEN_C + 0.1f || t >= SENSOR_SHORT_C - 0.1f) return false; } return true; } bool heaterTempsSafe() { for (float t : heaterTempsC) { if (!isfinite(t) || t >= HEATER_SAFETY_SHUTDOWN_C) return false; } return true; } void updateSensors() { for (int i = 0; i < 4; i++) heaterTempsC[i] = readNtcC(PIN_TEMP_HEATER[i]); for (int i = 0; i < 2; i++) ambientTempsC[i] = readNtcC(PIN_TEMP_AMBIENT[i]); if (!sensorsHealthy() || !heaterTempsSafe()) { heatersAllowed = false; setAllHeatersOff(); setLed(true, false, false); } } bool i2cDevicePresent(uint8_t addr) { Wire.beginTransmission(addr); return Wire.endTransmission() == 0; } void scanI2C() { Serial.println("I2C scan:"); for (uint8_t a = 0x08; a < 0x78; a++) { if (i2cDevicePresent(a)) { Serial.printf(" found 0x%02X\n", a); } } } uint8_t readStusbReg(uint8_t reg) { Wire.beginTransmission(STUSB4500_ADDR); Wire.write(reg); if (Wire.endTransmission(false) != 0) return 0xFF; if (Wire.requestFrom(STUSB4500_ADDR, static_cast<uint8_t>(1)) != 1) return 0xFF; return Wire.read(); } void checkPdController() { if (!i2cDevicePresent(STUSB4500_ADDR)) { Serial.println("STUSB4500 not found; assume unknown input capability, heaters disabled."); heatersAllowed = false; return; } uint8_t alertStatus = readStusbReg(0x0B); // Informational diagnostic read; verify register map in production firmware. Serial.printf("STUSB4500 present, status/reg 0x0B = 0x%02X\n", alertStatus); // IMPORTANT: Production firmware should verify the STUSB4500 NVM/PDOs are 5 V-only before enabling heaters. } void IRAM_ATTR handleEncoder() { uint8_t a = digitalRead(PIN_ENC_A); uint8_t b = digitalRead(PIN_ENC_B); uint8_t state = (a << 1) | b; uint8_t transition = (lastEncoderState << 2) | state; if (transition == 0b0001 || transition == 0b0111 || transition == 0b1110 || transition == 0b1000) encoderTicks++; if (transition == 0b0010 || transition == 0b0100 || transition == 0b1101 || transition == 0b1011) encoderTicks--; lastEncoderState = state; } void initGpio() { pinMode(PIN_PD_ALERT, INPUT_PULLUP); pinMode(PIN_LED_RED, OUTPUT); pinMode(PIN_LED_GREEN, OUTPUT); pinMode(PIN_LED_BLUE, OUTPUT); setLed(false, false, true); pinMode(PIN_ENC_A, INPUT_PULLUP); pinMode(PIN_ENC_B, INPUT_PULLUP); pinMode(PIN_ENC_BUTTON, INPUT_PULLUP); lastEncoderState = (digitalRead(PIN_ENC_A) << 1) | digitalRead(PIN_ENC_B); attachInterrupt(digitalPinToInterrupt(PIN_ENC_A), handleEncoder, CHANGE); attachInterrupt(digitalPinToInterrupt(PIN_ENC_B), handleEncoder, CHANGE); for (int i = 0; i < 4; i++) { pinMode(PIN_HEATER_PWM[i], OUTPUT); digitalWrite(PIN_HEATER_PWM[i], LOW); ledcSetup(i, PWM_FREQ_HZ, PWM_RES_BITS); ledcAttachPin(PIN_HEATER_PWM[i], i); ledcWrite(i, 0); } for (int pin : PIN_TEMP_HEATER) analogSetPinAttenuation(pin, ADC_11db); for (int pin : PIN_TEMP_AMBIENT) analogSetPinAttenuation(pin, ADC_11db); } void initDisplay() { if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) { Serial.println("OLED not found at 0x3C; continuing headless."); return; } display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); display.println("ESP32 Heater Ctrl"); display.println("Boot: heaters OFF"); display.display(); } void updateDisplay() { if (!i2cDevicePresent(OLED_ADDR)) return; display.clearDisplay(); display.setCursor(0, 0); display.printf("Allowed: %s\n", heatersAllowed ? "YES" : "NO"); for (int i = 0; i < 4; i++) display.printf("H%d: %.1fC D:%u\n", i + 1, heaterTempsC[i], heaterDuty[i]); display.printf("A: %.1f %.1fC\n", ambientTempsC[0], ambientTempsC[1]); display.printf("Enc: %ld Btn:%d\n", static_cast<long>(encoderTicks), digitalRead(PIN_ENC_BUTTON) == LOW); display.display(); } void connectWiFiNonBlocking() { if (strcmp(WIFI_SSID, "YOUR_SSID") == 0) return; if (WiFi.status() == WL_CONNECTED) return; WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); } void printStatus() { Serial.printf("H=[%.1f %.1f %.1f %.1f] A=[%.1f %.1f] allowed=%d enc=%ld button=%d\n", heaterTempsC[0], heaterTempsC[1], heaterTempsC[2], heaterTempsC[3], ambientTempsC[0], ambientTempsC[1], heatersAllowed, static_cast<long>(encoderTicks), digitalRead(PIN_ENC_BUTTON) == LOW); } void setup() { Serial.begin(115200); delay(200); Serial.println("ESP32 Heater Controller safe firmware starting"); initGpio(); setAllHeatersOff(); heatersAllowed = false; Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL, 400000); scanI2C(); checkPdController(); initDisplay(); esp_task_wdt_init(8, true); esp_task_wdt_add(NULL); updateSensors(); Serial.println("Init complete. Heaters remain disabled until firmware policy enables them."); } void loop() { esp_task_wdt_reset(); uint32_t now = millis(); if (now - lastSensorMs >= 500) { lastSensorMs = now; updateSensors(); printStatus(); } if (now - lastDisplayMs >= 1000) { lastDisplayMs = now; updateDisplay(); } if (now - lastWifiMs >= 10000) { lastWifiMs = now; connectWiFiNonBlocking(); } // First firmware policy: only allow manual enable by holding encoder button, and only if sensors are healthy. // Production firmware should replace this with power-source classification, PID/control logic, and latched fault handling. if (digitalRead(PIN_ENC_BUTTON) == LOW && sensorsHealthy() && heaterTempsSafe()) { heatersAllowed = true; setLed(false, true, false); } if (!heatersAllowed) { setAllHeatersOff(); } delay(10); }
Bash
pio run pio run -t upload pio device monitor -b 115200
USB_D_P/USB_D_N through the USB-C connector.Platform & Toolchain
Pin Mapping
Dependencies & Project Setup
Complete Firmware Source
Build & Flash Instructions
Firmware Safety Requirements for Production