abse/isv57t090-motion-controller
A 36 V-powered RP2040 motion-controller PCB that regulates 5 V, drives isolated STEP/DIR servo inputs, monitors alarms, temperature, limits and E-stop signals, and provides protected safety/field connectors.
- Version
- 0.0.67
- License
- unset
- Stars
- 0
examples/ISV57T090MotionController/firmware/thermal_guard.py
"""Fail-closed thermal policy for the iSV57T-090 RP2040 controller.
The remote 10 kOhm B3950 NTC must be bonded to the motor heatsink and wired
between J_IO pin 5 and pin 6 (GND). The onboard TMP117 monitors PCB temperature.
The motor manual limits heatsink temperature to 70 C; the remote trip is 65 C.
"""
import math
NTC_PULLUP_OHMS = 10000.0
NTC_NOMINAL_OHMS = 10000.0
NTC_BETA_K = 3950.0
NTC_NOMINAL_K = 298.15
REMOTE_TRIP_C = 65.0
BOARD_TRIP_C = 75.0
MIN_PLAUSIBLE_C = -20.0
MAX_PLAUSIBLE_C = 125.0
ADC_MIN_VALID = 1000
ADC_MAX_VALID = 64535
def tmp117_temperature_c(msb, lsb):
"""Decode TMP117 temperature register 0x00 (signed, 1/128 C per bit)."""
raw = (msb << 8) | lsb
if raw & 0x8000:
raw -= 0x10000
return raw / 128.0
def remote_ntc_temperature_c(adc_u16):
"""Return degrees C, or None for an open/shorted/unreadable sensor."""
if adc_u16 is None or not ADC_MIN_VALID < adc_u16 < ADC_MAX_VALID:
return None
resistance = NTC_PULLUP_OHMS * adc_u16 / (65535.0 - adc_u16)
inverse_k = (1.0 / NTC_NOMINAL_K) + math.log(
resistance / NTC_NOMINAL_OHMS
) / NTC_BETA_K
if inverse_k <= 0:
return None
return (1.0 / inverse_k) - 273.15
class ThermalGuard:
"""Never re-arm after a fault until the RP2040 is intentionally reset."""
def __init__(self, startup_samples=10, alarm_startup_samples=40):
self.startup_samples = startup_samples
self.alarm_startup_samples = alarm_startup_samples
self.healthy_samples = 0
self.alarm_wait_samples = 0
self.ready = False
self.fault = None
def trip(self, reason):
if self.fault is None:
self.fault = reason
return False
def update(self, board_c, remote_c, estop_closed, alarm_ok):
if self.fault is not None:
return False
if not estop_closed:
return self.trip("estop_open")
if board_c is None or remote_c is None:
return self.trip("temperature_sensor_missing")
if not (MIN_PLAUSIBLE_C <= board_c <= MAX_PLAUSIBLE_C):
return self.trip("board_temperature_invalid")
if not (MIN_PLAUSIBLE_C <= remote_c <= MAX_PLAUSIBLE_C):
return self.trip("remote_temperature_invalid")
if board_c >= BOARD_TRIP_C:
return self.trip("board_overheat")
if remote_c >= REMOTE_TRIP_C:
return self.trip("motor_overheat")
# The motor's normally-low alarm is unpowered while the external
# contactor is open. Energize the coil only after temperatures and
# E-stop pass, then allow its alarm output time to become valid.
if self.healthy_samples < self.startup_samples:
self.healthy_samples += 1
if self.healthy_samples < self.startup_samples:
return False
if not self.ready:
if alarm_ok:
self.ready = True
else:
self.alarm_wait_samples += 1
if self.alarm_wait_samples > self.alarm_startup_samples:
return self.trip("motor_alarm_startup_timeout")
return True
if not alarm_ok:
return self.trip("motor_alarm")
return True