Arduino Waveform Generator Frequency Problem

Hello everyone, I have this code here which I'm trying to fix as it's not giving the set frequency on the oscilloscope. I am new to Arduino and don't have much experience with it. This is for a project and most of this code is AI generated with a few exceptions.

Most of the code works properly as it's supposed with all the buttons and LEDs on the breadboard, it generates the waveforms (sine, saw and rectangle) but only at the wrong frequency.

For example if I put it at 1000 Hz, it generates at 180-ish Hz, I presume there's something wrong with the way it calculates the samples or something.

If anyone could help me fix it, it would be greatly appreciated.

I'm using an Arduino UNO R4 Minima for testing purposes as it has a built-in DAC on the A0 pin, but for the project I would need to use either the Uno R3 or the Nano which don't have a built in DAC.

For the Uno R3 and Nano I would need an RC filter so the PWM signal could be converted into analog signal on the output. Any guide on how to create an RC filter would also be appreciated.

#include <Wire.h>
#include <PWM.h>
#include <LiquidCrystal_I2C.h>
#include <math.h>  // For the sin() function

// LCD settings (I2C address 0x27)
LiquidCrystal_I2C lcd(0x27, 16, 2);

#define MAX_FREQUENCY 20000  // Maximum frequency in Hz (20 kHz)

// DAC settings
#define DAC_PIN A0  // DAC pin on Arduino Uno R4

// Button settings
const int buttonShape = 2; // Button for changing waveform
const int buttonFreq = 3;  // Button for increasing frequency

// LED settings
const int ledShape = 4;  // LED for waveform change
const int ledFreq = 5;   // LED for frequency change

// Variables for waveforms and frequency
volatile int currentWaveform = 0; // 0: sine, 1: sawtooth, 2: square

// Variables
int frequency = 1000;              // Initial frequency
const int freqStep = 500;         // Step for increasing frequency

// Other variables
int i = 0;
long sampleTime;

// Debouncing variables
unsigned long lastDebounceTimeShape = 0;
unsigned long lastDebounceTimeFreq = 0;
const unsigned long debounceDelay = 200; // Increased debounce delay in milliseconds

// Button functions (without interrupts)
void changeWaveform() {
  unsigned long currentTime = millis();
  if (currentTime - lastDebounceTimeShape > debounceDelay) {
    currentWaveform++;
    if (currentWaveform > 2) currentWaveform = 0; // Cycle through available waveforms
    lastDebounceTimeShape = currentTime; // Update last press time
    updateLCD(); // Update LCD
    logSerial(); // Print to Serial Monitor
    digitalWrite(ledShape, HIGH);  // Turn on LED for waveform
  }
}

void increaseFrequency() {
  unsigned long currentTime = millis();
  if (currentTime - lastDebounceTimeFreq > debounceDelay) {
    frequency += freqStep;
    if (frequency > MAX_FREQUENCY) frequency = freqStep; // Reset to minimum frequency
    lastDebounceTimeFreq = currentTime; // Update last press time
    updateLCD(); // Update LCD
    logSerial(); // Print to Serial Monitor
    digitalWrite(ledFreq, HIGH);   // Turn on LED for frequency
  }
}

void turnOffLEDs() {
  // Turn off LEDs when buttons are released
  if (digitalRead(buttonShape) == HIGH) {
    digitalWrite(ledShape, LOW);
  }
  if (digitalRead(buttonFreq) == HIGH) {
    digitalWrite(ledFreq, LOW);
  }
}

// LCD update function
void updateLCD() {
  lcd.clear(); // Clear the LCD before showing new text
  
  lcd.setCursor(0, 0);
  switch (currentWaveform) {
    case 0: lcd.print("Sine Wave  "); break;  // Added space to shorten text
    case 1: lcd.print("Sawtooth   "); break;
    case 2: lcd.print("Square     "); break;
  }

  lcd.setCursor(0, 1);
  lcd.print("Freq: ");
  lcd.print(frequency);
  lcd.print(" Hz  ");  // Added space to avoid overlapping
}

// Serial Monitor log function
void logSerial() {
  Serial.print("Waveform: ");
  switch (currentWaveform) {
    case 0: Serial.print("Sine"); break;
    case 1: Serial.print("Sawtooth"); break;
    case 2: Serial.print("Square"); break;
  }
  Serial.print(", Frequency: ");
  Serial.print(frequency);
  Serial.println(" Hz");
}

void setup() {
  // LCD initialization
  lcd.init();
  lcd.backlight();

  // Pin settings
  pinMode(buttonShape, INPUT_PULLUP);
  pinMode(buttonFreq, INPUT_PULLUP);
  pinMode(ledShape, OUTPUT);  // Set LED pin as output
  pinMode(ledFreq, OUTPUT);   // Set LED pin as output

  // Serial communication for input
  Serial.begin(9600);

  // Initial LCD display
  updateLCD();
  logSerial(); // Initial print to Serial Monitor
}

void loop() {
  // Check button and LED status
  if (digitalRead(buttonShape) == LOW) {
    changeWaveform();
  }
  if (digitalRead(buttonFreq) == LOW) {
    increaseFrequency();
  }

  // Turn off LEDs if buttons are released
  turnOffLEDs();

  // Calculate sample time based on current frequency
  sampleTime = 1000000 / (frequency * 100); // 100 samples per cycle (100 Hz for 10kHz)

  // Generate waveform
  generateWaveform();

  // Precise delay to achieve the desired frequency
  delayMicroseconds(sampleTime);  // Set delay between samples
}

void generateWaveform() {
  int value = 0;

  // Generate sine wave
  if (currentWaveform == 0) {
    value = (sin(2 * PI * i / 100) * 127 + 128);  // Sine wave with offset
  }
  // Generate sawtooth wave
  else if (currentWaveform == 1) {
    value = (i % 100) * 255 / 100;  // Sawtooth wave
  }
  // Generate square wave
  else if (currentWaveform == 2) {
    value = (i < 50) ? 255 : 0;  // Square wave
  }

  // Generate waveform on DAC
  analogWrite(DAC_PIN, value);

  i++;
  if (i == 100) i = 0;  // Reset sample indices after one cycle
}

You calculate the time period that is necessary between samples to produce your waveform at a particular frequency :

// Calculate sample time based on current frequency
  sampleTime = 1000000 / (frequency * 100); // 100 samples per cycle (100 Hz for 10kHz)

and then you have a line that has a delay for that length of time:

// Precise delay to achieve the desired frequency
  delayMicroseconds(sampleTime);  // Set delay between samples

But you have not allowed for the time it takes to go round loop().
That will take several microseconds.

So the time between each sample being used is not what you have calculated it to be, but a longer time, resulting in too low a frequency.

This produces a (very) incorrect result. Try printing sampleTime to verify

Use something like

sampleTime = 1000000UL / (frequency * 100UL);

why not use a DDS module, e.g. Arduino-Controlled-AD9833-Function-Generator?

Unfortunately, I can't use a DDS for this project.

How are you changing the PWM? Or is it fixed at some percentage?

-jim lee

Idle curiousity. Why?

It's just not possible as the theme of the project requires me to make the board itself produce waveforms, not by using external waveform generators and then just controlling them through the Arduino itself.

I think you are being too ambitious trying to generate three different waveforms at up to 20kHz using an Arduino Uno R4 Minima.

To generate a waveform at a certain frequency, you can use the techniques shown in the IDE Example 'BlinkWithoutDelay' to do the timing, but using micros() instead of millis() to get the required frequency.

Here is some code (based on yours, but with a lot removed) that will generate a sinewave of the required frequency.
I've set the frequency as a constant, so you can't change it in this demo without recompiling.
However it will only work correctly up to around 100Hz.

Code
#include <digitalWriteFast.h>

int DAC_PIN = A0;
int trigPin = 12;                     // Pin used to trigger oscilloscope
unsigned int frequency = 100;         // Desired frequency (Hz)                                                                        ;
unsigned long previousMicros = 0;     // Used for timing
unsigned long sampleTime = 0;         // Time between each sample
int numSamples = 100;                 // Number of samples in one cycle of the waveform
int value = 0;
int i = 0;

void setup() {
  Serial.begin(9600);
  pinMode(trigPin, OUTPUT);

  // Calculate sample time based on desired frequency
  sampleTime = 1000000 / (frequency * numSamples);

  Serial.print("Desired Frequency: ");
  Serial.print(frequency);
  Serial.print("Hz,   sampleTime: ");
  Serial.print(sampleTime);
  Serial.print("µs,   Frequency: ");
  Serial.print(1000000 / (sampleTime * numSamples));
  Serial.println("Hz");
}

void loop() {

  unsigned long currentMicros = micros();
  if (currentMicros - previousMicros >= sampleTime) {
    previousMicros = previousMicros + sampleTime;

    // Generate sine wave
    value = (sin(2 * PI * i / numSamples) * 127 + 128);

    // Generate waveform on DAC
    analogWrite(DAC_PIN, value);

    i++;

    // Reset sample indices after one cycle,
    // and generate oscilloscope trigger signal
    if (i == numSamples) {
      digitalWriteFast(trigPin, HIGH);
      i = 0;
      digitalWriteFast(trigPin, LOW);
    }
  }
}
Oscilloscope trace: 100Hz

Repeatedly calculating the amplitude of the signal in loop() is time consuming, limiting the maximum frequency that can be generated.
A faster method would be to calculate the sinewave values once in setup() and store them in an array. Then use the ready calculated values in loop() to send to the DAC.

Code
#include <digitalWriteFast.h>

int DAC_PIN = A0;
int monitorPin = 12;                // Pin used to trigger oscilloscope
unsigned int frequency = 500;       // Desired frequency                                                                        ;
unsigned long previousMicros = 0;   // Used for timing
unsigned long sampleTime = 0;       // Time between each sample
int numSamples = 100;               // Number of samples in one cycle of the waveform
int value[100];                     // array to hold sinewave values
int i = 0;

void setup() {
  Serial.begin(9600);
  pinMode(monitorPin, OUTPUT);

  // Calculate sample time based on desired frequency
  sampleTime = 1000000 / (frequency * numSamples);

  Serial.print("Desired Frequency: ");
  Serial.print(frequency);
  Serial.print("Hz,   sampleTime: ");
  Serial.print(sampleTime);
  Serial.print("µs,  Frequency: ");
  Serial.print(1000000 / (sampleTime * numSamples));
  Serial.println("Hz");

  // Generate sine wave and populate array
  for (int i = 0; i < numSamples; i++) {
    value[i] = (sin(2 * PI * i / numSamples) * 127 + 128);
  }
}

void loop() {

  unsigned long currentMicros = micros();
  if (currentMicros - previousMicros >= sampleTime) {
    previousMicros = previousMicros + sampleTime;

    // Generate waveform on DAC
    analogWrite(DAC_PIN, value[i]);

    i++;

    // Reset sample indices after one cycle,
    // and generate oscilloscope trigger signal
    if (i == numSamples) {
      digitalWriteFast(monitorPin, HIGH);
      i = 0;
      digitalWriteFast(monitorPin, LOW);
    }
  }
}
Oscilloscope trace: 500Hz

You can get the code to work at higher frequencies by reducing the number of samples per cycle of the waveform.

Oscilloscope trace: 2.5kHz

Using an Arduino Uno R4, it will not be possible to generate a 20kHz sinewave, as it takes at least 12µs to update the DAC output.

I really appreciate your input and the work you've done to make this. I've only put 20 KHz as placeholder as I didn't know what else to put.

The max frequency (as a fixed number) doesn't really matter, it just has to generate the required waveform and allow switching the frequency up to the maximum level the board itself can produce in my code.

Another way to synthesize a waveform is by using phase accumulation.

Rather than publish every calculated (or stored) value of a waveform stepping through at a variabke rate , an index is manipulated to run through the waveform at a constant rate as far as the step-taking is concerned.

This means you always have N samples per second regardless of the frequency.

Any waveform can be stored, of course you will be limited to the speed with which you can make a loop go.

Here

is a video that will lead you to an implementation that manages over 9000 samples per second, which in practical terms might make waveforms whose maximum frequency component was 3000 Hz practical.

I used ~7000 Hz loop rate (143 ms per sample) and the 2600 Hz sine wave it generated was very pretty.

The sine wave was 256 samples of 8 bits.

R-2R ladders are sometimes shown. Theoretically yes, practically no. Get a good D/A chip or chip on a module; you'll still want a bit of post-prandial signal conditioning.

a7

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.