diff --git a/lib/pimoroni_yukon/__init__.py b/lib/pimoroni_yukon/__init__.py index 6f51189..52aa255 100644 --- a/lib/pimoroni_yukon/__init__.py +++ b/lib/pimoroni_yukon/__init__.py @@ -6,7 +6,7 @@ import tca from machine import ADC, Pin from pimoroni_yukon.modules import KNOWN_MODULES -from pimoroni_yukon.modules.common import ADC_FLOAT, ADC_LOW, ADC_HIGH +from pimoroni_yukon.modules.common import ADC_FLOAT, ADC_LOW, ADC_HIGH, YukonModule import pimoroni_yukon.logging as logging from pimoroni_yukon.errors import OverVoltageError, UnderVoltageError, OverCurrentError, OverTemperatureError, FaultError, VerificationError from pimoroni_yukon.timing import ticks_ms, ticks_add, ticks_diff @@ -116,7 +116,7 @@ class Yukon: ABSOLUTE_MAX_VOLTAGE_LIMIT = 18 DETECTION_SAMPLES = 64 - DETECTION_ADC_LOW = 0.1 + DETECTION_ADC_LOW = 0.2 DETECTION_ADC_HIGH = 3.2 CURRENT_SENSE_ADDR = 12 # 0b1100 @@ -124,8 +124,13 @@ class Yukon: VOLTAGE_OUT_SENSE_ADDR = 14 # 0b1110 VOLTAGE_IN_SENSE_ADDR = 15 # 0b1111 - OUTPUT_STABLISE_TIMEOUT_US = 100 * 1000 - OUTPUT_STABLISE_TIME_US = 5 * 1000 + OUTPUT_STABLISE_TIMEOUT_US = 200 * 1000 + OUTPUT_STABLISE_TIME_US = 10 * 1000 + OUTPUT_STABLISE_V_DIFF = 0.1 + + OUTPUT_DISSIPATE_TIMEOUT_S = 15 # When a bench power module is attached and there is no additional output load, it can take a while for it to return to an idle state + OUTPUT_DISSIPATE_TIMEOUT_US = OUTPUT_DISSIPATE_TIMEOUT_S * 1000 * 1000 + OUTPUT_DISSIPATE_LEVEL = 0.4 # The voltage below which we can reliably obtain the address of attached modules def __init__(self, voltage_limit=DEFAULT_VOLTAGE_LIMIT, current_limit=DEFAULT_CURRENT_LIMIT, temperature_limit=DEFAULT_TEMPERATURE_LIMIT, logging_level=logging.LOG_INFO): self.__voltage_limit = min(voltage_limit, self.ABSOLUTE_MAX_VOLTAGE_LIMIT) @@ -153,8 +158,8 @@ def __init__(self, voltage_limit=DEFAULT_VOLTAGE_LIMIT, current_limit=DEFAULT_CU # ADC mux enable pins self.__adc_mux_ens = (Pin.board.ADC_MUX_EN_1, Pin.board.ADC_MUX_EN_2) - self.__adc_mux_ens[0].init(Pin.OUT, value=False) - self.__adc_mux_ens[1].init(Pin.OUT, value=False) + self.__adc_mux_ens[0].init(Pin.OUT, value=True) # Active low + self.__adc_mux_ens[1].init(Pin.OUT, value=True) # Active low # ADC mux address pins self.__adc_mux_addrs = (Pin.board.ADC_ADDR_1, @@ -192,17 +197,12 @@ def __init__(self, voltage_limit=DEFAULT_VOLTAGE_LIMIT, current_limit=DEFAULT_CU self.__monitor_action_callback = None - def reset(self) -> None: + def reset(self): # Only disable the output if enabled (avoids duplicate messages) if self.is_main_output_enabled() is True: self.disable_main_output() - self.__adc_mux_ens[0].value(False) - self.__adc_mux_ens[1].value(False) - - self.__adc_mux_addrs[0].value(False) - self.__adc_mux_addrs[1].value(False) - self.__adc_mux_addrs[2].value(False) + self.__deselect_address() self.__leds[0].value(False) self.__leds[1].value(False) @@ -210,7 +210,7 @@ def reset(self) -> None: # Configure each module so they go back to their default states for module in self.__slot_assignments.values(): if module is not None and module.is_initialised(): - module.configure() + module.reset() def change_logging(self, logging_level): logging.level = logging_level @@ -244,6 +244,8 @@ def find_slots_with_module(self, module_type): else: logging.info(f"No '{module_type.NAME}` module") + logging.info() # New line + return slots def register_with_slot(self, module, slot): @@ -252,6 +254,13 @@ def register_with_slot(self, module, slot): slot = self.__check_slot(slot) + module_type = type(module) + if module_type is YukonModule: + raise ValueError("Cannot register YukonModule") + + if module_type not in KNOWN_MODULES: + raise ValueError(f"{module_type} is not a known module. If this is custom module, be sure to include it in the KNOWN_MODULES list.") + if self.__slot_assignments[slot] is None: self.__slot_assignments[slot] = module else: @@ -272,6 +281,8 @@ def __match_module(self, adc_level, slow1, slow2, slow3): for m in KNOWN_MODULES: if m.is_module(adc_level, slow1, slow2, slow3): return m + if YukonModule.is_module(adc_level, slow1, slow2, slow3): + return YukonModule return None def __detect_module(self, slot): @@ -382,6 +393,21 @@ def initialise_modules(self, allow_unregistered=False, allow_undetected=False, a if self.is_main_output_enabled(): raise RuntimeError("Cannot verify modules whilst the main output is active") + logging.info("> Checking output voltage ...") + if self.read_output_voltage() >= self.OUTPUT_DISSIPATE_LEVEL: + logging.info("> Waiting for output voltage to dissipate ...") + + start = time.ticks_us() + while True: + new_voltage = self.read_output_voltage() + if new_voltage < self.OUTPUT_DISSIPATE_LEVEL: + break + + new_time = time.ticks_us() + if new_time - start > self.OUTPUT_DISSIPATE_TIMEOUT_US: + raise FaultError("[Yukon] Output voltage did not dissipate in an acceptable time. Aborting module initialisation") + + logging.info("> Verifying modules") self.__verify_modules(allow_unregistered, allow_undetected, allow_discrepencies, allow_no_modules) @@ -453,7 +479,7 @@ def enable_main_output(self): raise OverVoltageError(f"[Yukon] Output voltage of {new_voltage}V exceeded the user set limit of {self.__voltage_limit}V! Turning off output") new_time = time.ticks_us() - if abs(new_voltage - old_voltage) < 0.05: + if abs(new_voltage - old_voltage) < self.OUTPUT_STABLISE_V_DIFF: if first_stable_time == 0: first_stable_time = new_time elif new_time - first_stable_time > self.OUTPUT_STABLISE_TIME_US: @@ -492,8 +518,9 @@ def is_main_output_enabled(self): return self.__main_en.value() == 1 def __deselect_address(self): - self.__adc_mux_ens[0].value(False) - self.__adc_mux_ens[1].value(False) + # Deselect the muxes and reset the address to zero + state = self.__adc_io_ens_addrs[0] | self.__adc_io_ens_addrs[1] + tca.change_output_mask(self.__adc_io_chip, self.__adc_io_mask, state) def __select_address(self, address): if address < 0: @@ -593,7 +620,7 @@ def monitor(self): # Run some user action based on the latest readings if self.__monitor_action_callback is not None: - self.__monitor_action_callback(voltage_out, current, temperature) + self.__monitor_action_callback(voltage_in, voltage_out, current, temperature) for module in self.__slot_assignments.values(): if module is not None: @@ -606,7 +633,7 @@ def monitor(self): self.__max_voltage_in = max(voltage_in, self.__max_voltage_in) self.__min_voltage_in = min(voltage_in, self.__min_voltage_in) self.__avg_voltage_in += voltage_in - + self.__max_voltage_out = max(voltage_out, self.__max_voltage_out) self.__min_voltage_out = min(voltage_out, self.__min_voltage_out) self.__avg_voltage_out += voltage_out diff --git a/lib/pimoroni_yukon/conversion.py b/lib/pimoroni_yukon/conversion.py index 011a9da..1888a68 100644 --- a/lib/pimoroni_yukon/conversion.py +++ b/lib/pimoroni_yukon/conversion.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2023 Christopher Parrott for Pimoroni Ltd +# +# SPDX-License-Identifier: MIT + from math import log # ----------------------------------------------------- diff --git a/lib/pimoroni_yukon/errors.py b/lib/pimoroni_yukon/errors.py index 8fe0844..8d3aaea 100644 --- a/lib/pimoroni_yukon/errors.py +++ b/lib/pimoroni_yukon/errors.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2023 Christopher Parrott for Pimoroni Ltd +# +# SPDX-License-Identifier: MIT + class OverVoltageError(Exception): """Exception to be used when a voltage value exceeds safe levels""" pass diff --git a/lib/pimoroni_yukon/logging.py b/lib/pimoroni_yukon/logging.py index f81edd5..61daf60 100644 --- a/lib/pimoroni_yukon/logging.py +++ b/lib/pimoroni_yukon/logging.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2023 Christopher Parrott for Pimoroni Ltd +# +# SPDX-License-Identifier: MIT + LOG_NONE = 0 LOG_WARN = 1 LOG_INFO = 2 diff --git a/lib/pimoroni_yukon/modules/__init__.py b/lib/pimoroni_yukon/modules/__init__.py index d21a603..89f29f9 100644 --- a/lib/pimoroni_yukon/modules/__init__.py +++ b/lib/pimoroni_yukon/modules/__init__.py @@ -2,23 +2,25 @@ # # SPDX-License-Identifier: MIT -from .led_strip import LEDStripModule -from .quad_servo_direct import QuadServoDirectModule -from .quad_servo_reg import QuadServoRegModule +from .audio_amp import AudioAmpModule +from .bench_power import BenchPowerModule from .big_motor import BigMotorModule from .dual_motor import DualMotorModule from .dual_switched import DualSwitchedModule -from .bench_power import BenchPowerModule -from .audio_amp import AudioAmpModule +from .led_strip import LEDStripModule from .proto import ProtoPotModule +from .quad_servo_direct import QuadServoDirectModule +from .quad_servo_reg import QuadServoRegModule + KNOWN_MODULES = ( - LEDStripModule, - QuadServoDirectModule, - QuadServoRegModule, + AudioAmpModule, + BenchPowerModule, BigMotorModule, DualMotorModule, DualSwitchedModule, - BenchPowerModule, - AudioAmpModule, - ProtoPotModule) + LEDStripModule, + ProtoPotModule, + QuadServoDirectModule, + QuadServoRegModule + ) diff --git a/lib/pimoroni_yukon/modules/audio_amp.py b/lib/pimoroni_yukon/modules/audio_amp.py index effbaa3..e046d96 100644 --- a/lib/pimoroni_yukon/modules/audio_amp.py +++ b/lib/pimoroni_yukon/modules/audio_amp.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: MIT -from .common import YukonModule, ADC_FLOAT, HIGH +from .common import YukonModule, ADC_FLOAT, LOW, HIGH import tca from machine import Pin from ucollections import OrderedDict @@ -130,11 +130,10 @@ class AudioAmpModule(YukonModule): # | ADC1 | SLOW1 | SLOW2 | SLOW3 | Module | Condition (if any) | # |-------|-------|-------|-------|----------------------|-----------------------------| - # | FLOAT | 0 | 1 | 1 | [Proposed] Audio Amp | | + # | FLOAT | 0 | 1 | 1 | Audio Amp | | @staticmethod def is_module(adc_level, slow1, slow2, slow3): - # return adc_level == ADC_FLOAT and slow1 is LOW and slow2 is HIGH and slow3 is HIGH - return adc_level == ADC_FLOAT and slow1 is HIGH and slow2 is HIGH and slow3 is HIGH + return adc_level == ADC_FLOAT and slow1 is LOW and slow2 is HIGH and slow3 is HIGH def __init__(self): super().__init__() diff --git a/lib/pimoroni_yukon/modules/bench_power.py b/lib/pimoroni_yukon/modules/bench_power.py index 5c1060a..b54375b 100644 --- a/lib/pimoroni_yukon/modules/bench_power.py +++ b/lib/pimoroni_yukon/modules/bench_power.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: MIT -from .common import YukonModule, LOW, HIGH +from .common import YukonModule, ADC_LOW, LOW, HIGH from machine import Pin, PWM from ucollections import OrderedDict from pimoroni_yukon.errors import FaultError, OverTemperatureError @@ -26,11 +26,9 @@ class BenchPowerModule(YukonModule): # | ADC1 | SLOW1 | SLOW2 | SLOW3 | Module | Condition (if any) | # |-------|-------|-------|-------|----------------------|-----------------------------| # | LOW | 1 | 0 | 0 | Bench Power | | - # | FLOAT | 1 | 0 | 0 | Bench Power | When V+ is discharging | - # FLOAT address included as may not be able to rely on the ADC level being low @staticmethod def is_module(adc_level, slow1, slow2, slow3): - return slow1 is HIGH and slow2 is LOW and slow3 is LOW + return adc_level is ADC_LOW and slow1 is HIGH and slow2 is LOW and slow3 is LOW def __init__(self, halt_on_not_pgood=False): super().__init__() @@ -42,7 +40,7 @@ def __init__(self, halt_on_not_pgood=False): def initialise(self, slot, adc1_func, adc2_func): try: # Create the voltage pwm object - self.voltage_pwm = PWM(slot.FAST2, freq=250000, duty_u16=0) + self.__voltage_pwm = PWM(slot.FAST2, freq=250000, duty_u16=0) except ValueError as e: if slot.ID <= 2 or slot.ID >= 5: conflicting_slot = (((slot.ID - 1) + 4) % 8) + 1 @@ -57,7 +55,7 @@ def initialise(self, slot, adc1_func, adc2_func): super().initialise(slot, adc1_func, adc2_func) def reset(self): - self.voltage_pwm.duty_u16(0) + self.__voltage_pwm.duty_u16(0) self.__power_en.init(Pin.OUT, value=False) self.__power_good.init(Pin.IN) @@ -72,7 +70,7 @@ def is_enabled(self): return self.__motors_en.value() == 1 def __set_pwm(self, percent): - self.voltage_pwm.duty_u16(int(((2 ** 16) - 1) * percent)) + self.__voltage_pwm.duty_u16(int(((2 ** 16) - 1) * percent)) def set_target_voltage(self, voltage): if voltage >= self.VOLTAGE_MID: diff --git a/lib/pimoroni_yukon/modules/common.py b/lib/pimoroni_yukon/modules/common.py index 62223c3..e668ddc 100644 --- a/lib/pimoroni_yukon/modules/common.py +++ b/lib/pimoroni_yukon/modules/common.py @@ -2,23 +2,26 @@ # # SPDX-License-Identifier: MIT -from ucollections import OrderedDict +from collections import OrderedDict from pimoroni_yukon.conversion import analog_to_temp ADC_LOW = 0 ADC_HIGH = 1 ADC_FLOAT = 2 -ADC_ANY = 3 LOW = False HIGH = True class YukonModule: - NAME = "Unnamed" + NAME = "Unknown" + # | ADC1 | SLOW1 | SLOW2 | SLOW3 | Module | Condition (if any) | + # |-------|-------|-------|-------|----------------------|-----------------------------| + # | FLOAT | 1 | 1 | 1 | Empty | | @staticmethod def is_module(adc_level, slow1, slow2, slow3): - return False + # This will return true if a slot is detected as not being empty, so as to give useful error information + return adc_level is not ADC_FLOAT or slow1 is not HIGH or slow2 is not HIGH or slow3 is not HIGH def __init__(self): self.slot = None @@ -78,7 +81,7 @@ def process_readings(self): pass def clear_readings(self): - # Clear any readings that may accumulate, such as min, max, or average + # Override this to clear any readings that may accumulate, such as min, max, or average pass def __message_header(self): diff --git a/lib/pimoroni_yukon/modules/dual_motor.py b/lib/pimoroni_yukon/modules/dual_motor.py index c3149b4..f7aaccb 100644 --- a/lib/pimoroni_yukon/modules/dual_motor.py +++ b/lib/pimoroni_yukon/modules/dual_motor.py @@ -2,10 +2,22 @@ # # SPDX-License-Identifier: MIT -from .common import YukonModule, ADC_HIGH, HIGH +from .common import YukonModule, ADC_HIGH, LOW, HIGH from machine import Pin from ucollections import OrderedDict from pimoroni_yukon.errors import FaultError, OverTemperatureError +import pimoroni_yukon.logging as logging + +# The current (in amps) associated with each limit (Do Not Modify!) +CURRENT_LIMIT_1 = 0.161 +CURRENT_LIMIT_2 = 0.251 +CURRENT_LIMIT_3 = 0.444 +CURRENT_LIMIT_4 = 0.786 +CURRENT_LIMIT_5 = 1.143 +CURRENT_LIMIT_6 = 1.611 +CURRENT_LIMIT_7 = 1.890 +CURRENT_LIMIT_8 = 2.153 +CURRENT_LIMIT_9 = 2.236 class DualMotorModule(YukonModule): @@ -16,23 +28,37 @@ class DualMotorModule(YukonModule): NUM_STEPPERS = 1 FAULT_THRESHOLD = 0.1 DEFAULT_FREQUENCY = 25000 + DEFAULT_CURRENT_LIMIT = CURRENT_LIMIT_3 TEMPERATURE_THRESHOLD = 50.0 # | ADC1 | SLOW1 | SLOW2 | SLOW3 | Module | Condition (if any) | # |-------|-------|-------|-------|----------------------|-----------------------------| - # | HIGH | 1 | 1 | 1 | Dual Motor | | - # | HIGH | 0 | 1 | 1 | Dual Motor | | + # | HIGH | 0 | 0 | 1 | Dual Motor | | @staticmethod def is_module(adc_level, slow1, slow2, slow3): - return adc_level == ADC_HIGH and slow2 is HIGH and slow3 is HIGH + return adc_level == ADC_HIGH and slow1 is LOW and slow2 is LOW and slow3 is HIGH - def __init__(self, motor_type=DUAL, frequency=DEFAULT_FREQUENCY): + def __init__(self, motor_type=DUAL, frequency=DEFAULT_FREQUENCY, current_limit=DEFAULT_CURRENT_LIMIT): super().__init__() self.__motor_type = motor_type if self.__motor_type == self.STEPPER: self.NAME += " (Stepper)" self.__frequency = frequency + self.__current_limit = current_limit + + # An ascending order list of current limits with the pin states to achieve them + self.__current_limit_states = OrderedDict({ + CURRENT_LIMIT_1: (0, 0), + CURRENT_LIMIT_2: (-1, 0), + CURRENT_LIMIT_3: (0, -1), + CURRENT_LIMIT_4: (1, 0), + CURRENT_LIMIT_5: (-1, -1), + CURRENT_LIMIT_6: (0, 1), + CURRENT_LIMIT_7: (1, -1), + CURRENT_LIMIT_8: (-1, 1), + CURRENT_LIMIT_9: (1, 1), + }) def initialise(self, slot, adc1_func, adc2_func): try: @@ -51,14 +77,15 @@ def initialise(self, slot, adc1_func, adc2_func): # Create motor objects self.motors = [Motor((self.__pwms_p[i], self.__pwms_n[i]), freq=self.__frequency) for i in range(len(self.__pwms_p))] else: - from adafruit_motor.stepper import StepperMotor + # from adafruit_motor.stepper import StepperMotor - self.stepper = StepperMotor(self.__pwms_p[0], self.__pwms_n[0], self.__pwms_p[1], self.__pwms_n[1]) + # self.stepper = StepperMotor(self.__pwms_p[0], self.__pwms_n[0], self.__pwms_p[1], self.__pwms_n[1]) + raise NotImplementedError("Stepper Motor support for the Dual Motor Module is currently not implemented") # Create motor control pin objects - self.__motors_decay = slot.SLOW1 - self.__motors_toff = slot.SLOW2 self.__motors_en = slot.SLOW3 + self.__motors_vref1 = slot.SLOW1 + self.__motors_vref2 = slot.SLOW2 # Pass the slot and adc functions up to the parent now that module specific initialisation has finished super().initialise(slot, adc1_func, adc2_func) @@ -70,9 +97,8 @@ def reset(self): else: self.stepper.release() - self.__motors_decay.init(Pin.OUT, value=False) - self.__motors_toff.init(Pin.OUT, value=False) self.__motors_en.init(Pin.OUT, value=False) + self.current_limit(self.__current_limit) def enable(self): self.__motors_en.value(True) @@ -83,17 +109,41 @@ def disable(self): def is_enabled(self): return self.__motors_en.value() == 1 - def decay(self, value=None): - if value is None: - return self.__motors_decay() == 1 - else: - self.__motors_decay(value) - - def toff(self, value=None): - if value is None: - return self.__motors_toff() == 1 + def current_limit(self, amps): + if amps is None: + return self.__current_limit else: - self.__motors_toff(value) + if self.is_enabled(): + raise RuntimeError("Cannot change current limit whilst motor driver is active") + + # Start with the lowest limit + chosen_limit = CURRENT_LIMIT_1 + chosen_state = self.__current_limit_states[CURRENT_LIMIT_1] + + # Find the closest current limit below the given amps value + for limit, state in self.__current_limit_states.items(): + if limit < amps: + break + chosen_limit = limit + chosen_state = state + + if chosen_state[0] == -1: + self.__motors_vref1.init(Pin.IN) + elif chosen_state[0] == 0: + self.__motors_vref1.init(Pin.OUT, value=False) + else: + self.__motors_vref1.init(Pin.OUT, value=True) + + if chosen_state[1] == -1: + self.__motors_vref2.init(Pin.IN) + elif chosen_state[1] == 0: + self.__motors_vref2.init(Pin.OUT, value=False) + else: + self.__motors_vref2.init(Pin.OUT, value=True) + + self.__current_limit = chosen_limit + + logging.info(self.__message_header() + f"Current limit set to {self.__current_limit}A") @property def motor1(self): diff --git a/lib/pimoroni_yukon/timing.py b/lib/pimoroni_yukon/timing.py index 335e95f..c8bcad2 100644 --- a/lib/pimoroni_yukon/timing.py +++ b/lib/pimoroni_yukon/timing.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2023 Christopher Parrott for Pimoroni Ltd +# +# SPDX-License-Identifier: MIT + from time import ticks_ms, ticks_add, ticks_diff