CCD Transmission Spectrograph


This unit captures 800 or so pixels from the TCD1304's 3648 pixels. Peer pressure being what it is, I've updated the project to use a fast 8-bit external ADC. Now it captures all 3648 pixels in 8-bit resolution. There is a new fast 8-bit spectrograph project.


A transmission spectrograph is a device which records the spectra of light passing through a subject. In this case, the subject is a photographic or color separation filter, specifically a dichroic filter - red, green, blue, cyan, magenta, yellow, Ha, O-III, and so on. This one uses a linear image sensor and an Arduino Uno to capture the image of the spectra and pass it to a python app on the host computer which graphs the output.

I learned on the ADC0820 project that the OpAmp is not required. You can hook the emitter of the transistor to the ADC, skipping the OpAmp, and linearity is improved. The code needs to be changed to subtract the ADC value from 1023, and I include that code below. As a bonus, it reads 10 bits from the ADC. And it requires no adjustments - if the circuit is wired correctly, and the parts all work, you should get a trace on the first run, assuming you don't over-expose the sensor.

The R4, C1 pair are for noise suppression. They may not be needed in your application. Try it without, then add them if required. Adjust the value of C1 until the noise is mostly gone. Don't mistake the quantization error for noise.

Microcontroller Software

The older Arduino spectrograph software is written in C in the Arduino IDE. Line readout consists of driving the clocks and reading the output with the ADC. The data is read in as fast as the Arduino can go, with the ADC clock sped up a little bit. The Mclk on the CCD is adjusted such that the 3648 pixels are output during the time it takes to digitize 800 readings with the ADC. It doesn't matter that the two don't match up - the CCD output is continuous, actually an analog signal, so there are no aliasing problems.

A new piece of code for the Arduino Spectrograph without OpAmp has the code to subtract the ADC value from 1023, inverting it. The full range of values in this mode is just under 0 to 420 ADU with my Uno and my CCD. The max range would be 0 to 1023. The data is referenced to zero, and increasing light amplitude raises the output value. You can also download the zip file.

When you program the Arduino Uno you will get a warning that you're about out of RAM. That's Ok. There is more than enough for the code to run reliably.

I used the serial plotter on the Arduino tools menu, and it worked great except that the maximum number of displayed points is 500, so it shows samples 300 to 800, cutting off everything below that.

Arduino serial plotter output


  • r - Readout
  • e123 - Exposure time (mS)
Spectrograph layout (prism)


So far we've only done the electronics. There are some mechanical/optical tasks to do as well. A spectrograph has to break the light into it's constituent colors in order to measure them. There are three ways to do that, and I tried two of them. A prism and a broken DVD. Both worked. I did not try a transmission diffraction grating. I don't have one to try, and they cost too much for this project. No matter which type you use, you must use a slit. I used two utility knife replacement blades edge to edge, with a piece of stiff paper between to space them. Screwing them down over a drilled hole, then removing the paper, leaves a tiny little slit that is just what is needed.

To use a prism, the angles of the light entering, the prism angle, and the distance to the CCD must be figured out. It is possible, but insane, to calculate these angles. It is best to set it all up on the kitchen table with the lights out and just measure the result. Make your setup adjustable, so you can calibrate on a known wavelength.

To use a DVD or CD, you should do the same thing, but since they "reflect" the spectrum, you will position the CCD in a different place than when using a prism. Again, darkness is your friend.

Total investment in this project was less than $20, and it appears to work great. I will refine the mechanics a little to toughen it up, and provide the collimated light source. Those two things should make it perfect for measuring overlap in dichroic filters.

Some Angles and the Host Software

I did manage to get some film holographic transmission gratings to play around with. I designed, but have not yet built, a bottom end for a spectrograph using these 1000 lines per millimeter gratings. The key takeaways from the drawing are the angles required and the way to achieve them.

One of the most important dimensions is the 2.82 inches between the grating and the CCD. That is 71628 microns, and goes with the 36.48 microns per pixel (3648 phys. pixels x 8 microns / 800 logical pixels). The math depends on those two numbers being proportional. The 800 pixels are 29184 microns long, non-adjustable, so it becomes important that the 2.82 inch (71.628mm) distance is accurate as well. I have an idea how I'm going to do it using a cardboard triangle to set the closest end of the sensor at 400nm and the spacing at 71.6mm.

Built to the drawing, the sensor should have 400nm lined up with the first pixel, and 718nm should fall in place on the last pixel. Within reason. In between, the pixel can be translated into a wavelength by some trigonometry:

wavelength in nm = math.degrees(math.atan(pixel / 1963.4868)) / 0.0695 + 400

That's python if it isn't obvious, and it yields:
pixel 0 = 400.00nm
pixel 399 = 565.28nm
pixel 799 = 718.60nm

That gets you the wavelength at any given pixel. But we also want the pixel of any given wavelength, so we can put up a grid. That is:

pixel = math.tan(math.radians((wavelength - 400) * 0.0695)) * 1963.4868

Which gives us:
400.00nm = pixel 0.00
565.28nm = pixel 399.01
718.60nm = pixel 798.99

Armed with these two lines of code, one could write a python app to display the output with a grid labeled with wavelength, and be pretty confident in the numbers.

A Little Python App

I put together a little app to read the Arduino (the no-op-amp code above) and display the result. It beats copying and pasting all the values into a spreadsheet, but you can still use the spreadsheet if you want. It outputs a csv file in the directory it is run from. It has a cursor and displays the wavelength and amplitude values of the point at the cursor.

Before you run it, you need to put the com port in near the bottom of the file, where it says:

app = Application(master=root, port="/dev/ttyACM0", exposure=30)

Replace /dev/ttyACM0 with COM4 or /dev/cu.ttyUSB-12345 or whatever your port is.

It isn't obvious that to get the blue cursor you have to hover over the number area at the bottom of the window. The numbers on the right end of the header are the wavelength and value at the blue cursor. A left-click will freeze the cursor and a right-click on either white area will remove the cursor.

To end the app, click the "Quit" button. Otherwise TK doesn't clean up and you get an error. It doesn't cause any problems, but it complains.

"Clear" clears the display. Multiple scans just stack up on the screen until you hit clear.

If you need to change the exposure time, you change it in the input field, then click "Exposure". Set the exposure time to a value that keeps the output linear. 2x the exposure should give 2x the output. Exactly. If it doesn't, it is non-linear and you need to back it off.

Download the python app. You will need PySerial:
pip install pyserial
And tkinter. You usually install tkinter with the OS supplied installer, for instance apt with Debian/Ubuntu:

sudo apt-get install python-tk