Skip to content

Double Exposure Laser Illuminator

luni64 edited this page Aug 28, 2020 · 4 revisions

This example shows how the TeensyTimerTool is used to control two pulsed, illuminating lasers and a camera to capture two images in quick succession.

As shown in the figure above, the lasers used require a pre-trigger before the laser pulse can be triggered by the main trigger. Of course, the camera shutter must be open while the laser illuminates the scene. However, to avoid excessive image noise, it should only be opened as short as necessary.

Modelling

It is always a good idea to break down programming tasks into small, manageable and of course testable units.

As shown in the timing diagram above, the lasers and the camera are controlled by sending out some pulsed signals. Those pulses are characterized by their duration and their start time. So, it might be a good idea to have a PulseGenerator class to take care of the details of pulse generation.

On a higher level of abstraction we will need a LaserController class which handles the scheduling of the pre- and main laser triggers and the camera shutter. Of course it will use PulseGenerator objects to outsource the actual pulse generation.

Lastly, we will define a SystemController class which synchronizes the two LaserControllers in such a way that they generate the needed delay time between two exposures. The class will also provide some convenience functions like repetitive shooting and setting the exposure delay time.

Classes

The Pulse Generator Class

class PulseGenerator
{
 public:
    PulseGenerator();
    void begin(unsigned pin);
    void schedulePulse(float delay, float width);

 protected:
    TeensyTimerTool::OneShotTimer pulseTimer;
    void callback();

    uint32_t pin;
    float width;
};

The PulseGenerator class has a very simple interface: Besides the usual pair of a constructor and a begin function it only provides a schedulePulse function which sets up the internal timer to generate the pulse at the given time and duration and returns immediately after that.

The actual timing is done by a TeensyTimerTool::OneShotTimer. The begin function attaches the class member callback() to this timer. Both, the constructor and the begin function are implemented straight forward, you find the complete code here.

Lets have a quick look at the implementation of the pulse generation code.

void PulseGenerator::schedulePulse(float delay, float _width)
{
    width = _width;

    if (delay != 0)
    {
        pulseTimer.trigger(delay);
    }
    else
    {
        digitalWriteFast(pin, HIGH);
        pulseTimer.trigger(width);
    }
}

void PulseGenerator::callback()
{
    if (digitalReadFast(pin) == LOW)
    {
        digitalWriteFast(pin, HIGH);
        pulseTimer.trigger(width);   // retrigger the timer
    } else
    {
        digitalWriteFast(pin, LOW);
    }
}

The schedulePulse function is called with the parameters delay and width. It distinguishes if the caller wants to start the pulse immediately (delay==0) or after the passed in delay time. In case an immediate pulse is requested schedulePulse sets the pulse pin HIGH and starts the OneShotTimer to fire after 'width µs'. The callback then sets the pin back to LOW. In case of a delayed pulse it sets up the timer to fire after 'delay µs'. In this case the callback will set the pin HIGH and retrigger the timer to switch it off after another 'width µs'.

The callback function uses the state of the pulse pin to determine in which mode it was called. If the pin is LOW, it was called in delayed mode. Thus, it needs to set the pin to HIGH and then re-trigger the timer to reset it later. If the pin was already HIGH, it just needs to reset it.

Unit Test

Let's do a quick unit test of the class with the following sketch.

#include "PulseGenerator.h"

PulseGenerator g1, g2, g3;

void setup()
{
    g1.begin(1);
    g2.begin(2);
    g3.begin(3);
}

void loop()
{
    g1.schedulePulse(0, 2'000);      // immediate 2ms pulse on pin 1
    g2.schedulePulse(5'000, 10'000); // 10ms pulse on pin 2 starting  5ms after first pulse
    g3.schedulePulse(1'000, 5'000);  // 1ms pulse on pin3 starting 5ms after first pulse

    delay(50);
}

Which gives the following, correct measurement:

Unit Test

Looks good, so lets call this PASSED

The Laser Controller Class

The LaserController class is even simpler than the PulseGenerator class we discussed above.

class LaserController
{
 public:
    void begin(unsigned preTriggerPin, unsigned triggerPin, unsigned camPin);
    void shoot();

 protected:
    PulseGenerator preTrigger, trigger, camera;
};

Again, the begin function is implemented in a straight forward way and doesn't need to be discussed here. The complete code can be found here.

The the shoot function triggers one shot of the laser and opens the camera shutter at the right time. I.e., it schedules an immediate pre-trigger pulse, a delayed main-trigger pulse and a delayed camera pulse. Its definition is quite simple:

void LaserController::shoot()
{
    constexpr float t_warmup = 140 - 5.5;
    constexpr float t_p = 10 - 3;
    constexpr float t_camDelay = 130 - 7.5;
    constexpr float t_int = 30 - 3;

    preTrigger.schedulePulse(0, t_p);
    trigger.schedulePulse(t_warmup, t_p);
    camera.schedulePulse(t_camDelay, t_int);
}

The correction terms to the timing constants are required to compensate for the execution time of the schedulePulse and the underlying OneShotTimer.trigger functions. In principle, these effects could be minimized by pre calculating the timer reload values and choosing a smaller timer pre-scaler value for the FTM0 module (see Configuration). However, for the sake of simplicity the required correction terms were determined experimentally and are set manually as shown above.

Unit Test

#include "LaserController.h"

LaserController laserController;

void setup()
{
    laserController.begin(1, 2, 3);
}

void loop()
{
    laserController.shoot();
    delay(1);
}

Here the result for 140µs warmup time, 130µs camera delay, 10µs pulse width and 30µs shutter time.

Unit Test

-> PASS

The System Controller Class

The last class to discuss is the high level SystemController:

class SystemController
{
 public:
    SystemController();
    void begin();
    void shoot();

    void continousMode(bool on);
    void setExposureDelay(unsigned delay);

 protected:
    TeensyTimerTool::PeriodicTimer mainTimer;
    LaserController lCtrl1, lCtrl2;
    unsigned exposureDelay = 300;
};

Its main purpose is to synchronize the two laser controllers to achieve the desired time between two exposures which happens in its shoot() function.

void SystemController::shoot()
{
    elapsedMicros stopwatch = 0;
    lCtrl1.shoot();
    while (stopwatch < exposureDelay) { yield(); }
    lCtrl2.shoot();
}

It fires the first laser by calling its shoot function. Since shoot is not blocking it returns quickly but still needs a few microseconds to execute. To eliminate the inaccuracies caused by this, we don't use a simple delay between the shots. Instead, we reset a stopwatch before the first shoot and issue the shoot of the second laser when the stopwatch equals the required exposure delay.

Convenience functions

The delay between both exposures can be set at any time with the setExposureDelay function.

The class also comprises a TCK based PeriodicTimer which is used to periodically call shoot. It can be enabled by the continuousMode function.

The implementation of both functions is straight forward. Again, you find the complete code here: https://github.com/luni64/TeensyTimerTool/tree/master/examples/DoubleExposure

Measurement Results

ere a measurement showing the generated pulses for the following settings (see the timing diagram above for details)

Time Value
twarmup 140 µs
tcam-delay 130 µs
tdelta 500 µs
tp 10 µs
tint 30 µs