From 978f5f71d81a4d8008a3f8f50dd6db3af5d84d19 Mon Sep 17 00:00:00 2001 From: Nikolay Khabarov <2xl@mail.ru> Date: Sun, 23 Jul 2017 03:10:27 +0300 Subject: [PATCH] hardware watchdog --- README.md | 22 ++++++++--- cnc/gmachine.py | 2 + cnc/hal.py | 10 +++++ cnc/hal_raspberry/hal.py | 9 +++++ cnc/hal_raspberry/rpgpio.py | 58 ++++++++++++++++++++++++++++- cnc/hal_raspberry/rpgpio_private.py | 2 + cnc/hal_virtual.py | 6 +++ cnc/watchdog.py | 25 +++++++++++++ 8 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 cnc/watchdog.py 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.coordinates import * from cnc.heater import * from cnc.enums import * +from cnc.watchdog import * class GMachineException(Exception): @@ -35,6 +36,7 @@ def __init__(self): 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 @@ 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 @@ 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 @@ def read(self, pin): # 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 @@ def __init__(self): 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 @@ def __init__(self): 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 @@ def remove_all(self): 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 @@ 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_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...")