How accurate is timing?

Hi guys.

I have a sketch which acts as a high accuracy pulse delay unit running on a Nano ATMega328P (old bootloader). I am using the high accuracy timer2 library of Gabriel Staples, a concise and really useful piece of coding. That claims accuracy to 0.5us of course.

It has a delay hard coded in and sets up a delay count beyond the start point when it is run up. According to the Timer2 library that should be simply [Desired Time (us) * 2] and I reckon there will need to be a small fixed correction for the other small bits of processing performed outside the loop which is pretty much constant and not dependent on the target delay at all.

The input and output pulses are buffered by simple single transistor amps, pulldown in the case of the input (FALLING) and pullup for the output as it will eventually have to drive optoisolated equipment and the current needs of an LED/resistor input are better served from a PNP collector than through the Rload of an NPN. It is triggered by interrupt on an I/O port and basically just calculates the final target count from the start count and desired delay then starts looping, reading the count over and over and doing nothing else at all until the target count is exceeded. I did anticipate that each iteration of the loop would take a certain number of clock cycles and would be pretty much a constant overhead for each read. When the target count is exceeded it fires an output pulse (FALLING) to the output transistor and reports back on the diagnostic serial line with a final count total so I have an idea of how close to the desired delay I am.

This is working perfectly and it reports back with various stats I have programmed in reporting of the average error beyond the target count as the pulse count builds up, (I used 100 tests). All of the reporting is outside the delay and pulse firing code. It reports that I am getting around an average of 2.7us accuracy for a delay of 1000ms. The error varies at random between 0us min - 9us max.

Now the oddity. I am trying to check this against my scope, (a GW Instek GDS1052U, basic but workmanlike). That is saying the pulse delay of 1000ms is out by about 300us. I need this to be reliably better than 50us which the stats suggested it was easily beating. I am loathe to believe that sort of delay is inherent in a couple of simple single transistor stages so I’m looking for other reasons for the discrepancy. The scope could be one of them of course but I would have thought that a digital scope of this tiype would be pretty much on the beam with this sort of measurement.

Does anyone out there have an idea of how close to exactly 0.5us per count the high accuracy timer2 library achieves?

There's three sources of inaccuracy.

  1. The Arduino environment has some interrupts built-in. Notably the millis() interrupt that triggers about once per second. If you are trying to create exact delays in your loop() with delayMilliseconds() then you can get hit by that interrupt at the wrong time, which will affect your timing.

Using the hardware timer will almost completely eliminate this source of error. The only time it could hit you is if your interrupt fires while it's already servicing another interrupt. Your interrupt will be delayed.

  1. The clock source can be a crystal (most accurate), resonator (pretty good), or internal to the AVR chip. I forget which one the Nano uses and cheap clones tend to use the cheaper option. I expect you have a resonator.

A resonator will give you errors in the 0.2% to 0.5% kind of range. Some of this can be tuned out by adding a small offset but temperature changes will change the speed of the resonator.

  1. When was the scope calibrated? Do you know its accuracy?

A couple of points which may be useful to explain here.

My interrupt service routine simply sets a boolean telling the main loop that it is now in processing mode. This boolean is only unset once the timing process has completed. It then turns on a “Processing” LED on one of the ports and drops back into the loop(). This costs a little processing time but it should be small enough to not do too much damage to accuracy as it means only extending the time between one pair of reads. And any additional unnecessary calls to the ISR made during processing will be ineffective as the boolean is already set, even though they would also cost a little time lost.

From what I understood of the background counting process which the timer is based upon, my repeated calls to get the count for comparison would be very unlikely to bring in the dead accurate point where it completes but it would step forward in discrete jumps of a few counts at a time. Likewise, any dead time for unnecessary processing, such as that example of the ISR having to be processed an additional time, would simply make that particular jump between queries a little bigger than usual. Overall accuracy would not be affected. There is no cumulative error here.

With that said, I have a very streamlined loop function with absolutely no unnecessary calculations, reporting or decision making other than: 1 Get the current count, 2 Is it now greater than the target final count? It just loops doing that until the target final count is passed then it fires the pulse immediately before anything else. It can then turn on/off LEDs and set up the stats and report them back.

I don’t use millis() or delayMilliseconds() at all, I only call the high accuracy Timer2’s ‘timer2.get_count()’. Here is the ISR and the loop() function:

// Main loop
//
void loop() {
   
    if (bFire) {	// Responding to interrupt informing Fire Pulse received

        ulTimerCount = timer2.get_count();       // timer2 is the high accuracy timer object
      
        if (ulTargetCount <= ulTimerCount) {     // Timing delay has been exceeded

            fireDelayPulse();                               // Release output pulse

            bFire = false;                                    // Now out of processing mode

            CalculateStats(ulTimerCount - ulStartCount);    // Pass value of this unique delay in counts elapsed

            ReportStats();                                   // Stream errors and averages to Serial Monitor
        }
    }
}

// ISR for Fire interrupt
// Sets start time into ulStartCount, sets LEDs
// then passes back to Loop()
// This has lost time for processing but it is
// a once only overhead at the start of the delay
//
void InterruptFire() {

    if (bFire == false) {
		
        ulStartCount = timer2.get_count();                    // Get reference point to count from

        ulTargetCount = ulStartCount + ulCountDelay;    // ulCountDelay is the required delay time converted to Timer counts in setup()
	  
        bFire = true;              // We are in processing mode
      
        digitalWrite(iProcessingLedPin, HIGH);      // Display 'Currently Processing Delay' via LED
    }
}

Hi MorganS, thanks for your thoughts.

As you can see from the code above the first point you suggested isn't relevant to this case. I don't use those functions at all. I knew they would not give me the accuracy I needed so I went down the high accuracy timer library route from the off. If the millis() interrupt is only once a second or so then it won't give the consistent error I was seeing on the scope as that was the maximum delay I was using. Generally I was working more at around 200ms which statistically would have taken it between millis() interrupts most of the time.

I'll have to look into the "hardware timer" you mentioned, it doesn't ring any bells with me at the moment. And the Nanos I am using are clones just as you suggest, for proof of concept. I hadn't realised they would be significantly less accurate than a genuine Nano. Now you describe it it makes perfect sense to me. That will definitely be a factor.

And the scope calibration date. We-e-e-e-ll, if I'm honest, "who knows" is the real answer so it may very well be as much or more of the scope's inaccuracy as the Nano's. I'll have to find a way of checking the timing of the scope before I can really be sure what is what.

I'll look into the hardware timer. Thanks for the heads up (and the general support).

Please post the full code and a link to the timer library you’re using.

Even if you don't use millis() that timer is still running. You have to do extra work to eliminate that.

The timer2 library likely is using a hardware timer. If you showed your complete code it will be more helpful. And where you got that library from.

"Consistent" means you are either using the library or the scope incorrectly.

Ok, here is the .ino file with all code I wrote:

=========================================================

/*   Digital Pulse Delay
 *
 *   Compiled for ARDUINO NANO
 *
 *   Data is hard coded below and uploaded with program
 *   
 *   Input pulse detected via Interrupt on D2 (Pin 5)
 *   Output pulse via D10 (pin 13)
 *   Internal LEDs controlled on D13 (pin 19) and D14 (pin 20)
 *
 *   All writes direct to internal LEDs are HIGH to light them up
 *   
 *   Input pulses to the Arduino pin and Writes to the output pin
 *   are interfaced with buffers which INVERT.  
 *   This must be accounted for in the logic of the writes
 */

#include <eRCaGuy_Timer2_Counter.h>			// High Accuracy Timer Library

// ENTER TIMING VALUES HERE:-


unsigned long ulDelay      = 200;     // Required DELAY of pulse in milliseconds.  Needs to be ULong to match high accuracy timer library values)
unsigned long ulPulseWidth = 50;      // OUTPUT PULSE DURATION in milliseconds.  (Needs to be ULong to match high accuracy timer library values)



// GLOBALS -----------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------


// PIN ALLOCATIONS ---------------------------------------------------------------------------

const int iFirePulsePin = 2;					// Fire pulse received on 'D2' (pin 5)
const int iDelayPulsePin = 10;			   // Delayed pulse transmitted on 'D10' (pin 13)
const int iProcessingLedPin = 14;		   // LED connected to analogue 'A0'/digital 'D11' (pin 14), displays status 'Pulse has been received and delay is active'
const int iOutputLedPin = 15;					// LED connected to analogue 'A1'/digital 'D12' (pin 15), displays in time with delayed output pulse

// TIMING COUNT VALUES -----------------------------------------------------------------------

unsigned long ulStartCount = 0;		// Count at start of delay. N.B. this count is in 0.5us  Counts need to be ULong to match timer library values
unsigned long ulTargetCount = 0;    // Count to match for Main output pulse after delay in 0.5 microsecond counts
unsigned long ulCountDelay = 0;     // Delay converted to timer counts and corrected for fixed processing delay

unsigned long ulTimerCount = 0;     // Count at end of processing

const unsigned long ulMaxDelay = 1000;	// The maximum permitted value of delay in milliseconds
const unsigned long ulMinDelay = 1;		// The minimum permitted value of delay in milliseconds

// LOGIC CONTROL VARIABLES --------------------------------------------------------------------

bool bFire = false;						// Fire pulse detected sets true, program is in delay counting mode

// TEMPORARY APPRAISAL VARIABLES --------------------------------------------------------------

float fMaxError = 0;
float fAverage = 0;
float fCtrError = 0;
unsigned long ulLastError = 0;
unsigned long ulAbsRunningTotal = 0;
unsigned long ulCtrRunningTotal = 0;
unsigned long ulTestCount = 0;
int iShotCount = 0;

// CODING --------------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------

// Put setup code here, to run once automatically
//
void setup() {

   Serial.begin(115200);

   ValidateDelay();

   // Calculate the value of required delay in High Accuracy Timer counts
   ulCountDelay = (2000 * ulDelay) - 12;      // Try "12".   timer2 works on "0.5us" counts hence "2000 * " to convert from milliseconds to 'counts'
                                                                // After producing stats from the programme's action 6us seems to be close to the
                                                                // inherent overhead in Arduino processing which is constant, hence "-12" counts allows
                                                                // the post delay processing to be taken into account.  No other processing is allowed to
                                                                // occur while in delay mode hence the overhead remains constant for any delay setting
                                  
   pinMode(iFirePulsePin, INPUT);
   pinMode(iProcessingLedPin, OUTPUT);
   pinMode(iDelayPulsePin, OUTPUT);
   pinMode(iOutputLedPin, OUTPUT);

   attachInterrupt(digitalPinToInterrupt(iFirePulsePin), InterruptFire, FALLING);

   FlashWarning(3, 100, 400);
   FlashWarning(3, 400, 100);
   
   timer2.setup();
}

// Main loop
//
void loop() {
   
   if (bFire) {	// Responding to interrupt informing Fire Pulse received
      ulTimerCount = timer2.get_count();
      
      if (ulTargetCount <= ulTimerCount) {      // Timing delay has been exceeded
         fireDelayPulse();
         bFire = false;

         CalculateStats(ulTimerCount - ulStartCount);
         ReportStats();
      }
   }
}

// ISR for Fire interrupt
// Sets start time into ulStartCount, sets LEDs
// then passes back to Loop()
//
void InterruptFire() {
   if (bFire == false) {
		
      ulStartCount = timer2.get_count();						// Get reference point to count from
      ulTargetCount = ulStartCount + ulCountDelay;			// ulCountDelay has already been converted to Timer counts
	  
      bFire = true;
      
      digitalWrite(iProcessingLedPin, HIGH);      // Display 'Processing delay' via LED
   }
}

// Fire delayed pulse on iDelayPulsePin
// Accurate counting of delay has been completed. This call
// blocks but it is now outside of the high accuracy loop
// so it isn't a problem
//
void fireDelayPulse() {

   digitalWrite(iDelayPulsePin, LOW);     // Output pulse started - must be first to avoid any additional delay
   
   digitalWrite(iProcessingLedPin, LOW);  // Turn off Processing pin now we are outputting the delayed pulse
   digitalWrite(iOutputLedPin, HIGH);		// Turn on Output Pulse LED

   delay(ulPulseWidth);						   // delay() works in milliseconds only. THIS CALL BLOCKS but it is irrelevant at this stage, the high accuracy work is done

   digitalWrite(iDelayPulsePin, HIGH);		// Output pulse completed
   digitalWrite(iOutputLedPin, LOW);		// Turn off Output Pulse LED
}

// Validate entered values of delay
// Protects against ridiculously high values entered by accident
// Converts entered value in milliseconds to High Accuracy Counter counts (0.5us)
//
bool ValidateDelay() {
   bool bValid = true;

   // Prevent accidental '0' or -ve value to minimum delay and warn
   if (ulDelay < ulMinDelay) {
      ulDelay = ulMinDelay;
      bValid = false;
      
      FlashWarning(10, 100, 400);    // Give indication this has been corrected
   }

   // Limit accidentally high value to maximum delay and warn
   if (ulDelay > ulMaxDelay) {
      ulDelay = ulMaxDelay;
      bValid = false;
      FlashWarning(10, 400, 100);    // Give indication this has been corrected
   }

   return bValid;
}

// Flash LEDs as generic warning with Number of Flashes and ON and OFF times specified milliseconds
//
void FlashWarning(int iNumFlashes, int iOnCount, int iOffCount) {
   for (int i=0; i<iNumFlashes; i++) {
      digitalWrite(iProcessingLedPin, HIGH);
      delay(iOnCount);
      digitalWrite(iProcessingLedPin, LOW);
      delay(iOffCount);
   }
}

// TEMPORARY FUNCTIONS ************************************

// Calculate totals and averages
// and update Max and Min values
//
void CalculateStats(unsigned long ulVal) {

   ++ulTestCount;

   ulCtrRunningTotal += (ulVal - ulCountDelay);

   fCtrError = float(ulCtrRunningTotal) / (2 * float(ulTestCount));

   // unsigned long ulError = abs( ulVal - ulCountDelay);    // Original with no stats

   ulLastError = abs( ulVal - ulCountDelay);

   ulAbsRunningTotal += ulLastError;
   
   fAverage = float(ulAbsRunningTotal) / (2 * float(ulTestCount));

   if (fMaxError < float(ulLastError) / 2) fMaxError = float(ulLastError) / 2;
}

// Send stats to programming machine via USB
//
void ReportStats() {
   Serial.write("Shot number: ");
   Serial.print(iShotCount);
   Serial.write("\r\n");
   Serial.write("Error: ");
   Serial.print(ulLastError);
   Serial.write("us\r\nAverage Error: ");
   Serial.print(fAverage);
   Serial.write("us;  Max Error: ");
   Serial.print(fMaxError);
   Serial.write("us;  Error Centre: ");
   Serial.print(fCtrError);
   Serial.write("us\r\n\r\n");
   iShotCount++;
}

=========================================================

And here is a link to the library:

Arduino High AccuracyTimer Library

IMO, your whole approach is overly-complicated and designed to throw in timing uncertainty. There's too many superfluous function calls in the critical timing paths and you're not taking advantage of what the hardware can do for you. Doing things like calling digitalWrite() takes way longer than using direct port manipulation. And even doing that in code is less certain then letting the hardware timer set the output pin on Counter Match.

The library you linked says it uses Timer2 which is an 8-bit counter. So, it's unclear to me how you're going to get your desired 200ms total delay and the claimed 0.5 us accuracy at the same time. Wouldn't that be 200E-3 / 0.5E-6 = 400,000 counts from 8-bits? I'm guessing the library uses software to extend the effective bits. But, that also introduces uncertainty due to calling the ISR needed to update the upper bytes stored in RAM via software. I don't know because it doesn't seem to be on GitHub and I'm not about to click on some random link found on Instructables. Editorial Comment -- 90% of the code I've seen on Instructables is crap.

I'd recommend using Timer1 which is 16 bits. By selecting the prescaler setting you can tradeoff between delay resolution and maximum delay possible.

I just posted some code to do something similar in response to a different question: Reply 22 Here.

It uses Timer1's output compare ability to control the output pins with the best precision / accuracy you're going to get using an ATMega328. Even that code is a little bloated because of the FIFO that lets you have an input pulse repetition period that's shorter than the delay period. If you don't need that, the code can be simplified even more.

I agree and I understand the significance of superfluous function call overhead while processing very well. The point is that the only thing in the entire process that is critical is the value of the delay between receiving the interrupt from the input pulse and starting the output pulse. I have made absolutely sure that during that time there are an absolute minimum of calls to other functions, only a single digitalWrite() call is made immediately timing starts to light the “delay in progress” LED and that is only made once not in every iteration. They are all made either before in the setup phase, or after in the reporting phase.

The process I envisaged is this:

Setup all hardware (communication and LED and ports)
Calculate the count which is equivalent to the time of delay requested
Receive input pulse which triggers interrupt

In ISR set one LED to indicate processing is active
In ISR query library high accuracy timer for count and add to calculated delay count to give target finishing count
In ISR set boolean to indicate we are in delay mode

**** We are now in the critical loop

In loop() check we are still in delay mode and if so…
In loop() query library high accuracy timer for count
In loop() compare current count with target finishing count

**** We are leaving the critical loop

If target count is exceeded immediately fire output pulse and unset boolean to be back in waiting mode
Clean up with calculations and reporting back of stats

Here is just the loop() function again with a few notes:

// Main loop
//
void loop() {
   
   if (bFire) {                                // Has been set in ISR.  We are only responding once the interrupt has been received

     ulTimerCount = timer2.get_count();        // Get the current count
      
     if (ulTargetCount <= ulTimerCount) {      // Only enter once the timing delay has been exceeded

         fireDelayPulse();                     // Immediately fire the delayed pulse

         bFire = false;                        // Exit delay mode and go back to waiting for next interrupt

  
         // Everything from here on in is outside of the timing loop as we are back in
         // wait mode and the output pulse has been sent
         CalculateStats(ulTimerCount - ulStartCount);
         ReportStats();
      }
   }
}

I was very careful to make sure that, as much as possible, every single call to anything else was outside of the single critical delay loop. I know there is a lot of fluff in the programme, I initially tried to get it out of the way to show the programme function more clearly, but as full code was requested there it is. In essence there is a single clean looping path in there at the start of the loop() function which does not access any of that fluff during its “delaying” timing stage.

I wonder if I have made an assumption which is not true here. I was of the impression that the count proceeded in the background at a steady pace continuously, independent of everything else, (or at least pretty much so), which is going on in the programme. My belief was that if a function call was made, it would only take up time which would delay the next getCount() call without altering the count process behind it that it would return. I would be in the situation where the count would have still proceeded and, when the next count call was eventually made, we would just be a number of clicks further on than we needed but the overall accuracy of the count in continuity terms would still be unchanged. Is that not the case?

“Complete” doesn’t necessarily mean “include the fluff”. The preferred route is to post an MCVE. This is the absolute minimum code possible that compiles (so someone can try it) and demonstrates the issue you’re dealing with. Anything beyond that is fluff.

You’ve done a decent optimization job given the boundary condition that you want to use that library. I would probably in-line the fireDelayPulse() operation rather than put it in a separate function. And, I’d DEFINITELY use direct port manipulation to fire the time-critical edge rather than calling digitialWrite().

You haven’t stated what your requirements are in terms of maximum delay needed, resolution in setting that delay, and absolute accuracy of the delay.

If you can’t achieve those requirements with your software, I’d start looking into what the hardware can do for you in terms of Timer input-capture events and output-compare events. The ability to set an output pin via hardware upon Timer output-compare is probably the most accurate timing you’re going to get. I’d move to Timer1 (16 bits) and handle it myself without a library.

All that being said, from your first post, it looks like you want 50ppm accuracy (50us out of 1000ms). That may be a stretch given you’re using a hobbyist-class microcontroller board and don’t know the accuracy of your measurement gear.

I was very careful to make sure that, as much as possible, every single call to anything else was outside of the single critical delay loop.

With a hardware timer, that is not necessary. But if you truly have nothing else for the Arduino to do during that time, use delayMicroseconds().

I had a little time, so I wrote up an example of the Timer technique I’m was talking about. It compiles, but I don’t have an ATMege328 board to try it on right now.

This is a bare-bones, no-fluff example. I’ve hardcoded the counter values needed for a 200ms delay and shown where you can add some LED indicators. The counter is running at the full 16MHz rate (no prescaler). So, it should give sub-microsecond precision. The absolute accuracy will depend on the board’s clock.

The pulse input must be Pin 8 and output must be Pin 9.

A real implementation would need a clever way to determine the counter values for an arbitrary delay. That’s left as an exercise to the reader.

#include "Arduino.h"

const uint8_t pulseWidth = 50;	// milli-seconds, not critical
const uint8_t ICP = 8;      // Input capture pin

volatile uint16_t terminalTicks = 32000;
volatile uint16_t intermediateTicks = 32000;
volatile uint16_t intermediateMatches = 99;

volatile bool delayStarted = false;
volatile bool delayFinished = false;

void setup() {
	// turn off the "Delay Finished LED"
	// turn off the "Delay Started LED"

	pinMode(ICP, INPUT);
	TCCR1B = (1 << ICES1);    // Stop counter, waveform generator Mode 0, capture input on rising edge of ICP
	TCCR1A = 0;         // waveform generator Mode 0
	TCNT1 = 0;          // reset counter
	OCR1A = 0xFFFF;       // park output compare value here for now
	TCCR1A = (1 << COM1A1);   // OC1A low on A-match, waveform generator Mode 0
	TCCR1C = (1 << FOC1A);    // Force A-match, OC1A Low
	TCCR1A = 0;         // disconnect OC1A
	PORTB &= ~(1 << PB1);   // PB1 (Pin 9) to 0 when switched to output
	DDRB |= (1 << DDB1);    // PB1 (Pin 9) as output
	TIMSK1 = (1 << ICIE1);    // interrupt on input capture
	TCCR1B |= (1 << CS10);  // Start counter, bypass prescaler
}

void loop() {
	if (delayStarted) {								// input pulse detected and delay has started
		delayStarted = false;
		// turn on the "Delay Started LED"
		// turn off the "Delay Finished LED"
	}

	if (delayFinished) {							// output rising edge has been set
		delayFinished = false;
		// turn on the "Delay Finished LED"
		// turn off the "Delay Started LED"
		delay(pulseWidth);							// falling edge of pulse not critical
		TCCR1A = 0;									// disconnect OC1A, sends falling edge
		TIMSK1 = (1 << ICIE1);						// set interrupt for next input capture
	}

}

ISR(TIMER1_CAPT_vect) {
	uint16_t matchValue;
	uint16_t timeStamp = ICR1;
	delayStarted = true;							// tell main program we've started
	TIMSK1 = 0;										// disable counter interrupts

	if (intermediateMatches == 0) {					// Set output on next counter match
		matchValue = timeStamp + terminalTicks;		// counter value to output rising edge
		OCR1A = matchValue;							// set next compare time
		TCCR1A = (1 << COM1A1) | (1 << COM1A0);		// OC1A high on A-match
	} else {										// Not in terminal count, wait for next match
		matchValue = timeStamp + intermediateTicks;
		OCR1A = matchValue;							// set count match value
	}

	TIMSK1 = (1 << OCIE1A);							// Enable interrupt on match
}

ISR(TIMER1_COMPA_vect) {

	if (intermediateMatches == 0) {					// output rising edge has been sent, we're done
		TIMSK1 = 0;									// disable counter interrupts
		delayFinished = true;						// tell main program we're done
		return;
	}

	intermediateMatches--;							// that was an intermediate count that just completed
	if (intermediateMatches == 0) {					// now we're on terminal count
		OCR1A += terminalTicks;						// set count for next match;
		TCCR1A = (1 << COM1A1) | (1 << COM1A0);		// OC1A high on A-match
		return;
	}

	OCR1A += intermediateTicks;						// not on terminal count yet, set count for next match;
}

Hahaha! You guys are amazing, and incredibly helpful too. Huge bunch of thanks for taking the time to share your thoughts.

MorganS. Your simple statement there actually says a lot I think and thanks for the suggestion. I did have the same idea last night, (in the wee hours), that I might just get away with using delayMicroseconds() because of the fact, just as you say, that there is absolutely nothing else for the device to do.

It waits. It detects a pulse on a single specific pin. It waits a hard coded period of time. It fires a single pulse out on a different pin. It waits for the next one. That's it. I can make that looping delay section as clean as I am physically able. (For example, it finally did occur to me that the simple noInterrupts() passed me by in the first instance. I have a lot of C++ experience but none that is Arduino or even embedded specific.) The stats and reporting is just a diagnostic/trimming tool and will be completely removed in the final version. The LED control is necessary but, as I have said before, I made sure to keep it out of the accurate delay loop with the exception of that one call at the start. I'll make sure to change these for calls direct to hardware even if only as an exercise.

I am writing a sketch at the moment which works with delayMicroseconds() as you suggested to test that against the original idea. And that brings up the question of requirements of course...

Gfvalvo. That's a good point about "complete" code and I am suitably chided and contrite! :-* I get what you mean about it being the minimum code able to compile and function. I'll do better on that one in the future. :wink:

Your requirements question is a good one and it points out that I didn't really ever explain that fully. I need this to work only for delays defined in milliseconds. They will vary from a few ms, (2 or 3) to a maximum of 1000ms. The delay itself needs to be made as accurate as possible given the simple nature of the hardware and the setup process, i.e. a couple of data values embedded in the program transferred to the device via USB from the Arduino IDE. I dislike that process intensely given my background but it is pragmatic and reliably doable by non-programming users.

The figure of 50us was requested by the guy who is the domain expert. This is ultimately for use as a piece of test gear within the seismic exploration field and they are notoriously fussy about error values, though it is only for internal use in the UK and not to use out on location in live data gathering.

It isn't necessary to have 50us resolution in the choice of delay value but the final result should be accurate to within that figure if it is at all possible. I did think this would be manageable with trimming of the delay values to reflect delays inherent in the program which I felt would be mostly constant. I realised that it will drift of course but the device is fitted in a small sealed diecast box in controlled conditions as a piece of test gear on the bench of a nice comfy, warm, dry, air conditioned office. I didn't see there being too much of a variation in temperature once the device had been running for enough time for it to stabilise.

The idea is for it to respond to regular single pulses at a rate of up to one every 3secs or so but maybe as long as every 30secs. It simulates delays in 3rd party gear in a system which automates deep sea seismic survey data collection. The main software controls the path of the boat along preplotted lines, predicting its upcoming position and firing the compressed air gun arrays as close to GPS located points as possible. It then records the positions of the thousands of listening devices on the streamer cables behind the boat as they pick up the reflected sound waves. Delays are relevant as they mean a shift in positioning of both guns and recording devices which means smearing of the pictures of under seabed strata that the results generate.

I know the limitations of GPS and laser positioning and how that should ease our own requirements, (50us = 7.5cm in sea water!), but, I promise you, you can't discuss the effects of that or its relevance to the final result with many seismic guys! :grinning: The difference between pulse delay times should also be kept within 50us. You can see that the 50us is perhaps just a convenient round figure but it is at least an easy target value for the more task oriented guys who requested it.

I am really grateful for your thoughts and code and I will now move on to dissecting and digesting it before trying the different approaches it may throw up. First I'll try out MorganS's suggestion as an easier task, it may just fit the bill. Then it's on to the code posted. "The reader" is looking forward to customising it to fit his own needs.

Thanks all round, I'll make sure to keep you up to date with progress.

So, I just got an ATMega328P board and started playing with the code I previously posted. Found and corrected a few problems and also “improved” it along the way.

Anyhow, this code uses Timer 1 and can provide delay values from 32us (my somewhat arbitrary lower limit) to 250 seconds all with 1/16 us (62.5 ns) resolution. It’s based on hardware (vs software) control of the output pulse. So, I think it’s the best that can be done given the Uno board.

The code is functional and the delays seem correct. I probably won’t get a chance to put the signals on a scope for a few days. But, if you’re interested, give it a shot. Input is on Pin 8, output on Pin 9. Be sure to update the line in ‘setup()’ depending on whether you want to start the delay on the input’s rising or falling edge. That line is commented as such.

#include "Arduino.h"

void computeCounterLoad(uint32_t delay);
void loadCounters();

const uint16_t minDelay = 512; // minimum allowed delay = 512 * (1/16) = 32us  -- rather arbitrary
const uint32_t maxDealy = 4000000000; // max allowed delay = 4,000,000,000 * (1/16) = 250,000,000 us

const uint16_t initialTickCount = 1 << 15;  // 2048 us
const uint16_t maxTickCount = 49152; // 3072 us

volatile uint16_t terminalTickCount;
volatile uint16_t terminalTickLoad;
volatile uint32_t initialCycleCount;
volatile uint32_t initialCycleLoad;

const uint8_t pulseWidth = 50; // milli-seconds, not critical
const uint8_t ICP = 8;      // Input capture pin


volatile bool delayStarted = false;
volatile bool delayFinished = false;
volatile bool okToUpdate = true;

bool counterUpdatePending = false;

uint32_t currentDelay = 5000UL * 1000 * 16; // 1++++s initial delay time

void setup() {
 Serial.begin(115200);
 delay(1000);
 Serial.println("Starting");
 computeCounterLoad(currentDelay);

 pinMode(ICP, INPUT_PULLUP);
 pinMode(LED_BUILTIN, OUTPUT); // LED_BUILTIN lit during delay period
 digitalWrite(LED_BUILTIN, LOW);

 TCCR1B = 0; // Stop counter, waveform generator Mode 0
 TCCR1B |= (0 << ICES1); // capture timer count on input on falling edge of ICP, use (1 << ICES1) for rising edge capture
 TCCR1A = 0;         // waveform generator Mode 0
 TCNT1 = 0;          // reset counter
 OCR1A = 0xFFFF;       // park output compare value here for now
 TCCR1A = (1 << COM1A1);   // OC1A low on A-match, waveform generator Mode 0
 TCCR1C = (1 << FOC1A);    // Force A-match, OC1A Low
 TCCR1A = 0;         // disconnect OC1A
 PORTB &= ~(1 << PB1);   // PB1 (Pin 9) to 0 when switched to output
 DDRB |= (1 << DDB1);    // PB1 (Pin 9) as output
 TCCR1B |= (1 << CS10);  // Start counter, bypass prescaler
 TIFR1 = 0xFF; // clear all pending timer interrupt flags
 TIMSK1 = (1 << ICIE1); // set interrupt for first input capture
}

void loop() {
 if (delayStarted) { // input pulse detected and delay has started
 delayStarted = false;
 digitalWrite(LED_BUILTIN, HIGH);
 }

 if (delayFinished) { // output rising edge has been set
 delayFinished = false;
 digitalWrite(LED_BUILTIN, LOW);
 delay(pulseWidth); // falling edge of pulse not critical
 TCCR1A = 0; // disconnect OC1A, sends falling edge
 TIFR1 = 0xFF; // clear all pending timer interrupt flags
 TIMSK1 = (1 << ICIE1); // set interrupt for next input capture
 }

 // Check if user wants to change delay
 while (Serial.available()) {
 char c = Serial.read();
 if (c == '+') {
 currentDelay += 1000UL * 1000 * 16;
 counterUpdatePending = true;
 }
 if (c == '-') {
 currentDelay -= 1000UL * 1000 * 16;
 counterUpdatePending = true;
 }
 }

 if (counterUpdatePending) {
 computeCounterLoad(currentDelay);
 }
}

ISR(TIMER1_CAPT_vect) {
 uint16_t matchValue;
 uint16_t timeStamp = ICR1;
 delayStarted = true; // tell main program we've started
 TIMSK1 = 0; // disable counter interrupts

 if (initialCycleCount == 0) { // Send output pulse on next counter match
 matchValue = timeStamp + terminalTickCount; // counter value to output rising edge
 OCR1A = matchValue; // set next compare time
 TCCR1A = (1 << COM1A1) | (1 << COM1A0); // OC1A high on A-match
 } else { // Not ready for terminal count, wait for next match
 matchValue = timeStamp + initialTickCount;
 OCR1A = matchValue; // set count match value
 }

 TIMSK1 = (1 << OCIE1A); // Enable interrupt on match
 okToUpdate = false;
}

ISR(TIMER1_COMPA_vect) {

 if (initialCycleCount == 0) { // output rising edge has been sent, we're done
 TIMSK1 = 0; // disable counter interrupts
 loadCounters(); // reload counters for next trigger
 delayFinished = true; // tell main program we're done
 okToUpdate = true;
 return;
 }

 initialCycleCount--; // that was an initial count that just completed, decrement counter
 if (initialCycleCount == 0) { // now we're on terminal count
 OCR1A += terminalTickCount; // set count for next match;
 TCCR1A = (1 << COM1A1) | (1 << COM1A0); // OC1A high on A-match
 return;
 }

 OCR1A += initialTickCount; // not on terminal count yet, set count for next match;
}

void computeCounterLoad(uint32_t delay) {
 // Set a new delay value
 // Delays values are integers (uint32_t) in units of 1/16 us (62.5 ns).

 uint32_t localInitialCycleLoad;
 uint16_t localTerminalTickLoad;
 static const uint16_t minTerminalTickCount = 1 << 14;

 if (delay < minDelay) {
 delay = minDelay;
 }
 if (delay > maxDealy) {
 delay = maxDealy;
 }

 if (delay <= maxTickCount) {
 localTerminalTickLoad = delay;
 localInitialCycleLoad = 0;
 } else {
 localInitialCycleLoad = delay >> 15;
 localTerminalTickLoad = delay - (localInitialCycleLoad << 15);
 if (localTerminalTickLoad < minTerminalTickCount) {
 localTerminalTickLoad += initialTickCount;
 localInitialCycleLoad--;
 }
 }

 if (okToUpdate) {
 noInterrupts();
 if (!okToUpdate) { // previous check was not atomic, check again with interrupts off
 interrupts();
 return;
 }
 initialCycleLoad = localInitialCycleLoad;
 terminalTickLoad = localTerminalTickLoad;
 loadCounters();
 counterUpdatePending = false;
 interrupts();
 }
}

void loadCounters() {
 initialCycleCount = initialCycleLoad;
 terminalTickCount = terminalTickLoad;
}

Thank you so much Gfvalvo. I've now had a little time to play with your code and edit/extend it to suit my needs. There are a couple of bugs in small things like simple arithmetic calculations, just down to cut and paste I suspect, but that is always a good thing with gifted code because it forces you to actually go into it and figure it out rather than just run it as given.

It's a pretty awesome tutorial on the hardware approach and it gave me a real lift up the steep learning curve. I've got a much better appreciation of how to go about this now and I'm amazed at the breadth of what is available to directly control the ATMega processors.

One minor irritation is that I have no easy way of testing it for accuracy though it should be pretty much cut and dried with this strategy. As you pointed out, my scope is a basic model for home use rather than the ones I used 'back in the day' which were much wider bandwidth and calibrated regularly. I am also interested in adding in the pretty much non-invasive higher accuracy timer library I used originally, with which I can bracket the critical timing loop and see what it gives for comparison. Your own hardware method should be a lot more accurate of course.

i didn't even expect the additional core of the serial communication code to be able to trim values on the fly, that was a bonus. I expanded it to offer much more control, (+-1, +-10, +-100 stepping of delay from the Serial Monitor for example is very useful).

Thanks again to you and the others who responded with help. You all did the job and got me up and running with the onus on figuring it out for myself, just as it should be. I can see how you are doing what you are doing, it is just down to me to use it correctly now.

I wouldn't trust that timer library or a scope to provide accurate delay measurements. You need a universal counter kind of box that can do time interval measurements between two different trigger events. An HP 53131 / 53132 kind of thing.

Anyway, post your code when do with it.

I only meant to test the other way Gfvalvo. I realise your own method is more accurate so I was going to use your code to look at the accuracy of the timer library purely out of interest. I could bracket your own code timing loop with a little code which would report the start and end times for the 3rd party timer and see how close they were. Believe me I'm sold on your hardware method now I have seen how it is set up.

Hi Gfvalvo. I’m working on making some simple changes to your code in setup() to check my understanding of what is going on. I am constrained by an existing PCB to using pin D10 (pcb pin 13) for my output pulse and pin D2 (pcb pin 5) for my input so I am testing the result of my changing these in the code.

I found the info on the port registers and I seem to have the output working by just changing:

PORTB &= ~(1 << PB1); // PB1 (Pin 9) to 0 when switched to output
DDRB |= (1 << DDB1); // PB1 (Pin 9) as output

to:

PORTB &= ~(1 << PB2); // D2 (Pin 10) to 0 when switched to output
DDRB |= (1 << DDB2); // PB2 (Pin 10) as output

However, I am having trouble seeing how the input pin designation is made in order to change that. I have it edited from your original 8 to 2 in the general code as:

const uint8_t ICP = 2; // Input capture pin D2

I still have the call to:

pinMode(ICP, INPUT_PULLUP); // ‘Pullup’ because of inverting buffer to pin.

…but beyond that I can’t see how your original pin 8 designation is linked to the interrupt ISR(TIMER1_CAPT_vect) function.

In the ATMega328 datasheet I have found DDRD, PORTD and PIND and have set up:

DDRD |= (0 << DDD2);

…but I’m missing something as I’m not getting any response on D2 whereas the original D8 is still fine.

There must be something obvious I am missing, can you point me at it?

EDIT:

With the “show the code” in mind here is the whole setup() function:

void setup() {
   ulCurrentDelay = ulDelay * 1000 * 16;   // Added in order to allow on the fly changing of value
                                                              // but easy revert to original hard coded value of ulDelay

   computeCounterLoad(ulCurrentDelay);

   Serial.begin(115200);
   delay(100);
   Serial.println("Starting");

   // I/O pins
   pinMode(ICP, INPUT_PULLUP);

   // LED pins
   pinMode(PLED, OUTPUT);
   digitalWrite(PLED, LOW);
   pinMode(OLED, OUTPUT);
   digitalWrite(OLED, LOW);
   pinMode(LED_BUILTIN, OUTPUT); // LED_BUILTIN lit during delay period
   digitalWrite(LED_BUILTIN, LOW);

   TCCR1B = 0; // Stop counter, waveform generator Mode 0
   TCCR1B |= (0 << ICES1); // capture timer count on input on falling edge of ICP, use (1 << ICES1) for rising edge capture

   TCCR1A = 0;         // waveform generator Mode 0
   TCNT1 = 0;          // reset counter
   OCR1A = 0xFFFF;       // park output compare value here for now

   TCCR1A = (1 << COM1A1);   // OC1A low on A-match, waveform generator Mode 0
   TCCR1C = (1 << FOC1A);    // Force A-match, OC1A Low
   TCCR1A = 0;         // disconnect OC1A

   PORTB &= ~(1 << PB2);   // B2 (Pin 10) to 0 when switched to output 
   DDRB |= (1 << DDB2);    // PB2 (Pin 10) as output

   DDRD |= (0 << DDD2);   // D2 (pin 2) as input
   
   TCCR1B |= (1 << CS10);  // Start counter, bypass prescaler
   TIFR1 = 0xFF; // clear all pending timer interrupt flags
   TIMSK1 = (1 << ICIE1); // set interrupt for first input capture

}

bordonbert:
Hi Gfvalvo. I'm working on making some simple changes to your code in setup() to check my understanding of what is going on. I am constrained by an existing PCB to using pin D10 (pcb pin 13) for my output pulse and pin D2 (pcb pin 5) for my input

When using Timer 1, the input capture pin must be PB0 or the output of the Analog Comparator. See Section 16.6.1 of the ATMega328P datasheet.

The output pulse from the output compare operation must come from PB1 (Compare Unit A) or PB2 (Compare Unit B). So, you'll have to switch all the code to use Compare Unit B in order to use PB2. See Section 16.7 of the ATMega328P datasheet.