Quantum Interactive

ODMR Experiment with QOOOL Kit DIY

QOOOL Kit DIY - CW-ODMR Experiment

In this example, we demonstrate how to implement a quantum sensor with microwave excitation using the QOOOL Kit DIY and the senseBox, which functions as a magnetic detector. In addition to the senseBox, we use a PC to retrieve and display measurement values, a red LED, a transistor, various resistors, and the Arduino IDE, as well as some 3D-printed parts designed in Tinkercad. A Python script running on the PC controls the measurement.

The Idea

The green LED of the QOOOL Kit DIY continuously optically excites the diamond, hence the term Continuous Wave Optically Detected Magnetic Resonance (CW-ODMR). Simultaneously, a microwave signal is swept through individual frequency steps, and the photodiode with a red color filter detects the fluorescence intensity of the diamond at each microwave frequency. The diamond fluoresces with varying brightness depending on the microwave frequency and the applied magnetic field. Plotting the fluorescence against the microwave frequency yields a fluorescence spectrum with resonance dips.

To achieve this, the LED must be powered, and the photodiode current (note: the photodiode must be operated in reverse bias) must be measured. The digitization of the photocurrent is done by measuring the voltage drop across a resistor, which is then digitized using the Analog-to-Digital Converter (ADC) of the senseBox. A red LED serves as an indicator that the microwave generator (in this case, an ADF4351 board, available for around €30 on eBay or Amazon) is not outputting the correct frequency for microwave excitation of the diamond. The frequencies are set via the USB port, and the measured values from the photodiode are displayed. The basic wiring on the senseBox breadboard is largely similar to the quenching experiment, with two differences: The resistor on the photodiode is now 330 kΩ, and there is an additional red LED. The OLED display is no longer used; instead, the PC is connected via USB.

Here is the polarity of the photodiode, BC547, and LEDs (partly from "Elektronik-Kompendium"):

Polarity

The corresponding circuit diagram is as follows:

Circuit Diagram

Functionality - Circuit Design for Excitation

The ADF4351 microwave generator is connected to the Serial Peripheral Interface (SPI) of the senseBox. The SPI pins are available at pins 11, 17, and 18 of the XBEE2 connector (see the circuit diagram and senseBox-MCU hardware documentation, Sheet 2/4, bottom). Additionally, 5V and GND are taken from the middle digital connector, and the Chip Enable (CE) output and Lock Detect input (Muxout of the ADF4351) are connected. The micro-coaxial connector, a U.FL connector on the microwave resonator on the green excitation LED of the quantum sensor, must also be connected to the ADF4351 via a high-frequency cable (see the circuit diagram).

The SPI can sometimes produce oscillations, known as "ringing," on the lines, which can prevent correct data transmission, especially if the cables are longer than about five centimeters. This can be improved by adding small resistors (e.g., 33 Ω or 47 Ω) in series with the SCK and MOSI lines. These resistors should be placed as close as possible to the outputs of the senseBox. Additionally, pull-down resistors (4.7 kΩ to 10 kΩ) can be added to these lines as close as possible to the ADF4351 to ground. Capacitors of 10–15 pF at the same location have a comparable effect.

For the calculation of the LED resistor R1 and transistor base resistor R2, refer to the "Quenching" experiment.

Due to the more complex programming, C code is used in the Arduino IDE instead of Blockly, and a Python script is used for control from the PC.

Functionality - Photodiode Current Measurement and Indicator LED

The photodiode generates a light-dependent photocurrent, which is converted into a voltage by resistor R3. This voltage is then digitized by the Analog-to-Digital Converter (ADC) of the senseBox MCU and can be read and processed by the C program. R3 can be chosen between approximately 150 kΩ and 470 kΩ; we have had good experiences with 330 kΩ.

To ensure that the photodiode responds primarily to the red fluorescence light of the micro-diamond with NV centers rather than the green light from the excitation LED, a red color filter is applied to the photodiode.

In the presence of an external magnetic field and while sweeping the frequency range from 2.67 GHz to 3.07 GHz, typical brightness dips appear in the "Fluorescence Intensity vs. Microwave Frequency" diagram. The farther apart these dips are, the stronger the external magnetic field.

Programming the senseBox: Arduino IDE Program "QOOOL_DIY_MICROWAVE.ino"

The senseBox is programmed using the Arduino IDE in C. The corresponding program is as follows:

#include <senseBoxIO.h>
#include <SPI.h>
#include <variant.h>
#include <stdlib.h>
#include <stdio.h>
#include <HSS_ADI_ADF4351.h>

HSS_ADI_ADF4351 adf4351;

void setup() {
    adf4351.begin();
    delay(10);

    pinMode(5, OUTPUT);   // Bias-LED NV sensor (green)
    digitalWrite(5, HIGH);

    pinMode(1, OUTPUT);   // GREEN status-LED
    digitalWrite(1, LOW);

    pinMode(A6, INPUT);   // ADC input

    pinMode(2, OUTPUT);   // RED status-LED
    digitalWrite(2, !adf4351.readLockDetect());

    SerialUSB.begin(115200);
    SerialUSB.setTimeout(30000);
}

void loop() {
    String strReceived = SerialUSB.readStringUntil('\r');

    if (adf4351.processVcomStr(strReceived) && ADCprocessVcomStr(strReceived))
        SerialUSB.println("Unknown: " + strReceived);

    digitalWrite(2, !adf4351.readLockDetect());
}

The proprietary ADF4351 driver HSS_ADI_ADF4351 is included in the appendix.

Here is the VCOM routine for the internal ADC of the senseBox:

bool ADCprocessVcomStr(String strRX)
{
    uint32_t ADCresult, ui32convVal;
    char* pcBuf;
    char* pcEnd;
    char outStr[18];
    if (strRX.length() < 4)
        return true;

    String strStart = strRX.substring(0, 4);

    if (strStart != "ADC?")
        return true;

    if (adf4351.readLockDetect())
    {
        strStart = strRX.substring(4, strRX.length());
        if ((strStart.length() < 1) || (strStart.length() > 8))
        {
            SerialUSB.println("ADC0000!ErrParLN");
            return false;
        }
        pcBuf = (char*)(strStart.c_str()); // get internal char array
        ui32convVal = strtoul ((const char*)pcBuf, &pcEnd, 16); // convert hex str to ui32

        if ((((uint32_t)(pcEnd-pcBuf)) < 1) || (((uint32_t)(pcEnd-pcBuf)) > 8)) // check if conversion was completed
        {
            SerialUSB.println("ADC0000!ErrParCV");
            return false;
        }

        if ((ui32convVal == 0) || (ui32convVal > 4096))
        {
            SerialUSB.println("ADC0000!ErrParVL");
            return false;
        }
        //delay(20);
        ADCresult = 0;

        for (uint16_t i = 0; i < (uint16_t)ui32convVal; i++)
        {
            //delay(2);
            ADCresult += (uint32_t)analogRead(A6);
        }
        sprintf(outStr, "ADC%04X=%08X", ui32convVal, ADCresult);

        SerialUSB.println(outStr);
    }
    else
        SerialUSB.println("ADC0000!LockFail");

    return false;
}

PC Programming: Python Script "QooolKitDIYpython.py"

from tokenize import Double
import serial
import time
import decimal
import numpy as np
import matplotlib.pyplot as plt

# Settings to control the program
serialPort = serial.Serial(
    port="COM7",
    baudrate=115200,
    bytesize=8,
    timeout=40,
    stopbits=serial.STOPBITS_ONE
)

decFreqBeg = decimal.Decimal('2670')  # Start frequency for ADF4351 in MHz
decFreqEnd = decimal.Decimal('3070')  # Stop frequency for ADF4351 in MHz
decNumSteps = decimal.Decimal('200')  # '100', '200', '400', '800'
intNumValAdc = int(100)  # Number of ADC values for averaging

# Auxiliary variables
serialString1 = ""
serialString2 = ""
serialString3 = ""

decFreqDif = decFreqEnd - decFreqBeg
decFreqPfd = decimal.Decimal('10')
decNumbMod = decimal.Decimal('4095')

adConvVal = float('NaN')
adConvArr = np.zeros((2, int(decNumSteps + 1)))

decFreqStep = decFreqDif / decNumSteps

# RFOUT/fPFD = (INT + (FRAC/MOD))

# Prepare command for ADC read
strADCread = "ADC?" + f"{intNumValAdc:04X}" + "\r"

# Select GHz output
intReg4 = 0x00832424
strReg4 = "ADF4351:" + f"{intReg4:08X}" + "\r"

serialPort.reset_output_buffer()
serialPort.reset_input_buffer()

# Send command to set new frequency
serialPort.write(bytes(strReg4, "ascii"))

# Read response
serialString1 = serialPort.readline().decode("ascii")
serialString2 = serialPort.readline().decode("ascii")
serialString3 = serialPort.readline().decode("utf_8")

print(serialString2.rstrip() + " -> " + serialString3)

# Initialize loop values
rfOut = decFreqBeg
runInt = 0

while rfOut <= decFreqEnd:

    rawSteps = rfOut / decFreqPfd

    decimal.getcontext().rounding = decimal.ROUND_DOWN
    intSteps = round(rawSteps, 0)

    decimal.getcontext().rounding = decimal.ROUND_HALF_DOWN
    fracSteps = round((rawSteps - intSteps) * decNumbMod, 0)

    # Construct register 0 value for ADF4351
    intReg0 = ((int(intSteps) << 15) | (int(fracSteps) << 3))

    strReg0 = "ADF4351:" + f"{intReg0:08X}" + "\r"

    serialPort.reset_output_buffer()
    serialPort.reset_input_buffer()

    serialPort.write(bytes(strReg0, "ascii"))

    serialString1 = serialPort.readline().decode("ascii")
    serialString2 = serialPort.readline().decode("ascii")
    serialString3 = serialPort.readline().decode("utf_8")

    try:
        if serialString3.rstrip() == "LockDet.":

            serialPort.reset_output_buffer()
            serialPort.reset_input_buffer()

            serialPort.write(bytes(strADCread, "ascii"))

            serialString1 = serialPort.readline().decode("ascii")

            adConvVal = float(int(serialString1.split("=")[1], 16))

        else:
            adConvVal = float('NaN')

    except:
        adConvVal = float('NaN')

    adConvArr[0, runInt] = float(rfOut)
    adConvArr[1, runInt] = adConvVal

    print(
        f"{adConvArr[0, runInt]:04.4f}"
        + " | "
        + f"{adConvArr[1, runInt]:08.0f}"
    )

    rfOut += decFreqStep
    runInt += 1

plt.plot(adConvArr[0, :], adConvArr[1, :], "-b")
plt.show()

Here is the output of the Python script. The green-highlighted area shows the expected measurement signal from the diamond without an externally applied magnetic field. The red-highlighted jump is an occasional undesirable error observed during the experiment.

Python Script Output

Undesirable error in the measurement

ODMR Signal

Experimental Setup

This image clearly shows where the SPI cables must be connected to the senseBox (yellow-18-CLK, brown-17-LE, green-11-DATA):

Experimental Setup 01

In our setup, we did not use resistors in the SPI lines. Instead, we connected the oscilloscope to the ADF4351 board at these lines. The input capacitance of the oscilloscope probes (12 pF) acted like a pull-down resistor, thus stabilizing the data transmission.

The "QOOOLBrick" housing from the quenching experiment was redesigned in Tinkercad, and an access point for the microwave cable was created. The housing can still be 3D-printed and shields against ambient light.

The entire experimental setup with the oscilloscope probes looks as follows:

Experimental Setup 02

APPENDIX: ADF4351 Driver

/*!
 * @file HSS_ADI_ADF4351.h
 *
 * This is part of Hahn-Schickard's library for Analog Devices ADF4351 PLL frequency synthesizer
 *
 * These PLLs use SPI to communicate. Simplex SPI requires 3 pins (SCK, MOSI, and CS).
 * Additionally, a Chip Enable digital output and a Lock Detect digital input are required.
 *
 * Hahn-Schickard invests time and resources providing this open-source code,
 * please support Hahn-Schickard by advertising our solutions.
 *
 * Written by Dr. Eng. Volker Kible for Hahn-Schickard Stuttgart, with
 * contributions from the open-source community.
 *
 * BSD license, all text above, and the splash screen header file,
 * must be included in any redistribution.
 */

#ifndef _HSS_ADI_ADF4351_H_
#define _HSS_ADI_ADF4351_H_

#include <Arduino.h>
#include <SPI.h>
#include <variant.h>

class HSS_ADI_ADF4351 {

public:
    HSS_ADI_ADF4351(
        uint8_t chipSel_pin = (28u),
        uint8_t chipEnable_pin = (4u),
        uint8_t lockDetect_pin = (3u),
        SPIClassSAMD* p_SPI = &SPI
    );

    //~HSS_ADI_ADF4351(void);

    bool begin(bool configureADF4351 = true, bool resetConfig = true, bool periphBegin = true);

    void writeChipEnable(bool bState);
    void writeChipSelectNot(bool bState);
    bool readLockDetect(void);

    void writeRegister(uint32_t reg);
    void writeConfig(uint32_t* pConfig);

    bool processVcomStr(String strRX, Stream& vCom = SerialUSB);

private:
    SPIClassSAMD* pSpi;

    uint8_t pinChipSel;
    uint8_t pinChipEna;
    uint8_t pinLockDet;

    uint32_t ui32Config[6];
};

#endif // _HSS_ADI_ADF4351_H_
/*!
 * @file HSS_ADI_ADF4351.cpp
 */

#if defined(__AVR__) || defined(ARDUINO_ARCH_RTTHREAD)
#include <avr/pgmspace.h>

#elif defined(ARDUINO_NANO33BLE) || \
      defined(ARDUINO_ARCH_MBED_RP2040) || defined(ARDUINO_ARCH_RP2040)
#include <api/deprecated-avr-comp/avr/pgmspace.h>

#elif defined(ESP8266) || defined(ESP32)
#include <pgmspace.h>

#else
#define pgm_read_byte(addr) (*(const unsigned char *)(addr))
#endif

#if !defined(__ARM_ARCH) && !defined(ENERGIA) && !defined(ESP8266) && \
    !defined(ESP32) && !defined(__arc__) && !defined(__RL78__) && \
    !defined(CH32V20x) && !defined(PICO_RISCV)
#include <util/delay.h>
#endif

#include <SPI.h>
#include <variant.h>
#include "HSS_ADI_ADF4351.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

HSS_ADI_ADF4351::HSS_ADI_ADF4351(
    uint8_t chipSel_pin,
    uint8_t chipEnable_pin,
    uint8_t lockDetect_pin,
    SPIClassSAMD* p_SPI
) {
    if (p_SPI == nullptr) {
        SPIClassSAMD adf4351spi(&sercom1, 21u, 20u, 19u,
                               SPI_PAD_0_SCK_1, SERCOM_RX_PAD_3);
        pSpi = &adf4351spi;
    } else {
        pSpi = p_SPI;
    }

    pinChipEna = chipEnable_pin;
    pinMode(pinChipEna, OUTPUT);
    writeChipEnable(LOW);

    delayMicroseconds(10);

    pinChipSel = chipSel_pin;
    pinMode(pinChipSel, OUTPUT);
    writeChipSelectNot(LOW);

    pinLockDet = lockDetect_pin;
    pinMode(pinLockDet, INPUT);
}

void HSS_ADI_ADF4351::writeChipEnable(bool bState) {
    digitalWrite(pinChipEna, bState);
}

void HSS_ADI_ADF4351::writeChipSelectNot(bool bState) {
    digitalWrite(pinChipSel, bState);
}

bool HSS_ADI_ADF4351::readLockDetect(void) {
    return digitalRead(pinLockDet);
}

void HSS_ADI_ADF4351::writeRegister(uint32_t reg) {
    uint8_t runb;
    uint8_t* transfData = (uint8_t*)&reg;

    writeChipSelectNot(LOW);
    delayMicroseconds(1);

    for (runb = 4; runb > 0; runb--)
        pSpi->transfer(transfData[runb - 1]);

    delayMicroseconds(1);
    writeChipSelectNot(HIGH);
    delayMicroseconds(5);
}