CCD Spectrograph Core - Fast 8-bit

New CCD Spectrograph Hardware and Software

This unit captures all of the TCD1304's 3648 pixels using the ADC0820 8-bit half-flash converter. The time to digitize a frame is 32mS. At 115.2kBaud it takes a couple of seconds to download a frame. I've added a Python script to capture and display the scans. There is also a 16-bit version in the works, using the same hardware, except for a 16-bit ADC.

Hardware

An ATmega1284 was chosen as the microcontroller. It has 16kB of RAM, which is double the Arduino Mega2560. The sensor is the Toshiba TCD1304AP, a 3648 pixel linear CCD sensor which operates on a single voltage (3.0V to 5.5V). The sensor is driven directly by the microcontroller, and the analog output is buffered by a transistor and an op-amp. The signal is digitized by an AD0820 analog to digital converter.

The ADC0820 has + and - reference inputs, and this design takes advantage of them. A pot is used to set the maximum range of the ADC, and another pot sets the minimum range. In this way you can tune out the unused portion of the range and get all 256 values from the ADC.

Although this outperforms the original in every respect, it actually cost less to build, about $25 total.

Software

The things that set this spectrograph apart from the last one are its ability to read all 3648 pixels from the CCD, and read them faster than the previous version. Because the AD0820 is so fast, I was able to tighten the code up and get all of the pixels read in 32mS. It reads 3694 pixels, but the first 30 and the last 16 are dark reference or dummy pixels and are discarded.

The Mclk signal has increased from 380kHz to 470kHz on this version.

#include <util/delay_basic.h>

// Debug point pin 16
#define DBG 0x04

// 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 CLOCK PORTD

// ADC data
#define ADATA PINC

uint8_t buffer[3694];
char cmdBuffer[16];
int cmdIndex;
int exposureTime = 10;

void setup()
{
  // Initialize the clocks.
  DDRD |= (WR | SH | ICG | MCLK | RD | DBG);  // Set the clock lines to outputs
  CLOCK |= ICG;              // Set the integration clear gate high.
  CLOCK |= (RD | WR);        // Set the ADC wr line high.

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

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

void readCCD(void)
{
  int x;
  uint8_t dummy;
  
  // Clear the CCD shift register and
  // dump the electrons into it.
  CLOCK |= DBG;
  CLOCK &= ~ICG;
  _delay_loop_1(12);
  CLOCK |= SH;
  delayMicroseconds(5);
  CLOCK &= ~SH;
  delayMicroseconds(10);
  CLOCK |= ICG;
  delayMicroseconds(1);

  for (x = 0; x < 3694; x++)
  {
    // Shift out one pixel and
    // digitize it.
    CLOCK |= SH;

    // ADC write.
    CLOCK &= ~WR;
    delayMicroseconds(1);
    CLOCK |= WR;

    // ADC convert.
    delayMicroseconds(2);

    // ADC read.
    CLOCK &= ~RD;
    delayMicroseconds(1);
    buffer[x] = ADATA;
    CLOCK |= RD;

    // Eat up a few cycles for timing.
    asm("nop");
    asm("nop");
    asm("nop");
    asm("nop");

    CLOCK &= ~SH;
    delayMicroseconds(4);
  }
  CLOCK &= ~DBG;
}

void sendData(void)
{
  int x;

  for (x = 30; x < 3678; ++x)
  {
    Serial.print(x - 30);
    Serial.print(",");
    Serial.print(buffer[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);
  readCCD();
  if (Serial.available())
  {
    ch = Serial.read();
    if (ch == 0x0a) {
      cmdBuffer[cmdIndex++] = '\0';
      cmdRecvd = 1;
    } else {
      cmdBuffer[cmdIndex++] = ch;
      cmdRecvd = 0;
    }
  }
}
                

I wrote a little script in python to read the CCD and output the results. You can set the eposure time, in mS, or take an empty frame to subtract from each frame that gets displayed. It will sample and average up to 4 frames. New frames are overlaid on the previous frames, so there is a clear button to wipe the slate clean. It runs on OS X and Windows, providing you put the correct port in the code. In my setup the OS X port is "/dev/cu.usbserial-A9WFN915" and the Windows port is "COM3:". The baud rate is 115200, from the firmware above.

The graph is 1216 pixels wide, or 1/3 of the width of the CCD. There are 256 pixels vertically so it is one-to-one on the vertical axis. The fake spectra is a piece of electrical tape with three cuts from an X-acto knife across it. An LED dimly lit at the other end of a foot long darkened cardboard tube illuminates it on the CCD.

import serial
import time
from Tkinter import *
from ttk import *


class Application(Frame):

    def __init__(self, master=None, port=None, exposure=50):
        Frame.__init__(self, master)
        self.parent = master
        self.pack()
        self.result = {}
        self.entry_text = ''
        self.dark = [0] * 3648
        self.exposure = StringVar()
        self.exposure.set(str(exposure))
        self.port = port
        self.createWidgets()
        self.ser = serial.Serial(self.port, 115200, timeout=3)

    def createWidgets(self):

        self.TOP = Frame(self)
        self.TOP.pack()

        self.RUN = Button(self.TOP)
        self.RUN["text"] = "Sample"
        self.RUN["command"] = self.run
        self.RUN.pack({"side":"left"})

        self.CLEAR = Button(self.TOP)
        self.CLEAR["text"] = "Clear"
        self.CLEAR["command"] = self.clear
        self.CLEAR.pack({"side":"left"})

        self.QUIT = Button(self.TOP)
        self.QUIT["text"] = "Quit"
        self.QUIT["command"] = self.quit
        self.QUIT.pack({"side":"left"})

        self.LABEL_SAMPLES = Label(self.TOP, text="Samples:")
        self.LABEL_SAMPLES.pack({"side":"left"})
        self.sample_count = IntVar()
        self.SAMPLES_1 = Radiobutton(self.TOP, value=1, variable=self.sample_count, text="1", width=4)
        self.SAMPLES_2 = Radiobutton(self.TOP, value=2, variable=self.sample_count, text="2", width=4)
        self.SAMPLES_3 = Radiobutton(self.TOP, value=3, variable=self.sample_count, text="3", width=4)
        self.SAMPLES_4 = Radiobutton(self.TOP, value=4, variable=self.sample_count, text="4", width=4)
        self.SAMPLES_1.pack({"side":"left"})
        self.SAMPLES_2.pack({"side":"left"})
        self.SAMPLES_3.pack({"side":"left"})
        self.SAMPLES_4.pack({"side":"left"})
        self.SAMPLES_1.invoke()

        self.EXPOSURE = Entry(self.TOP, textvariable=self.exposure)
        self.EXPOSURE.pack({"side":"left"})

        self.SETEXP = Button(self.TOP)
        self.SETEXP["text"] = "Exposure"
        self.SETEXP["command"] = self.send_exposure
        self.SETEXP.pack({"side":"left"})

        self.BASELINE = Button(self.TOP)
        self.BASELINE["text"] = "Baseline"
        self.BASELINE["command"] = self.baseline
        self.BASELINE.pack({"side":"left"})

        self.canvas = Canvas(self, bg="white", height=258, width=1216)
        self.canvas.pack({"side": "right"})

        self.blank_graph()

    def baseline(self):

        self.dark = [0] * 3648

        self.ser.write("e0\n")
        time.sleep(0.1)

        for x in range(0, 1):
            self.ser.write("r\n")

            chars = ''
            while True:
                char = self.ser.read()
                if char == '':
                    break
                chars += char

            data = chars.split("\n")

            for x in data:
                x = x.strip()
                if x == '':
                    continue
                y = x.split(",")

                if len(y) < 2:
                    continue
                if y[0] == '' or y[1] == '':
                    continue
                a = int(y[0])
                b = int(y[1])

                if b > 0:
                    self.dark[a] += b
            for a in range(0, 3648):
                self.dark[a] /= 2

        self.send_exposure()

    def send_exposure(self):

        exp = self.exposure.get()
        try:
            exposure = int(exp)
        except Exception as e:
            print e.message
            exposure = 20

        if exposure > 1000:
            exposure = 1000
            self.exposure.set("1000")

        exp = "e" + str(exposure) + "\n"
        self.ser.write(exp)

    def run(self):

        buckets = [0] * 3649
        counts = [0] * 3649
        self.result = {}

        if self.sample_count.get() == 0:
            self.sample_count.set(1)

        for t in range(0, self.sample_count.get()):
            time.sleep(0.1)
            self.ser.write("r\n")
            chars = ''
            while True:
                char = self.ser.read()
                if char == '':
                    break
                chars += char

            data = chars.split("\n")

            for x in data:
                x = x.strip()
                if x == '':
                    continue
                y = x.split(",")

                if len(y) < 2:
                    continue
                if y[0] == '' or y[1] == '':
                    continue
                a = int(y[0])
                b = int(y[1])

                if b > 0:
                    counts[a] += 1
                    buckets[a] += b

        output = []
        for x in range(0, 3648):
            if counts[x] > 0:
                val = buckets[x] / counts[x]
            else:
                val = buckets[x]
            output.append(val)
        fp = open("ccd.csv", "w")
        for x in range(1, 3648):
            y = int(x/3)
            if y in self.result:
                self.result[y] += (output[x] - self.dark[x])/3
            else:
                self.result[y] = (output[x] - self.dark[x])/3

            fp.write(str(x) + "," + str(output[x]) + "\n")
        fp.close()
        self.draw_graph()

    def clear(self):
        self.canvas.delete("line")
        self.blank_graph()

    def blank_graph(self):

        for x in range(0,1215,25):
            self.canvas.create_line(x, 0, x, 255, fill="#eeeeee", width=1)
        for y in range(0, 255, 25):
            self.canvas.create_line(0, y, 1215, y, fill="#eeeeee", width=1)

        self.canvas.create_line(3, 3, 3, 255, fill="#555555", width=1)
        self.canvas.create_line(3, 255, 1215, 255, fill="#555555", width=1)
        self.canvas.create_line(1215, 255, 1215, 0, fill="#555555", width=1)
        self.canvas.create_line(1215, 3, 3, 3, fill="#555555", width=1)

    def draw_graph(self):

        for x in range(0,1215):
            x1 = x
            y1 = 255 - int(self.result[x1])
            x2 = x+1
            y2 = 255 - int(self.result[x2])
            self.canvas.create_line(x1, y1, x2, y2, fill="#000000", width=1)
        self.canvas.addtag_all("line")


root = Tk()
app = Application(master=root, port="/dev/tty.usbserial-A9WFN915", exposure=50)
app.master.title("TCD1304 Sampler")
app.mainloop()
app.ser.close()
root.destroy()
                

A little cardboard and some electrical tape and you have a dark stage for the prototype. It looks busier than it is. One 40-pin chip, one 22-pin CCD (only 5 actually do anything), one 20-pin ADC and one 14-pin opamp.