diff --git a/MTB_Test.py b/MTB_Test.py new file mode 100644 index 0000000..680b681 --- /dev/null +++ b/MTB_Test.py @@ -0,0 +1,279 @@ +# -*- coding: utf-8 -*- +""" +MTB Calibration + Qualification Script (v3) +----------------------------------------- +Auf Wunsch angepasste Zielgrenzen und Iterationen : +* **Kalibrierung**: |ppm| < 1000 als Ziel, max 5 Iterationen. +* **Qualifikation**: ``OK`` ≤ 10 000 ppm (≈1 nA @ 100 µA FS), + ``COOL`` ≤ 5 000 ppm ; sonst ``FAIL``. +Weitere Features aus v2 bleiben unverändert. +""" + +from __future__ import annotations +import sys, time, numpy as np, pyvisa, serial.tools.list_ports +from typing import Sequence +from pymeasure.instruments.keithley import Keithley2400, KeithleyDMM6500 +from ModuleTestBox import ModuleTestBox +from sklearn.linear_model import LinearRegression +import json + +# ---------- Gerätesuche ------------------------------------------------------- +rm = pyvisa.ResourceManager() + +def find_visa(hint: str) -> str: + for res in rm.list_resources(): + if hint.lower() in res.lower(): + return res + raise RuntimeError(f'VISA-Resource "{hint}" nicht gefunden') + +def find_serial(hint: str) -> str: + for port, desc, _ in serial.tools.list_ports.comports(): + if hint.lower() in desc.lower(): + return port + raise RuntimeError(f'Serial-Port "{hint}" nicht gefunden') + +# ---------- User-Prompt ------------------------------------------------------- + +def ask_user(prompt: str = "Last umgesteckt? =OK, n=repeat, q=quit → ") -> str: + while True: + ans = input(prompt).strip().lower() + if ans in ("", "y", "yes"): + return "ok" + if ans in ("n", "no"): + return "repeat" + if ans == "q": + sys.exit("Abbruch durch Benutzer") + print("Bitte nur Enter, n oder q eingeben …") + +# ---------- Mess-Hilfsfunktionen --------------------------------------------- + +VOLTAGES = np.arange(10, 201, 10) # 10 … 200 V, 10 V-Step +VOLTAGES_FULLRANGE = np.arange(10, 1001, 10) # 1 … 100 V, 10 V-Step +CAL_TARGET = 100 # ppm-Band für Kalibrierung +CAL_MAX_ITERS = 5 # max. Iterationen +QUAL_LIMIT_COOL = 100 # ppm für "cool" +QUAL_LIMIT_OK = 500 # ppm für "ok" -> 1nA @ 2uA +SHUNT_OHM = 102.9807197e6 # Referenz-Widerstand (Ω) +SETTLING_S = 0.2 # Settling-Zeit pro Step + + +def sweep_currents(kei: Keithley2400, mtb: ModuleTestBox): + """Sweep über VOLTAGES - liefert (I_ref, I_dut).""" + I_ref, I_dut = [], [] + kei.enable_source() + for v in VOLTAGES: + kei.source_voltage = v + time.sleep(SETTLING_S) + I_ref.append(v / SHUNT_OHM) + I_dut.append(mtb.GetI() * -1e-12) + kei.disable_source() + return np.asarray(I_ref), np.asarray(I_dut) + +def sweep_currents_fullRange(kei: Keithley2400, mtb: ModuleTestBox): + """Sweep über VOLTAGES - liefert (I_ref, I_dut).""" + I_ref, I_dut = [], [] + kei.enable_source() + for v in VOLTAGES_FULLRANGE: + kei.source_voltage = v + time.sleep(SETTLING_S) + I_ref.append(v / SHUNT_OHM) + I_dut.append(mtb.GetI() * -1e-12) + kei.disable_source() + return np.asarray(I_ref), np.asarray(I_dut) + +def lineaer_fit(x: Sequence[float], y: Sequence[float]) -> tuple[float, float]: + """Lineare Regression: y = m * x + b""" + x = np.asarray(x).reshape(-1, 1) + y = np.asarray(y).reshape(-1, 1) + model = LinearRegression() + model.fit(x, y) + m = model.coef_[0][0] + b = model.intercept_[0] + return m, b + + +def gain_ppm(I_ref: Sequence[float], I_dut: Sequence[float]) -> float: + slope, offset = np.polyfit(I_ref, I_dut, 1) + # print(f" Fit: {curve:.3e} * x² + {slope:.3e} * x + {offset:.3e}") + return (1.0 / slope - 1.0) * 1e6 + +# ---------- Hauptprogramm ----------------------------------------------------- + +dataToSave = [] + +if __name__ == "__main__": + serial_id = int(input("Enter the MTB Serial ID [default 100]: ") or "100") + print(f"Serial ID = {serial_id}") + + with Keithley2400(find_serial("Prolific")) as kei, \ + KeithleyDMM6500(find_visa("0x05E6::0x6500")) as dmm, \ + ModuleTestBox(hints=["VID:PID=CAFE:4001"], verbose=False) as mtb: + + print("Verbunden. FW:", mtb.GetFirmwareVersion()) + if mtb.HV_IsLocked(): + sys.exit("HV ist gesperrt - Abbruch") + + mtb.SetBoardID(serial_id) + mtb.SetMeanCount(60) + dataToSave.append({"serial_id": serial_id, "firmware": mtb.GetFirmwareVersion()}) + mtb.HV_SetAll(0) + + kei.apply_voltage(compliance_current=11e-6) # 11 µA-Limit + + dataChannels = [] + + for ch in range(1, 9): + print(f"\n=== Channel {ch} ===") + mtb.SelectChannel(ch) + mtb.HV_Enable(True) + mtb.AdjustScaleI(0) + + # ------ Kalibrier-Loop ------ + corr_ppm = 0.0 + for it in range(CAL_MAX_ITERS): + try: + mtb.AdjustScaleI(round(corr_ppm)) + except Exception as e: + print(f" Fehler bei der Kalibrierung: {e}") + break + I_ref, I_dut = sweep_currents(kei, mtb) + ppm = gain_ppm(I_ref, I_dut) + print(f" Iter {it}: gain error = {ppm:+.0f} ppm") + if abs(ppm) < CAL_TARGET: + break + corr_ppm += ppm + print(f"→ final gain error for channel {ch}: {corr_ppm:+.0f} ppm") + + # ------ Qualifikations-Sweep ------ + I_ref, I_dut = sweep_currents(kei, mtb) + ppm_chk = abs(gain_ppm(I_ref, I_dut)) + if ppm_chk <= QUAL_LIMIT_COOL: + status = "COOL" # ≤5 k + elif ppm_chk <= QUAL_LIMIT_OK: + status = "OK" # ≤10 k + else: + status = "FAIL" + print(f"Qualification: {ppm_chk:.0f} ppm → {status}") + + print("Doing full range sweep …") + I_ref, I_dut = sweep_currents_fullRange(kei, mtb) + + kei.beep(1000, 0.5) + + # ------ Benutzer Prompt ------ + if ask_user() == "repeat": + print("Wiederhole Kanal …") + ch -= 1 + continue + + data = { + 'v': VOLTAGES_FULLRANGE.tolist(), + 'i_ref': I_ref.tolist(), + 'i_dut': I_dut.tolist() + } + + dataChannels.append({ + "channel": ch, + "gain_error": corr_ppm, + "qualification": status, + "data": data + }) + + mtb.HV_SetAll(0) + + print("\nAlle Kanäle bearbeitet. Kalibrierung beendet.") + dataToSave.append({"HVchannels": dataChannels}) + print("Tests starten …") + print("Interlock aktivieren …") + + while True: + if mtb.HV_IsLocked(): + print("Interlock detected") + break + time.sleep(0.1) + + print("Interlock Reset Button drücken") + print(mtb.HV_GetAll()) + + while True: + if mtb.HV_GetAll(): + print("Interlock released") + break + time.sleep(0.1) + + input("Interlock Reset Button gedrückt. zum Fortfahren …") + print("Bias Test starten …") + + dataChannels = [] + + dmm.measure_current(max_current=0.001) + dmm.current_nplc = 12 + + for ch in range(1, 9): + print(f"\n=== Channel {ch} ===") + mtb.SelectChannel(ch) + mtb.Bias_Enable(True) + + voltages = range(18, 901, 9) + currents = [] + dmmcurrent = [] + failed = False + for v in voltages: + mtb.Bias_SetV(v / 1000) + time.sleep(SETTLING_S/2) + dmmcur = dmm.current + while dmmcur > 1: + dmmcur = dmm.current + dmmcurrent.append(dmmcur * 1e6) + current = mtb.Bias_GetI() + currents.append(current * 1e6) + + if abs(current - dmmcur) >= (2e-6 + (v/300 * 1e-6)): + print(f" Bias-Test für Kanal {ch} fehlgeschlagen: {current:.3e} != {dmmcur:.3e}") + failed = True + + mtb.Bias_SetV(0) + mtb.Bias_Enable(False) + + if not failed: + print(f"Bias-Test für Kanal {ch} erfolgreich") + status = "OK" + else: + status = "FAIL" + + kei.beep(1000, 0.5) + + # ------ Benutzer Prompt ------ + if ask_user() == "repeat": + print("Wiederhole Kanal …") + ch -= 1 + continue + + dataChannels.append({ + "channel": ch, + "voltages": list(voltages), + "currents": currents, + "dmmcurrent": dmmcurrent, + "status": status + }) + + print("\nAlle Kanäle bearbeitet. Bias-Test beendet.") + dataToSave.append({"Biaschannels": dataChannels}) + print("Tests abgeschlossen. Daten speichern …") + # check if file exists, if yes, append number to filename + i = 1 + #save json data to file + filename = f"TestResults/MTB_Test_{serial_id}.json" + while True: + try: + with open(filename, "x") as f: + json.dump(dataToSave, f, indent=4) + break + except FileExistsError: + filename = f"TestResults/MTB_Test_{serial_id}_{i}.json" + i += 1 + print("Daten gespeichert.") + print("Programm beendet.") + + + diff --git a/MTB_UploadResults.py b/MTB_UploadResults.py new file mode 100644 index 0000000..bfa972d --- /dev/null +++ b/MTB_UploadResults.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Restore calibration settings on MTB: +- Seriennummer eingeben +- Neueste JSON-Datei laden +- meanCount und AdjustScaleI pro Kanal anwenden +""" + +import json +import glob +import os +import sys +import serial.tools.list_ports +from ModuleTestBox import ModuleTestBox + +def find_latest_file(serial_id: int, folder: str = "TestResults") -> str: + pattern = os.path.join(folder, f"MTB_Test_{serial_id}*.json") + files = glob.glob(pattern) + if not files: + raise FileNotFoundError(f"Keine Dateien gefunden für Serial {serial_id!r}") + # sortiere nach Suffix (höchste Zahl zuletzt) + files.sort(key=lambda f: int(os.path.splitext(f)[0].rsplit('_',1)[-1]) if '_' in os.path.splitext(f)[0] else 0) + return files[-1] + +def prompt_mean_count() -> int: + while True: + try: + mc = int(input("MeanCount nicht in Datei gefunden. Bitte eingeben (z.B. 60): ")) + return mc + except ValueError: + print("Bitte eine ganze Zahl eingeben.") + +def main(): + serial_id = int(input("Seriennummer eingeben: ").strip()) + try: + filename = find_latest_file(serial_id) + except FileNotFoundError as e: + print(e) + sys.exit(1) + print(f"Lade Datei: {filename}") + with open(filename, "r") as f: + data = json.load(f) + + # Versuch, meanCount auszulesen + mean_count = None + for entry in data: + if "meanCount" in entry: + mean_count = entry["meanCount"] + break + if mean_count is None: + mean_count = prompt_mean_count() + print(f"Verwende meanCount = {mean_count}") + + # Hole gain_error pro Kanal + hv_channels = next((e["HVchannels"] for e in data if "HVchannels" in e), None) + if hv_channels is None: + print("Keine HVchannels in der Datei gefunden.") + sys.exit(1) + + # Verbinden und hochladen + print("Verbinde mit MTB …") + with ModuleTestBox(hints=["VID:PID=CAFE:4001"], verbose=False) as mtb: + mtb.SetBoardID(serial_id) + mtb.SetMeanCount(mean_count) + for ch_info in hv_channels: + ch = ch_info["channel"] + corr_ppm = ch_info["gain_error"] + print(f"Channel {ch}: AdjustScaleI({corr_ppm:+.0f})") + mtb.SelectChannel(ch) + mtb.AdjustScaleI(int(round(corr_ppm))) + print("Fertig. Alle Einstellungen geladen.") + +if __name__ == "__main__": + main() diff --git a/ModuleTestBox.py b/ModuleTestBox.py new file mode 100644 index 0000000..6681ccf --- /dev/null +++ b/ModuleTestBox.py @@ -0,0 +1,344 @@ +import numpy as np +import serial +import serial.tools.list_ports + + +class ModuleTestBox: + + def __init__(self, port=None, hints=None, verbose=False): + self.ser = None # port handle + self.port = port # port name COMn + self.portdesc = '' + self.hints = hints + self.verbose = verbose + self.portlist = {} # port list { port: description } + self.UpdateComPortList() + + def __enter__(self): + self._Connect() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._Disconnect() + if exc_type is not None: + print(f"Exception {exc_type}') # occurred with value {exc_value}") + # except serial.SerialException: + return True + + # Update the serial port list + def UpdateComPortList(self): + ports = serial.tools.list_ports.comports() + self.portlist = {} + if self.verbose: print('* serial ports found:') + for port, desc, hwid in sorted(ports): + self.portlist[port] = hwid + if self.verbose: print(f'{port:7s}: {desc}, {hwid}') + + # Finds a specific serial port based on a keyword (hint) in the description + def FindComPort(self, hints): + for port, desc in self.portlist.items(): + for hint in hints: + if hint in desc: + self.port = port + self.portdesc = desc + return port + self.port = None + return None + + # Checks if there is a connection to a serial port + def IsConnected(self): + return self.ser is not None + + # Connect to a serial port by specific port or a hint in the description + def _Connect(self): + if self.hints is not None: + self.FindComPort(self.hints) + if self.port is not None: + if self.verbose: print(f'* open {self.port:s} ({self.portdesc})') + self.ser = serial.Serial(self.port, 115200, timeout=0.5) + + def _Disconnect(self): + if self.IsConnected(): + self.ser.close() + + def TestConnection(self): + testdata = [0x5A, 0xA5] + # Test connection PC <-> HV box + for x in testdata: + y = self.Echo(x) + if y != x: + print(f'Communication failed: {y:02X} should be {x:02X}') + return False + # Test ADC register read/write (repeat for all ADCs) + adc_ok = True + for channel in range(1, 9): + self.SelectChannel(channel) + for x in testdata: + self.ADC_RegisterWrite(0x13, x) + y = self.ADC_RegisterRead(0x13) + if y != x: + print(f'ADC Register IO failed: {y:02X} should be {x:02X}') + adc_ok = False + if self.verbose: + adc = self.GetADCversion() + print(f'CH{channel}: ADC TYPE:{adc["ver"]} REV:{adc["rev"]} ID:{adc["id"]}') + return adc_ok + + def _PutStr(self, s): + self.ser.write(s.encode('ASCII')) + + def _PutINT8(self, x): + self.ser.write(x.to_bytes(1, signed=True, byteorder='little')) + + def _PutINT16(self, x): + self.ser.write(x.to_bytes(2, signed=True, byteorder='little')) + + def _PutINT32(self, x): + self.ser.write(x.to_bytes(4, signed=True, byteorder='little')) + + def _PutUINT8(self, x): + self.ser.write(x.to_bytes(1, signed=False, byteorder='little')) + + def _PutUINT16(self, x): + self.ser.write(x.to_bytes(2, signed=False, byteorder='little')) + + def _PutUINT32(self, x): + self.ser.write(x.to_bytes(4, signed=False, byteorder='little')) + + def _GetUINT8(self): + bv = self.ser.read(size=1) + return int.from_bytes(bv, signed=False, byteorder='little') + + def _GetUINT16(self): + bv = self.ser.read(size=2) + return int.from_bytes(bv, signed=False, byteorder='little') + + def _GetUINT32(self): + bv = self.ser.read(size=4) + return int.from_bytes(bv, signed=False, byteorder='little') + + def _GetINT32(self): + bv = self.ser.read(size=4) + return int.from_bytes(bv, signed=True, byteorder='little') + + def _Get2INT32(self): + bv = self.ser.read(size=8) + i = int.from_bytes(bv[0:4], signed=True, byteorder='little') + u = int.from_bytes(bv[4:8], signed=True, byteorder='little') + return i, u + + + def _CommandReq(self, command): + self.ser.write(command.encode('ASCII')) + + def _CommandRsp(self, retform): + n = sum(retform) + bv = self.ser.read(size=n) + # split return bytes value + pos = 0 + ret = [] + for size in retform: + ret.append(int.from_bytes(bv[pos:pos+size], signed=False, byteorder='little')) + pos += size + return ret + + def _Request(self, command, retform): + self._CommandReq(command) + return self._CommandRsp(retform) + + + def GetFirmwareVersion(self): + self._CommandReq('S0') + n = self._GetUINT8() + d = [self._GetUINT8() for _ in range(n)] + return ''.join(chr(i) for i in d) + + def GetBoardID(self): + self._CommandReq('SI') + return self._GetUINT32() + + def SetBoardID(self, id): + self._CommandReq('SD') + self._PutUINT32(id) + + def SelectChannel(self, n): + if n-1 in range(8): + self._CommandReq('12345678'[n-1]) + + def GetI(self): + self._CommandReq('I') + return self._GetINT32() + + def GetV(self): + self._CommandReq('V') + return self._GetINT32() + + def GetIV(self): + self._CommandReq('D') + return self._GetINT32(), self._GetINT32() + + def ScanI(self, fs, n): + self._CommandReq('MI') + self._PutUINT8(fs) + self._PutUINT16(n) + N = self._GetUINT16() + + i = np.zeros(N, dtype=int) + for k in range(N): + i[k] = self._GetINT32() + return i + + def ScanV(self, fs, n): + self._CommandReq('MV') + self._PutUINT8(fs) + self._PutUINT16(n) + N = self._GetUINT16() + + u = np.zeros(N, dtype=int) + for k in range(N): + u[k] = self._GetINT32() + return u + + def ScanIV(self, fs, n): + self._CommandReq('MD') + self._PutUINT8(fs) + self._PutUINT16(n) + N = self._GetUINT16() + + i = np.zeros(N, dtype=int) + u = np.zeros(N, dtype=int) + for k in range(N): + i[k], u[k] = self._Get2INT32() + return i, u + + def GetError(self): + status = self._Request('SE', (2,))[0] + if status == 0: return 'Status Ok' + return f'Error {status:04X}' + + def GetADCversion(self): + self._CommandReq('SV') + b = self.ser.read(size=8) + s = ('ADE9113', 'ADE9112', 'ADE9103')[b[1]] if b[1] <= 3 else '?' + if len(b) >= 8: + id = f'{b[2]:02X}{b[3]:02X}{b[4]:02X}{b[5]:02X}{b[6]:02X}{b[7]:02X}' + else: + id = '????' + + adc = {'ver':s, 'rev':f'{b[0]:02X}', 'id':id} + return adc + + + + def SetRawData(self, unit=False): + self._CommandReq('SU') + self._PutUINT8(1 if unit else 0) + + def SetMeanCount(self, n): + self._CommandReq('SN') + self._PutUINT8(n) + + def AdjustOffsetI(self): + self._CommandReq('SCI') + return self._GetINT32() + + def AdjustOffsetV(self): + self._CommandReq('SCV') + return self._GetINT32() + + def AdjustScaleI(self, value): + self._CommandReq('SAI') + self._PutINT32(value) + + def AdjustScaleV(self, value): + self._CommandReq('SAV') + self._PutINT32(value) + + def ADC_Transmit(self, txdata): + self._CommandReq('X1') + self._PutUINT32(txdata) + + def ADC_Receive(self): + self._CommandReq('X2') + return self._GetUINT32() + + def ADC_Transfer(self, txdata): + self._CommandReq('XT') + self._PutUINT32(txdata) + return self._GetUINT32() + + def ADC_RegisterWrite(self, reg, value): + self._CommandReq('XW') + self._PutUINT8(reg) + self._PutUINT8(value) + + def ADC_RegisterRead(self, reg): + self._CommandReq('XR') + self._PutUINT8(reg) + return self._CommandRsp((1,))[0] + + def ADC_RegisterRead2(self, reg): + self._CommandReq('Xr') + self._PutUINT8(reg) + return self._CommandRsp((1,1)) + + def ReadADC_I(self): + self._CommandReq('XI') + return self._GetINT32() + + def ReadADC_V(self): + self._CommandReq('XV') + return self._GetINT32() + + def ReadADC_IV(self): + self._CommandReq('XD') + return self._Get2INT32() + + def ADC_Init(self): + self._CommandReq('XC') + + def ADC_Sleep(self, sleep=True): + self._CommandReq('XS') + self._PutINT8(1 if sleep else 0) + + def AddCRC(self, d): + self._CommandReq('XX') + self._PutUINT32(d) + return self._CommandRsp((4,))[0] + + def Echo(self, din): + self._CommandReq('XE') + self._PutUINT8(din) + return self._CommandRsp((1,))[0] + + def Bias_Enable(self, on): + self._CommandReq('BC') + self._PutINT8(1 if on else 0) + + def Bias_GetAll(self): + self._CommandReq('BS') + return self._CommandRsp((1,))[0] + + def Bias_SetV(self, v=0.0): + self._CommandReq('BV') + self._PutINT16(int(v*1000)) + + def Bias_GetI(self): + self._CommandReq('BI') + return self._GetINT32()/1000000000.0 + + def HV_Enable(self, on): + self._CommandReq('LC') + self._PutINT8(1 if on else 0) + + def HV_SetAll(self, on): + self._CommandReq('LA') + self._PutINT8(1 if on else 0) + + def HV_GetAll(self): + self._CommandReq('LS') + return self._CommandRsp((1,))[0] + + def HV_IsLocked(self): + self._CommandReq('SL') + return self._CommandRsp((1,))[0] != 0 diff --git a/biastest.py b/biastest.py new file mode 100644 index 0000000..506c06d --- /dev/null +++ b/biastest.py @@ -0,0 +1,72 @@ +from ModuleTestBox import ModuleTestBox +import sys, time, numpy as np, pyvisa, serial.tools.list_ports +from pymeasure.instruments.keithley import KeithleyDMM6500 +import matplotlib.pyplot as plt +from time import sleep +import pandas as pd + +hints=["VID:PID=CAFE:4001"] + +# ---------- Gerätesuche ------------------------------------------------------- +rm = pyvisa.ResourceManager() + +def find_visa(hint: str) -> str: + for res in rm.list_resources(): + if hint.lower() in res.lower(): + return res + raise RuntimeError(f'VISA-Resource "{hint}" nicht gefunden') + +channel = [] +ref = [] +dmmcurrent = [] +ch = 2 +count = range(18, 901, 9) + +with ModuleTestBox(hints=hints, verbose=True) as hvp, KeithleyDMM6500(find_visa(hint='0x05E6::0x6500')) as dmm: + hvp.SelectChannel(ch) + hvp.Bias_Enable(1) + # hvp.Bias_SetV(0.5) + + R = 1223.6841 #load resistance in Ohm + + dmm.measure_current(max_current=0.001) + dmm.current_nplc = 12 + + sleep(2) + + for u in count: + hvp.Bias_SetV(u/1000) + + sleep(0.1) + ref.append(u*0.9092396440596435 + 0) + dmmcur = dmm.current + while dmmcur > 1: + dmmcur = dmm.current + dmmcurrent.append(dmmcur * 1e6) + current = hvp.Bias_GetI() * 1e6 + channel.append(current) + + # print(channel) + + hvp.Bias_SetV(0) + hvp.Bias_Enable(0) + + data = pd.DataFrame({ + 'v': count, + 'i_hv': channel, + 'i_dmm': dmmcurrent, + 'i_ref': ref + }) + data.to_excel('data_biasTEST.xlsx', index=False) + + plt.figure() + plt.title('BiasingBox current measurements') + plt.ylabel('current [uA]') + plt.xlabel('measurements') + plt.grid() + plt.plot(channel, label=f'CH{ch}', marker='o', markersize=2) + # plt.plot(ref, label=f'ref', marker='o', markersize=2) + plt.plot(dmmcurrent, label=f'dmm', marker='o', markersize=2) + plt.legend() + + plt.show() \ No newline at end of file diff --git a/live_measurement.py b/live_measurement.py new file mode 100644 index 0000000..9ec6d52 --- /dev/null +++ b/live_measurement.py @@ -0,0 +1,31 @@ +from pymeasure.instruments.keithley import Keithley2400 +import serial.tools.list_ports +import pandas as pd +from ModuleTestBox import ModuleTestBox +from time import sleep +import matplotlib.pyplot as plt + + +channel = 7 + + +hints=["VID:PID=CAFE:4001"] + +try: + with ModuleTestBox(hints=hints, verbose=True) as mtb: + print("Connected to Device") + + mtb.SelectChannel(channel) + mtb.Bias_Enable(False) + mtb.HV_Enable(True) + + # sleep(2) + + while True: + current = mtb.GetI() / 1e6 + print(current) + sleep(0.5) +except Exception as e: + print(e) + exit() + diff --git a/measurement.py b/measurement.py new file mode 100644 index 0000000..6ee585b --- /dev/null +++ b/measurement.py @@ -0,0 +1,134 @@ +from pymeasure.instruments.keithley import Keithley2400, KeithleyDMM6500 +import serial.tools.list_ports +import pyvisa +import pandas as pd +from ModuleTestBox import ModuleTestBox +from time import sleep +import matplotlib.pyplot as plt + +rm = pyvisa.ResourceManager() + +def find_instr_device(hint): + """Scan available serial ports and return the first port whose description + contains the given hint (case-insensitive). Raise ValueError if not found. + """ + ports = rm.list_resources() + for port in ports: + if hint.lower() in port.lower(): + return port + raise ValueError(f"No valid port found for hint '{hint}'.") + +def find_device(hint): + """Scan available serial ports and return the first port whose description + contains the given hint (case-insensitive). Raise ValueError if not found. + """ + ports = serial.tools.list_ports.comports() + for port, desc, hwid in ports: + if hint.lower() in desc.lower(): + return port + raise ValueError(f"No valid port found for hint '{hint}'.") + +def plotData(data): + """Plot the data contained in the given DataFrame.""" + + fig, ax = plt.subplots(2, 1) + ax[0].plot(data['v'], data['i_kei'], label='Keithley 2400') + ax[0].plot(data['v'], data['i_dmm'], label='Keithley DMM 6500') + # ax[0].plot(data['v'], data['i_hv'], label='HV port') + ax[0].plot(data['v'], data['i_calc'], label='Calculated') + ax[0].set_xlabel('Voltage (V)') + ax[0].set_ylabel('Current (A)') + ax[0].legend() + ax[1].plot(data['v'], data['i_kei'] - data['i_calc'], label='Difference KEI') + ax[1].plot(data['v'], data['i_dmm'] - data['i_calc'], label='Difference DMM') + # ax[1].plot(data['v'], data['i_hv'] - data['i_calc'], label='Difference HV') + ax[1].set_xlabel('Voltage (V)') + ax[1].set_ylabel('Current difference (A)') + ax[1].legend() + plt.show() + +hints=["VID:PID=CAFE:4001"] + +if __name__ == '__main__': + try: + with Keithley2400(find_device(hint='Prolific')) as kei: + with KeithleyDMM6500(find_instr_device(hint='0x05E6::0x6500')) as dmm: + with ModuleTestBox(hints=hints, verbose=True) as hv: + print("Connected to Keithley 2400 and HV port.") + + if not hv.TestConnection(): + print("HV port not connected.") + exit() + print(f'Firmware: {hv.GetFirmwareVersion()}') + print(f'HV: {"locked" if hv.HV_IsLocked() else "unlocked"}') + + # perform a voltage ramp measurement with kei and measure current with kei and hv to compare + # and store the results in a pandas DataFrame + nSamples = 90 + vStart = 0.0 + vStop = 900.0 + + hv.HV_SetAll(0) + + # configure HV port + hv.SelectChannel(1) + hv.Bias_Enable(False) + hv.HV_Enable(True) + + # hv.SetMeanCount(60) + # hv.AdjustScaleI(0) # ppm + # hv.AdjustScaleV(-4512) # ppm + + # perform voltage ramp measurement with kei + kei.apply_voltage(compliance_current=1.1e-5) + kei.source_voltage = vStart + kei.enable_source() + # kei.current_nplc = 10 + kei.measure_current(10) + kei.current_range = 10e-6 + + dmm.measure_current(max_current=0.000001) + dmm.current_nplc = 12 + + + values = [] + + print("Starting voltage ramp measurement...") + + for i in range(nSamples): + v = vStart + i * (vStop - vStart) / nSamples + kei.source_voltage = v + sleep(0.1) + i_kei = kei.current + # i_kei = 0 + # i_dmm = 0 + i_dmm = dmm.current + # i_hv = hv.GetI() * -1e-12 + i_hv = 0 + + while i_dmm > 1: + i_dmm = dmm.current + # i_calc = v / 100.003e6 + # i_calc = v / 5068423720.2 + i_calc = v / 102980719.7 + + values.append((v, i_kei, i_dmm, i_hv, i_calc)) + + kei.disable_source() + + data = pd.DataFrame(values, columns=['v', 'i_kei', 'i_dmm', 'i_hv', 'i_calc']) + data['ppm'] = (data['i_calc'] - data['i_hv']) / data['i_calc'] * 1e6 + + print(data) + print(hv.GetError()) + + sample_cal = round(nSamples * 0.8) + print(f"Sample for calibration: {data['ppm'].iloc[sample_cal:sample_cal+50].mean()} ppm") + + # save data to Excel file + data.to_excel('data.xlsx', index=False) + + plotData(data) + + except Exception as e: + print(f"Error during programm: {e}") diff --git a/plot.py b/plot.py new file mode 100644 index 0000000..61ed12a --- /dev/null +++ b/plot.py @@ -0,0 +1,46 @@ +from matplotlib import pyplot as plt +import numpy as np +import pandas as pd + + + + + + +def plotData(data): + """Plot the data contained in the given DataFrame.""" + + fig, ax = plt.subplots(2, 1) + ax[0].plot(data['v'], data['i_dmm'], label='Keithley') + ax[0].plot(data['v'], data['i_hv'], label='HV port') + ax[0].plot(data['v'], data['i_calc'], label='Calculated') + ax[0].set_xlabel('Voltage (V)') + ax[0].set_ylabel('Current (1 uA)') + ax[0].legend() + ax[1].plot(data['v'], data['i_dmm'] - data['i_calc'], label='Difference KEI') + ax[1].plot(data['v'], data['i_hv'] - data['i_calc'], label='Difference HV') + ax[1].set_xlabel('Voltage (V)') + ax[1].grid() + ax[1].set_ylabel('Current difference (uA)') + ax[1].legend() + plt.show() + + +#import excel file +data = pd.read_excel('data_biasTEST.xlsx') +# data['ppm'] = (data['i_kei'] - data['i_hv']) / data['i_kei'] * 1e6 +# sample_cal = round(len(data) * 0.8) +# print(f"Sample for calibration: {data['ppm'].iloc[sample_cal:sample_cal+20].mean()} ppm") + +# linear regression for i_kei +from sklearn.linear_model import LinearRegression +X = data['v'].values.reshape(-1, 1) +y = data['i_hv'].values +reg = LinearRegression().fit(X, y) +data['i_calc'] = reg.predict(X) +print(f"Linear regression: {reg.coef_[0]} A/V") +print(f"Linear regression: {reg.intercept_} A") + + +print(data) +plotData(data) diff --git a/relaistest.py b/relaistest.py new file mode 100644 index 0000000..5ea38b2 --- /dev/null +++ b/relaistest.py @@ -0,0 +1,34 @@ +from ModuleTestBox import ModuleTestBox +from time import sleep + +hints=["VID:PID=CAFE:4001"] + +with ModuleTestBox(hints=hints, verbose=True) as hvp: + # hvp.SelectADC(1) + # hvp.Relais_SetCh(1) + # hvp.SelectADC(4) + # hvp.Relais_SetCh(1) + # hvp.SelectADC(8) + # hvp.Relais_SetCh(1) + while True: + try: + + for x in range(1, 9): + hvp.SelectChannel(x) + hvp.Bias_Enable(1) + hvp.Bias_SetV(0) + # sleep(0.1) + # sleep(0.1) + print(hvp.Bias_GetAll()) + input("Press enter to turn off Relais") + for x in range(1, 9): + hvp.SelectChannel(x) + hvp.Bias_Enable(0) + hvp.Bias_SetV(0) + # sleep(0.1) + + print(hvp.Bias_GetAll()) + + except KeyboardInterrupt: + print("KeyboardInterrupt") + break \ No newline at end of file