Table of Contents
Introduction
I recently received a brand new Analog Discovery 3 (AD3) from Digilent. I had heard of the Analog Discovery 2 before, but I never had the opportunity to use it. At first, I was a little reluctant to use it as I thought it was a simple gadget and that a benchtop scope would be better for almost all the available functionnalities.
Now that I have the AD3 at home, I can do some tests and see if my initial opinion was right or wrong.
Obviously, I had not the opportunity to try all the functionnalities offered by the AD3. I decided to try just a few of them for now. I preferred to test functionnalities with customizable aspects because I thought it would be more fun and that it would allow me to better discover the advantages of the AD3 over traditionnal benchtop scope.
The three tests are:
- Eye-diagram plotting using a custom script to control the signal generation and data acquisition.
- Logic analyzer with a custom script-based decoder to decode a chip-specific protocol.
- Constellation diagram using X-Y plot with persistence (no custom script here).
Note: the tests explained here are available on GitHub on the following link: https://github.com/martingu4/AD3_Intro/tree/main. I am available for any question or if a file/project does not work the intended way.
1. First test: eye diagram
1.1. Test introduction
1.2. Using the Waveforms scripting capability
The Waveforms software that allows the user to use the Analog Discovery 3 (among other Digilent devices) offers a scripting capability. This means the user can write a JavaScript code that can be executed to perform several operations, such as: configure scope, AWG or other tools, start/stop acquisition, create custom signals and more.
This capability can be very powerful to create several setups that can be switched from one to another whenever needed or in order to create test sequences for example.
The script that I have written for this test is given in the following box.
// This script will build a custom signal.
// The signal is trapezoidal. Parameters allow the user to
// change the rise/fall times and the noise level.
////////////////////////////////////////////////////////////////
// Script parameters
////////////////////////////////////////////////////////////////
transitionTime = 60 // The transitionTime must be between 0 and 100
noiseLevel = 100 // The noiseLevel must be between 0 and 100
////////////////////////////////////////////////////////////////
// Misc. initializations
////////////////////////////////////////////////////////////////
clear()
// Test if scope and wavegen are opened
if(!('Wavegen' in this) || !('Scope' in this))
throw "Please open a Scope and a Wavegen instrument";
////////////////////////////////////////////////////////////////
// Scope configuration
////////////////////////////////////////////////////////////////
Scope1.Channel2.setDisable() // Disable channel 2
Scope1.Channel1.Offset.value = 0 // Channel 1 offset to 0V
Scope1.Channel1.Range.value = 5 // Channel 1 range to 500mV/div
Scope1.Time.Base.value = 0.002 // Timescale to 200us/div
if(Scope1.window.eye == undefined) // If the eye window is in undefined state, enable it
Scope1.window.toggleEye()
Scope1.Eye.Lock.value = 1
Scope1.Eye.Rate.value = 5000 // Eye diagram rate to 5kHz
print("Scope 1 configured.")
////////////////////////////////////////////////////////////////
// Custom signal creation
////////////////////////////////////////////////////////////////
// Create the signal array
level = -1
wave = Array()
for(var i = 0; i < 80; i++)
{
precLevel = level
// Set the level for the current symbol (-1 or 1)
if(i < 79)
{
// Randomly pick a level
level = random() > 0.5 ? 1 : -1
}
else
{
// Always low for last symbol because the first one always starts with a rising edge
level = -1
}
// Transition between two symbols
randTransitionTime = transitionTime + (random() - 0.5)*40
incr = (level - precLevel) / randTransitionTime
val = precLevel
for(var j = 0; j < randTransitionTime; j++)
{
val = val + incr
wave.push(addNoise(val, noiseLevel))
}
for(var j = 0; j < 200-randTransitionTime; j++)
{
wave.push(addNoise(level, noiseLevel))
}
}
////////////////////////////////////////////////////////////////
// Custom signal output on Wavegen
////////////////////////////////////////////////////////////////
Wavegen1.Channel1.Mode.text = "Eye_Diagram_Test"
Wavegen1.Custom.set("Eye_Diagram_Test", wave)
print("Wavegen 1 signal built and set.")
////////////////////////////////////////////////////////////////
// Run the Wavegen and the Scope
////////////////////////////////////////////////////////////////
Wavegen1.Channel1.run()
Scope1.run()
print("Wavegen and Scope running. Script is finished.")
////////////////////////////////////////////////////////////////
// Add some noise to a sample
////////////////////////////////////////////////////////////////
function addNoise(sample, noiseLevel)
{
// Random number from -0.1 to 0.1
noise = (random() - 0.5) / 5
// Scale the noise from noiseLevel
noise = noise * noiseLevel / 100
// Scale the input sample value so we do not have an overflow
scaledSample = sample * 0.9
return scaledSample + noise
}
// EOF
Unfortunately, the documentation about this scripting capability is very poor. In order to write the script above, I had to use features not included in the documentation. The good thing is that Digilent included two useful tools to their software in order to allow anybody to write scripts (those two tools are shown in the "help" inside the Waveforms software):
- The script editor has code completion. It allows you to see what methods are available for a given object and therefore find what you are looking for.
- Whenever you hover your mouse above an element that can be used in a script (for example, the time position value of a scope), the access name is written on the bottom-left corner of the window. For the previous example, the window will print "Script: Scope1.Time.Position.text/value".
In my opinion, those two tools are really good features but those are not worth a well-written documentation. For example, you can identify a function to access a field you wanted to change, but you do not know the allowed range or the format of the data you need to give to the field.
1.3. Test results
The image gallery bellow shows the results from the three windows: script console, waveform generator and scope. For each image, we can focus on:
- Console: this simple result highlights the fact that the script can not only control all the tools of the AD3 but also inform the user about warnings, errors or information messages related to the script's execution. This feature is helpful so the user can quickly know if the script was successful or not. In our case, the script starts by checking if a waveform and a scope are opened and, if not, a message error is displayed and the script is stopped.
- Waveform: this result highlights the fact that our custom signal generated by the script was successfuly set to the waveform generator and that the output is running.
- Scope: the last result is the more important because it shows the custom signal after being measured by the scope. The most important part is on the bottom part of the screen with the eye diagram. We can see that not only the eye is plotted but also some useful measurements (SNR, amplitude, jitter, etc.). Some metrics are displayed on the signal such as the statistical distribution of the noise.
{gallery}Eye Diagram test results |
---|
Test result in the console: Info/error/warning messages are prompted into the Waveforms console after the script's execution. |
Test result in the Waveform window: The randomly created signal was successfuly set and the waveform generator is running. |
Test result in the Scope window: The top part of the window is showing the signal and the bottom part is showing the measured eye diagram. |
By playing with the two script's parameters, one can change the SNR and the rise/fall times. The jitter can also be changed in the script (line 56). In the given version of the script, the jitter is random but it could quite easily be set by a parameter like it is done for the noise level and transitions times.
The results obtained for this test are good and allowed us to:
- Write and run a script that controls several aspects of the AD3 (wafeform generator, scope, eye diagram).
- Output a signal from the waveform generator.
- Measure a signal using the scope.
- Plot an eye diagram.
1.4. How to run this test yourself?
First, you need to setup the device:
- Have an Analog Discovery 3.
- Connect the scope's channel 1 positive pin to the waveform generator's channel 1 output.
- Connect the scope's channel 1 negative pin to the AD3 ground.
- Plug the AD3 to your computer.
- Launch the Waveforms software.
Once this is done, you need to execute the script and there are two ways to do so. You can clone the repository (link in the introduction) or you can create a new workspace yourself.
If you chose to clone the repository, this is very easy:
- Clone the repository.
- Open the workspace in the "Eye_Diagram" directory.
- Make sure a waveform and a scope windows are opened.
- Run the script.
If you chose to create a workspace yourself, you have to:
- Create the workspace.
- Open a waveform window and a scope window.
- Create a new script.
- Copy/paste the given script into the script window.
- Run the script.
If you face any issue, just contact me in the comments or with a private message on Element14 community.
2. Second test: logic analyzer
2.1. Test introduction
For the second test, I wanted to try the logic analyzer capability. I had previously made a RoadTest about the Eclypse Z7 (FPGA based development board from Digilent) and I wanted to decode data from the Eclypse Z7 using the AD3 logic analyzer (here is the link to the RoadTest Eclypse Z7 with AWG and Digitizer Zmods: making a 4-QAM MODEM). I was suggested by a contact I had with someone from Digilent to decode the calibration data the Eclypse Z7 board sends to the digitizer module (a module including two ADCs). This calibration data is sent on a SPI bus (SCLK, CS, SDIO) and could be decoded with the logic analyzer using the SPI preset.
This idea is good but it requires to create a custom Vivado project to forward the data to another pin of the FPGA. Unfortunately, the I/O buffer responsible for the control of the SDIO signal is instanciated inside the Digitizer Controller IP Core. I therefore cannot connect another pin to the signal outside of the IP Core. I had to find a workaround and create a tcl script that connects another output buffer to the net inside the IP Core. This way, we are able to output the SDIO signal to a PMOD pin and send this signal to the AD3. A simplified view is showed in the next image.
The good thing about this workaround is that we will be able to see both of the devices communication (we get the signal from the ADC and the one generated by the FPGA), just as if we had access to the signal on the PCB between the two chips.
2.2. First results
When we connect the Eclypse Z7 with the AD3 and turn on the Eclypse, the ADC configuration phase starts and we can see SPI packets being sent. We obtain the following results on Waveforms:
{gallery}First result of serial analyzer |
---|
SPI clock measurement: SPI clock frequency measurement for communication between Eclypse Z7 and Digitizer Zmod. |
SPI data decoding: This image shows more data. |
The first image shows that we have a clock frequency of 6.25MHz. On the second image, we can see the decoded data with packets of three bytes at a time.
2.3. Create a custom decoder
The results we got at the previous section are good. The SPI packets are decoded and the data is readable. But the AD3 allows the user to go deeper into serial decoding by creating a custom decoder. It consists of two scripts: "Decoder" and "Value to text" which are described bellow:
- Decoder: this first script manages all the decoding steps. It takes the raw data as input and feeds "Value to text" with two output vectors: values and flags. The purpose of this script is to analyze the signals to be decoded (in our case: CS, SCK and SDIO) and create two vectors, the first one contains all the decoded data and the second one contains flags.
- Value to text: this second script gets the data and flags values from the previous one and translates it into text to be displayed on the Waveforms serial analyzer window.
The documentation provided by Digilent regarding the custom decoders is quite poor. When I wanted to create my own custom decoder, my best friend was the example code provided in the Waveforms software. The "Decoder" + "Value to text" logic needs a bit of time to be understood and is a bit tricky to explain. The best way to create a custom decoder is to start with the example code and try to change things yourself.
In order to implement my custom decoder, I decided to use a simple FSM. It eases the code development and the handling of errors. The following diagram shows the three states of this FSM.
Both decoder scripts are also given bellow.
// rgData: input, raw digital sample array // rgValue: output, decoded data array // rgFlag: output, decoded flag array var rec_val = 0; // Receive value buffer var Rcv_state = 0; // Receive state-machine state var Sclk_prec = 0; // Previous clock state var bits_cnt = 0; // Bits counter var payload_length; // The data payload has variable length specified in the INSTR 16-bits word // Variables for rgValue and rgFlag management var start_Idx = 0; var len = 0; // Loop on all samples for(var i = 0; i < rgData.length; i++) { // By default, set all samples to IDLE. // Values will be changed for samples not in IDLE rgValue[i] = 0; rgFlag[i] = 0; // Increment the amount of samples for the current state // This value will be used to write the output values and // flags for the whole state at once. len = len + 1; /////////////////////////////////////////////// // Extract the signals from pins 0, 1 and 2 /////////////////////////////////////////////// var CS_val = 1 & (rgData[i] >> 0); var Sclk_val = 1 & (rgData[i] >> 1); var data = 1 & (rgData[i] >> 2); /////////////////////////////////////////////// // Switch on the current receiver state. // The state machine handles the message reception // phases. /////////////////////////////////////////////// switch(Rcv_state) { // IDLE state: CS is inactive case 0: // CS active: go to INSTR state if(CS_val == 0) { // Go to INSTR state Rcv_state = 1; // Clear the receive buffer bits_cnt = 0; rec_val = 0; // Set start index and length variables for next state start_Idx = i; len = 0; } break; // INSTR state: 16-bits instruction is being received case 1: // Data bits are sampled on clock rising-edges if(Sclk_prec == 0 && Sclk_val != 0) { // Increment the bits counter bits_cnt++; // Insert the new bit rec_val <<= 1; rec_val |= data; } // Check for the end of INSTR phase (16-bits received) // This check is performed on falling-edge if(Sclk_prec != 0 && Sclk_val == 0) { if(bits_cnt == 16) { // Insert the INSTR flag with the data for(var j = 0; j < len; j++) { rgFlag[start_Idx+j] = 1; rgValue[start_Idx+j] = rec_val; } // Extract the payload length from the INSTR word payload_length = ((rec_val >> 13) & 0x03) + 1; if(payload_length == 0) { // Go to IDLE state Rcv_state = 0; start_Idx = i; len = 0; } else { // Reset the receive buffer bits_cnt = 0; rec_val = 0; // Go to DATA state Rcv_state = 2; start_Idx = i; len = 0; } } } // CS is inactive before end of instruction: ERROR // (don't check for clock here because when CS is de-asserted, // the clock signal is disabled) if(CS_val == 1) { // Insert error flags starting from the beginning of INSTR phase for(var j = 0; j < len; j++) rgFlag[start_Idx+j] = 3; // Go to IDLE state Rcv_state = 0; // Set start index and length variables for next state start_Idx = i; len = 0; } break; // DATA state: x-bytes data is being received case 2: // Data bits are sampled on clock rising-edges if(Sclk_prec == 0 && Sclk_val != 0) { // Increment the bits counter bits_cnt++; // Insert the new bit rec_val <<= 1; rec_val |= data; } // When CS is de-asserted, check for the amount of bits received if(CS_val == 1) { // We received the correct amount of bits: no error if(bits_cnt == (8*payload_length)) { // Insert the DATA flag with the data for(var j = 0; j < len; j++) { rgFlag[start_Idx+j] = 2; rgValue[start_Idx+j] = rec_val; } // Go to IDLE state Rcv_state = 0; start_Idx = i; len = 0; } else // Bad amount of bits received: ERROR { // Insert error flags starting from the beginning of DATA phase for(var j = 0; j < len; j++) rgFlag[start_Idx+j] = 4; // Go to IDLE state Rcv_state = 0; // Set start index and length variables for next state start_Idx = i; len = 0; } } break; default: break; } // Store previous clock value Sclk_prec = Sclk_val; }
// value: value sample // flag: flag sample function Value2Text(flag, value) { switch(flag) { // IDLE flag case 0: return "X"; // INSTR flag, print R/W, W1|W0 and address case 1: // Decode the R/W bit var RW_str; if((value & 0x8000) == 0) RW_str = "W"; else RW_str = "R"; // Decode the word length field var WL_str = (((value >> 13) & 0x03) + 1).toString(10); // Extract the address var Addr_str = "0x" + (value & 0x1FFF).toString(16).toUpperCase(); return RW_str + " " + WL_str + " byte(s) at " + Addr_str; // DATA flag, print the data payload case 2: return "D=0x" + value.toString(16).toUpperCase(); // ERROR: CS de-asserted in the middle of INSTR phase case 3: return "ERROR: INSTR not fully received."; // ERROR: CS de-asserted when invalid amount of data bits were received case 4: return "ERROR: Invalid amount of data bits."; // The default case should never happen default: return "Unhandled case (val = " + value.toString(10).toUpperCase() + ")"; } }
2.4. Custom decoder result
The result obtained with our custom decoder is shown in the image bellow. We have a comparison between the native SPI decoder and the custom one that decodes the protocol specific to our ADC (AD9648, link to the datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/ad9648.pdf).
Note: we can see on the logic analyzer window that this screenshot was taken in "Demo mode". That means that no AD3 was connected to my computer at the time. But the good thing is that Waveforms keeps the last captured data in the workspace. This feature allows you to create your decoder without being forced to recapture data everytime. You just change the code of your custom decoder and the changes will be applied on the saved data. This is a good thing because the data I had to decode was only sent at the Eclypse Z7 startup and it can be quite boring to power cycle the FPGA board each time I changed the decoder's script (especially when this is your first decoder script, this is not successful at first try).
I compared the first packets with the AD9648 datasheet to see what was done by the Eclypse on the ADC. The first write transaction performs a soft reset. It is followed by a read transaction where the soft reset bit is reset meaning the reset was performed (see https://www.analog.com/media/en/technical-documentation/application-notes/AN-877.pdf?doc=AD9648.pdf for details).
Another transaction that shows our decoder works is the third one. It is a read transaction at address 0x01 (chip ID register). The AD9648 datasheet states that the ID of the chip is 0x88. This is what was read in this example, cool!
3. Third test: constellation diagram
2.1. Test introduction
This third test is the most basic one. It only uses "vanilla" functionnalities from the Waveforms software, no custom script had to be written for the AD3/Waveforms part. However, it requires the Eclypse Z7 board since the aim is to measure the constellation diagram of a 4-QAM modulated signal generated by the FPGA inside the Eclypse Z7. I designed the project that generates the 4-QAM modulated signal for a roadtest I made last year (link: /products/roadtest/rv/roadtest_reviews/1737/eclypse_z7_with_awg_and_digitizer_zmods).
2.2. X-Y plots using Waveforms
The Eclypse Z7 outputs the I-Q signals from the 4-QAM modulated signal. I connected one to the scope channel 1 and the other to scope channel 2. By plotting the X-y plot, we should be able to see the 4-QAM constellation and the transitions between the symbols. What we should see is a square with a cross inside it. Each corner of the square represents a symbol and the lines between them are the transitions between two symboles.
In order to get the result shown in the previous image, I had to add some persistence to the measurement.
We can see that we have a perfect square, meaning the I-Q signals have sharp transistions without overshoots. This is mainly due to the fact the modulator designed for the Eclypse Z7 roadtest is very basic, with a very low baudrate.
Conclusion
I really enjoyed working with the AD3. I was wrong, it is definitely not a gadget as I first thought. It offers a wide range of functionalities but for me, the best thing about the AD3 is its customisation capabilities (Waveforms scripts or APIs). Of course, its bandwidth is not as good as what we are used to when working with benchtop oscilloscopes but it is perfect for low-speed digital communications (UART, SPI, I2C, CAN).
It allows quick setups for debugging embedded systems. A single script can manage power supplies, I/Os and protocols to interface a device under test. This is a very good tool for any embedded system developer.
I have not tested all the features available, but I will certainly be using it in the future, even if I have a benchtop scope and power supply. If you need a tool for fast and modular testing at a reasonable cost, the AD3 is for you!