Introduction: Digital Oscilloscope Using Digilent Zybo Board

The Digilent Zybo board is built around Xilinx's Zynq SoC (System on Chip) part. This IC has dual Arm-A9 cores (referred to as PS - Processing System) that perform like any other microcontroller. What makes it special is that it also has FPGA hardware (referred to as PL - Programmable Logic) on the same IC as the PS, enabling the user to create custom peripherals for the PS.

This tutorial was written from my experience in Cal Poly SLO's CPE439 : Real Time Embedded Systems course. A good application for this Zynq part is an oscilloscope : the PS can handle the less time critical operations (user interface, drawing graphics), while the PL can handle time critical operations (input ADC buffer, triggering, VGA timing signals). This oscilloscope was designed to meet some basic specs :

- Uses a standard 10:1 scope probe

- Input voltage range of -10V to +10V

- Bandwidth = 100kHz [limited by built in ADC sample rate = 1 MHz]

- VGA monitor display, resolution ~640 x 480

- User Input through rotary encod

This oscilloscope is made up of several key blocks. These are :

- Analog Front End : Attenuate and level shift input signals to a level usable by the Zynq's built in ADC. Implemented with several op amp filters/amplifiers.

- ADC Buffer / Trigger : Values sampled by the ADC are continuously sampled to a buffer. When the trigger event occurs (passing through a certain voltage with a selected positive / negative slope), the buffer fills up, and signals that it is now full and ready to be read in.

- User Input Processing : User input for this oscilloscope is taken in in the form of rotary encoder and button signals. The encoder signals must be processed to give useful rotational data, and both this data and the button data must be sent into the processing unit.

- Processing System : User input and ADC buffer data are processed and values are written to the video driver to generate the graphical user interface (GUI).

- Video Driver : A frame buffer (memory holding values of each screen pixel) is written to by the processing system. A VGA driver generates the timing signals required by the VGA protocol, as well as reading values from the frame buffer to be sent to the screen.

Step 1: Analog Front End

The Xilinx Zynq IC features the "XADC" Analog to Digital Converter. The XADC samples at a maximum of 1MSPS (megasample per second), outputting a 12 bit value. This ADC has an input range of 0-1V.

The XADC is capable of sampling several input channels, but on the Zynq board only several specific auxiliary inputs are actually accessible by the user. In my oscilloscope, I used the AUX14 ADC input available on the XADC PMOD of the Zybo.

Because an oscilloscope input range of 0-1V is not very useful, analog circuitry must be added in front of the ADC input in order to scale and shift the input voltage, see the block diagram for this step.

This block diagram was implemented using the circuits shown in the LTSPICE schematic/simulation (schematic file attached for simulation). As desired, the gain is 1/20 = 0.05 until the anti-aliasing filter causes it to drop off around 400kHz (nyquist frequency = 500kHz for 1MSPS ADC).

I implemented this design on a breadboard (didn't have time to design a PCB) using through hole components. The components used to implement this design are shown in the second LTSPICE schematic (not simulatable, just used to design circuit for ordering parts), as well as the actual built circuit as well. The 3.3V and GND were sourced from the XADC PMOD connector.

Step 2: ADC Buffer / Trigger

This portion of the design is responsible for sampling the input signal, triggering on a waveform event, and making the data available to the PS when complete.

- processing_system7_0: Zynq PS

- rst_processing_system7_0_100M: Part of AXI communication (added in connection automation)

- processing_system7_0_ai_periph: Part of AXI communication (added in connection automation)

- xadc_wiz_0: XADC implementation. Configured in DRP, single channel mode, with maximum conversion rate (actual of 961KSPS). VAUX14 was configured in bipolar mode.

- gpio_trigger_settings: GPIO IP used to set subsampling and trigger threshold. Subsampling refers to choosing how often to accept an input value from the ADC. If the time base of the scope is zoomed out to a long period of time, the buffer will be too small to fill up (for example) 1 second of data taken at 1MSPS. Trigger threshold refers to the voltage level at which the trigger occurs at.

- gpio_trigger_control: GPIO IP. Channel 1 is output, able to reset trigger block (when PS is ready for a new buffer of data) and assert the address value of the memory to be read. Channel 2 is an input, allowing reading of the output data of the data buffer, the last address written to, and if the trigger buffer has filled up yet.

- drp_int_0: My IP for interpreting the DRP output of the XADC.

- trigger_0: My IP for implementing trigger behavior

- blk_mem_gen_0: IP for holding past sampled values.

SystemVerilog files for all my IPs for this section are included.

Step 3: User Input Processing

This portion of the design is responsible for sampling the encoder and button inputs. The "standard speed" PMOD was used to bring these signals into the Zynq. Button inputs were sampled simply with a GPIO module. Encoder inputs are processed by an encoder IP block (written by me) in order to get a "count" value related to how much the encoder has been turned.

Since mechanical rotary encoders were used, both the switch and encoder outputs required pullup resistors (5k Ohm), and a debouncing LP filter was also added. This was probably the weakest part of my design, and definitely requires some more work. I had problem getting accurate count values from the encoders, despite simulations of the hardware blocks showing proper function (images attached). I didn't have time to diagnose this problem before the final demo, so it remains as something I advise you to take more time to design than me!

- encoder_btn_gpio: Channel 1 is input, taking in count values from the three encoders and two buttons

- encoder0,1,2 : My IP for interpreting quadrature inputs from rotary encoders. Steps of the encoder are recorded as a "count" value that can be negative or positive. The RST input is present to reset the count value, allowing the PS to read the count value, then reset it. In this way, the count value should never overflow and cause strange results.

Step 4: Video Driver

This part of the design is responsible for drawing the GUI and input waveforms. Scope waveforms, trigger level lines, and other changing indicators are stored in a frame buffer, with each memory address holding a pixel's value. This frame buffer is a block ram, which the VGA driver reads values out of to be sent to the VGA port to be displayed onscreen.

Originally, the screen was desired to be 640 x 480. However, storing all these pixel values in one block RAM is not possible, as the maximum depth of a block ram is 262144 (corresponding to an address with of 18 bits, 2^18 = 262144), and 640 x 480 = 307200 pixels. A compromise was made and the screen would be reduced to 480 x 512, so that the entire screen buffer could be stored in one block ram.

In order to simplify later coding, the background image (grid and outlines of the screen) were drawn on the computer in an image editor. A matlab script was written (included) to convert the black and white BMP to a .coe file, which may be preloaded into a block ram cell. This means that the image is already stored in memory, and does not need to be "drawn" in code or hardware. This is a separate block ram than the screen buffer, so the two block RAMs' outputs are run into a custom IP block (color processor) in order to "decide" which color should be displayed, with scope signals and indicators having higher layer priority over the grid.

-axi_gpio_0: Channel 1 is output. Controls the address, data, and enable signals to write to the screen buffer. Channel 2 is input. Reads an acknowledge bit to confirm that the write operation has completed.

-bram_write_controller: Takes a address, data, and enable signal from a GPIO module to write to a block ram. The address and data signals are simply routed through without change. The complication comes with the EN signal. I wasn't sure how long a write "1" and "0" would take from the GPIO (in terms of clock cycles), so I wanted to make sure that the block wram was only written to once. This write controller runs on the rising edge of the GPIO's EN signal in order to create a 1 clock width enable pulse to the write enable of the block ram. When it has completed this pulse, the acknowledge signal can be asserted high to confirm it was written.

- signals_buffer1: This is a 3 bit wide block ram used to store the changing pixel values on the screen, such as the waveform, trigger level line, and voltage/division indicator. Each memory address represents one pixel. The address system works so that the top 9 bits are the row of the pixel, and the bottom 9 bits are the column of the pixel.

- gui_buffer: This is a 1 bit wide block ram used to store the unchanging GUI (grid, outlines etc) that is loaded in via .coe file generated from image. (matlab script attached)

- vga_driver_0: Generates horizontal and vertical sync signals for the VGA protocol. Generates column and row values which represent the current pixel being displayed. These are used to read out of the block RAMs to read the stored values for the current pixel

- color_processor_0: This IP decides which color will be displayed as a function of stored pixel values. I set my priorities from highest to lowest as (red trigger level line, yellow signal line, white GUI/grid). Each pixel's value is stored as a 3 bit value, each bit representing a "layer". Higher priority signals (like the trigger line) should appear on the "top" layer, while lower priority signals (like the background grid) should be in the "bottom" layer.

Step 5: Processing System

The designs presented previously were combined to give the final design, with a single PS controlling them all. The interface to all the blocks are through GPIO IPs. See the image for the naming convention for all the previous GPIO modules when combined:

- gpio_trigger_control : Control trigger module, read buffer values

- gpio_trigger_settings : Set threshold and subsample

- screen_buffer_gpio : Write values to screen buffer

- encoder_btn_gpio : Read encoder counts and button values.

Libraries were written (included) used to write pixels, read encoder / button inputs, and read sample buffer values. Due to limited time, the main code is somewhat large, didn't have enough time to make it super pretty with modular functions.

During initial development, FreeRTOS was going to be used in the PS. Due to time constraints, this was abandoned and no tasks were used. However the CORTEX_A9_Zynq_ZC702 FreeRTOS Demo project I had been working out of was still used, in the name of changing as little as possible at the last minute.

The final version of my oscilloscope only had vertical scale adjustment and trigger level adjustment, so the time division display was unused.

The basic flow of the main program is :

1) Check encoder and button inputs, adjust trigger values appropriately

2) Check if scope has triggered yet (repeat 1&2 until it has triggered)

3) Erase and update the markers showing V/Second division.

4) Read all values of trigger buffer.

5) Calculate location of trigger level line in pixels locations

6) Calculate location of present and next sample in pixels

7) Erase all pixels in present column

8) Draw a vertical line from present sample location's row to next sample location's row. This creates a display of connected vertical lines, rather than unconnected dots.

9) Draw trigger level line

10) Draw triangle to display 0V level

11) Reset the trigger to start taking samples again.

12) Repeat starting from 1.

Step 6: Conclusions, Notes, Future Improvements

Looking back there are some changes I'd like to make. Most of this was limited by my tight time constraints.

- Encoders: Currently encoders work quite poorly. I need to do more debugging with an oscilloscope to make sure the quadrature outputs are "bounce free" and clean, so that there are no errors in interpreting them.

- Should add vertical offset, and horizontal offset/scaling.

I hope this was useful to you!