Using UNO to operate imaging system with millisecond accuracy

Hi all,

I have an Arduino Uno, and I am trying to use it to control a camera and LED in an imaging system I assembled.

My goal is to use the Arduino to send trigger signals to the camera telling it to turn exposure on, wait for 1 millisecond, then send a trigger to the LED to turn on and wait 10 milliseconds with the camera and LED on, then turn LED off and wait 1 millisecond until turning the camera off as well. With both camera and LED off, I want to wait 38 milliseconds so that overall, the frame rate of the camera is 20 Hz.

In my first attempt, I used the delay() function to send HIGH signals, wait, then send LOW signals, but I found out that the delay function is not very precise. I have since tried using the millis() function to get more accurate timing, but I have found that if I record for 1 minute, the final time is off by around 80ms (might be getting delayed by the serial printing of the time itself?).

I am interesting in recording for 10-20 minutes, so being off by around 1 second is not ideal (if I could keep the error to under the 10s of milliseconds, that would be great). I attached my code below, but any assistance regarding how I could use the Arduino to get millisecond precision would be great. Thank you!

/*
  Turns LEDs and cameras on and off to record fluorescent reflectance and emission
*/

#define blue_LED 9
#define xyla_CAM 7
#define DAQoutpin 11
char magic = '!';

// for 20Hz frame rate, we want 50ms overall frame time
const unsigned long idleTime = 1000;            //1thousand millis means 1 second of delay before ANYTHING
const unsigned long deltaTime = 1;              // in millis, THIS is timing interval for arduino doing anything (period) (1ms)
const unsigned long camOnledOffDarkframe = 12;  // this is a 12ms long dark frame
const unsigned long camOnledOffInterval = 1;    //  1ms delay of cam on led off
const unsigned long camOnledOnInterval = 10;    // 10ms cam and led both on
const unsigned long camOffledOffInterval = 19;  //  19ms (x2 is 38ms) of cam and led off
unsigned long startTime;
unsigned long currentTime;
unsigned long phaseStartTime;
const int numDarkFrames = 20;          //this will be checked against darkFrameCounter
const int numLightFrames = 1200;         //this will be checked against lightFrameCounter (60sec*20Hz = 1200frames)
int darkFrameCounter = 0;      //updates to determine next time in which the if statement should execute
int lightFrameCounter = 0;

unsigned int time = 0; // testing

int phase = 1;
byte state = LOW;  //toggle whether to collect sample
bool started = false;


// the setup function runs once when you press reset or power the board
void setup() {
  pinMode(blue_LED, OUTPUT);
  pinMode(xyla_CAM, OUTPUT);
  pinMode(DAQoutpin, OUTPUT);
  Serial.begin(115200);

  // Identify device
  while (true) {
    if (Serial.available() > 0) {
      char id_input = Serial.read();
      if (id_input == magic) {
        // Serial.write(deviceID,3);
        Serial.println("Starting...");
        Serial.println("Time, Phase");
        break;
      }
    }
  }
}

void loop() {
  time = micros();
  if (!started) {
    startTime = millis();  // this is the time in milliseconds when we start (i.e. t=0)
    started = true;
    digitalWrite(DAQoutpin, HIGH);
  } 
  else {
    currentTime = millis();                      // this the current time used to check against start time for loop iterations
    if (currentTime - startTime >= deltaTime) {  // if at least a millisecond has elapsed since entering the loop
      switch (phase) {
        case 0:  // idle, wait 1 second before collecting dark frames
          if (currentTime - startTime >= idleTime) {
            phaseStartTime = currentTime;
            phase = 1;
            digitalWrite(xyla_CAM, LOW);
            digitalWrite(blue_LED, LOW);
          }
          break;
        case 1:  // have cam and led off before collecting frame
          digitalWrite(xyla_CAM, LOW);
          digitalWrite(blue_LED, LOW);
          if (currentTime - phaseStartTime >= camOffledOffInterval) {
            phaseStartTime = currentTime;
            phase = 2;
            digitalWrite(xyla_CAM, HIGH);
            digitalWrite(blue_LED, LOW);
          }
          break;
        case 2:  // camera on and led off for dark frame collection
          digitalWrite(xyla_CAM, HIGH);
          digitalWrite(blue_LED, LOW);
          if (currentTime - phaseStartTime >= camOnledOffDarkframe) {
            phaseStartTime = currentTime;
            phase = 3;
            digitalWrite(xyla_CAM, LOW);
            digitalWrite(blue_LED, LOW);
          }
          break;
        case 3:  // cam and led off for inter frame interval
          digitalWrite(xyla_CAM, LOW);
          digitalWrite(blue_LED, LOW);
          if (currentTime - phaseStartTime >= camOffledOffInterval) {
            phaseStartTime = currentTime;
            darkFrameCounter++;  //this increases dark frame counter by 1 every time
            if (darkFrameCounter >= numDarkFrames) {
              phase = 4;
            } else {
              phase = 1;
            }
            digitalWrite(xyla_CAM, LOW);
            digitalWrite(blue_LED, LOW);
          }
          break;
        case 4:  // starting actual light frame collection, starting w cam and led off
          digitalWrite(xyla_CAM, LOW);
          digitalWrite(blue_LED, LOW);
          if (currentTime - phaseStartTime >= camOffledOffInterval) {
            phaseStartTime = currentTime;
            phase = 5;
            digitalWrite(xyla_CAM, HIGH);
            digitalWrite(blue_LED, LOW);
          }
          break;
        case 5:  // camera on before LED turns on
          digitalWrite(xyla_CAM, HIGH);
          digitalWrite(blue_LED, LOW);
          if (currentTime - phaseStartTime >= camOnledOffInterval) {
            phaseStartTime = currentTime;
            phase = 6;
            digitalWrite(xyla_CAM, HIGH);
            digitalWrite(blue_LED, HIGH);
          }
          break;
        case 6:  // camera on and LED on
          digitalWrite(xyla_CAM, HIGH);
          digitalWrite(blue_LED, HIGH);
          if (currentTime - phaseStartTime >= camOnledOnInterval) {
            phaseStartTime = currentTime;
            phase = 7;
            digitalWrite(xyla_CAM, HIGH);
            digitalWrite(blue_LED, LOW);
          }
          break;
        case 7:  // camera on and LED off
          digitalWrite(xyla_CAM, HIGH);
          digitalWrite(blue_LED, LOW);
          if (currentTime - phaseStartTime >= camOnledOffInterval) {
            phaseStartTime = currentTime;
            phase = 8;
            digitalWrite(xyla_CAM, LOW);
            digitalWrite(blue_LED, LOW);
          }
          break;
        case 8:  // camera off and LED off, update counter
          digitalWrite(xyla_CAM, LOW);
          digitalWrite(blue_LED, LOW);
          if (currentTime - phaseStartTime >= camOffledOffInterval) {
            phaseStartTime = currentTime;
            lightFrameCounter++;  //this increases light frame counter by 1 every time
            if (lightFrameCounter >= numLightFrames) {
              phase = 9;
              digitalWrite(DAQoutpin, LOW);

            } else {
              phase = 4;
            }
            digitalWrite(xyla_CAM, LOW);
            digitalWrite(blue_LED, LOW);
          }
          break;
        case 9: // make sure everything turns off
          digitalWrite(xyla_CAM, LOW);
          digitalWrite(blue_LED, LOW);
          break;
      }
      //if (phase != 9) {
        //Serial.println(String(currentTime - startTime) + "," + String(phase));
      //}
    }
  }
  if (phase = 9){
    Serial.println(String(currentTime - startTime));
  }
  //Serial.println(time, DEC);
  //Serial.println(String(currentTime - startTime));
}

Recently, keyboards added a new key, labeled ENTER or RETURN.

It helps break text into paragraphs, so the readers can catch a breath while reading.

CLICK HERE

3 Likes

You specify milliseconds "here and there". Why? No eye will register that. It looks like a "paper design", not connected to the reality.
Do some testing, checking what Your eye say about a 10 ms blink! Expand the test and find out how long LED on time is needed to be noticed!

Thanks for the advice! Did you have any guidance for the actual problem, or was the snarky wiki link the only help you could offer?

Just to follow up on @lastchancename, this post is quite difficult to read, breaking up will help us digest your issue better. Perhaps visiting this site might help you understand better how to use Arduino for timing events.

What I would suggest is connecting an oscilloscope to measure the timing capabilities of your script and to assess whether an Arduino is the best fit for your use case (could consider using a different device if the Arduino timing is not up to par.)

Before coming on here next time please consider the time that users take to respond to these inquiries!

I am using a camera to image, not my eye.

The loss of accuracy arises because you are accumulating (wasting) small time intervals here and there.

For instance, by the time you execute this, millis() has advanced maybe 1 millisecond. You should put this at the very top of the loop().

The same applies to all your intervals. Those intervals actually begin when you run

and by then several cpu cycles have gone by.

But, instead of using individual interval times for each phase you can instead use “absolute” times relative to the beginning of the cycle (currentTime)

Shutter speed, time between exposures? Still looks not very easy.

Another approach to the timing would be to set up Timer1 to overflow at 25ms. With a 16MHz clock, you could set the prescaler to 8, which reduces the Timer1 clock source to 2MHz. Setting the overflow value to 50,000 would give you 40 overflows per second, one every 25ms.

You would pre-calculate the timer count values that correspond to the actions you want to take., and when the current count value reaches each such point you would take that action, then wait until the next count value is reached. On the second overflow you would do nothing, and the combined two overflows per frame would give you 20Hz.

while(timercount < target1);   // wait for first count value
//do step1;

while(timercount < target2);
//do step2;
// etc.

Then at the end of the frame you would have some count left to go, plus another complete overflow during which nothing happens.

while(timercount);
while(timercount == 0);
while(timercount);

Something like this would give you exactly 20fps, but you would need to adjust the counter points to account for the time it takes to execute each step.

Edit: Ideally you would also disable the millis() interrupt so it won't throw off your timing.

Use '==' for testing equality. Print without using Strings:

  if (phase == 9) {
    Serial.println(currentTime - startTime);
  }

Know that millis() advances by two from time to time.

Use micros() for the timing. Get clever about providing status and reporting without using printing you can't be certain isn't interfering.

Please say how accurate you need the timing to be.

Know that the UNO system clock isn't precise, certainly not good enough for a regular clock to keep good time. I know it's bad, dunno how bad.

I can't now but if your code is almost working I don't see why a few tweaks wouldn't get you there.

Prepare outputs to be ready to go on the edge of the clock. Take all your sweet time figuring out the next outputs.

a7

An arduino's standard ceramic oscillator is good for 0.5% precision, which is 6 seconds in 20 minutes. There are clones and arduinos with actual crystal oscillators that should be able to do 20ppm, which in the 20*60*1000= 1200000ms should be +-24ms.

You should be basing your time off of micros to get millisecond precision, and if you want a stable frame rate at 20Hz, you should step currentTime forward by 50ms (or 50000us) instead of including whatever slop finished the cycle. I mean that instead of:

do:

    startTime += 50;  // this is the time in milliseconds when we start (i.e. t=0)

or

    startTime += 50000;  // this is the time in microseconds when we start (i.e. t=0)

I've hacked away (I have no life), and have not taken into account @DaveX's post, which I can assure you I should do, and.

I edited your code to remove things like the magic '!' startup character, and changed some constants so the thing finishes after having cycled through all it can do for waaaay less than life too short.

Switching to micros(), I see a train of 11.99 ms pulses at 50.05 Hz, then a train of pulses at 50.04 Hz, a 12.02 ms pulse with a 10.006 ms pulse centered within that pulse. I have no time to review your stated requirements, I will assume that is correct as I have assumed the code works but not well enough yet.

So yeah, micros(), and a bit of rearrangement to place the slop outside the timing constraints. I think publishing the digital outputs on the clock (calculated last time) would remove some of the remaining variation.

I used the wokwi simulator. And it has one of these

which I have IRL and since I got it my considerably more spendy "real" logic analyzer has been needed almost never.

Highly recommended both, the simulator and the simulated or real logic analyzer.

Using it will let you gather some stats without needing to put diagnostic anything in your code.

a7

1 Like

+1

@dlloyd also simulated 4-channel 0.1MHz oscilloscope and PWM generator for Wokwi: