Important changes to repositories hosted on mbed.com
Mbed hosted mercurial repositories are deprecated and are due to be permanently deleted in July 2026.
To keep a copy of this software download the repository Zip archive or clone locally using Mercurial.
It is also possible to export all your personal repositories from the account settings page.
Dependencies: mbed FastIO FastPWM USBDevice
Fork of Pinscape_Controller by
TSL1410R/tsl1410r.h
- Committer:
- mjr
- Date:
- 2016-02-18
- Revision:
- 47:df7a88cd249c
- Parent:
- 45:c42166b2878c
- Child:
- 48:058ace2aed1d
File content as of revision 47:df7a88cd249c:
/*
* TSL1410R/TSL1412R interface class.
*
* This provides a high-level interface for the Taos TSL1410R and
* TSL1412R linear CCD array sensors. (The 1410R and 1412R are
* identical except for the pixel array size, which the caller
* provides as a parameter when instantiating the class.)
*
* For fast reads of the pixel file from the sensor, we use the KL25Z's
* DMA capability. The method we use is very specific to the KL25Z
* hardware and is pretty tricky. These are attributes I don't normally
* like, but the speedup is amazing, enough to justify the complex
* design. I don't think there's any other way to even get close to
* this kind of read speed for this sensor with this MCU. (And I've
* tried!) Reading the sensor quickly is more than just academic,
* too: the plunger moves very fast during a release motion, so we
* have to be able to take correspondingly fast pictures to get
* clean images without motion blur. Before the speedup of this DMA
* approach, images captured during release motions had a lot of
* motion blur, and even aliasing from the sinusoidal motion when
* the plunger bounces back and forth off the springs. The speedup
* gets us over the threshold where we can capture images with very
* little blur, so we can track the motion much more precisely even
* at release speeds. This lets us determine the plunger position
* more precisely and more quickly, which improves responsiveness in
* the pinball simulator on the PC.
*
* Here's our approach.
*
* First, we put the analog input port (the ADC == Analog-to-Digital
* Converter) in "continuous" mode, at the highest clock speed we can
* program with the available clocks and the fastest read cycle
* available in the ADC hardware. (The analog input port is the
* GPIO pin attached to the sensor's AO == Analog Output pin, where
* it outputs each pixel's value, one at a time, as an analog voltage
* level.) In continuous mode, every time the ADC finishes taking a
* sample, it stores the result value in its output register and then
* immediately starts taking a new sample. This means that no MCU
* (or even DMA) action is required to start each new sample. This
* is where most of the speedup comes from, since it takes significant
* time (multiple microseconds) to move data through the peripheral
* registers, and it takes more time (also multiple microseconds) for
* the ADC to spin up for each new sample when in single-sample mode.
* We cut out about 7us this way and get the time per sample down to
* about 2us. This is close to the documented maximum speed for the
* ADC hardware.
*
* Second, we use the DMA controller to read the ADC result register
* and store each sample in a memory array for processing. The ADC
* hardware is designed to work with the DMA controller by signaling
* the DMA controller when a new sample is ready; this allows DMA to
* move each sample immediately when it's available without any CPU
* involvement.
*
* Third - and this is where it really gets tricky - we use two
* additional "linked" DMA channels to generate the clock signal
* to the CCD sensor. The clock signal is how we tell the CCD when
* to place the next pixel voltage on its AO pin, so the clock has
* to be generated in lock step with the ADC sampling cycle. The
* ADC timing isn't perfectly uniform or predictable, so we can't
* just generate the pixel clock with a *real* clock. We have to
* time the signal exactly with the ADC, which means that we have
* to generate it from the ADC "sample is ready" signal. Fortunately,
* there is just such a signal, and in fact we're already using it,
* as described above, to tell the DMA when to move each result from
* the ADC output register to our memory array. So how do we use this
* to generate the CCD clock? The answer lies in the DMA controller's
* channel linking feature. This allows one DMA channel to trigger a
* second DMA channel each time the first channel completes one
* transfer. And we can use DMA to control our clock GPIO pin by
* using the pin's GPIO IPORT register as the DMA destination address.
* Specifically, we can take the clock high by writing our pin's bit
* pattern to the PSOR ("set output") register, and we can take the
* clock low by writing to the PCOR ("clear output") register. We
* use one DMA channel for each of these operations.
*
* Putting it all together, the cascade of linked DMA channels
* works like this:
*
* - The ADC sample completes, which triggers channel 1, the
* "Clock Up" channel. This performs one transfer of the
* clock GPIO bit to the clock PSOR register, taking the clock
* high, which causes the CCD to move the next pixel onto AO.
*
* - After the Clock Up channel does its transfer, it triggers
* its link to channel 2, the ADC transfer channel. This
* channel moves the ADC output register value to our memory
* array.
*
* - After the ADC channel does its transfer, it triggers channel
* 3, the "Clock Down" channel. This performs one transfer of
* the clock GPIO bit to the clock PCOR register, taking the
* clock low.
*
* Note that the order of the channels - Clock Up, ADC, Clock Down -
* is important, because it ensures that we don't toggle the clock
* bit too fast. The CCD has a minimum pulse duration of 50ns for
* the clock signal. The DMA controller is so fast that we could
* toggle the clock faster than this limit if we did the Up and
* Down transfers adjacently.
*
* Note also that it's important that Clock Up be the first operation,
* because the ADC is in continuous mode, meaning that it starts
* taking a new sample immediately upon finishing the previous one.
* So when the ADC DMA signal fires, the new sample is just starting.
* We therefore have to get the next pixel onto the sampling pin as
* quickly as possible. The CCD sensor's "analog output settling
* time" is 120ns - this is the time for a new pixel voltage to
* stabilize on AO after a clock rising edge. So assuming that the
* ADC raises the DMA signal immediately, and the DMA controller
* responds within a couple of MCU clock cycles, we should have the
* new pixel voltage stable on the sampling pin by about 200ns after
* the new ADC sample cycle starts. The sampling cycle with our
* current parameters is about 2us, so the voltage level is stable
* for 90% of the cycle.
*
* Also, it's okay that the ADC sample transfer doesn't happen until
* after the Clock Up DMA transfer. The ADC output register holds the
* last result until the next sample completes, so we have about 2us
* to grab it. The first Clock Up DMA transfer only takes a couple
* of clocks - order of 100ns - so we get to it with time to spare.
*
* (Note that it's tempting to try to handle the clock with a single
* DMA channel, by using the PTOR "toggle output" to do TWO writes:
* one to toggle the clock up and another to toggle it down. But
* I haven't found a good way to do this. The problem is that the
* DMA controller can only do one transfer per trigger in the fully
* autonomous mode we're using, and we need to do two writes. In
* fact, we'd really need to do three or four writes: we'd have to
* throw in one or two no-op writes (of all zeroes) between the two
* toggles, for time padding to ensure that we meet the minimum 50ns
* pulse width for the TSL1410R clock signal. But it's the same
* issue whether it's two writes or four. The DMA controller does
* have a "continuous" mode that does an entire transfer on a single
* trigger, but it can't reset itself after such a transfer, so CPU
* intervention would be required on every ADC cycle to set up the
* next clock write. We could do that with an interrupt, but given
* the 2us cycle time, an interrupt would create a ton of CPU load,
* and probably isn't even enough time to reliably complete each
* interrupt service call before the next cycle. Fortunately, at
* the moment we only have one other module in the whole system
* using DMA at all - the TLC5940 PWM controller interface, which
* only needs one channel. So with the four available channels in
* the hardware, we can afford to use three of them here.)
*/
#include "mbed.h"
#include "config.h"
#include "AltAnalogIn.h"
#include "SimpleDMA.h"
#include "DMAChannels.h"
#ifndef TSL1410R_H
#define TSL1410R_H
// To allow DMA access to the clock pin, we need to point the DMA
// controller to the IOPORT registers that control the pin. PORT_BASE()
// gives us the address of the register group for the 32 GPIO pins with
// the same letter name as our target pin (e.g., PTA0 through PTA31),
// and PINMASK gives us the bit pattern to write to those registers to
// access our single GPIO pin. Each register group has three special
// registers that update the pin in particular ways: PSOR ("set output
// register") turns pins on, PCOR ("clear output register") turns pins
// off, and PTOR ("toggle output register") toggle pins to the opposite
// of their current values. These registers have special semantics:
// writing a bit as 0 has no effect on the corresponding pin, while
// writing a bit as 1 performs the register's action on the pin. This
// allows a single GPIO pin to be set, cleared, or toggled with a
// 32-bit write to one of these registers, without affecting any of the
// other pins addressed by the register. (It also allows changing any
// group of pins with a single write, although we don't use that
// feature here.)
//
// - To turn a pin ON: PORT_BASE(pin)->PSOR = PINMASK(pin)
// - To turn a pin OFF: PORT_BASE(pin)->PCOR = PINMASK(pin)
// - To toggle a pin: PORT_BASE(pin)->PTOR = PINMASK(pin)
//
#define GPIO_PORT(pin) (((unsigned int)(pin)) >> PORT_SHIFT)
#define GPIO_PORT_BASE(pin) ((GPIO_Type *)(PTA_BASE + GPIO_PORT(pin) * 0x40))
#define GPIO_PINMASK(pin) gpio_set(pin)
class TSL1410R
{
public:
TSL1410R(int nPixSensor, PinName siPin, PinName clockPin, PinName ao1Pin, PinName /*ao2Pin*/)
: adc_dma(DMAch_ADC),
clkUp_dma(DMAch_CLKUP),
clkDn_dma(DMAch_CLKDN),
si(siPin),
clock(clockPin),
ao1(ao1Pin, true),
nPixSensor(nPixSensor)
{
// allocate our double pixel buffers
pix1 = new uint8_t[nPixSensor*2];
pix2 = pix1 + nPixSensor;
// put the first DMA transfer into the first buffer (pix1)
pixDMA = 0;
// remember the clock pin port base and pin mask for fast access
clockPort = GPIO_PORT_BASE(clockPin);
clockMask = GPIO_PINMASK(clockPin);
// clear out power-on random data by clocking through all pixels twice
clear();
clear();
// Set up the Clock Up DMA channel. This channel takes the
// clock high by writing the clock bit to the PSOR (set output)
// register for the clock pin.
clkUp_dma.source(&clockMask, false, 32);
clkUp_dma.destination(&clockPort->PSOR, false, 32);
// Set up the Clock Down DMA channel. This channel takes the
// clock low by writing the clock bit to the PCOR (clear output)
// register for the clock pin.
clkDn_dma.source(&clockMask, false, 32);
clkDn_dma.destination(&clockPort->PCOR, false, 32);
// Set up the ADC transfer DMA channel. This channel transfers
// the current analog sampling result from the ADC output register
// to our pixel array.
ao1.initDMA(&adc_dma);
// Set up our chain of linked DMA channel:
// ADC sample completion triggers Clock Up
// ...which triggers the ADC transfer
// ... which triggers Clock Down
clkUp_dma.trigger(Trigger_ADC0);
clkUp_dma.link(adc_dma);
adc_dma.link(clkDn_dma, false);
// Set the trigger on the downstream links to NONE - these are
// triggered by their upstream links, so they don't need separate
// peripheral or software triggers.
adc_dma.trigger(Trigger_NONE);
clkDn_dma.trigger(Trigger_NONE);
// Register an interrupt callback so that we're notified when
// the last transfer completes.
clkDn_dma.attach(this, &TSL1410R::transferDone);
// clear the timing statistics
totalTime = 0.0;
nRuns = 0;
}
// end of transfer notification
void transferDone()
{
// stop the ADC sampler
ao1.stop();
// clock out one extra pixel to leave A1 in the high-Z state
clock = 1;
clock = 0;
// stop the clock
t.stop();
// count the statistics
totalTime += t.read();
nRuns += 1;
}
// Get the stable pixel array. This is the image array from the
// previous capture. It remains valid until the next startCapture()
// call, at which point this buffer will be reused for the new capture.
void getPix(uint8_t * &pix, int &n)
{
// return the pixel array that ISN'T assigned to the DMA
pix = pixDMA ? pix1 : pix2;
n = nPixSensor;
}
// Start an image capture from the sensor. This waits for any previous
// capture to finish, then starts a new one and returns immediately. The
// new capture proceeds autonomously via the DMA hardware, so the caller
// can continue with other processing during the capture.
void startCapture()
{
// wait for the previous transfer to finish
while (adc_dma.isBusy()) { }
// swap to the other DMA buffer
pixDMA ^= 1;
// start timing this transfer
t.reset();
t.start();
// set up the active pixel array as the destination buffer for
// the ADC DMA channel
adc_dma.destination(pixDMA ? pix2 : pix1, true);
// start the DMA transfers
clkDn_dma.start(nPixSensor*4);
adc_dma.start(nPixSensor);
clkUp_dma.start(nPixSensor*4);
// start the next integration cycle by pulsing SI and one clock
si = 1;
clock = 1;
si = 0;
clock = 0;
// clock in the first pixel
clock = 1;
clock = 0;
// Start the ADC sampler. The ADC will read samples continuously
// until we tell it to stop. Each sample completion will trigger
// our linked DMA channel, which will store the next sample in our
// pixel array and pulse the CCD serial data clock to load the next
// pixel onto the analog sampler pin. This will all happen without
// any CPU involvement, so we can continue with other work.
ao1.start();
}
// Clock through all pixels to clear the array. Pulses SI at the
// beginning of the operation, which starts a new integration cycle.
// The caller can thus immediately call read() to read the pixels
// integrated while the clear() was taking place.
void clear()
{
// clock in an SI pulse
si = 1;
clockPort->PSOR = clockMask;
si = 0;
clockPort->PCOR = clockMask;
// clock out all pixels
for (int i = 0 ; i < nPixSensor + 1 ; ++i)
{
clock = 1;
clock = 0;
}
}
// get the timing statistics
void getTimingStats(float &totalTime, uint32_t &nRuns)
{
totalTime = this->totalTime;
nRuns = this->nRuns;
}
private:
// DMA controller interfaces
SimpleDMA adc_dma; // DMA channel for reading the analog input
SimpleDMA clkUp_dma; // "Clock Up" channel
SimpleDMA clkDn_dma; // "Clock Down" channel
// Sensor interface pins
DigitalOut si; // GPIO pin for sensor SI (serial data)
DigitalOut clock; // GPIO pin for sensor SCLK (serial clock)
GPIO_Type *clockPort; // IOPORT base address for clock pin - cached for DMA writes
uint32_t clockMask; // IOPORT register bit mask for clock pin
AltAnalogIn ao1; // GPIO pin for sensor AO (analog output)
// number of pixels in the physical sensor array
int nPixSensor; // number of pixels in physical sensor array
// pixel buffers - we keep two buffers so that we can transfer the
// current sensor data into one buffer via DMA while we concurrently
// process the last buffer
uint8_t *pix1; // pixel array 1
uint8_t *pix2; // pixel array 2
// DMA target buffer. This is the buffer for the next DMA transfer.
// 0 means pix1, 1 means pix2. The other buffer contains the stable
// data from the last transfer.
uint8_t pixDMA;
// timing statistics
Timer t; // timer - started when we start a DMA transfer
float totalTime; // total time consumed by all reads so far
uint32_t nRuns; // number of runs so far
};
#endif /* TSL1410R_H */
