IRNAS / ppk2-api-python

Power Profiling Kit 2 unofficial python api.
http://irnas.eu
GNU General Public License v2.0
145 stars 37 forks source link

Very High measured Ampere peaks #36

Open MrGloriousFast opened 1 year ago

MrGloriousFast commented 1 year ago

Sometimes when i start the measurement it measures insane high peaks of like 9000 ampere.

I am using it in a test stand to test pcb's and the device goes into standby after a few minutes of no activity. To prevent this i wrote the following code to keep it busy: def watchdog_thread(self): self.watchdog_is_running = True while self.watchdog_is_running:

only do it if we are not using the device for measurements and we were doing nothing for a while

  delta_t = (datetime.now()-self.last_update).total_seconds()
  if not self.is_running and delta_t > self.watchdog_timeout:
    try:
      # just some random command so the device doesnt go into standby
      self.ppk2_test.get_modifiers()

      # alternatively we could make a small measurement, however this creates the same 9000ampere bugs
      #self.start()
      #time.sleep(1)
      #self.get_microampere()
      #self.stop()
      self.last_update = datetime.now()
    except:
      pass
      #print('self.ppk2_test.get_modifiers() failed in watchdog')
  time.sleep(1)

Could this code be the problem creating the insane peak measurements? Note that i am running it in another thread via python multithreading library. Which is not real multi processing as everyone knows.

i am measuring like this: def start(self): ''' before doing any measurements you have to set the voltage and start the measurements ''' if self.is_running: return self.ppk2_test.set_source_voltage(self.volt) self.ppk2_test.toggle_DUT_power("ON") # Enable DUT power self.ppk2_test.start_measuring() # Start measuring

and getting ameasurement like so:

def get_microAmpere(self, limit_uA = None): """ Get the median microampere value from measurements. If the value exceeds the critical threshold [limit_uA], perform an emergency shutdown. note: the shutdown will still take about 5 seconds before the power goes out """

# check for default limit value
if limit_uA is None:
  limit_uA = self.limit_uA

if self.ppk2_test is not None and self.is_running:
  retries = 0
  while retries < 5:
    read_data = self.ppk2_test.get_data()
    if read_data != b'':
      samples = self.ppk2_test.get_samples(read_data)
      median = statistics.median(samples)
      if median >= limit_uA:
        self.stop()
        print('Critical ampere exceeded. Turning power off')
      self.last_update = datetime.now() # update the last communication timestamp
      return median
    retries += 1
    time.sleep(0.01)
return None

i added the limit_uA so that i can test boards that might have a short somehwere on them. This way the power will be cut as soon as a ampere peak is detected. This way i can save components and resolder the board before testing again.

But this limit is getting triggered way too often and if i test the same board with the official software 'power profiler' its obvious that the board is fine and does not pull peaks of 9000 ampere.

NejcKle commented 11 months ago

Hi @MrGloriousFast,

The PPK2 connection should never timeout, as long as it is connected.

I suspect the start() and stop() methods you are using are the cause of the issue. When calling the stop() method the stop_measuring command is sent to the ppk2, which takes some time to actually execute and stop the measurements. If the start() command is issued too early this might cause issues.

Please try using the slightly modified version of the included Power Profiler:


import time
import csv
import datetime
from threading import Thread
# import numpy as np
# import matplotlib.pyplot as plt
# import matplotlib
from ppk2_api.ppk2_api import PPK2_MP as PPK2_API

class PowerProfiler():
    def __init__(self, serial_port=None, source_voltage_mV=3300, filename=None):
        """Initialize PPK2 power profiler with serial"""
        self.measuring = None
        self.measurement_thread = None
        self.ppk2 = None

        print(f"Initing power profiler")

        # try:
        if serial_port:
            self.ppk2 = PPK2_API(serial_port)
        else:
            serial_port = self.discover_port()
            print(f"Opening serial port: {serial_port}")
            if serial_port:
                self.ppk2 = PPK2_API(serial_port)

        try:
            ret = self.ppk2.get_modifiers()  # try to read modifiers, if it fails serial port is probably not correct
            print(f"Initialized ppk2 api: {ret}")
        except Exception as e:
            print(f"Error initializing power profiler: {e}")
            ret = None
            raise e

        if not ret:
            self.ppk2 = None
            raise Exception(f"Error when initing PowerProfiler with serial port {serial_port}")
        else:
            self.ppk2.use_ampere_meter()

            self.source_voltage_mV = source_voltage_mV

            self.ppk2.set_source_voltage(self.source_voltage_mV)  # set to 3.3V

            print(f"Set power profiler source voltage: {self.source_voltage_mV}")

            self.measuring = False
            self.current_measurements = []

            # local variables used to calculate power consumption
            self.measurement_start_time = None
            self.measurement_stop_time = None

            time.sleep(1)

            self.stop = False

            self.measurement_thread = Thread(target=self.measurement_loop, daemon=True)
            self.measurement_thread.start()

            # write to csv
            self.filename = filename
            if self.filename is not None:
                with open(self.filename, 'w', newline='') as file:
                    writer = csv.writer(file)
                    row = []
                    for key in ["ts", "avg1000"]:
                        row.append(key)
                    writer.writerow(row)

    def write_csv_rows(self, samples):
        """Write csv row"""
        with open(self.filename, 'a', newline='') as file:
            writer = csv.writer(file)
            for sample in samples:
                row = [datetime.datetime.now().strftime('%d-%m-%Y %H:%M:%S.%f'), sample]
                writer.writerow(row)

    def delete_power_profiler(self):
        """Join thread"""
        self.measuring = False
        self.stop = True

        print("Deleting power profiler")

        if self.measurement_thread:
            print(f"Joining measurement thread")
            self.measurement_thread.join()
            self.measurement_thread = None

        if self.ppk2:
            print(f"Disabling ppk2 power")
            self.disable_power()
            del self.ppk2

        print(f"Deleted power profiler")

    def discover_port(self):
        """Discovers ppk2 serial port"""
        ppk2s_connected = PPK2_API.list_devices()
        if(len(ppk2s_connected) == 1):
            ppk2_port = ppk2s_connected[0]
            print(f'Found PPK2 at {ppk2_port}')
            return ppk2_port
        else:
            print(f'Too many connected PPK2\'s: {ppk2s_connected}')
            return None

    def enable_power(self):
        """Enable ppk2 power"""
        if self.ppk2:
            self.ppk2.toggle_DUT_power("ON")
            return True
        return False

    def disable_power(self):
        """Disable ppk2 power"""
        if self.ppk2:
            self.ppk2.toggle_DUT_power("OFF")
            return True
        return False

    def measurement_loop(self):
        """Endless measurement loop will run in a thread"""
        while True and not self.stop:
            if self.measuring:  # read data if currently measuring
                read_data = self.ppk2.get_data()
                if read_data != b'':
                    samples, _ = self.ppk2.get_samples(read_data)
                    self.current_measurements += samples  # can easily sum lists, will append individual data
            time.sleep(0.001)  # TODO figure out correct sleep duration

    def _average_samples(self, list, window_size):
        """Average samples based on window size"""
        chunks = [list[val:val + window_size] for val in range(0, len(list), window_size)]
        avgs = []
        for chunk in chunks:
            avgs.append(sum(chunk) / len(chunk))

        return avgs

    def start_measuring(self):
        """Start measuring"""
        if not self.measuring:  # toggle measuring flag only if currently not measuring
            self.current_measurements = []  # reset current measurements
            self.measuring = True  # set internal flag
            self.ppk2.start_measuring()  # send command to ppk2
            self.measurement_start_time = time.time()

    def stop_measuring(self):
        """Stop measuring and return average of period"""
        self.measurement_stop_time = time.time()
        self.measuring = False
        self.ppk2.stop_measuring()  # send command to ppk2

        #samples_average = self._average_samples(self.current_measurements, 1000)
        if self.filename is not None:
            self.write_csv_rows(self.current_measurements)

    def get_min_current_mA(self):
        return min(self.current_measurements) / 1000

    def get_max_current_mA(self):
        return max(self.current_measurements) / 1000

    def get_num_measurements(self):
        return len(self.current_measurements)

    def get_average_current_mA(self):
        """Returns average current of last measurement in mA"""
        if len(self.current_measurements) == 0:
            return 0

        average_current_mA = (sum(self.current_measurements) / len(self.current_measurements)) / 1000 # measurements are in microamperes, divide by 1000
        return average_current_mA

    def get_average_power_consumption_mWh(self):
        """Return average power consumption of last measurement in mWh"""
        average_current_mA = self.get_average_current_mA()
        average_power_mW = (self.source_voltage_mV / 1000) * average_current_mA  # divide by 1000 as source voltage is in millivolts - this gives us milliwatts
        measurement_duration_h = self.get_measurement_duration_s() / 3600  # duration in seconds, divide by 3600 to get hours
        average_consumption_mWh = average_power_mW * measurement_duration_h
        return average_consumption_mWh

    def get_average_charge_mC(self):
        """Returns average charge in milli coulomb"""
        average_current_mA = self.get_average_current_mA()
        measurement_duration_s = self.get_measurement_duration_s()  # in seconds
        return average_current_mA * measurement_duration_s

    def get_measurement_duration_s(self):
        """Returns duration of measurement"""
        measurement_duration_s = (self.measurement_stop_time - self.measurement_start_time)  # measurement duration in seconds
        return measurement_duration_s

Use it like so:

p = PowerProfiler()
p.enable_power()
p.start_measuring()

while True:
    curr = p.get_average_current_mA()
    print(f"Average current: {curr}")
    time.sleep(0.001)

This will continously print the average current, you can do the monitoring inside this loop.

NejcKle commented 1 month ago

@MrGloriousFast does the above suggestion work by any chance?