Arduino 8-bit Spectrograph project

Project

Arduino linear CCD array for spectrograph.
TCD1304AP Linear Array CCD

The goal of this project is to make the electronics for a spectrograph using an ATmega1284P running Arduino code and a TCD1304AP linear array CCD. The digitization is 8-bit.

An Arduino can't really keep up with a CCD using the internal analog to digital converter, but with an external 8-bit half-flash ADC it is no problem at all. The ADC is the TI ADC0820, which can digitize up to 666k samples per second. The working sample rate works out to be a hair over 111kSPS using the code below. The 3694 pixel frame takes around 32mS to read and digitize, and around 4 seconds to download.

Hardware

Microcontroller

The Arduino Uno can't pull off a 3694 pixel buffer. It just doesn't have enough RAM. That leaves either the Arduino Mega2560 with just 4kB or a standalone ATmega1284 with 16kB RAM. It is a whole lot easier, and cheaper, to embed a 40-pin IC into a project than it is an Arduino Mega2560, so I went with the ATmega1284. When using the Arduino Mega2560 the compiler will complain about not having enough RAM, but it will still run. The same goes for the ATmega644.

Analog to Digital Converter

The ADC0820 ADC has + and - reference inputs designed to set the range of the ADC. In this way you can zone in on the used portion of the input range, keeping full resolution. The apparent noise increases as you trim the range down, though, since it becomes a larger part of the digital range.

Linear Array CCD

The CCD is a Toshiba TCD1304AP 3648 pixel linear CCD sensor which requires only a single supply voltage. The sensor is driven by the microcontroller, and the analog output is buffered by a transistor and inverted and amplified by an op-amp before being digitized by the ADC0820.

The TCD1304AP CCD is available on ebay or aliexpress for anywhere from $4 to $40. All of the other parts are available from Mouser or ebay or other outlets. An 80mm x 120mm prototype board from ebay holds it all, or a PCB can be used. The Gerber files can be used at about any PCB shop, or I do have a few spares. Email me if you are interested. When they're gone they're gone.

Parts List

  • 1 ea. ATmega1284-PU or ATmega1284P-PU ($5.15 or $5.52)
  • 1 ea. AD0820 Analog to Digital Converter ($4.07)
  • 1 ea. LM324 ($0.58)
  • 1 ea. LM7805 ($0.83)
  • 1 ea. 40-pin IC socket($0.57)
  • 1 ea. 22-pin x 10.16mm IC socket ($1.97)
  • 1 ea. 20-pin IC socket($0.61)
  • 1 ea. 14-pin IC socket($0.48)
  • 1 ea. 50-pin SIP socket machine pin ($4.66)
  • 1 ea. 2N3906 or similar PNP transistor($0.46)
  • 2 ea. 240Ω 1/4W resistor($0.20)
  • 3 ea. 2.2kΩ 1/4W resistor($0.30)
  • 1 ea. 10kΩ 1/4W resistor($0.10)
  • 1 ea. 16MHz crystal($0.69)
  • 2 ea. 22pF capacitor($0.44)
  • 12 ea. 0.1µF capacitor($0.96)
  • 1 ea. 100µF/16V electrolytic capacitor($0.10)
  • 1 ea. 220uF/25V electrolytic capacitor($0.10)
  • 2 ea. 10kΩ 10-turn potentiometer($2.34)
  • 1 ea. 2kΩ 10-turn potentiometer($1.26)
  • 1 ea. 6-pin male header($0.26)
  • 1 ea. 5.5 x 2.1 mm power jack($1.00)
  • 1 ea. TCD1304 Linear array CCD ($11.23 [in USA ebay])
  • 1 ea. 9VDC "Wall Wart"($6.00 ebay)
  • 1 ea. 80mm x 120mm perfboard ($4.99 for 3 on ebay)
  • Total (more or less) $49.83

That cost assumes your parts box is completely empty and you want to buy everything from places in the USA.

Arduino assembled 8-bit spectrograph.
Assembled spectrograph PCB

Notes:

Assembly is typical, but be aware that the CCD requires 22 pins with a width of 400 mils (10.16mm). A socket is listed in the parts list. Mouser carries the Mill-Max 110-43-422-41-001000 socket.

Be careful to keep the digital and analog wiring away from each other where possible. Use 0.1µF bypass capacitors at every Vcc connection. Keep all of the digital wires as short as practical to keep them from radiating more than necessary. Be especially careful when routing the Mclk and SH signals near analog areas.

Software

Use the MightyCore boards manager plugin by MCUdude. The board will be ATmega1284 and the variant will be the 1284 or the 1284P, depending on which one you have. Pinout is "standard" and the frequency is 16MHz. You can add the MightyCore boards to the boards manager by adding the following url to the "Additional Boards Manager URLs" textbox in Arduino preferences:

https://mcudude.github.io/MightyCore/package_MCUdude_MightyCore_index.json

Then just open the Boards Manager under Tools->Board->Boards Manager and go to the bottom to install the MightyCore boards. The selection of boards will increase considerably to include all of the ATmegaxx4 chips, ATmega16/32, and ATmega8535. Additional programmers will appear, too, and you should use one of them with the MightyCore.

You will need an AVR programmer to burn the Arduino bootloader into the ATmega1284.

The source code for the linear CCD application is very carefully crafted in places to maintain the phase relationship between all of the CCD clocks and stay true to the specification. It takes a logic analyzer to measure the results of any code changes in the "readLine()" function.

#include <util/delay_basic.h>

// ADC RD signal pin 17
#define RD 0x08

// ADC write signal pin 18
#define WR 0x10

// CCD Shift Gate pin 19
#define SH 0x20

// CCD Integration Clear Gate pin 20
#define ICG 0x40

// CCD Master clock pin 21
#define MCLK 0x80

// CCD and ADC clocks
#define CLOCKS PORTD
#define CLOCKP PIND

// ADC data
#define ADATA PINC

#define PIXEL_COUNT 3694

uint8_t pixBuf[PIXEL_COUNT];
char cmdBuffer[16];
int cmdIndex;
int exposureTime = 10;

void setup()
{
  // Initialize the clocks.
  DDRD |= (WR | SH | ICG | MCLK | RD);  // Set the clock lines to outputs
  CLOCKS = (ICG + RD + WR);       // Set the ICG, RD, and WR high.

  // Setup the ADC data port.
  DDRC = 0;
  // Enable the serial port.
  Serial.begin(115200);

  // Setup timer2 to generate an 888.8kHz frequency on pin 21
  TCCR2A =  + (0 << COM2A1) | (1 << COM2A0) | (1 << WGM21) | (0 << WGM20);
  TCCR2B = (0 << WGM22) | (1 << CS20);
  OCR2A = 8;
  TCNT2 = 1;
}

void readLine() {
  // Get a pointer to the buffer.
  uint8_t *buf = pixBuf;
  int x = 0;
  uint8_t scratch = 0;
  
  // Disable interrupts or the timer will get us.
  cli();
  
  // Synchronize with MCLK and
  // set ICG low and SH high.
  scratch = CLOCKS;
  scratch &= ~ICG;
  scratch |= SH;
  while(!(CLOCKP & MCLK));
  while((CLOCKP & MCLK));
  TCNT2 = 0;
  _delay_loop_1(1);
  __asm__("nop");
  __asm__("nop");
  __asm__("nop");
  __asm__("nop");
  __asm__("nop");
  CLOCKS = scratch;
  
  // Wait the remainder of 4.5uS @ 16MHz.
  _delay_loop_1(22);
  __asm__("nop");
  __asm__("nop");

  // Set SH low.
  CLOCKS ^= SH;

  // Wait the remainder of 4.5uS.
  _delay_loop_1(23);

  // Start the readout loop at the first pixel.
  CLOCKS |= (RD + WR + ICG + SH);
  
  do {
    // Wait a minimum of 250nS for acquisition.
    _delay_loop_1(2);

    // Start the conversion.
    CLOCKS &= ~WR;
    CLOCKS |= WR;

    // Wait a minimum of 2uS for conversion.
    _delay_loop_1(12);

    // Read the low byte of the result.
    CLOCKS &= ~RD;
    _delay_loop_1(3);
    __asm__("nop");
    __asm__("nop");
    *buf++ = ADATA;

    // Set the clocks back to idle state
    CLOCKS |= RD;

    // Toggle SH for the next pixel.
    CLOCKS ^= SH;

  } while (++x < PIXEL_COUNT);
  CLOCKS = (ICG + RD + WR);
  sei();
}

void sendData(void)
{
  int x;

  for (x = 30; x < 3678; ++x)
  {
    Serial.print(x - 30);
    Serial.print(",");
    Serial.print(pixBuf[x]);
    Serial.print("\n");
  }
}

int cmdRecvd = 0;

void loop()
{
  int x;
  char ch;
  
  if (cmdRecvd) {
    if (cmdBuffer[0] == 'r')
    {
      sendData();
    }
    else if (cmdBuffer[0] == 'e')
    {
      sscanf(cmdBuffer+1,"%d", &exposureTime);
      if (exposureTime > 1000) exposureTime = 1000;
    }
    memset(cmdBuffer, 0, sizeof(cmdBuffer));
    cmdIndex = 0;
    cmdRecvd = 0;
  }
  delay(exposureTime);
  readLine();
  if (Serial.available())
  {
    ch = Serial.read();
    if (ch == 0x0a) {
      cmdBuffer[cmdIndex++] = '\0';
      cmdRecvd = 1;
    } else {
      cmdBuffer[cmdIndex++] = ch;
      cmdRecvd = 0;
    }
  }
}                

Timing

Arduino linear CCD array output.

At time 0s:0ms:0µs you can see the MCLK is stretched a little. That is to get the ICG, SH, WR and RD lines in sync with the timer, which generates the MCLK signal. The code has to maintain this timing relationship throughout the entire read process. The readLine() function is riddled with assembly language "NOP" code to keep the timing precise. Keep that in mind if you feel the need to change that particular function.

Calibration

To calibrate, start by setting the VR2 pot so that the voltage on pin 12 of the ADC0820 is close to 5.0V. Set VR3 such that the voltage on pin 11 of the ADC0820 is 0V. Then cover the CCD with electrical tape in a room with dimmed lighting. The tape will come off without damaging the CCD. Adjust pot VR1 to get the lowest voltage you can get on pin 1 of the ADC0820. Adjust it back up so that the voltage just begins to rise. It may be anywhere from 0.65V to 0.75V. That adjusts the lowest signal to be just measurable by the ADC. Remove the electrical tape, but keep it handy. We'll use it again. Cover the CCD with a dark cloth under dimmed room lighting so the CCD just barely saturates (a scope helps) and adjust the 2kΩ VR2 Vref(+) pot and readout a frame. Adjust until the digital value is 255, and then back off a little to 253 - 255. Then cover the CCD using the electrical tape, and adjust the 10k VR3 Vref(-) pot so the lowest digital reading just hits 1 or 2 ADU. You must adjust the pots in that order because they interact with each other. That is by design. The maximum voltage you can put on Vref(-) is the voltage on Vref(+), and the minimum is ground, so changing Vref(+) changes the range of Vref(-).

Output

Arduino linear CCD array output.
Click to download python code

A few lines up from the end of the python code is a line:

			
    app = Application(master=root, port="/dev/tty.usbserial-00000000", exposure=50)
			

It should match your serial port. If you are using Windows, change that to:

			
    app = Application(master=root, port="COM3", exposure=50)
			

Make it match the serial port you are using. When you run the program it draws a graph of the CCD output, but also makes a file in the program directory named 'ccd.csv'. That file can be imported into a spreadsheet application for further processing.

The commands are:

  • Sample - Read the CCD and put the trace on the screen.
  • Clear - Clear all traces from the screen.
  • Quit - End the program (you need to do this to keep from generating an error on exit).
  • Samples - How many scans to average together (removes noise).
  • Exposure - An integer value from 1 to 1000. The milliseconds to expose.
  • Baseline - Makes a zero second exposure to subtract from other exposures.

Mechanics

Here is where we all go our separate ways. If you want a Raman spectrometer, you will be adding a laser, a grating, and a few mirrors. If you want a barcode reader, you will add a lens. My particular use for this is to characterize dichroic color separation filters used in CCD astrophotography. To do that, I needed a collimated light source, a stage for the filter, a slit, a transmission grating, and the board.

Arduino Board Logo

 

Arduino-Board is the go-to source for information on many available Arduino and Arduino-like boards, tutorials and projects.

Help and Support

Arduino-Board

Stay updated

Sign up if you would like to receive our once monthly newsletter.