When searching for mini projects to learn PCB design with, most beginners make the same mistake: they pick a purely digital circuit (like a basic LED flasher) or a high-speed RF board (like a custom USB hub). Pure digital teaches you nothing about ground return paths, and high-speed RF will break your heart with impedance matching failures on your first fab run. The ideal beginner PCB must force you to route mixed signals—digital logic, I2C communication, and analog filtering—on a standard 2-layer FR4 stackup.

The undisputed best project for this is an ESP32-C3 PWM-to-DC Digital-to-Analog Converter (DAC) with an RC Low-Pass Filter and I2C OLED readout. This build bridges fundamental AC/DC theory (cutoff frequencies and ripple voltage) with embedded debugging, giving you a functional bench tool when you're done.

1. The Decision Path: Choosing Your PCB Learning Project

Use this decision matrix to select your project. We terminate at the mixed-signal DAC because it provides the highest information gain per square inch of fiberglass.

Project Type Example PCB Skills Learned Verdict
Pure Digital / Power 555 Timer LED Flasher Basic footprint creation, thick power traces. Reject: No analog theory, no ground plane strategy.
High-Speed / RF ESP32-S3 Custom USB-C Board Differential pairs, impedance control, 4-layer vias. Reject: Too complex for a first fab; high failure rate.
Pure Sensor I2C BME280 Weather Node I2C pull-ups, decoupling caps, MCU routing. Good, but lacks analog signal conversion theory.
Mixed-Signal DAC ESP32-C3 PWM RC Filter Analog/digital ground separation, RC filter routing, ADC feedback. DEFAULT PICK: Perfect balance of theory and layout.

2. Circuit Theory: PWM-to-DC Conversion & RC Filters

To turn a digital PWM square wave into a smooth DC voltage, we use a passive RC (Resistor-Capacitor) low-pass filter. The microcontroller outputs a 3.3V square wave at a specific duty cycle. The filter averages this out, but leaves a residual AC "ripple" voltage.

The cutoff frequency ($f_c$) of our filter dictates what gets through. The formula is:

$f_c = \frac{1}{2 \pi R C}$

Worked Numeric Example:
If we select a 10kΩ resistor and a 100nF (0.1µF) capacitor:
$f_c = \frac{1}{2 \pi \times 10,000 \times 0.0000001} = 159.15 \text{ Hz}$

To get a clean DC signal, your PWM frequency must be at least 10x to 100x higher than the cutoff frequency to adequately attenuate the fundamental switching harmonic. We will configure the ESP32-C3 to output a 5,000 Hz (5kHz) PWM signal. Because 5kHz is roughly 31 times higher than our 159Hz cutoff, the filter will aggressively knock down the AC ripple, leaving a steady DC voltage proportional to the duty cycle.

💡 PCB Layout Tip: Dielectric Selection
When sourcing your 100nF capacitor for the BOM, specify X7R dielectric for general DC control. If you plan to extend this project to pass audio signals later, swap to an NP0/C0G dielectric. X7R ceramics exhibit piezoelectric microphonics (they act as tiny microphones and generate voltage when vibrated), which ruins audio fidelity but is irrelevant for a DC control voltage.

3. Hardware BOM & Pin Mapping

This parts list uses standard 0805 SMD footprints. 0805 is the sweet spot for hand-soldering with a standard iron and tweezers, avoiding the microscopic frustration of 0402 components while still teaching you SMD pad design in KiCad or Altium.

Component Value / Variant Footprint Notes
Microcontroller ESP32-C3 SuperMini Castellated Headers Ensure you buy the "SuperMini" variant (single row headers), not the full DevKit.
Resistor (R1) 10kΩ ±1% 0805 SMD Forms the 'R' in the RC filter.
Capacitor (C1) 100nF (0.1µF) X7R 0805 SMD Forms the 'C' in the RC filter. Place physically close to R1.
Display SSD1306 0.96" OLED 4-pin 2.54mm Header I2C variant (VCC, GND, SCL, SDA). Do not buy the SPI variant.
Pull-up Resistors 4.7kΩ 0805 SMD Required for I2C SDA/SCL lines to ensure clean rise times.

Pin Mapping Table

The ESP32-C3 has specific strapping pin constraints. Never use GPIO 0 for analog reads; it has an internal pull-up active during boot that will skew your ADC readings. We use GPIO 1 for the ADC feedback.

ESP32-C3 Pin Function Destination
GPIO 2 PWM Output Connects to R1 (Input side of RC filter)
GPIO 1 ADC Input (12-bit) Connects to C1 (Output side of RC filter)
GPIO 6 I2C SDA OLED SDA + 4.7kΩ pull-up to 3.3V
GPIO 7 I2C SCL OLED SCL + 4.7kΩ pull-up to 3.3V

4. Firmware & Error Handling

The following code targets the ESP32C3 Dev Module board variant in the Arduino IDE (ensure you have the Espressif ESP32 core v3.0+ installed). It generates a 50% duty cycle PWM wave, reads the filtered DC voltage back via the ADC, and displays it on the OLED. It includes robust I2C initialization error handling.

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// --- PIN DEFINITIONS ---
#define PWM_PIN     2    // Output to RC Filter
#define ADC_PIN     1    // ADC reading from RC Filter (Avoid GPIO 0!)
#define I2C_SDA     6
#define I2C_SCL     7

// --- DISPLAY CONFIG ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET   -1
#define SCREEN_ADDRESS 0x3C

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// --- PWM CONFIG ---
const int pwmFreq = 5000;    // 5kHz (well above 159Hz RC cutoff)
const int pwmResolution = 12; // 12-bit (0-4095)

void setup() {
  Serial.begin(115200);
  delay(500);

  // Initialize I2C with explicit pins for ESP32-C3
  Wire.begin(I2C_SDA, I2C_SCL);

  // Initialize OLED with Error Handling
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed or I2C bus error!"));
    // Blink onboard LED or halt safely
    while(true) { 
      delay(1000); 
    }
  }
  
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.display();

  // Initialize PWM using modern ESP32 Arduino Core v3.x API
  ledcAttach(PWM_PIN, pwmFreq, pwmResolution);
  
  // Set initial duty cycle to 50% (2048 out of 4095)
  ledcWrite(PWM_PIN, 2048);
  
  // Configure ADC pin
  analogReadResolution(12); // Match 12-bit PWM resolution
  pinMode(ADC_PIN, INPUT);
}

void loop() {
  // Read the filtered DC voltage (12-bit ADC: 0-4095 maps to 0-3.3V)
  int rawAdc = analogRead(ADC_PIN);
  float voltage = (rawAdc / 4095.0) * 3.3;
  
  // Calculate expected voltage based on PWM duty cycle
  int currentDuty = ledcRead(PWM_PIN);
  float expectedV = (currentDuty / 4095.0) * 3.3;

  // Update OLED
  display.clearDisplay();
  display.setCursor(0,0);
  display.println("ESP32-C3 PWM DAC");
  display.println("----------------");
  
  display.print("Expected: ");
  display.print(expectedV, 2);
  display.println(" V");
  
  display.print("Measured: ");
  display.print(voltage, 2);
  display.println(" V");
  
  display.print("ADC Raw:  ");
  display.println(rawAdc);
  
  display.display();
  
  Serial.printf("Exp: %.2fV | Meas: %.2fV | Raw: %d\n", expectedV, voltage, rawAdc);
  delay(250);
}

Debugging: First 3 Things to Check When It Fails

If your serial monitor spits out the exact error string: [E][Wire.cpp:422] requestFrom(): i2cWriteReadNonStop returned Error -1 or the display remains black, follow this ranked troubleshooting path:

  1. Missing I2C Pull-ups (Most Likely): The ESP32-C3 internal pull-ups are too weak (~45kΩ) for reliable I2C at 400kHz. Verify your 4.7kΩ SMD pull-up resistors are soldered correctly between SDA/SCL and the 3.3V rail. Measure with a multimeter; you should read ~4.7kΩ from pin to VCC.
  2. Wrong I2C Address: Many cheap SSD1306 modules are shipped with address 0x3C, but some use 0x3D. Run an I2C scanner sketch. If it returns 0x3D, change SCREEN_ADDRESS in the code.
  3. Strapping Pin Interference: If your measured voltage is stuck at ~3.3V regardless of PWM duty cycle, check if you accidentally routed the ADC to GPIO 0, GPIO 2, GPIO 8, or GPIO 9. These are strapping pins with boot-time pull-ups/pull-downs that will destroy your analog readings. Stick to GPIO 1, 3, 4, or 5 for ADC.

5. Extending or Simplifying the Build

Once your 2-layer PCB arrives from the fab house and you've verified the baseline circuit, you have clear paths to modify the project based on your bench needs.

🛠️ How to Simplify (If you lack SMD soldering skills):
Drop the SSD1306 OLED entirely. Remove the Wire and Adafruit libraries from the code, and rely solely on the Arduino IDE Serial Plotter (Tools > Serial Plotter). This reduces your BOM cost by $4, eliminates the I2C routing requirement, and lets you focus purely on the PWM-to-DC analog trace routing.

How to Extend (The Next Step Up):
A passive RC filter has a high output impedance (roughly equal to R1, so 10kΩ). If you connect this to a low-impedance load (like a motor driver or a 1kΩ resistor), the voltage will sag drastically due to the voltage divider effect. To fix this, extend your PCB layout to include an LM358 dual op-amp configured as a unity-gain voltage buffer. Route the output of the RC filter into the non-inverting input (+) of the op-amp, and tie the output directly to the inverting input (-). This drops your output impedance to near zero, allowing your custom DAC to drive real-world analog loads without the voltage collapsing.

Order your boards, stick to 10mil trace widths for your signal lines, pour a solid ground plane on the bottom layer, and start soldering. For further reading on ESP32-C3 pin constraints, consult the Espressif ESP32-C3 Datasheet, and for a deeper mathematical breakdown of passive filtering, review SparkFun's PWM Tutorial.