88. Triangular Wave Generator

As per the given task, we need to generate a triangular waveform using DAC

In this task, we will be using the ESP32’s built-in DAC to generate the analog triangular waveform.

Additionally, we need to control the amplitude and frequency of the waveform using two potentiometers.

To achieve this, the potentiometers will be connected to two different ADC channels (GPIO pins) of the ESP32.

Based on their values, the waveform will change amplitude and frequency changes in real time

Hardware Connections

  1. Connect Potentiometer 1 to GPIO34 (ADC channel) for amplitude control
  2. Connect Potentiometer 2 to GPIO35 (ADC channel) for frequency control
  3. Connect oscilloscope to GPIO25 (DAC_PIN) to observe output
  4. Power the ESP32 via USB

Circuit connection


Firmware

We are generating a smooth triangle waveform using the ESP32's built-in DAC. And the shape and speed of the wave adjust in real-time using two potentiometers:

  • One controls the amplitude.
  • The other controls the frequency (how fast it goes up and down).

The waveform is output as an analog voltage signal on GPIO25 (DAC1).

Code

#define DAC_PIN 25       // GPIO25 = DAC1
#define AMP_POT_PIN 34   // Pot 1 for amplitude
#define FREQ_POT_PIN 35  // Pot 2 for frequency

// Global variables
int amplitude = 128;    // Default amplitude (mid-range)
float frequency = 250;  // Default frequency (mid-range)
unsigned long lastPotReadTime = 0;
const unsigned long POT_READ_INTERVAL = 100000;  // Read pots every 100ms

// Wave generation variables
int currentValue = 0;
bool rising = true;
unsigned long lastStepTime = 0;
long delaySteps = 0;

int readAvg(int pin, int samples = 5) {  // Reduced samples for speed
  int sum = 0;
  for (int i = 0; i < samples; i++) sum += analogRead(pin);
  return sum/samples;
}

void setup() {
  Serial.begin(115200);
  analogReadResolution(12);
}

void loop() {
  unsigned long currentTime = micros();

  // 1. Read pots every 100ms (non-blocking)
  if (currentTime - lastPotReadTime >= POT_READ_INTERVAL) { 
    lastPotReadTime = currentTime;
    int ampRaw = readAvg(AMP_POT_PIN);
    int freqRaw = readAvg(FREQ_POT_PIN);

    amplitude = map(ampRaw, 0, 4095, 1, 255);   // Ensure amplitude ≥ 1
    frequency = map(freqRaw, 0, 4095, 1, 500);  // Target: 1Hz–500Hz

    // Calculate the delay between each DAC step in microseconds.
    // Formula: delaySteps = 1000000L (1 second in µs) / total number of DAC steps per waveform.
    // A full triangle wave has 2 * amplitude steps (rising + falling).
    // So, for a given frequency and amplitude, the total time per waveform is 1/frequency,
    // and each DAC step must occur at:
    // delaySteps = 1,000,000 µs / (2 * amplitude * frequency)
    delaySteps = 1000000L / (2 * amplitude * frequency);
  }

  // 2. Generate a triangle wave (non-blocking)
  if (currentTime - lastStepTime >= delaySteps) {
    lastStepTime = currentTime;

    dacWrite(DAC_PIN, currentValue);

    if (rising) {
      currentValue++;
      if (currentValue >= amplitude) rising = false;
    } else {
      currentValue--;
      if (currentValue <= 0) rising = true;
    }
  }
}

Code Explanation

readAvg(): This function reads the ADC value 5 times & returns the average of the ADC values.

analogReadResolution(12): Set ADC resolution to 12-bit.

loop() :

  • if (currentTime - lastPotReadTime >= POT_READ_INTERVAL) : This checks if 100,000 microseconds have passed.
  • We check the potentiometers 10 times a second.
  • The raw 12-bit values are mapped to useful ranges:
    • Amplitude: 1–255 (we avoid 0 to prevent math issues).
    • Frequency: 1– ~500 Hz.
  • We calculate delaySteps using:
    delaySteps = 1000000 / (2 * amplitude * frequency);
  • This ensures the total wave (up + down) takes the right amount of time for the chosen frequency.

Generating the Triangle Wave

  • Every delaySteps microseconds, we either:
    • Increase currentValue by 1 if we’re going up, or
    • Decrease it by 1 if we’re going down.
  • When we hit the peak (amplitude) or bottom (0), we flip the direction using the rising flag.
  • The updated currentValue is sent to the DAC via dacWrite().


Output

Video

Submit Your Solution

Note: Once submitted, your solution goes public, helping others learn from your approach!