To build a reliable 12v pwm solar charge controller circuit diagram, you need an ESP32 microcontroller, a P-channel MOSFET (like the IRF9540N) for high-side switching, and a voltage divider network to monitor both panel and battery states. Unlike MPPT controllers that use buck converters to maximize harvest, a PWM controller acts as a rapid solid-state switch, connecting the solar panel directly to the battery and modulating the duty cycle to regulate voltage. This approach is highly efficient for small-scale 12V systems where the panel's Vmp (maximum power voltage) is close to the battery's absorption voltage.
System Architecture and the 12V PWM Solar Charge Controller Circuit Diagram
The system follows a strict source-to-load block architecture: Solar Panel (Source) → PWM Switch → Battery Bank (Storage) → Inverter (Load). The ESP32 sits in the middle, reading analog voltages and outputting a PWM signal to the gate driver.
| Component | Part Number / Value | Function in Circuit |
|---|---|---|
| Microcontroller | ESP32-WROOM-32 DevKit v1 | ADC voltage sensing, PWM generation, logic control |
| High-Side Switch | IRF9540N (P-Channel MOSFET) | Switches panel voltage to battery; handles up to 23A continuous |
| Gate Driver NPN | 2N3904 or 2N2222 | Level-shifts 3.3V ESP32 GPIO to drive the P-channel gate |
| Voltage Dividers | 100kΩ and 27kΩ (1% tolerance) | Steps down 0-25V panel/battery voltages to 0-3.3V for ESP32 ADC |
| Flyback Diode | 1N5822 (Schottky, 3A) | Prevents reverse current from battery to panel at night |
In this 12v pwm solar charge controller circuit diagram, the solar panel's positive terminal connects to the Source pin of the IRF9540N. The Drain pin connects to the battery positive. The ESP32 GPIO pin drives the base of the 2N3904 NPN transistor through a 1kΩ resistor. When the GPIO goes HIGH, the NPN conducts, pulling the MOSFET gate LOW and turning the P-channel ON. A 10kΩ pull-up resistor on the gate ensures the MOSFET stays OFF if the ESP32 resets.
Sizing the Battery Bank, Inverter, and Charge Limits
Before writing firmware, you must size the storage and load. Let's design for a 50W continuous load running for 12 hours (600Wh daily consumption).
Inverter and Load Sizing
A 50W continuous load requires an inverter capable of handling startup surges. A 500W pure sine wave inverter is the correct choice, providing a 10x surge margin for inductive loads like small pumps or compressors. Assuming an inverter efficiency of 85%, the actual energy drawn from the battery is:
600Wh / 0.85 = 705.8Wh
Battery Sizing and Peukert's Law
For a 12.8V nominal LiFePO4 battery, the required Amp-hours (Ah) is:
705.8Wh / 12.8V = 55.1Ah
If we were using Lead-Acid, we would apply Peukert's Law ($T = C / I^k$), where the exponent $k$ (typically 1.2) penalizes high-discharge rates, effectively reducing usable capacity. LiFePO4 chemistry is largely immune to Peukert losses ($k \approx 1.05$), but we must apply a strict Depth of Discharge (DoD) limit. To maximize cycle life, we limit DoD to 80%:
55.1Ah / 0.80 = 68.9Ah
Therefore, a 100Ah 12V LiFePO4 battery is the correct specification, providing a comfortable buffer for cloudy days.
| Configuration | Voltage Consequence | Ah Capacity Consequence | Best Application |
|---|---|---|---|
| Series (2x 12V 100Ah) | Doubles to 24V (25.6V nominal) | Remains 100Ah | High power systems (>1000W) to reduce current and wire gauge |
| Parallel (2x 12V 100Ah) | Remains 12V (12.8V nominal) | Doubles to 200Ah | Low voltage RV/Marine systems requiring extended runtime |
Never parallel mismatched lithium cells or packs with different internal resistances, ages, or state-of-charge (SoC) levels. Doing so causes uncontrolled cross-currents that can exceed the C-rate limits of the weaker pack, leading to thermal runaway and fire. Always use a dedicated BMS (Battery Management System) rated for your maximum charge/discharge current, and ensure your DIY controller includes a hardware fuse on the battery positive line.
Charge and Discharge Limits
For a 100Ah LiFePO4 bank, the maximum continuous charge C-rate is typically 0.5C (50A), and the maximum discharge is 1C (100A). The ESP32 firmware must enforce the following voltage thresholds:
- Bulk/CC: 100% PWM duty cycle until battery reaches 14.2V.
- Absorption/CV: Modulate PWM to hold battery exactly at 14.2V - 14.4V.
- Float: Drop PWM to maintain 13.6V (or turn off completely if the BMS handles balancing).
- Low Voltage Disconnect (LVD): Cut load at 11.5V to prevent deep-discharge damage.
ESP32 Firmware Logic and PWM Implementation
The ESP32's ADC is notoriously non-linear near the 0V and 3.3V rails. For accurate voltage sensing in a 12v pwm solar charge controller circuit diagram, use the analogReadMilliVolts() function introduced in ESP32 Arduino Core v2.0.0, which applies factory eFuse calibration data automatically. For the PWM output, we use the LEDC peripheral, which provides hardware-backed PWM that won't stutter if the Wi-Fi stack interrupts the CPU.
#include <Arduino.h>
// Pin Definitions
#define PIN_PANEL_ADC 34 // ADC1_CH6 (GPIO 34)
#define PIN_BATT_ADC 35 // ADC1_CH7 (GPIO 35)
#define PIN_PWM_OUT 25 // PWM output to NPN base
#define PIN_LOAD_EN 26 // Load disconnect relay
// Voltage Divider Ratios (R1=100k, R2=27k)
const float DIVIDER_RATIO = (100.0 + 27.0) / 27.0; // ~4.703
// PWM Configuration
const int PWM_FREQ = 1000; // 1kHz switching frequency
const int PWM_RESOLUTION = 10; // 0-1023 duty cycle
const int PWM_CHANNEL = 0;
// LiFePO4 Thresholds
const float ABSORPTION_V = 14.2;
const float FLOAT_V = 13.6;
const float LVD_V = 11.5;
float readVoltage(int pin) {
int raw_mv = analogReadMilliVolts(pin);
return (raw_mv / 1000.0) * DIVIDER_RATIO;
}
void setup() {
Serial.begin(115200);
pinMode(PIN_LOAD_EN, OUTPUT);
digitalWrite(PIN_LOAD_EN, HIGH); // Enable load initially
// Configure LEDC PWM
ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
ledcAttachPin(PIN_PWM_OUT, PWM_CHANNEL);
}
void loop() {
float panelV = readVoltage(PIN_PANEL_ADC);
float battV = readVoltage(PIN_BATT_ADC);
// Low Voltage Disconnect (LVD) Protection
if (battV < LVD_V) {
digitalWrite(PIN_LOAD_EN, LOW); // Kill load to save battery
ledcWrite(PWM_CHANNEL, 0); // Stop charging if deeply depleted
Serial.println("LVD TRIGGERED: Load disconnected.");
delay(60000); // Wait 1 min before re-checking
return;
} else {
digitalWrite(PIN_LOAD_EN, HIGH);
}
// Charge State Machine
int duty = 0;
if (battV < ABSORPTION_V) {
// Bulk Phase: 100% duty (Panel V must be > Batt V to push current)
if (panelV > battV + 0.5) {
duty = 1023;
}
} else if (battV < FLOAT_V + 0.2) {
// Absorption/Float Phase: Proportional control to hold voltage
// Simple P-controller for PWM modulation
float error = ABSORPTION_V - battV;
duty = constrain((int)(error * 500), 0, 1023);
} else {
// Fully charged, cut PWM to prevent overcharge
duty = 0;
}
ledcWrite(PWM_CHANNEL, duty);
Serial.printf("Panel: %.2fV | Batt: %.2fV | PWM Duty: %d\n", panelV, battV, duty);
delay(1000);
}
This code implements a basic proportional controller for the absorption phase. In a production environment, you would replace the simple P-controller with a full PID loop or a perturb-and-observe algorithm to minimize oscillation around the 14.2V setpoint. For deeper integration with home automation, you can extend this firmware to publish voltage and PWM telemetry over MQTT via the ESP32's native Wi-Fi stack, referencing the Espressif ADC Oneshot Driver documentation for advanced interrupt-driven sampling.
Frequently Asked Questions
How does a 12v pwm solar charge controller circuit diagram differ from MPPT?
A PWM controller acts as a switch, forcing the solar panel's operating voltage down to match the battery's current voltage. If your panel's Vmp is 18V and the battery is at 13V, the 5V difference is lost as heat, reducing harvest efficiency by up to 25%. An MPPT (Maximum Power Point Tracking) controller uses a synchronous buck converter to step down the voltage while proportionally increasing the current, preserving the total wattage. PWM is only recommended when the panel Vmp is very close to the battery absorption voltage (e.g., using a 15V "12V nominal" panel).
Can I use this 12v pwm solar charge controller circuit diagram for a 24V battery bank?
Not without hardware modifications. The IRF9540N MOSFET has a maximum Drain-Source voltage ($V_{DS}$) of -55V, which is sufficient for 24V systems (peaking around 29V), but the ESP32's voltage dividers must be recalibrated. You would need to change the divider resistors (e.g., 150kΩ and 27kΩ) to ensure the ADC never sees more than 3.3V when the panel reaches its open-circuit voltage (Voc) of ~45V. Additionally, the gate driver NPN must be rated to handle the higher gate-to-source voltage threshold of the 24V system.
What size wire do I need for the 12v pwm solar charge controller circuit diagram connections?
Wire sizing depends on the maximum short-circuit current ($I_{sc}$) of your solar panel and the NEC 310.16 ampacity tables. For a standard 100W 12V panel with an $I_{sc}$ of roughly 6A, 14 AWG THHN wire is more than sufficient (rated for 20A at 90°C). However, for the battery-to-inverter connection drawing up to 40A (500W / 12V = 41.6A), you must use at least 8 AWG copper wire to keep voltage drop under 3% over a 5-foot run and prevent insulation melting. Always size the fuse at 125% of the continuous current.
Why is my ESP32 PWM frequency causing the MOSFET to overheat in this circuit?
If your MOSFET is overheating, your PWM switching frequency is likely too high, causing excessive switching losses in the gate capacitance. The ESP32 code above uses 1kHz, which is ideal for the IRF9540N. If you pushed the frequency to 20kHz+ without a dedicated high-current gate driver (like a TC4420), the ESP32 GPIO cannot source enough current to charge the gate quickly. The MOSFET spends too much time in the linear (partially ON) region, acting as a resistor and dissipating massive heat. Stick to 500Hz - 2kHz for direct-GPIO-driven MOSFETs, or add a push-pull gate driver for higher frequencies.






