Welcome to DwfPy!
Overview
DwfPy is a Python package that allows you to access Digilent WaveForms devices via Python. It provides a low-level API with complete access to the Digilent WaveForms API, and also a simple but powerful high-level API, which allows you to configure WaveForms devices with a single statement.
For instance, to output a 1kHz sine-wave on a Analog Discovery 2, you can simply write:
import dwfpy as dwf
with dwf.AnalogDiscovery2() as device:
print('Generating a 1kHz sine wave on WaveGen channel 1...')
device.analog_output['ch1'].setup('sine', frequency=1e3, amplitude=1, start=True)
input('Press Enter key to exit.')
Features
Pythonic abstraction of Digilent Waveforms API.
Low-level API with complete access to the Digilent Waveforms API.
Powerful high-level API that supports one-line configuration statements.
Supports all sub-modules, such as oscilloscope, arbitrary waveform generator, logic analyzer, pattern generator, digital I/O, and power supplies.
Works with all WaveForms devices, such as the Analog Discovery 2 or the Digital Discovery.
Installing DwfPy
You can install the dwfpy package from PyPI using pip:
pip install dwfpy
In order to use the DwfPy package, you need Python 3.6 or higher.
As DwfPy builds on top of the WaveForms API, you need to install the WaveForms software, which includes the required runtime components to access the WaveForms devices.
The source code for the DwfPy package can be found at GitHub at https://github.com/mariusgreuel/dwfpy.
Documentation
You can find the DwfPy user’s guide at https://dwfpy.readthedocs.io/.
Detailed information about the Digilent Waveforms API is available from the WaveForms.
Examples
You can find Python examples using DwfPy in the dwfpy GitHub repository at https://github.com/mariusgreuel/dwfpy/tree/main/examples.
Getting help
For issues with DwfPy, please visit the dwfpy GitHub issue tracker.
Working with Devices
Creating a Device Instance
To find and create an instance of a Digilent Waveforms device,
you simply create an instance of the Device
class.
You should use the Python with statement
to ensure that your device is closed automatically on exit, or when an exception occurs:
import dwfpy as dwf
with dwf.Device() as device:
print(f'Found device: {device.name} ({device.serial_number})')
Using a Specific Device Instance
If you know the kind of device you have connected to your PC,
you can use a specialized class, such as AnalogDiscovery2
or DigitalDiscovery
,
which provides additional functions that are specific to that device:
import dwfpy as dwf
with dwf.AnalogDiscovery2() as device:
print(f'Found an Analog Discovery 2: {device.user_name} ({device.serial_number})')
You can find the available device classes in the device
module.
Filtering Devices
If you have multiple devices installed, you can pass additional filter parameters, while creating the device instance. For example, you can pass a serial number to pick a specific device:
import dwfpy as dwf
with dwf.Device(serial_number='123456ABCDEF') as device:
print(f'Found a device with matching serial number: {device.user_name} ({device.serial_number})')
You can find additional filter parameters in the class constructor method Device
.
Enumerating Devices
If you want to enumerate all present devices and get the devices properties,
you can use the Device.enumerate()
function to enumerate devices:
import dwfpy as dwf
for device in dwf.Device.enumerate():
print(f'Found device: {device.name} {device.serial_number}')
For a complete example, see examples/device_enumeration.py and examples/device_info.py
Using the Oscilloscope
If your device has an oscilloscope, you can access it via the property analog_input
.
Individual channels can be accessed via a zero-based index, or a label, such as ‘ch1’.
There are various high-level functions that you can use to control the oscilloscope:
Channel Setup
analog_input[ch].setup()
- Sets up the channel for data acquisition.
Data Acquisition
analog_input[ch].get_sample()
- Gets the last ADC conversion sample.analog_input.single()
- Starts a single data acquisition.analog_input.record()
- Starts a data recording.
Triggering
analog_input.setup_edge_trigger()
- Trigger upon a certain voltage level in the positive or negative slope of the waveform.analog_input.setup_pulse_trigger()
- Trigger upon a positive or negative pulse width when measured at a certain voltage level.analog_input.setup_transition_trigger()
- Sets up a transition trigger.analog_input.setup_window_trigger()
- Trigger upon a signal entering or exiting a window at certain voltage thresholds.
Setting Up Channels
You can use the analog_input[ch].setup()
function
on a respective analog input channel to setup a channel.
You can specify the channel voltage range, offset voltage, and more.
import dwfpy as dwf
with dwf.Device() as device:
scope = device.analog_input
scope[0].setup(range=5.0, offset=2.0)
Getting the Current ADC Sample
You can use the analog_input[ch].get_sample()
function
on a respective analog input channel to get the current ADC reading.
Note
You need to call the analog_input.read_status()
function
before calling get_sample()
to read a sample from the device.
import dwfpy as dwf
with dwf.Device() as device:
scope = device.analog_input
scope[0].setup(range=50.0)
scope.configure()
scope.read_status()
print(f'CH1: {scope[0].get_sample()}V')
For a complete example, see examples/analog_in_sample.py.
Single Data Acquisition
You can use the single()
function
on the analog input unit to perform a single-shot data acquisition.
To configure the oscilloscope, pass the parameter configure=True
.
To start the acquisition immediately and wait for the acquisition to finish, pass the parameter start=True
.
import dwfpy as dwf
with dwf.Device() as device:
scope = device.analog_input
scope[0].setup(range=5.0)
scope.single(sample_rate=1e6, buffer_size=4096, configure=True, start=True)
samples = scope[0].get_data()
print(samples)
For a complete example, see examples/analog_in_single.py.
Recording Samples
You can use the record()
function on the analog input unit to perform data recording.
To configure the oscilloscope, pass the parameter configure=True
.
To start the acquisition immediately, pass the parameter start=True
.
import dwfpy as dwf
with dwf.Device() as device:
scope = device.analog_input
scope[0].setup(range=5.0)
recorder = scope.record(sample_rate=1e6, sample_count=1e6, configure=True, start=True)
samples = recorder.channels[0].data_samples
print(samples)
For a complete example, see examples/analog_in_record.py.
Setting Up an Edge Trigger
You can use the setup_edge_trigger()
function
to trigger the oscilloscope on a positive or negative slope of the waveform.
import dwfpy as dwf
with dwf.Device() as device:
scope[0].setup(range=5.0)
scope.setup_edge_trigger(mode='normal', channel=0, slope='rising', level=0.5, hysteresis=0.01)
scope.single(sample_rate=1e6, buffer_size=4096, configure=True, start=True)
samples = scope[0].get_data()
For a complete example, see examples/analog_in_single.py.
Using the Waveform Generator
If your device has an arbitrary waveform generator, you can access it via the property analog_output
.
Individual channels can be accessed via a zero-based index, or a label, such as ‘ch1’.
There are various high-level functions that you can use to control the waveform generator:
Channel Setup
analog_output[ch].setup()
- Sets up a new carrier waveform.analog_output[ch].setup_am()
- Applies an AM modulation to a waveform.analog_output[ch].setup_fm()
- Applies an FM modulation to a waveform (sweep).
Setting up a new Waveform
You can use the setup()
function to create a new waveform.
You can specify the generator function, frequency, amplitude, offset, symmetry, and phase.
By default, the carrier node is enabled via the parameter enabled=True
.
To start the channel immediately, pass the parameter start=True
.
import dwfpy as dwf
with dwf.Device() as device:
wavegen = device.analog_output
wavegen[0].setup(function='sine', frequency=1e3, amplitude=1.0, start=True)
For a complete example, see examples/analog_out_sine.py.
Setting up an Amplitude Modulation
You can use the setup_am()
function to modulate the amplitude.
You can specify the generator function, frequency, amplitude, offset, symmetry, and phase, similar to the setup()
function.
By default, the AM node is enabled via the parameter enabled=True
.
To start the channel immediately, pass the parameter start=True
.
import dwfpy as dwf
with dwf.Device() as device:
wavegen = device.analog_output
wavegen[0].setup(function='sine', frequency=1e3, amplitude=1.0)
wavegen[0].setup_am(function='triangle', frequency=10, amplitude=50, start=True)
Setting up a Frequency Modulation
You can use the setup_fm()
function to modulate the frequency.
You can specify the generator function, frequency, amplitude, offset, symmetry, and phase, similar to the setup()
function.
By default, the FM node is enabled via the parameter enabled=True
.
To start the channel immediately, pass the parameter start=True
.
import dwfpy as dwf
with dwf.Device() as device:
wavegen = device.analog_output
wavegen[0].setup(function='sine', frequency=1e3, amplitude=1.0)
wavegen[0].setup_fm(function='sine', frequency=10, amplitude=10, start=True)
Using the Logic Analyzer
If your device has a logic analyzer, you can access it via the property digital_input
.
Individual channels can be accessed via a zero-based index, or a label, such as ‘ch1’.
There are various high-level functions that you can use to control the logic analyzer:
Acquisition
digital_input.single()
- Starts a single data acquisition.digital_input.record()
- Starts a data recording.
Triggering
digital_input.setup_trigger()
- Sets up the trigger condition.digital_input.setup_edge_trigger()
- Sets up an edge trigger.digital_input.setup_level_trigger()
- Sets up a level trigger.digital_input.setup_glitch_trigger()
- Sets up a glitch trigger.digital_input.setup_timeout_trigger()
- Sets up a timeout trigger.digital_input.setup_more_trigger()
- Sets up a more trigger.digital_input.setup_length_trigger()
- Sets up a length trigger.digital_input.setup_counter_trigger()
- Sets up a counter trigger.
Channel specific Triggering
digital_input[ch].setup_trigger()
- Sets up the trigger condition for this channel.digital_input[ch].setup_reset_trigger()
- Sets up the trigger reset condition for this channel.
Single Data Acquisition
You can use the single()
function
on the digital input unit to perform a single-shot data acquisition.
To configure the logic analyzer, pass the parameter configure=True
.
To start the acquisition immediately and wait for the acquisition to finish, pass the parameter start=True
.
import dwfpy as dwf
with dwf.Device() as device:
logic = device.digital_input
samples = logic.single(sample_rate=1e6, buffer_size=4096, configure=True, start=True)
print(samples)
For a complete example, see examples/digital_in_acquisition.py.
Recording Samples
You can use the record()
function
on the digital input unit to perform data recording.
To configure the logic analyzer, pass the parameter configure=True
.
To start the acquisition immediately, pass the parameter start=True
.
import dwfpy as dwf
with dwf.Device() as device:
logic = device.digital_input
recorder = logic.record(sample_rate=1e6, sample_count=1e6, configure=True, start=True)
samples = recorder.data_samples
print(samples)
For a complete example, see examples/digital_in_record.py.
For an examples that uses data compression, see examples/digital_in_record_compress.py.
Using the Pattern Generator
If your device has a pattern generator, you can access it via the property digital_output
.
Individual channels can be accessed via a zero-based index, or a label, such as ‘ch1’.
There are various functions that you can use to configure pins:
digital_output[ch].setup_constant()
- Sets up the channel as a constant output.digital_output[ch].setup_clock()
- Sets up the channel as a clock output.digital_output[ch].setup_pulse()
- Sets up the channel as a pulse output.digital_output[ch].setup_random()
- Sets up the channel as a random output.digital_output[ch].setup_custom()
- Sets up the channel with a custom output.
All setup function allow you to speficy the output mode (‘push-pull’, ‘open-drain’, ‘open-source’, or ‘three-state’) and the idle state (‘init’, ‘low’, ‘high’, or ‘z’).
Setting up a Constant Output
You can use the setup_constant()
function
to drive a channel with a constant output value.
To start the channel immediately, pass the parameter start=True
.
import dwfpy as dwf
with dwf.Device() as device:
pattern = device.digital_output
# Output a high level on pin DIO-0.
pattern[0].setup_constant(value=True, start=True)
Setting up a Clock Signal
You can use the setup_clock()
function
to output a clock or PWM signal.
You can specify the frequency, duty_cycle, phase, delay, and repetition count.
By default, the channel is enabled via the parameter enabled=True
.
To start the channel immediately, pass the parameter start=True
.
import dwfpy as dwf
with dwf.Device() as device:
pattern = device.digital_output
# Output a 1kHz clock on pin DIO-0.
pattern[0].setup_clock(frequency=1e3, start=True)
For a complete example, see examples/digital_out_clock.py.
Setting up a Random Signal
You can use the setup_random()
function to output a clock or PWM signal.
You can specify the rate, delay, and repetition count.
By default, the channel is enabled via the parameter enabled=True
.
To start the channel immediately, pass the parameter start=True
.
import dwfpy as dwf
with dwf.Device() as device:
pattern = device.digital_output
# Output a random pattern at a 1kHz rate on pin DIO-0.
pattern[0].setup_random(rate=1e3, start=True)
For a complete example, see examples/digital_out_pins.py.
Using the Digital I/O
If your device has Digital I/O, you can access it via the property digital_io
.
Individual channels can be accessed via a zero-based index, or a label, such as ‘dio0’, ‘dio1’, etc.
Reading from Inputs
By default, pins are configured as inputs.
Note
You need to call the digital_io.read_status()
function
before reading from input_state
to read all input states from the device.
import dwfpy as dwf
with dwf.Device() as device:
io = device.digital_io
# Configure DIO-0 as input
io[0].setup(enabled=False, configure=True)
# Read all I/O pins
io.read_status()
dio0 = io[1].input_state
print(f'DIO-0={dio0}')
Writing to Outputs
You can use the setup()
function
to setup a Digital I/O pin.
To configure the Digital I/O pins, pass the parameter configure=True
.
import dwfpy as dwf
with dwf.Device() as device:
io = device.digital_io
# Configure DIO-0 as output, and set the state to high
io[0].setup(enabled=True, state=True)
# Configure DIO-1 as output, and set the state to low
io[1].setup(enabled=True, state=False, configure=True)
# Output a one on DIO-1
io[1].output_state = True
For a complete example, see examples/digital_in_acquisition.py.
Using the Power Supplies
Depending on the device you have, there may be one or more power supplies that can be accessed via the channels and nodes of the Analog IO module, which are documented in the WaveForms.
Note
In order to output a voltage, most power supplies have to be enabled both individually and via the master enable switch.
For instance, to output 3.3V on the positive power supply of an Analog Discovery 2, you can write:
import dwfpy as dwf
with dwf.Device() as device:
# Set the voltage of the positive power supply to 3.3V.
device.analog_io[0][1].value = 3.3
# Enable the positive power supply.
device.analog_io[0][0].value = True
# Enable the master-enable switch.
device.analog_io.master_enable = True
Instead of the generic Device class, you can also use the specialized device classes, which simplifies the access to the power supplies.
For instance, to output 3.3V on the positive power supply of an Analog Discovery 2, you can write:
import dwfpy as dwf
with dwf.AnalogDiscovery2() as device:
# Set the positive power supply to 3.3V and enable it.
device.supplies.positive.setup(voltage=3.3)
device.supplies.master_enable = True
Labelling Channels
Instead of indexing channels via a zero-based integer index, the channels can be accessed via a label. By default, analog channels are labelled ch1, ch2, etc. Digital channels are labelled dio0, dio1, etc.
You can rename the channel labels to you needs. For instance, if you have a clock signal on DIO pin 0 that you want to access via the label clock, you could write:
import dwfpy as dwf
with dwf.Device() as device:
# Rename pin DIO-0 from 'dio0' to 'clock'
device.digital_output[0].label = 'clock'
# DIO-0 can now be referenced via the label 'clock'
device.digital_output['clock'].setup_clock(frequency=1e3)
dwfpy Design
The design of the dwfpy package consists of three layers: Python bindings, low-level API, and high-level API.
Bindings
The dwfpy bindings give you raw access to the C API of the DWF DLL. In order to provide a natural look-and-feel while working in Python, the following API changes have been made:
The naming of the DWF API has been adapted to match Python naming conventions.
The DWF function API has been declared using the ctypes CFUNCTYPE prototypes, which allow you to pass Python types such as int, instead of ctypes types such as c_int.
Error handling is performed automatically, i.e. the C functions return value is checked and an exception is thrown when appropriate.
Function return values are directly returned as a scalar or tuple.
For instance, the DWF C API to get the minimum and maximum offset voltage of an analog-out channel is:
int FDwfAnalogOutNodeOffsetInfo(HDWF hdwf, int idxChannel, AnalogOutNode node, double *pMin, double *pMax);
When working with the dwfpy bindings, it can be used as follows:
import dwfpy as dwf
import dwfpy.bindings as dwfb
with dwf.Device() as device:
min_offset, max_offset = dwfb.dwf_analog_out_node_offset_info(device.handle, 0, dwfb.ANALOG_OUT_NODE_CARRIER)
Typically, you do not work with the dwfpy bindings. Instead, use the low-level or high-level API.
Low-Level API
The dwfpy low-level API is designed to map the bindings to a more structured API.
Functional units of the device have been grouped into objects.
Channels and nodes are abstracted as collections, using a dict-like API.
For instance, the DWF C API to set the offset voltage of the analog_out channel 1 (arbitary waveform generator) is:
int FDwfAnalogOutNodeOffsetSet(HDWF hdwf, int idxChannel, AnalogOutNode node, double vOffset);
When working with the dwfpy low-level API, it can be used as follows:
import dwfpy as dwf
with dwf.Device() as device:
device.analog_output.channels[0].nodes[dwf.AnalogOutputNode.CARRIER].offset = 1.23
High-Level API
The dwfpy high-level API is designed to perform multiple common actions with a single line of Python code.
For instance, to start channel 1 of the arbitary waveform generator to output a sine-wave, using a frequency of 1kHz and an amplitude of 1Vpp, you would write:
import dwfpy as dwf
with dwf.Device() as device:
device.analog_output['ch1'].setup(function='sine', frequency=1e3, amplitude=1, start=True)
dwfpy is a package for accessing Digilent WaveForms devices. |