Skip to content

Commit bf1bca1

Browse files
committed
ad9740: Add pyadi-iio support
1 parent 6df270b commit bf1bca1

File tree

4 files changed

+539
-0
lines changed

4 files changed

+539
-0
lines changed

adi/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from adi.ad7746 import ad7746
4040
from adi.ad7768 import ad7768, ad7768_4
4141
from adi.ad7799 import ad7799
42+
from adi.ad9740 import ad9740
4243
from adi.ad9081 import ad9081
4344
from adi.ad9081_mc import QuadMxFE, ad9081_mc
4445
from adi.ad9083 import ad9083

adi/ad9740.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Copyright (C) 2025 Analog Devices, Inc.
2+
#
3+
# SPDX short identifier: ADIBSD
4+
5+
from decimal import Decimal
6+
7+
from adi.attribute import attribute
8+
from adi.context_manager import context_manager
9+
from adi.rx_tx import tx
10+
11+
12+
class ad9740(tx, context_manager):
13+
"""AD9740/AD9742/AD9744/AD9748 10/12/14/8-bit, 210 MSPS DAC with DDS support"""
14+
15+
_complex_data = False
16+
_device_name = "AD9740"
17+
18+
def disable_dds(self):
19+
"""Override DDS disable to use data_source attribute instead.
20+
21+
AD9740 uses data_source control, not the standard DDS raw attribute.
22+
When switching to DMA mode, set data_source to 'normal'.
23+
"""
24+
# Set data source to normal (DMA mode)
25+
if hasattr(self, 'channel') and len(self.channel) > 0:
26+
try:
27+
self.channel[0].data_source = "normal"
28+
except (AttributeError, OSError):
29+
# If data_source doesn't work, just pass
30+
# (driver might already be in correct mode)
31+
pass
32+
33+
def __init__(self, uri="", device_name=""):
34+
"""Constructor for AD9740 driver class"""
35+
36+
context_manager.__init__(self, uri, self._device_name)
37+
38+
compatible_parts = [
39+
"ad9740", # 10-bit DAC
40+
"ad9742", # 12-bit DAC
41+
"ad9744", # 14-bit DAC
42+
"ad9748", # 8-bit DAC
43+
]
44+
45+
self._ctrl = None
46+
self._txdac = None
47+
48+
if not device_name:
49+
device_name = compatible_parts[0]
50+
else:
51+
if device_name not in compatible_parts:
52+
raise Exception(
53+
f"Not a compatible device: {device_name}. Supported device names "
54+
f"are: {','.join(compatible_parts)}"
55+
)
56+
57+
# Select the device matching device_name as working device
58+
for device in self._ctx.devices:
59+
if device.name == device_name:
60+
self._ctrl = device
61+
self._txdac = device
62+
break
63+
64+
if not self._ctrl:
65+
raise Exception("Error in selecting matching device")
66+
67+
if not self._txdac:
68+
raise Exception("Error in selecting matching device")
69+
70+
self.output_bits = []
71+
self.channel = []
72+
self._tx_channel_names = []
73+
for ch in self._ctrl.channels:
74+
name = ch._id
75+
output = ch._output
76+
self.output_bits.append(ch.data_format.bits)
77+
self._tx_channel_names.append(name)
78+
self.channel.append(self._channel(self._ctrl, name, output))
79+
if output is True:
80+
setattr(self, name, self._channel(self._ctrl, name, output))
81+
82+
tx.__init__(self)
83+
84+
class _channel(attribute):
85+
"""AD9740 channel"""
86+
87+
def __init__(self, ctrl, channel_name, output):
88+
self.name = channel_name
89+
self._ctrl = ctrl
90+
self._output = output
91+
92+
@property
93+
def sample_rate(self):
94+
"""Sample rate of the DAC (from backend)
95+
Note: AD9740 driver doesn't expose sampling_frequency attribute,
96+
so we return a default value. Override if needed."""
97+
try:
98+
return self._get_iio_attr(self.name, "sampling_frequency", True)
99+
except (KeyError, OSError):
100+
# Attribute doesn't exist, return default (210 MHz from ADF4351)
101+
return 210000000
102+
103+
@sample_rate.setter
104+
def sample_rate(self, value):
105+
"""Set sample rate - may not be supported by all backends"""
106+
try:
107+
self._set_iio_attr(self.name, "sampling_frequency", True, value)
108+
except (KeyError, OSError):
109+
# Attribute doesn't exist, silently ignore
110+
pass
111+
112+
@property
113+
def raw(self):
114+
"""Get channel raw value
115+
DAC code in the range 0-16383 (14-bit)"""
116+
return self._get_iio_attr(self.name, "raw", True)
117+
118+
@raw.setter
119+
def raw(self, value):
120+
"""Set channel raw value"""
121+
self._set_iio_attr(self.name, "raw", True, str(int(value)))
122+
123+
@property
124+
def data_source(self):
125+
"""Get/Set data source: normal, dds, or ramp"""
126+
return self._get_iio_attr_str(self.name, "data_source", True)
127+
128+
@data_source.setter
129+
def data_source(self, value):
130+
"""Set data source (normal, dds, ramp)"""
131+
self._set_iio_attr(self.name, "data_source", True, value)
132+
133+
@property
134+
def data_source_available(self):
135+
"""Available data sources"""
136+
return self._get_iio_attr_str(self.name, "data_source_available", True)
137+
138+
# DDS Tone 0 controls
139+
@property
140+
def frequency0(self):
141+
"""DDS Tone 0 frequency in Hz"""
142+
return int(self._get_iio_attr(self.name, "frequency0", True))
143+
144+
@frequency0.setter
145+
def frequency0(self, value):
146+
self._set_iio_attr(self.name, "frequency0", True, str(int(value)))
147+
148+
@property
149+
def scale0(self):
150+
"""DDS Tone 0 scale (0.0 to 1.0)"""
151+
return float(self._get_iio_attr_str(self.name, "scale0", True))
152+
153+
@scale0.setter
154+
def scale0(self, value):
155+
self._set_iio_attr(self.name, "scale0", True, str(Decimal(value).real))
156+
157+
@property
158+
def phase0(self):
159+
"""DDS Tone 0 phase in degrees"""
160+
# Kernel returns phase in radians as a decimal string
161+
radians = float(self._get_iio_attr_str(self.name, "phase0", True))
162+
return radians * 180.0 / 3.14159265359 # Convert to degrees
163+
164+
@phase0.setter
165+
def phase0(self, value):
166+
"""Set phase in degrees"""
167+
# Convert degrees to radians for kernel
168+
radians = float(value) * 3.14159265359 / 180.0
169+
self._set_iio_attr(self.name, "phase0", True, f"{radians:.5f}")
170+
171+
# DDS Tone 1 controls
172+
@property
173+
def frequency1(self):
174+
"""DDS Tone 1 frequency in Hz"""
175+
return int(self._get_iio_attr(self.name, "frequency1", True))
176+
177+
@frequency1.setter
178+
def frequency1(self, value):
179+
self._set_iio_attr(self.name, "frequency1", True, str(int(value)))
180+
181+
@property
182+
def scale1(self):
183+
"""DDS Tone 1 scale (0.0 to 1.0)"""
184+
return float(self._get_iio_attr_str(self.name, "scale1", True))
185+
186+
@scale1.setter
187+
def scale1(self, value):
188+
self._set_iio_attr(self.name, "scale1", True, str(Decimal(value).real))
189+
190+
@property
191+
def phase1(self):
192+
"""DDS Tone 1 phase in degrees"""
193+
# Kernel returns phase in radians as a decimal string
194+
radians = float(self._get_iio_attr_str(self.name, "phase1", True))
195+
return radians * 180.0 / 3.14159265359 # Convert to degrees
196+
197+
@phase1.setter
198+
def phase1(self, value):
199+
"""Set phase in degrees"""
200+
# Convert degrees to radians for kernel
201+
radians = float(value) * 3.14159265359 / 180.0
202+
self._set_iio_attr(self.name, "phase1", True, f"{radians:.5f}")

examples/ad9740_dds.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Simple DDS signal generator for AD974x DACs with waveform display.
4+
5+
Usage:
6+
python3 ad9740_dds.py -f 1e6 -s 0.5
7+
python3 ad9740_dds.py --frequency 5000000 --scale 1.0
8+
python3 ad9740_dds.py -f 1e6 -s 0.5 --device ad9744
9+
"""
10+
11+
import argparse
12+
import sys
13+
import numpy as np
14+
import matplotlib.pyplot as plt
15+
import adi
16+
17+
18+
def main():
19+
parser = argparse.ArgumentParser(
20+
description='Generate DDS signal on AD974x DAC',
21+
formatter_class=argparse.RawTextHelpFormatter,
22+
epilog="""Examples:
23+
ad9740_dds.py -f 1e6 -s 0.5 # 1 MHz at 50% scale
24+
ad9740_dds.py -f 5000000 -s 1.0 # 5 MHz at full scale
25+
ad9740_dds.py -f 100e3 -s 0.25 # 100 kHz at 25% scale
26+
ad9740_dds.py -f 1e6 --device ad9744 # Use AD9744 device"""
27+
)
28+
parser.add_argument('-f', '--frequency', type=float, required=True,
29+
help='Output frequency in Hz (e.g., 1e6 for 1 MHz)')
30+
parser.add_argument('-s', '--scale', type=float, default=1.0,
31+
help='Output scale 0.0-1.0 (default: 1.0)')
32+
parser.add_argument('-u', '--uri', type=str, default='ip:10.48.65.163',
33+
help='Device URI (default: ip:10.48.65.163)')
34+
parser.add_argument('-d', '--device', type=str, default='ad9744',
35+
choices=['ad9748', 'ad9740', 'ad9742', 'ad9744'],
36+
help='DAC device name (default: ad9744)')
37+
38+
args = parser.parse_args()
39+
40+
# Validate parameters
41+
if args.frequency <= 0:
42+
print(f"Error: Frequency must be positive, got {args.frequency}")
43+
sys.exit(1)
44+
45+
if not 0.0 <= args.scale <= 1.0:
46+
print(f"Error: Scale must be between 0.0 and 1.0, got {args.scale}")
47+
sys.exit(1)
48+
49+
# Connect to DAC
50+
print(f"Connecting to {args.device} at {args.uri}...")
51+
52+
try:
53+
dac = adi.ad9740(uri=args.uri, device_name=args.device)
54+
except Exception as e:
55+
print(f"Error connecting to DAC: {e}")
56+
sys.exit(1)
57+
58+
# Configure DDS
59+
try:
60+
dac.channel[0].data_source = 'dds'
61+
dac.channel[0].frequency0 = int(args.frequency)
62+
dac.channel[0].scale0 = args.scale
63+
64+
freq_str = f"{args.frequency/1e6:.3f} MHz" if args.frequency >= 1e6 else f"{args.frequency/1e3:.3f} kHz"
65+
print(f"DDS configured: {freq_str} at scale {args.scale:.2f}")
66+
67+
except Exception as e:
68+
print(f"Error configuring DDS: {e}")
69+
sys.exit(1)
70+
71+
# Generate expected waveform for display
72+
num_cycles = 4
73+
samples_per_cycle = 100
74+
num_samples = num_cycles * samples_per_cycle
75+
76+
t = np.linspace(0, num_cycles / args.frequency, num_samples)
77+
waveform = args.scale * np.sin(2 * np.pi * args.frequency * t)
78+
79+
# Convert time to appropriate units for display
80+
if args.frequency >= 1e6:
81+
t_display = t * 1e6 # microseconds
82+
t_unit = 'us'
83+
elif args.frequency >= 1e3:
84+
t_display = t * 1e3 # milliseconds
85+
t_unit = 'ms'
86+
else:
87+
t_display = t
88+
t_unit = 's'
89+
90+
# Create plot
91+
fig, ax = plt.subplots(figsize=(10, 6))
92+
ax.plot(t_display, waveform, 'b-', linewidth=1.5)
93+
ax.axhline(y=0, color='gray', linestyle='--', linewidth=0.5)
94+
ax.set_xlabel(f'Time ({t_unit})')
95+
ax.set_ylabel('Amplitude (normalized)')
96+
ax.set_title(f'{args.device.upper()} DDS Output: {freq_str} @ {args.scale:.0%} scale')
97+
ax.set_ylim(-1.1, 1.1)
98+
ax.grid(True, alpha=0.3)
99+
100+
# Add info text
101+
info_text = f"Frequency: {freq_str}\nScale: {args.scale:.2f}\nDevice: {args.device}"
102+
ax.text(0.02, 0.98, info_text, transform=ax.transAxes, fontsize=10,
103+
verticalalignment='top', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
104+
105+
plt.tight_layout()
106+
print("Close the plot window to stop DDS and exit.")
107+
plt.show()
108+
109+
print("DDS stopped.")
110+
111+
112+
if __name__ == '__main__':
113+
main()

0 commit comments

Comments
 (0)