A simple, low-cost, modular, multi-channel temperature controller for holding a lab laser steady enough to stay injection-locked. The figure of merit is relative stability — keeping the temperature put — not absolute accuracy. A vertical slice around an AD7124-8 24-bit ADC and a Teensy 4.1 (C++), borrowing only the proven MAX1968 TEC power stage from the open-source Sinara Thermostat.
Specification, and every other number that appears twice: docs/sources.md.
That file is the single home for values.
Read
docs/design-report.mdfirst — the official design report. Anyone planning to work on this instrument should go through it before touching anything else; it is the complete account of what the design is and why. The wider plan and conventions:docs/project-guide.md.
├─ docs/
│ ├─ design-report.md THE OFFICIAL DESIGN REPORT — read this before working on anything
│ ├─ project-guide.md the full plan: phases, gates, conventions (Appendix A = firmware rules)
│ ├─ sources.md canonical values — the single home for every number
│ └─ bom-part-selection.md
├─ firmware/ Teensy sketches: controller/, ad7124_firstlight/, ad7124_ratiometric/
├─ hardware/ reference-design/ (Sinara Thermostat) + datasheets/
├─ PCB/ the KiCad project, reviews and fabrication outputs
└─ tools/ bench utilities + check_docs.py
The full lab notebook — session-by-session history, gate measurements and their artifacts — is kept in the lab's internal repository and is not published. This repository carries the design.
Every choice is judged against four things: low cost (the lab builds one per laser), JLCPCB-assembly-friendly, modular / multi-channel (a channel is a firmware loop over more ADC inputs and MAX1968 stages, not a redesign), and simple.
We copy the Sinara power-stage topology and its architecture lessons — nothing else. We diverge on purpose: AD7124-8 + Teensy/C++ against their ADC and STM32/Rust.
Full conventions in Appendix A of docs/project-guide.md.
- All math in plain floating point on the Teensy.
- The PID loop closes locally — never depends on a network packet. Ethernet is telemetry only.
- Read the ADC ID register first before trusting anything downstream.
- Fail safe: open sensor or railed ADC → TEC current to zero, never to full heating.
Do the steps in order. Several of them are irreversible-if-skipped: the Teensy mod must happen before the Teensy is soldered down, and the TEC must not be connected until the front end is trusted.
Written after the session-42 schematic review. Every number here is traced to a datasheet; the derivations are in the lab's internal notebook (session 42).
0.1 Verify SW1's pinout against the vendor drawing. SW1 (MST-12D18G4, LCSC C49023767)
is wired with pin 2 as the common pole, pin 1 to GND, pin 3 to +5V. That is only correct
if pin 2 really is the wiper.
With a loose switch on the bench, meter across pin 1 and pin 3. It must read open in both slider positions. If it ever reads short, the footprint numbering is wrong and soldering it down puts a dead short across +5 V. Stop and re-map the footprint.
0.2 Confirm the four screw terminals are labelled. J4–J7 are four identical 5.08 mm
2-pin terminals and nothing physically stops you swapping them. Re-read from
PCB/review-2026-08-10-drive-stage.md before wiring —
this table was wrong for three sessions, naming J1 as the TEC and J4 as arm 2's sensor, which
is the one swap that is not survivable:
| Ref | Signal | |
|---|---|---|
| TEC | J4 | TEC+ (pin 1) / TEC− (pin 2) |
| Sensor — arm 1 (current excitation) | J6 | NTC_HI1 (pin 1) / NTC_LO1 (pin 2) |
| Sensor — arm 2 (ADR4525 excitation) | J7 | NTC_LO2 (pin 1) / NTC_HI2 (pin 2) ← pin order reversed vs J5/J6 |
| Sensor — arms 3/4 (JP1-selected) | J5 | NTC_HI3 (pin 1) / NTC_LO3 (pin 2) |
J1/J2/J3 are not sensor terminals. They are 2-pin headers for metering the MAXV/MAXIP/MAXIN
trimmers, and they are pin headers rather than screw terminals — the shape is the tell.
Plugging the TEC into a sensor terminal is survivable — the 1 kΩ series resistors (R19–R23, R28, R29) limit the fault to ~0.4 mA into the AD7124's clamp diodes against a 10 mA absolute maximum. The reverse is not: a sensor in J4 sits directly across a 6 V, 3 A bipolar driver. Label the panel.
0.3 JP1 silkscreen. A = ADR4525 (+2V5), B = AD7124 REFOUT. A 2-pin shorting block
bridges A–C or C–B, never both. Jumper removed entirely floats the divider and rails
AIN3/AIN4 — detectable, harmless.
Do this before the Teensy 4.1 is soldered to the board. It is far easier on a loose Teensy and the pads are on the underside.
The board ORs two 5 V sources into the Teensy's VIN through Schottky diodes:
+5V (backplane, J8 A9–A17) ──▶|── D3 ──┐
├── U1.48 VIN
VUSB (U1.49) ─────────────▶|── D2 ──────┘
Both diodes are oriented correctly (verified: pin 1 is the cathode on the MBR120VLSFT1G
symbol, so both conduct into VIN). But a stock Teensy 4.1 ships with VUSB and VIN
joined by a trace on the underside. Leave it intact and:
+5Vfrom the backplane reachesVUSBthrough D3, so the board back-feeds the host PC's USB port whenever the crate is powered.- With USB also plugged in, the backplane supply and the host's 5 V fight each other across D3 with nothing but their output impedances between them.
- D2 is shorted out and does nothing.
- On the underside of the Teensy 4.1, find the two small pads joined by a short trace,
marked for separating
VUSBfromVIN(PJRC documents this as the standard "external power" mod — check their photo for the exact location on your board revision). - Before cutting, meter continuity between the
VUSBpad and theVINpin. It should read ~0 Ω. This confirms you have found the right trace. - Cut the trace with a sharp knife. Cut once, cleanly — do not scrape a wide gouge.
- After cutting, meter again.
VUSBtoVINmust now read open (> 1 MΩ). This is the authoritative check; do not proceed on visual inspection alone. - Note the cut in the build record. It is invisible once the Teensy is mounted, and the next person to touch the board cannot tell by looking.
If you ever replace the Teensy, repeat this step. A fresh Teensy dropped into a working board re-introduces the fault silently.
2.1 SW1 to the OFF position (~SHDN low) before applying power. This is the only
shutdown path on the board — firmware cannot disable the MAX1968 (noted in the lab
notebook, session 42, kept internally). D1 lit = driver armed.
2.2 Apply +5 V from the backplane. Confirm at the test points:
| Rail | Expected | Source |
|---|---|---|
+5V |
5.0 V | backplane J8 |
+3.3VD |
3.3 V | Teensy onboard LDO (U1.46) |
+3.3VA |
3.3 V | U3 TPS7A2033 |
Net-(U5-AVDD) |
= +3.3VA |
through R25 (0 Ω) |
+2V5 |
2.500 V | U4 ADR4525 |
Net-(U4-IN) |
≈ 4.87 V | +5V through R16 (100 Ω) |
Net-(U2-MAXIP) (= REF) |
1.500 V | MAX1968 internal reference |
Net-(U2-MAXV) |
wherever R6 is turned | 50 kΩ trimpot, not preset |
Net-(U2-MAXIP) |
wherever R14 is turned | 50 kΩ trimpot, not preset |
Net-(U2-MAXIN) |
wherever R10 is turned | 50 kΩ trimpot, not preset |
2.3 Read the AD7124 ID register before trusting anything downstream (project rule,
Appendix A). Expect 0x1_. This is the first-light gate.
Connect the NTCs to J6/J7/J5 and confirm the DC node voltages with a DMM before believing any conversion. Predicted values (10 kΩ NTC at 25 °C):
| Arm | Excitation | Nodes | Expected |
|---|---|---|---|
| 1 (J6) | IOUT 50 µA on AIN7 | IOUT node / NTC_HI1 / NTC_LO1 |
1.835 / 0.735 / 0.235 V |
| REFIN1± = 1.835 / 0.735 V → V_REF = 1.100 V | |||
| 2 (J7) | +2V5 |
NTC_HI2 (AIN13) / NTC_LO2 (AIN12) |
1.869 / 0.631 V |
| 3/4 (J5) | JP1: A = +2V5, B = REFOUT |
NTC_HI3 (AIN3) / NTC_LO3 (AIN4) |
2.500 / 1.250 V |
If a node is wrong, stop. Do not connect the TEC to debug a front end.
Only after Step 3 passes.
TEC: RS PRO 2172415, Imax = 2.5 A (both directions), Vmax = 7.62 V @ Th 25 °C
(8.1 V @ 50 °C), R_AC = 2.67 Ω @ 25 °C (3.01 Ω @ 50 °C), Qmax = 10.6 W, ΔTmax = 67 °C.
🔴 The three limits are trimpots, not fixed dividers. Nothing is preset. R6 → MAXV,
R14 → MAXIP, R10 → MAXIN are 50 kΩ potentiometers from REF to GND; full travel reaches
±3.000 A, above this TEC's 2.5 A rating. They protect nothing until you turn them down.
Transfer functions and travel are in
PCB/review-2026-08-10-drive-stage.md.
Set them with firmware/board_monitor + tools/panel reading them live and the TEC
unplugged. Targets, and how they interact with that TEC:
MAXV → 4.00 V ⇒ I ≤ 4.00/2.67 = 1.50 A (cold) ← THE BINDING LIMIT
I ≤ 4.00/3.01 = 1.33 A (Th = 50 °C, TEC resistance rises)
MAXIP → 1.50 A ⇒ matches what MAXV already allows; above this it cannot bind
MAXIN → 1.50 A ⇒ same, in the heating direction
MAX1968 output range = ±4.3 V at VDD = 5 V
Set that way the board is a ±1.5 A machine, 60 % of the TEC's rating, in both directions. MAXV is what saturates the loop, not the current limit. Expect ~8.9 W of pumping at ΔT = 0.
Supply budget: at 1.5 A, P_TEC = 1.5² × 2.67 = 6.0 W → roughly 1.5 A drawn from +5 V
including the Teensy. Confirm the LRS-100 has the headroom.
Sanity-check current direction before closing the loop. V_CTLI > 1.50 V = cooling,
current flowing OS2 → OS1 (out of J4.1, into J4.2). Verify with a thermometer on the block
and an open loop before you let the PID drive it.
These are not optional. The reasoning is in the lab's internal notebook (session 42).
- Open-sensor latch on arms 2/3/4. Those arms have no hardware fail-safe — an open
sensor rails the reading to full scale, which reads as coldest, which commands maximum
heating. A railed code (
0xFFFFFF) must latch a fault and force CTLI to zero current, not be silently discarded and retried the wayad7124_ratiometric.ino:200does. - Check
REF_DET_ERRon arm 1. Arm 1's hardware fail-safe is real but only fires through that flag — on an open sensor both V_AIN and V_REF go to zero, so the raw code is indeterminate. The flag is the signal, not the value. - Never use
analogWrite(pin, 0)as "off". That drives CTLI to 0.03 V ≈ full current in the heating direction. Zero current is 45.45 % duty (CTLI = 1.500 V). - Clamp CTLI to its specified range, 0.5 V – 2.5 V, i.e. roughly 14 %–76 % duty.
- Per-arm reference buffer settings. Arm 1 needs
REF_BUFon (0x01E0); arm 2 needs it off (0x0068), because REFIN2(−) is tied to GND, which is legal unbuffered and out of spec buffered. Copying arm 1's config onto arm 2 goes out of spec with no error flag.
- Never power the board with SW1 armed and an untested loop. 1.5 A into a laser mount is enough to do damage before you can reach the switch.
- Never connect the TEC before Step 3 passes.
- Never solder a replacement Teensy without repeating Step 1.
- Never plug the TEC into J5/J6/J7.
- The bench is the final authority. Every calculation states the number it predicts, so it can be checked against reality. When the bench disagrees, the bench is right.
- One fact, one home. A number lives in exactly one file —
docs/sources.mdif it appears more than once — and everywhere else links to it. Superseding means updating it there and deleting every surviving copy, not adding a note beside it.
python3 tools/check_docs.pyChecks the docs for oversized sections and duplication.