If you want to measure an RC filter’s cutoff frequency and time constant with a microcontroller, you cannot simply wire a resistor and capacitor directly to an ESP32 ADC pin. The ESP32-S3’s internal Successive Approximation Register (SAR) ADC has a dynamic input impedance that fluctuates during sampling—often dropping below 10kΩ. If your passive filter uses a 10kΩ resistor, the ADC itself will load the circuit, skewing your voltage readings and destroying your theoretical calculations.
The direct answer to building an accurate RC filter analyzer is to use a unity-gain op-amp buffer between your passive network and the microcontroller. This project bridges core AC/DC theory (impedance, time constants, and dielectric absorption) with practical embedded debugging, giving you a bench tool to visualize exponential charge curves and verify component tolerances in real time.
The Theory: Why Passive RC Filters Need Buffering
A first-order passive low-pass filter consists of a series resistor (R) and a shunt capacitor (C). The governing equations for its behavior in the time and frequency domains are foundational to AC circuit theory:
- Time Constant (τ): τ = R × C (The time required for the capacitor to charge to ~63.2% of the applied DC voltage).
- Cutoff Frequency (fc): fc = 1 / (2π × R × C) (The frequency at which the output power drops by half, or -3dB).
When we build this for electronics projects targeting the ESP32-S3, we typically choose R = 10kΩ and C = 100nF. This yields a theoretical τ of 1ms and an fc of 159.15 Hz. However, when the ESP32's internal sampling switch closes to take an ADC reading, it draws a brief spike of current to charge its internal sampling capacitor. Without a low-impedance buffer, the external 10kΩ resistor limits this current, causing the ADC reading to settle late and read artificially low. By inserting an MCP6002 op-amp configured as a voltage follower (unity-gain buffer), we present a near-infinite input impedance to the RC network and a near-zero output impedance to the ESP32 ADC, eliminating sampling settling errors.
Hardware Spec Sheet & Pin Mapping
Component selection matters immensely here. Do not use standard X7R ceramic capacitors for the filter; X7R dielectrics exhibit piezoelectric effects and voltage coefficients that will distort your charge curve. Use C0G (NP0) ceramics for near-ideal linear behavior.
| Component | Exact Variant / Specification | Purpose |
|---|---|---|
| Microcontroller | ESP32-S3-DevKitC-1 (N8R8) | Main processor; selected for its improved ADC linearity over the original ESP32. |
| Op-Amp | MCP6002-I/P (DIP-8) | Rail-to-rail I/O unity-gain buffer to isolate ADC from filter. |
| Resistor (R) | 10kΩ 1% Metal Film (1/4W) | Sets the time constant; 1% tolerance ensures predictable τ. |
| Capacitor (C) | 100nF C0G/NP0 Ceramic (5mm pitch) | Filter element; C0G dielectric prevents voltage-coefficient distortion. |
| Decoupling Cap | 100nF X7R Ceramic | Placed physically adjacent to MCP6002 VCC and GND pins. |
Pin Mapping Table
| ESP32-S3 Pin | Direction | Destination | Notes |
|---|---|---|---|
| GPIO 5 | Output (PWM) | Resistor (10kΩ) Input | Generates the step-response square wave. |
| GPIO 4 | Input (ADC1_CH3) | MCP6002 Output (Pin 1) | Reads the filtered exponential curve. |
| 3V3 | Power | MCP6002 VCC (Pin 8) | Powers the op-amp rail. |
| GND | Ground | MCP6002 GND (Pin 3), Cap GND | Common ground reference. |
Assembly & Verification Steps
- Build the Passive Network: Connect GPIO 5 to the 10kΩ resistor. Connect the other end of the resistor to the 100nF C0G capacitor. Connect the other end of the capacitor to GND. The junction between the resistor and capacitor is your filtered signal.
- Wire the Buffer: Connect the filtered signal junction to the MCP6002 non-inverting input (Pin 3). Connect the inverting input (Pin 2) directly to the output (Pin 1) to create a unity-gain follower.
- Decouple the Op-Amp: Solder or breadboard the 100nF X7R decoupling capacitor directly across Pin 8 (VCC) and Pin 3 (GND) of the DIP chip. Skipping this is the #1 cause of erratic ADC readings in embedded electronics projects.
- First-Power Verification: Before uploading code, power the board and use a multimeter to verify Pin 8 of the MCP6002 reads exactly 3.3V relative to GND. Check the op-amp output (Pin 1); with GPIO 5 floating or LOW, it should read near 0V. If it reads 1.6V or oscillates, your unused op-amp channel is floating.
ESP32-S3 Firmware & Error Handling
This firmware targets the ESP32-S3-DevKitC-1. It outputs a 5Hz square wave (period = 200ms) to allow the 1ms time constant circuit to fully charge and discharge. It samples the ADC continuously during the rising edge to map the exponential curve, outputting CSV data for the Arduino Serial Plotter.
#include <Arduino.h>
#include <driver/adc.h>
#include <esp_adc_cal.h>
// --- PIN DEFINITIONS ---
#define PWM_OUT_PIN 5
#define ADC_IN_PIN 4
#define ADC_CHANNEL ADC1_GPIO4_CHANNEL
// --- THEORY CONSTANTS ---
const float R_OHMS = 10000.0;
const float C_FARADS = 0.0000001; // 100nF
const float THEORETICAL_TAU_MS = (R_OHMS * C_FARADS) * 1000.0; // 1.0 ms
esp_adc_cal_characteristics_t adc_chars;
void setup() {
Serial.begin(115200);
delay(1000); // Allow serial monitor to connect
// Characterize ADC for accurate voltage translation
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 0, &adc_chars);
// Configure PWM for step-response generation (5Hz, 50% duty)
pinMode(PWM_OUT_PIN, OUTPUT);
ledcSetup(0, 5, 8); // Channel 0, 5Hz, 8-bit resolution
ledcAttachPin(PWM_OUT_PIN, 0);
ledcWrite(0, 128); // 50% duty cycle
Serial.println("Time(us), Voltage(mV), Theoretical_63p2(mV)");
}
void loop() {
// Wait for the PWM cycle to reset (approximate sync via delay)
// In a production tool, you would use an interrupt on the rising edge.
delay(100);
uint32_t start_us = micros();
uint32_t target_mv = 3300 * 0.632; // 63.2% of 3.3V
// Capture the charge curve for 5ms (5x Tau)
while((micros() - start_us) < 5000) {
uint32_t adc_raw = analogRead(ADC_IN_PIN);
uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(adc_raw, &adc_chars);
uint32_t elapsed_us = micros() - start_us;
// Error Handling: Check for ADC Saturation or GND Fault
if (adc_raw > 4050 || adc_raw < 10) {
Serial.println("ERROR: ADC Saturation or GND Fault Detected. Check Buffer Wiring.");
delay(2000);
return;
}
Serial.print(elapsed_us);
Serial.print(",");
Serial.print(voltage_mv);
Serial.print(",");
Serial.println(target_mv);
// Small delay to prevent flooding the serial buffer (yields ~200 samples)
delayMicroseconds(20);
}
Serial.println("---CYCLE_END---");
delay(50); // Brief pause before next cycle
}
Debugging: The "Brownout" Error
When uploading this code or running high-speed ADC sampling, the most common fatal error string you will see in the serial monitor is:
Brownout detector was triggered
ets_main.c 371
Rebooting...
This is not a code bug; it is a hardware power-collapse issue. The ESP32's Wi-Fi radio and ADC sampling circuits draw transient current spikes. If the voltage at the chip drops below ~2.4V for even a microsecond, the brownout detector resets the CPU to prevent flash memory corruption. Espressif's ADC documentation heavily emphasizes stable power delivery for accurate calibration.
The First Three Things to Check When It Fails:
- USB Cable Voltage Drop: 90% of brownouts are caused by cheap, thin-gauge USB-C cables. Swap to a high-quality, short (under 1 meter) data cable capable of 2A+ delivery. Measure the 5V pin on the DevKit with a multimeter while the code runs; if it dips below 4.7V, your cable is the culprit.
- Missing Op-Amp Decoupling: If the MCP6002 lacks its 100nF bypass capacitor, the op-amp will pull high-frequency transient current directly through the ESP32's 3.3V regulator, triggering a brownout. Verify the capacitor is physically touching the op-amp pins.
- GPIO Strapping Pin Conflicts: Ensure you are not using GPIO 0, 3, 45, or 46 for your PWM output. These are strapping pins on the ESP32-S3. Pulling them low or toggling them during boot can cause the board to enter download mode or trigger erratic power states.
Extending and Simplifying the Build
How to Simplify: If you do not have an op-amp on hand, you can drop the MCP6002 and change the resistor to 1kΩ. This lowers the filter's output impedance enough to drive the ESP32 ADC directly. Trade-off: Your new τ is 0.1ms, requiring much faster ADC sampling (using I2S DMA or ESP-IDF continuous ADC mode) to capture the curve, and your cutoff frequency shifts to 1.59 kHz.
How to Extend: Turn this into an automated component tester. Add an I2C OLED display (SSD1306) and a rotary encoder. Write a routine that measures the exact microsecond timestamp when the ADC crosses the 63.2% threshold, then back-calculate the actual capacitance of the unknown part using $C = \tau / R$. This elevates the circuit from a learning tool into a genuine bench instrument.
FAQ: Common Electronics Projects Questions
What are the best beginner electronics projects for learning AC theory?
The best projects for AC theory involve visualizing phase shift and impedance. Building an RC or RL phase-shift oscillator, or wiring up an audio transformer to measure inductive kickback and flyback diode clamping, forces you to deal with alternating current natively. For microcontroller users, generating a sine wave via an R-2R resistor ladder DAC and measuring the RMS voltage with an analog multiplier IC provides a hands-on masterclass in AC power fundamentals.
How do I debug noisy ADC readings in embedded electronics projects?
Noisy ADC readings usually stem from three sources: high-impedance source loading (fixed with an op-amp buffer), missing analog ground planes (fixed by separating digital and analog ground returns and tying them at a single star point), and switching power supply ripple. If your ESP32 is powered via its onboard AMS1117 switching regulator, expect 10-20mV of high-frequency noise. For precision projects, power the analog sensor network from a dedicated low-dropout (LDO) linear regulator like the HT7333, bypassing the noisy internal switching supply entirely.
Why do my electronics projects keep resetting when I connect a motor or relay?
This is caused by inductive kickback and ground bounce. When you de-energize a relay coil or DC motor, the collapsing magnetic field generates a massive reverse-voltage spike (often hundreds of volts). This spike couples back into your microcontroller's 5V/3.3V rail through shared ground traces, instantly triggering a brownout reset or frying the GPIO pin. Always place a flyback diode (like a 1N4148 or 1N4007) in reverse-parallel across any inductive load, and opt for optocouplers or MOSFET gate drivers to physically isolate the microcontroller's low-voltage logic from the motor's high-current ground return.






