|
| 1 | +"""Proof of concept TCD1304 driver. |
| 2 | +
|
| 3 | +This driver can only drive the TCD1304 in normal mode, not electronic shutter |
| 4 | +mode. This is because the PSLab can (currently) only output two different PWM |
| 5 | +frequencies simultaneously, and electronic shutter mode requires three. |
| 6 | +
|
| 7 | +Furthermode, the clock frequencies are locked to 2 MHz (master) and 125 Hz (SH |
| 8 | +and ICG). The reason is the following: |
| 9 | +
|
| 10 | +The ICG period must be greater than the readout period, which is: |
| 11 | + master clock period * 4 * number of pixels = 7.4 ms |
| 12 | +7.4 ms -> 135 Hz, which is therefore the fastest the ICG clock can run. |
| 13 | +
|
| 14 | +The lowest possible frequency the PSLab can generate with sufficient |
| 15 | +clock precision is 123 Hz. Below that the 16-bit timer that drives the |
| 16 | +PWM must be prescaled so much that we can no longer satisfy the TCD1304's |
| 17 | +timing requirements. |
| 18 | +
|
| 19 | +Thus, the range of possible ICG frequencies is [123, 135], which is so small |
| 20 | +that it makes more sense to just lock it to 125 Hz, which has the added |
| 21 | +advantage of being an even divisor of the PSLab's MCU frequency (64 MHz). |
| 22 | +
|
| 23 | +It should be possible to increase the master clock to 4 MHz, which would also |
| 24 | +make ICG frequencies up to 250 Hz possible. However, the readout period would |
| 25 | +be 3.7 ms, which the PSLab's oscilloscope might struggle to capture with good |
| 26 | +quality. |
| 27 | +""" |
| 28 | + |
| 29 | +from typing import List |
| 30 | + |
| 31 | +from numpy import ndarray |
| 32 | + |
| 33 | +from pslab import Oscilloscope |
| 34 | +from pslab import PowerSupply |
| 35 | +from pslab import PWMGenerator |
| 36 | +from pslab.instrument.waveform_generator import _get_wavelength |
| 37 | +from pslab.protocol import MAX_SAMPLES |
| 38 | +from pslab.serial_handler import SerialHandler |
| 39 | + |
| 40 | + |
| 41 | +class TCD1304: |
| 42 | + def __init__(self, device: SerialHandler): |
| 43 | + self._pwm = PWMGenerator(device) |
| 44 | + self._oscilloscope = Oscilloscope(device) |
| 45 | + self._sh_frequency = 125 |
| 46 | + |
| 47 | + def start_clocks(self, inverted: bool = True): |
| 48 | + """Start the Master, SH, and ICG clocks. |
| 49 | +
|
| 50 | + Parameters |
| 51 | + ---------- |
| 52 | + inverted : bool, optional |
| 53 | + The TCD1304 datasheet recommends placing a hex inverter between the |
| 54 | + sensor and the MCU. By default, the clocks are therefore inverted |
| 55 | + relative to what they should be to drive the sensor. If you do not |
| 56 | + use a hex inverter, set this to False. |
| 57 | +
|
| 58 | + Returns |
| 59 | + ------- |
| 60 | + None. |
| 61 | +
|
| 62 | + """ |
| 63 | + self._pwm.map_reference_clock("SQ1", 6) # 2 MHz |
| 64 | + |
| 65 | + resolution = _get_wavelength(self._sh_frequency)[0] ** -1 |
| 66 | + # Timing requirements: |
| 67 | + # (1) The SH clock must go high between 100 ns to 1000 ns after the ICG |
| 68 | + # clock goes low. |
| 69 | + # (2) The SH clock must stay high for at least 1 µs. |
| 70 | + # (3) The ICG clock must stay low at least 1 µs after the SH clock goes |
| 71 | + # low. |
| 72 | + # I got the following numbers through trial and error. They meet the |
| 73 | + # above requirements. |
| 74 | + # TODO: Calculate appropriate duty cycles and phases. |
| 75 | + magic_numbers = [ |
| 76 | + 12 * resolution, |
| 77 | + 48 * resolution, |
| 78 | + 16 * resolution, |
| 79 | + 1 - 42 * resolution, |
| 80 | + ] |
| 81 | + |
| 82 | + if inverted: |
| 83 | + self._pwm.generate( |
| 84 | + ["SQ2", "SQ3"], |
| 85 | + frequency=self._sh_frequency, |
| 86 | + duty_cycles=[1 - magic_numbers[0], magic_numbers[1]], |
| 87 | + phases=[magic_numbers[2], 0], |
| 88 | + ) |
| 89 | + else: |
| 90 | + self._pwm.generate( |
| 91 | + ["SQ2", "SQ3"], |
| 92 | + frequency=self._sh_frequency, |
| 93 | + duty_cycles=[magic_numbers[0], 1 - magic_numbers[1]], |
| 94 | + phases=[magic_numbers[3], 0], |
| 95 | + ) |
| 96 | + |
| 97 | + def stop_clocks(self): |
| 98 | + """Stop the Master, SH, and ICG clocks. |
| 99 | +
|
| 100 | + Returns |
| 101 | + ------- |
| 102 | + None. |
| 103 | +
|
| 104 | + """ |
| 105 | + self._pwm.set_state(sq1=0, sq2=0, sq3=0) |
| 106 | + |
| 107 | + def read(self, analogin: str = "CH1", trigger: str = "CH2") -> List[ndarray]: |
| 108 | + """Read the sensor's analog output. |
| 109 | +
|
| 110 | + Connect one of the PSLab's analog inputs to the sensor's analog output. |
| 111 | +
|
| 112 | + Parameters |
| 113 | + ---------- |
| 114 | + channel : str, optional |
| 115 | + The analog input connected to the sensor's OS pin. |
| 116 | + Defaults to "CH1". |
| 117 | + trigger : str, optional |
| 118 | + The analog input connected to the sensor's ICG pin. |
| 119 | + Defaults to "CH2". |
| 120 | +
|
| 121 | + Returns |
| 122 | + ------- |
| 123 | + List[ndarray] |
| 124 | + Timestamps and corresponding voltages. |
| 125 | +
|
| 126 | + """ |
| 127 | + return self._oscilloscope.capture( |
| 128 | + analogin, 8000, 1, trigger=3, trigger_channel=trigger |
| 129 | + ) |
0 commit comments