Help with discrete integral, returns offset AGAIN

Hi to everybody heres my problem:
The code performs the derivative and discrete integral of an analog input. The result of the derivative of the input "D" has values with a mean of 0 because when deriving a function, for example, v(t)=2.5+2.5sin⁡(2πt)v(t) V, it returns its derivative without the 2.5V offset value. When integrating the derivative, it should return the original signal WITHOUT the 2.5V (512 in ADC value aprox.), which does not happen, the code returns the original signal with the offset again.
Why does this happen, and how can it be fixed? Am I wrong?
Thank you for your attention.

float D=0.0,D1=0.0,v=0.0,v1=0.0,Tk=20.0, msT=50.0;
float I=0.0,I1=0.0,T=0.05,Tdiv2=0.025;
const int in=1;
unsigned long tf=0,ti=0,t=0;

void setup() {
  Serial.begin(9600);
}

void loop() {
  tf=millis();
  t=tf-ti;
  if(t>=msT){
    ti=tf;
    v1=v;
    I1=I;
    D1=D;
    v=analogRead(in);
    D=(v-v1)*Tk;
    I=I1+(Tdiv2*(D+D1));
  }
    Serial.println(I,8);
}

Hi, I do not see any error in your code, except for perhaps an initial value issue, as millis() starts from the program start. The first tick length might be quite wrong, and the previous analogue read value is probably quite different from your declared initial v=0 value.

I have tried to rewrite your code, with a first reading of the of values in the setup() section but I'm missing time so set up a system to test it out just now.

This might give you some clues ...

// derivation and integration example 2025 feb 16
const pin_size_t in = 1;       // Analog input pin
const unsigned long msT = 50;  // desired sampling time in [ms]

const float Tk = 1000. / float(msT);  // inverse of sampling in [ms]
const float Tkdiv2 = (1. / Tk ) / 2.; // half of sampling time in [s]

unsigned long t = 0, t_prev = 0;  // millis() timer values
int v = 0.0, v_prev = 0.0;        // Voltage values integers
float D = 0.0, D_prev = 0.0;      // Derivative values
float I = 0.0, I_prev = 0.0;      // Integration values

void setup() {
  Serial.begin(9600);
  delay(2000);        // hope this is enough to settle initialisations
  
  t_prev = millis();      // initialise first values
  v_prev = analogRead(in); 
}

void loop() {

  t = millis();               // get time in [ms]
  if ( (t-t_prev) >= msT ) {  // wait msT milliseconds, integer arithmetics
    v = analogRead(in);       // get analog value first to avoid any interrupt delays

    t_prev = t;  // keep new time in [ms] and assuming ïs ((t - t_prev) == msT) exactly
    v_prev = v;  // bump old values
    I_prev = I;
    D_prev = D;

    D = float(v - v_prev) * Tk;  // Derive simple rule
    I = I_prev + (D + D_prev) * Tkdiv2; // Integrate

  }

  Serial.println(D,8);
  Serial.println(I,8);
}
1 Like

I'd be tempted to say that the small numerical errors and the discrete integration method cause the integral to not perfectly reconstruct the original signal as these errors lead to a slow accumulation of unwanted values.

may be you can compute the bias numerically and work with the raw value without the bias ?

    float raw_v = analogRead(in);
    bias = bias * (1.0 - alpha) + raw_v * alpha; // Low-pass filter for bias estimation
    v = raw_v - bias;  // Remove DC component dynamically

Hello again,
having been out of home for the day, I'm back and see I have a logic error in my algorithm above, the "v_prev = v;" instruction should be just above the "v = analogRead(in);" line!

But for a more general use, if you add more code in the "loop()" section, it is better to add a test and initialise section just before the call to the integration loop, as in the following code, with the boolean variable that might be set TRUE to reinitialise your algorithm:

// derivation and integration example 2025 feb 16 v2
const pin_size_t Pin = 1;       // Analog input pin
const unsigned long msT = 50;  // desired sampling time in [ms]

const float Tk = 1000. / float(msT);  // inverse of sampling in [ms]
const float Tkdiv2 = (1. / Tk ) / 2.; // half of sampling time in [s]

unsigned long t = 0, t_prev = 0;  // millis() timer values
int v = 0.0, v_prev = 0.0;        // Voltage values integers

float D = 0.0, D_prev = 0.0;      // Derivative values
float I = 0.0, I_prev = 0.0;      // Integration values

bool Init_DerInt = true;          // Initialise the derivative and integrand    

void setup() {
  Serial.begin(9600);
  delay(2000);        // hope this is enough to settle initialisations

}

void loop() {

// DERIVATION AND INTEGRATION
if(Init_DerInt) {     // if true initialise
  t_prev = millis();  // initialise first values, get time
  v = analogRead(Pin); // get analog readout
  D = float(v);
  I = 0.0;
  Init_DerInt = false;    // init done
}
  t = millis();               // get time in [ms]
  if ( (t-t_prev) >= msT ) {  // wait msT milliseconds, integer arithmetics
    v_prev = v;               // bump old value
    v = analogRead(Pin);      // get new analog value first to avoid any interrupt delays

    t_prev = t;  // keep new time in [ms] and assuming ïs ((t - t_prev) == msT) exactly !
                 // a better way would be to use the correct time readings, in case of delays
                 // which means Tk and Tkdiv2 needs to be fully recalculated each time, 
                 // and with en precision of +/1 [ms] that may integrate and still give offsets too.
    I_prev = I;  // bump old values
    D_prev = D;

    D = float(v - v_prev) * Tk;  // Derive simple rule
    I = I_prev + (D + D_prev) * Tkdiv2; // Integrate

  }

  Serial.println(D,8);
  Serial.println(I,8);
}
1 Like

I tried the code, but the output oscillates between 0 and a negative value. Thank you for helping

I also tried this solution, but it only introduces a small delay and does not remove the DC offset. Thanks.

To remove a DC offset, use a very low pass exponential filter to estimate the offset.

There is an excellent discussion of the technique and good code examples at Digital filters for offset removal — OpenEnergyMonitor 0.0.1 documentation

If you have a good estimate of the offset and provide it at the start the low pass filter will converge quickly.

This is the code I use with a 10 bit ADC:

#define FILTERSHIFT 13 	// for low pass filters to determine ADC offsets
#define FILTERROUNDING (1<<12)
int voltsOffset=512;
static long fVoltsOffset=512L<<13;
// ...
{  
  newV=sampleV-voltsOffset;  //sampleV is the value read by ADC,
                             //newV is the output value

  fVoltsOffset += (sampleV-voltsOffset);  // update the filter
  voltsOffset=(int)((fVoltsOffset+FILTERROUNDING)>>FILTERSHIFT);
}

Hello again,

Just to say it's my first hardware tests on Arduinos, I have found a couple of things.

  1. in my code v2 change the I=0.0; in the initialise part to I=D;
  2. replace the two lines "Serial.println( ...);" by a single line:
    "Serial.print(v); Serial.print(", "); Serial.print(D); Serial.print(", "); Serial.println(I);" but put it inside the integration loop (just above the preceeding "}", and use the Serial Plotter output to look at the results.

I used a MKRWifi1010, added a 0-3.3V, 1Hz sinus wave to port A1 (Pin=1) and I get some interesting results. "v" and "I" are in phase from 0-1024 about (with a slight delay due to the simple integration method. And the D is a "noisy" +/- 3300 count symmetric sine.
If you keep my original "I=0.0;" in the initialisation you will get a I sinus that is offset, depending on the first value of "v". I believe you will understand that once you think over it.

Then the Arduino being powered by a 0-3.3V power supply. it's normal that the adc counts "unsigned" from 0-1024, no ?

1 Like

Here is my v3 code :wink:

// derivation and integration example 2025 feb 16 v3
const pin_size_t Pin = 1;       // Analog input pin
const unsigned long msT = 50;  // desired sampling time in [ms]

const float Tk = 1000. / float(msT);  // inverse of sampling in [ms]
const float Tkdiv2 = (1. / Tk ) / 2.; // half of sampling time in [s]

unsigned long t = 0, t_prev = 0;  // millis() timer values
int v = 0.0, v_prev = 0.0;        // Voltage values integers

float D = 0.0, D_prev = 0.0;      // Derivative values
float I = 0.0, I_prev = 0.0;      // Integration values

bool Init_DerInt = true;          // Initialise the derivative and integrand    

void setup() {
  Serial.begin(9600);
  delay(2000);        // hope this is enough to settle initialisations

}

void loop() {

// DERIVATION AND INTEGRATION
if(Init_DerInt) {     // if true initialise
  t_prev = millis();  // initialise first values, get time
  v = analogRead(Pin); // get analog readout
  D = float(v);
  I = D;
  Init_DerInt = false;    // init done
}
  t = millis();               // get time in [ms]
  if ( (t-t_prev) >= msT ) {  // wait msT milliseconds, integer arithmetics
    v_prev = v;               // bump old value
    v = analogRead(Pin);      // get new analog value first to avoid any interrupt delays

    t_prev = t;  // keep new time in [ms] and assuming ïs ((t - t_prev) == msT) exactly !
                 // a better way would be to use the correct time readings, in case of delays
                 // which means Tk and Tkdiv2 needs to be fully recalculated each time, 
                 // and with en precision of +/1 [ms] that may integrate and still give offsets too.
    I_prev = I;  // bump old values
    D_prev = D;

    D = float(v - v_prev) * Tk;  // Derive simple rule
    I = I_prev + (D + D_prev) * Tkdiv2; // Integrate

  // Serial.println(t); Serial.print(", "); 
  Serial.print(v); Serial.print(", "); Serial.print(D); Serial.print(", "); Serial.println(I);
  }

  // delay(1000);
}
1 Like

By the way, at 9600 bauds you transmit some 960 characters per second, or max 48 characters per 50 ms sampling time so do not use the "8" digit readout you will interfere with the sampling precision and your "D" curve will become far noisier

1 Like

That was an attempt at the low pass filter (described as well in @jremington’s link)

They do

filtered_value = last_filtered_value + 0.004 × (sample - last_filtered_value)

Which you can rewrite as

filtered_value =(1-0.004) * last_filtered_value + 0.004 × sample ;

And so up you see

As the long term average will be your bias but as they say

Unfortunately, this takes quite a long time to settle

Thanks for the fact, I will considerate the baud rate for serial plotting too.

That seems to be the case, but integrating the derivative function should also give a symmetric result, which does not happen. The operation does its job, but it brings back the DC constant that was previously removed by the derivative. The problem even persists on another board, like the ESP32. Thanks for replying.

The answer may have something to do with the fundamental theorem of calculus.

which initialisations ?


this belongs to the setup() function and you don't need the Init_DerInt variable


this is a fair comment, ∆t won't always be msT

Hello,

You are right, but your integration formula with my "Iprev+" or your "I1+" variable offsets the result. If you rewrite the integration formula as "I = (D + D_prev) / 2.;" you will get a sinus that is the same as the "D", but offset in time with a full time step, with respect to the original ADC value.

// derivation and integration example 2025 feb 16 v4
// for MKR WiFi 1010
const pin_size_t Pin = A1;     // Analog input pin
const unsigned long msT = 50;  // desired sampling time in [ms]

const float Tk = 1000. / float(msT);  // inverse of sampling in [ms]

unsigned long t = 0, t_prev = 0;  // millis() timer values
int v = 0.0, v_prev = 0.0;        // Voltage values integers

float D = 0.0, D_prev = 0.0;      // Derivative values
float I = 0.0;                    // Integration values

bool Init_DerInt = true;          // Initialise the derivative and integrand    

void setup() {
  Serial.begin(9600);

  pinMode(Pin,INPUT);             // set Pin A1 to analogue input
  analogReadResolution(10);       // default for MKR WiFi 1010 

  delay(2000);                    // hope this is enough to settle initialisations

}

void loop() {

// DERIVATION AND INTEGRATION
if(Init_DerInt) {     // if true initialise
  t_prev = millis();  // initialise first values, get time
  v = analogRead(Pin); // get analog readout
  D = float(v);
  I = D;
  Init_DerInt = false;    // init done
}
  t = millis();               // get time in [ms]
  if ( (t-t_prev) >= msT ) {  // wait msT milliseconds, integer arithmetics
    v_prev = v;               // bump old value
    v = analogRead(Pin);      // get new analog value first to avoid any interrupt delays

    t_prev = t;  // keep new time in [ms] and assuming ïs ((t - t_prev) == msT) exactly !
                 // a better way would be to use the correct time readings, in case of delays
                 // which means Tk and Tkdiv2 needs to be fully recalculated each time, 
                 // and with en precision of +/1 [ms] that may integrate and still give offsets too.
    D_prev = D; // bump old value

    D = float(v - v_prev) * Tk / 2.; // Derive simple rule, will be 1/2 sampling step behind true v value
    I = (D + D_prev) / 2.;           // Integrate, will be 1 full sampling step behind true v value

  // Serial.println(t); Serial.print(", "); Serial.print(Tk,6);
  // Serial.println(ARR); Serial.println(ADCscale,6);
  // Serial.print(v); Serial.print(", "); 
  Serial.print(D); Serial.print(", "); Serial.println(I);
 
  }

  // delay(1000);
}
1 Like

Hello again,

Sorry but it's getting too late in the night for me. I'm mixing up my values here, so my v4 is definitely wrong for the integration part.
Sorry about that.
I'll try again tomorrow morning :wink:

Hello,

Yes, and no, my "if ..." was in the setup() for my previous version, but as I believe this code could be called several times, therefore I proposed here to add it in the main loop() .
Normally for me such a code comes in a function.

And for the delay(2000); I'm not yet sure if all Arduino function calls return when fully finished, or if they are triggering a sequence driven by interrupts in the background.

Well, I'll stop for tonight, but this v5 works sometimes, but it gives different integration offsets depending on when I hit the Arduino reset button, which means that my initial integration value is varying, probably because some interrupt has delayed the code between my initialisation and the first sample. Sometimes the integration is symmetric, sometimes no and the offset depends on the moment I hit the reset, w.r.t. the sinus wave value.
Next step would be to use the true time between two readouts, and not the expected time.

// derivation and integration example 2025 feb 17 v5
// for MKR WiFi 1010
const pin_size_t Pin = A1;     // Analog input pin
const unsigned long msT = 50;  // desired sampling time in [ms]

const float Tk = 1000. / float(msT);  // inverse of sampling in [ms]

unsigned long t = 0, t_prev = 0;  // millis() timer values
int v = 0.0, v_prev = 0.0;        // Voltage values integers

float D = 0.0, D_prev = 0.0;      // Derivative values
float I = 0.0, I_prev = 0.0;      // Integration values

bool Init_DerInt = true;          // Initialise the derivative and integrand

void setup() {
  Serial.begin(9600);

  pinMode(Pin,INPUT);             // set Pin A1 to analogue input
  analogReadResolution(10);       // default for MKR WiFi 1010 

  delay(1000);                    // hope this is enough to settle any initialisations

}

void loop() {

// DERIVATION AND INTEGRATION
if(Init_DerInt) {     // if true initialise
  t_prev = millis();  // initialise first values, get time
  v = analogRead(Pin); // get analog readout
  D = float(v);
  I = 0.0;
  Init_DerInt = false;    // init done
}
  t = millis();               // get time in [ms]
  if ( (t-t_prev) >= msT ) {  // wait msT milliseconds, integer arithmetics
    v_prev = v;               // bump old value
    v = analogRead(Pin);      // get new analog value first to avoid any interrupt delays

    t_prev = t;  // keep new time in [ms] and assuming ïs ((t - t_prev) == msT) exactly !
                 // a better way would be to use the correct time readings, in case of delays
                 // which means Tk needs to be fully recalculated each time, 
                 // and with en precision of +/1 [ms] that may integrate and still give offsets too.
    D_prev = D; // bump old value
    I_prev = I;

    D = float(v - v_prev) * Tk;  // looks OK
    I = I_prev + (D + D_prev) / 2.0 / Tk; // gives different offsets depending on when I hit the reset button

  // at 9600 bauds with 10 bits per character in 50 msec we may write <<48 characters before interfering with sampling
  // Serial.println(t); Serial.print(", "); Serial.print(Tk,6);
  // Serial.println(ARR); Serial.println(ADCscale,6);
  // Serial.print(v); Serial.print(", "); 
  Serial.print(v); Serial.print(", "); Serial.print(D); Serial.print(", "); Serial.println(I);
 
  }

  // delay(1000);
}
1 Like

Not sure what you mean by that. Functions are C++ functions, they return to whatever part of the code called them.

And delay() is blocking the main code that called it.