An AD9913 DDS Signal Source.
Features
Agile frequency source to ∼100 MHz.
0.058 Hz frequency resolution, 0.022° phase tuning resolution.
Programmable modulus for exact frequency signal generation.
Low power (∼ 100 mW).
Phase noise ∼ –130 dBc at 1 kHz (10 MHz carrier with external AD9552 based 250 MHz reference)
Linear frequency sweeping, 8 frequency and phase offset profiles.
Optional anti-alias filter.
Optional output amplifier.
Design files are available at https://gitlab.com/dyadic-groups/rfblocks-hardware-modules/dds_2 (licensed under CERN-OHL-S v2)
Typical Performance Characteristics
A number of different board variants are referred to in this section. See Table 2 for a list of board variants currently recognized by the ad9913 class. (The ad9913.board_models class method returns a list of board variant models.)
Phase noise
Phase noise is measured using the 'phase detector' method as described here: Phase Noise Measurement with the HP11729C. The measurements were taken using an AD9913 module with a single anti-aliased output. The output level was maintained at approximately -14 dBm for all measurements.
Output response
The frequency response of the board output is determined primarily by a
combination of the DAC output response and the anti-alias filter. The
amplitude of DAC output follows the usual sinc
response above about 60
MHz with the anti-alias filter adding some rolloff above about 70 MHz and
then dropping off sharply at 100 MHz. The 27dB amplifer board variant
also shows increased rolloff above about 50 MHz due to the limited
response of the OPA847 at that gain.
Note that the response shown in Figure 3 is to 100 MHz (rather than 110 MHz as with the other response curves) in order to eliminate the effect of aliases.
Board Connector Configuration
Board Pin |
Type |
Description |
---|---|---|
|
Input/Output |
Serial control data |
|
Input |
Serial control data clock |
|
Input |
Chip select signal. Active low |
|
Input |
I/O Update. Transfer serial data to |
internal registers. |
||
|
Input |
Profile select pin 0 |
|
Input |
Profile select pin 1 |
|
Input |
Profile select pin 2 |
|
Input |
Active high. This should be pulled high |
for normal operation. |
||
|
Input |
Active high. Note that this pin is pulled |
low on the board. Set this pin to high |
||
for normal operation. |
||
|
Power Input |
Main 3.3V DC power input |
|
Power Input |
Output amplifier 6-12V DC power input |
|
Power Input |
Module ground |
|
Signal Input |
Female MMCX (or optionally, SMA connector) |
for external clock generator reference |
||
signal input. |
||
|
Signal Output |
Female MMCX (or optionally, SMA connector) |
for direct DDS signal output. |
||
|
Signal Output |
Female MMCX for unamplified but anti-alias filtered |
DDS signal output. |
||
|
Signal Output |
Female MMCX (or optionally, SMA connector) for |
anti-alias filtered and amplifier DDS signal output. |
Hardware Configurations
A number of board modifications are possible in order to change the hardware configuration. These are:
Table 2 lists some of the possible board variants. The board variants
described here have calibration data included in the rfblocks Python
ad9913
class.
Board Variant |
Description |
---|---|
0dB |
Single output on the |
Anti-alias filtered but output amplifer not populated. |
|
14dB |
Single output on the |
Anti-alias filtered and output amplifer configured |
|
for 14 dB overall gain. Note that this configuration |
|
is not recommended since gains less than about 15 dB may |
|
cause excessive peaking in the amplifier frequency |
|
response and increases the possibility of sustained |
|
oscillation. |
|
20dB |
Single output on the |
Anti-alias filtered and output amplifier configured |
|
for 20 dB overall gain. |
|
20dB-RF |
Split output with the unfiltered and unamplified DDS DAC |
output signal available on the |
|
connector. An amplified and anti-aliased signal is |
|
available on the |
|
amplifier is configured for 20 dB overall gain. |
|
20dB-DC |
DAC output is DC coupled. Single output on the |
MMCX connector. Anti-alias filtered and output amplifier |
|
configured for 20 dB overall gain. |
|
27dB-RF |
Split output with the unfiltered and unamplified DDS DAC |
output signal available on the |
|
connector. An amplified and anti-aliased signal is |
|
available on the |
|
amplifier is configured for 20 dB overall gain. |
Space requirements within the standard prototyping enclosures make it
more convenient for the coaxial connection configuration to be MMCX
connectors on all RF inputs and outputs (Refclk
, Unfiltered
,
Filtered
, and Sig Out
). These are all low frequency connections and
hence there are no performance penalties implied by doing this.
Applications Information
Figure 6 illustrates a minimal setup for controlling the DDS board. The board is connected to the atmega USB controller which is running the ECApp embedded control firmware. (The Pi Pico controller can also be used.) DDS board pins are described in Table 1.
The configuration in Figure 6 makes use of the Clock Generator to provide the DDS reference signal. This could be replaced either with an HMC833 synthesizer module or with a general purpose signal generator.
The rfblocks Python package provides the AD9913Controller class which is
used to control the DDS board via the ad9913
device class. If the Clock
Generator board is used as the DDS reference source then the
AD9552Controller class is used to control that. The following code
snippet shows an example. The board variant used in this example has the
output amplifier configured with 20dB of overall gain. See Table 2 for a
list of board variants currently recognized by the ad9913
class. (The
ad9913.board_models class method returns a list of board variant models.)
> python Python 3.8.5 (default, Sep 9 2020, 11:44:13) [Clang 10.0.1 (clang-1001.0.46.4)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> from rfblocks import ( ... ad9913, AD9913Controller, ad9552, AD9552Controller, ... create_serial ... ) >>> ser_device = '/dev/tty.usbmodem14101' >>> ser = create_serial(ser_device) >>> clk = ad9552('C7', 'B4', None, 10.0, refselect='B5') >>> clk_ctl = AD9552Controller('ClkControl', clk) >>> clk_ctl.initialize(ser) True >>> clk_ctl.channels['1'].state = ad9552.OutputState.ACTIVE >>> clk_ctl.configure(ser) True >>> dds = ad9913('C5', 'B7', 'C4', board_model='20dB') >>> dds_ctl = AD9913Controller('DDS_1', dds) >>> dds_ctl.initialize(ser) >>> dds_ctl.freq = 25.0 >>> dds_ctl.level = 256 >>> dds_ctl.state = ad9913.POWER_UP >>> dds_ctl.configure(ser)
The code does the following:
Import the rfblocks AD9913Controller and
ad9913
classes. In this example the AD9552Controller and ad9552 classes are also required for the control of the DDS clock reference.Open the USB/RS-232 serial device which connects the host computer to the
ECApp
control firmware running on the microcontroller board.Setup and initialize the DDS clock reference. The default output frequency of 250 MHz and output mode of LVPECL are correct for this application so we only need to ensure that clock channel 1 is active. The boolean value returned by both
clk_ctl.initialize
andclk_ctl.configure
report the clock generator PLL lock status.-
Create an instance of
AD9913Controller
andinitialize
the DDS board hardware. The default state after initialization will be an output frequency of 10 MHz and level of 512 (DAC code).Note that the AD9913 uses a 10-bit output DAC so the DAC code can range from 0 to 1023.
Set the controller output frequency to 25 MHz and the output level to 256 (DAC code). The call to the controller
configure
method pushes the new state to the board hardware and updates the DDS registers and output.
Calibrated output levels
Calibration measurements for the DDS board variants listed in Table 2 are
included in the rfblocks ad9913
class. This calibration data is used
in the associated AD9913Controller
class and will allow the specification of
output levels in mV
or dBm
(as well as DAC codes). Continuing the code
example from above:
>>> dds_ctl.dbm = 0.0 >>> dds_ctl.configure(ser) >>> dds_ctl.millivolts = 71 >>> dds_ctl.configure(ser)
Here the board signal output level is first set to 0.0 dBm. The level is
then set to 71 mV RMS (-10 dBm). Internally, the AD9913Controller
class
converts dBm and mV level specifications to a corresponding DAC code using
the calibration data associated with the board variant and makes use of
the fact that the DDS DAC output current is a linear function of the DAC
code for a given frequency.
The calibration data is a set of slopes and intercepts for these linear functions across a range of signal frequencies from 0 to 100 MHz. The linear function for a specific output signal frequency is then calculated by interpolation between the known calibrated slopes and intercepts.
Calibration of the output levels is carried through using the Calibration Procedure documented below. The resulting calibration data can be loaded using AD9913Controller.set_cal_data. Note that this will override any default calibration data that was loaded when the controller instance was initially created.
>>> import json >>> >>> with open('measuredData/ad9913/ad9913-20dB-cal-data.json') as fd: ... d = json.load(fd) ... >>> lf = d['linear_fits']['5.0'] >>> lf_dict = { 0.0: [lf[0], lf[1]]} >>> for fs in d['linear_fits']: ... lf = d['linear_fits'][fs] ... lf_dict[float(fs)] = [lf[0], lf[1]] ... >>> dds_ctl.set_cal_data(lf_dict)
Sweeping and profiles
If the DDS sweeping facility or the frequency and phase profiles are
to be used then it's recommended that the three profile pins, PS0
,
PS1
, and PS2
be connected to the same port on the controller.
This allows for the possibility of rapid, concurrent updates of the
profile pin signals. The dashed lines in Figure 6 show the connections
used for this example setup.
Continuing the code example from the last section:
>>> dds = ad9913('C5', 'B7', 'C4', ps0='D0', ps1='D1', ps2='D2', ... board_model='20dB') >>> dds_ctl = AD9913Controller('DDS_1', dds) >>> dds_ctl.initialize(ser) >>> dds_ctl.freq = 20.0 >>> dds_ctl.level = 512 >>> dds_ctl.state = ad9913.POWER_UP >>> dds_ctl.configure(ser) >>> dds_ctl.sweep_start = 20.0 >>> dds_ctl.sweep_end = 30.0 >>> dds_ctl.sweep_type = ad9913.SweepType.FREQUENCY >>> dds_ctl.sweep_ramp_type = ad9913.SweepRampType.RAMP_BIDIR >>> dds_ctl.sweep_rising_step = 0.001 >>> dds_ctl.sweep_falling_step = 0.001 >>> dds_ctl.sweep_rising_rate = 256 >>> dds_ctl.sweep_falling_rate = 256 >>> dds_ctl.configure_sweep(ser) >>> dds_ctl.start_sweep(ser) ... >>> dds_ctl.stop_sweep(ser) >>> dds_ctl.sweep_end = 40.0 >>> dds_ctl.configure_sweep(ser) >>> dds_ctl.start_sweep(ser) ... >>> dds_ctl.stop_sweep(ser)
The code does the following:
Rebuild the
AD9913Controller
instance using anad9913
device instance with the required profile select pin connections.Initialize the controller and set the DDS state such that the output signal has a frequency of 20 MHz and level of 512 (DAC code) which corresponds to a power level of a little more than 2 dBm (given the board variant model of
20dB
).Set frequency sweep parameters. The sweep start and stop frequencies are 20 and 30 MHz respectively. The sweep ramp type is set to bidirectional which means the signal will traverse up and down the specified frequecy range. The sweep step and rate are set so that the progress of the output signal as it traverses the sweep frequency range is easily observable on a spectrum analyzer or oscilloscope.
The
configure_sweep
method pushes the sweep state to the DDS registers.The actual sweep process is started by calling the
start_sweep
method with the sweep continuing until thestop_sweep
method is called.After stopping the sweep any of the sweep parameters may be updated and the resulting sweep state pushed to the DDS registers before again starting the sweep.
The AD9913 allows the rapid switching between up to eight frequency/phase profiles. This capability is illustrated by the following code examples which follow on from the code snippet above. In the first snippet, the profile switching is achieved using internal profile control bits in the AD9913 control function register.
>>> dds_ctl.direct_switch_enabled = True >>> dds_ctl.profile_type = ad9913.SweepType.FREQUENCY >>> dds_ctl.set_profile_freq(0, 25.0) >>> dds_ctl.set_profile_freq(1, 35.0) >>> dds_ctl.update_profile(ser, 0) >>> dds_ctl.update_profile(ser, 1) >>> dds_ctl.selected_profile = 0 >>> dds_ctl.configure_profile(ser) >>> dds_ctl.selected_profile = 1 >>> dds_ctl.configure_profile(ser) >>> dds_ctl.selected_profile = 0 >>> dds_ctl.configure_profile(ser)
The code does the following:
Enables direct switch mode. This mode allows the direct update of either frequency or phase from the AD9913 profiles registers.
Set the profile type to be
FREQUENCY
. When a direct update is made the DDS frequency will be configured from the selected profile frequency register.Set the profile frequency registers for profiles 0 and 1. Push the profile values to the DDS profile registers using the
AD9913Controller
update_profile
method.Set a selected profile and update the DDS frequency from the associated profile register using the
configure_profile
register.
In the code example below the profile switching is carried out by changing the profile select pins. This technique allows for higher data throughput.
>>> from rfblocks import write_cmd >>> dds_ctl._ad9913.set_profile_control( ... ad9913.ProfileControl.PINS) >>> write_cmd(dds_ctl._ad9913.config_cfr1()) >>> cmd0 = 'M{},{:02X},{:02X}:'.format(dds.ps_port, dds.ps_mask, 0) >>> cmd0 'MD,07,00:' >>> cmd1 = 'M{},{:02X},{:02X}:'.format(dds.ps_port, dds.ps_mask, 1) >>> cmd1 'MD,07,01:' >>> write_cmd(ser, cmd1) >>> write_cmd(ser, cmd0)
The code does the following:
Sets the profile control to be via the profile selection pins (
PS0
,PS1
,PS2
).Formats ECApp 'set port pins' commands which will set the profile selection pins to either
0x00
or0x01
therefore switching to either profile 0 or profile 1.Writing the previously formatted command strings to the controller will effect the profile changes. It's possible to rapidly switch profiles by sending many contiguous symbols encoded in the command strings. The next code example gives a demonstration of this.
As a more extensive example of profile switching the following code is taken from the DDSGen reference design app. The code executes either FSK or PSK modulation of a random stream of symbols. From 2 to 8 tones or phases may be specified.
loop = asyncio.get_event_loop() mask_str = '{:02X}'.format(self.ctl._ad9913.ps_mask) tones = self.modulation_tones rgen = random.Random() self.ctl._ad9913.set_profile_control(ad9913.ProfileControl.PINS) self.ctl._ad9913.set_direct_switch_mode_enable(True) if self.modulation_type == ModulationType.FREQUENCY: self.ctl._ad9913.set_sweep_type(ad9913.SweepType.FREQUENCY) else: self.ctl._ad9913.set_sweep_type(ad9913.SweepType.PHASE) cmd = self.ctl._ad9913.config_cfr1() await loop.run_in_executor(None, self.write_chan_cmd, cmd) while self.modulating: symbols = [round(rgen.random() * (tones-1)) for i in range(100)] signal_str = ','.join(['{:02X}'.format(b) for b in [self.ctl._ad9913.ps_bits(s) for s in symbols]]) cmd = 'M{},{},{}:'.format(port, mask_str, signal_str) await loop.run_in_executor(None, self.write_chan_cmd, cmd)
Programmable modulus
The programmable modulus mode of the AD9913 is used to provide an exact frequency output. For further details see the Programmable Modulus Mode section on p 14 of the Rev. D AD9913 datasheet and Direct Digital Synthesis (DDS) with a Programmable Modulus.
Reference Designs
Calibration Procedure
The steps taken to calibrate DDS board output signals are as follows:
-
Calibration measurements are taken using the ad9913-measure-cal-data.py script. The DDS under test should be connected as shown in Figure 8. Two assumptions are made here:
The DDS board is supplied with a reference clock. See, for example, Figure 6 in which a clock generator module is used to supply the required 250 MHz clock.
The DDS board is connected to a microcontroller board which is in turn connected to a host computer via a USB connection.
The spectrum analyzer is controlled remotely using GPIB or Visa/LXI. The script source code can be viewed here: Measure calibration data.
-
The ad9913-measure-cal-data.py is now run to capture the DDS signal output voltages across a range of DDS DAC codes and frequencies. Here's an example command line:
python scripts/ad9913-measure-cal-data.py -d /dev/cu.usbmodem14101 -U B7 -C C5
The measurements are saved to a file as well as being echoed to
stdout
. An example of this output is shown below.5.0: 40.18 79.52 119.10 158.71 197.42 236.93 276.33 315.66 353.79 393.45 431.75 470.84 510.35 547.87 587.43 625.73 10.0: 39.36 77.86 116.70 155.29 193.63 231.67 270.35 308.65 346.38 384.60 423.66 460.60 498.85 537.62 575.40 611.94 15.0: 38.15 75.66 113.26 150.66 188.24 225.28 262.37 300.00 336.47 373.08 410.90 447.96 483.29 521.73 558.24 593.99 20.0: 39.34 78.10 116.83 155.75 194.22 232.35 270.82 308.76 347.37 385.66 423.61 461.89 499.49 537.27 575.69 613.16 ... 95.0: 18.53 35.25 52.16 69.11 85.99 102.92 119.38 136.23 152.35 168.99 185.53 202.38 217.87 233.61 250.17 265.19 100.0: 16.29 30.84 45.68 60.48 75.34 89.95 104.66 119.28 133.69 148.27 162.70 176.67 191.35 205.11 219.01 232.86
Each line begins with the output signal frequency and is then followed by a list of the measured peak signal output voltages (in mV) for each of the DDS DAC codes configured.
-
The script now makes linear fits to the measured voltages for each of the test frequencies. This produces a slope and intercept for the linear fits at each of the test frequencies. Figure 9 shows example plots of the linear fits.
-
Plots of the calculated linear fit slopes and intercepts as a function of output signal frequency are made. Figure 10 shows an example of these plots.
It's the functions plotted here that form the core of the calculation used to generate calibrated output levels in the AD9913Controller class. For more information see Calibrated output levels
-
The calibration slope and intercept data can now be included as an item in the
ad9913
CAL_DATA
dict. The format of the dict item is described in ad9913.cal_data. The following code shows an example of how the data can be extracted from the calibration data file:>>> import json >>> from pprint import pprint as pp >>> >>> with open('measuredData/ad9913/ad9913-20dB-cal-data.json') as fd: ... d = json.load(fd) ... >>> lfs = [] >>> cal_data_name = '20dB-2' >>> lf = d['linear_fits']['5.0'] >>> lf_dict = { 0.0: [lf[0], lf[1]]} >>> for fs in d['linear_fits']: ... lf = d['linear_fits'][fs] ... lf_dict[float(fs)] = [lf[0], lf[1]] ... >>> pp(lf_dict) {0.0: [0.6013739809274393, 1.522515000001093], 5.0: [0.6013739809274393, 1.522515000001093], 10.0: [0.5898015666351346, 1.5392552500011525], 15.0: [0.5728032952737382, 1.507987996086448], 20.0: [0.5616700735284558, 1.8379037500017927], 25.0: [0.5416191187949562, 1.576826250001273], 30.0: [0.5340883423703072, 1.8014730000017325], 35.0: [0.5301647238041223, 1.919015250001992], 40.0: [0.5297908778832199, 1.9003730731542878], 45.0: [0.5307727290072409, 2.1737304696323974], 50.0: [0.532739564796775, 2.466679250003161], 55.0: [0.5294871201736061, 2.655759750003534], 60.0: [0.5143739634640143, 3.1962870000047605], 65.0: [0.48885358570660564, 3.146362500004677], 70.0: [0.4563410818010057, 3.2514990002529007], 75.0: [0.42409794898771436, 3.4119032500052104], 80.0: [0.3972877128054187, 3.5073298906843], 85.0: [0.37823979549496733, 3.3779000000051207], 90.0: [0.36362696530192085, 3.6423777500056844], 95.0: [0.3516909167299737, 3.8765600635610893], 100.0: [0.34216751368844056, 4.082412568570491]} >>> if cal_data_name not in ad9913.CAL_DATA: ... ad9913.CAL_DATA[cal_data_name] = lf_dict
Design Notes
The core of the design is the AD9913 10-bit DDS. This is powered by
separate regulators for the digital and analog supplies (U2
, U3
). To
obtain the best performance from the AD9913 the internal PLL should be
bypassed and the DDS clock taken directly from the reference input. The
single ended reference input is converted to a differential signal using
T1
. Although the design has made provision for an onboard crystal this
facility has not been tested.
The AD9913 makes use of a current output DAC. The output current is
delivered as a balanced output via the two IOUT
pins. This is converted
to a single ended output using R10
, R11
, R12
and T2
. The
transformer T2
can be replaced by a 0 \(\Omega\) jumper for low frequency
(down to DC) use. The DAC output is described in more detail below.
DDS output can be routed to either the unfiltered output or the 100 MHz
elliptic filter by populating R13
, R14
, and R16
approriately with
0 \(\Omega\) jumpers. This is the unsplit configuration. Alternatively,
the DDS signal can be split using a 6 dB resistive splitter by populating
R13
, R14
, and R16
with 16.7 \(\Omega\) resistors. This is referred to
as the split configuration.
The output of the 100 MHz elliptic filter may either be used directly by
populating R27
or used as input to the output amplifier by populating
R29
. The output amplifer is an 'embedded' version of the opamp based IF
amplifer which is described in more detail here:
A Wideband, High Linearity IF Amplifier.
A schematic of the amplifier
as implemented on the DDS board is shown in Figure 12. In normal
operation, an external 10V supply is connected to J7
(note that this
supply should not exceed 12V).
Together with the filter, the gain of the output amplifier will determine
the high frequency response of the DDS and is set using R19
and R23
.
Two possible gain settings are summarized in Table 3.
Gain (dB) |
|
|
3 dB Frequency (MHz) |
---|---|---|---|
20 |
39 |
820 |
90 |
27 |
39 |
1.8k |
60 |
For other values of R23
and R19
the gain, G, can be calculated using:
The factor of 6dB appears in the gain calculation because the output
of the OPA847 is split between the amplifier load (assumed to be
50 \(\Omega\)) and R21
. Decreasing the amplifier gain below 20 dB
decreases the phase margin. Gains less than about 15 dB may
cause excessive peaking in the amplifier frequency response and
increases the possibility of sustained oscillation.
When using the unsplit configuration an output amplifier gain of 20 dB is optimal. If the split configuration is used an output amplifier gain of 27 dB should be used. Increasing gains above these levels will result in appreciable signal compression at the higher limit of the DDS output amplitude. Also note that the higher output amplifier gains will limit the high frequency response.
The AD9913 uses 1.8V supplies and control signals must use voltage levels
compatible with this. U4
is the level converter for control signals
between the 3.3V input control lines and the 1.8V control lines required by
the AD9913. The output enable for the level converter is made available as
one of the external control lines. In general this can be tied to 3.3V.
DAC output
The AD9913 uses a current output DAC. The reference current for the DAC is set by R9 and the full scale DAC output current will scale as follows:
with \(x\) being the value of the DAC gain control (bits 9:0 in register 2). Using a value of R9 = 4.64K and \(x =\) 0x3FF, the maximum full scale DAC output current will be 4.55 mA
The primary winding of T2
will usually see a 50 \(\Omega\) load either
connected to the output J3/J4
(if that is used) or as the input
impedance of a resistive splitter made up of R13
, R14
, R16
(see
Figure 3). Since T2
is 1:1 the load seen by the secondary is also 50
\(\Omega\). With the ground point midway on the secondary, the IOUT
side of
the DAC output will see a parallel combination of the 25 \(\Omega\) from the
T2
secondary, R10
, and one half of R11
(since it will have a virtual
ground in the middle). A similar calculation can be done for the other
leg of the DAC output. R11
is optional and for the prototype builds is
not populated. With R10
and R12
set at 100 \(\Omega\), this results in
approximately 150 mV peak to peak or about 50 mV (-13 dBm). This is well
within the 400 mV compliance range of the AD9913 DAC.
Given that the DAC output splitter (R13
, R14
, R16
) is populated,
the available power on J3/J4
will be approximately -19 dBm.
Likewise the available power at the input of the DAC output amplifier
(after the low pass filter) will be approximately -19 dBm. With the
optional output amplifier in use, the output level at J5/J6
will be 6 dBm (assuming an amplifer gain of 25).
The low frequency response will be determined by T2
. The MABAES0060
data sheet specifies the insertion loss down to 300 kHz. However, direct
measurement indicates that the low frequency response drops off by
3 dB at about 50 kHz.
The output response can be extended down to DC by removing T2
and inserting
a 0 \(\Omega\) link between R10
and R16
. However, using the single-ended
output of the DAC in this manner will cause degradation in the common-mode
rejection and increase second-order distortion products, compared to a
differential operating mode.