If you are searching for electrical engineering projects for beginners that actually teach circuit theory rather than just copy-paste coding, you need to bridge the digital and analog domains. The best way to do this is by building an automated RC (Resistor-Capacitor) Time Constant and Cutoff Frequency Analyzer. Using the ESP32's internal Digital-to-Analog Converter (DAC) and Analog-to-Digital Converter (ADC), this project injects a voltage step into a passive low-pass filter and measures the transient step response to calculate the exact capacitance and -3dB cutoff frequency in real-time.

This build targets the ESP32-WROOM-32E (38-pin DevKit V1). It requires no external I2C sensors, no complex libraries, and costs under $8 in components, making it the definitive bench exercise for understanding impedance, transient response, and microcontroller ADC sampling.

Project Decision Matrix: Why This Build?

Most beginner embedded projects focus entirely on digital protocols (I2C/SPI) or simple GPIO toggling. Here is a decision framework to help you choose the right project based on your learning goals, terminating in the RC Analyzer for pure electrical engineering fundamentals.

Project Type Analog Theory Depth Hardware Cost Primary Skill Learned Verdict
LED Fader (PWM) Low (Digital only) < $5 GPIO timing, basic loops Too simple for EE theory
I2C Weather Station None (Digital protocol) $10 - $15 Bus arbitration, API parsing Great for IoT, poor for circuits
ESP32 RC Analyzer High (Transient/AC) $6 - $8 RC time constants, ADC sampling, dielectric behavior WINNER: Best EE fundamentals

Hardware Spec Sheet & Parts List

Precision matters when measuring analog transients. Do not substitute the capacitor type listed below; the dielectric material fundamentally alters the charge curve.

  • Microcontroller: ESP32-WROOM-32E DevKit V1 (38-pin). Warning: Do not use the ESP32-S3, ESP32-C3, or ESP32-C6. These newer variants lack the internal DAC on GPIO25/26 required for this build (Espressif DAC Docs).
  • Resistor: 10kΩ 1% Tolerance, 1/4W Metal Film (e.g., Vishay MFR-25). Carbon composition or 5% tolerance resistors will introduce unacceptable error into the time constant calculation.
  • Capacitor: 100nF (0.1µF) C0G/NP0 Ceramic Capacitor (e.g., KEMET or Murata). Critical: Avoid X7R or Y5V dielectrics. X7R capacitors exhibit severe voltage coefficient and dielectric absorption, which will distort the exponential charge curve and ruin your step-response measurements (SparkFun Capacitor Guide).
  • Miscellaneous: Half-size breadboard, 22 AWG solid core jumper wires, 5V/2A USB-C data cable.

Pin Mapping & Wiring Steps

The circuit relies on the ESP32's internal DAC to generate a clean DC voltage step, and the ADC to sample the capacitor's charging curve.

ESP32 Pin Function Connection Target
GPIO 25 DAC Output (Channel 1) Resistor (Lead 1)
GPIO 34 ADC Input (Channel 6) Resistor (Lead 2) & Capacitor (Lead 1)
GND System Ground Capacitor (Lead 2)
Callout Tip: Stray Capacitance
At 10kΩ and 100nF, your time constant is 1ms. Breadboards introduce roughly 2pF to 5pF of stray parallel capacitance. This is negligible here (0.005% error), but if you scale this project down to 10pF test capacitors, breadboard parasitics will completely invalidate your measurements. For sub-nanofarad testing, you must solder the components directly to the ESP32 header pins.

The Theory: Step Response and Cutoff Frequency

Instead of sweeping AC frequencies (which requires complex FFT math and is prone to DAC jitter), we use transient step response. When a DC voltage step is applied to an RC low-pass filter, the capacitor voltage $V_c(t)$ follows the equation:

$V_c(t) = V_{in}(1 - e^{-t/\tau})$

Where $\tau$ (Tau) is the time constant, defined as $\tau = R \times C$. At exactly $t = \tau$, the capacitor reaches 63.2% of the final input voltage. By using the ESP32's microsecond timer to measure exactly how long it takes the ADC to read 63.2% of the DAC's maximum output, we can solve for the actual capacitance and the theoretical -3dB cutoff frequency ($f_c = \frac{1}{2\pi\tau}$). This elegantly bridges time-domain transient analysis with frequency-domain AC theory (All About Circuits RC Filters).

Complete ESP32 Firmware (Arduino IDE)

Flash this code using the Arduino IDE. Ensure you have the official esp32 by Espressif Systems board manager package installed (v2.0.14 or newer). Select "ESP32 Dev Module" as your board.

#include <Arduino.h>

// --- PIN DEFINITIONS ---
#define DAC_PIN 25      // Internal DAC Channel 1
#define ADC_PIN 34      // ADC1 Channel 6

// --- COMPONENT VALUES ---
#define R_OHMS 10000.0  // 10k Ohm Resistor
#define TARGET_PERCENT 0.63212 // 1 - 1/e

// --- TIMING & ERROR THRESHOLDS ---
#define TIMEOUT_US 500000 // 500ms max wait for step response
#define ADC_MAX 4095.0    // 12-bit ADC resolution
#define DAC_MAX 255       // 8-bit DAC resolution

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("ESP32 RC Step-Response Analyzer Initialized.");
  
  // Configure ADC attenuation for full 3.3V range
  analogSetPinAttenuation(ADC_PIN, ADC_11db);
}

void loop() {
  // 1. Discharge capacitor completely
  dacWrite(DAC_PIN, 0);
  delay(100); // Wait 100ms for full discharge
  
  // 2. Verify baseline is near zero
  int baseline = analogRead(ADC_PIN);
  if (baseline > 100) {
    Serial.println("ERROR: STEP_RESPONSE_TIMEOUT - Baseline not discharging.");
    Serial.println("Check for shorted capacitor or external pull-up on GPIO34.");
    delay(5000);
    return;
  }

  // 3. Inject voltage step and start timer
  unsigned long startTime = micros();
  dacWrite(DAC_PIN, DAC_MAX); // Step to ~3.3V
  
  // Calculate the target ADC value (63.2% of max)
  float targetVoltageFraction = TARGET_PERCENT;
  int targetADC = (int)(ADC_MAX * targetVoltageFraction);
  
  unsigned long currentTime = micros();
  int currentADC = 0;
  bool stepDetected = false;

  // 4. Poll ADC until target is reached or timeout occurs
  while ((currentTime - startTime) < TIMEOUT_US) {
    currentADC = analogRead(ADC_PIN);
    
    if (currentADC >= targetADC) {
      stepDetected = true;
      break;
    }
    currentTime = micros();
  }

  // 5. Error Handling & Calculation
  if (!stepDetected) {
    Serial.println("ERROR: STEP_RESPONSE_TIMEOUT - Did not reach 63.2% within 500ms.");
    Serial.println("Ranked Causes: 1) Wrong board (ESP32-S3 lacks DAC). 2) Resistor value >> 10k. 3) Open ground connection.");
  } else {
    unsigned long elapsedTime = currentTime - startTime;
    float tau_seconds = elapsedTime / 1000000.0;
    float calculated_C = tau_seconds / R_OHMS;
    float cutoff_freq = 1.0 / (2.0 * PI * tau_seconds);
    
    Serial.print("Time Constant (Tau): ");
    Serial.print(elapsedTime);
    Serial.println(" us");
    
    Serial.print("Calculated Capacitance: ");
    Serial.print(calculated_C * 1000000.0); // Convert to uF
    Serial.println(" uF");
    
    Serial.print("Theoretical Cutoff Freq (-3dB): ");
    Serial.print(cutoff_freq);
    Serial.println(" Hz");
    Serial.println("-----------------------------------");
  }
  
  delay(2000); // Pause before next measurement cycle
}

Debugging: Analog Gotchas and Error Strings

When working with mixed-signal microcontrollers, digital logic errors are rare, but analog physics will bite you. If your serial monitor throws an error, follow this decision path.

Error: "ERROR: STEP_RESPONSE_TIMEOUT - Did not reach 63.2%"

This means the ADC never saw the voltage cross the ~2580 threshold within 500ms. Ranked Causes:

  1. Wrong Silicon Variant: You are using an ESP32-S3, ESP32-C3, or ESP32-C6. These chips do not have internal DACs. The dacWrite() function fails silently or throws a compilation error depending on the core version. Fix: Verify your board has the original WROOM-32 or WROOM-32E module.
  2. Resistor Value Too High: If you accidentally grabbed a 1MΩ resistor instead of 10kΩ, your time constant becomes 0.1 seconds, but breadboard leakage currents will prevent the capacitor from ever reaching the full 3.3V rail, stalling the charge curve. Fix: Measure your resistor with a multimeter; it should read between 9.9kΩ and 10.1kΩ.
  3. Floating Ground: The ground rail on your breadboard is broken or not connected to the ESP32 GND pin. Fix: Check continuity from the capacitor's ground lead to the ESP32 GND pin.

Error: "ERROR: STEP_RESPONSE_TIMEOUT - Baseline not discharging"

The ADC reads a value > 100 before the DAC even turns on. Ranked Causes:

  1. GPIO34 Pulled High: GPIO34 is an input-only pin on the ESP32. It has no internal pull-down resistor. If it is floating or accidentally touched by a 3.3V jumper, it will saturate. Fix: Ensure GPIO34 is only connected to the RC junction.
  2. Shorted Capacitor: A manufacturing defect or breadboard short is bypassing the capacitor. Fix: Remove the capacitor and test the circuit; the ADC should read near 4095 immediately. Re-insert to confirm the RC delay.
The First 3 Things to Check When It Fails:
  1. Board Variant: Look at the metal shield on the ESP32. It must say "ESP32-WROOM-32". If it says "S3" or "C3", the DAC hardware does not exist.
  2. USB Cable: Ensure you are using a data-sync cable, not a charge-only cable, otherwise the serial monitor will fail to connect and you won't see the analog readouts.
  3. Capacitor Dielectric: If your calculated capacitance varies wildly between runs, you are likely using an X7R ceramic capacitor. Replace it with a C0G/NP0 variant to eliminate dielectric absorption errors.

Extending or Simplifying the Build

Once you have the baseline step-response working, you can scale the complexity to match your current understanding of circuit theory.

To Simplify (The PWM Alternative):
If you do not have an original ESP32 with a DAC, you can replace the dacWrite() function with a high-frequency PWM signal on GPIO2, passed through a hardware 1kΩ/1µF low-pass filter to create a crude analog voltage source. This introduces the concept of PWM ripple and duty-cycle-to-voltage conversion, though it sacrifices measurement precision.

To Extend (Op-Amp Buffering & Bode Plots):
The ESP32's internal DAC has a relatively high output impedance (roughly 1kΩ to 2kΩ depending on the voltage level). This parasitic resistance adds to your 10kΩ test resistor, skewing your calculations by up to 10%. To fix this, insert an MCP6001 or LM358 op-amp configured as a unity-gain voltage buffer between GPIO25 and the 10kΩ resistor. This drops the source impedance to near zero. From there, modify the code to generate a sine-wave lookup table via a hardware timer interrupt, sweep from 10Hz to 10kHz, and plot the amplitude attenuation to generate a full Bode plot directly in the serial plotter.