spindle implementation

This commit is contained in:
Nikolay Khabarov
2017-05-14 02:03:19 +03:00
parent 7229bc1c4f
commit 12894f2795
7 changed files with 217 additions and 47 deletions
+8 -4
View File
@@ -20,19 +20,23 @@ G0, G1, G4, G20, G21, G28, G90, G91, G92, M2, M3, M5, M30
Commands can be easily added, see [gmachine.py](./cnc/gmachine.py) file. Commands can be easily added, see [gmachine.py](./cnc/gmachine.py) file.
# Config # Config
All config is stored in [config.py](./cnc/config.py) and contains hardware All configs are stored in [config.py](./cnc/config.py) and contain hardware
properties, limitations and pin names for hardware control. properties, limitations and pin names for hardware control.
Raspberry Pi implementation should be connected to A4988, DRV8825 or any other Raspberry Pi implementation should be connected to A4988, DRV8825 or any other
stepper motor controllers with DIR and STEP pin inputs. stepper motor drivers with DIR and STEP pin inputs.
Default config is created for Raspberry Pi 2-3 and this wiring diagram: Default config is created for Raspberry Pi 2-3 and this wiring diagram:
![](https://cloud.githubusercontent.com/assets/8740775/26024664/bc13d5a6-37de-11e7-98ed-9391109fcfd0.jpg) ![](https://cloud.githubusercontent.com/assets/8740775/26024664/bc13d5a6-37de-11e7-98ed-9391109fcfd0.jpg)
So having Raspberry Pi connected this was, there is no need to configure So having Raspberry Pi connected this way, there is no need to configure
pin map for project. pin map for project.
_Note: spindle control is not implemented yet_
# Hardware # Hardware
Currently, this project supports Raspberry Pi 1-3. Tested with RPI2. But there Currently, this project supports Raspberry Pi 1-3. Tested with RPI2. But there
is a way to add new boards. See [hal.py](./cnc/hal.py) file. is a way to add new boards. See [hal.py](./cnc/hal.py) file.
_Note: Current Raspberry Pi implementation uses the same resources as on board
GPU(memory). So video output will not work with this project. Use ssh
connection to board. And do not connect HDMI cable, otherwise project would not
run. Probably, increasing of GPU dedicated memory(at least to 64 MB) could solve
it and allow to work project and GPU together, but it was never tested._
# Usage # Usage
Just clone this repo and run `./pycnc` from repo root. It will start in Just clone this repo and run `./pycnc` from repo root. It will start in
+3 -1
View File
@@ -34,8 +34,9 @@ class GMachine(object):
def release(self): def release(self):
""" Return machine to original position and free all resources. """ Return machine to original position and free all resources.
""" """
self._spindle(0)
self.home() self.home()
hal.join() hal.deinit()
def reset(self): def reset(self):
""" Reinitialize all program configurable thing. """ Reinitialize all program configurable thing.
@@ -48,6 +49,7 @@ class GMachine(object):
self._absoluteCoordinates = True self._absoluteCoordinates = True
def _spindle(self, spindle_speed): def _spindle(self, spindle_speed):
hal.join()
hal.spindle_control(100.0 * spindle_speed / SPINDLE_MAX_RPM) hal.spindle_control(100.0 * spindle_speed / SPINDLE_MAX_RPM)
def _move(self, delta, velocity): def _move(self, delta, velocity):
+7
View File
@@ -29,6 +29,11 @@
# """ Wait till motors work. # """ Wait till motors work.
# """ # """
# do_something() # do_something()
#
# def deinit():
# """ De-initialise hal, stop any hardware.
# """
# do_something()
# check which module to import # check which module to import
@@ -48,4 +53,6 @@ if 'move_linear' not in locals():
raise NotImplementedError("hal.move_linear() not implemented") raise NotImplementedError("hal.move_linear() not implemented")
if 'join' not in locals(): if 'join' not in locals():
raise NotImplementedError("hal.join() not implemented") raise NotImplementedError("hal.join() not implemented")
if 'deinit' not in locals():
raise NotImplementedError("hal.deinit() not implemented")
+17 -3
View File
@@ -22,6 +22,7 @@ US_IN_SECONDS = 1000000
gpio = rpgpio.GPIO() gpio = rpgpio.GPIO()
dma = rpgpio.DMAGPIO() dma = rpgpio.DMAGPIO()
pwm = rpgpio.DMAPWM()
STEP_PIN_MASK_X = 1 << STEPPER_STEP_PIN_X STEP_PIN_MASK_X = 1 << STEPPER_STEP_PIN_X
STEP_PIN_MASK_Y = 1 << STEPPER_STEP_PIN_Y STEP_PIN_MASK_Y = 1 << STEPPER_STEP_PIN_Y
@@ -40,6 +41,8 @@ def init():
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) 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)
gpio.init(SPINDLE_PWM_PIN, rpgpio.GPIO.MODE_OUTPUT)
gpio.clear(SPINDLE_PWM_PIN)
# calibration # calibration
gpio.set(STEPPER_DIR_PIN_X) gpio.set(STEPPER_DIR_PIN_X)
@@ -71,10 +74,13 @@ def init():
def spindle_control(percent): def spindle_control(percent):
""" Spindle control implementation. """ Spindle control implementation.
:param percent: Spindle speed in percent. :param percent: Spindle speed in percent. If 0, stop the spindle.
""" """
# TODO spindle control. logging.info("spindle control: {}%".format(percent))
logging.info("TODO spindle control: {}%".format(percent)) if percent > 0:
pwm.add_pin(SPINDLE_PWM_PIN, percent)
else:
pwm.remove_pin(SPINDLE_PWM_PIN)
def move_linear(delta, velocity): def move_linear(delta, velocity):
@@ -147,6 +153,14 @@ def move_linear(delta, velocity):
def join(): def join():
""" Wait till motors work. """ Wait till motors work.
""" """
logging.info("hal join()")
# wait till dma works # wait till dma works
while dma.is_active(): while dma.is_active():
time.sleep(0.01) time.sleep(0.01)
def deinit():
""" De-initialize hardware.
"""
join()
pwm.remove_all()
+127 -39
View File
@@ -78,10 +78,12 @@ class GPIO(object):
return 1 return 1
class DMAGPIO(object): # When DMAGPIO is an active with two channels simultaneously, delay time shifts
# a little bit, because all DMA channels query the same PWM(which is used as
# clock for delay). So, do not create two or more instances of DMAGPIO.
class DMAGPIO(DMAProto):
_DMA_CONTROL_BLOCK_SIZE = 32 _DMA_CONTROL_BLOCK_SIZE = 32
_DMA_CHANNEL = 5 _DMA_CHANNEL = 5
def __init__(self): def __init__(self):
""" Create object which control GPIO pins via DMA(Direct Memory """ Create object which control GPIO pins via DMA(Direct Memory
Access). Access).
@@ -92,12 +94,11 @@ class DMAGPIO(object):
otherwise memory will be unlocked and it could be overwritten by otherwise memory will be unlocked and it could be overwritten by
operating system. operating system.
""" """
# allocate buffer for control blocks, always 32 MB super(DMAGPIO, self).__init__(31 * 1024 * 1024)
self._physmem = CMAPhysicalMemory(32 * 1024 * 1024)
self.__current_address = 0 self.__current_address = 0
# prepare dma registers memory map # get helpers registers, this class uses PWM module to create precise
self._dma = PhysicalMemory(PERI_BASE + DMA_BASE) # delays
self._pwm = PhysicalMemory(PERI_BASE + PWM_BASE) self._pwm = PhysicalMemory(PERI_BASE + PWM_BASE)
self._clock = PhysicalMemory(PERI_BASE + CM_BASE) self._clock = PhysicalMemory(PERI_BASE + CM_BASE)
@@ -119,7 +120,10 @@ class DMAGPIO(object):
def add_pulse(self, pins_mask, length_us): def add_pulse(self, pins_mask, length_us):
""" Add single pulse at the current position. """ Add single pulse at the current position.
:param pins_mask: bitwise mask of GPIO pins to trigger. Only for first 32 pins. Note: GPIO pins are not initialized in this method and should be
initialized in advance before running.
:param pins_mask: bitwise mask of GPIO pins to trigger. Only for
first 32 pins.
:param length_us: length in us. :param length_us: length in us.
""" """
next_cb = self.__current_address + 3 * self._DMA_CONTROL_BLOCK_SIZE next_cb = self.__current_address + 3 * self._DMA_CONTROL_BLOCK_SIZE
@@ -187,16 +191,7 @@ class DMAGPIO(object):
| PWM_DMAC_PANIC(15) | PWM_DMAC_DREQ(15)) | 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_CLRF)
self._pwm.write_int(PWM_CTL, PWM_CTL_USEF1 | PWM_CTL_PWEN1) self._pwm.write_int(PWM_CTL, PWM_CTL_USEF1 | PWM_CTL_PWEN1)
# configure DMA super(DMAGPIO, self)._run_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): def run(self, loop=False):
""" Run DMA module and start sending specified pulses. """ Run DMA module and start sending specified pulses.
@@ -217,32 +212,115 @@ class DMAGPIO(object):
""" Stop any DMA activities. """ Stop any DMA activities.
""" """
self._pwm.write_int(PWM_CTL, 0) self._pwm.write_int(PWM_CTL, 0)
addr = 0x100 * self._DMA_CHANNEL super(DMAGPIO, self)._stop_dma()
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): def clear(self):
""" Remove any specified pulses. """ Remove any specified pulses. Doesn't affect currently running
sequence.
""" """
self.__current_address = 0 self.__current_address = 0
class DMAPWM(DMAProto):
_DMA_CHANNEL = 14
_DMA_CONTROL_BLOCK_SIZE = 32
_DMA_DATA_OFFSET = 24
_TOTAL_NUMBER_OF_BLOCKS = 256
def __init__(self):
""" Initialise PWM. PWM has 8 bit resolution and fixed frequency
(~11.5 KHz and may flow). Though duty cycle is quite precise and
it uses the minimum amount of system resources (just one lite DMA
channel without any anything else).
That's why such PWM is best to use with collector motors, heaters
and other non sensitive hardware.
Implementation is super simple and uses lite DMA channel.
Overall frequency depends on number of blocks.
To adjust frequency, just write more byte per operation, use Wait
Cycles in info field of control blocks.
"""
super(DMAPWM, self).__init__(self._TOTAL_NUMBER_OF_BLOCKS
* self._DMA_CONTROL_BLOCK_SIZE)
self._clear_pins = dict()
# first control block always set pins
self.__add_control_block(0, GPIO_SET_OFFSET)
# fill control blocks
for i in range(1, self._TOTAL_NUMBER_OF_BLOCKS):
self.__add_control_block(i * self._DMA_CONTROL_BLOCK_SIZE,
GPIO_CLEAR_OFFSET)
# loop
self._physmem.write_int((self._TOTAL_NUMBER_OF_BLOCKS - 1)
* self._DMA_CONTROL_BLOCK_SIZE + 20,
self._physmem.get_bus_address())
self._gpio = PhysicalMemory(PERI_BASE + GPIO_REGISTER_BASE)
def __add_control_block(self, address, offset):
ba = self._physmem.get_bus_address() + address
data = (
DMA_TI_NO_WIDE_BURSTS | DMA_TI_WAIT_RESP
| DMA_TI_DEST_INC | DMA_TI_SRC_INC, # info
ba + self._DMA_DATA_OFFSET, # source, last 8 bytes are padding, use it to store data
PHYSICAL_GPIO_BUS + offset, # destination
4, # length
0, # stride
ba + self._DMA_CONTROL_BLOCK_SIZE, # next control block
0, # padding, uses as data storage
0 # padding
)
self._physmem.write(address, data)
def add_pin(self, pin, duty_cycle):
""" Add pin to PMW with specified duty cycle.
:param pin: pin number to add.
:param duty_cycle: duty cycle 0..100 which represent percents.
"""
assert 0 <= duty_cycle <= 100
self.remove_pin(pin)
block_number = int(duty_cycle * self._TOTAL_NUMBER_OF_BLOCKS
/ 100.0)
if block_number == 0:
self._gpio.write_int(GPIO_CLEAR_OFFSET, 1 << pin)
elif block_number == self._TOTAL_NUMBER_OF_BLOCKS:
self._gpio.write_int(GPIO_SET_OFFSET, 1 << pin)
self._clear_pins[pin] = self._DMA_DATA_OFFSET
else:
value = self._physmem.read_int(self._DMA_DATA_OFFSET)
value |= 1 << pin
self._physmem.write_int(self._DMA_DATA_OFFSET, value)
clear_address = block_number * self._DMA_CONTROL_BLOCK_SIZE \
+ self._DMA_DATA_OFFSET
value = self._physmem.read_int(clear_address)
value |= 1 << pin
self._physmem.write_int(clear_address, value)
self._clear_pins[pin] = clear_address
if not self.is_active():
super(DMAPWM, self)._run_dma()
def remove_pin(self, pin):
""" Remove pin from PWM
:param pin: pin number to remove.
"""
assert 0 <= pin < 32
if pin in self._clear_pins.keys():
address = self._clear_pins[pin]
value = self._physmem.read_int(address)
value &= ~(1 << pin)
self._physmem.write_int(address, value)
value = self._physmem.read_int(self._DMA_DATA_OFFSET)
value &= ~(1 << pin)
self._physmem.write_int(self._DMA_DATA_OFFSET, value)
del self._clear_pins[pin]
self._gpio.write_int(GPIO_CLEAR_OFFSET, 1 << pin)
if len(self._clear_pins) == 0 and self.is_active():
super(DMAPWM, self)._stop_dma()
def remove_all(self):
""" Remove all pins from PWM and stop it.
"""
pins_list = self._clear_pins.keys()
for pin in pins_list:
self.remove_pin(pin)
assert len(self._clear_pins) == 0
# for testing purpose # for testing purpose
def main(): def main():
pin = 21 pin = 21
@@ -272,8 +350,8 @@ def main():
print("now " + hex(a)) print("now " + hex(a))
del cma del cma
dg = DMAGPIO() dg = DMAGPIO()
dg.add_pulse(1 << pin, 4000) dg.add_pulse(1 << pin, 100000)
dg.add_delay(12000) dg.add_delay(600000)
dg.run(True) dg.run(True)
print("dmagpio is started") print("dmagpio is started")
try: try:
@@ -284,6 +362,16 @@ def main():
dg.stop() dg.stop()
g.clear(pin) g.clear(pin)
print("dma stopped") print("dma stopped")
pwm = DMAPWM()
pwm.add_pin(pin, 20)
print("pwm is started")
try:
print("press enter to stop...")
sys.stdin.readline()
except KeyboardInterrupt:
pass
pwm.remove_pin(pin)
print("pwm stopped")
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+49
View File
@@ -175,6 +175,8 @@ class CMAPhysicalMemory(PhysicalMemory):
atexit.register(self.free) atexit.register(self.free)
def free(self): def free(self):
"""Release and free allocated memory
"""
self._send_data(0x3000e, [self._handle]) # unlock memory self._send_data(0x3000e, [self._handle]) # unlock memory
self._send_data(0x3000f, [self._handle]) # free memory self._send_data(0x3000f, [self._handle]) # free memory
self._close_dev(self._vcio_fd) self._close_dev(self._vcio_fd)
@@ -196,3 +198,50 @@ class CMAPhysicalMemory(PhysicalMemory):
def get_phys_address(self): def get_phys_address(self):
return self._busmem & ~0xc0000000 return self._busmem & ~0xc0000000
class DMAProto(object):
def __init__(self, memory_size):
""" This class provides basic access to DMA and creates buffer for
control blocks.
"""
# allocate buffer for control blocks
self._physmem = CMAPhysicalMemory(memory_size)
# prepare dma registers memory map
self._dma = PhysicalMemory(PERI_BASE + DMA_BASE)
def _run_dma(self):
""" Run DMA module from created buffer.
"""
address = 0x100 * self._DMA_CHANNEL
cs = self._dma.read_int(address)
cs |= DMA_CS_END
self._dma.write_int(address, cs)
self._dma.write_int(address + 4, self._physmem.get_bus_address())
cs = DMA_CS_PRIORITY(7) | DMA_CS_PANIC_PRIORITY(7) | DMA_CS_DISDEBUG
self._dma.write_int(address, cs)
cs |= DMA_CS_ACTIVE
self._dma.write_int(address, cs)
def _stop_dma(self):
""" Stop DMA
"""
address = 0x100 * self._DMA_CHANNEL
cs = self._dma.read_int(address)
cs |= DMA_CS_ABORT
self._dma.write_int(address, cs)
cs &= ~DMA_CS_ACTIVE
self._dma.write_int(address, cs)
cs |= DMA_CS_RESET
self._dma.write_int(address, cs)
def is_active(self):
""" Check if DMA is working. Method can check if single sequence
still active or cycle sequence is working.
:return: boolean value
"""
address = 0x100 * self._DMA_CHANNEL
cs = self._dma.read_int(address)
if cs & DMA_CS_ACTIVE == DMA_CS_ACTIVE:
return True
return False
+6
View File
@@ -89,3 +89,9 @@ def join():
""" Wait till motors work. """ Wait till motors work.
""" """
logging.info("hal join()") logging.info("hal join()")
def deinit():
""" De-initialise.
"""
logging.info("hal deinit()")