Pulse output timing accuracy?

Hello everyone! I am new here and also to micro controller programming. I find working with MCs it fascinating and also frustrating, especially when things don't go as planned. I ran into an issue that hopefully, someone else has too, and there is a solution to it.
The project involves simulating pulses from a natural gas meter, industrial style, in order to measure instantaneous gas flow in cubic feet per hour. The device, a Volume Converter, I am feeding the pulses to, is highly accurate and has gone through rigorous testing incl. also has 3rd body certification, no issue there.
The required pulses are of longer durations then a PWM can handle, at least that is my believe. For example: for a flow of 160 cubic feet an hour, a continuous pulse with a width of >=40ms<=150ms and a delay of 36000ms (rising to rising) is required; that represents 1% of the gas meters capacity. For a flow of 100% capacity, a continuous pulse with a width of >=40ms<=150ms and a delay of 360ms (rising to rising) is required. That gives a range from 36000ms (1%) to 360ms (100%). I have used the below code to generate the pulse with an Arduino Nano Every, 3600ms delay and 75ms width; forgive me if it's not the best solution; I am open to suggestions.


//Pulse output

#define DI1_OUT 3

unsigned long interval1 = 0;
unsigned long currentMillis = 0;
unsigned long currentMillis1 = 0;
unsigned long previousMillis1 = 0;
unsigned long currentMillisP = 0;
unsigned long currentMillisP1 = 0;
unsigned long previousMillisP1 = 0;
signed long timeDif1 = 0;
const int pulseWidth = 75; //>=40ms<=150ms
int pulseWidth1 = -1;


void setup(){
  Serial.begin(115200);
  pinMode(DI1_OUT, OUTPUT);
}

//Main program
void loop()
{
  interval1 = 3600;

//Qu 1:
  currentMillis = millis();
  currentMillis1 = currentMillis;
  timeDif1 = currentMillis1 - previousMillis1 - interval1;

  if (timeDif1 >= 0) {
    digitalWrite(DI1_OUT, HIGH);
    Serial.print(currentMillis1 - previousMillis1);
    previousMillis1 = currentMillis1; //save the last time for LED HIGH
    previousMillisP1 = currentMillis1; //start pulseWidth 1
    pulseWidth1 = pulseWidth;
  }

  currentMillisP = millis();
  currentMillisP1 = currentMillisP;

// Pulse Width 1:
  if (currentMillisP1 - previousMillisP1 >= pulseWidth1) {
    digitalWrite(DI1_OUT, LOW);
    Serial.print("\t");
    Serial.println(currentMillisP1 - previousMillisP1);
    previousMillisP1 = currentMillis1;
    pulseWidth1 = -1;
  } 

}

This is working fine and the values printed on the serial port are accurate. The problem is the pin output; it's not accurate and fluctuates to the extent that the device doesn't show a stable flow. To further investigate, I used a second board, an Arduino Uno R4 Minima, to measure the outgoing pulses; see code below:


#define Pulse1In 2

int pin1_Start = -1;
unsigned long pulse1millis = 0;
unsigned long currentMillisP1 = 0;
unsigned long pulse1Start = 0;
unsigned long pulseWidth1 = 0;


void setup() {

  pulse1Start = micros();
  Serial.begin(115200);
  pinMode(Pulse1In, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(Pulse1In),CheckPulse1, CHANGE);
}

void CheckPulse1(){
  pin1_Start = 1;
}

void loop() {

if (pin1_Start == 1){
  currentMillisP1 = micros();
  pulseWidth1 = currentMillisP1 - pulse1Start;
  Serial.println(pulseWidth1/1000);
  pulse1Start = currentMillisP1;
  pin1_Start = 0;
  }

}

I used the status CHANGE as it returns both values, the pulse delay and the pulse width, and I am also measuring in microseconds. Here is what I am reading the above settings of 3600ms (10%) and 75ms on the sending device:

3525029
74810
3524654
75862
3525596
74832
3525891
74827

The reading on the sending device is stable at 3600ms and 75ms. At a setting of 360ms/75ms, the readings are closer but the error in % is bigger.

285764
74858
284776
75895
284776
74869
284780
75891
284785
74867

The Volume Converter shows a fluctuating flow of 15984 -15995 cubic feet an hour; not quite the result I expected. As you can see, the sending device, an Arduino Nano Every, calculates the pulse delay and width correctly, but it can't seem to output it with the same precision? Maybe using an Interrupt Timer may help? Any help, comments are appreciated.

Thank you for your reply. I dont think you can use millis() or micros() inside an ISR?
Also, measuring the pulses is not the issue, that is confirmed by the Volume Corrector, since it shows the same deviation as I measure with the Minima. The problem is in the output of the pulses from the Nano Every, those are not accurate and in sync with the calculated value and what is shown on the serial port.

You can read millis() or micros() within an ISR, but you can't wait and expect them to increment within an ISR because their interrupts will be blocked.

https://www.renesas.com/us/en/document/mah/renesas-ra4m1-group-users-manual-hardware?r=1054146
Thanks! I got to try that. Unfortunately it doesn't help me with the pulse output.

Thanks for the advice. Unfortunately, that is way over my capabilities, for now. :wink: Has anyone written a timer library that works with the Nano's 4809? Would using a GPT improve the pulse output accuracy?

You can get better accuracy by using micros() to do the timing, instead of millis().

pulseWidth and pulseWidth1 need to be declared as long now, as 75000 microseconds is too large for an int.

Here's your code with millis() replaced with micros():

//Pulse output

#define DI1_OUT 3

unsigned long interval1 = 0;
unsigned long currentMicros = 0;
unsigned long currentMicros1 = 0;
unsigned long previousMicros1 = 0;
unsigned long currentMicrosP = 0;
unsigned long currentMicrosP1 = 0;
unsigned long previousMicrosP1 = 0;
signed long timeDif1 = 0;
const long pulseWidth = 75000; // >=40ms<=150ms
long pulseWidth1 = -1;


void setup(){
  Serial.begin(115200);
  pinMode(DI1_OUT, OUTPUT);
}

//Main program
void loop()
{
  interval1 = 3600000;

//Qu 1:
  currentMicros = micros();
  currentMicros1 = currentMicros;
  timeDif1 = currentMicros1 - previousMicros1 - interval1;

  if (timeDif1 >= 0) {
    digitalWrite(DI1_OUT, HIGH);
    Serial.print(currentMicros1 - previousMicros1);
    previousMicros1 = currentMicros1; //save the last time for LED HIGH
    previousMicrosP1 = currentMicros1; //start pulseWidth 1
    pulseWidth1 = pulseWidth;
  }

  currentMicrosP = micros();
  currentMicrosP1 = currentMicrosP;

// Pulse Width 1:
  if (currentMicrosP1 - previousMicrosP1 >= pulseWidth1) {
    digitalWrite(DI1_OUT, LOW);
    Serial.print("\t");
    Serial.println(currentMicrosP1 - previousMicrosP1);
    previousMicrosP1 = currentMicros1;
    pulseWidth1 = -1;
  } 

}

Look at the measurements on these two oscilloscope traces, particularly the Min, Max and Pk-Pk values:


Using millis() for the timing, the pulsewidth (and period) varies by just over 1ms.


Using micros() for the timing, the pulsewidth only varies by 14µs, and the period by 23µs.

I'm not sure if the logic of the loop generating the signal is correct. What you print is what you expect, but not the calculations.

First you count the time for the interval:

//Qu 1:
  currentMillis = millis();
  currentMillis1 = currentMillis;
  timeDif1 = currentMillis1 - previousMillis1 - interval1;

  if (timeDif1 >= 0) {
    (...)
  }

Then you count the time for the pulse duration:

  currentMillisP = millis();
  currentMillisP1 = currentMillisP;

// Pulse Width 1:
  if (currentMillisP1 - previousMillisP1 >= pulseWidth1) {
     (...)

But this counting is running at the same time that the counting of the interval, so overlaping.
Altough you print the correct result that you expect:

Serial.println(currentMillisP1 - previousMillisP1);

But it was actually overlaped with the interval period.

I haven't run it, so I'm not sure and I could be wrong. But it seems to match with your results deviations. 285764 + 74858 = 360622
You can check it.

you could use timer to generate the pulses
e.g. this is using a ESP32 but the concepts apply to other micros

// ESP32 - continuous pulse with a width of 40ms and a delay of 3600ms (rising to rising)

// ideas from https://deepbluembedded.com/esp32-timers-timer-interrupt-tutorial-arduino-ide/

#define LED 19  // pulse output

hw_timer_t *Timer1_Cfg = NULL;  // timer object

bool pulse = true;
long int pulseWidth = 40;
long int delayTime = 3600;

void IRAM_ATTR Timer1_ISR() {  // timer ISR called on every timer event
  if (pulse) {    // if just generated pulse go LOW for delayTime - pulseWidth
    timerAlarmWrite(Timer1_Cfg, (delayTime - pulseWidth) * 1000, true);  // setup timer rising to rising)
    pulse = false;
    digitalWrite(LED, LOW);
  } else {        // generate next pulse go HIGH
    timerAlarmWrite(Timer1_Cfg, pulseWidth * 1000, true);     // setup timer for pulse width
    pulse = true;
    digitalWrite(LED, HIGH);
  }
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.printf("\n\nESP32 continuous pulse with a width of 40ms and a delay of 3600ms (rising to rising)\n");
  pinMode(LED, OUTPUT);
  Timer1_Cfg = timerBegin(0, 80, true);                  // setup timer prescaler value for 1uSec
  timerAttachInterrupt(Timer1_Cfg, &Timer1_ISR, true);   // attach callback fundtion
  timerAlarmWrite(Timer1_Cfg, pulseWidth * 1000, true);  // setup timer count for pulse width
  timerAlarmEnable(Timer1_Cfg);                          // enable timer
  digitalWrite(LED, HIGH);
}
void loop() {}

scope shows 3600mSec delay between pulses
image

pulse width 40mSec
image

1 Like

John,
I appreciate all the time you spent helping, especially measuring the output with an oscilloscope.
Changing to micros() did stabilize the values, but it didn't fully resolve it. I am still seeing fluctuations at 100% flow (36000) between 15985 - 15995. The readings on the Uno R4 Minima are
360339
360357
360345
360390
360264
That is an error of 0.094% which would account for the deviation/fluctuation of the value on the Volume Corrector. I am now wondering if the Volume Corrector also uses micros() to calculate the flow; after all these devices are for custody transfer metering and the error must not be greater then 0.5%.

Yes they are overlapping because the Volume Corrector is measuring peak to peak.

I understand that the timers work differently on the Nano Every, but I will give your code a try.

You could try to remove the Serial.prints. It buffers the sendings, but at the end it can add some delay between the first millis() and the second one. The serial bus is relative slow.

Actually it would be better if you work in 2 states (high/low) and take the time sample only once in the loop.

Actually, baud doesn't matter much, the variability in timing is due to the interrupt-and-transfer of another outgoing byte happening asynchronously to any timing. You can minimize that by only doing sensitive timing when you can guarantee the outgoing buffer is empty, but you'll still have the millis interrupt happening asynchronously, so there's only so much you can do. If you want good pulse timing, you pretty much must use hardware timers.

That's also good advice.

I did take out the Serial.pints but it made no difference. I have tried many things to make it better aside from using interrupt timers.

Ok, then use the timers. The frequency is very low, but it's not clear to me which actual precision do you need.

Another option would be to use a faster MCU, like the ESP32; 240 MHz and 2 cores.

1 Like

That was my other option. The final version will have to output pulses on 2 channels, 2 additional latched (high/Low) outputs, 2 channels for incoming pulses and also 2 high/low inputs. In addition, it will have to display all of it on a 4x20 LCD. So far, aside from the pulses outputs not really being accurate, the None Every handled it well.
I do expect to see 16000 cubic feet on the Volume Corrector when I select 360ms as delay, maybe +/- 2 cubic feet.

think it is time to give us a detailed specification
having half a specification it is possible to give advice which becomes invalid when the full specification is known
e.g. what is the relationship between the two pulses (none, synchronized, etc) and the inputs

It means a precision of about: 360ms * 2 / 16000 = 45 micro senconds, right? (edit: maybe this is not correct...)
May be possible, but the margin is not so big for a 16Mhz MCU.

What you could try is to take the time sample only once in the loop, to avoid the delay between one and the other. Should be small, but everything sums. And make it more clear. Something like this, (not tested):

const unsigned long high_width = 150*1000; // in micro seconds
const unsigned long low_Width = 360*1000 - high_width; // in micro seconds

bool high_state = true;
unsigned long curr_time = 0;
unsigned long prev_time = 0;

void loop()
{
  curr_time = micros();
  if( high_state){
    if( curr_time - prev_time >= high_width){
      digitalWrite(DI1_OUT, LOW);
      high_state = false;
      prev_time = curr_time;
    }
  }
  else { // low state
     if( curr_time - prev_time >= low_Width){
      digitalWrite(DI1_OUT, HIGH);
      high_state = true;
      prev_time = curr_time;
    } 
  }
}

as a check following on from post 10 this ESP32 code measures the pulse width and rising edge to edge time

// ESP32 - measure pulse width and rising edge-to-edge time using timer1

hw_timer_t *Timer1_Cfg = NULL;  // timer object

volatile unsigned long pulseWidth = 0, pulseEdge = 0;  // used in interrupt routines

// rising edge interrupt calculate rising edge-to-edge time
void IRAM_ATTR rising() {
  pulseEdge = timerReadMicros(Timer1_Cfg);
  timerWrite(Timer1_Cfg, 0);
}

// falling edge interrupt calculate pulse HIGH width
void IRAM_ATTR falling() {
  pulseWidth = (timerReadMicros(Timer1_Cfg));
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.printf("\n\nESP32 measure pulse width and rising edge-to-edge time \n");
  Timer1_Cfg = timerBegin(0, 80, true);  // setup timer prescaler value for 1uSec
  attachInterrupt(16, rising, RISING);   // setup interrput routines
  attachInterrupt(17, falling, FALLING);
}


void loop() {  // print information every second
  static unsigned long timer = millis();
  if (millis() - timer >= 1000) {
    timer = millis();
    Serial.printf("pulse HIGH width %luuSec %.3fmSec\n", pulseWidth, round(pulseWidth / 1000.0));
    Serial.printf("rising edge-to-edge time %luuSec %.3fmSec \n", pulseEdge, round(pulseEdge / 1000.0));
  }
}

connected to program of post 10 it displays

ESP32 measure pulse width and rising edge-to-edge time 
pulse HIGH width 0uSec 0.000mSec
rising edge-to-edge time 0uSec 0.000mSec 
pulse HIGH width 0uSec 0.000mSec
rising edge-to-edge time 0uSec 0.000mSec 
pulse HIGH width 0uSec 0.000mSec
rising edge-to-edge time 0uSec 0.000mSec 
pulse HIGH width 39955uSec 40.000mSec
rising edge-to-edge time 3202929uSec 3203.000mSec 
pulse HIGH width 39955uSec 40.000mSec
rising edge-to-edge time 3202929uSec 3203.000mSec 
pulse HIGH width 39955uSec 40.000mSec
rising edge-to-edge time 3202929uSec 3203.000mSec 
pulse HIGH width 39989uSec 40.000mSec
rising edge-to-edge time 3599918uSec 3600.000mSec 
pulse HIGH width 39989uSec 40.000mSec
rising edge-to-edge time 3599918uSec 3600.000mSec 
pulse HIGH width 39989uSec 40.000mSec
rising edge-to-edge time 3599918uSec 3600.000mSec 
pulse HIGH width 39989uSec 40.000mSec
rising edge-to-edge time 3599918uSec 3600.000mSec 
pulse HIGH width 39987uSec 40.000mSec
rising edge-to-edge time 3599953uSec 3600.000mSec 
pulse HIGH width 39987uSec 40.000mSec
rising edge-to-edge time 3599953uSec 3600.000mSec 
pulse HIGH width 39987uSec 40.000mSec
rising edge-to-edge time 3599953uSec 3600.000mSec 
pulse HIGH width 39987uSec 40.000mSec
rising edge-to-edge time 3599953uSec 3600.000mSec 
pulse HIGH width 39989uSec 40.000mSec
rising edge-to-edge time 3599952uSec 3600.000mSec 

as the time between pulses is 3.6 seconds it take a few seconds to get results

a program using micros() function gives similar results

I was curious to see how fast can be the Arduino in the loop generating a square wave. So I have connected the oscilloscope to an Arduino Uno and this test loop:

bool high_state = true;
unsigned long curr_time = 0;
unsigned long prev_time = 0;

const unsigned long high_width = 1 * 1000; // micro seconds
const unsigned long low_Width = 1 * 1000;  // micro seconds

void loop()
{
  curr_time = micros();
  if( high_state){
    if( curr_time - prev_time >= high_width){
      digitalWrite( PINOUT, LOW);
      high_state = false;
      prev_time = curr_time;
    }
  }
  else { // low state
     if( curr_time - prev_time >= low_Width){
      digitalWrite( PINOUT, HIGH);
      high_state = true;
      prev_time = curr_time;
    } 
  }
}

With 1000 us each half of the wave:

Zooming in:


The error is around 2us ~ 4us, more or less.

Then with amplitude of each half reduced to 100 us:

And reduced to 10 us:


Erros is always 2 ~ 5 us.

Then I tested with this minimal loop to see how fast can it switch:

bool high_low = true;
void loop()
{
  digitalWrite( PINOUT, high_low);
  high_low = !high_low;
}

The amplitude of each half was: 3.180 us The total is similar to the error before.


I added 8 lines with millis() and some calculations in temp variables, then the amplitude increased to 4.6 us.

But, adding Serial.print:

bool high_low = true;
void loop()
{
  digitalWrite( PINOUT, high_low);
  high_low = !high_low;
  Serial.print("Olakease!");
}

Then amplitude of each half was: 760 us
Printing longer text the amplitude was longer, more than 1 ms.