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

/media/uploads/mjr/pinscape_no_background_small_L7Miwr6.jpg

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 mechanical plunger, button inputs, and feedback device control.

In case you haven't heard of the idea 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 show the backglass artwork. Some cabs also include a third monitor to simulate the DMD (Dot Matrix Display) used for scoring on 1990s machines, or even an original plasma DMD. A computer (usually a Windows PC) 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 trim hardware.

It's possible to buy a pre-built virtual pinball machine, but it also makes a great 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 vpforums.org. Also visit my Pinscape Resources page for more about this project and other virtual pinball projects I'm working on.

Downloads

  • 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.

Documentation

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 potentiometer (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 mouser.com 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 Aliexpress.com 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 KL25Z can only run one firmware program at a time, so if you install the Pinscape firmware on your KL25Z, it will replace and erase your existing VirtuaPin proprietary firmware. If you do this, the only way to restore your VirtuaPin firmware is to physically ship the KL25Z back to VirtuaPin and ask them to re-flash it. They don't allow you to do this at home, and they don't even allow you to back up your firmware, since they want to protect their proprietary software from copying. For all of these reasons, if you want to run the Pinscape software, I strongly recommend that you buy a "blank" retail KL25Z to use with Pinscape. They only cost about $15 and are available at several online retailers, including Amazon, Mouser, and eBay. The blank retail boards don't come with any proprietary firmware pre-installed, so installing Pinscape won't delete anything that you paid extra for.

With those warnings in mind, if you're absolutely sure that you don't mind permanently erasing your VirtuaPin firmware, it is at least possible to use Pinscape as a replacement for the VirtuaPin firmware. Pinscape uses the same button wiring conventions as the VirtuaPin setup, so you can keep your buttons (although you'll have to update the GPIO pin mappings in the Config Tool to match your physical wiring). As of the June, 2021 firmware, the Vishay VCNL4010 plunger sensor that comes with the VirtuaPin v3 plunger kit is supported, so you can also keep your plunger, if you have that chip. (You should check to be sure that's the sensor chip you have before committing to this route, if keeping the plunger sensor is important to you. The older VirtuaPin plunger kits came with different IR sensors that the Pinscape software doesn't handle.)

Committer:
mjr
Date:
Mon May 04 21:05:35 2020 +0000
Revision:
110:bf332f824585
Parent:
109:310ac82cbbee
Child:
111:42dc75fbe623
TCD1103 comments

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