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:
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.
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:
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)
Here is the Waveforms capture for this setup:
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 :
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):
Here is what the circuit looks like:
Using the Waveforms Software, the task to generate the bode plot was simple:
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.
The data exported contains also all the information about the measurement taken and the configuration of the device, facilitating traceability.
To perform this task:
Here is a screenshot of the transient of the circuit:
To see the step response, I just configured the Wavegen to generate a pulse. As can be seen in the video:
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.
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.
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:
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:
Example measurement of the channel 1:
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))
Going on the lines below show the generation of the channel 1 pattern:
# 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))
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.
Finally, all the changes are applied at once and the patterns are generated:
dwf.FDwfDigitalOutConfigure(hdwf, c_int(1))
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
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:
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:
The way to access the power supply, I found in the AnalogIO_AnalogDiscovery3_Power.py. Now the logic of the program is
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))
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.
My setup looks like this:
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.
Then I moved to try to read the register of the temperature manually in the master tab.
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()
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
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.
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