PCB Design for Fog Generator
+14V_BUS → +28V_BUS+14V_MUX → +28V_OR_PRE_FUSEVBAT_A_IN → VBAT_SERIES_INPUMP_PWM → STEPPER_STEP+28V_BUS, GND, STEPPER_STEP, STEPPER_DIR, STEPPER_ENABLETable
| Function | ESP32 GPIO | Schematic Net | Direction | Notes |
|---|---|---|---|---|
| Heater PWM | GPIO4 | HEATER_PWM | Output | Drives Q1 heater MOSFET gate via R13 |
| Pump PWM | GPIO5 | PUMP_PWM | Output | Drives Q2 pump MOSFET gate via R16 |
| Fan PWM | GPIO6 | FAN_PWM | Output | Reserved for fan driver |
| Status LED 1 | GPIO7 | STATUS_LED_1 | Output | Routed through R31 330Ω to J10 pin 8 |
| Status LED 2 | GPIO8 | STATUS_LED_2 | Output | Routed through R32 330Ω to J10 pin 9 |
| Encoder A | GPIO9 | ENCODER_A | Input | J10 pin 5, 10kΩ pull-up R26 to +3V3 |
| Encoder B | GPIO10 | ENCODER_B | Input | J10 pin 6, 10kΩ pull-up R27 to +3V3 |
| Encoder button | GPIO11 | ENCODER_BTN | Input | J10 pin 7, 10kΩ pull-up R28 to +3V3 |
| SPI MOSI | GPIO12 | SPI_MOSI | Output | Not used by MAX31855; available for future shared SPI devices |
| SPI MISO | GPIO13 | SPI_MISO | Input | MAX31855 SO output |
| SPI SCK | GPIO14 | SPI_SCK | Output | MAX31855 SCK input |
| SPI CS | GPIO15 | SPI_CS_MAX31855 | Output | MAX31855 chip select, active low |
| I2C SDA | GPIO16 | I2C_SDA | Bidirectional | INA226 battery monitors U6A/U6B, 4.7k pull-up R22 |
| I2C SCL | GPIO17 | I2C_SCL | Output | INA226 battery monitors U6A/U6B, 4.7k pull-up R23 |
| DMX UART RX | GPIO18 | DMX_UART_RX | Input | Reserved for MAX3485 RO |
| USB D- | GPIO19 | USB_D_N | USB | Native USB D- |
| USB D+ | GPIO20 | USB_D_P | USB | Native USB D+ |
| DMX UART TX | GPIO21 | DMX_UART_TX | Output | Reserved for MAX3485 DI |
| BOOT | GPIO0 | ESP32_BOOT_IO0 | Input | 10k pull-up; BOOT button pulls low |
| DMX direction | GPIO35 | DMX_DIR | Output | Reserved for MAX3485 DE/~RE |
| Heater current sense | GPIO36 | HEATER_CURRENT_SENSE | Analog input | Optional / v2 |
| Fault LED | GPIO37 | FAULT_LED | Output | Drives amber D4 through R34 330Ω |
| Enable / reset | EN | ESP32_EN | Input | 10k pull-up, 0.1uF to GND, RESET button to GND |
Table
| J10 Pin | Net | Notes |
|---|---|---|
| 1 | +3V3 | Logic supply for off-board UI/OLED |
| 2 | GND | Ground |
| 3 | I2C_SCL | Shared I2C bus for off-board OLED at address 0x3C |
| 4 | I2C_SDA | Shared I2C bus |
| 5 | ENCODER_A | Rotary encoder A, board-side 10kΩ pull-up |
| 6 | ENCODER_B | Rotary encoder B, board-side 10kΩ pull-up |
| 7 | ENCODER_BTN | Encoder push-button, board-side 10kΩ pull-up |
| 8 | STATUS_LED_1_HDR | GPIO7 through 330Ω R31 |
| 9 | STATUS_LED_2_HDR | GPIO8 through 330Ω R32 |
| 10 | GND | Ground |
platformio.iniIni
[env:esp32-s3-devkitc-1] platform = espressif32 board = esp32-s3-devkitc-1 framework = arduino monitor_speed = 115200 upload_speed = 921600 board_build.flash_size = 16MB board_build.arduino.memory_type = qio_opi build_flags = -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1
src/main.cpp)Cpp
#include <Arduino.h> #include <Wire.h> #include <SPI.h> // Pin definitions from schematic #define PIN_HEATER_PWM 4 #define PIN_PUMP_PWM 5 #define PIN_FAN_PWM 6 #define PIN_STATUS_LED_1 7 #define PIN_STATUS_LED_2 8 #define PIN_ENCODER_A 9 #define PIN_ENCODER_B 10 #define PIN_ENCODER_BTN 11 #define PIN_SPI_MOSI 12 #define PIN_SPI_MISO 13 #define PIN_SPI_SCK 14 #define PIN_SPI_CS_MAX31855 15 #define PIN_I2C_SDA 16 #define PIN_I2C_SCL 17 #define PIN_DMX_UART_RX 18 #define PIN_DMX_UART_TX 21 #define PIN_DMX_DIR 35 #define PIN_HEATER_CURRENT_ADC 36 #define PIN_FAULT_LED 37 #define INA226_ADDR_BAT_A 0x40 #define INA226_ADDR_BAT_B 0x41 #define INA226_REG_CONFIG 0x00 #define INA226_REG_SHUNT_V 0x01 #define INA226_REG_BUS_V 0x02 #define INA226_REG_POWER 0x03 #define INA226_REG_CURRENT 0x04 #define INA226_REG_CALIBRATION 0x05 // 5mΩ shunts on VBAT_A_IN and VBAT_B_IN. // Current_LSB chosen as 1mA/bit for simple bring-up; update calibration if production scaling changes. #define INA226_SHUNT_OHMS 0.005f #define INA226_CURRENT_LSB_A 0.001f #define INA226_CAL_VALUE 1024 // PWM setup #define PWM_FREQ_HZ 2000 #define PWM_RES_BITS 10 #define PWM_HEATER_CHANNEL 0 #define PWM_PUMP_CHANNEL 1 #define PWM_FAN_CHANNEL 2 static unsigned long lastStatusMs = 0; static bool blinkState = false; void setupPwmOutputs() { ledcSetup(PWM_HEATER_CHANNEL, PWM_FREQ_HZ, PWM_RES_BITS); ledcAttachPin(PIN_HEATER_PWM, PWM_HEATER_CHANNEL); ledcWrite(PWM_HEATER_CHANNEL, 0); ledcSetup(PWM_PUMP_CHANNEL, PWM_FREQ_HZ, PWM_RES_BITS); ledcAttachPin(PIN_PUMP_PWM, PWM_PUMP_CHANNEL); ledcWrite(PWM_PUMP_CHANNEL, 0); ledcSetup(PWM_FAN_CHANNEL, PWM_FREQ_HZ, PWM_RES_BITS); ledcAttachPin(PIN_FAN_PWM, PWM_FAN_CHANNEL); ledcWrite(PWM_FAN_CHANNEL, 0); } void setupGpio() { pinMode(PIN_STATUS_LED_1, OUTPUT); pinMode(PIN_STATUS_LED_2, OUTPUT); digitalWrite(PIN_STATUS_LED_1, LOW); digitalWrite(PIN_STATUS_LED_2, LOW); pinMode(PIN_ENCODER_A, INPUT_PULLUP); pinMode(PIN_ENCODER_B, INPUT_PULLUP); pinMode(PIN_ENCODER_BTN, INPUT_PULLUP); pinMode(PIN_SPI_CS_MAX31855, OUTPUT); digitalWrite(PIN_SPI_CS_MAX31855, HIGH); pinMode(PIN_DMX_DIR, OUTPUT); digitalWrite(PIN_DMX_DIR, LOW); // default receive mode for future MAX3485 block pinMode(PIN_FAULT_LED, OUTPUT); digitalWrite(PIN_FAULT_LED, LOW); } void setupBuses() { Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL); Wire.setClock(400000); SPI.begin(PIN_SPI_SCK, PIN_SPI_MISO, PIN_SPI_MOSI, PIN_SPI_CS_MAX31855); // DMX uses 250000 baud, 8N2. Transceiver is reserved for a later schematic block. Serial1.begin(250000, SERIAL_8N2, PIN_DMX_UART_RX, PIN_DMX_UART_TX); } bool i2cWrite16(uint8_t addr, uint8_t reg, uint16_t value) { Wire.beginTransmission(addr); Wire.write(reg); Wire.write((uint8_t)(value >> 8)); Wire.write((uint8_t)(value & 0xFF)); return Wire.endTransmission() == 0; } bool i2cRead16(uint8_t addr, uint8_t reg, uint16_t &value) { Wire.beginTransmission(addr); Wire.write(reg); if (Wire.endTransmission(false) != 0) return false; if (Wire.requestFrom((int)addr, 2) != 2) return false; value = ((uint16_t)Wire.read() << 8) | Wire.read(); return true; } void setupIna226(uint8_t addr) { // Default averaging/conversion settings are acceptable for bring-up. // Program calibration so CURRENT register uses INA226_CURRENT_LSB_A. if (!i2cWrite16(addr, INA226_REG_CALIBRATION, INA226_CAL_VALUE)) { Serial.printf("INA226 0x%02X calibration write failed\n", addr); } } bool readIna226(uint8_t addr, float &busV, float &shuntV, float ¤tA) { uint16_t rawBus = 0; uint16_t rawShunt = 0; uint16_t rawCurrent = 0; if (!i2cRead16(addr, INA226_REG_BUS_V, rawBus)) return false; if (!i2cRead16(addr, INA226_REG_SHUNT_V, rawShunt)) return false; if (!i2cRead16(addr, INA226_REG_CURRENT, rawCurrent)) return false; busV = rawBus * 0.00125f; // 1.25mV/bit shuntV = (int16_t)rawShunt * 0.0000025f; // 2.5uV/bit currentA = (int16_t)rawCurrent * INA226_CURRENT_LSB_A; return true; } uint32_t readMax31855Raw() { SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); digitalWrite(PIN_SPI_CS_MAX31855, LOW); delayMicroseconds(1); uint32_t value = SPI.transfer32(0x00000000); digitalWrite(PIN_SPI_CS_MAX31855, HIGH); SPI.endTransaction(); return value; } bool readMax31855(float &thermocoupleC, float &coldJunctionC, uint8_t &faultBits) { uint32_t raw = readMax31855Raw(); faultBits = raw & 0x07; if (raw & 0x00010000UL) { return false; // any thermocouple fault } int16_t tcRaw = (raw >> 18) & 0x3FFF; if (tcRaw & 0x2000) tcRaw |= 0xC000; // sign extend 14-bit thermocouple field thermocoupleC = tcRaw * 0.25f; int16_t cjRaw = (raw >> 4) & 0x0FFF; if (cjRaw & 0x0800) cjRaw |= 0xF000; // sign extend 12-bit cold-junction field coldJunctionC = cjRaw * 0.0625f; return true; } void setup() { Serial.begin(115200); delay(250); Serial.println("Phantom Fogger Control Unit firmware starter"); Serial.println("ESP32-S3 native USB serial active"); setupGpio(); setupPwmOutputs(); setupBuses(); setupIna226(INA226_ADDR_BAT_A); setupIna226(INA226_ADDR_BAT_B); analogReadResolution(12); Serial.println("Initialization complete"); } void loop() { unsigned long now = millis(); if (now - lastStatusMs >= 500) { lastStatusMs = now; blinkState = !blinkState; digitalWrite(PIN_STATUS_LED_1, blinkState ? HIGH : LOW); float tcC = NAN; float cjC = NAN; uint8_t fault = 0; bool tcOk = readMax31855(tcC, cjC, fault); float batAV = NAN, batAShuntV = NAN, batAA = NAN; float batBV = NAN, batBShuntV = NAN, batBA = NAN; bool batAOk = readIna226(INA226_ADDR_BAT_A, batAV, batAShuntV, batAA); bool batBOk = readIna226(INA226_ADDR_BAT_B, batBV, batBShuntV, batBA); int adcRaw = analogRead(PIN_HEATER_CURRENT_ADC); if (tcOk) { Serial.printf("TC=%.2f C, CJ=%.2f C, BatA=%s %.2fV %.2fA, BatB=%s %.2fV %.2fA, encoder A=%d B=%d BTN=%d heater_current_adc=%d\n", tcC, cjC, batAOk ? "OK" : "ERR", batAV, batAA, batBOk ? "OK" : "ERR", batBV, batBA, digitalRead(PIN_ENCODER_A), digitalRead(PIN_ENCODER_B), digitalRead(PIN_ENCODER_BTN), adcRaw); } else { Serial.printf("MAX31855 fault=0x%02X, encoder A=%d B=%d BTN=%d heater_current_adc=%d\n", fault, digitalRead(PIN_ENCODER_A), digitalRead(PIN_ENCODER_B), digitalRead(PIN_ENCODER_BTN), adcRaw); } } }
Bash
pio run pio run -t upload pio device monitor
Platform & Toolchain
Pin Mapping
UI Header J10
`platformio.ini`
Complete Starter Source (`src/main.cpp`)
Build & Flash
Bootloader Entry