Why a DC Electronic Load Tops the List of Interesting Electronics Projects
If you ask seasoned bench engineers to name the most useful piece of test equipment that rarely gets bought by hobbyists, the DC electronic load is usually the answer. Unlike a standard multimeter or oscilloscope, an electronic load actively sinks current from a power source, allowing you to test batteries, solar panels, and power supplies under real-world stress. Building one yourself bridges the gap between embedded firmware, analog feedback loops, and power thermodynamics, making it one of the most interesting electronics projects you can tackle on the workbench.
At its core, a constant current (CC) electronic load relies on a fundamental analog principle: the operational amplifier virtual short. By placing a shunt resistor in series with a power MOSFET, we can measure the voltage drop across the shunt ($V = I \times R$). An op-amp compares this shunt voltage to a reference setpoint voltage and drives the MOSFET gate to force the two voltages to match. If our shunt is $0.1\Omega$ and we want a 2.0A load, the op-amp will adjust the gate voltage until exactly 0.2V drops across the shunt. The MOSFET operates in its linear (ohmic) region, acting as a voltage-controlled resistor that burns off the excess energy as heat.
System Architecture and Component Specifications
To achieve milliamp-level precision, we cannot rely on the ESP32's internal ADC or DAC. The internal ADC suffers from non-linearity and noise at low voltages, and the 8-bit internal DAC lacks the resolution for smooth current stepping. Instead, we offload the analog-to-digital and digital-to-analog conversion to dedicated I2C ICs. The target board for this firmware is the standard ESP32-WROOM-32 DevKit V1 (38-pin variant).
| Component | Exact Model / Variant | Key Specification | Role in Circuit |
|---|---|---|---|
| Microcontroller | ESP32-WROOM-32 DevKit V1 | 240MHz Dual-Core, 3.3V Logic | Runs PI control loop, handles I2C and UI |
| ADC Module | Adafruit ADS1115 Breakout | 16-bit, 860 SPS, PGA gain | Reads precise shunt voltage (current sense) |
| DAC Module | Adafruit MCP4725 Breakout | 12-bit, I2C, 2.7V-5.5V | Sets the analog current setpoint reference |
| Op-Amp | MCP6022 (Dual, R2R) | Rail-to-Rail I/O, 10MHz GBW | Error amplifier driving the MOSFET gate |
| Power MOSFET | IRLZ44N (Logic Level) | 55V, 47A, $V_{GS(th)}$ 1-2V | Linear region current sinking element |
| Shunt Resistor | 0.1$\Omega$ 5W Wirewound | 1% Tolerance, Low Tempco | Current-to-voltage transducer |
Hardware Pin Mapping and Wiring Steps
Wiring a mixed-signal circuit requires strict separation of high-current power paths and low-level I2C signal lines. Keep the shunt resistor and MOSFET source leads as short and thick as possible to minimize parasitic inductance.
| ESP32 Pin | Destination | Function |
|---|---|---|
| GPIO 21 | ADS1115 & MCP4725 SDA | I2C Data Line (Add 4.7k$\Omega$ pull-up to 3.3V) |
| GPIO 22 | ADS1115 & MCP4725 SCL | I2C Clock Line (Add 4.7k$\Omega$ pull-up to 3.3V) |
| 3V3 | ADS1115 VDD, MCP4725 VDD | Logic power for I2C peripherals |
| GND | Common Ground | Must tie to op-amp ground and shunt low-side |
When an op-amp drives the high gate capacitance of a power MOSFET (the IRLZ44N has a $C_{iss}$ of ~3300pF), the feedback loop will almost certainly oscillate at high frequencies, destroying the MOSFET. You must place a $100\Omega$ resistor in series with the op-amp output and the MOSFET gate, and a $1nF$ ceramic capacitor directly between the gate and source pins to create a dominant pole and stabilize the loop.
ESP32 Firmware: PI Control Loop and Error Handling
The following C++ code is written for the Arduino IDE. It implements a Proportional-Integral (PI) controller. A pure proportional controller will always leave a steady-state error (the current will be slightly below the target). The integral term accumulates the error over time to eliminate this offset. We use the Adafruit ADS1X15 library for the ADC.
#include <Wire.h>
#include <Adafruit_ADS1X15.h>
#include <Adafruit_MCP4725.h>
// --- Pin & I2C Definitions ---
#define I2C_SDA 21
#define I2C_SCL 22
#define ADS1115_ADDR 0x48
#define MCP4725_ADDR 0x60
// --- Hardware Constants ---
const float SHUNT_RESISTANCE = 0.1; // Ohms
const float ADC_GAIN_VOLTS = 0.125; // Volts per bit at PGA gain 1 (4.096V range / 32768)
const float TARGET_CURRENT = 2.0; // Amps
// --- PI Controller Tuning ---
const float Kp = 500.0;
const float Ki = 15.0;
float integral = 0.0;
Adafruit_ADS1115 ads;
Adafruit_MCP4725 dac;
void setup() {
Serial.begin(115200);
Wire.begin(I2C_SDA, I2C_SCL);
// Initialize ADC with error handling
if (!ads.begin(ADS1115_ADDR, &Wire)) {
Serial.println("ADS1115 init failed! Check I2C wiring.");
while (1) { delay(100); } // Halt execution
}
ads.setGain(GAIN_ONE); // +/- 4.096V range
// Initialize DAC
if (!dac.begin(MCP4725_ADDR, &Wire)) {
Serial.println("MCP4725 init failed! Check I2C wiring.");
while (1) { delay(100); }
}
// Start at 0A output for safety
dac.setVoltage(0, false);
Serial.println("Electronic Load Initialized. Engaging PI Loop.");
}
void loop() {
// 1. Read actual current via shunt voltage
int16_t adc_raw = ads.readADC_SingleEnded(0);
float shunt_voltage = adc_raw * ADC_GAIN_VOLTS;
float actual_current = shunt_voltage / SHUNT_RESISTANCE;
// 2. Calculate Error
float error = TARGET_CURRENT - actual_current;
// 3. PI Control Math
integral += error;
// Anti-windup clamping for the integral term
if (integral > 1000.0) integral = 1000.0;
if (integral < -1000.0) integral = -1000.0;
float control_output = (Kp * error) + (Ki * integral);
// 4. Map control output to 12-bit DAC value (0-4095)
// Max DAC output is ~3.3V, which maps to ~3.3A max on a 0.1 ohm shunt
int dac_value = constrain((int)control_output, 0, 4095);
// 5. Apply to DAC
dac.setVoltage(dac_value, false);
// Telemetry
Serial.print("Target: "); Serial.print(TARGET_CURRENT);
Serial.print("A | Actual: "); Serial.print(actual_current, 3);
Serial.print("A | DAC: "); Serial.println(dac_value);
delay(50); // 20Hz control loop rate
}
Debugging: First Three Things to Check When It Fails
When bridging digital logic and analog power, failures are rarely subtle. If your load does not regulate current, or the ESP32 halts on boot, follow this diagnostic sequence.
1. The Exact Error String: "ADS1115 init failed! Check I2C wiring."
If the serial monitor prints this exact string and halts, the ESP32 cannot handshake with the ADC over I2C. Ranked causes:
- Missing Pull-up Resistors: While breakout boards usually include 10k$\Omega$ pull-ups, they are often too weak for 400kHz I2C when combined with the gate capacitance of long breadboard wires. Add external 4.7k$\Omega$ resistors from SDA and SCL to 3.3V.
- ADDR Pin Floating: The ADS1115 ADDR pin dictates the I2C address. If left floating, it may default to an unexpected address. Tie it solidly to GND to force the $0x48$ address used in the code.
- Clone Board Silkscreen Errors: Many cheap ESP32 DevKit V1 clones swap the GPIO 21 and 22 silkscreen labels on the PCB header. Verify continuity with a multimeter against the official Espressif ESP32 datasheet pinout.
2. The First Three Hardware Checks for Analog Instability
If the code runs but the current reading is erratic, or the MOSFET gets instantly hot without a load:
- Check Op-Amp Power Rails: The MCP6022 must be powered by a clean 5V supply, not the noisy 3.3V LDO output of the ESP32. If the op-amp lacks the voltage headroom to fully drive the IRLZ44N gate threshold ($V_{GS(th)}$), the loop will saturate.
- Verify Kelvin (4-Wire) Shunt Connections: If you are reading 0.5A when the multimeter shows 2.0A, you are measuring the voltage drop across the solder joints and wires, not just the shunt. Run separate, dedicated sense wires directly from the shunt resistor body to the ADS1115 input terminals.
- Inspect the Gate Snubber: As mentioned in the wiring tips, if you omitted the $100\Omega$ gate resistor and $1nF$ capacitor, the op-amp is likely oscillating at MHz frequencies. This turns the MOSFET into a high-frequency heater, destroying the silicon junction in seconds. Probe the gate with an oscilloscope; it should be a flat DC line, not a fuzzy band of noise.
Extending and Simplifying the Build
Depending on your budget and precision requirements, this architecture can be scaled in either direction.
How to Simplify (The $8 Budget Build)
If you do not need milliamp precision and just want to discharge Li-Ion cells at roughly 1A, strip out the I2C peripherals. Use the ESP32's internal 8-bit DAC (GPIO 25) to set the reference voltage, and an internal ADC pin (GPIO 34) to read the shunt. You will lose resolution (steps of ~12mA instead of 0.2mA) and deal with a noisy ADC floor, but it reduces the BOM to just the ESP32, an LM358, a MOSFET, and a shunt.
How to Extend (Adding Constant Power and Resistance Modes)
To elevate this from a basic CC load to a professional-grade tester, implement Constant Power (CP) and Constant Resistance (CR) modes in the firmware.
In CR mode, the target current becomes dynamic: $I_{target} = V_{measured} / R_{set}$. You will need to add a voltage divider to a second ADS1115 channel to measure the source voltage.
In CP mode, the target current is $I_{target} = P_{set} / V_{measured}$. Be extremely careful with CP mode at low voltages; as the source voltage drops toward zero, the math demands infinite current to maintain the wattage. You must implement a hard software clamp on the maximum current limit to prevent the MOSFET from exceeding its Safe Operating Area (SOA) and shorting out internally. For deeper analog design theory on SOA limits, refer to the Texas Instruments ADS1115 product documentation and associated application notes on precision measurement.






