From 618774151141f8d0c79c2097647a2794a2970651 Mon Sep 17 00:00:00 2001 From: Rodrigo Alencar Date: Tue, 28 Apr 2026 16:16:53 +0100 Subject: [PATCH 1/5] adi: rx_tx: be able to resolve and set iio triggers This facilitates trigger handling on devices with triggered buffer capabilities. Signed-off-by: Rodrigo Alencar --- adi/rx_tx.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/adi/rx_tx.py b/adi/rx_tx.py index 32c8dbe5b..84188d269 100644 --- a/adi/rx_tx.py +++ b/adi/rx_tx.py @@ -648,6 +648,22 @@ def __init__( def __post_init__(self): pass + def _resolve_trigger(self, trigger): + if isinstance(trigger, str): + trig = self._ctx.find_device(trigger) # iio.Trigger + if not trig: + raise ValueError(f"Trigger device '{trigger}' not found") + elif hasattr(trigger, "_ctrl"): + # wrapper class instance is passed in instead of a raw iio trigger + trig = trigger._ctrl + else: + trig = trigger + + if trig is not None and not isinstance(trig, iio.Trigger): + raise ValueError(f"'{trig}' is not an IIO Trigger") + + return trig + class rx_def_no_buff(shared_def, rx, context_manager, metaclass=ABCMeta): @property @@ -724,6 +740,20 @@ def __init__( if self.__run_rx_post_init__: self.__post_init__() + def set_rx_trigger(self, trigger): + """Set the trigger for the RX ADC. + + This interface is typically used by devices with triggered buffer + support that does not have an internal/default trigger + + Args: + trigger: + Can be Trigger device name or id, an iio.Trigger instance, or + wrapper class instance with _ctrl attribute pointing to an iio.Trigger. + Set to None to clear the trigger. + """ + self._rxadc._set_trigger(self._resolve_trigger(trigger)) + class tx_def_no_buff(shared_def, tx, context_manager, metaclass=ABCMeta): @property @@ -800,6 +830,20 @@ def __init__( if self.__run_tx_post_init__: self.__post_init__() + def set_tx_trigger(self, trigger): + """Set the trigger for the TX DAC. + + This interface is typically used by devices with triggered buffer + support that does not have an internal/default trigger + + Args: + trigger: + Can be Trigger device name or id, an iio.Trigger instance, or + wrapper class instance with _ctrl attribute pointing to an iio.Trigger. + Set to None to clear the trigger. + """ + self._txdac._set_trigger(self._resolve_trigger(trigger)) + class rx_tx_def(tx_def, rx_def): """Template metaclass for rx and tx device specific interfaces.""" From 090cc1ccbe2475fb1371bf4cc899639a6daab721 Mon Sep 17 00:00:00 2001 From: Rodrigo Alencar Date: Fri, 14 Aug 2026 11:39:23 +0100 Subject: [PATCH 2/5] adi: rx_tx: tx_core: support processed input Mirror rx output type handling when having processed or raw input. Signed-off-by: Rodrigo Alencar --- adi/rx_tx.py | 79 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/adi/rx_tx.py b/adi/rx_tx.py index 84188d269..faf30b42d 100644 --- a/adi/rx_tx.py +++ b/adi/rx_tx.py @@ -301,6 +301,7 @@ class tx_core(dds, rx_tx_common, metaclass=ABCMeta): # Set to True if complex data for TX only, overrides _complex_data _tx_complex_data = None _tx_data_type = None + _tx_input_type = "raw" _txbuf = None _output_byte_filename = "out.bin" _push_to_file = False @@ -332,6 +333,17 @@ def _complex_data(self): return super()._complex_data return self._tx_complex_data + @property + def tx_input_type(self) -> str: + """tx_input_type: Set input data type for tx()""" + return self._tx_input_type + + @tx_input_type.setter + def tx_input_type(self, value: str): + if value not in ["raw", "SI"]: + raise ValueError(f"Invalid tx_input_type: {value}. Must be raw or SI") + self._tx_input_type = value + @property def tx_cyclic_buffer(self): """tx_cyclic_buffer: Enable cyclic buffer for TX""" @@ -409,6 +421,60 @@ def tx_destroy_buffer(self): """tx_destroy_buffer: Clears TX buffer""" self._txbuf = None + def __get_tx_channel_scales(self): + tx_scale = [] + for i in self.tx_enabled_channels: + chan_name = self._tx_channel_names[i] + v = self._txdac.find_channel(chan_name, True) + if "scale" in v.attrs: + scale = self._get_iio_attr(chan_name, "scale", True, self._txdac) + else: + scale = 1.0 + tx_scale.append(scale) + return tx_scale + + def __get_tx_channel_offsets(self): + tx_offset = [] + for i in self.tx_enabled_channels: + chan_name = self._tx_channel_names[i] + v = self._txdac.find_channel(chan_name, True) + if "offset" in v.attrs: + offset = self._get_iio_attr(chan_name, "offset", True, self._txdac) + else: + offset = 0.0 + tx_offset.append(offset) + return tx_offset + + def __tx_raw_data(self, data_np): + if self._num_tx_channels_enabled == 1: + data_np = [data_np] + + if len(data_np) != self._num_tx_channels_enabled: + raise Exception("Not enough data provided for channel mapping") + + if self._tx_input_type == "SI": + tx_scale = self.__get_tx_channel_scales() + tx_offset = self.__get_tx_channel_offsets() + raw_data_np = [] + + for i, ch in enumerate(data_np): + chan_name = self._tx_channel_names[self.tx_enabled_channels[i]] + df = self._txdac.find_channel(chan_name, True).data_format + if df.is_signed: + lo, hi = -(1 << (df.bits - 1)), (1 << (df.bits - 1)) - 1 + else: + lo, hi = 0, (1 << df.bits) - 1 + raw = np.clip( + np.asarray(ch) / tx_scale[i] - tx_offset[i], lo, hi + ).astype(self._tx_data_type) + raw_data_np.append(raw << df.shift) + elif self._tx_input_type == "raw": + raw_data_np = data_np + else: + raise Exception("invalid _tx_input_type") + + return raw_data_np + def tx(self, data_np=None): """Transmit data to hardware buffers for each channel index in tx_enabled_channels. @@ -447,13 +513,8 @@ def tx(self, data_np=None): "To push more data the tx buffer must be destroyed first." ) + data_np = self.__tx_raw_data(data_np) if self._complex_data: - if self._num_tx_channels_enabled == 1: - data_np = [data_np] - - if len(data_np) != self._num_tx_channels_enabled: - raise Exception("Not enough data provided for channel mapping") - indx = 0 stride = self._num_tx_channels_enabled * 2 data = np.empty(stride * len(data_np[0]), dtype=self._tx_data_type) @@ -464,12 +525,6 @@ def tx(self, data_np=None): data[indx + 1 :: stride] = q.astype(self._tx_data_type) indx = indx + 2 else: - if self._num_tx_channels_enabled == 1: - data_np = [data_np] - - if len(data_np) != self._num_tx_channels_enabled: - raise Exception("Not enough data provided for channel mapping") - indx = 0 stride = self._num_tx_channels_enabled data = np.empty(stride * len(data_np[0]), dtype=self._tx_data_type) From 8eb191f93624923064af36b9cd6f6ebdbbfb6fc4 Mon Sep 17 00:00:00 2001 From: Rodrigo Alencar Date: Tue, 28 Apr 2026 16:21:55 +0100 Subject: [PATCH 3/5] adi: trigger: create hrtimer and sysfs trigger classes Create classes for basic triggers used with triggered buffer devices. Signed-off-by: Rodrigo Alencar --- adi/__init__.py | 1 + adi/trigger.py | 99 ++++++++++++++++++++++++++++++ doc/source/devices/adi.trigger.rst | 7 +++ doc/source/devices/index.rst | 1 + test/test_trigger.py | 97 +++++++++++++++++++++++++++++ 5 files changed, 205 insertions(+) create mode 100644 adi/trigger.py create mode 100644 doc/source/devices/adi.trigger.rst create mode 100644 test/test_trigger.py diff --git a/adi/__init__.py b/adi/__init__.py index 8afe00466..21d37b52a 100644 --- a/adi/__init__.py +++ b/adi/__init__.py @@ -149,6 +149,7 @@ from adi.tdd import tdd from adi.tddn import tddn from adi.tmc5240 import tmc5240 +from adi.trigger import hrtimer_trig, sysfs_trig try: from adi.jesd import jesd diff --git a/adi/trigger.py b/adi/trigger.py new file mode 100644 index 000000000..710735e8e --- /dev/null +++ b/adi/trigger.py @@ -0,0 +1,99 @@ +# Copyright (C) 2026 Analog Devices, Inc. +# +# SPDX short identifier: ADIBSD + +import iio + +from adi.attribute import attribute +from adi.context_manager import context_manager + + +class hrtimer_trig(context_manager, attribute): + """High-Resolution Timer Trigger + + This class provides an interface for controlling an IIO hrtimer trigger. + The trigger must be created first: + + * Ensure the ``iio-trig-hrtimer`` kernel module is loaded or built-in into the kernel + * Ensure configfs is mounted + * Create the a new trigger with: + * ``mkdir /config/iio/triggers/hrtimer/mytrig`` + * Restart iiod service if relying on a remote iio context + """ + + def __init__(self, uri="", trig_name="trigger0"): + """Initialize the hrtimer trigger + + Args: + uri: URI string for connecting to the hardware + trig_name: Trigger id or name. Default: trigger0 + """ + context_manager.__init__(self, uri) + + self._ctrl = self._ctx.find_device(trig_name) # type: iio.Trigger + if not self._ctrl: + raise Exception(f"Trigger '{trig_name}' not found") + + if not isinstance(self._ctrl, iio.Trigger): + raise ValueError(f"'{self._ctrl.name}' is not an IIO Trigger") + + if "sampling_frequency" not in self._ctrl.attrs: + raise Exception(f"Trigger '{self._ctrl.name}' is not a hrtimer trigger") + + @property + def sampling_frequency(self): + """Get/Set sampling frequency of the trigger""" + return self._get_iio_dev_attr("sampling_frequency", self._ctrl) + + @sampling_frequency.setter + def sampling_frequency(self, value): + self._set_iio_dev_attr("sampling_frequency", value, self._ctrl) + + @property + def name(self): + """Get the name of the trigger""" + return self._ctrl.name + + +class sysfs_trig(context_manager, attribute): + """Sysfs Trigger + + This class provides an interface for controlling an IIO sysfs trigger. + The trigger must be created first: + + * Ensure the ``iio-trig-sysfs`` kernel module is loaded or built-in into the kernel + * Create the a new trigger with: + * ``echo 0 > /sys/bus/iio/devices/iio_sysfs_trigger/add_trigger`` + * Restart iiod service if relying on a remote iio context + """ + + def __init__(self, uri="", trig_name="sysfstrig0"): + """Initialize the sysfs trigger + + Args: + uri: URI string for connecting to the hardware + trig_id: Trigger id or name. Default: sysfstrig0 + """ + context_manager.__init__(self, uri) + + self._ctrl = self._ctx.find_device(trig_name) + if not self._ctrl: + raise Exception(f"Trigger '{trig_name}' not found") + + if not isinstance(self._ctrl, iio.Trigger): + raise ValueError(f"'{self._ctrl.name}' is not an IIO Trigger") + + if ( + not self._ctrl.name.startswith("sysfstrig") + or "trigger_now" not in self._ctrl.attrs + ): + raise Exception(f"Trigger '{self._ctrl.name}' is not a sysfs trigger") + + @property + def name(self): + """Get the name of the trigger""" + return self._ctrl.name + + def trigger_now(self): + """Trigger an immediate event""" + self._set_iio_dev_attr("trigger_now", 1, self._ctrl) diff --git a/doc/source/devices/adi.trigger.rst b/doc/source/devices/adi.trigger.rst new file mode 100644 index 000000000..e7bd56bf6 --- /dev/null +++ b/doc/source/devices/adi.trigger.rst @@ -0,0 +1,7 @@ +Triggers +================== + +.. automodule:: adi.trigger + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/source/devices/index.rst b/doc/source/devices/index.rst index 81dba64cf..478d0885b 100644 --- a/doc/source/devices/index.rst +++ b/doc/source/devices/index.rst @@ -157,6 +157,7 @@ Supported Devices adi.one_bit_adc_dac adi.tdd adi.tddn + adi.trigger ----- diff --git a/test/test_trigger.py b/test/test_trigger.py new file mode 100644 index 000000000..6afdc53ff --- /dev/null +++ b/test/test_trigger.py @@ -0,0 +1,97 @@ +"""Unit tests for adi.trigger (no hardware / no iio-emu required).""" + +from unittest.mock import MagicMock + +import iio +import pytest + +import adi + + +def _install_trigger_context(monkeypatch, trigger): + """Patch context_manager.__init__ to inject a context returning `trigger`.""" + + class FakeContext: + def find_device(self, name): + return trigger if trigger is not None and name == trigger.name else None + + context = FakeContext() + + def init_context(self, uri="", _device_name=""): + self._ctx = context + self.uri = uri + + monkeypatch.setattr("adi.context_manager.context_manager.__init__", init_context) + + +def _fake_trigger(name, attrs): + """Build a fake iio.Trigger-like device.""" + trigger = MagicMock(spec=iio.Trigger) + trigger.name = name + trigger.attrs = {a: MagicMock() for a in attrs} + return trigger + + +def test_hrtimer_trig_reads_and_writes_sampling_frequency(monkeypatch): + trig = _fake_trigger("trigger0", ["sampling_frequency"]) + trig.attrs["sampling_frequency"].value = "1000" + _install_trigger_context(monkeypatch, trig) + + dev = adi.trigger.hrtimer_trig() + assert dev.name == "trigger0" + assert dev.sampling_frequency == 1000 + + dev.sampling_frequency = 2500 + assert trig.attrs["sampling_frequency"].value == "2500" + + +def test_hrtimer_trig_rejects_missing_device(monkeypatch): + _install_trigger_context(monkeypatch, None) + with pytest.raises(Exception, match="not found"): + adi.trigger.hrtimer_trig(trig_name="missing") + + +def test_hrtimer_trig_rejects_non_trigger_device(monkeypatch): + non_trigger = MagicMock() + non_trigger.name = "not-a-trigger" + _install_trigger_context(monkeypatch, non_trigger) + with pytest.raises(ValueError): + adi.trigger.hrtimer_trig(trig_name="not-a-trigger") + + +def test_hrtimer_trig_rejects_trigger_without_sampling_frequency(monkeypatch): + bogus = _fake_trigger("trigger0", ["trigger_now"]) + _install_trigger_context(monkeypatch, bogus) + with pytest.raises(Exception, match="not a hrtimer trigger"): + adi.trigger.hrtimer_trig() + + +def test_sysfs_trig_trigger_now_writes_one(monkeypatch): + trig = _fake_trigger("sysfstrig0", ["trigger_now"]) + _install_trigger_context(monkeypatch, trig) + + dev = adi.trigger.sysfs_trig() + assert dev.name == "sysfstrig0" + + dev.trigger_now() + assert trig.attrs["trigger_now"].value == "1" + + +def test_sysfs_trig_rejects_missing_device(monkeypatch): + _install_trigger_context(monkeypatch, None) + with pytest.raises(Exception, match="not found"): + adi.trigger.sysfs_trig(trig_name="nope") + + +def test_sysfs_trig_rejects_missing_trigger_now_attr(monkeypatch): + trig = _fake_trigger("sysfstrig0", []) + _install_trigger_context(monkeypatch, trig) + with pytest.raises(Exception, match="not a sysfs trigger"): + adi.trigger.sysfs_trig() + + +def test_sysfs_trig_rejects_wrong_name_prefix(monkeypatch): + trig = _fake_trigger("trigger0", ["trigger_now"]) + _install_trigger_context(monkeypatch, trig) + with pytest.raises(Exception, match="not a sysfs trigger"): + adi.trigger.sysfs_trig(trig_name="trigger0") From 7d9be8465cfeccbb1f0d24fff2d2cd05178ca315 Mon Sep 17 00:00:00 2001 From: Rodrigo Alencar Date: Tue, 28 Apr 2026 16:31:52 +0100 Subject: [PATCH 4/5] adi: ad5686: extend device support and features - Add new compatible parts: ad5313r, ad5316r, ad5317r, ad5338r ad5674, ad5675, ad5679, ad5686, ad5687, ad5687r, ad5689, ad5689r ad5697r - Add triggered buffer support tx_chan_comp inheritance and tx_voltage helper. - Add gain control with scale_available attribute Signed-off-by: Rodrigo Alencar --- adi/ad5686.py | 106 ++++++++++++++++++++++++++++-------- supported_parts.md | 11 ++++ test/emu/devices/ad5686.xml | 16 ++++-- test/test_ad5686.py | 92 +++++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+), 27 deletions(-) create mode 100644 test/test_ad5686.py diff --git a/adi/ad5686.py b/adi/ad5686.py index 4415547c2..e9bf7a55b 100644 --- a/adi/ad5686.py +++ b/adi/ad5686.py @@ -2,10 +2,11 @@ # # SPDX short identifier: ADIBSD -import numpy as np +from decimal import Decimal +from enum import Enum from adi.attribute import attribute -from adi.device_base import tx_chan_comp_no_buff +from adi.device_base import tx_chan_comp class ad5686_channel(attribute): @@ -14,15 +15,17 @@ class ad5686_channel(attribute): def __init__(self, ctrl, channel_name): self.name = channel_name self._ctrl = ctrl + self._format = self._ctrl.find_channel(channel_name, True).data_format @property def raw(self): """AD5686 channel raw value""" - return self._get_iio_attr(self.name, "raw", True, self._ctrl) + return int(self._get_iio_attr_str(self.name, "raw", True, self._ctrl)) @raw.setter def raw(self, value): - self._set_iio_attr(self.name, "raw", True, str(int(value))) + value = max(0, min(int(value), (2 ** self._format.bits) - 1)) + self._set_iio_attr(self.name, "raw", True, value) @property def powerdown(self): @@ -35,55 +38,85 @@ def powerdown(self, val): self._set_iio_attr(self.name, "powerdown", True, val) @property - def powerdown_mode(self): + def powerdown_mode(self) -> "ad5686.powerdown_mode": """AD5686 channel powerdown mode value""" - return self._get_iio_attr_str(self.name, "powerdown_mode", True) + return ad5686.powerdown_mode( + self._get_iio_attr_str(self.name, "powerdown_mode", True) + ) @powerdown_mode.setter - def powerdown_mode(self, val): + def powerdown_mode(self, mode): """AD5686 channel powerdown value""" - self._set_iio_attr_str(self.name, "powerdown_mode", True, val) + value = mode.value if isinstance(mode, ad5686.powerdown_mode) else mode + self._set_iio_attr(self.name, "powerdown_mode", True, value) @property def powerdown_mode_available(self): - """Provides all available powerdown mode settings for the AD5686""" - return self._get_iio_attr_str(self.name, "powerdown_mode_available", True) + """AD5686 channel available powerdown modes""" + modes_str = self._get_iio_attr_str(self.name, "powerdown_mode_available", True) + return [ad5686.powerdown_mode(m) for m in modes_str.split()] @property - def scale(self): + def scale(self) -> Decimal: """AD5686 channel scale(gain)""" - return self._get_iio_attr(self.name, "scale", True) + return Decimal(self._get_iio_attr_str(self.name, "scale", True)) - def to_raw(self, val): - """Converts raw value to SI""" - return int(1000.0 * val / self.scale) + @scale.setter + def scale(self, value): + self._set_iio_attr(self.name, "scale", True, value) @property - def volts(self): - """AD5686 channel value in volts""" - return self.raw * self.scale + def scale_available(self): + """AD5686 channel available scale values""" + scale_str = self._get_iio_attr_str(self.name, "scale_available", True) + return [Decimal(s) for s in scale_str.split()] - @volts.setter - def volts(self, val): - """AD5686 channel value in volts""" - self.raw = self.to_raw(val) + @property + def voltage(self) -> float: + """AD5686 channel value in Volts""" + return float(self.raw * self.scale / 1000) + + @voltage.setter + def voltage(self, val: float): + """AD5686 channel value in Volts""" + self.raw = int(1000 * Decimal(val) / self.scale) -class ad5686(tx_chan_comp_no_buff): +class ad5686(tx_chan_comp): """ AD5686 DAC """ + class powerdown_mode(Enum): + """AD5686 Powerdown Mode Enumeration""" + + PULLDOWN_1K = "1kohm_to_gnd" + PULLDOWN_100K = "100kohm_to_gnd" + TRISTATE = "three_state" + + class gain(Enum): + """AD5686 Gain Mode Enumeration""" + + NORMAL = 1 + DOUBLE = 2 + compatible_parts = [ "ad5686", "ad5310r", "ad5311r", + "ad5313r", + "ad5316r", + "ad5317r", + "ad5338r", "ad5671r", "ad5672r", "ad5673r", + "ad5674", "ad5674r", + "ad5675", "ad5675r", "ad5676", "ad5676r", "ad5677r", + "ad5679", "ad5679r", "ad5681r", "ad5682r", @@ -93,6 +126,10 @@ class ad5686(tx_chan_comp_no_buff): "ad5684r", "ad5685r", "ad5686r", + "ad5687", + "ad5687r", + "ad5689", + "ad5689r", "ad5691r", "ad5692r", "ad5693", @@ -102,7 +139,30 @@ class ad5686(tx_chan_comp_no_buff): "ad5695r", "ad5696", "ad5696r", + "ad5697r", ] _complex_data = False _channel_def = ad5686_channel _device_name = "" + + def __init__(self, uri="", device_name="", device_index=0, trigger=None): + super().__init__(uri, device_name, device_index) + self._scales = self.channel[0].scale_available + if trigger: + self.set_tx_trigger(trigger) + + def set_gain(self, value: "ad5686.gain"): + """Set the DAC output gain. + + Args: + value: + :class:`ad5686.gain` member selecting the output gain + (``NORMAL`` or ``DOUBLE``). Only supported on devices that + expose two entries in ``scale_available``. + """ + if len(self._scales) != 2: + raise ValueError("Cannot set gain on this device") + + self.channel[0].scale = ( + self._scales[1] if value == ad5686.gain.DOUBLE else self._scales[0] + ) diff --git a/supported_parts.md b/supported_parts.md index a2e8e9bc0..7ca4d795f 100644 --- a/supported_parts.md +++ b/supported_parts.md @@ -52,6 +52,10 @@ - AD514x (AD5141, AD5142, AD5142A, AD5143, AD5144) - AD5310R - AD5311R +- AD5313R +- AD5316R +- AD5317R +- AD5338R - AD552XR (AD5529R) - AD5592R - AD5593R @@ -59,11 +63,14 @@ - AD5671R - AD5672R - AD5673R +- AD5674 - AD5674R +- AD5675 - AD5675R - AD5676 - AD5676R - AD5677R +- AD5679 - AD5679R - AD5681R - AD5682R @@ -75,6 +82,9 @@ - AD5686 - AD5686R - AD5687 +- AD5687R +- AD5689 +- AD5689R - AD5691R - AD5692R - AD5693 @@ -84,6 +94,7 @@ - AD5695R - AD5696 - AD5696R +- AD5697R - AD5706R - AD5710R - AD5760 diff --git a/test/emu/devices/ad5686.xml b/test/emu/devices/ad5686.xml index 86ba1e8cc..f32b345cd 100644 --- a/test/emu/devices/ad5686.xml +++ b/test/emu/devices/ad5686.xml @@ -20,29 +20,37 @@ + - + + + - + + + - + + + - + + diff --git a/test/test_ad5686.py b/test/test_ad5686.py new file mode 100644 index 000000000..82f1d5207 --- /dev/null +++ b/test/test_ad5686.py @@ -0,0 +1,92 @@ +from decimal import Decimal + +import pytest + +import adi + +hardware = "ad5686" +classname = "adi.ad5686" + + +######################################### +@pytest.mark.iio_hardware(hardware) +@pytest.mark.parametrize("classname", [(classname)]) +@pytest.mark.parametrize("channel", ["voltage0", "voltage1", "voltage2", "voltage3"]) +@pytest.mark.parametrize( + "attr, start, stop, step, tol, repeats", [("raw", 0, 65535, 10000, 0, 3)], +) +def test_ad5686_raw_attr( + test_attribute_single_value, + iio_uri, + classname, + channel, + attr, + start, + stop, + step, + tol, + repeats, +): + test_attribute_single_value( + iio_uri, classname, attr, start, stop, step, tol, repeats, channel + ) + + +######################################### +@pytest.mark.iio_hardware(hardware) +@pytest.mark.parametrize("channel", [0, 1, 2, 3]) +def test_ad5686_powerdown_mode_roundtrip(iio_uri, channel): + dev = adi.ad5686(iio_uri) + ch = dev.channel[channel] + for mode in adi.ad5686.powerdown_mode: + ch.powerdown_mode = mode + assert ch.powerdown_mode is mode + + +######################################### +@pytest.mark.iio_hardware(hardware) +@pytest.mark.parametrize("channel", [0, 1, 2, 3]) +def test_ad5686_powerdown_toggle(iio_uri, channel): + dev = adi.ad5686(iio_uri) + ch = dev.channel[channel] + ch.powerdown = 1 + assert ch.powerdown == 1 + ch.powerdown = 0 + assert ch.powerdown == 0 + + +######################################### +@pytest.mark.iio_hardware(hardware) +def test_ad5686_channel_voltage_roundtrip(iio_uri): + dev = adi.ad5686(iio_uri) + scale = Decimal(dev.channel[0].scale) + + for target in (0.5, 1.0, 1.5): + dev.channel[0].voltage = target + expected_raw = int(1000 * Decimal(target) / scale) + assert dev.channel[0].raw == expected_raw + assert dev.channel[0].voltage == pytest.approx(target, abs=float(scale) / 1000) + + +######################################### +@pytest.mark.iio_hardware(hardware) +def test_ad5686_scale_available_and_set_gain(iio_uri): + dev = adi.ad5686(iio_uri) + scales = dev.channel[0].scale_available + assert len(scales) == 2 + assert scales[0] < scales[1] + + dev.set_gain(adi.ad5686.gain.DOUBLE) + assert Decimal(dev.channel[0].scale) == scales[1] + + dev.set_gain(adi.ad5686.gain.NORMAL) + assert Decimal(dev.channel[0].scale) == scales[0] + + +######################################### +@pytest.mark.iio_hardware(hardware) +def test_ad5686_set_gain_rejects_single_scale_device(iio_uri): + dev = adi.ad5686(iio_uri) + dev._scales = dev._scales[:1] + with pytest.raises(ValueError): + dev.set_gain(adi.ad5686.gain.DOUBLE) From 34f3a30956504a82fc19d6f72b5776c296acdd30 Mon Sep 17 00:00:00 2001 From: Rodrigo Alencar Date: Thu, 6 Aug 2026 14:51:03 +0100 Subject: [PATCH 5/5] examples: add ad5686r notebook example Add jupyter notebook example that covers voltage configuration, gain control, and buffered output with a hrtimer trigger. Signed-off-by: Rodrigo Alencar --- examples/ad5686r_example.ipynb | 193 +++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 examples/ad5686r_example.ipynb diff --git a/examples/ad5686r_example.ipynb b/examples/ad5686r_example.ipynb new file mode 100644 index 000000000..0b53a0615 --- /dev/null +++ b/examples/ad5686r_example.ipynb @@ -0,0 +1,193 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "bf0b3a67", + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright (C) 2026 Analog Devices, Inc.\n", + "#\n", + "# SPDX short identifier: ADIBSD\n", + "\n", + "from adi.ad5686 import ad5686\n", + "\n", + "nanodac = ad5686(\"ip:192.168.101.2\", device_name=\"ad5686r\")" + ] + }, + { + "cell_type": "markdown", + "id": "919d7695", + "metadata": {}, + "source": [ + "#### Channel Voltage" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb644025", + "metadata": {}, + "outputs": [], + "source": [ + "nanodac.channel[0].voltage = 2.5\n", + "nanodac.channel[1].voltage = 2\n", + "nanodac.channel[2].voltage = 1.5\n", + "nanodac.channel[3].voltage = 1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b3257789", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Channel voltage0: 2.49996181002 V (65535)\n", + "Channel voltage1: 1.999969448016 V (52428)\n", + "Channel voltage2: 1.499977086012 V (39321)\n", + "Channel voltage3: 0.999984724008 V (26214)\n" + ] + } + ], + "source": [ + "for ch in nanodac.channel:\n", + " print(f\"Channel {ch.name}: {ch.voltage} V ({ch.raw})\")" + ] + }, + { + "cell_type": "markdown", + "id": "0e02e695", + "metadata": {}, + "source": [ + "#### Enable 2x Gain" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f0bbc9b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Channel voltage0: 4.999923685575 V (65535)\n", + "Channel voltage1: 3.99993894846 V (52428)\n", + "Channel voltage2: 2.999954211345 V (39321)\n", + "Channel voltage3: 1.99996947423 V (26214)\n" + ] + } + ], + "source": [ + "nanodac.set_gain(ad5686.gain.DOUBLE)\n", + "\n", + "for ch in nanodac.channel:\n", + " print(f\"Channel {ch.name}: {ch.voltage} V ({ch.raw})\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0abe7f23", + "metadata": {}, + "outputs": [], + "source": [ + "nanodac.set_gain(ad5686.gain.NORMAL)" + ] + }, + { + "cell_type": "markdown", + "id": "5ea4814e", + "metadata": {}, + "source": [ + "#### Buffered Output" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be7fd2e4", + "metadata": {}, + "outputs": [], + "source": [ + "from adi.trigger import hrtimer_trig\n", + "import numpy as np\n", + "\n", + "trig0 = hrtimer_trig(uri=nanodac._ctx.attrs[\"uri\"])\n", + "trig0.sampling_frequency = 1000\n", + "\n", + "nanodac.set_tx_trigger(trig0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25ecb589", + "metadata": {}, + "outputs": [], + "source": [ + "# processed (scaled) values are in mV\n", + "voltage0_mV = np.linspace(0, 2500, num=30)\n", + "voltage1_mV = np.linspace(2500, 0, num=30)\n", + "\n", + "nanodac.tx_enabled_channels = [0, 1]\n", + "nanodac.tx_input_type = \"SI\"\n", + "nanodac.tx([voltage0_mV, voltage1_mV])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e66755a", + "metadata": {}, + "outputs": [], + "source": [ + "# Plot of Expected Output:\n", + "import pandas as pd\n", + "import plotly.express as px\n", + "\n", + "num_samples = len(voltage0_mV)\n", + "time = np.arange(num_samples) / trig0.sampling_frequency\n", + "\n", + "fig = px.line(\n", + " pd.DataFrame({\n", + " 'Time (s)': time,\n", + " 'Voltage 0': voltage0_mV,\n", + " 'Voltage 1': voltage1_mV\n", + " }),\n", + " x='Time (s)',\n", + " y=['Voltage 0', 'Voltage 1']\n", + ")\n", + "fig.update_traces(line_shape=\"hv\")\n", + "fig.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv (3.12.3)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}