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.
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.
Top Comments