first commit

This commit is contained in:
Nikolay Khabarov
2017-05-10 04:25:12 +03:00
commit b70dd3c9ea
23 changed files with 1594 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
*.pyc
__pycache__
.iws
workspace.xml
tasks.xml
.idea/dictionaries
pycnc.tar.bz2

12
.idea/PyCNC.iml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 2.7.13 (/usr/bin/python2.7)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TestRunnerService">
<option name="projectConfiguration" value="Nosetests" />
<option name="PROJECT_TEST_RUNNER" value="Nosetests" />
</component>
</module>

View File

@@ -0,0 +1,15 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyCompatibilityInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ourVersions">
<value>
<list size="2">
<item index="0" class="java.lang.String" itemvalue="2.7" />
<item index="1" class="java.lang.String" itemvalue="3.6" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>

7
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 2.7.13 (/usr/bin/python2.7)" project-jdk-type="Python SDK" />
<component name="PythonCompatibilityInspectionAdvertiser">
<option name="version" value="2" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/PyCNC.iml" filepath="$PROJECT_DIR$/.idea/PyCNC.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017 Nikolay Khabarov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

59
README.md Normal file
View File

@@ -0,0 +1,59 @@
This project bring CNC control for ARM based Linux boards like Raspberry Pi.
Typically there is no way to control stepper motors from Linux runtime
environment due to the lack of real time GPIO control. Even kernel based
modules can not guarantee precise control of pulses for steppers. There is
always way out. DMA(Direct Memory Access) is a separated hardware module which
provides high precision for GPIO outputs. This module can copy bytes which
represent GPIO states from RAM buffer directly to GPIO with some clock based
on main chip internal oscillator without using CPU's cores. Using such approach
this project generates impulses for moving stepper motors and that is very
precise way regardless CPU load and OS time jitter.
This approach also allows to use Python language for this project. Typically,
Python is not good choice for real time application, but since project just
needs to set up DMA buffers and hardware will do the rest, Python become the
perfect choice for easy development of this project.
Video demo - [YouTube video](https://youtu.be/vcedo59raS4)
#Current command support
G0, G1, G4, G20, G21, G28, G90, G91, G92, M2, M3, M5, M30
Commands can be easily added, see [gmachine.py](./gmachine.py) file.
#Config
All config is stored in [config.py](./config.py) and contains hardware properties, limitations
and pin names for hardware control.
#Hardware
Currently, this project supports Raspberry Pi 1-3. But there is a way to add new boards.
See [hal.py](./hal.py) file.
#Performance notice
Pure Python interpreter wouldn't provide great performance for high speed machines.
Overspeeding setting causes motors mispulses and probably lose of trajectory. According
to my tests, Raspberry Pi 2 can handle axises with 400 pulses on mm with top velocity ~800 mm
per min. There is always way out! :) Use JIT Python implementation like PyPy. RPi2 can handle
up to 18000 mm per minute on the machine with 80 steps per millimeter motors with PyPy.
_Note: Raspbian has outdated PyPy version in repositories(v4.0). Moreover v4.0 has issue with
`mmap` module implementation. Use PyPy v5.0+, download it for your OS from
[here](https://pypy.org/download.html)._
PyPy installation:
```bash
wget wget https://bitbucket.org/pypy/pypy/downloads/pypy2-v5.7.1-linux-armhf-raspbian.tar.bz2
sudo mkdir /opt/pypy
sudo tar xvf pypy2-v5.7.1-linux-armhf-raspbian.tar.bz2 --directory /opt/pypy/ --strip-components=1
sudo ln -s /opt/pypy/bin/pypy /usr/local/bin/pypy
```
#Dependencies
Nothing. Just pure Python code.
#GCode simulation
Just a link to a nice web software for gcode files emulation (very helpful for manual creating of
gcode files): [https://nraynaud.github.io/webgcode/](https://nraynaud.github.io/webgcode/)
#License
see [LICENSE](./LICENSE) file.
#Author
Nikolay Khabarov

35
config.py Normal file
View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python
# Hardware limitations config
STEPPER_PULSE_LINGTH_US = 2
STEPPER_MAX_VELOCITY_MM_PER_MIN = 1800 # mm per min
STEPPER_MAX_ACCELERATION_MM_PER_S2 = 200 # mm per sec^2
STEPPER_PULSES_PER_MM = 400
TABLE_SIZE_X_MM = 200
TABLE_SIZE_Y_MM = 300
TABLE_SIZE_Z_MM = 48
SPINDLE_MAX_RPM = 10000
# Pins config
STEPPER_STEP_PIN_X = 16
STEPPER_STEP_PIN_Y = 20
STEPPER_STEP_PIN_Z = 21
STEPPER_DIR_PIN_X = 13
STEPPER_DIR_PIN_Y = 19
STEPPER_DIR_PIN_Z = 26
SPINDLE_PWM_PIN = 7
ENDSTOP_PIN_X = 12
ENDSTOP_PIN_Y = 6
ENDSTOP_PIN_Z = 5
# Hardware behavior config
# Run command immediately after receiving and stream new pulses, otherwise
# buffer will be prepared firstly and then command will run.
# Before enabling this feature, please make sure that board performance is
# enough for streaming pulses(faster then real time).
INSTANT_RUN = True

86
coordinates.py Normal file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/env python
import math
class Coordinates(object):
""" This object represent machine coordinates.
Machine supports 3 axis, so there are X, Y and Z.
"""
def __init__(self, x, y, z):
""" Create object.
:param x: x coordinated.
:param y: y coordinated.
:param z: z coordinated.
"""
self.x = round(x, 10)
self.y = round(y, 10)
self.z = round(z, 10)
def is_zero(self):
""" Check if all coordinates are zero.
:return: boolean value.
"""
return self.x == 0.0 and self.y == 0.0 and self.z == 0.0
def is_in_aabb(self, p1, p2):
""" Check coordinates are in aabb(Axis-Aligned Bounding Box).
aabb is specified with two points.
:param p1: First point in Coord object.
:param p2: Second point in Coord object.
:return: boolean value.
"""
minx, maxx = sorted((p1.x, p2.x))
miny, maxy = sorted((p1.y, p2.y))
minz, maxz = sorted((p1.z, p2.z))
if self.x < minx or self.y < miny or self.z < minz:
return False
if self.x > maxx or self.y > maxy or self.z > maxz:
return False
return True
def length(self):
""" Calculate the length of vector.
:return: Vector length.
"""
return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
def round(self, base):
""" Round values to specified base, ie 0.49 with base 0.25 will be 0.5.
:param base: Base.
:return: New rounded object.
"""
return Coordinates(round(self.x / base) * base,
round(self.y / base) * base,
round(self.z / base) * base)
def find_max(self):
""" Find a maximum value of all values.
:return: maximum value.
"""
return max(self.x, self.y, self.z)
# build in function implementation
def __add__(self, other):
return Coordinates(self.x + other.x, self.y + other.y, self.z + other.z)
def __sub__(self, other):
return Coordinates(self.x - other.x, self.y - other.y, self.z - other.z)
def __mul__(self, v):
return Coordinates(self.x * v, self.y * v, self.z * v)
def __div__(self, v):
return Coordinates(self.x / v, self.y / v, self.z / v)
def __truediv__(self, v):
return Coordinates(self.x / v, self.y / v, self.z / v)
def __eq__(self, other):
return self.x == other.x and self.y == other.y and self.z == other.z
def __str__(self):
return '(' + str(self.x) + ', ' + str(self.y) + ', ' + str(self.z) + ')'
def __abs__(self):
return Coordinates(abs(self.x), abs(self.y), abs(self.z))

11
deploy.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/sh
set -e
PASS=raspberry
ADDR=pi@192.168.0.208
if [ ! -z $1 ]; then
ADDR=$1
fi
tar -cvjSf $(dirname "$0")/pycnc.tar.bz2 $(dirname "$0")/*.py > /dev/null
sshpass -p${PASS} scp $(dirname "$0")/pycnc.tar.bz2 "${ADDR}:~/pycnc"
sshpass -p${PASS} ssh -t ${ADDR} "(cd ~/pycnc && tar xvf pycnc.tar.bz2) > /dev/null" &> /dev/null
sshpass -p${PASS} ssh -t ${ADDR} "sudo pypy ~/pycnc/main.py"

87
gcode.py Normal file
View File

@@ -0,0 +1,87 @@
#!/usr/bin/env python
import re
from coordinates import Coordinates
gpattern = re.compile('([A-Z])([-+]?[0-9.]+)')
cleanpattern = re.compile('\s+|\(.*?\)|;.*') # white spaces and comments start with ';' and in '()'
class GCodeException(Exception):
""" Exceptions while parsing gcode.
"""
pass
class GCode(object):
""" This object represent single line of gcode.
Do not create it manually, use parse_line() instead.
"""
def __init__(self, params):
""" Create object.
:param params: dict with gcode key-values.
"""
self.params = params
def get(self, argname, default=None, multiply=1.0):
""" Get value from gcode line.
:param argname: Value name.
:param default: Default value if value doesn't exist.
:param multiply: if value exist, multiply it by this value.
:return: Value if exists or default otherwise.
"""
if argname not in self.params:
return default
return float(self.params[argname]) * multiply
def getXYZ(self, default, multiply):
""" Get X, Y and Z values as Coord object.
:param default: Default values, if any of coords is not specified.
:param multiply: If value exist, multiply it by this value.
:return: Coord object.
"""
x = self.get('X', default.x, multiply)
y = self.get('Y', default.y, multiply)
z = self.get('Z', default.z, multiply)
return Coordinates(x, y, z)
def isXYZ(self):
""" Check if at least one of the coordinates is present.
:return: Boolean value.
"""
return 'X' in self.params or 'Y' in self.params or 'Z' in self.params
def command(self):
""" Get value from gcode line.
:return: String with command or None if no command specified.
"""
if 'G' in self.params:
return 'G' + self.params['G']
if 'M' in self.params:
return 'M' + self.params['M']
return None
@staticmethod
def parse_line(line):
""" Parse line.
:param line: String with gcode line.
:return: gcode objects.
"""
line = line.upper()
line = re.sub(cleanpattern, '', line)
if len(line) == 0:
return None
if line[0] == '%':
return None
m = gpattern.findall(line)
if not m:
raise GCodeException('gcode not found')
if len(''.join(["%s%s" % i for i in m])) != len(line):
raise GCodeException('extra characters in line')
params = dict(m)
if len(params) != len(m):
raise GCodeException('duplicated gcode entries')
if 'G' in params and 'M' in params:
raise GCodeException('g and m command found')
return GCode(params)

139
gmachine.py Normal file
View File

@@ -0,0 +1,139 @@
#!/usr/bin/env python
import time
import logging
import hal
from coordinates import Coordinates
from config import *
class GMachineException(Exception):
""" Exceptions while processing gcode line.
"""
pass
class GMachine(object):
""" Main object which control and keep state of whole machine: steppers,
spindle, extruder etc
"""
def __init__(self):
""" Initialization.
"""
self._position = Coordinates(0.0, 0.0, 0.0)
# init variables
self._velocity = 0
self._spindle_rpm = 0
self._pause = 0
self._local = None
self._convertCoordinates = 0
self._absoluteCoordinates = 0
self.reset()
hal.init()
def destroy(self):
self.home()
hal.join()
def reset(self):
""" Reinitialize all program configurable thing.
"""
self._velocity = 1000
self._spindle_rpm = 1000
self._pause = 0
self._local = Coordinates(0.0, 0.0, 0.0)
self._convertCoordinates = 1.0
self._absoluteCoordinates = True
def _spindle(self, spindle_speed):
hal.spindle_control(100.0 * spindle_speed / SPINDLE_MAX_RPM)
def _move(self, delta, velocity):
delta = delta.round(1.0 / STEPPER_PULSES_PER_MM)
if delta.is_zero():
return
np = self._position + delta
if not np.is_in_aabb(Coordinates(0.0, 0.0, 0.0),
Coordinates(TABLE_SIZE_X_MM, TABLE_SIZE_Y_MM, TABLE_SIZE_Z_MM)):
raise GMachineException("out of effective area")
hal.move_linear(delta, velocity)
# save position
self._position = np
def home(self):
""" Move head to park position
"""
d = Coordinates(0, 0, -self._position.z)
self._move(d, STEPPER_MAX_VELOCITY_MM_PER_MIN)
d = Coordinates(-self._position.x, -self._position.y, 0)
self._move(d, STEPPER_MAX_VELOCITY_MM_PER_MIN)
def do_command(self, gcode):
""" Perform action.
:param gcode: GCode object which represent one gcode line
"""
if gcode is None:
return
logging.debug("got command " + str(gcode.params))
# read command
c = gcode.command()
if c is None and gcode.isXYZ():
c = 'G1'
# read parameters
if self._absoluteCoordinates:
coord = gcode.getXYZ(self._position, self._convertCoordinates)
coord = coord + self._local
delta = coord - self._position
else:
delta = gcode.getXYZ(Coordinates(0.0, 0.0, 0.0), self._convertCoordinates)
coord = self._position + delta
velocity = gcode.get('F', self._velocity)
spindle_rpm = gcode.get('S', self._spindle_rpm)
pause = gcode.get('P', self._pause)
# check parameters
if velocity <= 0 or velocity > STEPPER_MAX_VELOCITY_MM_PER_MIN:
raise GMachineException("bad feed speed")
if spindle_rpm < 0 or spindle_rpm > SPINDLE_MAX_RPM:
raise GMachineException("bad spindle speed")
if pause < 0:
raise GMachineException("bad delay")
# select command and run it
if c == 'G0': # rapid move
self._move(delta, STEPPER_MAX_VELOCITY_MM_PER_MIN)
elif c == 'G1': # linear interpolation
self._move(delta, velocity)
elif c == 'G4': # delay in s
hal.join()
time.sleep(pause)
elif c == 'G20': # switch to inches
self._convertCoordinates = 25.4
elif c == 'G21': # switch to mm
self._convertCoordinates = 1.0
elif c == 'G28': # home
self.home()
elif c == 'G90': # switch to absolute coords
self._absoluteCoordinates = True
elif c == 'G91': # switch to relative coords
self._absoluteCoordinates = False
elif c == 'G92': # switch to local coords
self._local = self._position - \
gcode.getXYZ(Coordinates(0.0, 0.0, 0.0), self._convertCoordinates)
elif c == 'M3': # spinle on
self._spindle(spindle_rpm)
elif c == 'M5': # spindle off
self._spindle(0)
elif c == 'M2' or c == 'M30': # program finish, reset everything.
self.reset()
elif c == 'M111': # enable debug
logging.getLogger().setLevel(logging.DEBUG)
elif c is None: # command not specified(for example, just F was passed)
pass
else:
raise GMachineException("unknown command")
# save parameters on success
self._velocity = velocity
self._spindle_rpm = spindle_rpm
self._pause = pause
logging.debug("position {}, {}, {}".format(
self._position.x, self._position.y, self._position.z))

8
hal.py Normal file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env python
try:
from hal_rpi import *
except ImportError:
print("----- Hardware not detected, using virtual environment -----")
print("----- Use M111 command to enable more detailed debug -----")
from hal_virtual import *

154
hal_rpi.py Normal file
View File

@@ -0,0 +1,154 @@
#!/usr/bin/env python
import logging
import time
import rpgpio
from pulses import PulseGeneratorLinear
from coordinates import Coordinates
from config import *
# Stepper motors channel for RPIO
STEPPER_CHANNEL = 0
# Since there is no way to add pulses and then start cycle in RPIO,
# use this delay to start adding pulses to cycle. It can be easily
# solved by modifying RPIO in a way of adding method to start cycle
# explicitly.
RPIO_START_DELAY_US = 200000
# Since RPIO generate cycles in loop, use this delay to stop RPIO
# It can be removed if RPIO would allow to run single shot cycle.
RPIO_STOP_DELAY_US = 5000000
US_IN_SECONDS = 1000000
gpio = rpgpio.GPIO()
dma = rpgpio.DMAGPIO()
STEP_PIN_MASK_X = 1 << STEPPER_STEP_PIN_X
STEP_PIN_MASK_Y = 1 << STEPPER_STEP_PIN_Y
STEP_PIN_MASK_Z = 1 << STEPPER_STEP_PIN_Z
def init():
""" Initialize GPIO pins and machine itself, including callibration if
needed. Do not return till all procedure is completed.
"""
gpio.init(STEPPER_STEP_PIN_X, rpgpio.GPIO.MODE_OUTPUT)
gpio.init(STEPPER_STEP_PIN_Y, rpgpio.GPIO.MODE_OUTPUT)
gpio.init(STEPPER_STEP_PIN_Z, rpgpio.GPIO.MODE_OUTPUT)
gpio.init(STEPPER_DIR_PIN_X, rpgpio.GPIO.MODE_OUTPUT)
gpio.init(STEPPER_DIR_PIN_Y, rpgpio.GPIO.MODE_OUTPUT)
gpio.init(STEPPER_DIR_PIN_Z, rpgpio.GPIO.MODE_OUTPUT)
gpio.init(ENDSTOP_PIN_X, rpgpio.GPIO.MODE_INPUT_PULLUP)
gpio.init(ENDSTOP_PIN_X, rpgpio.GPIO.MODE_INPUT_PULLUP)
gpio.init(ENDSTOP_PIN_X, rpgpio.GPIO.MODE_INPUT_PULLUP)
# calibration
gpio.set(STEPPER_DIR_PIN_X)
gpio.set(STEPPER_DIR_PIN_Y)
gpio.set(STEPPER_DIR_PIN_Z)
pins = STEP_PIN_MASK_X | STEP_PIN_MASK_Y | STEP_PIN_MASK_Z
dma.clear()
dma.add_pulse(pins, STEPPER_PULSE_LINGTH_US)
while True:
if (STEP_PIN_MASK_X & pins) != 0 and gpio.read(ENDSTOP_PIN_X) == 0:
pins &= ~STEP_PIN_MASK_X
dma.clear()
dma.add_pulse(pins, STEPPER_PULSE_LINGTH_US)
if (STEP_PIN_MASK_Y & pins) != 0 and gpio.read(ENDSTOP_PIN_Y) == 0:
pins &= ~STEP_PIN_MASK_Y
dma.clear()
dma.add_pulse(pins, STEPPER_PULSE_LINGTH_US)
if (STEP_PIN_MASK_Z & pins) != 0 and gpio.read(ENDSTOP_PIN_Z) == 0:
pins &= ~STEP_PIN_MASK_Z
dma.clear()
dma.add_pulse(pins, STEPPER_PULSE_LINGTH_US)
if pins == 0:
break
dma.run(False)
# limit velocity at ~10% of top velocity
time.sleep((1 / 0.10) / (STEPPER_MAX_VELOCITY_MM_PER_MIN
/ 60 * STEPPER_PULSES_PER_MM))
def spindle_control(percent):
""" Spindle control implementation.
:param percent: Spindle speed in percent.
"""
# TODO spindle control.
logging.info("TODO spindle control: {}%".format(percent))
def move_linear(delta, velocity):
""" Move head to specified position
:param delta: Coordinated object, delta position in mm
:param velocity: velocity in mm per min
"""
logging.info("move {} with velocity {}".format(delta, velocity))
# initialize generator
generator = PulseGeneratorLinear(delta, velocity)
# wait if previous command still works
while dma.is_active():
time.sleep(0.001)
# set direction pins
if delta.x > 0.0:
gpio.clear(STEPPER_DIR_PIN_X)
else:
gpio.set(STEPPER_DIR_PIN_X)
if delta.y > 0.0:
gpio.clear(STEPPER_DIR_PIN_Y)
else:
gpio.set(STEPPER_DIR_PIN_Y)
if delta.z > 0.0:
gpio.clear(STEPPER_DIR_PIN_Z)
else:
gpio.set(STEPPER_DIR_PIN_Z)
# prepare and run dma
dma.clear()
prev = 0
is_ran = False
st = time.time()
for tx, ty, tz in generator:
pins = 0
k = int(round(min(x for x in (tx, ty, tz) if x is not None)
* US_IN_SECONDS))
if tx is not None:
pins |= STEP_PIN_MASK_X
if ty is not None:
pins |= STEP_PIN_MASK_Y
if tz is not None:
pins |= STEP_PIN_MASK_Z
if k - prev > 0:
dma.add_delay(k - prev)
dma.add_pulse(pins, STEPPER_PULSE_LINGTH_US)
# TODO not a precise way! pulses will set in queue, instead of crossing
# if next pulse start during pulse length. Though it almost doesn't
# matter for pulses with 1-2us length.
prev = k + STEPPER_PULSE_LINGTH_US
# instant run handling
if not is_ran and INSTANT_RUN:
if k > 500000: # wait at least 500 ms is uploaded
if time.time() - st > 0.5:
# may be instant run should be canceled here?
logging.warn("Buffer preparing for instant run took more "
"time then buffer time")
dma.run_stream()
is_ran = True
pt = time.time()
if not is_ran:
dma.run(False)
else:
dma.finalize_stream()
logging.info("prepared in " + str(round(pt - st, 2)) + "s, estimated in "
+ str(round(generator.total_time_s(), 2)) + "s")
def join():
""" Wait till motors work.
"""
# wait till dma works
while dma.is_active():
time.sleep(0.01)

91
hal_virtual.py Normal file
View File

@@ -0,0 +1,91 @@
#!/usr/bin/env python
import logging
import time
from pulses import PulseGeneratorLinear
from config import *
from coordinates import Coordinates
""" This is virtual device class which is very useful for debugging.
It checks PulseGenerator with some tests.
"""
def init():
""" Initialize GPIO pins and machine itself, including calibration if
needed. Do not return till all procedure is completed.
"""
logging.info("initialize hal")
def spindle_control(percent):
""" Spindle control implementation.
:param percent: Spindle speed in percent.
"""
logging.info("spindle control: {}%".format(percent))
def move_linear(delta, velocity):
""" Move head to specified position
:param delta: Coordinated object, delta position in mm
:param velocity: velocity in mm per min
"""
logging.info("move {} with velocity {}".format(delta, velocity))
ix = iy = iz = 0
generator = PulseGeneratorLinear(delta, velocity)
lx, ly, lz = None, None, None
dx, dy, dz = 0, 0, 0
mx, my, mz = 0, 0, 0
st = time.time()
for tx, ty, tz in generator:
if tx is not None:
if tx > mx:
mx = tx
tx = int(round(tx * 1000000))
ix += 1
if lx is not None:
dx = tx - lx
assert dx > 0, "negative or zero time delta detected for x"
lx = tx
else:
dx = None
if ty is not None:
if ty > my:
my = ty
ty = int(round(ty * 1000000))
iy += 1
if ly is not None:
dy = ty - ly
assert dy > 0, "negative or zero time delta detected for y"
ly = ty
else:
dy = None
if tz is not None:
if tz > mz:
mz = tz
tz = int(round(tz * 1000000))
iz += 1
if lz is not None:
dz = tz - lz
assert dz > 0, "negative or zero time delta detected for z"
lz = tz
else:
dz = None
# very verbose, uncomment on demand
# logging.debug("Iteration {} is {} {} {}".format(max(ix, iy, iz), tx, ty, tz))
f = list(x for x in (tx, ty, tz) if x is not None)
assert f.count(f[0]) == len(f), "fast forwarded pulse detected"
pt = time.time()
assert ix / STEPPER_PULSES_PER_MM == abs(delta.x), "x wrong number of pulses"
assert iy / STEPPER_PULSES_PER_MM == abs(delta.y), "y wrong number of pulses"
assert iz / STEPPER_PULSES_PER_MM == abs(delta.z), "z wrong number of pulses"
assert max(mx, my, mz) <= generator.total_time_s(), "interpolation time or pulses wrong"
logging.debug("Did {}, {}, {} iterations".format(ix, iy, iz))
logging.info("prepared in " + str(round(pt - st, 2)) \
+ "s, estimated " + str(round(generator.total_time_s(), 2)) + "s")
def join():
""" Wait till motors work.
"""
logging.info("hal join()")

58
main.py Executable file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env python
import sys
import readline
import logging
logging.basicConfig(level=logging.CRITICAL,
format='[%(levelname)s] %(message)s')
from gcode import GCode, GCodeException
from gmachine import GMachine, GMachineException
try: # python3 compatibility
type(raw_input)
except NameError:
raw_input = input
machine = GMachine()
def do_line(line):
try:
g = GCode.parse_line(line)
machine.do_command(g)
except (GCodeException, GMachineException) as e:
print('ERROR ' + str(e))
return False
print('OK')
return True
def main():
try:
if len(sys.argv) > 1:
# Read file with gcode
with open(sys.argv[1], 'r') as f:
for line in f:
line = line.strip()
print('> ' + line)
if not do_line(line):
break
else:
# Main loop for interactive shell
# Use stdin/stdout, additional interfaces like
# UART, Socket or any other can be added.
print("*************** Welcome to PyCNC! ***************")
while True:
line = raw_input('> ')
if line == 'quit' or line == 'exit':
break
do_line(line)
except KeyboardInterrupt:
pass
print("\r\nExiting...")
machine.destroy()
if __name__ == "__main__":
main()

227
pulses.py Normal file
View File

@@ -0,0 +1,227 @@
#!/usr/bin/env python
from __future__ import division
import math
import logging
from config import *
from coordinates import Coordinates
SECONDS_IN_MINUTE = 60
class PulseGenerator(object):
""" Stepper motors pulses generator.
It generates time for each pulses for specified path as accelerated
movement for specified velocity, then moves linearly and then braking
with the same acceleration.
Internally this class treat movement as uniform movement and then
translate timings to accelerated movements. To do so, it base on
formulas for distance of uniform movement and accelerated move.
S = V * Ta = a * Tu^2 / 2
where Ta - time for accelerated and Tu for uniform movement.
Velocity will never be more then Vmax - maximum velocity of all axises.
At the point of maximum velocity we change accelerated movement to
uniform, so we can translate time for accelerated movement with this
formula:
Ta(Tu) = a * Tu^2 / Vmax / 2
Now we need just to calculate how much time will accelerate and
brake will take and recalculate time for them. Linear part will be as
is. Since maximum velocity and acceleration is always the same, there
is the ACCELERATION_FACTOR_PER_SEC variable.
In the same way round or other interpolation can be implemented based on
this class.
Note: round interpolation would require direction change during movement.
It's not implemented yet.
"""
def __init__(self):
""" Create object. Do not create directly this object, inherit this class
and implement interpolation function and related methods.
All child have to call this method ( super().__init__() ).
"""
self._iteration_x = 0
self._iteration_y = 0
self._iteration_z = 0
self._acceleration_time_s = 0.0
self._linear_time_s = 0.0
self._2Vmax_per_a = 0.0
def _get_movement_parameters(self):
""" Get for interpolation. This method have to be reimplemented
in parent classes and should calculate 3 parameters.
:return: Tuple of three values:
acceleration_time_s: time for accelerating and breaking motors
during movement
linear_time_s: time for uniform movement, it is total movement
time minus acceleration and braking time
max_axis_velocity_mm_per_sec: maximum velocity of any of axis during
movement. Even if whole movement is
accelerated, this value should be
calculated as top velocity.
"""
raise NotImplemented
def _interpolation_function(self, pulse_number):
""" Get function for interpolation path. This function should returned
values as it is uniform movement. There is only one trick, function
must be expressed in terms of position, i.e. t = S / V for linear,
where S - distance would be increment on motor minimum step.
:param pulse_number: number of pulse.
:return: time for each axis or None if movement for axis is finished.
"""
raise NotImplemented
def __iter__(self):
""" Get iterator.
:return: iterable object.
"""
self._acceleration_time_s, self._linear_time_s, \
max_axis_velocity_mm_per_sec = self._get_movement_parameters()
# helper variable
self._2Vmax_per_a = 2.0 * max_axis_velocity_mm_per_sec \
/ STEPPER_MAX_ACCELERATION_MM_PER_S2
self._iteration_x = 0
self._iteration_y = 0
self._iteration_z = 0
logging.debug(', '.join("%s: %s" % i for i in vars(self).items()))
return self
def _to_accelerated_time(self, pt_s):
""" Internal function to translate uniform movement time to time for
accelerated movement.
:param pt_s: pseudo time of uniform movement.
:return: time for each axis or None if movement for axis is finished.
"""
# acceleration
# S = Tpseudo * Vmax = a * t^2 / 2
t = math.sqrt(pt_s * self._2Vmax_per_a)
if t <= self._acceleration_time_s:
return t
# linear
# pseudo acceleration time Tpseudo = t^2 / ACCELERATION_FACTOR_PER_SEC
t = self._acceleration_time_s + pt_s - (self._acceleration_time_s ** 2
/ self._2Vmax_per_a)
# pseudo breaking time
bt = t - self._acceleration_time_s - self._linear_time_s
if bt <= 0:
return t
# braking
# Vmax * Tpseudo = Vlinear * t - a * t^2 / 2
# V on start braking is Vlinear = Taccel * a = Tbreaking * a
# Vmax * Tpseudo = Tbreaking * a * t - a * t^2 / 2
return 2.0 * self._acceleration_time_s + self._linear_time_s \
- math.sqrt(self._acceleration_time_s ** 2 - self._2Vmax_per_a * bt)
def __next__(self):
# for python3
return self.next()
def next(self):
""" Iterate pulses.
:return: Tuple of three values for each axis which represent time for
the next pulse. If there is no pulses left None will be
returned.
"""
tx, ty, tz = self._interpolation_function(self._iteration_x,
self._iteration_y,
self._iteration_z)
# check condition to stop
if tx is None and ty is None and tz is None:
raise StopIteration
# convert to real time
m = min(x for x in (tx, ty, tz) if x is not None)
am = self._to_accelerated_time(m)
# sort pulses in time
if tx is not None:
if tx > m:
tx = None
else:
tx = am
self._iteration_x += 1
if ty is not None:
if ty > m:
ty = None
else:
ty = am
self._iteration_y += 1
if tz is not None:
if tz > m:
tz = None
else:
tz = am
self._iteration_z += 1
return tx, ty, tz
def total_time_s(self):
""" Get total time for movement.
:return: time in seconds.
"""
acceleration_time_s, linear_time_s, _ = self._get_movement_parameters()
return acceleration_time_s * 2.0 + linear_time_s
class PulseGeneratorLinear(PulseGenerator):
def __init__(self, delta_mm, velocity_mm_per_min):
""" Create pulse generator for linear interpolation.
:param delta_mm: movement distance of each axis.
:param velocity_mm_per_min: desired velocity.
"""
super(PulseGeneratorLinear, self).__init__()
# this class doesn't care about direction
self._distance_mm = abs(delta_mm)
# velocity of each axis
distance_xyz_mm = self._distance_mm.length()
self.max_velocity_mm_per_sec = self._distance_mm * (
velocity_mm_per_min / SECONDS_IN_MINUTE / distance_xyz_mm)
# acceleration time
self.acceleration_time_s = self.max_velocity_mm_per_sec.find_max() \
/ STEPPER_MAX_ACCELERATION_MM_PER_S2
# check if there is enough space to accelerate and brake, adjust time
# S = a * t^2 / 2
if STEPPER_MAX_ACCELERATION_MM_PER_S2 * self.acceleration_time_s ** 2 \
> distance_xyz_mm:
self.acceleration_time_s = math.sqrt(distance_xyz_mm /
STEPPER_MAX_ACCELERATION_MM_PER_S2)
self.linear_time_s = 0.0
# V = a * t -> V = 2 * S / t, take half of total distance for acceleration and braking
self.max_velocity_mm_per_sec = self._distance_mm / self.acceleration_time_s
else:
# calculate linear time
linear_distance_mm = distance_xyz_mm\
- self.acceleration_time_s ** 2 \
* STEPPER_MAX_ACCELERATION_MM_PER_S2
self.linear_time_s = linear_distance_mm \
/ self.max_velocity_mm_per_sec.length()
def _get_movement_parameters(self):
""" Return movement parameters, see super class for details.
"""
return self.acceleration_time_s, \
self.linear_time_s, \
self.max_velocity_mm_per_sec.find_max()
def __linear(self, position_mm, distance_mm, velocity_mm_per_sec):
""" Helper function for linear movement.
"""
# check if need to calculate for this axis
if distance_mm == 0.0 or position_mm >= distance_mm:
return None
# Linear movement, S = V * t -> t = S / V
return position_mm / velocity_mm_per_sec
def _interpolation_function(self, ix, iy, iz):
""" Calculate interpolation values for linear movement, see super class
for details.
"""
t_x = self.__linear(ix / STEPPER_PULSES_PER_MM, self._distance_mm.x,
self.max_velocity_mm_per_sec.x)
t_y = self.__linear(iy / STEPPER_PULSES_PER_MM, self._distance_mm.y,
self.max_velocity_mm_per_sec.y)
t_z = self.__linear(iz / STEPPER_PULSES_PER_MM, self._distance_mm.z,
self.max_velocity_mm_per_sec.z)
return t_x, t_y, t_z

290
rpgpio.py Executable file
View File

@@ -0,0 +1,290 @@
#!/usr/bin/env python
from rpgpio_private import *
import time
import logging
import sys
import struct
class GPIO(object):
MODE_OUTPUT = 1
MODE_INPUT_NOPULL = 2
MODE_INPUT_PULLUP = 3
MODE_INPUT_PULLDOWN = 4
def __init__(self):
""" Create object which can control GPIO.
This class writes directly to CPU registers and doesn't use any libs
or kernel modules.
"""
self._mem = PhysicalMemory(PERI_BASE + GPIO_REGISTER_BASE)
def _pullupdn(self, pin, mode):
p = self._mem.read_int(GPIO_PULLUPDN_OFFSET)
p &= ~3
if mode == self.MODE_INPUT_PULLUP:
p |= 2
elif mode == self.MODE_INPUT_PULLDOWN:
p |= 1
self._mem.write_int(GPIO_PULLUPDN_OFFSET, p)
addr = 4 * int(pin / 32) + GPIO_PULLUPDNCLK_OFFSET
self._mem.write_int(addr, 1 << (pin % 32))
p = self._mem.read_int(GPIO_PULLUPDN_OFFSET)
p &= ~3
self._mem.write_int(GPIO_PULLUPDN_OFFSET, p)
self._mem.write_int(addr, 0)
def init(self, pin, mode):
""" Initialize or re-initialize GPIO pin.
:param pin: pin number.
:param mode: one of MODE_* variables in this class.
"""
addr = 4 * int(pin / 10) + GPIO_FSEL_OFFSET
v = self._mem.read_int(addr)
v &= ~(7 << ((pin % 10) * 3)) # input value
if mode == self.MODE_OUTPUT:
v |= (1 << ((pin % 10) * 3)) # output value, base on input
self._mem.write_int(addr, v)
else:
self._mem.write_int(addr, v)
self._pullupdn(pin, mode)
def set(self, pin):
""" Set pin to HIGH state.
:param pin: pin number.
"""
addr = 4 * int(pin / 32) + GPIO_SET_OFFSET
self._mem.write_int(addr, 1 << (pin % 32))
def clear(self, pin):
""" Set pin to LOW state.
:param pin: pin number.
"""
addr = 4 * int(pin / 32) + GPIO_CLEAR_OFFSET
self._mem.write_int(addr, 1 << (pin % 32))
def read(self, pin):
""" Read pin current value.
:param pin: pin number.
:return: integer value 0 or 1.
"""
addr = 4 * int(pin / 32) + GPIO_INPUT_OFFSET
v = self._mem.read_int(addr)
v &= 1 << (pin % 32)
if v == 0:
return 0
return 1
class DMAGPIO(object):
_DMA_CONTROL_BLOCK_SIZE = 32
_DMA_CHANNEL = 5
def __init__(self):
""" Create object which control GPIO pins via DMA(Direct Memory
Access).
This object allows to add arbitrary sequence of pulses to any GPIO
outputs and run this sequence in background without using CPU since
DMA is a separated hardware module.
Note: keep this object out of garbage collector until it stops,
otherwise memory will be unlocked and it could be overwritten by
operating system.
"""
# allocate buffer for control blocks, always 32 MB
self._physmem = CMAPhysicalMemory(32 * 1024 * 1024)
self.__current_address = 0
# prepare dma registers memory map
self._dma = PhysicalMemory(PERI_BASE + DMA_BASE)
self._pwm = PhysicalMemory(PERI_BASE + PWM_BASE)
self._clock = PhysicalMemory(PERI_BASE + CM_BASE)
# pre calculated variables for control blocks
self._delay_info = DMA_TI_NO_WIDE_BURSTS | DMA_SRC_IGNORE \
| DMA_TI_PER_MAP(DMA_TI_PER_MAP_PWM) \
| DMA_TI_DEST_DREQ
self._delay_destination = PHYSICAL_PWM_BUS + 0x18
self._delay_stride = 0
self._pulse_info = DMA_TI_NO_WIDE_BURSTS | DMA_TI_TDMODE \
| DMA_TI_WAIT_RESP
self._pulse_destination = PHYSICAL_GPIO_BUS + GPIO_SET_OFFSET
# YLENGTH is transfers count and XLENGTH size of each transfer
self._pulse_length = DMA_TI_TXFR_LEN_YLENGTH(2) \
| DMA_TI_TXFR_LEN_XLENGTH(4)
self._pulse_stride = DMA_TI_STRIDE_D_STRIDE(12) \
| DMA_TI_STRIDE_S_STRIDE(4)
def add_pulse(self, pins_mask, length_us):
""" Add single pulse at the current position.
:param pins_mask: bitwise mask of GPIO pins to trigger. Only for first 32 pins.
:param length_us: length in us.
"""
next_cb = self.__current_address + 3 * self._DMA_CONTROL_BLOCK_SIZE
if next_cb > self._physmem.get_size():
raise MemoryError("Out of allocated memory.")
next3 = next_cb + self._physmem.get_bus_address()
next2 = next3 - self._DMA_CONTROL_BLOCK_SIZE
next1 = next2 - self._DMA_CONTROL_BLOCK_SIZE
source1 = next1 - 8 # last 8 bytes are padding, use it to store data
length2 = 16 * length_us
source3 = next3 - 8
data = (
self._pulse_info, source1, self._pulse_destination,
self._pulse_length,
self._pulse_stride, next1, pins_mask, 0,
self._delay_info, 0, self._delay_destination, length2,
self._delay_stride, next2, 0, 0,
self._pulse_info, source3, self._pulse_destination,
self._pulse_length,
self._pulse_stride, next3, 0, pins_mask
)
self._physmem.write(self.__current_address, data)
self.__current_address = next_cb
def add_delay(self, delay_us):
""" Add delay at the current position.
:param delay_us: delay in us.
"""
next_cb = self.__current_address + self._DMA_CONTROL_BLOCK_SIZE
if next_cb > self._physmem.get_size():
raise MemoryError("Out of allocated memory.")
next = self._physmem.get_bus_address() + next_cb
source = next - 8 # last 8 bytes are padding, use it to store data
length = 16 * delay_us
data = (
self._delay_info, source, self._delay_destination, length,
self._delay_stride, next, 0, 0
)
self._physmem.write(self.__current_address, data)
self.__current_address = next_cb
def finalize_stream(self):
""" Mark last added block as the last one.
"""
self._physmem.write_int(self.__current_address + 20
- self._DMA_CONTROL_BLOCK_SIZE, 0)
logging.info("DMA took {}MB of memory".
format(round(self.__current_address / 1024.0 / 1024.0, 2)))
def run_stream(self):
""" Run DMA module in stream mode, i.e. does'n finalize last block
and do not check if there is anything to do.
"""
# configure PWM hardware module which will clocks DMA
self._pwm.write_int(PWM_CTL, 0)
self._clock.write_int(CM_CNTL, CM_PASSWORD | CM_SRC_PLLD) # disable
while (self._clock.read_int(CM_CNTL) & (1 << 7)) != 0:
time.sleep(0.00001) # 10 us, wait until BUSY bit is clear
self._clock.write_int(CM_DIV, CM_PASSWORD | CM_DIV_VALUE(50)) # 10MHz
self._clock.write_int(CM_CNTL, CM_PASSWORD | CM_SRC_PLLD | CM_ENABLE)
self._pwm.write_int(PWM_RNG1, 100)
self._pwm.write_int(PWM_DMAC, PWM_DMAC_ENAB
| PWM_DMAC_PANIC(15) | PWM_DMAC_DREQ(15))
self._pwm.write_int(PWM_CTL, PWM_CTL_CLRF)
self._pwm.write_int(PWM_CTL, PWM_CTL_USEF1 | PWM_CTL_PWEN1)
# configure DMA
addr = 0x100 * self._DMA_CHANNEL
cs = self._dma.read_int(addr)
cs |= DMA_CS_END
self._dma.write_int(addr, cs)
self._dma.write_int(addr + 4, self._physmem.get_bus_address())
cs = DMA_CS_PRIORITY(7) | DMA_CS_PANIC_PRIORITY(7) | DMA_CS_DISDEBUG
self._dma.write_int(addr, cs)
cs |= DMA_CS_ACTIVE
self._dma.write_int(addr, cs)
def run(self, loop=False):
""" Run DMA module and start sending specified pulses.
:param loop: If true, run pulse sequence in infinite loop. Otherwise
"""
if self.__current_address == 0:
raise RuntimeError("Nothing was added.")
# fix 'next' field in previous control block
if loop:
self._physmem.write_int(self.__current_address + 20
- self._DMA_CONTROL_BLOCK_SIZE,
self._physmem.get_bus_address())
else:
self.finalize_stream()
self.run_stream()
def stop(self):
""" Stop any DMA activities.
"""
self._pwm.write_int(PWM_CTL, 0)
addr = 0x100 * self._DMA_CHANNEL
cs = self._dma.read_int(addr)
cs |= DMA_CS_ABORT
self._dma.write_int(addr, cs)
cs &= ~DMA_CS_ACTIVE
self._dma.write_int(addr, cs)
cs |= DMA_CS_RESET
self._dma.write_int(addr, cs)
def is_active(self):
""" Check if DMA is working. Method can check if single sent sequence
still active.
:return: boolean value
"""
addr = 0x100 * self._DMA_CHANNEL
cs = self._dma.read_int(addr)
if cs & DMA_CS_ACTIVE == DMA_CS_ACTIVE:
return True
return False
def clear(self):
""" Remove any specified pulses.
"""
self.__current_address = 0
# for testing purpose
def main():
pin = 21
g = GPIO()
g.init(pin, GPIO.MODE_INPUT_NOPULL)
print("nopull " + str(g.read(pin)))
g.init(pin, GPIO.MODE_INPUT_PULLDOWN)
print("pulldown " + str(g.read(pin)))
g.init(pin, GPIO.MODE_INPUT_PULLUP)
print("pullup " + str(g.read(pin)))
time.sleep(1)
g.init(pin, GPIO.MODE_OUTPUT)
g.set(pin)
print("set " + str(g.read(pin)))
time.sleep(1)
g.clear(pin)
print("clear " + str(g.read(pin)))
time.sleep(1)
cma = CMAPhysicalMemory(1*1024*1024)
print(str(cma.get_size() / 1024 / 1024) + "MB of memory allocated at " \
+ hex(cma.get_phys_address()))
a = cma.read_int(0)
print("was " + hex(a))
cma.write_int(0, 0x12345678)
a = cma.read_int(0)
assert a == 0x12345678, "Memory isn't written or read correctly"
print("now " + hex(a))
del cma
dg = DMAGPIO()
dg.add_pulse(1 << pin, 4000)
dg.add_delay(12000)
dg.run(True)
print("dmagpio is started")
try:
print("press enter to stop...")
sys.stdin.readline()
except KeyboardInterrupt:
pass
dg.stop()
g.clear(pin)
print("dma stopped")
if __name__ == "__main__":
main()

200
rpgpio_private.py Normal file
View File

@@ -0,0 +1,200 @@
#!/usr/bin/env python
import os
import mmap
import struct
import re
import fcntl
import array
import atexit
import ctypes
# Raspberry Pi registers
# https://www.raspberrypi.org/wp-content/uploads/2012/02/BCM2835-ARM-Peripherals.pdf
RPI1_PERI_BASE = 0x20000000
RPI2_3_PERI_BASE = 0x3F000000
# detect board version
with open("/proc/cpuinfo", "r") as f:
d = f.read()
r = re.search("^Revision\s+:\s+(.+)$", d, flags=re.MULTILINE)
h = re.search("^Hardware\s+:\s+(.+)$", d, flags=re.MULTILINE)
RPI_1_REVISIONS = ['0002', '0003', '0004', '0005', '0006', '0007', '0008',
'0009', '000d', '000e', '000f', '0010', '0011', '0012',
'0013', '0014', '0015', '900021', '900032']
if h is None:
raise ImportError("This is not raspberry pi board.")
elif r.group(1) in RPI_1_REVISIONS:
PERI_BASE = RPI1_PERI_BASE
elif "BCM2" in h.group(1):
PERI_BASE = RPI2_3_PERI_BASE
else:
raise ImportError("Unknown board.")
PAGE_SIZE = 4096
GPIO_REGISTER_BASE = 0x200000
GPIO_INPUT_OFFSET = 0x34
GPIO_SET_OFFSET = 0x1C
GPIO_CLEAR_OFFSET = 0x28
GPIO_FSEL_OFFSET = 0x0
GPIO_PULLUPDN_OFFSET = 0x94
GPIO_PULLUPDNCLK_OFFSET = 0x98
PHYSICAL_GPIO_BUS = 0x7E000000 + GPIO_REGISTER_BASE
# registers and values for DMA
DMA_BASE = 0x007000
DMA_TI_NO_WIDE_BURSTS = 1 << 26
DMA_TI_SRC_INC = 1 << 8
DMA_TI_DEST_INC = 1 << 4
DMA_SRC_IGNORE = 1 << 11
DMA_DEST_IGNORE = 1 << 7
DMA_TI_TDMODE = 1 << 1
DMA_TI_WAIT_RESP = 1 << 3
DMA_TI_SRC_DREQ = 1 << 10
DMA_TI_DEST_DREQ = 1 << 6
DMA_CS_RESET = 1 << 31
DMA_CS_ABORT = 1 << 30
DMA_CS_DISDEBUG = 1 << 28
DMA_CS_END = 1 << 1
DMA_CS_ACTIVE = 1 << 0
DMA_TI_PER_MAP_PWM = 5
DMA_TI_PER_MAP_PCM = 2
def DMA_TI_PER_MAP(x):
return x << 16
def DMA_TI_TXFR_LEN_YLENGTH(y):
return (y & 0x3fff) << 16
def DMA_TI_TXFR_LEN_XLENGTH(x):
return x & 0xffff
def DMA_TI_STRIDE_D_STRIDE(x):
return (x & 0xffff) << 16
def DMA_TI_STRIDE_S_STRIDE(x):
return x & 0xffff
def DMA_CS_PRIORITY(x):
return (x & 0xf) << 16
def DMA_CS_PANIC_PRIORITY(x):
return (x & 0xf) << 20
# hardware PWM controller registers
PWM_BASE = 0x0020C000
PWM_CTL= 0x00
PWM_DMAC = 0x08
PWM_RNG1 = 0x10
PWM_FIFO = 0x18
PWM_CTL_MODE1 = 1 << 1
PWM_CTL_PWEN1 = 1 << 0
PWM_CTL_CLRF = 1 << 6
PWM_CTL_USEF1 = 1 << 5
PWM_DMAC_ENAB = 1 << 31
PHYSICAL_PWM_BUS = 0x7E000000 + PWM_BASE
def PWM_DMAC_PANIC(x):
return x << 8
def PWM_DMAC_DREQ(x):
return x
# clock manager module
CM_BASE = 0x00101000
CM_CNTL = 40
CM_DIV = 41
CM_PASSWORD = 0x5A << 24
CM_ENABLE = 1 << 4
CM_SRC_OSC = 1 # 19.2 MHz
CM_SRC_PLLC = 5 # 1000 MHz
CM_SRC_PLLD = 6 # 500 MHz
CM_SRC_HDMI = 7 # 216 MHz
def CM_DIV_VALUE(x):
return x << 12
class PhysicalMemory(object):
def __init__(self, phys_address, size=PAGE_SIZE):
""" Create object which maps physical memory to Python's mmap object.
:param phys_address: based address of physical memory
"""
self._size = size
phys_address -= phys_address % PAGE_SIZE
fd = self._open_dev("/dev/mem")
self._rmap = mmap.mmap(fd, size, flags=mmap.MAP_SHARED,
prot=mmap.PROT_READ | mmap.PROT_WRITE,
offset=phys_address)
atexit.register(self.cleanup)
def cleanup(self):
self._rmap.close()
@staticmethod
def _open_dev(name):
fd = os.open(name, os.O_SYNC | os.O_RDWR | os.O_LARGEFILE)
if fd < 0:
raise IOError("Failed to open " + name)
return fd
@staticmethod
def _close_dev(fd):
os.close(fd)
def write_int(self, address, int_value):
self._rmap[address:address + 4] = struct.pack("I", int_value)
def write(self, address, data):
self._rmap.seek(address)
self._rmap.write(struct.pack(str(len(data)) + "I", *data))
def read_int(self, address):
return struct.unpack("I", self._rmap[address:address + 4])[0]
def get_size(self):
return self._size
class CMAPhysicalMemory(PhysicalMemory):
IOCTL_MBOX_PROPERTY = ctypes.c_long(0xc0046400).value
def __init__(self, size):
""" This class allocates continuous memory with specified size, lock it
and provide access to it with Python's mmap. It uses RPi video
buffers to allocate it (/dev/vcio).
:param size: number of bytes to allocate
"""
size = (size + PAGE_SIZE - 1) // PAGE_SIZE * PAGE_SIZE
self._vcio_fd = self._open_dev("/dev/vcio")
self._handle = self._send_data(0x3000c, [size, PAGE_SIZE, 0xC]) # allocate memory
if self._handle == 0:
raise OSError("No memory to allocate with /dev/vcio")
self._busmem = self._send_data(0x3000d, [self._handle]) # lock memory
if self._busmem == 0:
# memory should be freed in __del__
raise OSError("Failed to lock memory with /dev/vcio")
# print("allocate {} at {} (bus {})".format(size,
# hex(self.get_phys_address()), hex(self.get_bus_address())))
super(CMAPhysicalMemory, self).__init__(self.get_phys_address(), size)
atexit.register(self.free)
def free(self):
self._send_data(0x3000e, [self._handle]) # unlock memory
self._send_data(0x3000f, [self._handle]) # free memory
self._close_dev(self._vcio_fd)
def _send_data(self, request, args):
data = array.array('I')
data.append(24 + 4 * len(args)) # total size
data.append(0) # process request
data.append(request) # request id
data.append(4 * len(args)) # size of the buffer
data.append(4 * len(args)) # size of the data
data.extend(args) # arguments
data.append(0) # end mark
fcntl.ioctl(self._vcio_fd, self.IOCTL_MBOX_PROPERTY, data, True)
return data[5]
def get_bus_address(self):
return self._busmem
def get_phys_address(self):
return self._busmem & ~0xc0000000

35
tests/rects.gcode Normal file
View File

@@ -0,0 +1,35 @@
g21
g90
; move to start position
g1x50y50f1800
z20
g91
; run
x100
y100
x-100
y-90
x90
y80
x-80
y-70
x70
y60
x-60
y-50
x50
y40
x-40
y-30
x30
y20
x-20
y-10
x10
y5
x-5
y-2.5
x2.5
; homing
g28

10
tests/rpgpio-test.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/sh
set -e
PASS=raspberry
ADDR=pi@192.168.0.208
if [ ! -z $1 ]; then
ADDR=$1
fi
sshpass -p${PASS} scp $(dirname "$0")/../rpgpio_private.py "${ADDR}:~"
sshpass -p${PASS} scp $(dirname "$0")/../rpgpio.py "${ADDR}:~"
sshpass -p${PASS} ssh -t ${ADDR} "sudo ~/rpgpio.py"

28
tests/test-parser.gcode Normal file
View File

@@ -0,0 +1,28 @@
%
g28
n1 g1x0y0z0
n1 g1 x1 y1 z1 ; check if comments is ok
g1(check if inline comments work)x2y2(two times)z2
(or on separated line)
; or like this
m3s04000
g4 p0.5
m5
f500
g91
g20
g0x1
g0x1
g0x1
g0x-1
g0x-1
g0x-1
g21
g1y1
g1y-1
g90
g92x100y100z100
m111
g1x98y98z98
(head should be in zero position, and last movent with 500 mm/min velocity)
m2