reorganise project

This commit is contained in:
Nikolay Khabarov
2017-05-13 03:52:36 +03:00
parent 82d0f5f016
commit 1391b03724
17 changed files with 69 additions and 34 deletions
View File
+152
View File
@@ -0,0 +1,152 @@
import logging
import time
import rpgpio
from cnc.pulses import PulseGeneratorLinear
from cnc.coordinates import Coordinates
from cnc.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)
+289
View File
@@ -0,0 +1,289 @@
#!/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()
+198
View File
@@ -0,0 +1,198 @@
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