# /*******************************************************************************
# * File Name: cs.py
# *
# * Description:
# * The Python API for the ChipShover.
# *
# *
# ********************************************************************************
# * Copyright 2021 NewAE Technology Inc.
# * SPDX-License-Identifier: Apache-2.0
# *
# * Licensed under the Apache License, Version 2.0 (the "License");
# * you may not use this file except in compliance with the License.
# * You may obtain a copy of the License at
# *
# * http://www.apache.org/licenses/LICENSE-2.0
# *
# * Unless required by applicable law or agreed to in writing, software
# * distributed under the License is distributed on an "AS IS" BASIS,
# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# * See the License for the specific language governing permissions and
# * limitations under the License.
# ********************************************************************************/
'''
ChipShover API Documentation
============================
For typical usage, after starting the ChipShover, you should
first home the stepper motors. This serves as a calibration step:
Example
-------
>>> from chipshover import ChipShover
>>> shv = ChipShover('COM5')
>>> shv.home()
From there, you can use the API to set the ChipShover's position:
>>> shv.move(10, 20, 190) # x=10, y=20, z=190
Note that the Z-axis default position is typically 200 to start with.
The ChipShover can also be swept along the XY axis:
>>> for x,y in shv.sweep_x_y(0, 5, 0, 5, step=0.5):
print("at %f, %f"%(x, y))
While using the ChipShover, it may become necessary to pause
or stop the ChipShover. This can be done by either the stop
or kill command. With the former, the ChipShover can
continue on as usual after the stoppage; however, the latter
will stop the ChipShover until it is power cycled.
>>> shv.stop() # can continue on from here with new movement commands
>>> shv.kill() # require a power cycle to continue operation
Note that if a stop command is used, the ChipShover's measured position
may become incorrect. As such, it is recommended that a homing
command be performed after a stop is issued. In practice, the position
seems to still be fairly accurate after a stop and so this is only
recommended and not required.
If using most commands interactively (from a Jupyter notebook),
hitting Ctrl-C should issue a `stop()` command. The assumption is if you
interrupt the program with Ctrl-C that is because bad things were about to
happen. Without the `stop()` command, the controller will finish
executing the last command, such as a move.
'''
import serial
import time
from .samba import Samba
import os
import base64
import datetime
import binascii
def firmware_update(comport, fw_path=None):
""" Flashes new firmware to the SAM3X8E (this command is for ChipShover-One only).
Args:
comport (str): Path to serial port, ex COM4
fw_path (str): Path to binary firmware file. If None, flash using default firmware
Defautls to None.
"""
sam = Samba()
try:
sam.con(comport)
print("Connected")
sam.erase()
print("Erased")
if fw_path:
fw_data = open(fw_path, "rb").read()
else:
from .firmware import getsome
fw_data = getsome('firmware.bin').read()
sam.write(fw_data)
print("Written")
if sam.verify(fw_data):
sam.flash.setBootFlash(True)
print("Setting boot from flash")
sam.ser.close()
print("Firmware update succeeded: PLEASE POWER CYCLE CHIPSHOVER")
else:
sam.ser.close()
raise OSError("Firmware verify FAILED!")
except:
sam.ser.close()
raise
def _gen_firmware(fw_path=None):
f = open("firmware.py", "w")
f.write("# This file was auto-generated. Do not manually edit or save. What are you doing looking at it? Close it now!\n")
f.write("# Generated on %s\n"%datetime.datetime.now())
f.write("#\n")
f.write("import binascii\n")
f.write("import io\n\n")
f.write("fwver = [%d, %d]\n" % (0, 1))
f.write("def getsome(item, filelike=True):\n")
f.write(" data = _contents[item].encode('latin-1')\n")
f.write(" data = binascii.a2b_base64(data)\n")
f.write(" if filelike:\n")
f.write(" data = io.BytesIO(data)\n")
f.write(" return data\n\n")
f.write("_contents = {\n")
f.write("")
if fw_path is None:
fw_path = '../../../ChipSHOVER-Marlin/.pio/build/DUE_chipshover/firmware.bin'
with open(fw_path, "rb") as e_file:
# json_str = base64.b64encode(e_file.read())# json.dumps(e_file.read(), ensure_ascii=False)
json_str = binascii.b2a_base64(e_file.read())
f.write("\n#Contents from %s\n"%fw_path)
f.write("'%s':'"%'firmware.bin')
f.write(json_str.decode().replace("\n",""))
f.write("',\n\n")
f.flush()
f.write("}\n")
pass
[docs]class ChipShover:
"""ChipShover is a controller for XYZ tables. Assumes Marlin-based
firmware for commands.
"""
#Default ChipShover table/firmware combo
STEPS_PER_MM = 1600
def __init__(self, comport):
"""Connect to ChipShover-Controller using given serial port."""
self.ser = serial.Serial(comport, rtscts=True)
self._com = comport
#Required for ChipShover-One + Archim2 USB serial
self.ser.rtscts = True
self.ser.timeout = 0.25
self.z_home = None
self.ser.flush()
self.ser.reset_input_buffer()
#Check if table seems legit
#Abs mode by default
self.ser.write(b"G90\n")
self.wait_done()
#MM by default
self.ser.write(b"G21\n")
self.wait_done()
#Check the "steps per unit" is valid
self.ser.write(b"M503\n")
results = self.wait_done()
try:
splitres = results.split(b"Steps per unit:\necho: M92")[1].split(b"\n")[0]
splitres = splitres.split(b" ")
if splitres[1][0] != ord('X') or \
splitres[2][0] != ord('Y') or \
splitres[3][0] != ord('Z'):
raise IOError("Communication problem attempting to read" + \
"M503 response. %s was splitres"%splitres)
xsteps = float(splitres[1][1:])
ysteps = float(splitres[2][1:])
zsteps = float(splitres[3][1:])
if xsteps != ysteps != zsteps:
raise ValueError("XSTEPS/YSTEPS/ZSTEPS differs. Abort. %f %f %f"%(xsteps, ysteps, zsteps))
if xsteps < 100 or xsteps > 10E3:
raise ValueError("Sanity check in XSTEPS failed. %f"%xsteps)
self.STEPS_PER_MM = int(xsteps)
except:
print("Failed to read steps/mm, check communication is OK.")
print("Response to M503: %s"%results)
raise
self.set_fan(50)
self.call_stop_on_ctrlc = True
#TODO -
#signal.signal(signal.SIGINT, self.stop)
[docs] def set_fan(self, fan_speed=100):
"""Sets cooling fan speed, range of 0 - 100"""
fan_pwm = (float(fan_speed) / 100.0)*255
fan_pwm = int(round(fan_pwm))
fan_pwm = min(fan_pwm, 255)
fan_pwm = max(fan_pwm, 0)
#Early protos had this as P1, now P0
self.ser.write(b"M106 P0 S%d\n"%fan_pwm)
self.wait_done()
[docs] def close(self):
"""Closes serial port"""
self.set_fan(0)
self.ser.close()
[docs] def stop(self):
"""Calls EMERGENCY STOP command (M410).
Stops movement, but allows further commands. Sending this will
cause positionvto be wrong if table was moving at the time."""
self.ser.write(b"M410\n")
print("**STOP CALLED. Motor positions will be incorrect. Please re-home.")
self.wait_done()
[docs] def kill(self):
"""Calls KILL command (M112) to stop all movement.
This will stop all table movement and shut down the
controller, requiring a power cycle to recover. Useful
when you have a serious error condition you want to
ensure someone physically clears.
"""
self.ser.write(b"M112\n")
print("***KILL CALLED. Power Cycle Needed!***")
self.wait_done()
[docs] def move_zdepth(self, z_depth):
"""Sets the Z axis to a given 'depth', as referenced from home.
The default Z-Axis homing sets the Z axis to some positive value, with
Z = 0 being the axis bottom. Most of the time you'd like to specify depth
below home position instead, this function lets you do that.
"""
if self.z_home is None:
raise ValueError("Run Homing First")
self.move(z= self.z_home - z_depth)
[docs] def move(self, x=None, y=None, z=None, debug=False):
"""Move table to commanded X, Y, Z location.
Uses a `G0` command to move the table. The function
will use a `M400` command to wait for the movement
to complete before returning.
WARNING: The `z` is an absolute position - the default
home `z` is often the MAXIMUM value. Thus a
move to z=0` may slam your table into the ground.
You can use the `move_zdepth()` function for
moving a depth from the home position instead.
"""
cmdstr = b"G0 "
if x is not None:
cmdstr += b"X%f"%x
if y is not None:
cmdstr += b"Y%f"%y
if z is not None:
cmdstr += b"Z%f"%z
cmdstr += b"\n"
if debug:
print(cmdstr)
self.ser.write(cmdstr)
self.wait_done()
self.wait_for_move()
[docs] def wait_for_move(self):
"""Wait for current movement to be done"""
self.ser.flush()
self.ser.reset_input_buffer()
#wait for move to finish
self.ser.write(b"M400\n")
self.wait_done()
[docs] def get_position(self, forcefinish=True):
"""Gets the X/Y/Z position of the table.
By default will wait for any movement to
finish, as reading position during movement
will return incorrect (final not current)
position.
"""
if forcefinish:
#wait for move to finish
self.wait_for_move()
self.ser.write(b"M114\n")
pos_line = self.ser.readline()
ok = self.ser.readline()
if ok != b'ok\n':
print("DEBUG: 'pos_line': %s"%pos_line)
print("DEBUG: 'ok' line : %s"%ok)
raise IOError("Com error on M114 - received %s, expected 'ok'\n"%ok)
pos_line = pos_line.split(b' ')
if (pos_line[0][0] != ord('X')) or \
(pos_line[1][0] != ord('Y')) or \
(pos_line[2][0] != ord('Z')) or \
(pos_line[5][0] != ord('X')) or \
(pos_line[6][0] != ord('Y')) or \
(pos_line[7][0] != ord('Z')):
raise IOError("Unknown position format: %s"%pos_line)
x_mm = float(pos_line[0][2:])
y_mm = float(pos_line[1][2:])
z_mm = float(pos_line[2][2:])
x_cnt = float(pos_line[5][2:])
y_cnt = float(pos_line[6][2:])
z_cnt = float(pos_line[7][2:])
#Count values are more accurate
calc_x_mm = x_cnt * (1.0/float(self.STEPS_PER_MM))
calc_y_mm = y_cnt * (1.0/float(self.STEPS_PER_MM))
calc_z_mm = z_cnt * (1.0/float(self.STEPS_PER_MM))
if round(calc_x_mm, 2) != x_mm:
raise IOError("Reporting error: %f != %f (based on count of %d)"%(x_mm, calc_x_mm, x_cnt))
if round(calc_y_mm, 2) != y_mm:
raise IOError("Reporting error: %f != %f (based on count of %d)"%(y_mm, calc_y_mm, y_cnt))
if round(calc_z_mm, 2) != z_mm:
raise IOError("Reporting error: %f != %f (based on count of %d)"%(z_mm, calc_z_mm, z_cnt))
return calc_x_mm, calc_y_mm, calc_z_mm
[docs] def home(self, x=True, y=True, z=True):
"""Perform homing operation using G28 command.
Calling this will home the X, Y, and Z axis (you can
disable specific axis as well). The command will block
until the homing operation is complete.
"""
self.ser.flush()
self.ser.reset_input_buffer()
if x == y == z == False:
return
self.ser.write(b"G28")
if x:
self.ser.write(b" X")
if y:
self.ser.write(b" Y")
if z:
self.ser.write(b" Z")
self.ser.write(b"\n")
home_resp = self.wait_done()
self.z_home = self.get_position()[2]
return home_resp
[docs] def sweep_x_y(self, x_start, x_end, y_start, y_end, step=0.1, x_step=None, y_step=None, z_plunge=0):
"""Sweep X-Y range, yielding at each point.
This function should be used in a simple sweep, for example:
for x,y in cs.sweep_x_y(0, 5, 0, 5, step=0.5):
print("At %f, %f"%(x,y))
If you call your fault injection probe to active at the point, you will
get a simple fault injection performed over a linear X-Y range.
The `z_plunge` parameter can be used to specify a certain amount of z-plunge
performed at each point. This is normally used with BBI or similar probes that
must be put in contact with the die.
If using interactive Python (Jupyter), hitting `Ctrl-C` during this
function run will call `stop()` by default.
"""
if x_start > x_end:
raise ValueError("X End must be numerically larger than X Start")
if y_start > y_end:
raise ValueError("Y End must be numerically larger than Y Start")
if x_step is None:
x_step = step
if y_step is None:
y_step = step
x = x_start
while x <= x_end:
self.move(x=x)
y = y_start
while y <= y_end:
self.move(y=y)
if z_plunge:
old_z = self.get_position()[2]
self.move(z = (old_z-z_plunge))
yield (x, y)
if z_plunge:
self.move(z = old_z)
y += y_step
x += x_step
[docs] def wait_done(self, timeout=5, debug=False):
"""Wait for a command to be acknowledged by checking for 'ok' response.
Some G commands return immediatly, for example G0 returns an 'ok' and
does not wait for the command to execute. Others will block until the
command finishes executing, for example the homing operation (G28)
does not return 'ok' until it is done.
By default if a `KeyboardInterrupt` is detected (from a Ctrl-C operation)
then stop() will be called. This is done in case you are using
ChipShover interactively and hit Ctrl-C to try and abort a move operation.
"""
try:
timeout = timeout * 4
timeout_cnt = 0
debug_data = b""
while True:
resp = self.ser.readline()
if resp:
debug_data += resp
if resp and debug:
print(resp)
#This is an OK response - indicates device is alive
if resp == b'echo:busy: processing\n':
timeout_cnt = 0
#Done deal I guess
if resp == b'ok\n':
break
time.sleep(0.25)
timeout_cnt += 1
if timeout_cnt > timeout:
raise IOError("Device timed out, responses: %s"%str(debug_data))
except KeyboardInterrupt:
if self.call_stop_on_ctrlc:
print("Ctrl-C detected - calling stop() to stop table movement")
self.stop()
raise
#Sometimes buffer seems to have stuff in it still - do one last read
resp = self.ser.readline()
debug_data += resp
return debug_data
[docs] def status(self):
""" Gets the status of the ChipShouter
This function CANNOT tell whether a fuse
or emergency stop event has happened.
Statuses:
1. Idle
2. Unhomed
3. 5V fuse blown
"""
self.ser.write(b"M14400\n")
pos_line = self.ser.readline()
if not pos_line:
return None
if (pos_line[0] == 0):
return "Idle"
elif (pos_line[0] == 2):
return "Unhomed"
elif (pos_line[0] == 8):
return "5V fuse blown"
#ok = self.ser.readline()
[docs] def erase_firmware(self):
"""Erases the firmware of the SAM3X on the ChipShover
Reprogram with::
from chipshover import update_firmware
firmware_update("comport")
"""
self.ser.write(b"M997\n")
[docs] def auto_program(self, fw_path=None):
"""Erase and reprogram the chipshover
Args:
fw_path (str): Path to firmware. If none, use default firmware.
Defaults to None.
If com port detection fails, reprogramming must be done with firmware_update()
"""
import time, serial.tools.list_ports
before = serial.tools.list_ports.comports()
before = [b.device for b in before]
time.sleep(0.5)
self.ser.write(b"M997\n")
time.sleep(5.5)
after = serial.tools.list_ports.comports()
after = [a.device for a in after]
candidate = list(set(before) ^ set(after) ^ set([self._com]))
# print(candidate)
# print(before, after, self._com)
if len(candidate) == 0:
raise OSError("Could not detect COMPORT. Continue using programmer.program()")
com = candidate[0]
print("Detected com port {}".format(com))
firmware_update(com, fw_path)