One of the first projects we did here on Hackster was to create a bare metal robot arm which could be positioned using Pmod Joysticks. It was a great project to create and demonstrated well how to control servos and and interface to the joysticks to create a fun solution.
However, I wanted to come back to the robot arm and redesign it, work from PYNQ. There are several reasons behind this update
- PYNQ provides the ability to control remotely the robot arm - The arm is something I want to show at conferences and presentations so the distance means the attendees cannot hurt themselves if they drive it.
- PYNQ has some really great advanced features such as the ability to create widgets and asynchronous tasks. Creating a notebook which drives the arm will demonstrate how we can use these features very well.
One of the first things we need to do to get started is create a new PYNQ overlay which enables us to drive the Adafriut PWM / Servo Shield. driving the shield is pretty straight forward, we connect to the AdaFruit shield using I2C. We then write in the command for the correct servo position for a desired servo to the shield and the arm will move.
All the shield is doing is generating a PWM waveform, we could do this easily from the PYNQ board. However, interfacing and supplying the power as the servos want 6V, would also be a challenge in that case, as such Adafruit Shield offers the best system level approach.
To get started with the design we need to use Vivado to create the overlay, to be able to create the over lay we need
- PYNQ Z1 / Z2 Board definition files - I used the Pynq Z1 and files are here
- PYNQ Z1 / Z2 XDC constraints - Pynq Z1 constraints available here
For Vivado to be able to make use of the board definition files they need to be unzipped and placed in the directory
<inst path>Vivado//2019.1/data/boards/board_files
Once completed we are able to open Vivado create a new project, using the new project wizard.
At the project type select RTL and check do not specify sources at this time
On the device selection page, select the board tab and select PYNQ-Z1
This will create a new project, targeting the PYNQ Z1, click finish
The next thing to do is to create a new block diagram and add to it the processing system.
Once the processing system is added, click on the run block automation option and the processor system will be configured for the Pynq Z1 settings.
The completed configuration should look like below
Now we can add in the AX IIC interface
Click on the run connection automation and the AXI IIC will be connected into the AXI network
The next step is to configure the Zynq PS, open the PS for customization select interrupts and enable the PL to PS interrupts.
With this enabled we need to add in a AXI interrupt controller from the IP library
Running the connection automation wizard will again result in the AXI Interrupt controller being mapped into the AXI network.
The final block we need to add in a concat block, it is to this block we will connect the interrupts from the IP blocks. In this case the AXI IIC.
We then connect the output of the concat block to the input of the AXI Interrupt controller. The interrupt output of the AXI Interrupt controller is then connected to the PS interrupt input.
We now have the design complete except for the constraints which define the IIC pin locations.
Use the sources tab to create a new constraints file
I called mine IO and add in the SDA and SCL pins
The contents of my XDC file
set_property -dict { PACKAGE_PIN P16 IOSTANDARD LVCMOS33 } [get_ports { IIC_0_scl_io }]; #IO_L24N_T3_34 Sch=ck_scl
set_property -dict { PACKAGE_PIN P15 IOSTANDARD LVCMOS33 } [get_ports { IIC_0_sda_io }]; #IO_L24P_T3_34 Sch=ck_sda
Finally validate and build the design
To be able to use the created bit file with the PYNQ Z1 we need to create an overlay this can be achieved very simply we need four files
- Bit file generated by Vivado - Available under the project Runs directory
- HWH file generated by Vivado - Available under the SRCS directory
- Python definition of the Overlay Class
- Python Initialization file
The latter two files can be created from scratch using a text file, I placed all these files in a directory named arm_pynq
With the exception of the initialization file all of the files are renamed arm_pynq ith the appropriate file extension. The initialization file is named __init__.py
These files can be seen below in the folder
The contents of the arm_pynq.py are
import pynq
from pynq import GPIO
__author__ = "Adam Taylor"
__copyright__ = "Copyright 2020, Adiuvo"
__email__ = "Adam@adiuvoengineering.com"
class arm_pynqOverlay(pynq.Overlay):
""".
"""
def __init__(self, bitfile, **kwargs):
super().__init__(bitfile, **kwargs)
if self.is_loaded():
pass
While the contents of the __init__.py are
from .arm_pynq import arm_pynqOverlay
To be able to upload the overlay, the easiest way is to make use of the PYNQ Z1 samba server.
We can map the network drive as \\pynq\xilinx and the username and password are both xilinx.
Once the PYNQ Z1 is mapped in we can copy the arm_pynq files from our development machine into the pynq/overlays directory
We can now make use of the overlay and start creating our application.
Notebook DevelopmentTo get started developing this application we are going to make use of the Jupyter lab environment rather than the traditional environment.
We can open Jupyter labs by going to
pynq:9090/lab
in our browser.
Once in the jupyter lab environment we are going to do the following
- Configure the AXI IIC for use using the PYNQ AXI IIC driver
- Define two new RX and TX buffers for the AXI IIC using the CFFI interface these will be char sized
- Configure the Adafruit shield for the correct PWM frequency 60 Hz
- Enable the shield PWM generation
- Write out the PWM settings to put the Arm into a neutral position
- Configure and launch five Asynchronous processes one for each Axis on the Robot
- Configure and launch functions to open and close the end effector
- Create several widget sliders and buttons which control the axis of the robot each one connected to one of the Asynchronous processes
The reason we use asynchronous processes is because we want to be able to control each axis of the robot at will.
The strucutre for a creation and working with a Asynchronous process is
- Define a widget slider
- Create a function which can wait for a change on a slider
- Create a async fucntion that calls the slider change function - this writes out the slider position to the Robot Arm specific joint
- display the slider
Changes to the slider will then drive the motor, to provide a simple example the code below shows how the wrist slider works
Define the slider
WristTilt = widgets.IntSlider(value=369,description="Wrist Tilt", min=122,max=616,orientation='vertical')
Create a process which can wait for change on a widget value
def wait_for_change(widget, value):
future = asyncio.Future()
def getvalue(change):
# make the new value available
future.set_result(change.new)
widget.unobserve(getvalue, value)
widget.observe(getvalue, value)
return future
Create the async process for the wrist slider, this waits for change on the wrist slider using the function above. Once a change is detected the function will output the new value over the I2C to the Adafruit shield
async def wrist():
# out.append_stdout('did work ' + '\n')
while True:
#wrist tilt
Wrist_tilt = await wait_for_change(WristTilt, 'value')
#out.append_stdout('async wrist' + str(Wrist_tilt) + '\n')
tx_buf[0] = 0x14
tx_buf[1] = Wrist_tilt & 0xff
iic.send(0x40,tx_buf,2,0)#low
tx_buf[0] = 0x15
tx_buf[1] = Wrist_tilt >>8
iic.send(0x40,tx_buf,2,0)#upper
asyncio.ensure_future(wrist())
All that remains then is to display the slider in the note book which can be done using the command
display(WristTilt)
For each of the joints a similar slider process and ascync process was created.
For the end effector which can be closed or open we use a different approach of two buttons one for open one for close.
To define the buttons we use the code
GraspClose = widgets.Button(description="Grasp Close")
GraspOpen = widgets.Button(description="Grasp Open")
To open and close the end effector we use the code defined below
def close_open(b):
tx_buf[0] = 0x18
tx_buf[1] = 0x01
iic.send(0x40,tx_buf,2,0)#low
tx_buf[0] = 0x19
tx_buf[1] = 0x00
iic.send(0x40,tx_buf,2,0)#upper
def close_grasp(b):
tx_buf[0] = 0x18
tx_buf[1] = 0x00
iic.send(0x40,tx_buf,2,0)#low
tx_buf[0] = 0x19
tx_buf[1] = 0x08
iic.send(0x40,tx_buf,2,0)#upper
Finally we display the buttons and the associate the click action with the fucntions above
display(GraspClose)
display(GraspOpen)
GraspClose.on_click(close_grasp)
GraspOpen.on_click(close_open)
Putting all of this together to create actions for each of the joints gives us the display below to control the robot.
The video below shows the robot arm working under PYNQ control
Wrap UpThis project shows how we can implement advanced control features using PYNQ to interact with complex real world hardware.
The Vivado project, overlay and notebook can be found here
Comments