Unable to keep sync with measured waveform

So, I think this is a programming question (possibly a math question?), but I am not sure where the problem is.

Basically, I want to create an idealized waveform in code, and compare it to a real waveform read from an ADC. If the measured waveform deviates too far from the idealized waveform, I can trigger an event.

I am reading the waveform using an ADS1115. The waveform is sinusoidal measuring from 0 to 5 volts. I am also using an h11aa1 opto-isolator to generate a pulse during the trough of the waveform which triggers an interrupt on my ESP32. Here is what those signals look like on my scope. Edit: Probe 1 (yellow) is the waveform that's fed into my ADC, and probe 2 (blue) is the pulse generated by the opto-isolator.

As the title mentions, I can't get the measured waveform and the idealized waveform to sync up in the serial plotter. They drift in and out of sync with each other as shown below. Edit: The blue line is the measured wave form, the orange line, is my idealized wave form.



I didn't expect the two waves to perfectly match up (especially without adjustment), but I wasn't expecting them to drift. The frequency of the measured waveform is tied to the grid, so it is pretty stable. I was expecting the idealized value to be consistently out of phase with the measured waveform, because each interrupt should reset/resync the waves.

Does anyone have any idea what I am missing or doing wrong?

Here is the sketch:

#include <ADS1X15.h>

#define WAVEFORM_GAIN 0.0129F
#define WAVEFORM_OFFSET 2.2352D

#define PERIOD (1000000 / (double)60) // Period in microseconds = 1 microsecond over 60 hz
#define SIN_PERIOD (1 / (PERIOD / TWO_PI)) // The period multiplier
#define PEAK_VOLTAGE 173.0D

#define ZERO_CROSS_INTERRUPT_PIN 18
volatile unsigned long _lastZeroCross = 0;

void IRAM_ATTR zeroCrossHandler() {
  // This doesn't detect the zerocross, but instead detects the trough of the wave

  unsigned long cross = (micros() - 1666.6667); // The detection is 500us long centered on the trough. Therefore, the last zero happened 1/4 of a period minus 2,500us
  unsigned long diff = (_lastZeroCross > cross) ? _lastZeroCross - cross : cross - _lastZeroCross; 

  // Each pulse should be very nearly 16,666us apart.
  if (_lastZeroCross == 0 || (diff < 16700 && diff > 16600)) {
    _lastZeroCross = cross;
  }
}

ADS1115 ADS(0x48);

void setupADC() {
  Serial.println("Setting up ADC ... ");

  Wire.begin();
  Wire.setClock(400000);

  ADS.begin();

  ADS.setGain(0);
  ADS.setDataRate(7);
  ADS.setMode(1);

  ADS.readADC(0);  // Forces settings to be applied.

  Serial.println("Complete! Gain: " + String(ADS.getGain()) + " | Sample Rate: " + String(ADS.getDataRate()));
}

void setup() {
  Serial.begin(115200);
  Serial.println("");

  setupADC();

  pinMode(ZERO_CROSS_INTERRUPT_PIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(ZERO_CROSS_INTERRUPT_PIN), zeroCrossHandler, RISING);

  uint32_t freq = getXtalFrequencyMhz();
  Serial.println("");
  Serial.println("Setup Complete! | CPU: " + String(freq) + "Mhz\n");
}

void loop() {
  double value = 0;
  float adc0Voltage = 0;

  // Request value of waveform
  ADS.requestADC(0);
  while (ADS.isBusy()) {
    // block until value is ready
  }

  adc0Voltage = ADS.toVoltage(ADS.getValue());
  if (adc0Voltage > 0.05) {
    value = (adc0Voltage - WAVEFORM_OFFSET) / WAVEFORM_GAIN;
  }

  unsigned long now = micros();
  double elapsedPeriodTime = (now > _lastZeroCross ? now - _lastZeroCross : _lastZeroCross - now);
  double idealizedValue = (PEAK_VOLTAGE * sin(elapsedPeriodTime * (double)SIN_PERIOD));

  Serial.println("variable_1:" + String(value) + ",variable_2:" + String(idealizedValue));

  // This delay allows the time between measurements to be consistent.
  // Without it, the plotter becomes really hard to read
  delayMicroseconds(3000);
}

I would add toggling additional IOpins at different places in your code to see what has a constant difference in time and what variates in time.

And then looking at the signal-change not with the serial monitor but with the oscillosope.

Have you calculated how much time it needs for the serial printing of all the characters?

I dont see that on your image.

so 50Hz?

So the edges will drift if the amplitude changes? Perhaps you should capacitively couple and use zero crossing to eliminate that?

They look perfectly in sync on the scope, so what is the real problem?

I was concerned that the serial plotter might be introducing some anomalies but wasn't sure how to eliminate it. I was originally considering using PWM, but I figured it would be way to slow, but toggling a couple of pins at different places in the wave might be enough though. Thanks.

Sorry, I didn't do a very good job explaining the graphics.
They are perfectly in sync on the scope. Probe 1 (yellow) is what I am trying to read with my ADC. I am also feeding this wave into the opto-isolator to generate the pulses which are shown with probe 2 (blue).

The problem comes in when I am looking at the serial plotter. The values I am creating mathematically, don't seem to align with the values I am reading from my ADC. If I know the amplitude, frequency, and the elapsed time from the start (first zerocross) of my sine wave, I can derive the y-coordinate of the wave at that specific point in time (my idealized value). But when looking at the plotter, I can see the numbers I am mathematically creating don't align correctly with the values I am reading from the ADC.

So, during each loop, I read a value from the ADC and I look at the elapsed time since the last zerocross. Based on the elapsed time, I know what value the ADC should have given me.

My serial output includes both the actual value read from the ADC and my idealized value. So when the plotter plots them, I would expect them to be plotted at the same time index. But maybe it doesn't? Is it reading variable_1, plotting at that time index, then reading variable_2 a moment later and plotting it at a new time index? I think this is what is happening though...

Reading in the value conditionally

calculating the idelizedValue un-conditionally

what happens if you put the calculation inside the if-condition?

How about storing snapshots of micros() before and after these lines of code at multiple places?
or alternatively switching on/off IO-pins not by using digitalWrite() but by direct port-manipulation which is faster executed
and then have a look with the oscilloscope how much time is between the IO-pin switching?

You don't need to cast the variables value and idealiesValue to strings
assign everything to a single but big enough char-array and print the char-array

Hi @kirk_berkley,

how would the graphics look if the measured signal had a slightly different frequency compared to the theoretical signal?

This statement will cause your sampling rate to be completely non-uniform.

Interesting. Why is that?

String(value) will result in a different number of characters to be printed. Example 0.00 (4 characters, 100.00 (6 characters).
println will also block if the buffer is full.
Also not sure but String() may take a varying amount of time.

I'm not sure what is reason of the drift, it's strange, but there are some things that could produce problems:

In the interrupt handler:

  if (_lastZeroCross == 0 || (diff < 16700 && diff > 16600)) {
    _lastZeroCross = cross;
  }

You shouldn't do that this way. You are testing against the assumption that diff should be in that margin. But what if not? you are just silently ignoring it and skipping a whole cycle.

I would do just this in the handler:

volatile unsigned long _lastZeroCross = 0;
void IRAM_ATTR zeroCrossHandler() {
  // This doesn't detect the zerocross, but instead detects the trough of the wave
  _lastZeroCross = micros() - 1666.6667;
}

That's all you need.
Eventually you could add the test and toggle a digital pin if it fails, something that in theory should never happen, and monitor the pin with the scope.

Then in the loop, read the ADC, get the time and calculate the micros elapsed since the last zero crossing, that was always in the past:

now = micros();
elapsedPeriodTime = now - _lastZeroCross;  // always positive
idealizedValue = (PEAK_VOLTAGE * sin(elapsedPeriodTime * (double)SIN_PERIOD));

Then, with the delay you are adjusting the sampling rate. You could also toggle another digital pin at each loop cycle, to make sure that it is sampling as you expect, with the correct and uniform rate, not skipping or overflowing some cycles, etc.

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