Digilent Analog Discovery 3 Road Test

Table of contents

RoadTest: Enroll to Review the Digilent Analog Discovery 3

Author: jpnbino

Creation date:

Evaluation Type: Test Equipment

Did you receive all parts the manufacturer stated would be included in the package?: True

What other parts do you consider comparable to this product?: AD2

What were the biggest problems encountered?: No Problems found

Detailed Review:

Intro

I've used AD2 before and the seleae logic as my units for working. By far the instruments that I use the most are the logic analyzer and the protocol, a few times I use a scope as I'm mostly a firmware developer integrating sensors (SPI, UART, 1-wire, and I2C). With that in mind, I was happy to apply for the AD3 roadTest and share my findings and what I could do with it. Lastly, I hope that my review contributes to the community, so if you come around and feel like it, I am glad to hear your constructive feedback.

I start this review with an unboxing video. I find it fun to do and relaxing to watch, if it is not for you, you can just skip it.

Logic Analyzer

This is the number one instrument I use in my daily work and the 16 channels AD3 has is pretty much enough, I don't even recall when I used more than 8 channels simultaneously, but sure many would need it. For the test on AD3, I used one example where I connected an Arduino to the  I2C of the dev board I have. The board setup is as follows:

  • DIO0: I2C Clock
  • DIO1: I2C SDA
  • DIO2: GPIO7 on Arduino

Note: I also made a test on I2C reading at the 1V8 domain, I made it to test if it would read this I/O level as the website states 3V3 CMOS. I'm surprised that it read correctly, although 1V8 LVCMOS support was removed in hardware)

image

Here is the Waveforms capture for this setup:

Protocol

Filtering

This use case I'm going to describe here is an example from when I had to design a filter in my master's for the Analog Circuit Design discipline. I think this is an interesting example because it shows that the network tool is great for verifying a filter design and also for providing a quick graphical view of the results obtained in hardware. The task was the design of a fourth-order band-pass Butterworth filter. The topology is as follows :

image

Here we have two cascaded second-order filters in Multifeedback topology. if you wanna the details about calculation and theory, it's in chapter 16 Op Amps For Everyone. The design was made for the following parameters:

Filter type

Gain at mid-frequency [dB]

lower cutoff frequency (fl)

upper cutoff frequency(fu)

Butterworth

20

6000Hz

7000Hz

values for the resistors are (in Ohms):

  • R2 = 47000
  • R1 = 5100
  • R3 = 130
  • R4 = 43000
  • R5 = 4700
  • R6 = 120

Here is what the circuit looks like:

Network Instrument

Using the Waveforms Software, the task to generate the bode plot was simple:

  • switch the power on
  • select my frequency range
  • select a suitable amplitude for the Wavegen ( be careful with saturation ) 
  • and click Single to swipe once

The image below is a screenshot from the measurements I took, on the -3dB Cutt-off frequencies ( lower and upper ) and also mid-frequency. The software offers an exporter which allows the inclusion of the Serial Number and time as well in the image or export the data as a CSV for further processing.

image

The data exported contains also all the information about the measurement taken and the configuration of the device, facilitating traceability.

image

Scope Instrument

Transient Response

To perform this task:

  • configured the trigger to the output which was channel 2
  • set trigger level to around 400mV ( I just checked my steady state for that frequency)
  • configured the scope to single shot
  • then I clicked the run button over the tab of Wavegen

Here is a screenshot of the transient of the circuit:

image

Step response

To see the step response, I just configured the Wavegen to generate a pulse. As can be seen in the video:

image

FFT

The fact that I can choose the window applied in the FFT is a plus and for learning purposes of DSP, this is quite interesting. In the video, channel 1 is still the input signal ( from Wavegen), and channel 2 is the output of my circuit. When opening the FFT Tab, it's visible the spectrum of the triangle wave in the input and then its attenuated components at the output. Also, it's visible the effect of the window method chosen in the analyzes. 

image

Scripting with python

As part of its tools, Digilent provides the SDK to allow us to use the instruments in the  AD3 in the most flexible way possible through scripts. My interest lies in the possibility of automation of tests and tasks using Python, therefore it is the language I chose for the demos and my attempts. As part of my setup, I already had a Python 3.11.5 previously installed, so didn't need to install it.

To start, I located the examples in the installation directory of the Waveforms Software. It should be something like C:\Program Files (x86)\Digilent\WaveFormsSDK\samples. The figure below shows directories also for other languages, like C  and CS.

waveforms installation directory

In the py folder are many examples of how to use AD3 ( and other instruments) with Python. Opening it, I located the DigitalOut_Pins.py script.

 

The instrument used in this example, the Pattern Generator, is described in Chapter 10 of the WaveForms SDK Reference Manual (Revised October 12, 2023) where also the structure of the instrument is shown as a block diagram:

image

Here is the code that will configure the elements of the instrument in the block diagram:

"""
   DWF Python Example
   Author:  Digilent, Inc.
   Revision:  2020-04-07

   Requires:                       
       Python 2.7, 3
"""

from ctypes import *
from dwfconstants import *
import math
import time
import sys

if sys.platform.startswith("win"):
    dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
    dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
    dwf = cdll.LoadLibrary("libdwf.so")

version = create_string_buffer(16)
dwf.FDwfGetVersion(version)
print("DWF Version: "+str(version.value))

print("Opening first device")
hdwf = c_int()
dwf.FDwfDeviceOpen(c_int(-1), byref(hdwf))

if hdwf.value == 0:
    print("failed to open device")
    szerr = create_string_buffer(512)
    dwf.FDwfGetLastErrorMsg(szerr)
    print(str(szerr.value))
    quit()

# the device will only be configured when FDwf###Configure is called
dwf.FDwfDeviceAutoConfigureSet(hdwf, c_int(0)) 

hzSys = c_double()
dwf.FDwfDigitalOutInternalClockInfo(hdwf, byref(hzSys))

# 1kHz pulse on IO pin 0
dwf.FDwfDigitalOutEnableSet(hdwf, c_int(0), c_int(1))
# prescaler to 2kHz, SystemFrequency/1kHz/2
dwf.FDwfDigitalOutDividerSet(hdwf, c_int(0), c_int(int(hzSys.value/1e3/2)))
# 1 tick low, 1 tick high
dwf.FDwfDigitalOutCounterSet(hdwf, c_int(0), c_int(1), c_int(1))

# 1kHz 25% duty pulse on IO pin 1
dwf.FDwfDigitalOutEnableSet(hdwf, c_int(1), c_int(1))
# prescaler to 4kHz SystemFrequency/1kHz/2
dwf.FDwfDigitalOutDividerSet(hdwf, c_int(1), c_int(int(hzSys.value/1e3/4)))
# 3 ticks low, 1 tick high
dwf.FDwfDigitalOutCounterSet(hdwf, c_int(1), c_int(3), c_int(1))

# 2kHz random on IO pin 2
dwf.FDwfDigitalOutEnableSet(hdwf, c_int(2), c_int(1))
dwf.FDwfDigitalOutTypeSet(hdwf, c_int(2), DwfDigitalOutTypeRandom)
dwf.FDwfDigitalOutDividerSet(hdwf, c_int(2), c_int(int(hzSys.value/2e3)))
dwf.FDwfDigitalOutCounterSet(hdwf, c_int(2), c_int(1), c_int(1))

rgdSamples = (c_byte*6)(*[0xFF,0x80,0xC0,0xE0,0xF0,0x00])
# 10kHz sample rate custom on IO pin 3
dwf.FDwfDigitalOutEnableSet(hdwf, c_int(3), 1)
dwf.FDwfDigitalOutTypeSet(hdwf, c_int(3), DwfDigitalOutTypeCustom)
dwf.FDwfDigitalOutDividerSet(hdwf, c_int(3), c_int(int(hzSys.value/1e4)))
dwf.FDwfDigitalOutDataSet(hdwf, c_int(3), byref(rgdSamples), c_int(6*8))

dwf.FDwfDigitalOutConfigure(hdwf, c_int(1))

print("Generating output for 10 seconds...")
time.sleep(10)

dwf.FDwfDigitalOutReset(hdwf)
dwf.FDwfDeviceCloseAll()

Then open and run it from inside the VS Code (It could be from pure command line or any other flavor, it's just my editor of choice) show the message in the terminal as below:

To make sure, it was all working as the code stated, I attached my scope and got the result as expected:

  1. Channel 1 - 1kHz 50%duty cycle ( voltage level is 3V6)
  2. Channel 2 - 1kHz 25%duty cycle
  3. Channel 3 -  Random pattern
  4. Channel 4 - Custom pattern

image

Example measurement of the channel 1:

image

Depicting the Code 

To get the gist of the code working, it was necessary to get my hands on the API documentation since I cannot ctrl+click on the functions as they are loaded from a DLL, further details about this are in the SDK documentation ( ..path_to_installation\Digilent\WaveFormsSDK ). A first look at the code example I showed might be daunting if one is not used to Python, but all that needs to be remembered is that here we are interfacing with a C library loaded into Python, and therefore due to compatibility the ctype module is used everywhere. 

So I would point out a few things in the code. The first is that here it loads the Dynamic Library according to the OS you are using.

if sys.platform.startswith("win"):
    dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
    dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
    dwf = cdll.LoadLibrary("libdwf.so")

Then, as per documentation, -1 is passed as a parameter, and therefore the first device discovered will be enumerated and open. Note that c_int() and byref() functions are used, the first is for compatibility of integer type and the second is the same as passing a pointer in C, it's just grammar.

print("Opening first device")
hdwf = c_int()
dwf.FDwfDeviceOpen(c_int(-1), byref(hdwf))

If the device is successfully opened, the rest of the code will start running.

Now, the following line disables the AutoConfigure. When it is enabled imagine that instead of drinking as much water as you want from a bottle, you have to close it and open it after every sip. In the example code, it would be like calling the line dwf.FDwfDigitalOutConfigure(hdwf, c_int(1)) after every ...Set() function.

# the device will only be configured when FDwf###Configure is called
dwf.FDwfDeviceAutoConfigureSet(hdwf, c_int(0)) 

image

Going on the lines below show the generation of the channel 1 pattern:

  1. Enables the Output
  2. Set the divider   ( I'm glad I had a numerical example because there is none in the SDK document) 
  3. Configure how many counts should be low and high, and therefore with both 1, a square can be generated

# 1kHz pulse on IO pin 0
dwf.FDwfDigitalOutEnableSet(hdwf, c_int(0), c_int(1))
# prescaler to 2kHz, SystemFrequency/1kHz/2
dwf.FDwfDigitalOutDividerSet(hdwf, c_int(0), c_int(int(hzSys.value/1e3/2)))
# 1 tick low, 1 tick high
dwf.FDwfDigitalOutCounterSet(hdwf, c_int(0), c_int(1), c_int(1))

image

image

image

Likewise, the other channels were configured. The difference is that the type of output is configured with 

dwf.FDwfDigitalOutTypeSet(hdwf, c_int(2), DwfDigitalOutTypeRandom)

which means that the DwfDigitalOutTypePulse is the default if not configured.

image

Finally, all the changes are applied at once and the patterns are generated:

dwf.FDwfDigitalOutConfigure(hdwf, c_int(1))

image

Seems that using the examples and the documentation, it doesn't take much time to understand how to use the SDK. Next, I describe how I tried a small design more custom-based on the I/O instrument

Making a semaphore 

With the example in hand, I tried to make a simple application just to check If I can make something run on my own. This time I didn't want to use the pattern generator, instead I just wanted the I/O capability to make a semaphore. First, trying to run the script, I had the following message which was an error because I tried to run my script from outside the example folder: 

All I needed to do was to copy dwfcontants to the folder I was running. The code was based on the DigitalIO.py. all I did was create a while loop with a time.sleep() call to give a delay in each IO. The pins are connected as follows:

  • DIO0 - Red LED
  • DIO1 - Yellow LED
  • DIO2 - Green LED

The modified code follows:

"""
   Traffic Light example
"""

from ctypes import *
from dwfconstants import *
import math
import time
import sys


if sys.platform.startswith("win"):
    dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
    dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
    dwf = cdll.LoadLibrary("libdwf.so")

version = create_string_buffer(16)
dwf.FDwfGetVersion(version)
print("DWF Version: "+str(version.value))

print("Opening device")
hdwf = c_int()
dwf.FDwfDeviceOpen(c_int(-1), byref(hdwf))

if hdwf.value == 0:
    print("failed to open device")
    szerr = create_string_buffer(512)
    dwf.FDwfGetLastErrorMsg(szerr)
    print(str(szerr.value))
    quit()


# enable output/mask on 8 LSB IO pins, from DIO 0 to 7
dwf.FDwfDigitalIOOutputEnableSet(hdwf, c_int(0x00FF)) 
# set value on enabled IO pins
dwf.FDwfDigitalIOOutputSet(hdwf, c_int(0x07)) 

while True:
    dwf.FDwfDigitalIOOutputSet(hdwf, c_int(0x04)) 
    time.sleep(1)
    dwf.FDwfDigitalIOOutputSet(hdwf, c_int(0x02)) 
    time.sleep(1)
    dwf.FDwfDigitalIOOutputSet(hdwf, c_int(0x01))
    time.sleep(1)    

To increment the example a bit more, I added a button, so I needed:

  • an Input - I chose the DIO3;
  • and power supply for the pull-up of the button.

The way to access the power supply, I  found in the AnalogIO_AnalogDiscovery3_Power.py. Now the logic of the program is 

  • The I/O and the power are configured;
  • The light red is always on
  • When the button is pressed
    • The traffic light shall go to the green

Finally, I wanted to add a buzzer for when the light goes green and then turn off when the light goes red again. I thought about trying the wavegen, but from the previous example, I could just use the pattern generator as the code was already ready. 

The modified code follows:

"""
   Traffic Light + Button + Buzzer example
"""

from ctypes import *
from dwfconstants import *
import math
import time
import sys


def initialize_power_supply ():
    # set up analog IO channel nodes
    # enable positive supply
    dwf.FDwfAnalogIOChannelNodeSet(hdwf, c_int(0), c_int(0), c_double(1)) 
    # set voltage to 5 V
    dwf.FDwfAnalogIOChannelNodeSet(hdwf, c_int(0), c_int(1), c_double(5.0)) 
    # enable negative supply
    dwf.FDwfAnalogIOChannelNodeSet(hdwf, c_int(1), c_int(0), c_double(1)) 
    # set voltage to -5 V
    dwf.FDwfAnalogIOChannelNodeSet(hdwf, c_int(1), c_int(1), c_double(-5.0)) 
    # master enable
    dwf.FDwfAnalogIOEnableSet(hdwf, c_int(1))

def initialize_buzzer ():
    rgdSamples = (c_byte*10)(*[0x00, 0xff, 0x05, 0x05, 0xff, 0x01, 0x01, 0xff, 0x00, 0x00])
    # 10kHz sample rate custom on IO pin 3
    dwf.FDwfDigitalOutEnableSet(hdwf, c_int(5), 0)
    dwf.FDwfDigitalOutTypeSet(hdwf, c_int(5), DwfDigitalOutTypeCustom)
    dwf.FDwfDigitalOutDividerSet(hdwf, c_int(5), c_int(int(hzSys.value/1e3/8)))
    dwf.FDwfDigitalOutDataSet(hdwf, c_int(5), byref(rgdSamples), c_int(10*8))
    dwf.FDwfDigitalOutConfigure(hdwf, c_int(1))
    

if sys.platform.startswith("win"):
    dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
    dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
    dwf = cdll.LoadLibrary("libdwf.so")

version = create_string_buffer(16)
dwf.FDwfGetVersion(version)
print("DWF Version: "+str(version.value))

print("Opening device")
hdwf = c_int()
dwRead = c_uint32()

dwf.FDwfDeviceOpen(c_int(-1), byref(hdwf))

if hdwf.value == 0:
    print("failed to open device")
    szerr = create_string_buffer(512)
    dwf.FDwfGetLastErrorMsg(szerr)
    print(str(szerr.value))
    quit()

hzSys = c_double()
dwf.FDwfDigitalOutInternalClockInfo(hdwf, byref(hzSys))
print(str(hzSys.value/1e6)+" MHz")

initialize_power_supply()
initialize_buzzer()

# enable output/mask on 8 LSB IO pins, from DIO 0 to 7
dwf.FDwfDigitalIOOutputEnableSet(hdwf, c_int(0x0007)) 
# set value on enabled IO pins
dwf.FDwfDigitalIOOutputSet(hdwf, c_int(0x07)) 

while True:

    # Read input on IO3
    value = c_int()
    dwf.FDwfDigitalIOInputStatus(hdwf, byref(dwRead))

    dwf.FDwfDigitalIOOutputSet(hdwf, c_int(0x01)) 
    
    #if Button is pressed on DIO3
    if not ( (dwRead.value>>3)& 1):

        time.sleep(1)
        dwf.FDwfDigitalIOOutputSet(hdwf, c_int(0x02)) 
        time.sleep(1)

        dwf.FDwfDigitalOutEnableSet(hdwf, c_int(5), 1)
        dwf.FDwfDigitalOutConfigure(hdwf, c_int(1))

        dwf.FDwfDigitalIOOutputSet(hdwf, c_int(0x04))
        time.sleep(3)
        dwf.FDwfDigitalIOOutputSet(hdwf, c_int(0x02)) 
        time.sleep(1)    

        dwf.FDwfDigitalOutEnableSet(hdwf, c_int(5), 0)
        dwf.FDwfDigitalOutConfigure(hdwf, c_int(1))



I2C Temperature Sensor

In this part, I modified the Digital_I2C.py to get running a reader and parser for the LM75A temperature sensor from NXP. My choice was because I had the sensor easily accessible on a development board I have. Also, the sensor is pretty simple, and just to read the temperature, one needs simply to send via I2C the address of the temperature register (0x00) and then read two consecutive bytes and parse the temperature.

image

My setup looks like this:

image

Before writing any code, I used the protocol instrument through the Waveforms Software to make sure that all connections were fine and that the sensor could be found. To find the sensor I used the script example Find I2C Devices that is written in  javascript (another option to use scripts). From there I could get the device address without the need to check the sch that I have no idea where it is.

image

Then I moved to try to read the register of the temperature manually in the master tab.

image

although very useful for quick tests the disadvantage of this approach is that it is too manual work and if I want to dump several registers or read a parsed value it would be time-consuming and boring. Therefore I Modified the example Digital_I2C.py and in a few minutes I could have the code running properly.

"""
   DWF Python Example
   Author:  Digilent, Inc.
   Revision:  2018-07-23

   Requires:                       
       Python 2.7, 3
"""

from ctypes import *
import math
import sys
import time
import struct

if sys.platform.startswith("win"):
    dwf = cdll.LoadLibrary("dwf.dll")
elif sys.platform.startswith("darwin"):
    dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
    dwf = cdll.LoadLibrary("libdwf.so")

hdwf = c_int()

print("Opening first device")
#dwf.FDwfDeviceOpen(c_int(-1), byref(hdwf))
# device configuration of index 3 (4th) for Analog Discovery has 16kS digital-in/out buffer
dwf.FDwfDeviceConfigOpen(c_int(-1), c_int(3), byref(hdwf)) 

if hdwf.value == 0:
    print("failed to open device")
    szerr = create_string_buffer(512)
    dwf.FDwfGetLastErrorMsg(szerr)
    print(str(szerr.value))
    quit()

print("Configuring I2C...")

iNak = c_int()

dwf.FDwfDigitalI2cRateSet(hdwf, c_double(1e5)) # 100kHz
dwf.FDwfDigitalI2cSclSet(hdwf, c_int(0)) # SCL = DIO-0
dwf.FDwfDigitalI2cSdaSet(hdwf, c_int(1)) # SDA = DIO-1
dwf.FDwfDigitalI2cClear(hdwf, byref(iNak))
if iNak.value == 0:
    print("I2C bus error. Check the pull-ups.")
    quit()
time.sleep(1)

rgTX = (c_ubyte*1)(0)
rgRX = (c_ubyte*2)()
while True:
    #print("Write and Read with reStart:")
    dwf.FDwfDigitalI2cWriteRead(hdwf, c_int(0x48<<1), rgTX, c_int(1), rgRX, c_int(2), byref(iNak)) # write 1 byte restart and read 16 bytes
    if iNak.value != 0:
        print("NAK "+str(iNak.value))

    print(list(rgRX))

    # Convert temperature data to Celsius
    raw_temperature = struct.unpack('>h', bytes(rgRX))[0]
    temperature_celsius = raw_temperature / 256.0
    print(temperature_celsius)
    time.sleep(1)

dwf.FDwfDeviceCloseAll()

Little Struggle  with I2C

When I tried to get the I2C running from the script, I tried first to use the modules in Github as they offer a friendly API that runs the basic functions under the hood. for instance, the API for I2C would look like:

i2c.open(device_data, sda=0, scl=1)
 message, error = i2c.read(device_data, 2, TMP2_address)   # read 2 bytes

We see that the code would be better encapsulated and simpler. The excerpt was taken from here. What went wrong was that I could not manage to get rid of this error, although I checked the connections and power and everything with the Waveforms as I mentioned before. I posted the issue on the forum and soon they will certainly reply. link to issue  

image

Discussion on script

The AD3 has I believe most of the capabilities I used in my daily work. The SDK expands its value, I can see its applicability in automating several tasks, like when sensors need to be checked for certain conditions, for triggering the scope when a specific complex occurrence happens in the system.  Also, the rapid development time allows for fast prototypes. I remember once I wrote a Python script for checking the commands in a serial protocol with a UART-USB converter, which was fine for this test, but with an AD3 I could certainly test also if the output of the commands was as expected. So, a lot can be done and I think that the existence of an active forum adds points to the products. Lastly, one "standardized" tool like this makes it easier to share code and documentation when used professionally. 

Resource 

https://digilent.com/blog/whats-different-with-the-analog-discovery-3/

https://digilent.com/reference/test-and-measurement/guides/waveforms-sdk-getting-started

https://digilent.com/reference/test-and-measurement/analog-discovery-3/reference-manual

https://github.com/Digilent/WaveForms-SDK-Getting-Started-PY

https://web.mit.edu/6.101/www/reference/op_amps_everyone.pdf

Anonymous
  • Immediately after completing my AD3 project and sending the report, I carefully read the co-roadtesters studies.
    As with Dougw's, your roadtest is impressive! I truly enjoyed it!
    This is what a great roadtest should look like.
    Same as in case of comparison of my work with Dougw's, now reading yours I feel embarrassed, my project was more modest and much less inquisitive, now I know who to learn from ;)
    Great review, extreamly informative. Congratulations and thank you!