A professional-grade implementation of a real-time control loop on the ESP32-S3. This project uses high-speed DMA-driven ADC sampling and a dedicated control task to manage high-resolution PWM output with minimal CPU overhead.
The hardware interface is designed in KiCad.
System Schematic |
Hardware 3D View |
| Reference | Value | Package/Footprint | Description |
|---|---|---|---|
| U2 | ESP32-S3-WROOM-1 | Module_ESP32-S3 | Dual-core Wi-Fi/BT MCU |
| C1 | 10 nF | 0805 or 1206 (SMD) | Ceramic Decoupling Capacitor |
| C2 | 100 ΞΌF | Radial Electrolytic | Polarized Bulk Capacitor (Check Polarity!) |
| Cf1 | 0.10 nF | 0805 or 1206 (SMD) | Ceramic Filtering Capacitor |
| R1 | 10 kΞ© | 0805 or 1206 (SMD) | Pull-up Resistor for EN pin |
| Rf1 | 10 kΞ© | 0805 or 1206 (SMD) | Filtering Resistor for ADC input pin |
| RV1 | 10 kΞ© | Potentiometer | Through-hole Breadboard Friendly Pot |
| PWR1 | 2-pin Header | 2.54mm Pitch | Power Input (3.3V / GND) |
| IO1 | 4-pin Header | 2.54mm Pitch | Signal Output (Multimeter/Logic Analyzer) |
Using a Latency Trigger (GPIO toggle inside the ISR and after the PWM update), the control loop timing was verified:
- PWM Frequency: 5 KHz (Configured via LEDC)
- PWM Resolution: 13-bit (0...8191 steps)
- System Latency: ~60 ΞΌs from ADC interrupt to PWM update.
- Filter Type: Single-pole Digital IIR (Exponential Moving Average).
- Update Cadence: 19.53 Hz (Synchronized with DMA ISR).
- Coefficient (Ξ±): 0.1.
- Cutoff Frequency (fc): ~0.35 Hz.
- Effect: Provides -40dB attenuation at 10Hz, effectively eliminating mechanical potentiometer chatter and 50Hz environmental EMI.
The firmware is designed with a "Producer-Consumer" model to ensure that high-frequency sampling does not block the application logic:
- Producer (Core 0/ISR): High-speed ADC sampling using DMA. Data is transferred in batches to a FreeRTOS Queue without CPU intervention.
- Consumer (Core 1): A dedicated task that unblocks upon queue reception, performs IIR filtering, maps the 12-bit ADC range to 13-bit PWM space, and updates the LEDC peripheral.
- Actuation: The LEDC Hardware Fade Engine handles the PWM output, ensuring smooth duty cycle transitions.
To maintain observability, the project includes a debug_pins component that toggles GPIOs at critical execution points. These captures verify the real-time budget.
- Channel 1 (D1): Resulting 5kHz PWM Output.
- Channel 2 (D2): ADC Interrupt Lifecycle.
- Channel 3 (D3): Processing Task execution time.
The core processing logic is validated using the Unity Test Framework. Tests ensure that the mapping remains accurate and that edge cases (like calibration limits) are handled gracefully.
Test Case Example:
ADC to PWM Mapping is linear: Validates that a mid-point ADC value of 2048 results in a PWM duty cycle of 4096.
components/adc_dma: Low-level GDMA configuration for continuous ADC.components/pwm_engine: LEDC configuration for 13-bit precision.components/processing_logic: Math library for fixed-point filtering and mapping.components/debug_pins: GPIO abstraction for logic analyzer instrumentation.test_app: Unit tests for verifying the linear mapping and filter response.
- ESP-IDF v5.5.3
- Unity Test Component (for
test_app) - ESP32-S3 Development Board
- Potentiometer connected to GPIO1 (ADC1_CH0) through a Low Pass Filter (LPF) RC
Toggle debug hardware pins via menuconfig:
idf.py menuconfig
# Navigate to "Debug Configuration" -> "Enable Hardware Debug Toggles"idf.py set-target esp32s3
idf.py build
idf.py flash monitorThis project uses the Unity test framework. To run the math verification:
cd test_app
idf.py build flash monitordoxygen DoxyfileThe system performance was verified using an 8-channel logic analyzer to measure task execution timing and jitter.
The following capture shows the DEBUG_PIN_PWM_TASK behavior. The high duration represents the time taken to process 256 samples and update the duty cycle. The 15 ΞΌs execution time represents only 0.29% of the available 51.2ms processing window, allowing for future expansion into more complex DSP (Digital Signal Processing) or Wi-Fi telemetry without dropping frames.
Measurement of the on_conv_done callback frequency confirms the DMA is filling at 19.53 Hz β (5000 Hz / 256 samples), triggering the processing loop.
This confirms the system remains deterministic within a single 5 kHz PWM period 200 ΞΌs
End-to-end system latency measured at 100 ΞΌsThe 13-bit resolution allows for 8192 discrete steps, providing smooth, high-resolution control of the output power rail. a Β±1% variance is expected due to component tolerances and the IIR filter.
PWM at Different InputsDMA-backed acquisition ensures zero-drift sampling, critical for stable IIR filtering and control loop stability
Interrupt Jitter AnalysisTo evolve this project from a standalone control loop into a full-scale industrial monitoring solution, the following features are planned:
- Objective: Real-time remote monitoring of the ADC input and PWM duty cycle.
- Implementation: Integrate the ESP-RainMaker framework to provide a mobile app interface for viewing live "Forensic" data without needing a physical Logic Analyzer.
- Objective: Automatically detect the noise floor and physical limits of the potentiometer.
- Implementation: Develop a startup calibration routine that records the minimum and maximum raw values over a 5-second window, dynamically updating
calib_minandcalib_maxin NVS (Non-Volatile Storage).
- Objective: Detect mechanical wear in the potentiometer.
- Implementation: Utilize the ESP32-S3's SIMD (Single Instruction, Multiple Data) instructions to perform an FFT on the 256-sample buffer. High-frequency noise spikes during rotation can be used as a "Health Indicator" for the physical component.
- Objective: Prevent "Thermal Runaway" in the power rail.
- Implementation: Add a secondary ADC channel to monitor the temperature of the power MOSFETs, automatically throttling the PWM duty cycle if a thermal threshold is exceeded.
This project is licensed under the MIT License - see the LICENSE.txt file for details.
Copyright (c) 2026 Abanoub Salah.






