-
Notifications
You must be signed in to change notification settings - Fork 19
Double Exposure Laser Illuminator
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.
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.
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.
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:
Looks good, so lets call this PASSED
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.
#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.
-> PASS
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.
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
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 |
TeensyTimerTool - Generic Interface to Teensy Timers