This guide provides comprehensive documentation for operating the greymatter DAC Controller, including all available commands, addressing schemes, calibration procedures, and detailed implementation information.
- System Overview
- Getting Started
- Command Reference
- Addressing Scheme
- Span Configuration
- Calibration
- Code Flow and Implementation Details
- Hardware Architecture
- Troubleshooting
The greymatter DAC Controller manages 24 Digital-to-Analog Converters distributed across 8 daughter boards. Each board contains:
- 2x LTC2662: 5-channel current DACs (DAC 0 and DAC 1)
- 1x LTC2664: 4-channel voltage DAC (DAC 2)
Total outputs: 80 current channels + 32 voltage channels = 112 independent analog outputs
| DAC Type | Channels | Output Type | Range | Resolution |
|---|---|---|---|---|
| LTC2662 | 5 | Current | 3.125 mA - 300 mA | 12 or 16-bit |
| LTC2664 | 4 | Voltage | ±10V / 0-10V | 12 or 16-bit |
- Flash the firmware (see README.md)
- Connect via USB serial at 115200 baud
- Wait for the startup banner
# macOS/Linux
screen /dev/tty.usbmodem1101 115200
# Windows (use PuTTY or similar)
# Select appropriate COM port, 115200 baudOn power-up, the controller:
- Initializes USB CDC serial interface
- Waits for serial connection
- Configures SPI and GPIO peripherals
- Initializes all 24 DACs with default spans
- Loads calibration data from flash (if present)
- Reports any detected faults
- Enters command loop
*IDN? # Verify connection
FAULT? # Check for hardware faults
BOARD0:DAC2:CH0:VOLT 1.0 # Set a voltage output
BOARD0:DAC0:CH0:CURR 10.0 # Set a current output
All commands follow the SCPI (Standard Commands for Programmable Instruments) standard. Commands are case-insensitive and terminated with newline (\n).
| Command | Description | Response |
|---|---|---|
*IDN? |
Query device identification | greymatter,DAC Controller,<SN>,0.1 |
*RST |
Reset all DACs to power-on state | OK |
| Command | Description | Response |
|---|---|---|
FAULT? |
Query 24-bit fault status | OK or FAULT:0xNNNNNN |
SYST:ERR? |
Query error queue | 0,No error |
SYST:SN <string> |
Set controller serial number (max 31 chars, auto-saved to flash) | OK |
SYST:SN? |
Query controller serial number | Serial number or (not set) |
LDAC |
Pulse LDAC to update all outputs | OK |
UPDATE:ALL |
Update all DAC outputs | OK |
| Command | Format | Example |
|---|---|---|
| Set Voltage | BOARD<n>:DAC2:CH<c>:VOLT <value> |
BOARD0:DAC2:CH0:VOLT 5.0 |
| Set Channel Span | BOARD<n>:DAC2:CH<c>:SPAN <code> |
BOARD0:DAC2:CH0:SPAN 3 |
| Set All Spans | BOARD<n>:DAC2:SPAN:ALL <code> |
BOARD0:DAC2:SPAN:ALL 3 |
| Command | Format | Example |
|---|---|---|
| Set Current | BOARD<n>:DAC<0-1>:CH<c>:CURR <value> |
BOARD0:DAC0:CH0:CURR 50.0 |
| Set Channel Span | BOARD<n>:DAC<0-1>:CH<c>:SPAN <code> |
BOARD0:DAC0:CH0:SPAN 6 |
| Set All Spans | BOARD<n>:DAC<0-1>:SPAN:ALL <code> |
BOARD0:DAC0:SPAN:ALL 6 |
| Command | Format | Example |
|---|---|---|
| Set Code | BOARD<n>:DAC<m>:CH<c>:CODE <value> |
BOARD0:DAC0:CH0:CODE 32767 |
| Update DAC | BOARD<n>:DAC<m>:UPDATE |
BOARD0:DAC0:UPDATE |
| Command | Format | Description |
|---|---|---|
| Query Resolution | BOARD<n>:DAC<m>:RES? |
Returns 12 or 16 |
| Set Resolution | BOARD<n>:DAC<m>:RES <12|16> |
Re-initializes DAC with new resolution |
| Command | Format | Description |
|---|---|---|
| Power Down Channel | BOARD<n>:DAC<m>:CH<c>:PDOWN |
Powers down single channel |
| Power Down Chip | BOARD<n>:DAC<m>:PDOWN |
Powers down entire DAC |
| Command | Description | Response |
|---|---|---|
BOARD<n>:SN <string> |
Set board serial number | OK |
BOARD<n>:SN? |
Query board serial number | Serial number or (not set) |
BOARD<n>:DAC<m>:CH<c>:CAL:GAIN <value> |
Set gain factor | OK |
BOARD<n>:DAC<m>:CH<c>:CAL:GAIN? |
Query gain factor | Gain value |
BOARD<n>:DAC<m>:CH<c>:CAL:OFFS <value> |
Set offset | OK |
BOARD<n>:DAC<m>:CH<c>:CAL:OFFS? |
Query offset | Offset value |
BOARD<n>:DAC<m>:CH<c>:CAL:EN <0|1> |
Enable/disable calibration | OK |
BOARD<n>:DAC<m>:CH<c>:CAL:EN? |
Query calibration enable | 0 or 1 |
CAL:DATA? |
Export all calibration data | Formatted data string |
CAL:SAVE |
Save calibration to flash | OK or error |
CAL:LOAD |
Load calibration from flash | OK or error |
CAL:CLEAR |
Clear all calibration data | OK |
# === Voltage Output Examples ===
# Set Board 0, Voltage DAC, Channel 0 to +5V
BOARD0:DAC2:CH0:VOLT 5.0
# Set Board 3, Voltage DAC, Channel 2 to -3.3V (bipolar mode)
BOARD3:DAC2:CH2:VOLT -3.3
# Configure all voltage channels on Board 0 to ±10V range
BOARD0:DAC2:SPAN:ALL 3
# === Current Output Examples ===
# Set Board 0, Current DAC 0, Channel 1 to 50 mA
BOARD0:DAC0:CH1:CURR 50.0
# Set Board 5, Current DAC 1, Channel 4 to 100 mA
BOARD5:DAC1:CH4:CURR 100.0
# Configure all current channels on Board 2, DAC 0 to 200 mA range
BOARD2:DAC0:SPAN:ALL 7
# === Raw Code Examples ===
# Set mid-scale (32767 = 50% of full scale for 16-bit)
BOARD0:DAC0:CH0:CODE 32767
# Set maximum (65535 = 100% of full scale for 16-bit)
BOARD0:DAC0:CH0:CODE 65535
# === Resolution Examples ===
# Query current resolution
BOARD0:DAC0:RES?
# Set to 12-bit resolution
BOARD0:DAC0:RES 12
# === System Commands ===
# Check device identity
*IDN?
# Reset all DACs
*RST
# Check for faults
FAULT?
# Global LDAC pulse (update all pending outputs)
LDAC
# Power down a channel
BOARD0:DAC0:CH0:PDOWN
# Power down entire DAC chip
BOARD0:DAC0:PDOWNBOARD<n>:DAC<m>:CH<c>
│ │ │
│ │ └── Channel: 0-4 (LTC2662) or 0-3 (LTC2664)
│ │
│ └── DAC: 0, 1 (current DAC - LTC2662)
│ 2 (voltage DAC - LTC2664)
│
└── Board: 0-7 (8 daughter boards)
Internally, DACs are indexed 0-23:
dac_index = (board_id × 3) + device_id
| Board | DAC 0 (LTC2662) | DAC 1 (LTC2662) | DAC 2 (LTC2664) |
|---|---|---|---|
| 0 | Index 0 | Index 1 | Index 2 |
| 1 | Index 3 | Index 4 | Index 5 |
| 2 | Index 6 | Index 7 | Index 8 |
| 3 | Index 9 | Index 10 | Index 11 |
| 4 | Index 12 | Index 13 | Index 14 |
| 5 | Index 15 | Index 16 | Index 17 |
| 6 | Index 18 | Index 19 | Index 20 |
| 7 | Index 21 | Index 22 | Index 23 |
LTC2662 (Current DAC):
- 5 channels: CH0, CH1, CH2, CH3, CH4
- Total per board: 10 current channels
LTC2664 (Voltage DAC):
- 4 channels: CH0, CH1, CH2, CH3
- Total per board: 4 voltage channels
| Code | Full-Scale Current | Notes |
|---|---|---|
| 0x0 | Hi-Z (disabled) | Output disabled |
| 0x1 | 3.125 mA | |
| 0x2 | 6.25 mA | |
| 0x3 | 12.5 mA | |
| 0x4 | 25 mA | |
| 0x5 | 50 mA | |
| 0x6 | 100 mA | Default |
| 0x7 | 200 mA | |
| 0x8 | Switch to V- | Special mode |
| 0xF | 300 mA | Maximum |
Example: Set 200 mA range on Board 0, DAC 0:
BOARD0:DAC0:SPAN:ALL 7
| Code | Range | Type | Min | Max |
|---|---|---|---|---|
| 0 | 0V to 5V | Unipolar | 0V | +5V |
| 1 | 0V to 10V | Unipolar | 0V | +10V |
| 2 | ±5V | Bipolar | -5V | +5V |
| 3 | ±10V | Bipolar | -10V | +10V |
| 4 | ±2.5V | Bipolar | -2.5V | +2.5V |
Default: ±10V (code 3)
Example: Set ±5V range on Board 0, DAC 2:
BOARD0:DAC2:SPAN:ALL 2
Current Output (LTC2662):
I_out = (code / max_code) × I_fullscale
Voltage Output (LTC2664):
Unipolar: V_out = (code / max_code) × V_max
Bipolar: V_out = V_min + (code / max_code) × (V_max - V_min)
Where max_code is 4095 for 12-bit or 65535 for 16-bit resolution.
The greymatter firmware supports two-point linear calibration for each DAC channel to correct for gain and offset errors.
calibrated_output = (ideal_output × gain) + offset
Where:
ideal_outputis the requested voltage (V) or current (mA)gainis the gain correction factor (nominally 1.0)offsetis the offset correction in physical units (nominally 0.0)
- Keithley Source-Measure Unit (SMU) or equivalent high-precision meter
- For voltage DACs (LTC2664): Voltage measurement with at least 6-digit resolution
- For current DACs (LTC2662): Current measurement with at least 6-digit resolution
- Test leads appropriate for the output being measured
- Temperature-stable environment (allow 30 minutes warm-up)
The goal is to characterize the transfer function of each DAC channel by measuring the actual output at two known setpoints, then computing correction factors.
Given:
- Point 1: Set
V_set_low, measureV_meas_low - Point 2: Set
V_set_high, measureV_meas_high
Calibration factor calculation:
gain_cal = (V_set_high - V_set_low) / (V_meas_high - V_meas_low)
offset_cal = V_set_low - (gain_cal × V_meas_low)
-
Setup
- Connect SMU to the voltage output under test
- Configure SMU for voltage measurement (high-Z input)
- Ensure appropriate span is set for the channel
-
Set calibration points
- For bipolar spans (±10V, ±5V, ±2.5V): Use 10% and 90% of range
- For unipolar spans (0-5V, 0-10V): Use 10% and 90% of range
Example for ±10V span:
Low point: -8.0V (10% from -10V) High point: +8.0V (90% toward +10V) -
Measure low point
BOARD0:DAC2:CH0:VOLT -8.0 OKRecord SMU reading:
V_meas_low = -8.0123(example) -
Measure high point
BOARD0:DAC2:CH0:VOLT 8.0 OKRecord SMU reading:
V_meas_high = +7.9987(example) -
Calculate calibration factors
gain_cal = (8.0 - (-8.0)) / (7.9987 - (-8.0123)) = 16.0 / 16.011 = 0.999313 offset_cal = -8.0 - (0.999313 × (-8.0123)) = -8.0 - (-8.0068) = 0.0068 -
Apply calibration
BOARD0:DAC2:CH0:CAL:GAIN 0.999313 OK BOARD0:DAC2:CH0:CAL:OFFS 0.0068 OK BOARD0:DAC2:CH0:CAL:EN 1 OK -
Verify calibration
BOARD0:DAC2:CH0:VOLT -8.0SMU should now read closer to -8.000V
-
Setup
- Connect SMU to the current output under test
- Configure SMU for current measurement (low burden voltage)
- Set appropriate span for expected current range
-
Set calibration points
- Use 10% and 90% of the configured span
Example for 100mA span:
Low point: 10.0 mA High point: 90.0 mA -
Measure low point
BOARD0:DAC0:CH0:CURR 10.0 OKRecord SMU reading:
I_meas_low = 10.015mA (example) -
Measure high point
BOARD0:DAC0:CH0:CURR 90.0 OKRecord SMU reading:
I_meas_high = 89.985mA (example) -
Calculate calibration factors
gain_cal = (90.0 - 10.0) / (89.985 - 10.015) = 80.0 / 79.970 = 1.000375 offset_cal = 10.0 - (1.000375 × 10.015) = 10.0 - 10.0188 = -0.0188 -
Apply calibration
BOARD0:DAC0:CH0:CAL:GAIN 1.000375 OK BOARD0:DAC0:CH0:CAL:OFFS -0.0188 OK BOARD0:DAC0:CH0:CAL:EN 1 OK -
Verify calibration
BOARD0:DAC0:CH0:CURR 50.0SMU should now read closer to 50.000 mA
Each Pico controller can be assigned a unique serial number to identify it when multiple controllers are connected via USB. This is stored in a separate flash sector from calibration data, so CAL:CLEAR will not erase it.
# Set controller serial number (auto-saved to flash)
SYST:SN GM-CTRL-001
OK
# Query controller serial number
SYST:SN?
GM-CTRL-001
# Serial number appears in *IDN? response
*IDN?
greymatter,DAC Controller,GM-CTRL-001,0.1
The serial number also appears in the startup banner and is used by the ZMQ server to name discovered controllers.
Calibration data is stored in the RP2350's onboard flash memory, in the last 4KB sector (offset 0x1FF000).
Workflow:
- Perform calibration using the procedures above
- Save to flash:
CAL:SAVE - Calibration persists across power cycles
Automatic Loading: On power-up, the firmware automatically loads calibration data from flash if valid data is found.
Example session:
# Set calibration for a channel
BOARD0:DAC2:CH0:CAL:GAIN 0.999313
OK
BOARD0:DAC2:CH0:CAL:OFFS 0.0068
OK
BOARD0:DAC2:CH0:CAL:EN 1
OK
# Save to flash
CAL:SAVE
OK
# Power cycle the device...
# After power-up, verify calibration was loaded
BOARD0:DAC2:CH0:CAL:GAIN?
0.999313
BOARD0:DAC2:CH0:CAL:EN?
1
The CAL:DATA? command returns calibration data in a human-readable format:
BOARD0:SN=GM-2024-001
DAC2:CH0:G=0.999313,O=0.006800,E=1
DAC2:CH1:G=1.000125,O=-0.003200,E=1
BOARD1:SN=GM-2024-002
DAC0:CH0:G=1.000375,O=-0.018800,E=1
- Location: Last 4KB sector of 2MB flash (offset 0x1FF000)
- Data integrity: CRC-16 checksum validates stored data
- Magic number: 0x47524D43 ("GRMC") identifies valid calibration data
- Wear leveling: Flash is only written when
CAL:SAVEis explicitly called - Sector erase: Each save erases and rewrites the entire sector (typical flash endurance: 100,000 cycles)
- Temperature stability: Allow 30 minutes warm-up before calibrating
- Calibration points: Use 10% and 90% of range to capture the full transfer function
- Verification: Always verify calibration by measuring at the midpoint
- Documentation: Record serial numbers and calibration dates
- Periodic recalibration: Recalibrate annually or after significant temperature excursions
- Per-span calibration: If using multiple spans, consider calibrating each span separately
Here's a Python example for automated calibration using PyVISA:
import pyvisa
import time
# Initialize instruments
rm = pyvisa.ResourceManager()
keithley = rm.open_resource('GPIB0::24::INSTR') # Adjust address
greymatter = rm.open_resource('ASRL/dev/tty.usbmodem1101::INSTR')
def calibrate_voltage_channel(board, dac, channel, span_min, span_max):
"""Calibrate a voltage DAC channel."""
# Calculate calibration points (10% and 90% of range)
v_low = span_min + 0.1 * (span_max - span_min)
v_high = span_min + 0.9 * (span_max - span_min)
# Measure low point
greymatter.write(f'BOARD{board}:DAC{dac}:CH{channel}:VOLT {v_low}')
time.sleep(0.5) # Allow settling
v_meas_low = float(keithley.query(':MEAS:VOLT?'))
# Measure high point
greymatter.write(f'BOARD{board}:DAC{dac}:CH{channel}:VOLT {v_high}')
time.sleep(0.5)
v_meas_high = float(keithley.query(':MEAS:VOLT?'))
# Calculate calibration factors
gain = (v_high - v_low) / (v_meas_high - v_meas_low)
offset = v_low - (gain * v_meas_low)
# Apply calibration
greymatter.write(f'BOARD{board}:DAC{dac}:CH{channel}:CAL:GAIN {gain:.6f}')
greymatter.write(f'BOARD{board}:DAC{dac}:CH{channel}:CAL:OFFS {offset:.6f}')
greymatter.write(f'BOARD{board}:DAC{dac}:CH{channel}:CAL:EN 1')
print(f'Board {board}, DAC {dac}, CH {channel}:')
print(f' Gain: {gain:.6f}, Offset: {offset:.6f} V')
return gain, offset
# Example: Calibrate Board 0, DAC 2 (voltage), all channels
for ch in range(4):
calibrate_voltage_channel(0, 2, ch, -10.0, 10.0)
# Save calibration to flash
greymatter.write('CAL:SAVE')This section describes the internal implementation for developers and advanced users.
When the firmware starts, the following initialization sequence occurs:
main()
│
├─► stdio_init_all()
│ Initialize USB CDC for serial communication
│
├─► Wait for tud_cdc_connected()
│ Block until a serial terminal connects
│
├─► SpiManager::init()
│ │
│ ├─► init_gpio()
│ │ ├── GP21 = HIGH (enable TXB0106 level shifter) [CRITICAL FIRST]
│ │ ├── GP22 = output (expander reset)
│ │ ├── GP20 = input + pull-up (fault detection)
│ │ └── GP17 = output, HIGH (SPI chip select)
│ │
│ ├─► init_spi()
│ │ ├── spi_init(spi0, SPI_BAUDRATE) // Configurable, default 10 MHz
│ │ ├── Configure GP16 (MISO), GP18 (CLK), GP19 (MOSI)
│ │ └── SPI Mode 0 (CPOL=0, CPHA=0)
│ │
│ ├─► reset_io_expanders()
│ │ ├── GP22 = LOW for 10µs
│ │ └── GP22 = HIGH, wait 100µs
│ │
│ └─► IoExpander::init(spi0)
│ ├── Enable HAEN on all expanders (address broadcast)
│ ├── Configure EXPANDER_0 (CS bits, D_EN, LDAC, CLR)
│ ├── Configure EXPANDER_1 (FAULT0-15 inputs)
│ └── Configure EXPANDER_2 (FAULT16-23 inputs)
│
├─► BoardManager::init_all()
│ │
│ ├─► For each board (0-7):
│ │ ├── Create LTC2662 instances (DAC 0, 1)
│ │ │ └── init(): set_span_all(0x6), update_all()
│ │ │
│ │ └── Create LTC2664 instance (DAC 2)
│ │ └── init(): set_span_all(0x3), update_all()
│ │
│ └─► CalStorage::load_from_flash()
│ └── Load calibration data if valid
│
├─► Check initial fault status
│ └── If GP20 is LOW, read and report fault mask
│
└─► Enter main command loop
main loop
│
├─► Read line from USB serial
│ └── Accumulate characters until '\n' or '\r'
│
├─► ScpiParser::parse(line)
│ │
│ ├─► Skip leading whitespace
│ │
│ ├─► Try parse_common_command()
│ │ └── Match *IDN?, *RST
│ │
│ ├─► Try parse_system_command()
│ │ └── Match FAULT?, SYST:ERR?, LDAC, UPDATE:ALL, CAL:*
│ │
│ └─► Try parse_board_command()
│ ├── Extract BOARD<n>
│ ├── Extract :DAC<m> or :SN
│ └── Parse subcommand:
│ ├── CH<c>:VOLT, CH<c>:CURR, CH<c>:CODE
│ ├── CH<c>:SPAN, CH<c>:PDOWN, CH<c>:CAL:*
│ ├── SPAN:ALL, RES
│ ├── UPDATE
│ └── PDOWN
│
├─► BoardManager::execute(cmd)
│ │
│ └─► Switch on command type:
│ │
│ ├─► SET_VOLTAGE
│ │ ├── Validate: DAC must be 2 (LTC2664)
│ │ ├── Apply calibration if enabled
│ │ ├── Get span range from LTC2664_SPAN_INFO[]
│ │ ├── Clamp voltage to range
│ │ ├── Calculate: code = normalize(voltage) × max_code
│ │ └── dac->send_command(WRITE_UPDATE_N, channel, code)
│ │
│ ├─► SET_CURRENT
│ │ ├── Validate: DAC must be 0 or 1 (LTC2662)
│ │ ├── Apply calibration if enabled
│ │ ├── Get full-scale from span setting
│ │ ├── Clamp current to 0..full_scale
│ │ ├── Calculate: code = (current / full_scale) × max_code
│ │ └── dac->send_command(WRITE_UPDATE_N, channel, code)
│ │
│ └─► ... (other commands)
│
└─► Send response via USB serial
Every DAC command involves a multi-step SPI transaction:
SpiManager::transaction(board_id, device_id, tx_buf, rx_buf, len)
│
├─► Step 1: Select DAC via I/O Expander
│ │
│ └─► IoExpander::set_dac_select(board_id, device_id)
│ ├── Calculate dac_index = board_id × 3 + device_id
│ ├── Set CS0-CS4 bits (5-bit address)
│ ├── Set D_EN bit HIGH (enable decoder tree)
│ ├── Write to EXPANDER_0 Port A
│ └── sleep_us(1) // Decoder settling
│
├─► Step 2: SPI Transfer to DAC
│ │
│ ├── NOTE: CS (GP17) is NOT asserted here
│ │ The decoder tree provides chip select to the DAC
│ │
│ └── spi_write_read_blocking(spi0, tx_buf, rx_buf, len)
│
├─► sleep_us(1) // Data latch time
│
└─► Step 3: Deselect DAC
│
└─► IoExpander::deselect_dac()
├── Clear D_EN bit (disable decoder)
└── Write to EXPANDER_0 Port A
┌─────────────────────────────────────────────────────────────┐
│ Byte 0 (MSB) │ Byte 1 │ Byte 2 (LSB) │
├────────┬───────────┼──────────────────┼────────────────────┤
│ Command│ Address │ Data[15:8] │ Data[7:0] │
│ [7:4] │ [3:0] │ │ │
└────────┴───────────┴──────────────────┴────────────────────┘
DacDevice::send_command(command, address, data):
tx_buf[0] = ((command & 0x0F) << 4) | (address & 0x0F)
tx_buf[1] = (data >> 8) & 0xFF
tx_buf[2] = data & 0xFF
Command Codes:
| Code | Name | Description |
|---|---|---|
| 0x0 | WRITE_N | Write code to input register |
| 0x1 | UPDATE_N | Copy input to DAC register |
| 0x2 | WRITE_ALL_UPDATE_ALL | Write all, update all |
| 0x3 | WRITE_UPDATE_N | Write and update single channel |
| 0x4 | POWER_DOWN_N | Power down channel |
| 0x5 | POWER_DOWN_CHIP | Power down entire chip |
| 0x6 | WRITE_SPAN_N | Set span for channel |
| 0x7 | CONFIG | Configuration register |
| 0x8 | WRITE_ALL | Write to all channels |
| 0x9 | UPDATE_ALL | Update all channels |
| 0xA | WRITE_ALL_UPDATE_ALL | Write all, update all |
| 0xB | MUX | Monitor/analog mux |
| 0xE | WRITE_SPAN_ALL | Set span for all channels |
| 0xF | NOP | No operation |
The three MCP23S17 expanders are addressed via hardware pins:
EXPANDER_0 (Address 0x0): Control signals
├── Port A [7:0]:
│ ├── [4:0] CS0-CS4: 5-bit DAC address (bit-reversed)
│ └── [5] D_EN: Decoder enable
└── Port B [7:0]:
├── [0] LDAC: Load DAC (active-low)
└── [7] CLR: Clear (active-low)
EXPANDER_1 (Address 0x1): Fault inputs 0-15
├── Port A [7:0]: Boards 0-3, DACs 0-1 (active-low)
└── Port B [7:0]: Boards 4-7, DACs 0-1 (active-low)
EXPANDER_2 (Address 0x2): Temperature fault inputs
└── Port A [7:0]: All 8 boards, DAC 2 (active-low)
MCP23S17 SPI Protocol:
Write: [0x40 | (addr << 1) | 0] [register] [data]
Read: [0x40 | (addr << 1) | 1] [register] → [data]
Fault Monitoring:
│
├─► Hardware: GP20 connected to OR'd FAULT outputs
│ └── Active-low: LOW = at least one fault
│
├─► FAULT? Command:
│ ├── Check GP20 state
│ │
│ ├── If HIGH (no fault):
│ │ └── Return "OK"
│ │
│ └── If LOW (fault present):
│ ├── Read EXPANDER_1 GPIO (faults 0-15)
│ ├── Read EXPANDER_2 Port A (faults 16-23)
│ ├── Invert bits (active-low → active-high)
│ ├── Reorganize to DAC index order
│ └── Return "FAULT:0xNNNNNN"
│
└─► Bit Position Mapping:
Bit 0 → Board 0, DAC 0
Bit 1 → Board 0, DAC 1
Bit 2 → Board 0, DAC 2
...
Bit 23 → Board 7, DAC 2
┌─────────────────────────────────────┐
│ Daughter Board 0 │
│ ┌─────────┐ ┌─────────┐ ┌────────┐ │
┌────►│ │ LTC2662 │ │ LTC2662 │ │LTC2664 │ │
│ │ │ DAC 0 │ │ DAC 1 │ │ DAC 2 │ │
│ │ │ 5ch I │ │ 5ch I │ │ 4ch V │ │
│ │ └─────────┘ └─────────┘ └────────┘ │
┌──────────────┐ │ └─────────────────────────────────────┘
│ │ │ ⋮
│ RP2350 │ SPI │ ┌─────────────────────────────────────┐
│ (Pico 2) │◄────────────►│ │ Daughter Board 7 │
│ │ │ │ ┌─────────┐ ┌─────────┐ ┌────────┐ │
│ GP16 MISO │ └────►│ │ LTC2662 │ │ LTC2662 │ │LTC2664 │ │
│ GP17 CS │ │ │ DAC 21 │ │ DAC 22 │ │ DAC 23 │ │
│ GP18 CLK │ ┌──────────┐ │ │ 5ch I │ │ 5ch I │ │ 4ch V │ │
│ GP19 MOSI │────►│ TXB0106 │ │ └─────────┘ └─────────┘ └────────┘ │
│ │ │ Level │ └─────────────────────────────────────┘
│ GP20 FAULT◄─┼─────│ Shifter │
│ GP21 OE────►│ └──────────┘
│ GP22 RST───►│ │
│ │ ▼
└──────────────┘ ┌──────────────────────────────────┐
│ MCP23S17 I/O Expanders │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ EXP 0 │ │ EXP 1 │ │ EXP 2 │ │
│ │CS,LDAC │ │FAULT │ │FAULT │ │
│ │CLR,DEN │ │ 0-15 │ │ 16-23 │ │
│ └────────┘ └────────┘ └────────┘ │
└──────────────────────────────────┘
│
▼
┌──────────────────┐
│ Decoder Tree │
│ (5-to-24 CS) │
└──────────────────┘
- Commands arrive via USB serial (115200 baud)
- SCPI Parser tokenizes and validates the command
- Board Manager routes to the appropriate DAC driver
- DAC Driver constructs the 24-bit SPI command
- SPI Manager orchestrates the multi-step transaction:
- Sets decoder address via I/O expander
- Sends SPI data to DAC
- Deselects DAC
- Response sent back via USB serial
| Operation | Minimum Time | Code Implementation |
|---|---|---|
| Level shifter enable to SPI | 0 µs | Set GP21 first thing in init |
| Expander reset pulse | 10 µs low | sleep_us(10) |
| Expander reset recovery | 100 µs | sleep_us(100) |
| Decoder settling | 1 µs | sleep_us(1) |
| DAC data latch | 1 µs | sleep_us(1) |
| LDAC pulse width | 20 ns min | sleep_us(1) |
- Check USB connection: Device should enumerate as USB CDC
- Verify baud rate: Must be 115200
- Check terminal settings: No hardware flow control
- Try
*IDN?: Should return identification string
The fault mask indicates which DACs have faults:
FAULT:0x000001 → DAC 0 (Board 0, DAC 0) has fault
FAULT:0x000004 → DAC 2 (Board 0, DAC 2) has fault
FAULT:0x800000 → DAC 23 (Board 7, DAC 2) has fault
Common causes:
- Overcurrent condition (LTC2662)
- Thermal shutdown
- Output shorted
- Power supply issue
- Check span setting: Output must be within configured span
- Verify addressing: Board (0-7), DAC (0-2), Channel varies by DAC type
- Try raw code:
BOARD0:DAC0:CH0:CODE 32767(mid-scale) - Issue LDAC:
LDACto update all outputs
The firmware clamps values to the configured span:
# If span is 100 mA (code 6):
BOARD0:DAC0:CH0:CURR 200.0 # Clamped to 100 mA
# If span is ±5V (code 2):
BOARD0:DAC2:CH0:VOLT 8.0 # Clamped to +5VSolution: Set appropriate span first:
BOARD0:DAC0:SPAN:ALL 7 # 200 mA range
BOARD0:DAC2:SPAN:ALL 3 # ±10V range- Verify calibration is enabled:
BOARD<n>:DAC<m>:CH<c>:CAL:EN?should return1 - Check gain and offset values are reasonable (gain near 1.0, offset near 0)
- Ensure
CAL:SAVEwas called after setting calibration - Verify calibration was loaded:
BOARD<n>:DAC<m>:CH<c>:CAL:EN?should return1 - Check for valid data in flash:
CAL:LOADshould returnOK - If flash is corrupted, recalibrate and save again
- Check PICO_SDK_PATH: Must point to valid Pico SDK installation
- Verify compiler: ARM cross-compiler required
- Clean build: Delete
build/directory and rebuild
rm -rf build
mkdir build
cd build
cmake .. -DPICO_BOARD=pico2
make picotoolBuild
C_INCLUDE_PATH= CPLUS_INCLUDE_PATH= make| File | Purpose |
|---|---|
main.cpp |
Entry point, USB serial loop |
scpi_parser.cpp/hpp |
SCPI command parsing |
board_manager.cpp/hpp |
DAC routing and execution |
spi_manager.cpp/hpp |
SPI peripheral control |
io_expander.cpp/hpp |
MCP23S17 driver |
dac_device.cpp/hpp |
Abstract DAC base class |
ltc2662.cpp/hpp |
Current DAC driver |
ltc2664.cpp/hpp |
Voltage DAC driver |
cal_storage.cpp/hpp |
Flash-based calibration persistence |
utils.cpp/hpp |
String utilities |