diff --git a/README.md b/README.md index d725ceb..f0e7dcd 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,25 @@ perfect choice for easy development of this project. Video demo - [YouTube video](https://youtu.be/41wdmmztTNA) And the original video when PyCNC was just a prototype [YouTube video](https://youtu.be/vcedo59raS4) -# Current gcode support -Commands G0, G1, G2, G3, G4, G17, G18, G19, G20, G21, G28, G53, G90, G91, G92, +# Current gcode and features support +* Commands G0, G1, G2, G3, G4, G17, G18, G19, G20, G21, G28, G53, G90, G91, G92, M2, M3, M5, M30, M84, M104, M105, M106, M107, M109, M114, M140, M190 are supported. Commands can be easily added, see [gmachine.py](./cnc/gmachine.py) file. -Four axis are supported - X, Y, Z, E. -Circular interpolation for XY, ZX, YZ planes is supported. -Spindle with rpm control is supported. -Extruder and bed heaters are supported. +* Four axis are supported - X, Y, Z, E. +* Circular interpolation for XY, ZX, YZ planes is supported. +* Spindle with rpm control is supported. +* Extruder and bed heaters are supported. +* Hardware watchdog. + +# Watchdog +PyCNC uses one of DMA channels as hardware watchdog for safety purpose. If +board, OS or PyCNC hangs this watchdog should disable all GPIO pins(by +switching them into input state, for RPi this would be GPIO0-29) in 15 seconds. +Since there is a high current and dangerous devices like heated bed, extruder +heater, this feature should prevent uncontrollable overheating. But don't count +on such software features too much, they can hang too or output MOSFET become +shorted, use hardware protection like thermal cutoff switches in your machines. # Hardware Currently, this project supports Raspberry Pi 1-3. Developed and tested with diff --git a/cnc/gmachine.py b/cnc/gmachine.py index 2fc37b2..776db32 100644 --- a/cnc/gmachine.py +++ b/cnc/gmachine.py @@ -6,6 +6,7 @@ from cnc.pulses import * from cnc.coordinates import * from cnc.heater import * from cnc.enums import * +from cnc.watchdog import * class GMachineException(Exception): @@ -35,6 +36,7 @@ class GMachine(object): self._heaters = dict() self.reset() hal.init() + self.watchdog = HardwareWatchdog() def release(self): """ Free all resources. diff --git a/cnc/hal.py b/cnc/hal.py index 7c0b2e9..25e9893 100644 --- a/cnc/hal.py +++ b/cnc/hal.py @@ -86,6 +86,14 @@ # """ De-initialise hal, stop any hardware. # """ # do_something() +# +# +# def watchdog_feed(): +# """ Feed hardware watchdog. This method should be called at least +# once in 15 seconds. Also, this method can do no operation in hal +# implementation and there will not be emergency stop for heaters. +# """ +# do_something() # check which module to import @@ -121,3 +129,5 @@ if 'join' not in locals(): raise NotImplementedError("hal.join() not implemented") if 'deinit' not in locals(): raise NotImplementedError("hal.deinit() not implemented") +if 'watchdog_feed' not in locals(): + raise NotImplementedError("hal.watchdog_feed() not implemented") diff --git a/cnc/hal_raspberry/hal.py b/cnc/hal_raspberry/hal.py index 253b8a0..6a15824 100644 --- a/cnc/hal_raspberry/hal.py +++ b/cnc/hal_raspberry/hal.py @@ -10,6 +10,7 @@ US_IN_SECONDS = 1000000 gpio = rpgpio.GPIO() dma = rpgpio.DMAGPIO() pwm = rpgpio.DMAPWM() +watchdog = rpgpio.DMAWatchdog() STEP_PIN_MASK_X = 1 << STEPPER_STEP_PIN_X STEP_PIN_MASK_Y = 1 << STEPPER_STEP_PIN_Y @@ -41,6 +42,7 @@ def init(): gpio.clear(EXTRUDER_HEATER_PIN) gpio.clear(BED_HEATER_PIN) gpio.clear(STEPPERS_ENABLE_PIN) + watchdog.start() def spindle_control(percent): @@ -328,3 +330,10 @@ def deinit(): gpio.clear(FAN_PIN) gpio.clear(EXTRUDER_HEATER_PIN) gpio.clear(BED_HEATER_PIN) + watchdog.stop() + + +def watchdog_feed(): + """ Feed hardware watchdog. + """ + watchdog.feed() diff --git a/cnc/hal_raspberry/rpgpio.py b/cnc/hal_raspberry/rpgpio.py index 0886f2e..2030c42 100755 --- a/cnc/hal_raspberry/rpgpio.py +++ b/cnc/hal_raspberry/rpgpio.py @@ -82,6 +82,7 @@ class GPIO(object): # clock for delay). So, do not create two or more instances of DMAGPIO. class DMAGPIO(DMAProto): _DMA_CONTROL_BLOCK_SIZE = 32 + _DMA_CHANNEL = 4 def __init__(self): """ Create object which control GPIO pins via DMA(Direct Memory @@ -93,7 +94,7 @@ class DMAGPIO(DMAProto): otherwise memory will be unlocked and it could be overwritten by operating system. """ - super(DMAGPIO, self).__init__(30 * 1024 * 1024, 4) + super(DMAGPIO, self).__init__(30 * 1024 * 1024, self._DMA_CHANNEL) self.__current_address = 0 # get helpers registers, this class uses PWM module to create precise @@ -259,6 +260,7 @@ class DMAPWM(DMAProto): _DMA_CONTROL_BLOCK_SIZE = 32 _DMA_DATA_OFFSET = 24 _TOTAL_NUMBER_OF_BLOCKS = 256 + _DMA_CHANNEL = 14 def __init__(self): """ Initialise PWM. PWM has 8 bit resolution and fixed frequency @@ -273,7 +275,8 @@ class DMAPWM(DMAProto): Cycles in info field of control blocks. """ super(DMAPWM, self).__init__(self._TOTAL_NUMBER_OF_BLOCKS - * self._DMA_CONTROL_BLOCK_SIZE, 14) + * self._DMA_CONTROL_BLOCK_SIZE, + self._DMA_CHANNEL) self._clear_pins = dict() # first control block always set pins self.__add_control_block(0, GPIO_SET_OFFSET) @@ -356,6 +359,57 @@ class DMAPWM(DMAProto): assert len(self._clear_pins) == 0 +class DMAWatchdog(DMAProto): + _DMA_CONTROL_BLOCK_SIZE = 32 + _DMA_CHANNEL = 13 + _DMA_BLOCKS = 2047 + + def __init__(self): + """ Initialize hardware watchdog timer. + """ + super(DMAWatchdog, self).__init__(self._DMA_CONTROL_BLOCK_SIZE + * (self._DMA_BLOCKS + 1), + self._DMA_CHANNEL) + + def start(self): + """ Arm watchdog for ~15 seconds. If watchdog wasn't fed in 15 seconds, + GPIO pins 0-29 will be switched to input to prevent any output from + them. + """ + data = () + ba = self._phys_memory.get_bus_address() + # first blocks is just a delay + for _ in range(0, self._DMA_BLOCKS): + data += ( + DMA_TI_NO_WIDE_BURSTS | DMA_DEST_IGNORE | DMA_TI_WAIT_RESP + | DMA_TI_WAITS(31), + ba + 24, ba + 28, 65535, + 0, ba + self._DMA_CONTROL_BLOCK_SIZE, 0, 0 + ) + ba += self._DMA_CONTROL_BLOCK_SIZE + # The last block writes zero(switch to input state) in GPIO's FSEL + # register in normal operating, should never be called, until watchdog + # timeout is reached. + data += ( + DMA_TI_NO_WIDE_BURSTS | DMA_TI_WAIT_RESP | DMA_TI_DEST_INC, + ba + 24, PHYSICAL_GPIO_BUS + GPIO_FSEL_OFFSET, 12, + 0, 0, 0, 0 + ) + self._phys_memory.write(0, str(len(data)) + "I", data) + super(DMAWatchdog, self)._run_dma() + + def stop(self): + """ Disarm watchdog. + """ + super(DMAWatchdog, self)._stop_dma() + + def feed(self): + """ Feed watchdog, restart waiting loop from the very beginning. + """ + self._dma.write_int(self._DMA_CHANNEL_ADDRESS + DMA_NEXTCONBK, + self._phys_memory.get_bus_address()) + + # for testing purpose def main(): pin = 21 diff --git a/cnc/hal_raspberry/rpgpio_private.py b/cnc/hal_raspberry/rpgpio_private.py index dca139c..37f671e 100644 --- a/cnc/hal_raspberry/rpgpio_private.py +++ b/cnc/hal_raspberry/rpgpio_private.py @@ -45,6 +45,7 @@ PHYSICAL_GPIO_BUS = 0x7E000000 + GPIO_REGISTER_BASE DMA_BASE = 0x007000 DMA_CS = 0x00 DMA_CONBLK_AD = 0x04 +DMA_NEXTCONBK = 0x1C DMA_TI_NO_WIDE_BURSTS = 1 << 26 DMA_TI_SRC_INC = 1 << 8 DMA_TI_DEST_INC = 1 << 4 @@ -62,6 +63,7 @@ DMA_CS_ACTIVE = 1 << 0 DMA_TI_PER_MAP_PWM = 5 DMA_TI_PER_MAP_PCM = 2 DMA_TI_PER_MAP = (lambda x: x << 16) +DMA_TI_WAITS = (lambda x: x << 21) DMA_TI_TXFR_LEN_YLENGTH = (lambda y: (y & 0x3fff) << 16) DMA_TI_TXFR_LEN_XLENGTH = (lambda x: x & 0xffff) DMA_TI_STRIDE_D_STRIDE = (lambda x: (x & 0xffff) << 16) diff --git a/cnc/hal_virtual.py b/cnc/hal_virtual.py index 973157e..c44dac9 100644 --- a/cnc/hal_virtual.py +++ b/cnc/hal_virtual.py @@ -196,3 +196,9 @@ def deinit(): """ De-initialise. """ logging.info("hal deinit()") + + +def watchdog_feed(): + """ Feed hardware watchdog. + """ + pass diff --git a/cnc/watchdog.py b/cnc/watchdog.py new file mode 100644 index 0000000..bf00cce --- /dev/null +++ b/cnc/watchdog.py @@ -0,0 +1,25 @@ +import threading +import time + +from cnc import hal + + +class HardwareWatchdog(threading.Thread): + def __init__(self): + """ Run feed loop for hardware watchdog. + """ + super(HardwareWatchdog, self).__init__() + self.setDaemon(True) + self.start() + + def run(self): + while True: + hal.watchdog_feed() + time.sleep(3) + +# for test purpose +if __name__ == "__main__": + hal.init() + hal.fan_control(True) + print("Fan is on, it should turn off automatically in ~15 seconds." + "\nExiting...")