diff --git a/README.md b/README.md index e7c00f3..ce61d6c 100644 --- a/README.md +++ b/README.md @@ -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. # 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. 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: ![](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. -_Note: spindle control is not implemented yet_ # Hardware 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. +_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 Just clone this repo and run `./pycnc` from repo root. It will start in diff --git a/cnc/gmachine.py b/cnc/gmachine.py index 084032d..74bae3c 100644 --- a/cnc/gmachine.py +++ b/cnc/gmachine.py @@ -34,8 +34,9 @@ class GMachine(object): def release(self): """ Return machine to original position and free all resources. """ + self._spindle(0) self.home() - hal.join() + hal.deinit() def reset(self): """ Reinitialize all program configurable thing. @@ -48,6 +49,7 @@ class GMachine(object): self._absoluteCoordinates = True def _spindle(self, spindle_speed): + hal.join() hal.spindle_control(100.0 * spindle_speed / SPINDLE_MAX_RPM) def _move(self, delta, velocity): diff --git a/cnc/hal.py b/cnc/hal.py index fac2978..88e2cec 100644 --- a/cnc/hal.py +++ b/cnc/hal.py @@ -29,6 +29,11 @@ # """ Wait till motors work. # """ # do_something() +# +# def deinit(): +# """ De-initialise hal, stop any hardware. +# """ +# do_something() # check which module to import @@ -48,4 +53,6 @@ if 'move_linear' not in locals(): raise NotImplementedError("hal.move_linear() not implemented") if 'join' not in locals(): raise NotImplementedError("hal.join() not implemented") +if 'deinit' not in locals(): + raise NotImplementedError("hal.deinit() not implemented") diff --git a/cnc/hal_raspberry/hal.py b/cnc/hal_raspberry/hal.py index 36e4c59..b26694c 100644 --- a/cnc/hal_raspberry/hal.py +++ b/cnc/hal_raspberry/hal.py @@ -22,6 +22,7 @@ US_IN_SECONDS = 1000000 gpio = rpgpio.GPIO() dma = rpgpio.DMAGPIO() +pwm = rpgpio.DMAPWM() STEP_PIN_MASK_X = 1 << STEPPER_STEP_PIN_X 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(SPINDLE_PWM_PIN, rpgpio.GPIO.MODE_OUTPUT) + gpio.clear(SPINDLE_PWM_PIN) # calibration gpio.set(STEPPER_DIR_PIN_X) @@ -71,10 +74,13 @@ def init(): def spindle_control(percent): """ 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("TODO spindle control: {}%".format(percent)) + logging.info("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): @@ -147,6 +153,14 @@ def move_linear(delta, velocity): def join(): """ Wait till motors work. """ + logging.info("hal join()") # wait till dma works while dma.is_active(): time.sleep(0.01) + + +def deinit(): + """ De-initialize hardware. + """ + join() + pwm.remove_all() diff --git a/cnc/hal_raspberry/rpgpio.py b/cnc/hal_raspberry/rpgpio.py index e51c122..8323a68 100755 --- a/cnc/hal_raspberry/rpgpio.py +++ b/cnc/hal_raspberry/rpgpio.py @@ -78,10 +78,12 @@ class GPIO(object): 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_CHANNEL = 5 - def __init__(self): """ Create object which control GPIO pins via DMA(Direct Memory Access). @@ -92,12 +94,11 @@ class DMAGPIO(object): 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) + super(DMAGPIO, self).__init__(31 * 1024 * 1024) self.__current_address = 0 - # prepare dma registers memory map - self._dma = PhysicalMemory(PERI_BASE + DMA_BASE) + # get helpers registers, this class uses PWM module to create precise + # delays self._pwm = PhysicalMemory(PERI_BASE + PWM_BASE) self._clock = PhysicalMemory(PERI_BASE + CM_BASE) @@ -119,7 +120,10 @@ class DMAGPIO(object): 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. + 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. """ 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)) 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) + super(DMAGPIO, self)._run_dma() def run(self, loop=False): """ Run DMA module and start sending specified pulses. @@ -217,32 +212,115 @@ class DMAGPIO(object): """ 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 + super(DMAGPIO, self)._stop_dma() def clear(self): - """ Remove any specified pulses. + """ Remove any specified pulses. Doesn't affect currently running + sequence. """ 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 def main(): pin = 21 @@ -272,8 +350,8 @@ def main(): print("now " + hex(a)) del cma dg = DMAGPIO() - dg.add_pulse(1 << pin, 4000) - dg.add_delay(12000) + dg.add_pulse(1 << pin, 100000) + dg.add_delay(600000) dg.run(True) print("dmagpio is started") try: @@ -284,6 +362,16 @@ def main(): dg.stop() g.clear(pin) 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__": main() diff --git a/cnc/hal_raspberry/rpgpio_private.py b/cnc/hal_raspberry/rpgpio_private.py index 55ad072..33d3136 100644 --- a/cnc/hal_raspberry/rpgpio_private.py +++ b/cnc/hal_raspberry/rpgpio_private.py @@ -175,6 +175,8 @@ class CMAPhysicalMemory(PhysicalMemory): atexit.register(self.free) def free(self): + """Release and free allocated memory + """ self._send_data(0x3000e, [self._handle]) # unlock memory self._send_data(0x3000f, [self._handle]) # free memory self._close_dev(self._vcio_fd) @@ -196,3 +198,50 @@ class CMAPhysicalMemory(PhysicalMemory): def get_phys_address(self): 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 diff --git a/cnc/hal_virtual.py b/cnc/hal_virtual.py index 2140825..17628fd 100644 --- a/cnc/hal_virtual.py +++ b/cnc/hal_virtual.py @@ -89,3 +89,9 @@ def join(): """ Wait till motors work. """ logging.info("hal join()") + + +def deinit(): + """ De-initialise. + """ + logging.info("hal deinit()")