The experiment below linearly maps a float between 0.0 and 1.0 to an unsigned 32-bit integer between 0 and 2**32-1. The conversion happens on a Kasli SoC, completely without RPC calls.

Beware that you should run this experiment on a real hardware device. Running with artiq/examples/no_hardware/device_db.py changes the results.

from artiq.experiment import EnvExperiment, kernel, rpc
from artiq.language.types import TInt32, TFloat
import numpy as np

@rpc
def print_binary(number, type_cast, nr_bits):
    bits = ""
    for i in range(nr_bits):
        bits = str(i % 10) + bits
    print("bits    :", bits)
    print("binary  :", f"{type_cast(number):{int(nr_bits)}b}")

@rpc
def rpc_python_benchmark(amplitude):
    r"""Linearly maps amplitude ∈ [0.0, 1.0] to an unsigned 32-bit integer {0,1,..., 2**32-1}.
    Comparison function for `kernel_hack()` with python types."""
    if amplitude < 0.0 or amplitude > 1.0:
        raise ValueError("Invalid AD9910 fractional amplitude!")
    if amplitude > 0.9999999998: # increasing the last digit from 8 to 9 fails
        amplitude = 0.9999999998
    x = round(amplitude * (1 << 32))
    print("decimal :", x)
    print_binary(x, np.uint32, 32)

@rpc
def rpc_numpy_benchmark(amplitude: TFloat):
    r"""Linearly maps amplitude ∈ [0.0, 1.0] to an unsigned 32-bit integer {0,1,..., 2**32-1}.
    Comparison function for `kernel_hack()` with numpy types."""
    if amplitude < 0.0:
        raise ValueError("Invalid AD9910 fractional amplitude!")
    elif amplitude <= 0.4999999998: # increasing the last digit from 8 to 9 fails
        x = np.int32(round(amplitude * (1 << 32)))
    elif amplitude <= 0.9999999998: # increasing the last digit from 8 to 9 fails
        x = np.int32(-round((1-amplitude) * (1 << 32))) # the minus sign is a hack
    elif amplitude <= 1.0:
        x = np.int32(-round((1-0.9999999998) * (1 << 32)))
    else:
        raise ValueError("Invalid AD9910 fractional amplitude!")
    print("decimal :", x)
    print_binary(x, np.uint32, 32)

@kernel
def kernel_hack(amplitude: TFloat) -> TInt32:
    r"""Linearly maps amplitude ∈ [0.0, 1.0] to an unsigned 32-bit integer {0,1,..., 2**32-1}.
    Hacking is necessary because the ARTIQ compiler does *not* know unsigned integers."""
    if amplitude < 0.0:
        raise ValueError("Invalid AD9910 fractional amplitude!")
    elif amplitude <= 0.4999999998: # increasing the last digit from 8 to 9 fails
        # no clue why minus sign is inverted compared to `rpc_numpy_benchmark`
        return np.int32(-round(amplitude * 2 * (1 << 31))) # (1 << 32) produces gargabe
    elif amplitude <= 0.9999999998: # increasing the last digit from 8 to 9 fails
        # no clue why minus sign is inverted compared to `rpc_numpy_benchmark`
        return np.int32(round((1-amplitude) * 2 * (1 << 31))) # (1 << 32) produces gargabe
    elif amplitude <= 1.0:
        return np.int32(round((1-0.9999999998) * 2 * (1 << 31))) # (1 << 32) produces gargabe
    else:
        raise ValueError("Invalid AD9910 fractional amplitude!")
    return np.int32(0) # prevents compiler crash

class UnsignedIntegerTest(EnvExperiment):
    """
    Do must run this with *real* hardware in `device_db.py`.
    We have tested unsigned integer hacks with
    `artiq/examples/no_hardware/device_db.py` before,
    and the results were *different* from the real hardware.
    """
    def build(self):
        self.setattr_device("core")

    @kernel
    def run(self):
        amplitude = 0.5
        print("----------- RPC PYTHON TYPES -----------")
        rpc_python_benchmark(amplitude)
        print("----------- RPC NUMPY TYPES -----------")
        rpc_numpy_benchmark(amplitude)
        print("----------- KERNEL HACK -----------")
        x = kernel_hack(amplitude)
        print("decimal :", x)
        print_binary(x, np.uint32, 32)

Why did we do all this?

The AD9910's digital ramp generator accepts unsigned 32-bit integers in these registers:

  • Digital Ramp Limit Register—Address 0x0B
  • Digital Ramp Step Size Register—Address 0x0C

See pages 28, 29 and 59 of the AD9910 datasheet.

Unfortunately, artiq.coredevice.AD9910.amplitude_to_asf (see artiq/coredevice/ad9910.py#L747) does not handle and cannot be adapted to handle unsigned integers.