"""
RTIO Driver for the Analog Devices AD9834 DDS via 3-wire SPI interface.
"""
# https://www.analog.com/media/en/technical-documentation/data-sheets/AD9834.pdf
# https://www.analog.com/media/en/technical-documentation/app-notes/an-1070.pdf
from numpy import int32
from artiq.coredevice import spi2 as spi
from artiq.experiment import *
from artiq.language.core import *
from artiq.language.types import *
from artiq.language.units import *
AD9834_B28 = 1 << 13
AD9834_HLB = 1 << 12
AD9834_FSEL = 1 << 11
AD9834_PSEL = 1 << 10
AD9834_PIN_SW = 1 << 9
AD9834_RESET = 1 << 8
AD9834_SLEEP1 = 1 << 7
AD9834_SLEEP12 = 1 << 6
AD9834_OPBITEN = 1 << 5
AD9834_SIGN_PIB = 1 << 4
AD9834_DIV2 = 1 << 3
AD9834_MODE = 1 << 1
AD9834_FREQ_REG_0 = 0b01 << 14
AD9834_FREQ_REG_1 = 0b10 << 14
FREQ_REGS = [AD9834_FREQ_REG_0, AD9834_FREQ_REG_1]
AD9834_PHASE_REG = 0b11 << 14
AD9834_PHASE_REG_0 = AD9834_PHASE_REG | (0 << 13)
AD9834_PHASE_REG_1 = AD9834_PHASE_REG | (1 << 13)
PHASE_REGS = [AD9834_PHASE_REG_0, AD9834_PHASE_REG_1]
[docs]
class AD9834:
"""
AD9834 DDS driver.
This class provides control for the DDS AD9834.
The driver utilizes bit-controlled :const:`AD9834_FSEL`, :const:`AD9834_PSEL`, and
:const:`AD9834_RESET`. To pin control ``FSELECT``, ``PSELECT``, and ``RESET`` set
:const:`AD9834_PIN_SW`. The ``ctrl_reg`` attribute is used to maintain the state of
the control register, enabling persistent management of various configurations.
:param spi_device: SPI bus device name.
:param spi_freq: SPI bus clock frequency (default: 10 MHz, max: 40 MHz).
:param clk_freq: DDS clock frequency (default: 75 MHz).
:param core_device: Core device name (default: "core").
"""
kernel_invariants = {"core", "bus", "spi_freq", "clk_freq"}
def __init__(
self, dmgr, spi_device, spi_freq=10 * MHz, clk_freq=75 * MHz, core_device="core"
):
self.core = dmgr.get(core_device)
self.bus = dmgr.get(spi_device)
assert spi_freq <= 40 * MHz, "SPI frequency exceeds maximum value of 40 MHz"
self.spi_freq = spi_freq
self.clk_freq = clk_freq
self.ctrl_reg = 0x0000 # Reset control register
[docs]
@kernel
def write(self, data: TInt32):
"""
Write a 16-bit word to the AD9834.
This method sends a 16-bit data word to the AD9834 via the SPI bus. The input
data is left-shifted by 16 bits to ensure proper alignment for the SPI controller,
allowing for accurate processing of the command by the AD9834.
This method is used internally by other methods to update the control registers
and frequency settings of the AD9834. It should not be called directly unless
low-level register manipulation is required.
:param data: The 16-bit word to be sent to the AD9834.
"""
self.bus.write(data << 16)
[docs]
@kernel
def enable_reset(self):
"""
Enable the DDS reset.
This method sets :const:`AD9834_RESET`, putting the AD9834 into a reset state.
While in this state, the digital-to-analog converter (DAC) is not operational.
This method should be called during initialization or when a reset is required
to reinitialize the device and ensure proper operation.
"""
self.ctrl_reg |= AD9834_RESET
self.write(self.ctrl_reg)
[docs]
@kernel
def output_enable(self):
"""
Disable the DDS reset and start signal generation.
This method clears :const:`AD9834_RESET`, allowing the AD9834 to begin generating
signals. Once this method is called, the device will resume normal operation and
output the generated waveform.
This method should be called after configuration of the frequency and phase
settings to activate the output.
"""
self.ctrl_reg &= ~AD9834_RESET
self.write(self.ctrl_reg)
[docs]
@kernel
def init(self):
"""
Initialize the AD9834: configure the SPI bus and reset the DDS.
This method performs the necessary setup for the AD9834 device, including:
- Configuring the SPI bus parameters (clock polarity, data width, and frequency).
- Putting the AD9834 into a reset state to ensure proper initialization.
The SPI bus is configured to use 16 bits of data width with the clock frequency
provided as a parameter when creating the AD9834 instance. After configuring
the SPI bus, the method invokes :meth:`enable_reset()` to reset the AD9834.
This is an essential step to prepare the device for subsequent configuration
of frequency and phase.
This method should be called before any other operations are performed
on the AD9834 to ensure that the device is in a known state.
"""
self.bus.set_config(spi.SPI_CLK_POLARITY | spi.SPI_END, 16, self.spi_freq, 1)
self.enable_reset()
[docs]
@kernel
def set_frequency_reg_msb(self, freq_reg: TInt32, word: TInt32):
"""
Set the fourteen most significant bits MSBs of the specified frequency register.
This method updates the specified frequency register with the provided MSB value.
It configures the control register to indicate that the MSB is being set.
:param freq_reg: The frequency register to write to (0-1).
:param word: The value to be written to the fourteen MSBs of the frequency register.
The method first clears the appropriate control bits, sets :const:`AD9834_HLB` to
indicate that the MSB is being sent, and then writes the updated control register
followed by the MSB value to the specified frequency register.
"""
assert 0 <= freq_reg <= 1, "Invalid frequency register index"
self.ctrl_reg &= ~AD9834_B28
self.ctrl_reg |= AD9834_HLB
self.write(self.ctrl_reg)
self.write(FREQ_REGS[freq_reg] | (word & 0x3FFF))
[docs]
@kernel
def set_frequency_reg_lsb(self, freq_reg: TInt32, word: TInt32):
"""
Set the fourteen least significant bits LSBs of the specified frequency register.
This method updates the specified frequency register with the provided LSB value.
It configures the control register to indicate that the LSB is being set.
:param freq_reg: The frequency register to write to (0-1).
:param word: The value to be written to the fourteen LSBs of the frequency register.
The method first clears the appropriate control bits and writes the updated control
register followed by the LSB value to the specified frequency register.
"""
assert 0 <= freq_reg <= 1, "Invalid frequency register index"
self.ctrl_reg &= ~AD9834_B28
self.ctrl_reg &= ~AD9834_HLB
self.write(self.ctrl_reg)
self.write(FREQ_REGS[freq_reg] | (word & 0x3FFF))
[docs]
@kernel
def set_frequency_reg(self, freq_reg: TInt32, freq_word: TInt32):
"""
Set the frequency for the specified frequency register using a precomputed frequency word.
This writes to the 28-bit frequency register in one transfer.
:param freq_reg: The frequency register to write to (0-1).
:param freq_word: The precomputed frequency word.
"""
assert 0 <= freq_reg <= 1, "Invalid frequency register index"
self.ctrl_reg |= AD9834_B28
self.write(self.ctrl_reg)
lsb = freq_word & 0x3FFF
msb = (freq_word >> 14) & 0x3FFF
self.write(FREQ_REGS[freq_reg] | lsb)
self.write(FREQ_REGS[freq_reg] | msb)
[docs]
@portable(flags={"fast-math"})
def frequency_to_ftw(self, frequency: TFloat) -> TInt32:
"""Return the 28-bit frequency tuning word corresponding to the given
frequency.
"""
assert frequency <= 37.5 * MHz, "Frequency exceeds maximum value of 37.5 MHz"
return int((frequency * (1 << 28)) / self.clk_freq) & 0x0FFFFFFF
[docs]
@portable(flags={"fast-math"})
def turns_to_pow(self, turns: TFloat) -> TInt32:
"""Return the 12-bit phase offset word corresponding to the given phase
in turns."""
assert 0.0 <= turns <= 1.0, "Turns exceeds range 0.0 - 1.0"
return int32(round(turns * 0x1000)) & int32(0x0FFF)
[docs]
@kernel
def select_frequency_reg(self, freq_reg: TInt32):
"""
Select the active frequency register for the phase accumulator.
This method chooses between the two available frequency registers in the AD9834 to
control the frequency of the output waveform. The control register is updated
to reflect the selected frequency register.
:param freq_reg: The frequency register to write to (0-1).
"""
assert 0 <= freq_reg <= 1, "Invalid frequency register index"
if freq_reg:
self.ctrl_reg |= AD9834_FSEL
else:
self.ctrl_reg &= ~AD9834_FSEL
self.ctrl_reg &= ~AD9834_PIN_SW
self.write(self.ctrl_reg)
[docs]
@kernel
def set_phase_reg(self, phase_reg: TInt32, phase: TInt32):
"""
Set the phase for the specified phase register.
This method updates the specified phase register with the provided phase value.
:param phase_reg: The phase register to write to (0-1).
:param phase: The value to be written to the phase register.
The method masks the phase value to ensure it fits within the 12-bit limit
and writes it to the specified phase register.
"""
assert 0 <= phase_reg <= 1, "Invalid phase register index"
phase_word = phase & 0x0FFF
self.write(PHASE_REGS[phase_reg] | phase_word)
[docs]
@kernel
def select_phase_reg(self, phase_reg: TInt32):
"""
Select the active phase register for the phase accumulator.
This method chooses between the two available phase registers in the AD9834 to
control the phase of the output waveform. The control register is updated
to reflect the selected phase register.
:param phase_reg: The phase register to write to (0-1).
"""
assert 0 <= phase_reg <= 1, "Invalid phase register index"
if phase_reg:
self.ctrl_reg |= AD9834_PSEL
else:
self.ctrl_reg &= ~AD9834_PSEL
self.ctrl_reg &= ~AD9834_PIN_SW
self.write(self.ctrl_reg)
[docs]
@kernel
def sleep(self, dac_pd: bool = False, clk_dis: bool = False):
"""
Put the AD9834 into sleep mode by selectively powering down the DAC and/or disabling
the internal clock.
This method controls the sleep mode behavior of the AD9834 by setting or clearing the
corresponding bits in the control register. Two independent options can be specified:
:param dac_pd: Set to ``True`` to power down the DAC (:const:`AD9834_SLEEP12` is set).
``False`` will leave the DAC active.
:param clk_dis: Set to ``True`` to disable the internal clock (:const:`AD9834_SLEEP1` is set).
``False`` will keep the clock running.
Both options can be enabled independently, allowing the DAC and/or clock to be powered down as needed.
The method updates the control register and writes the changes to the AD9834 device.
"""
if dac_pd:
self.ctrl_reg |= AD9834_SLEEP12
else:
self.ctrl_reg &= ~AD9834_SLEEP12
if clk_dis:
self.ctrl_reg |= AD9834_SLEEP1
else:
self.ctrl_reg &= ~AD9834_SLEEP1
self.write(self.ctrl_reg)
[docs]
@kernel
def awake(self):
"""
Exit sleep mode and restore normal operation.
This method brings the AD9834 out of sleep mode by clearing any DAC power-down or
internal clock disable settings. It calls :meth:`sleep()` with no arguments,
effectively setting both ``dac_powerdown`` and ``internal_clk_disable`` to ``False``.
The device will resume generating output based on the current frequency and phase
settings.
"""
self.sleep()
[docs]
@kernel
def config_sign_bit_out(
self,
high_z: bool = False,
msb_2: bool = False,
msb: bool = False,
comp_out: bool = False,
):
"""
Configure the ``SIGN BIT OUT`` pin for various output modes.
This method sets the output mode for the ``SIGN BIT OUT`` pin of the AD9834 based on the provided flags.
The user can enable one of several modes, including high impedance, MSB/2 output, MSB output,
or comparator output. These modes are mutually exclusive, and passing ``True`` to one flag will
configure the corresponding mode, while other flags should be left as ``False``.
:param high_z: Set to ``True`` to place the ``SIGN BIT OUT`` pin in high impedance (disabled) mode.
:param msb_2: Set to ``True`` to output DAC Data MSB divided by 2 on the ``SIGN BIT OUT`` pin.
:param msb: Set to ``True`` to output DAC Data MSB on the ``SIGN BIT OUT`` pin.
:param comp_out: Set to ``True`` to output the comparator signal on the ``SIGN BIT OUT`` pin.
Only one flag should be set to ``True`` at a time. If no valid mode is selected, the ``SIGN BIT OUT``
pin will default to high impedance mode.
The method updates the control register with the appropriate configuration and writes it to the AD9834.
"""
if high_z:
self.ctrl_reg &= ~AD9834_OPBITEN
elif msb_2:
self.ctrl_reg |= AD9834_OPBITEN
self.ctrl_reg &= ~(AD9834_MODE | AD9834_SIGN_PIB | AD9834_DIV2)
elif msb:
self.ctrl_reg |= AD9834_OPBITEN | AD9834_DIV2
self.ctrl_reg &= ~(AD9834_MODE | AD9834_SIGN_PIB)
elif comp_out:
self.ctrl_reg |= AD9834_OPBITEN | AD9834_SIGN_PIB | AD9834_DIV2
self.ctrl_reg &= ~AD9834_MODE
else:
self.ctrl_reg &= ~AD9834_OPBITEN
self.write(self.ctrl_reg)
[docs]
@kernel
def set_mu(
self,
freq_word: TInt32 = 0,
phase_word: TInt32 = 0,
freq_reg: TInt32 = 0,
phase_reg: TInt32 = 0,
):
"""
Set DDS frequency and phase in machine units.
This method updates the specified frequency and phase registers with the provided
machine units, selects the corresponding registers, and enables the output.
:param freq_word: Frequency tuning word (28-bit).
:param phase_word: Phase tuning word (12-bit).
:param freq_reg: Frequency register to write to (0 or 1).
:param phase_reg: Phase register to write to (0 or 1).
"""
assert 0 <= freq_reg <= 1, "Invalid frequency register index"
assert 0 <= phase_reg <= 1, "Invalid phase register index"
self.set_frequency_reg_lsb(freq_reg, freq_word & 0x3FFF)
self.set_frequency_reg_msb(freq_reg, (freq_word >> 14) & 0x3FFF)
self.set_phase_reg(phase_reg, phase_word)
self.select_frequency_reg(freq_reg)
self.select_phase_reg(phase_reg)
self.output_enable()
[docs]
@kernel
def set(
self,
frequency: TFloat = 0.0,
phase: TFloat = 0.0,
freq_reg: TInt32 = 0,
phase_reg: TInt32 = 0,
):
"""
Set DDS frequency in Hz and phase using fractional turns.
This method converts the specified frequency and phase to their corresponding
machine units, updates the selected registers, and enables the output.
:param frequency: Frequency in Hz.
:param phase: Phase in fractional turns (e.g., 0.5 for 180 degrees).
:param freq_reg: Frequency register to write to (0 or 1).
:param phase_reg: Phase register to write to (0 or 1).
"""
assert 0 <= freq_reg <= 1, "Invalid frequency register index"
assert 0 <= phase_reg <= 1, "Invalid phase register index"
freq_word = self.frequency_to_ftw(frequency)
phase_word = self.turns_to_pow(phase)
self.set_mu(freq_word, phase_word, freq_reg, phase_reg)