An I/O controller for virtual pinball machines: accelerometer nudge sensing, analog plunger input, button input encoding, LedWiz compatible output controls, and more.

Dependencies:   mbed FastIO FastPWM USBDevice

Fork of Pinscape_Controller by Mike R


This is Version 2 of the Pinscape Controller, an I/O controller for virtual pinball machines. (You can find the old version 1 software here.) Pinscape is software for the KL25Z that turns the board into a full-featured I/O controller for virtual pinball, with support for accelerometer-based nudging, a real plunger, button inputs, and feedback device control.

In case you haven't heard of the concept before, a "virtual pinball machine" is basically a video pinball simulator that's built into a real pinball machine body. A TV monitor goes in place of the pinball playfield, and a second TV goes in the backbox to serve as the "backglass" display. A third smaller monitor can serve as the "DMD" (the Dot Matrix Display used for scoring on newer machines), or you can even install a real pinball plasma DMD. A computer is hidden inside the cabinet, running pinball emulation software that displays a life-sized playfield on the main TV. The cabinet has all of the usual buttons, too, so it not only looks like the real thing, but plays like it too. That's a picture of my own machine to the right. On the outside, it's built exactly like a real arcade pinball machine, with the same overall dimensions and all of the standard pinball cabinet hardware.

A few small companies build and sell complete, finished virtual pinball machines, but I think it's more fun as a DIY project. If you have some basic wood-working skills and know your way around PCs, you can build one from scratch. The computer part is just an ordinary Windows PC, and all of the pinball emulation can be built out of free, open-source software. In that spirit, the Pinscape Controller is an open-source software/hardware project that offers a no-compromises, all-in-one control center for all of the unique input/output needs of a virtual pinball cabinet. If you've been thinking about building one of these, but you're not sure how to connect a plunger, flipper buttons, lights, nudge sensor, and whatever else you can think of, this project might be just what you're looking for.

You can find much more information about DIY Pin Cab building in general in the Virtual Cabinet Forum on Also visit my Pinscape Resources page for more about this project and other virtual pinball projects I'm working on.


  • Pinscape Release Builds: This page has download links for all of the Pinscape software. To get started, install and run the Pinscape Config Tool on your Windows computer. It will lead you through the steps for installing the Pinscape firmware on the KL25Z.
  • Config Tool Source Code. The complete C# source code for the config tool. You don't need this to run the tool, but it's available if you want to customize anything or see how it works inside.


The new Version 2 Build Guide is now complete! This new version aims to be a complete guide to building a virtual pinball machine, including not only the Pinscape elements but all of the basics, from sourcing parts to building all of the hardware.

You can also refer to the original Hardware Build Guide (PDF), but that's out of date now, since it refers to the old version 1 software, which was rather different (especially when it comes to configuration).

System Requirements

The new config tool requires a fairly up-to-date Microsoft .NET installation. If you use Windows Update to keep your system current, you should be fine. A modern version of Internet Explorer (IE) is required, even if you don't use it as your main browser, because the config tool uses some system components that Microsoft packages into the IE install set. I test with IE11, so that's known to work. IE8 doesn't work. IE9 and 10 are unknown at this point.

The Windows requirements are only for the config tool. The firmware doesn't care about anything on the Windows side, so if you can make do without the config tool, you can use almost any Windows setup.

Main Features

Plunger: The Pinscape Controller started out as a "mechanical plunger" controller: a device for attaching a real pinball plunger to the video game software so that you could launch the ball the natural way. This is still, of course, a central feature of the project. The software supports several types of sensors: a high-resolution optical sensor (which works by essentially taking pictures of the plunger as it moves); a slide potentionmeter (which determines the position via the changing electrical resistance in the pot); a quadrature sensor (which counts bars printed on a special guide rail that it moves along); and an IR distance sensor (which determines the position by sending pulses of light at the plunger and measuring the round-trip travel time). The Build Guide explains how to set up each type of sensor.

Nudging: The KL25Z (the little microcontroller that the software runs on) has a built-in accelerometer. The Pinscape software uses it to sense when you nudge the cabinet, and feeds the acceleration data to the pinball software on the PC. This turns physical nudges into virtual English on the ball. The accelerometer is quite sensitive and accurate, so we can measure the difference between little bumps and hard shoves, and everything in between. The result is natural and immersive.

Buttons: You can wire real pinball buttons to the KL25Z, and the software will translate the buttons into PC input. You have the option to map each button to a keyboard key or joystick button. You can wire up your flipper buttons, Magna Save buttons, Start button, coin slots, operator buttons, and whatever else you need.

Feedback devices: You can also attach "feedback devices" to the KL25Z. Feedback devices are things that create tactile, sound, and lighting effects in sync with the game action. The most popular PC pinball emulators know how to address a wide variety of these devices, and know how to match them to on-screen action in each virtual table. You just need an I/O controller that translates commands from the PC into electrical signals that turn the devices on and off. The Pinscape Controller can do that for you.

Expansion Boards

There are two main ways to run the Pinscape Controller: standalone, or using the "expansion boards".

In the basic standalone setup, you just need the KL25Z, plus whatever buttons, sensors, and feedback devices you want to attach to it. This mode lets you take advantage of everything the software can do, but for some features, you'll have to build some ad hoc external circuitry to interface external devices with the KL25Z. The Build Guide has detailed plans for exactly what you need to build.

The other option is the Pinscape Expansion Boards. The expansion boards are a companion project, which is also totally free and open-source, that provides Printed Circuit Board (PCB) layouts that are designed specifically to work with the Pinscape software. The PCB designs are in the widely used EAGLE format, which many PCB manufacturers can turn directly into physical boards for you. The expansion boards organize all of the external connections more neatly than on the standalone KL25Z, and they add all of the interface circuitry needed for all of the advanced software functions. The big thing they bring to the table is lots of high-power outputs. The boards provide a modular system that lets you add boards to add more outputs. If you opt for the basic core setup, you'll have enough outputs for all of the toys in a really well-equipped cabinet. If your ambitions go beyond merely well-equipped and run to the ridiculously extravagant, just add an extra board or two. The modular design also means that you can add to the system over time.

Expansion Board project page

Update notes

If you have a Pinscape V1 setup already installed, you should be able to switch to the new version pretty seamlessly. There are just a couple of things to be aware of.

First, the "configuration" procedure is completely different in the new version. Way better and way easier, but it's not what you're used to from V1. In V1, you had to edit the project source code and compile your own custom version of the program. No more! With V2, you simply install the standard, pre-compiled .bin file, and select options using the Pinscape Config Tool on Windows.

Second, if you're using the TSL1410R optical sensor for your plunger, there's a chance you'll need to boost your light source's brightness a little bit. The "shutter speed" is faster in this version, which means that it doesn't spend as much time collecting light per frame as before. The software actually does "auto exposure" adaptation on every frame, so the increased shutter speed really shouldn't bother it, but it does require a certain minimum level of contrast, which requires a certain minimal level of lighting. Check the plunger viewer in the setup tool if you have any problems; if the image looks totally dark, try increasing the light level to see if that helps.

New Features

V2 has numerous new features. Here are some of the highlights...

Dynamic configuration: as explained above, configuration is now handled through the Config Tool on Windows. It's no longer necessary to edit the source code or compile your own modified binary.

Improved plunger sensing: the software now reads the TSL1410R optical sensor about 15x faster than it did before. This allows reading the sensor at full resolution (400dpi), about 400 times per second. The faster frame rate makes a big difference in how accurately we can read the plunger position during the fast motion of a release, which allows for more precise position sensing and faster response. The differences aren't dramatic, since the sensing was already pretty good even with the slower V1 scan rate, but you might notice a little better precision in tricky skill shots.

Keyboard keys: button inputs can now be mapped to keyboard keys. The joystick button option is still available as well, of course. Keyboard keys have the advantage of being closer to universal for PC pinball software: some pinball software can be set up to take joystick input, but nearly all PC pinball emulators can take keyboard input, and nearly all of them use the same key mappings.

Local shift button: one physical button can be designed as the local shift button. This works like a Shift button on a keyboard, but with cabinet buttons. It allows each physical button on the cabinet to have two PC keys assigned, one normal and one shifted. Hold down the local shift button, then press another key, and the other key's shifted key mapping is sent to the PC. The shift button can have a regular key mapping of its own as well, so it can do double duty. The shift feature lets you access more functions without cluttering your cabinet with extra buttons. It's especially nice for less frequently used functions like adjusting the volume or activating night mode.

Night mode: the output controller has a new "night mode" option, which lets you turn off all of your noisy devices with a single button, switch, or PC command. You can designate individual ports as noisy or not. Night mode only disables the noisemakers, so you still get the benefit of your flashers, button lights, and other quiet devices. This lets you play late into the night without disturbing your housemates or neighbors.

Gamma correction: you can designate individual output ports for gamma correction. This adjusts the intensity level of an output to make it match the way the human eye perceives brightness, so that fades and color mixes look more natural in lighting devices. You can apply this to individual ports, so that it only affects ports that actually have lights of some kind attached.

IR Remote Control: the controller software can transmit and/or receive IR remote control commands if you attach appropriate parts (an IR LED to send, an IR sensor chip to receive). This can be used to turn on your TV(s) when the system powers on, if they don't turn on automatically, and for any other functions you can think of requiring IR send/receive capabilities. You can assign IR commands to cabinet buttons, so that pressing a button on your cabinet sends a remote control command from the attached IR LED, and you can have the controller generate virtual key presses on your PC in response to received IR commands. If you have the IR sensor attached, the system can use it to learn commands from your existing remotes.

Yet more USB fixes: I've been gradually finding and fixing USB bugs in the mbed library for months now. This version has all of the fixes of the last couple of releases, of course, plus some new ones. It also has a new "last resort" feature, since there always seems to be "just one more" USB bug. The last resort is that you can tell the device to automatically reboot itself if it loses the USB connection and can't restore it within a given time limit.

More Downloads

  • Custom VP builds: I created modified versions of Visual Pinball 9.9 and Physmod5 that you might want to use in combination with this controller. The modified versions have special handling for plunger calibration specific to the Pinscape Controller, as well as some enhancements to the nudge physics. If you're not using the plunger, you might still want it for the nudge improvements. The modified version also works with any other input controller, so you can get the enhanced nudging effects even if you're using a different plunger/nudge kit. The big change in the modified versions is a "filter" for accelerometer input that's designed to make the response to cabinet nudges more realistic. It also makes the response more subdued than in the standard VP, so it's not to everyone's taste. The downloads include both the updated executables and the source code changes, in case you want to merge the changes into your own custom version(s).

    Note! These features are now standard in the official VP releases, so you don't need my custom builds if you're using 9.9.1 or later and/or VP 10. I don't think there's any reason to use my versions instead of the latest official ones, and in fact I'd encourage you to use the official releases since they're more up to date, but I'm leaving my builds available just in case. In the official versions, look for the checkbox "Enable Nudge Filter" in the Keys preferences dialog. My custom versions don't include that checkbox; they just enable the filter unconditionally.
  • Output circuit shopping list: This is a saved shopping cart at with the parts needed to build one copy of the high-power output circuit for the LedWiz emulator feature, for use with the standalone KL25Z (that is, without the expansion boards). The quantities in the cart are for one output channel, so if you want N outputs, simply multiply the quantities by the N, with one exception: you only need one ULN2803 transistor array chip for each eight output circuits. If you're using the expansion boards, you won't need any of this, since the boards provide their own high-power outputs.
  • Cary Owens' optical sensor housing: A 3D-printable design for a housing/mounting bracket for the optical plunger sensor, designed by Cary Owens. This makes it easy to mount the sensor.
  • Lemming77's potentiometer mounting bracket and shooter rod connecter: Sketchup designs for 3D-printable parts for mounting a slide potentiometer as the plunger sensor. These were designed for a particular slide potentiometer that used to be available from an seller but is no longer listed. You can probably use this design as a starting point for other similar devices; just check the dimensions before committing the design to plastic.

Copyright and License

The Pinscape firmware is copyright 2014, 2021 by Michael J Roberts. It's released under an MIT open-source license. See License.

Warning to VirtuaPin Kit Owners

This software isn't designed as a replacement for the VirtuaPin plunger kit's firmware. If you bought the VirtuaPin kit, I recommend that you don't install this software. The VirtuaPin kit uses the same KL25Z microcontroller that Pinscape uses, but the rest of its hardware is different and incompatible. In particular, the Pinscape firmware doesn't include support for the IR proximity sensor used in the VirtuaPin plunger kit, so you won't be able to use your plunger device with the Pinscape firmware. In addition, the VirtuaPin setup uses a different set of GPIO pins for the button inputs from the Pinscape defaults, so if you do install the Pinscape firmware, you'll have to go into the Config Tool and reassign all of the buttons to match the VirtuaPin wiring.

Sat Apr 18 19:08:55 2020 +0000
TCD1103 DMA setup time padding to fix sporadic missed first pixel in transfer; fix TV ON so that the TV ON IR commands don't have to be grouped in the IR command first slots

Who changed what in which revision?

UserRevisionLine numberNew contents of line
mjr 104:6e06e0f4b476 1 // Toshiba TCD1103 linear CCD image sensor, 1x1500 pixels.
mjr 100:1ff35c07217c 2 //
mjr 100:1ff35c07217c 3 // This sensor is conceptually similar to the TAOS TSL1410R (the original
mjr 100:1ff35c07217c 4 // Pinscape sensor!). Like the TSL1410R, it has a linear array of optical
mjr 100:1ff35c07217c 5 // sensor pixels that convert incident photons into electrical charge, an
mjr 100:1ff35c07217c 6 // internal shift register connected to the pixel file that acts as an
mjr 100:1ff35c07217c 7 // electronic shutter, and a serial interface that clocks the pixels out
mjr 100:1ff35c07217c 8 // to the host in analog voltage level format.
mjr 100:1ff35c07217c 9 //
mjr 104:6e06e0f4b476 10 // The big physical difference between this sensor and the old TAOS sensors
mjr 104:6e06e0f4b476 11 // is the size. The TAOS sensors were (by some miracle) approximately the
mjr 104:6e06e0f4b476 12 // same size as the plunger travel range, so we were able to take "contact"
mjr 104:6e06e0f4b476 13 // images without any optics, by placing the plunger close to the sensor,
mjr 104:6e06e0f4b476 14 // back-lighting it, and essentially taking a picture of its shadow. The
mjr 104:6e06e0f4b476 15 // Toshiba sensor, in contrast, has a pixel window that's only 8mm long, so
mjr 104:6e06e0f4b476 16 // the contact image approach won't work. Instead, we have to use a lens
mjr 104:6e06e0f4b476 17 // to focus a reduced image (about 1:10 scale) on the sensor. That makes
mjr 104:6e06e0f4b476 18 // the physical setup more complex, but it has the great advantage that we
mjr 104:6e06e0f4b476 19 // get a focused image. The shadow was always fuzzy in the old contact
mjr 104:6e06e0f4b476 20 // image approach, which reduced the effective resolution when determining
mjr 104:6e06e0f4b476 21 // the plunger position. With a focused image, we can get single-pixel
mjr 104:6e06e0f4b476 22 // resolution. With this Toshiba sensor's 1500 pixels, that's about 500
mjr 104:6e06e0f4b476 23 // dpi, which beats every other sensor we've come up with.
mjr 100:1ff35c07217c 24 //
mjr 104:6e06e0f4b476 25 // The electronic interface to this sensor is similar to the TAOS, but it
mjr 104:6e06e0f4b476 26 // has enough differences that we can't share the same code base.
mjr 100:1ff35c07217c 27 //
mjr 104:6e06e0f4b476 28 // As with the 1410R, we have to use DMA for the ADC transfers in order
mjr 100:1ff35c07217c 29 // to keep up with the high data rate without overloading the KL25Z CPU.
mjr 100:1ff35c07217c 30 // With the 1410R, we're able to use the ADC itself as the clock source,
mjr 100:1ff35c07217c 31 // by running the ADC in continous mode and using its "sample ready" signal
mjr 100:1ff35c07217c 32 // to trigger the DMA transfer. We used this to generate the external clock
mjr 100:1ff35c07217c 33 // signal for the sensor by "linking" the ADC's DMA channel to another pair
mjr 100:1ff35c07217c 34 // of DMA channels that generated the clock up/down signal each time an ADC
mjr 100:1ff35c07217c 35 // sample completed. This strategy won't work with the Toshiba sensor,
mjr 100:1ff35c07217c 36 // though, because the Toshiba sensor's timing sequence requires *two* clock
mjr 100:1ff35c07217c 37 // pulses per pixel. I can't come up with a way to accomplish that with the
mjr 104:6e06e0f4b476 38 // linked-DMA approach. Instead, we'll have to generate a true clock signal
mjr 104:6e06e0f4b476 39 // for the sensor, and drive the DMA conversions off of that clock.
mjr 100:1ff35c07217c 40 //
mjr 104:6e06e0f4b476 41 // The obvious (and, as far as I can tell, only) way to generate the clock
mjr 104:6e06e0f4b476 42 // signal with the KL25Z at the high frequency required is to use a TPM -
mjr 104:6e06e0f4b476 43 // the KL25Z module that drives PWM outputs. TPM channels are designed
mjr 100:1ff35c07217c 44 // precisely for this kind of work, so this is the right approach in terms of
mjr 100:1ff35c07217c 45 // suitability, but it has the downside that TPM units are an extremely scarce
mjr 104:6e06e0f4b476 46 // resource on the KL25Z. We only have three of them to work with. Luckily,
mjr 100:1ff35c07217c 47 // the rest of the Pinscape software only requires two of them: one for the
mjr 100:1ff35c07217c 48 // IR transmitter (which uses a TPM channel to generate the 41-48 kHz carrier
mjr 100:1ff35c07217c 49 // wave used by nearly all consumer IR remotes), and one for the TLC5940
mjr 100:1ff35c07217c 50 // driver (which uses it to generate the grayscale clock signal). Note that
mjr 100:1ff35c07217c 51 // we also use PWM channels for feedback device output ports, but those don't
mjr 100:1ff35c07217c 52 // have any dependency on the TPM period - they'll work with whatever period
mjr 100:1ff35c07217c 53 // the underlying TPM is set to use. So the feedback output ports can all
mjr 100:1ff35c07217c 54 // happily use free channels on TPM units claimed by any of the dedicated
mjr 100:1ff35c07217c 55 // users (IR, TLC5940, and us).
mjr 100:1ff35c07217c 56 //
mjr 100:1ff35c07217c 57 // But what do we do about the 2:1 ratio between master clock pulses and ADC
mjr 100:1ff35c07217c 58 // samples? The "right" way would be to allocate a second TPM unit to
mjr 100:1ff35c07217c 59 // generate a second clock signal at half the frequency of the master clock,
mjr 100:1ff35c07217c 60 // and use that as the ADC trigger. But as we just said, we only have three
mjr 100:1ff35c07217c 61 // TPM units in the whole system, and two of them are already claimed for
mjr 104:6e06e0f4b476 62 // other uses, so we only have one unit available for our use here.
mjr 100:1ff35c07217c 63 //
mjr 100:1ff35c07217c 64 // Fortunately, we can make do with one TPM unit, by taking advantage of a
mjr 100:1ff35c07217c 65 // feature/quirk of the KL25Z ADC. The quirk lets us take ADC samples at
mjr 100:1ff35c07217c 66 // exactly half of the master clock rate, in perfect sync. The trick is to
mjr 100:1ff35c07217c 67 // pick a combination of master clock rate and ADC sample mode such that the
mjr 100:1ff35c07217c 68 // ADC conversion time is *almost but not quite* twice as long as the master
mjr 100:1ff35c07217c 69 // clock rate. With that combination of timings, we can trigger the ADC
mjr 100:1ff35c07217c 70 // from the TPM, and we'll get an ADC sample on exactly every other tick of
mjr 100:1ff35c07217c 71 // the master clock. The reason this works is that the KL25Z ADC ignores
mjr 100:1ff35c07217c 72 // hardware triggers (the TPM trigger is a hardware trigger) that occur when
mjr 100:1ff35c07217c 73 // a conversion is already in progress. So if the ADC sampling time is more
mjr 100:1ff35c07217c 74 // than one master clock period, the ADC will always be busy one clock tick
mjr 100:1ff35c07217c 75 // after a sample starts, so it'll ignore that first clock tick. But as
mjr 100:1ff35c07217c 76 // long as the sampling time is less than *two* master clock periods, the
mjr 100:1ff35c07217c 77 // ADC will always be ready again on the second tick. So we'll get one ADC
mjr 100:1ff35c07217c 78 // sample for every two master clock ticks, exactly as we need.
mjr 100:1ff35c07217c 79 //
mjr 104:6e06e0f4b476 80 // This is all possible because the ADC timing is deterministic, and runs on
mjr 104:6e06e0f4b476 81 // the same clock as the TPM. The KL25Z Subfamily Reference Manual explains
mjr 104:6e06e0f4b476 82 // how to calculate the ADC conversion time for a given combination of mode
mjr 104:6e06e0f4b476 83 // bits. So we just have to pick an ADC mode, calculate its conversion time,
mjr 104:6e06e0f4b476 84 // and then select a TPM period that's slightly more than 1/2 of the ADC
mjr 104:6e06e0f4b476 85 // conversion time.
mjr 104:6e06e0f4b476 86 //
mjr 104:6e06e0f4b476 87 //
mjr 104:6e06e0f4b476 88 // Pixel output signal
mjr 104:6e06e0f4b476 89 //
mjr 104:6e06e0f4b476 90 // The pixel output signal from this sensor is an analog voltage level. It's
mjr 104:6e06e0f4b476 91 // inverted from the brightness: higher brightness is represented by lower
mjr 104:6e06e0f4b476 92 // voltage. The dynamic range is only about 1V - dark pixels read at about
mjr 104:6e06e0f4b476 93 // 2V, and saturated pixels read at about 1V.
mjr 104:6e06e0f4b476 94 //
mjr 104:6e06e0f4b476 95 //
mjr 104:6e06e0f4b476 96 // Inverted logic signals
mjr 104:6e06e0f4b476 97 //
mjr 104:6e06e0f4b476 98 // The Toshiba data sheet recommends buffering the logic signal inputs from
mjr 104:6e06e0f4b476 99 // an MCU through a 74HC04 inverter, because the sensor's logic gates have
mjr 104:6e06e0f4b476 100 // relatively high input capacitance that an MCU might not be able to drive
mjr 104:6e06e0f4b476 101 // fast enough directly to keep up with the sensor's timing requirements.
mjr 104:6e06e0f4b476 102 // SH in particular might be a problem because of its 150pF capacitance,
mjr 104:6e06e0f4b476 103 // which implies about a 2us rise/fall time if driven directly by KL25Z
mjr 104:6e06e0f4b476 104 // GPIOs, which is too slow.
mjr 104:6e06e0f4b476 105 //
mjr 104:6e06e0f4b476 106 // The software willo work with or without the logic inversion, in case anyone
mjr 104:6e06e0f4b476 107 // wants to try implementing it with direct GPIO drive (not recommended) or
mjr 104:6e06e0f4b476 108 // with a non-inverting buffer in place of the 74HC04. Simply instantiate the
mjr 104:6e06e0f4b476 109 // class with the 'invertedLogicGates' template parameter set to false to use
mjr 104:6e06e0f4b476 110 // non-inverted logic.
mjr 104:6e06e0f4b476 111 //
mjr 104:6e06e0f4b476 112 //
mjr 104:6e06e0f4b476 113 // How to connect to the KL25Z
mjr 104:6e06e0f4b476 114 //
mjr 104:6e06e0f4b476 115 // Follow the "typical drive circuit" presented in the Toshiba data sheet.
mjr 104:6e06e0f4b476 116 // They leave some of the parts unspecified, so here are the specific values
mjr 104:6e06e0f4b476 117 // we used for our reference implementation:
mjr 104:6e06e0f4b476 118 //
mjr 104:6e06e0f4b476 119 // - 3.3V power supply
mjr 104:6e06e0f4b476 120 // - 74HC04N hex inverter for the logic gate inputs (fM, SH, ICG)
mjr 104:6e06e0f4b476 121 // - 0.1uF ceramic + 10uF electrolytic decoupling capacitors (GND to Vcc))
mjr 104:6e06e0f4b476 122 // - BC212A PNP transistor for the output drive (OS), with:
mjr 104:6e06e0f4b476 123 // - 150 ohm resistor on the base
mjr 104:6e06e0f4b476 124 // - 150 ohm resistor between collector and GND
mjr 104:6e06e0f4b476 125 // - 2.2K ohm resistor between emitter and Vcc
mjr 104:6e06e0f4b476 126 //
mjr 100:1ff35c07217c 127
mjr 100:1ff35c07217c 128 #include "config.h"
mjr 100:1ff35c07217c 129 #include "NewPwm.h"
mjr 100:1ff35c07217c 130 #include "AltAnalogIn.h"
mjr 100:1ff35c07217c 131 #include "SimpleDMA.h"
mjr 100:1ff35c07217c 132 #include "DMAChannels.h"
mjr 100:1ff35c07217c 133
mjr 100:1ff35c07217c 134
mjr 100:1ff35c07217c 135 template<bool invertedLogicGates> class TCD1103
mjr 100:1ff35c07217c 136 {
mjr 100:1ff35c07217c 137 public:
mjr 100:1ff35c07217c 138 TCD1103(PinName fmPin, PinName osPin, PinName icgPin, PinName shPin) :
mjr 100:1ff35c07217c 139 fm(fmPin, invertedLogicGates),
mjr 100:1ff35c07217c 140 os(osPin, false, 6, 1), // single sample, 6-cycle long sampling mode, no averaging
mjr 100:1ff35c07217c 141 icg(icgPin),
mjr 100:1ff35c07217c 142 sh(shPin),
mjr 100:1ff35c07217c 143 os_dma(DMAch_TDC_ADC)
mjr 100:1ff35c07217c 144 {
mjr 103:dec22cd65b2a 145 // Idle conditions: SH low, ICG high.
mjr 103:dec22cd65b2a 146 sh = logicLow;
mjr 103:dec22cd65b2a 147 icg = logicHigh;
mjr 103:dec22cd65b2a 148
mjr 103:dec22cd65b2a 149 // Set a zero minimum integration time by default. Note that tIntMin
mjr 103:dec22cd65b2a 150 // has no effect when it's less than the absolute minimum, which is
mjr 103:dec22cd65b2a 151 // the pixel transfer time for one frame (around 3ms). tIntMin only
mjr 103:dec22cd65b2a 152 // kicks in when it goes above that absolute minimum, at which point
mjr 103:dec22cd65b2a 153 // we'll wait for any additional time needed to reach tIntMin before
mjr 103:dec22cd65b2a 154 // starting the next integration cycle.
mjr 103:dec22cd65b2a 155 tIntMin = 0;
mjr 103:dec22cd65b2a 156
mjr 100:1ff35c07217c 157 // Calibrate the ADC for best accuracy
mjr 100:1ff35c07217c 158 os.calibrate();
mjr 104:6e06e0f4b476 159
mjr 100:1ff35c07217c 160 // ADC sample conversion time. This must be calculated based on the
mjr 100:1ff35c07217c 161 // combination of parameters selected for the os() initializer above.
mjr 109:310ac82cbbee 162 // See the KL25 Sub-Family Reference Manual, section, for the
mjr 109:310ac82cbbee 163 // formula. We operate in single-sample mode, so when you read the
mjr 109:310ac82cbbee 164 // Reference Manual tables, the sample time value to use is the
mjr 109:310ac82cbbee 165 // "First or Single" value.
mjr 109:310ac82cbbee 166 const float ADC_TIME = 2.1041667e-6f; // 6-cycle long sampling, no averaging
mjr 100:1ff35c07217c 167
mjr 100:1ff35c07217c 168 // Set the TPM cycle time to satisfy our timing constraints:
mjr 100:1ff35c07217c 169 //
mjr 100:1ff35c07217c 170 // Tm + epsilon1 < A < 2*Tm - epsilon2
mjr 100:1ff35c07217c 171 //
mjr 100:1ff35c07217c 172 // where A is the ADC conversion time and Tm is the master clock
mjr 109:310ac82cbbee 173 // period, and the epsilons provide a margin of safety for any
mjr 100:1ff35c07217c 174 // non-deterministic component to the timing of A and Tm. The
mjr 100:1ff35c07217c 175 // epsilons could be zero if the timing of the ADC is perfectly
mjr 100:1ff35c07217c 176 // deterministic; this must be determined empirically.
mjr 100:1ff35c07217c 177 //
mjr 100:1ff35c07217c 178 // The most conservative solution would be to make epsilon as large
mjr 100:1ff35c07217c 179 // as possible, which means bisecting the time window by making
mjr 100:1ff35c07217c 180 // A = 1.5*T, or, equivalently, T = A/1.5 (the latter form being more
mjr 100:1ff35c07217c 181 // useful because T is the free variable here, as we can only control
mjr 100:1ff35c07217c 182 // A to the extent that we can choose the ADC parameters).
mjr 100:1ff35c07217c 183 //
mjr 100:1ff35c07217c 184 // But we'd also like to make T as short as possible while maintaining
mjr 100:1ff35c07217c 185 // reliable operation. Shorter T yields a higher frame rate, and we
mjr 100:1ff35c07217c 186 // want the frame rate to be as high as possible so that we can track
mjr 100:1ff35c07217c 187 // fast plunger motion accurately. Empirically, we can get reliable
mjr 100:1ff35c07217c 188 // results by using half of the ADC time plus a small buffer time.
mjr 100:1ff35c07217c 189 //
mjr 109:310ac82cbbee 190 fm.getUnit()->period(masterClockPeriod = ADC_TIME/2 + 0.25e-6f);
mjr 100:1ff35c07217c 191
mjr 100:1ff35c07217c 192 // Start the master clock running with a 50% duty cycle
mjr 100:1ff35c07217c 193 fm.write(0.5f);
mjr 100:1ff35c07217c 194
mjr 104:6e06e0f4b476 195 // Allocate our double pixel buffers.
mjr 104:6e06e0f4b476 196 pix1 = new uint8_t[nPixAlo * 2];
mjr 104:6e06e0f4b476 197 pix2 = pix1 + nPixAlo;
mjr 100:1ff35c07217c 198
mjr 100:1ff35c07217c 199 // put the first DMA transfer into the first buffer (pix1)
mjr 100:1ff35c07217c 200 pixDMA = 0;
mjr 101:755f44622abc 201 clientOwnsStablePix = false;
mjr 100:1ff35c07217c 202
mjr 100:1ff35c07217c 203 // start the sample timer with an arbitrary epoch of "now"
mjr 100:1ff35c07217c 204 t.start();
mjr 100:1ff35c07217c 205
mjr 100:1ff35c07217c 206 // Set up the ADC transfer DMA channel. This channel transfers
mjr 100:1ff35c07217c 207 // the current analog sampling result from the ADC output register
mjr 100:1ff35c07217c 208 // to our pixel array.
mjr 100:1ff35c07217c 209 os.initDMA(&os_dma);
mjr 100:1ff35c07217c 210
mjr 100:1ff35c07217c 211 // Register an interrupt callback so that we're notified when
mjr 100:1ff35c07217c 212 // the last ADC transfer completes.
mjr 100:1ff35c07217c 213 os_dma.attach(this, &TCD1103::transferDone);
mjr 100:1ff35c07217c 214
mjr 100:1ff35c07217c 215 // Set up the ADC to trigger on the master clock's TPM channel
mjr 100:1ff35c07217c 216 os.setTriggerTPM(fm.getUnitNum());
mjr 100:1ff35c07217c 217
mjr 100:1ff35c07217c 218 // clear the timing statistics
mjr 100:1ff35c07217c 219 totalXferTime = 0.0;
mjr 100:1ff35c07217c 220 maxXferTime = 0;
mjr 100:1ff35c07217c 221 minXferTime = 0xffffffff;
mjr 100:1ff35c07217c 222 nRuns = 0;
mjr 100:1ff35c07217c 223
mjr 101:755f44622abc 224 // start the first transfer
mjr 101:755f44622abc 225 startTransfer();
mjr 100:1ff35c07217c 226 }
mjr 100:1ff35c07217c 227
mjr 100:1ff35c07217c 228 // logic gate levels, based on whether or not the logic gate connections
mjr 100:1ff35c07217c 229 // in the hardware are buffered through inverters
mjr 100:1ff35c07217c 230 static const int logicLow = invertedLogicGates ? 1 : 0;
mjr 100:1ff35c07217c 231 static const bool logicHigh = invertedLogicGates ? 0 : 1;
mjr 100:1ff35c07217c 232
mjr 100:1ff35c07217c 233 // ready to read
mjr 101:755f44622abc 234 bool ready() { return clientOwnsStablePix; }
mjr 103:dec22cd65b2a 235
mjr 100:1ff35c07217c 236 // Get the stable pixel array. This is the image array from the
mjr 100:1ff35c07217c 237 // previous capture. It remains valid until the next startCapture()
mjr 100:1ff35c07217c 238 // call, at which point this buffer will be reused for the new capture.
mjr 100:1ff35c07217c 239 void getPix(uint8_t * &pix, uint32_t &t)
mjr 100:1ff35c07217c 240 {
mjr 104:6e06e0f4b476 241 // Return the pixel array that ISN'T assigned to the DMA.
mjr 100:1ff35c07217c 242 if (pixDMA)
mjr 100:1ff35c07217c 243 {
mjr 100:1ff35c07217c 244 // DMA owns pix2, so the stable array is pix1
mjr 100:1ff35c07217c 245 pix = pix1;
mjr 100:1ff35c07217c 246 t = t1;
mjr 100:1ff35c07217c 247 }
mjr 100:1ff35c07217c 248 else
mjr 100:1ff35c07217c 249 {
mjr 100:1ff35c07217c 250 // DMA owns pix1, so the stable array is pix2
mjr 100:1ff35c07217c 251 pix = pix2;
mjr 100:1ff35c07217c 252 t = t2;
mjr 100:1ff35c07217c 253 }
mjr 100:1ff35c07217c 254 }
mjr 100:1ff35c07217c 255
mjr 101:755f44622abc 256 // release the client's pixel buffer
mjr 101:755f44622abc 257 void releasePix() { clientOwnsStablePix = false; }
mjr 101:755f44622abc 258
mjr 101:755f44622abc 259 // figure the average scan time from the running totals
mjr 101:755f44622abc 260 uint32_t getAvgScanTime() { return static_cast<uint32_t>(totalXferTime / nRuns);}
mjr 101:755f44622abc 261
mjr 101:755f44622abc 262 // Set the requested minimum integration time. If this is less than the
mjr 101:755f44622abc 263 // sensor's physical minimum time, the physical minimum applies.
mjr 101:755f44622abc 264 virtual void setMinIntTime(uint32_t us)
mjr 100:1ff35c07217c 265 {
mjr 101:755f44622abc 266 tIntMin = us;
mjr 101:755f44622abc 267 }
mjr 101:755f44622abc 268
mjr 101:755f44622abc 269 protected:
mjr 100:1ff35c07217c 270 // Start an image capture from the sensor. Waits the previous
mjr 100:1ff35c07217c 271 // capture to finish if it's still running, then starts a new one
mjr 104:6e06e0f4b476 272 // and returns immediately. The new capture proceeds asynchronously
mjr 104:6e06e0f4b476 273 // via DMA hardware transfer, so the client can continue with other
mjr 100:1ff35c07217c 274 // processing during the capture.
mjr 101:755f44622abc 275 void startTransfer()
mjr 100:1ff35c07217c 276 {
mjr 101:755f44622abc 277 // if we own the stable buffer, swap buffers
mjr 101:755f44622abc 278 if (!clientOwnsStablePix)
mjr 100:1ff35c07217c 279 {
mjr 101:755f44622abc 280 // swap buffers
mjr 101:755f44622abc 281 pixDMA ^= 1;
mjr 101:755f44622abc 282
mjr 101:755f44622abc 283 // release the prior DMA buffer to the client
mjr 101:755f44622abc 284 clientOwnsStablePix = true;
mjr 100:1ff35c07217c 285 }
mjr 100:1ff35c07217c 286
mjr 104:6e06e0f4b476 287 // figure our destination buffer
mjr 104:6e06e0f4b476 288 uint8_t *dst = pixDMA ? pix2 : pix1;
mjr 104:6e06e0f4b476 289
mjr 100:1ff35c07217c 290 // Set up the active pixel array as the destination buffer for
mjr 100:1ff35c07217c 291 // the ADC DMA channel.
mjr 104:6e06e0f4b476 292 os_dma.destination(dst, true);
mjr 100:1ff35c07217c 293
mjr 100:1ff35c07217c 294 // Start the read cycle by sending the ICG/SH pulse sequence
mjr 100:1ff35c07217c 295 uint32_t tNewInt = gen_SH_ICG_pulse(true);
mjr 100:1ff35c07217c 296
mjr 100:1ff35c07217c 297 // Set the timestamp for the current active buffer. The ICG/SH
mjr 100:1ff35c07217c 298 // gymnastics we just did transferred the CCD pixels into the sensor's
mjr 100:1ff35c07217c 299 // internal shift register and reset the pixels, starting a new
mjr 100:1ff35c07217c 300 // integration cycle. So the pixels we just shifted started
mjr 100:1ff35c07217c 301 // integrating the *last* time we did that, which we recorded as
mjr 100:1ff35c07217c 302 // tInt at the time. The image we're about to transfer therefore
mjr 100:1ff35c07217c 303 // represents the light collected between tInt and the SH pulse we
mjr 100:1ff35c07217c 304 // just did. The image covers a time range rather than a single
mjr 100:1ff35c07217c 305 // point in time, but we still have to give it a single timestamp.
mjr 100:1ff35c07217c 306 // Use the midpoint of the integration period.
mjr 100:1ff35c07217c 307 uint32_t tmid = (tNewInt + tInt) >> 1;
mjr 100:1ff35c07217c 308 if (pixDMA)
mjr 100:1ff35c07217c 309 t2 = tmid;
mjr 100:1ff35c07217c 310 else
mjr 100:1ff35c07217c 311 t1 = tmid;
mjr 100:1ff35c07217c 312
mjr 100:1ff35c07217c 313 // Record the start time of the currently active integration period
mjr 100:1ff35c07217c 314 tInt = tNewInt;
mjr 100:1ff35c07217c 315 }
mjr 100:1ff35c07217c 316
mjr 101:755f44622abc 317 // End of transfer notification. This runs as an interrupt handler when
mjr 101:755f44622abc 318 // the DMA transfer completes.
mjr 101:755f44622abc 319 void transferDone()
mjr 100:1ff35c07217c 320 {
mjr 104:6e06e0f4b476 321 // stop the ADC triggering
mjr 104:6e06e0f4b476 322 os.stop();
mjr 104:6e06e0f4b476 323
mjr 101:755f44622abc 324 // add this sample to the timing statistics (for diagnostics and
mjr 101:755f44622abc 325 // performance measurement)
mjr 101:755f44622abc 326 uint32_t now = t.read_us();
mjr 101:755f44622abc 327 uint32_t dt = dtPixXfer = static_cast<uint32_t>(now - tXfer);
mjr 101:755f44622abc 328 totalXferTime += dt;
mjr 101:755f44622abc 329 nRuns += 1;
mjr 101:755f44622abc 330
mjr 101:755f44622abc 331 // collect debug statistics
mjr 101:755f44622abc 332 if (dt < minXferTime) minXferTime = dt;
mjr 101:755f44622abc 333 if (dt > maxXferTime) maxXferTime = dt;
mjr 104:6e06e0f4b476 334
mjr 104:6e06e0f4b476 335 // figure how long we've been integrating so far on this cycle
mjr 101:755f44622abc 336 uint32_t dtInt = now - tInt;
mjr 104:6e06e0f4b476 337
mjr 104:6e06e0f4b476 338 // Figure the time to the start of the next transfer. Wait for the
mjr 104:6e06e0f4b476 339 // remainder of the current integration period if we haven't yet
mjr 104:6e06e0f4b476 340 // reached the requested minimum, otherwise just start almost
mjr 104:6e06e0f4b476 341 // immediately. (Not *actually* immediately: we don't want to start
mjr 104:6e06e0f4b476 342 // the new transfer within this interrupt handler, because the DMA
mjr 104:6e06e0f4b476 343 // IRQ doesn't reliably clear if we start a new transfer immediately.)
mjr 104:6e06e0f4b476 344 uint32_t dtNext = dtInt < tIntMin ? tIntMin - dtInt : 1;
mjr 104:6e06e0f4b476 345
mjr 104:6e06e0f4b476 346 // Schedule the next transfer
mjr 104:6e06e0f4b476 347 integrationTimeout.attach_us(this, &TCD1103::startTransfer, dtNext);
mjr 100:1ff35c07217c 348 }
mjr 100:1ff35c07217c 349
mjr 100:1ff35c07217c 350 // Generate an SH/ICG pulse. This transfers the pixel data from the live
mjr 100:1ff35c07217c 351 // sensor photoreceptors into the sensor's internal shift register, clears
mjr 100:1ff35c07217c 352 // the live pixels, and starts a new integration cycle.
mjr 100:1ff35c07217c 353 //
mjr 100:1ff35c07217c 354 // If start_dma_xfer is true, we'll start the DMA transfer for the ADC
mjr 100:1ff35c07217c 355 // pixel data. We handle this here because the sensor starts clocking
mjr 100:1ff35c07217c 356 // out pixels precisely at the end of the ICG pulse, so we have to be
mjr 100:1ff35c07217c 357 // be very careful about the timing.
mjr 100:1ff35c07217c 358 //
mjr 100:1ff35c07217c 359 // Returns the timestamp (relative to our image timer 't') of the end
mjr 100:1ff35c07217c 360 // of the SH pulse, which is the moment the new integration cycle starts.
mjr 100:1ff35c07217c 361 //
mjr 100:1ff35c07217c 362 // Note that we send these pulses synchronously - that is, this routine
mjr 100:1ff35c07217c 363 // blocks until the pulses have been sent. The overall sequence takes
mjr 100:1ff35c07217c 364 // about 2.5us to 3us, so it's not a significant interruption of the
mjr 100:1ff35c07217c 365 // main loop.
mjr 100:1ff35c07217c 366 //
mjr 100:1ff35c07217c 367 uint32_t gen_SH_ICG_pulse(bool start_dma_xfer)
mjr 100:1ff35c07217c 368 {
mjr 109:310ac82cbbee 369 // Make sure the ADC is stopped
mjr 109:310ac82cbbee 370 os.stop();
mjr 109:310ac82cbbee 371
mjr 100:1ff35c07217c 372 // If desired, prepare to start the DMA transfer for the ADC data.
mjr 100:1ff35c07217c 373 // (Set up a dummy location to write in lieu of the DMA register if
mjr 100:1ff35c07217c 374 // DMA initiation isn't required, so that we don't have to take the
mjr 100:1ff35c07217c 375 // time for a conditional when we're ready to start the DMA transfer.
mjr 100:1ff35c07217c 376 // The timing there will be extremely tight, and we can't afford the
mjr 100:1ff35c07217c 377 // extra instructions to test a condition.)
mjr 100:1ff35c07217c 378 uint8_t dma_chcfg_dummy = 0;
mjr 100:1ff35c07217c 379 volatile uint8_t *dma_chcfg = start_dma_xfer ? os_dma.prepare(nPixSensor, true) : &dma_chcfg_dummy;
mjr 100:1ff35c07217c 380
mjr 100:1ff35c07217c 381 // The basic idea is to take ICG low, and while holding ICG low,
mjr 100:1ff35c07217c 382 // pulse SH. The coincidence of the two pulses transfers the charge
mjr 100:1ff35c07217c 383 // from the live pixels into the shift register, which effectively
mjr 100:1ff35c07217c 384 // discharges the live pixels and thereby starts a new integration
mjr 100:1ff35c07217c 385 // cycle.
mjr 100:1ff35c07217c 386 //
mjr 100:1ff35c07217c 387 // The timing of the pulse sequence is rather tightly constrained
mjr 100:1ff35c07217c 388 // per the data sheet, so we have to take some care in executing it:
mjr 100:1ff35c07217c 389 //
mjr 100:1ff35c07217c 390 // ICG -> LOW
mjr 100:1ff35c07217c 391 // 100-1000 ns delay (*)
mjr 100:1ff35c07217c 392 // SH -> HIGH
mjr 100:1ff35c07217c 393 // >1000ns delay
mjr 100:1ff35c07217c 394 // SH -> LOW
mjr 100:1ff35c07217c 395 // >1000ns delay
mjr 100:1ff35c07217c 396 // ICG -> high (**)
mjr 100:1ff35c07217c 397 //
mjr 100:1ff35c07217c 398 // There are two steps here that are tricky:
mjr 100:1ff35c07217c 399 //
mjr 100:1ff35c07217c 400 // (*) is a narrow window that we can't achieve with an mbed
mjr 100:1ff35c07217c 401 // microsecond timer. Instead, we'll do a couple of extra writes
mjr 100:1ff35c07217c 402 // to the ICG register, which take about 60ns each.
mjr 100:1ff35c07217c 403 //
mjr 100:1ff35c07217c 404 // (**) has the rather severe constraint that the transition must
mjr 100:1ff35c07217c 405 // occur AND complete while the master clock is high. Other people
mjr 100:1ff35c07217c 406 // working with similar Toshiba chips in MCU projects have suggested
mjr 100:1ff35c07217c 407 // that this constraint can safely be ignored, so maybe the data
mjr 100:1ff35c07217c 408 // sheet's insistence about it is obsolete advice from past Toshiba
mjr 100:1ff35c07217c 409 // sensors that the doc writers carried forward by copy-and-paste.
mjr 100:1ff35c07217c 410 // Toshiba has been making these sorts of chips for a very long time,
mjr 100:1ff35c07217c 411 // and the data sheets for many of them are obvious copy-and-paste
mjr 100:1ff35c07217c 412 // jobs. But let's take the data sheet at its word and assume that
mjr 100:1ff35c07217c 413 // this is important for proper operation. Our best hope of
mjr 100:1ff35c07217c 414 // satisfying this constraint is to synchronize the start of the
mjr 100:1ff35c07217c 415 // ICG->high transition with the start of a TPM cycle on the master
mjr 100:1ff35c07217c 416 // clock. That guarantees that the ICG transition starts when the
mjr 100:1ff35c07217c 417 // clock signal is high (as each TPM cycle starts out high), and
mjr 100:1ff35c07217c 418 // gives us the longest possible runway for the transition to
mjr 100:1ff35c07217c 419 // complete while the clock is still high, as we get the full
mjr 100:1ff35c07217c 420 // length of the high part of the cycle to work with. To quantify,
mjr 100:1ff35c07217c 421 // it gives us about 600ns. The register write takes about 60ns,
mjr 100:1ff35c07217c 422 // and waitEndCycle() adds several instructions of overhead, perhaps
mjr 100:1ff35c07217c 423 // 200ns, so we get around 300ns for the transition to finish. That
mjr 100:1ff35c07217c 424 // should be a gracious plenty assuming that the hardware is set up
mjr 100:1ff35c07217c 425 // with an inverter to buffer the clock signals. The inverter should
mjr 100:1ff35c07217c 426 // be able to pull up the 35pF on ICG in a "typical" 30ns (rise time
mjr 100:1ff35c07217c 427 // plus propagation delay, per the 74HC04 data sheet) and max 150ns.
mjr 100:1ff35c07217c 428 // This seems to be one place where the inverter might really be
mjr 100:1ff35c07217c 429 // necessary to meet the timing requirements, as the KL25Z GPIO
mjr 100:1ff35c07217c 430 // might need more like 2us to pull that load up.
mjr 100:1ff35c07217c 431 //
mjr 100:1ff35c07217c 432 // There's an additional constraint on the timing at the end of the
mjr 100:1ff35c07217c 433 // ICG pulse. The sensor starts clocking out pixels on the rising
mjr 100:1ff35c07217c 434 // edge of the ICG pulse. So we need the ICG pulse end to align
mjr 100:1ff35c07217c 435 // with the start of an ADC cycle. If we get that wrong, all of our
mjr 100:1ff35c07217c 436 // ADC samples will be off by half a clock, so every sample will be
mjr 100:1ff35c07217c 437 // the average of two adjacent pixels instead of one pixel. That
mjr 109:310ac82cbbee 438 // would have the effect of shifting the image by half a pixel,
mjr 109:310ac82cbbee 439 // which could make our edge detection jitter by one pixel from one
mjr 109:310ac82cbbee 440 // frame to the next. So we definitely want to avoid this.
mjr 100:1ff35c07217c 441 //
mjr 100:1ff35c07217c 442 // The end of the SH pulse triggers the start of a new integration
mjr 100:1ff35c07217c 443 // cycle, so note the time of that pulse for image timestamping
mjr 100:1ff35c07217c 444 // purposes. That will be the start time of the NEXT image we
mjr 100:1ff35c07217c 445 // transfer after we shift out the current sensor pixels, which
mjr 100:1ff35c07217c 446 // represent the pixels from the last time we pulsed SH.
mjr 100:1ff35c07217c 447 //
mjr 100:1ff35c07217c 448 icg = logicLow;
mjr 109:310ac82cbbee 449 icg = logicLow; // for timing, adds about 150ns > min 100ns
mjr 103:dec22cd65b2a 450
mjr 103:dec22cd65b2a 451 sh = logicHigh; // take SH high
mjr 103:dec22cd65b2a 452
mjr 100:1ff35c07217c 453 wait_us(1); // >1000ns delay
mjr 103:dec22cd65b2a 454 sh = logicHigh; // a little more padding to be sure we're over the minimum
mjr 103:dec22cd65b2a 455
mjr 103:dec22cd65b2a 456 sh = logicLow; // take SH low
mjr 103:dec22cd65b2a 457
mjr 103:dec22cd65b2a 458 uint32_t t_sh = t.read_us(); // this is the start time of the NEXT integration
mjr 103:dec22cd65b2a 459
mjr 103:dec22cd65b2a 460 wait_us(3); // >1000ns delay, 5000ns typical; 3us should get us most
mjr 103:dec22cd65b2a 461 // of the way there, considering that we have some more
mjr 103:dec22cd65b2a 462 // work to do before we end the ICG pulse
mjr 100:1ff35c07217c 463
mjr 100:1ff35c07217c 464 // Now the tricky part! We have to end the ICG pulse (take ICG high)
mjr 100:1ff35c07217c 465 // at the start of a master clock cycle, AND at the start of an ADC
mjr 100:1ff35c07217c 466 // sampling cycle. The sensor will start clocking out pixels the
mjr 100:1ff35c07217c 467 // instance ICG goes high, so we have to align our ADC cycle so that
mjr 100:1ff35c07217c 468 // we start a sample at almost exactly the same time we take ICG
mjr 100:1ff35c07217c 469 // high.
mjr 100:1ff35c07217c 470 //
mjr 100:1ff35c07217c 471 // Now, every ADC sampling cycle always starts at a rising edge of
mjr 100:1ff35c07217c 472 // the master clock, since the master clock is the ADC trigger. BUT,
mjr 100:1ff35c07217c 473 // the converse is NOT true: every rising edge of the master clock
mjr 100:1ff35c07217c 474 // is NOT an ADC sample start. Recall that we've contrived the timing
mjr 100:1ff35c07217c 475 // so that every OTHER master clock rising edge starts an ADC sample.
mjr 100:1ff35c07217c 476 //
mjr 100:1ff35c07217c 477 // So how do we detect which part of the clock cycle we're in? We
mjr 100:1ff35c07217c 478 // could conceivably use the COCO bit in the ADC status register to
mjr 100:1ff35c07217c 479 // detect the little window between the end of one sample and the
mjr 100:1ff35c07217c 480 // start of the next. Unfortunately, this doesn't work: the COCO
mjr 100:1ff35c07217c 481 // bit is never actually set for the duration of even a single CPU
mjr 100:1ff35c07217c 482 // instruction in our setup, no matter how loose we make the timing
mjr 100:1ff35c07217c 483 // between the ADC and the fM cycle. I think the reason is the DMA
mjr 100:1ff35c07217c 484 // setup: the COCO bit triggers the DMA, and the DMA controller
mjr 100:1ff35c07217c 485 // reads the ADC result register (the DMA source in our setup),
mjr 100:1ff35c07217c 486 // which has the side effect of clearing COCO. I've experimented
mjr 100:1ff35c07217c 487 // with this using different timing parameters, and the result is
mjr 100:1ff35c07217c 488 // always the same: the CPU *never* sees the COCO bit set. The DMA
mjr 100:1ff35c07217c 489 // trigger timing is evidently deterministic such that the DMA unit
mjr 100:1ff35c07217c 490 // invariably gets its shot at reading ADC0->R before the CPU does.
mjr 100:1ff35c07217c 491 //
mjr 100:1ff35c07217c 492 // The COCO approach would be a little iffy anyway, since we want the
mjr 100:1ff35c07217c 493 // ADC idle time to be as short as possible, which wouldn't give us
mjr 100:1ff35c07217c 494 // much time to do all we have to do in the COCO period, even if
mjr 100:1ff35c07217c 495 // there were one. What we can do instead is seize control of the
mjr 100:1ff35c07217c 496 // ADC cycle timing: rather than trying to detect when the cycle
mjr 100:1ff35c07217c 497 // ends, we can specify when it begins. We can do this by canceling
mjr 100:1ff35c07217c 498 // the TPM->ADC trigger and aborting any conversion in progress, then
mjr 100:1ff35c07217c 499 // reprogramming the TPM->ADC trigger at our leisure. What we *can*
mjr 100:1ff35c07217c 500 // detect reliably is the start of a TPM cycle. So here's our
mjr 100:1ff35c07217c 501 // strategy:
mjr 100:1ff35c07217c 502 //
mjr 100:1ff35c07217c 503 // - Turn off the TPM->ADC trigger and abort the current conversion
mjr 100:1ff35c07217c 504 // - Wait until a new TPM cycle starts
mjr 100:1ff35c07217c 505 // - Reset the TPM->ADC trigger. The first new conversion will
mjr 100:1ff35c07217c 506 // start on the next TPM cycle, so we have the remainder of
mjr 100:1ff35c07217c 507 // the current TPM cycle to make this happen (about 1us, enough
mjr 100:1ff35c07217c 508 // for 16 CPU instructions - plenty for this step)
mjr 100:1ff35c07217c 509 // - Wait for the new TPM cycle
mjr 100:1ff35c07217c 510 // - End the ICG pulse
mjr 100:1ff35c07217c 511 //
mjr 100:1ff35c07217c 512
mjr 100:1ff35c07217c 513 // Enable the DMA controller for the new transfer from the ADC.
mjr 100:1ff35c07217c 514 // The sensor will start clocking out new samples at the ICG rising
mjr 100:1ff35c07217c 515 // edge, so the next ADC sample to complete will represent the first
mjr 100:1ff35c07217c 516 // pixel in the new frame. So we need the DMA ready to go at the
mjr 100:1ff35c07217c 517 // very next sample. Recall that the DMA is triggered by ADC
mjr 100:1ff35c07217c 518 // completion, and ADC is stopped right now, so enabling the DMA
mjr 100:1ff35c07217c 519 // won't have any immediate effect - it just spools it up so that
mjr 100:1ff35c07217c 520 // it's ready to move samples as soon as we resume the ADC.
mjr 100:1ff35c07217c 521 *dma_chcfg |= DMAMUX_CHCFG_ENBL_MASK;
mjr 100:1ff35c07217c 522
mjr 100:1ff35c07217c 523 // wait for the start of a new master clock cycle
mjr 100:1ff35c07217c 524 fm.waitEndCycle();
mjr 100:1ff35c07217c 525
mjr 109:310ac82cbbee 526 // Wait one more cycle to be sure the DMA is ready. Empirically,
mjr 109:310ac82cbbee 527 // this extra wait is actually required; evidently DMA startup has
mjr 109:310ac82cbbee 528 // some non-deterministic timing element or perhaps an asynchronous
mjr 109:310ac82cbbee 529 // external dependency. In any case, *without* this extra wait,
mjr 109:310ac82cbbee 530 // the DMA transfer sporadically (about 20% probability) misses the
mjr 109:310ac82cbbee 531 // very first pixel that the sensor clocks out, so the entire image
mjr 109:310ac82cbbee 532 // is shifted "left" by one pixel. That makes the position sensing
mjr 109:310ac82cbbee 533 // jitter by a pixel from one frame to the next according to whether
mjr 109:310ac82cbbee 534 // or not we had that one-pixel delay in the DMA startup. Happily,
mjr 109:310ac82cbbee 535 // padding the timing by an fM cycle seems to make the DMA startup
mjr 109:310ac82cbbee 536 // perfectly reliable.
mjr 109:310ac82cbbee 537 fm.waitEndCycle();
mjr 109:310ac82cbbee 538
mjr 100:1ff35c07217c 539 // Okay, a master clock cycle just started, so we have about 1us
mjr 100:1ff35c07217c 540 // (about 16 CPU instructions) before the next one begins. Resume
mjr 100:1ff35c07217c 541 // ADC sampling. The first new sample will start with the next
mjr 109:310ac82cbbee 542 // TPM cycle 1us from now. This step itself takes about 3 machine
mjr 109:310ac82cbbee 543 // instructions for 180ns, so we have about 820ns left to go.
mjr 100:1ff35c07217c 544 os.resume();
mjr 100:1ff35c07217c 545
mjr 104:6e06e0f4b476 546 // Eerything is queued up! We just have to fire the starting gun
mjr 104:6e06e0f4b476 547 // on the sensor at the right moment. And that right moment is the
mjr 104:6e06e0f4b476 548 // start of the next TPM cycle. Wait for it...
mjr 100:1ff35c07217c 549 fm.waitEndCycle();
mjr 100:1ff35c07217c 550
mjr 100:1ff35c07217c 551 // And go!
mjr 100:1ff35c07217c 552 icg = logicHigh;
mjr 100:1ff35c07217c 553
mjr 100:1ff35c07217c 554 // note the start time of the transfer
mjr 100:1ff35c07217c 555 tXfer = t.read_us();
mjr 100:1ff35c07217c 556
mjr 100:1ff35c07217c 557 // return the timestamp of the end of the SH pulse - this is the start
mjr 100:1ff35c07217c 558 // of the new integration period that we just initiated
mjr 100:1ff35c07217c 559 return t_sh;
mjr 100:1ff35c07217c 560 }
mjr 100:1ff35c07217c 561
mjr 100:1ff35c07217c 562 // master clock
mjr 100:1ff35c07217c 563 NewPwmOut fm;
mjr 100:1ff35c07217c 564
mjr 100:1ff35c07217c 565 // analog input for reading the pixel voltage level
mjr 100:1ff35c07217c 566 AltAnalogIn_8bit os;
mjr 100:1ff35c07217c 567
mjr 100:1ff35c07217c 568 // Integration Clear Gate output
mjr 100:1ff35c07217c 569 DigitalOut icg;
mjr 100:1ff35c07217c 570
mjr 100:1ff35c07217c 571 // Shift Gate output
mjr 100:1ff35c07217c 572 DigitalOut sh;
mjr 100:1ff35c07217c 573
mjr 100:1ff35c07217c 574 // DMA channel for the analog input
mjr 100:1ff35c07217c 575 SimpleDMA os_dma;
mjr 100:1ff35c07217c 576
mjr 100:1ff35c07217c 577 // Master clock period, in seconds, calculated based on the ADC timing
mjr 100:1ff35c07217c 578 float masterClockPeriod;
mjr 100:1ff35c07217c 579
mjr 100:1ff35c07217c 580 // Number of pixels. The TCD1103 has 1500 image pixels, plus 32 dummy
mjr 100:1ff35c07217c 581 // pixels at the front end (before the first image pixel) and another 14
mjr 100:1ff35c07217c 582 // dummy pixels at the back end. The sensor always transfers the full
mjr 100:1ff35c07217c 583 // file on each read cycle, including the dummies, so we have to make
mjr 100:1ff35c07217c 584 // room for the dummy pixels during each read.
mjr 100:1ff35c07217c 585 static const int nPixSensor = 1546;
mjr 100:1ff35c07217c 586
mjr 104:6e06e0f4b476 587 // Figure the number of pixels to allocate per pixel buffer. Round
mjr 104:6e06e0f4b476 588 // up to the next 4-byte boundary, so that the buffers are both DWORD-
mjr 104:6e06e0f4b476 589 // aligned. (This allows using DWORD pointers into the buffer to
mjr 104:6e06e0f4b476 590 // operate on buffer pixels four at a time, such as in the negative
mjr 104:6e06e0f4b476 591 // image inversion code in the generic PlungerSensorImage base class.)
mjr 104:6e06e0f4b476 592 static const int nPixAlo = (nPixSensor + 3) & ~3;
mjr 104:6e06e0f4b476 593
mjr 100:1ff35c07217c 594 // pixel buffers - we keep two buffers so that we can transfer the
mjr 100:1ff35c07217c 595 // current sensor data into one buffer via DMA while we concurrently
mjr 100:1ff35c07217c 596 // process the last buffer
mjr 100:1ff35c07217c 597 uint8_t *pix1; // pixel array 1
mjr 100:1ff35c07217c 598 uint8_t *pix2; // pixel array 2
mjr 100:1ff35c07217c 599
mjr 100:1ff35c07217c 600 // Timestamps of pix1 and pix2 arrays, in microseconds, in terms of the
mjr 100:1ff35c07217c 601 // sample timer (this->t).
mjr 100:1ff35c07217c 602 uint32_t t1;
mjr 100:1ff35c07217c 603 uint32_t t2;
mjr 100:1ff35c07217c 604
mjr 100:1ff35c07217c 605 // DMA target buffer. This is the buffer for the next DMA transfer.
mjr 100:1ff35c07217c 606 // 0 means pix1, 1 means pix2. The other buffer contains the stable
mjr 100:1ff35c07217c 607 // data from the last transfer.
mjr 100:1ff35c07217c 608 uint8_t pixDMA;
mjr 100:1ff35c07217c 609
mjr 101:755f44622abc 610 // Stable buffer ownership. At any given time, the DMA subsystem owns
mjr 101:755f44622abc 611 // the buffer specified by pixDMA. The other buffer - the "stable" buffer,
mjr 101:755f44622abc 612 // which contains the most recent completed frame, can be owned by EITHER
mjr 101:755f44622abc 613 // the client or by the DMA subsystem. Each time a DMA transfer completes,
mjr 101:755f44622abc 614 // the DMA subsystem looks at the stable buffer owner flag to determine
mjr 101:755f44622abc 615 // what to do:
mjr 101:755f44622abc 616 //
mjr 101:755f44622abc 617 // - If the DMA subsystem owns the stable buffer, it swaps buffers. This
mjr 101:755f44622abc 618 // makes the newly completed DMA buffer the new stable buffer, and makes
mjr 101:755f44622abc 619 // the old stable buffer the new DMA buffer. At this time, the DMA
mjr 101:755f44622abc 620 // subsystem also changes the stable buffer ownership to CLIENT.
mjr 101:755f44622abc 621 //
mjr 101:755f44622abc 622 // - If the CLIENT owns the stable buffer, the DMA subsystem can't swap
mjr 101:755f44622abc 623 // buffers, because the client is still using the stable buffer. It
mjr 101:755f44622abc 624 // simply leaves things as they are.
mjr 101:755f44622abc 625 //
mjr 101:755f44622abc 626 // In either case, the DMA system starts a new transfer at this point.
mjr 101:755f44622abc 627 //
mjr 101:755f44622abc 628 // The client, meanwhile, is free to access the stable buffer when it has
mjr 101:755f44622abc 629 // ownership. If the client *doesn't* have ownership, it must wait for
mjr 101:755f44622abc 630 // the ownership to be transferred, which can only be done by the DMA
mjr 101:755f44622abc 631 // subsystem on completing a transfer.
mjr 101:755f44622abc 632 //
mjr 101:755f44622abc 633 // When the client is done with the stable buffer, it transfers ownership
mjr 101:755f44622abc 634 // back to the DMA subsystem.
mjr 101:755f44622abc 635 //
mjr 101:755f44622abc 636 // Transfers of ownership from DMA to CLIENT are done only by DMA.
mjr 101:755f44622abc 637 // Transfers from CLIENT to DMA are done only by CLIENT. So whoever has
mjr 101:755f44622abc 638 // ownership now is responsible for transferring ownership.
mjr 101:755f44622abc 639 //
mjr 101:755f44622abc 640 volatile bool clientOwnsStablePix;
mjr 101:755f44622abc 641
mjr 101:755f44622abc 642 // Minimum requested integration time, in microseconds
mjr 101:755f44622abc 643 uint32_t tIntMin;
mjr 101:755f44622abc 644
mjr 101:755f44622abc 645 // Timeout for generating an interrupt at the end of the integration period
mjr 101:755f44622abc 646 Timeout integrationTimeout;
mjr 101:755f44622abc 647
mjr 100:1ff35c07217c 648 // timing statistics
mjr 100:1ff35c07217c 649 Timer t; // sample timer
mjr 100:1ff35c07217c 650 uint32_t tInt; // start time (us) of current integration period
mjr 100:1ff35c07217c 651 uint32_t tXfer; // start time (us) of current pixel transfer
mjr 100:1ff35c07217c 652 uint32_t dtPixXfer; // pixel transfer time of last frame
mjr 100:1ff35c07217c 653 uint64_t totalXferTime; // total time consumed by all reads so far
mjr 100:1ff35c07217c 654 uint32_t nRuns; // number of runs so far
mjr 100:1ff35c07217c 655
mjr 100:1ff35c07217c 656 // debugging - min/max transfer time statistics
mjr 100:1ff35c07217c 657 uint32_t minXferTime;
mjr 100:1ff35c07217c 658 uint32_t maxXferTime;
mjr 100:1ff35c07217c 659 };