The next step in my Zynq and Pynq learning; get information from an FPGA design into the Linux part: a rotary decoder that can read the movement of a scroll wheel.

The original blog post for Spartan 6 and Xilinx ISE Webpack: Rotary Encoders - Part 5: Capturing Input on an FPGA

 

In this design, the FPGA decodes movements of a rotary encoder.

The value is then written to a memory mapped register, that can be read from the Linux side. I will read it from a Jupyter notebook.

 

scroll wheel updates progress bat

image: the scroll bar on a Jupyter notebook updates real time with the scroll wheel

 

 

This example has the following components:

  • hardware: a rotary wheel connected to one of the ZYNQ PMOD connectors.
  • VHDL: an FPGA rotary decoder.
  • a Vivado project to link the FPGA decoder to the Linux part.
  • a Jupyter notebook to load and test the design.

 

Hardware

 

Check Rotary Encoders - Part 1: Electronics for the details of the encoder.

image: the rotary encoder (scroll wheel) hardware. With pull-ups and debouncing capacitors

 

In this design, I'm connecting it to PMOD connector B.

Encoder pin A is connected to PMODB 1P, pin B to PMODB 1N.

The ground and 3.3V for the encoder are also connected to that PMODB connector.

 

image: physical connection between PMODB and the two scroll wheel connection

 

On the Zynq, the 2 PMODB pins (and the wheel pins) are connected to package pins W14 and Y14.

That info can be found in the package support file for the Zynq Z2 kit: PYNQ-Z2 v1.0.xdc.

image: the I/O view is used to set pin constraints and assign PMODB connected package pins to the two FPGA inputs.

 

image: hardware used in this example is the scroll wheel module of Hercules LaunchPad and GaN FETs - Part 3b: BoosterPack Layout - my version

 

VHDL Rotary Decoder

 

The code is fairly easy. It's one I used before in a Spartan 6 project. For this post, I added reset functionality. For the rest, it's identical to what I used years ago in Rotary Encoders - Part 5: Capturing Input on an FPGA.

It performs decoding of the pulses coming out of the scroll wheel. The current value of the encoder is available as 8 bit output port Position.

Later, this port will be connected to a Linux memory mapped location.

Latest source on github: https://gist.github.com/jancumps/0f89b68d961d969665e60b653de94e8a

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity quadrature_decoder is
    Port ( QuadA : in  STD_LOGIC;
           QuadB : in  STD_LOGIC;
           Clk : in  STD_LOGIC;
           nReset : in STD_LOGIC;
           Position : out  unsigned (7 downto 0));
end quadrature_decoder;

architecture Behavioral of quadrature_decoder is

signal QuadA_Delayed: std_logic_vector(2 downto 0) := "000";
signal QuadB_Delayed: std_logic_vector(2 downto 0) := "000";
signal Count_Enable: STD_LOGIC;
signal Count_Direction: STD_LOGIC;
signal Count: unsigned(7 downto 0) := "00000000";

begin

process (Clk, nReset)
begin
   if (nReset = '0') then
      Count <= "00000000";
      QuadA_Delayed <= "000";
      QuadB_Delayed <= "000";
   elsif rising_edge(Clk) then
      QuadA_Delayed <= (QuadA_Delayed(1), QuadA_Delayed(0), QuadA);
      QuadB_Delayed <= (QuadB_Delayed(1), QuadB_Delayed(0), QuadB);
      if Count_Enable='1' then
         if Count_Direction='1' then
            Count <= Count + 1;
            Position <= Count;
         else
            Count <= Count - 1;
            Position <= Count;
         end if;
      end if;
   end if;
end process;

Count_Enable <= QuadA_Delayed(1) xor QuadA_Delayed(2) xor QuadB_Delayed(1)
            xor QuadB_Delayed(2);
Count_Direction <= QuadA_Delayed(1) xor QuadB_Delayed(2);

end Behavioral;

 

Vivado Project

 

The project isn't a lot different than the previous one. The only difference is in the axi_gpio block.

This time, it's defined as an 8-bit input register.

image: the axi gpio block is set to input mode

 

The two pins for the rotary encoder A and B signals are made external. For the rest, this is exactly the same as in my previous posts for the PWM design.

 

 

image: the full block diagram is very similar to that of the previous pwm example

If you replicate this exercise: there is a mistake. The interconnect reset should go tho the first reset of the AXI Interconnect IP.

All other resets should connect to the peripheral reset pin.

 

 

The register needs an address. I have let Vivado generate that.

In the address editor, I clicked the S_AXI entry, right-clicked, and selected Assign.

image: the register memory map auto assigned in the address editor

 

Then, I ran the Synthesis, and assigned the IO pins (see the Hardware section above).

Then, I generated the bitstream.

 

Jupyter notebook

 

This one is also comparable with (and derived from) beacon_dave 's PYNQ-Z2 Workshop - AXI GPIO post.

image: the Jupyter notebook in action

 

First, the overlay is loaded

 

from pynq import Overlay
ol=Overlay("design_quadrature_decoder.bit")

 

Then, the utility lib for AXI GPIO is loaded, and I retrieve the interface for the VHDL wrapper:

 

from pynq.lib import AxiGPIO  
qd_dict = ol.ip_dict['axi_gpio_qd'] 
qd_output = AxiGPIO(qd_dict).channel1 

 

That's it. You can now get the current value of the encoder at any time:

 

print(f"decoder value: {qd_output.read()}")

 

image: the Jupyter notebook shows the current position of the scroll wheel

 

It also works together with the tqdm progress bar:

 

from tqdm import tqdm_notebook
from time import sleep

try:
    with tqdm_notebook(total=255) as pbar:  
   
        while True:
            pbar.update(qd_output.read() - pbar.n)
            pbar.refresh()
            sleep(0.001)
except KeyboardInterrupt:
    pass

 

When you rotate the wheel, the progress bar on the Jupyter book updates.

image: infinite loop gets current scroll wheel position and updates the progress bar

 

Note that this progress bar only shows updates for value increments, not if you set the value lower than the previous one.

That's why I added a progress bar refresh in the final code.

 

 

If you play along with this blog series, you now know how to send data from Linux to VHDL blocks, and how to read data from them.

As always, the Vivado project is attached.

 

Pynq - Zync - Vivado series
Add Pynq-Z2 board to Vivado
Learning Xilinx Zynq: port a Spartan 6 PWM example to Pynq
Learning Xilinx Zynq: use AXI with a VHDL example in Pynq
VHDL PWM generator with dead time: the design
Learning Xilinx Zynq: use AXI and MMIO with a VHDL example in Pynq
Learning Xilinx Zynq: port Rotary Decoder from Spartan 6 to Vivado and PYNQ
Learning Xilinx Zynq: FPGA based PWM generator with scroll wheel control
Learning Xilinx Zynq: use RAM design for Altera Cyclone on Vivado and PYNQ
Learning Xilinx Zynq: a Quadrature Oscillator - 2 implementations
Learning Xilinx Zynq: a Quadrature Oscillator - variable frequency
Learning Xilinx Zynq: Hardware Accelerated Software
Automate Repeatable Steps in Vivado
Learning Xilinx Zynq: Try to make my own Accelerated OpenCV Function - 1: Vitis HLS
Learning Xilinx Zynq: Try to make my own Accelerated OpenCV Function - 2: Vivado Block Design
Learning Xilinx Zynq: Logic Gates in Vivado
Learning Xilinx Zynq: Interrupt ARM from FPGA fabric
Learning Xilinx Zynq: reuse and combine components to build a multiplexer