Basic Waveform Playback

This tutorial is applicable to all HDAWG Instruments.

Goals and Requirements

The goal of this tutorial is to demonstrate the basic use of the HDAWG. We demonstrate waveform generation and playback, triggering and synchronization. In order to visualize the multi-channel signals, an oscilloscope with sufficient bandwidth and channel number is required.

Preparation

Connect the cables as illustrated below. Make sure that the instrument is powered on and connected by Ethernet to your local area network (LAN) where the host computer resides. After starting LabOne, the default web browser opens with the LabOne graphical user interface.

The instrument can also be connected via the USB interface, which can be simpler for a first test. As a final configuration for measurements, it is recommended to use the 1GbE interface, as it offers larger bandwidth.

fig tutorial basic setup
Figure 1. Connections for the arbitrary waveform generator basic playback tutorial

The tutorial can be started with the default instrument configuration (e.g. after a power cycle) and the default user interface settings (e.g. as is after pressing F5 in the browser).

Waveform Generation and Playback

In this tutorial we generate signals with the AWG and visualize them with the scope. In a first step we enable the Wave outputs, but disable all sinusoidal signals generated by the sine generators by default. We also configure the scope with a suitable time base (e.g. 500 ns per division) and range (e.g. 0.2 V per division) The following table summarizes the necessary settings.

Table 1. Settings: enable the output
Tab Sub-tab Section # Label Setting / Value / State

Output

Wave Outputs

1

Enable

ON

Output

Wave Outputs

2

Enable

ON

Output

Sine Generators

1&2

Wave 1 Enable

OFF

Output

Sine Generators

1&2

Wave 2 Enable

OFF

Table 2. Settings: configure the external scope
Scope Setting Value / State

Ch1 enable

ON

Ch1 range

0.2 V/div

Timebase

500 ns/div

Trigger source

Ch1

Trigger level

100 mV

Run / Stop

ON

functional output
Figure 2. LabOne UI: Output tab

In the Output tab, we configure the first output channel. The final signal amplitude is given by the product of the full scale output range of 1 V, the dimensionless amplitude scaling factor 1.0, and the actual dimensionless signal amplitude stored in the waveform memory. The necessary settings are summarized in the following table.

Table 3. Settings: configure the AWG output
Tab Sub-tab Section # Label Setting / Value / State

Seq

Control

Sampling Rate

2.4 GHz

Output

Waveform Generators

1

Amplitude

1.0

Output

Waveform Generators

1

Modulation

OFF

Output

Wave Outputs

1

Range

1 V

Output

Wave Outputs

1

Enable

ON

To operate the AWG we need to specify a sequence program. This can be done interactively by typing the program in the Sequence window. Let’s start by typing the following code into the sequence editor.

wave w_gauss = 1.0*gauss(8000, 4000, 1000);
playWave(1, w_gauss);

In the first line of the program, we generate a waveform with a Gaussian shape with a length of 8000 samples and store the waveform under the name w_gauss. The peak center position 4000 and the standard deviation 1000 are both defined in units of samples. You can convert them into time by dividing by the chosen Rate (2.4 GSa/s by default). The waveform generated by the gauss function has a peak amplitude of 1. This amplitude is dimensionless and the physical signal amplitude is given by this number multiplied with the Wave output range (here we chose 1.0 V). We put a scaling factor of 1.0 in place which can be replaced by any other value. The code line is terminated by a semicolon according to C conventions. With the second line of the program, the generated waveform w_gauss is played on AWG Output 1.

For this tutorial, we will keep the description of the Sequencer instructions short. You can find the full specification of the LabOne Sequencer language in LabOne Sequence Programming

The AWG has a waveform granularity of 16 samples. It’s recommended to use waveform lengths that are multiples of 16, e.g. 8000 like in this example, to avoid having ill-defined samples between successively played waveforms. Waveforms with other lengths are allowed, but they get padded with zeros by the compiler. This may be undesired in some cases, specifically when using the Hold feature of the AWG.

If we now click on btn uielements save um, the program gets compiled. This means the program is translated into instructions for the LabOne Sequencer on the instrument, see AWG Sequencer Tab . If no error occurs (due to wrong program syntax, for example), the Compiler Status LED lights up green, and the resulting program as well as the waveform data is written to the instrument memory. If an error or warning occurs, messages in the Compiler Status field will help in debugging the program. If we now have a look at the Waveform sub-tab, we see that our Gaussian waveform appeared in the list. The Memory Usage field at the bottom of the Waveform sub-tab shows what fraction of the instrument memory is filled by the waveform data. The Waveform Viewer sub-tab allows you to graphically display the currently marked waveform in the list.

By clicking on btn uielements single um, we have the AWG execute our program once. Since we have armed the scope previously with a suitable trigger level, it has captured our Gaussian pulse with a FWHM of about 1.0 μs as shown in Figure Figure 3.

fig tutorial awg scopeGauss
Figure 3. Gaussian pulse as generated by the AWG and captured by the scope

The LabOne Sequencer language offers a lot of run-time control. The basic functionality is to repeat a waveform several times. In the following example, all the code within the curly brackets {…} is repeated 5 times. Upon clicking btn uielements save um and btn uielements single um, you should observe 5 short Gaussian pulses in a new scope shot, see Figure 4.

wave w_gauss = 1.0 * gauss(640, 320, 50);

repeat (5) {
  playWave(1, w_gauss);
}
fig tutorial awg scopeFive
Figure 4. Burst of Gaussian pulses generated by the AWG and captured by the scope

In order to generate more complex waveforms, the LabOne Sequencer programming language offers a rich toolset for waveform editing. On the basis of a selection of standard waveform generation functions, waveforms can be added, multiplied, scaled, concatenated, and truncated. It’s also possible to use compile-time evaluated loops to generate pulse series with systematic parameter variations – see LabOne Sequence Programming for more precise information. In the following code example, we make use of these tools to generate a pulse with a smooth rising edge, a flat plateau, and a smooth falling edge. We use the cut function to cut a waveform at defined sample indices, the rect function to generate a waveform with constant level 1.0 and length 320, and the join function to concatenate three (or arbitrarily many) waveforms.

wave w_gauss = gauss(640, 320, 50);
wave w_rise = cut(w_gauss, 0, 319);
wave w_fall = cut(w_gauss, 320, 639);
wave w_flat = rect(320, 1.0);

wave w_pulse = join(w_rise, w_flat, w_fall);

while (true) {
  playWave(1, w_pulse);
}

Note that we replaced the finite repetition by an infinite repetition by using a while loop. Loops can be nested in order to generate complex playback routines. The output generated by the program above is shown in Figure 5.

fig tutorial awg scopeFlat
Figure 5. Infinite pulse series generated by the AWG and captured by the scope

As programs get longer, it becomes useful to store and recall them. Clicking on btn uielements saveas um allows you to store the present program under a new name. Clicking on btn uielements save um then saves your program to the file name displayed at the top of the editor. As you begin to work on sequence programs more regularly, it’s worth expanding your repertoire using some of the editor keyboard shortcuts listed in Sequence Editor Keyboard Shortcuts.

It’s also possible to iterate over the samples of a waveform array and calculate each one of them in a loop over a compile-time variable cvar. This often allows to go beyond the possibilities of using the predefined waveform generation function, particularly when using nested formulas of elementary functions like in the following example. The waveform array needs to be pre-allocated e.g. using the instruction zeros.

const N = 1024;
const width = 100;
const position = N/2;
const f_start = 0.1;
const f_stop = 0.2;
cvar i;
wave w_array = zeros(N);
for (i = 0; i < N; i++) {
  w_array[i] = sin(10/(cosh((i-position)/width)));
}

playWave(w_array);

Should you require more customization than what is offered by the LabOne AWG Sequencer language, it’s possible to load a waveform directly from the API. In the sequence the waveform should be declared using the placeholder function to define size and type of the waveform.

const LENGTH = 1024;
wave w = placeholder(LENGTH, true, false); // Create a waveform of size LENGTH, with one marker
assignWaveIndex(1, w, 10);                 // Create a wave table entry with placeholder waveform
                                           // routed to output 1, with index 10
playWave(1, w);

This sequence will not run until a valid waveform has been loaded. This can be done for example in Python

import zhinst.ziPython as zi
import zhinst.utils
import numpy as np

device = 'dev8000'
daq = zi.ziDAQServer('localhost', 8004, 6) #Connect to the dataserver
daq.connectDevice(device, '1GbE')          #Connect to the device

#Generate a waveform and marker
LENGTH = 1024
wave = np.sin(np.linspace(0, 10*np.pi, LENGTH))*np.exp(np.linspace(0, -5, LENGTH))
marker = np.concatenate([np.ones(32), np.zeros(LENGTH-32)]).astype(int)

#Convert them and send to the instrument
wave_raw = zhinst.utils.convert_awg_waveform(wave, markers=marker)
daq.setVector(f'/{device:s}/awgs/0/waveform/waves/10', wave_raw)
fig tutorial awg vector wfm
Figure 6. Waveform loaded by the API

The waveform data can be arbitrary, but consider that the final signal will pass through the output stage of the instrument. This means that signal components exceeding the output stage bandwidth are not reproduced exactly as suggested for example by looking at a plot of the waveform data. In particular, this concerns sharp transitions from one sample to the next.

In the usage of the instructions 'placeholder', 'assignWaveIndex', and 'playWave', there are a number of caveats due to the implicit creation of waveform table entries depending on the output channel assignment (the optional first arguments of 'assignWaveIndex' and 'playWave' instructions). The following code is for example valid, but not recommended because it is poorly readable:

const LENGTH = 1024;
wave w = placeholder(LENGTH);
assignWaveIndex(1, w, 10);
assignWaveIndex(w, w, 11);

playWave(1, w);
playWave(w, w);

Instead, it’s recommended to use a unique waveform variable name for each intended single-channel memory entry, and to use this variable name with consistent output channel assignment in 'placeholder', 'assignWaveIndex', and 'playWave' as is done in the following example:

const LENGTH = 1024;
wave w_a = placeholder(LENGTH, true, true);
wave w_b = placeholder(LENGTH, true, true);
wave w_c = placeholder(LENGTH, true, true);
assignWaveIndex(1, w_a,   10);
assignWaveIndex(w_b, w_c, 11);

playWave(1, w_a);
playWave(w_b, w_c);

In the latter case, a possible Python code to update the wave table is shown below. Note that we use the full amount of markers available in this example. The marker integer array encodes the available markers in its least significant bits.

import zhinst.ziPython as zi
import zhinst.utils
import numpy as np

device = 'dev8000'
daq = zi.ziDAQServer('localhost', 8004, 6) #Connect to the dataserver
daq.connectDevice(device, '1GbE')          #Connect to the device

#Generate a waveform and marker
LENGTH = 1024
wave_a = np.sin(np.linspace(0, 10*np.pi, LENGTH))*np.exp(np.linspace(0, -5, LENGTH))
wave_b = np.sin(np.linspace(0, 20*np.pi, LENGTH))*np.exp(np.linspace(0, -5, LENGTH))
wave_c = np.sin(np.linspace(0, 30*np.pi, LENGTH))*np.exp(np.linspace(0, -5, LENGTH))

marker_a  = np.concatenate([  0b11*np.ones(32), np.zeros(LENGTH-32)]).astype(int)
marker_bc = np.concatenate([0b1111*np.ones(32), np.zeros(LENGTH-32)]).astype(int)

#Convert and send them to the instrument
wave_raw_a = zhinst.utils.convert_awg_waveform(wave_a, markers=marker_a)
wave_raw_bc = zhinst.utils.convert_awg_waveform(wave_b, wave_c, markers=marker_bc)
daq.setVector(f'/{device:s}/awgs/0/waveform/waves/10', wave_raw_a)
daq.setVector(f'/{device:s}/awgs/0/waveform/waves/11', wave_raw_bc)

In channel grouping modes 2x4, 1x8, 1x4, the usage of 'placeholder', 'assignWaveIndex' combined with 'playWave' is also supported. However, in these modes it is more difficult to infer the format and content of the wave tables on the separate AWG cores from the high-level sequence program. When possible, use the default grouping modes 4x2 and 2x2. In other modes, follow the same basic guideline of using unique waveform variable names and consistent use of 'assignWaveIndex' and 'playWave' instructions. As an example, the following code will create dual-channel waveform table entries on the AWG cores 1 and 2. Both entries have the index 10 and need to be filled with data separately using the respective API nodes 'dev…​./awgs/0/waveform/waves/10' and 'dev…​./awgs/1/waveform/waves/10'.

const LENGTH = 1024;
wave w_a = placeholder(LENGTH, true, true);
wave w_b = placeholder(LENGTH, true, true);
wave w_c = placeholder(LENGTH, true, true);
wave w_d = placeholder(LENGTH, true, true);

assignWaveIndex(w_a, w_b, w_c, w_d, 10);

playWave(w_a, w_b, w_c, w_d);

Python code to fill the wave table in this case is shown below.

import zhinst.ziPython as zi
import zhinst.utils
import numpy as np

device = 'dev8000'
daq = zi.ziDAQServer('localhost', 8004, 6) #Connect to the dataserver
daq.connectDevice(device, '1GbE')          #Connect to the device

#Generate a waveform and marker
LENGTH = 1024
wave_a = np.sin(np.linspace(0, 10*np.pi, LENGTH))*np.exp(np.linspace(0, -5, LENGTH))
wave_b = np.sin(np.linspace(0, 20*np.pi, LENGTH))*np.exp(np.linspace(0, -5, LENGTH))
wave_c = np.sin(np.linspace(0, 30*np.pi, LENGTH))*np.exp(np.linspace(0, -5, LENGTH))
wave_d = np.sin(np.linspace(0, 40*np.pi, LENGTH))*np.exp(np.linspace(0, -5, LENGTH))

marker_ab = np.concatenate([0b1111*np.ones(32), np.zeros(LENGTH-32)]).astype(int)
marker_cd = np.concatenate([0b1111*np.ones(32), np.zeros(LENGTH-32)]).astype(int)


#Convert and send them to the instrument
wave_raw_ab = zhinst.utils.convert_awg_waveform(wave_a, wave_b, markers=marker_ab)
wave_raw_cd = zhinst.utils.convert_awg_waveform(wave_c, wave_d, markers=marker_cd)
daq.setVector(f'/{device:s}/awgs/0/waveform/waves/10', wave_raw_ab)
daq.setVector(f'/{device:s}/awgs/1/waveform/waves/10', wave_raw_cd)

Alternatively to the waveform upload via an API, you can import any waveform from a file. If the file is stored in the location (C:\Users\<user name>\Documents\Zurich Instruments\LabOne\WebServer\awg\waves\ under Windows or ~/Zurich Instruments/LabOne/WebServer/awg/waves/ under Linux), you can then play back the wave by referring to the file name without extension in the sequence program:

playWave("wave_file");

If you prefer, you can also store it in a wave data type first and give it a new name:

wave w = "wave_file";
playWave(w);

For more information about the file format, please refer to the LabOne Programming Manual, section AWG Module.

Triggering and Synchronization

Now we have a look at the triggering functionality of the AWG. In this section you will learn about the most important use cases:

  • Triggering the AWG with an external TTL signal

  • Generating a TTL signal with the AWG to trigger another piece of equipment

Triggering the AWG

In this section we show how to trigger the AWG with an external TTL signal. We start by generating a periodic TTL signal using the external scope (on the trigger, sync, or auxiliary output), if it has this capability, or by using another source of such a signal, e.g., a function generator. Generate a signal with the parameters listed in the following table.

Table 4. Settings: generate a 300 kHz TTL signal
Setting Value / State

Signal type

square wave

Signal level

0 V / 3.3 V

Frequency

300 kHz

We then connect this square signal to the HDAWG Trig input 1, and separately monitor the signal on the scope using a power splitter or a T-piece. The figure below shows the connection used.

fig tutorial basic setup exttrigger
Figure 7. Connections for triggering the arbitrary waveform generator

The AWG internally has 2 digital trigger input channels. These are not directly associated with physical device inputs but can be freely configured to probe a variety of internal or external signals. Here, we link the AWG Digital Trigger 1 to the physical Trig 1 connector.

Table 5. Settings: configure the AWG analog trigger input
Tab Sub-tab Section # Label Setting / Value / State

AWG

Trigger

Digital Trigger 1

Signal

Trigger In 1

AWG

Trigger

Digital Trigger 1

Slope

Rise

Finally, we modify our last AWG program by including a waitDigTrigger instruction just before the playWave instruction. The result is that upon every repetition inside the infinite while loop, the AWG will wait for a rising edge on Trigger input 1.

wave w_gauss = gauss(640, 320, 50);
wave w_rise  = cut(w_gauss, 0, 319);
wave w_fall  = cut(w_gauss, 320, 639);
wave w_flat  = rect(320, 1.0);

wave w_pulse = join(w_rise, w_flat, w_fall);

while (true) {
  waitDigTrigger(1);
  playWave(1, w_pulse);
}

Compile and run the above program. Figure 8 shows the pulse series as seen in the scope: the pulses are now spaced by the oscillator period of about 3.3 μs, unlike previously when the period was determined by the length of the waveform w_pulse. Try changing the trigger signal frequency or unplugging the trigger cable to observe the immediate effect on the signal.

fig tutorial awg scopePeriod
Figure 8. Externally triggered pulse series generated by the AWG and captured by the scope. The AWG signal is shown in blue, the external trigger signal (attenuated by 20 dB) is shown in green.

Generating Markers with the AWG

There are two ways of generating trigger output signals with the AWG: as markers, or through sequencer instructions.

The method using markers is recommended when precise timing is required, and/or complicated serial bit patterns need to be played on the Marker outputs. Marker bits are part of every waveform which is an array of 18-bit words: 16 bits of each word represent the analog waveform data, and the remaining 2 bits represent two digital marker channels. Upon playback, a digital signal with sample-precise alignment with the analog output is generated.

The method using a sequencer instruction is simpler, but the timing control is less powerful than when using markers. It is useful for instance to generate a single trigger signal at the start of an AWG program.

Table 6. Comparison: AWG markers and triggers
Marker Trigger

Implementation

Part of waveform

Sequencer instruction

Timing control

High

Low

Generation of serial bit patterns

Yes

No

Cross-device synchronization

Yes

Yes

Let us first demonstrate the use of markers . In the following code example we first generate a Gaussian pulse again. The generated wave actually does include marker bits – they are simply set to zero by default. We use the marker function to assign the desired non-zero marker bits to the wave. The marker function takes two arguments, the first is the length of the wave, the second is the marker configuration in binary encoding: the value 0 stands for a both marker bits low, the values 1, 2, and 3 stand for the first, the second, and both marker bits high, respectively. We use this to construct the wave called w_marker.

const marker_pos = 3000;

wave w_gauss  = gauss(8000, 4000, 1000);
wave w_left   = marker(marker_pos, 0);
wave w_right  = marker(8000-marker_pos, 1);
wave w_marker = join(w_left, w_right);
wave w_gauss_marker = w_gauss + w_marker;

playWave(1, w_gauss_marker);

The waveform addition with the '+' operator adds up analog waveform data but also combines marker data. The wave w_gauss contains zero marker data, whereas the wave w_marker contains zero analog data. Consequentially the wave called w_gauss_marker contains the merged analog and marker data. We use the integer constant marker_pos to determine the point where the first marker bit flips from 0 to 1 somewhere in the middle of the Gaussian pulse.

The add function and the '+' operator combine marker bits by a logical OR operation. This means combining 0 and 1 yields 1, and combining 1 and 1 yields 1 as well.

There is a certain freedom to assign different marker bits to different Mark outputs. The following table summarizes the settings to apply in order to output marker bit 1 on Mark 1.

Table 7. Settings: configure the AWG marker output and scope trigger
Tab Sub-tab Section # Label Setting / Value / State

DIO

Marker Out

2

Signal

Output 1 Marker 1

DIO

Output

2

Drive

ON

Scope

Trigger

Trigger

Signal

Trig Input 1

We connect the Mark 1 signal to one of the scope channels as shown below.

fig tutorial basic setup marker
Figure 9. Connections for generating marker signals with the arbitrary waveform generator

Figure 10 shows the AWG signal captured by the scope as a blue curve. The green curve shows the second scope channel displaying the marker signal. Try changing the marker_pos constant and re-running the sequence program to observe the effect on the temporal alignment of the Gaussian pulse.

fig tutorial awg scopeMarker
Figure 10. Gaussian pulse and square marker signal generated by the AWG and captured by the scope

Let us now demonstrate the use of sequencer instructions to generate a trigger signal. Copy and paste the following code example into the Sequence Editor.

wave w_gauss = gauss(8000, 4000, 1000);

setTrigger(1);
playWave(1, w_gauss);
waitWave();
setTrigger(0);

The setTrigger function takes a single argument encoding the four trigger output states in binary manner – the integer number 1 corresponds to a configuration of 0/0/0/1 for the trigger outputs 4/3/2/1. The binary integer notation of the form 0b0000 is useful for this purpose – e.g. setTrigger(0b0011) will set trigger outputs 1 and 2 to 1, and trigger outputs 3 and 4 to 0. We included a waitWave instruction after the playWave instruction. It ensures that the subsequent setTrigger instruction is executed only after the Gaussian wave has finished playing, and not during waveform playback.

The 'waitWave' instruction represents a means to control the timing of instructions in the the Wait & Set and the Playback queue which are described in the section AWG Architecture and Execution Timing. In the example above, the waitWave instruction puts the playback of the next instruction in the Wait & Set queue, the setTrigger(0), on hold until the waveform is finished. Without the 'waitWave' instruction, the AWG trigger would return to zero at the beginning of the waveform playback.

Between consecutive 'playWave' and 'playZero' instructions, the use of 'waitWave' is explicitly not required. Sequential instructions in the playback queue are played immediately after one another, back to back.

We reconfigure the Mark 1 connector in the DIO tab such that it outputs 'AWG Trigger 1', instead of 'Output 1 Marker 1'. The rest of the settings can stay unchanged.

Table 8. Settings: configure the AWG trigger output
Tab Sub-tab Section # Label Setting / Value / State

DIO

Marker Out

1

Signal

AWG Trigger 1

Figure 11 shows the AWG signal captured by the scope. This looks very similar to Figure 10 in fact. With this method, we’re less flexible in choosing the trigger time, as the rising trigger edge will always be at the beginning of the waveform. But we don’t have to bother about assigning the marker bits to the waveform.

fig tutorial awg scopeTrigger
Figure 11. Gaussian pulse and trigger signal generated by the AWG and captured by the scope