hvps_lib
1# py-hvps-interface GUI of the Project Peta-pico-Voltron 2# petapicovoltron.com 3# Copyright 2017-2023 Samuel Rosset 4# Distributed under the terms of the GNU General Public License GNU GPLv3 5 6 7# This file is part of py-hvps-interface. 8 9# py-hvps-interface is free software: you can redistribute it and/or modify 10# it under the terms of the GNU General Public License as published by 11# the Free Software Foundation, either version 3 of the License, or 12# (at your option) any later version. 13# 14# py-hvps-interface is distributed in the hope that it will be useful, 15# but WITHOUT ANY WARRANTY; without even the implied warranty of 16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17# GNU General Public License for more details. 18# 19# You should have received a copy of the GNU General Public License 20# along with py-hvps-interface. If not, see <http://www.gnu.org/licenses/> 21 22from time import sleep 23 24import pandas as pd 25 26try: 27 import serial 28 import serial.tools.list_ports 29 import struct 30 import numpy as np 31 32 from datetime import datetime 33 import configparser 34 import ctypes 35 import json 36 import os 37 from sys import exit 38except ImportError: 39 serial = None 40 struct = None 41 np = None 42 datetime = None 43 configparser = None 44 ctypes = None 45 json = None 46 os = None 47 print("Some of the required packages are missing. Execute \'pip install -r requirements.txt\' " 48 "from the interface folder") 49 sleep(4) 50 exit() 51 52DEBUG = False 53LIB_VER = "1.1" # Version of this library 54LIB_NAME = 'Release the kraken' 55HVPSX_PID = 0x0632 56 57# compatibility dictionary: 58compatibility_dict = { 59 'Lib-1.0': ['Fw-1.0'], 60 'Lib-1.1': ['Fw-1.1'], 61 'Fw-1.0': ['Hw-1.1', 'Hw-1.2', 'Hw-1.3'], 62 'Fw-1.1': ['Hw-1.1', 'Hw-1.2', 'Hw-1.3', 'Hw-1.4', 'Hw-1.5'] 63} 64# Switching mode constants: off=0, on-DC=1, on-Switching=2, on-waveform=3 65SWMODE_OFF = 0 66SWMODE_DC = 1 # only SHVPS 67SWMODE_SW = 2 68SWMODE_WFRM = 3 69SWMODE_HIGH = 4 # only hvps-x 70SWMODE_LOW = 5 # only hvps-x 71 72# Switching source constants: Timer=0,External=1, Button=2, 73SWSRC_TMR = 0 74SWSRC_EXT = 1 75SWSRC_BTTN = 2 76 77# Voltage control mode constants: Regulated=0, External=1, Open loop=2 78VMODE_R = 0 79VMODE_EXT = 1 80VMODE_O = 2 81 82# Stroboscobic illumination mode: Off=0 Fixed position=1, sweeping mode=2 83STMODE_OFF = 0 84STMODE_FIXED = 1 85STMODE_SWEEP = 2 86 87# Standard user-defined functions 88FUNC_SINE = 0 # sine + offset 89FUNC_TRI = 1 # Triangle 90FUNC_TRAP = 2 # Trapezoid 91FUNC_CSTM = 3 # Custom waveform (read from file) 92 93# look-up tables 94LOOKUP_VOUT = 0 95LOOKUP_ADC_P = 1 96LOOKUP_ADC_O = 2 97 98# Error bits 99ERR_FIRM = 0b1 # Incompatibility between library-firmware-hardware 100ERR_TYPE = 0b10 # The connected device is not a SHVPS or a hvpsx 101ERR_JACK = 0b100 # Unused 102ERR_COM = 0b1000 # Communication error: Cannot communicate with connected device 103ERR_CONF = 0b10000 # Configuration error: the HVPS running the hvps software, but appears to be unconfigured 104ERR_PORT = 0b100000 # port cannot be open. Used by another process 105ERR_CMD = 0b1000000 # serial command not understood 106 107# HVPS-X related constants 108# Serial commands and codes 109SERIAL_ERROR = 0x21727245 # int value of 'Err!' 110SERIAL_OK = 0x4B4F4B4F # int value of 'OKOK' 111SERIAL_SVMAX = 0x01 112SERIAL_QVMAX = 0x02 113SERIAL_SVSET = 0x03 114SERIAL_QVSET = 0x04 115SERIAL_SAVE = 0x05 116SERIAL_SCONF = 0x06 117SERIAL_QCONF = 0x07 118SERIAL_QVER = 0x08 119SERIAL_SNAME = 0x09 120SERIAL_QNAME = 0x0a 121SERIAL_SF = 0x0b 122SERIAL_QF = 0x0c 123SERIAL_SSWMODE = 0x0d 124SERIAL_QSWMODE = 0x0e 125SERIAL_SDUTY = 0x0f 126SERIAL_QDUTY = 0x10 127SERIAL_SPID = 0x11 128SERIAL_QPID = 0x12 129SERIAL_SCAL = 0x13 130SERIAL_QCAL = 0x14 131SERIAL_SBTTNCFG = 0x15 132SERIAL_QBTTNCFG = 0x16 133SERIAL_SSWSRC = 0x17 134SERIAL_QSWSRC = 0x18 135SERIAL_SVMODE = 0x19 136SERIAL_QVMODE = 0x1a 137SERIAL_QVNOW = 0x1b 138SERIAL_QKILL = 0x1c 139SERIAL_QVNOWRAW = 0x1d 140SERIAL_SLKUP = 0x1e 141SERIAL_QLKUP = 0x1f 142SERIAL_SST = 0x20 143SERIAL_SHW = 0x21 144SERIAL_SCALMETH = 0x22 145SERIAL_QCALMETH = 0x23 146SERIAL_QMEM = 0x24 147SERIAL_LST = 0x25 148SERIAL_CPYST = 0x26 149SERIAL_XFERST = 0x27 150SERIAL_QTRACE = 0x28 151SERIAL_STRRATE = 0x29 152SERIAL_QTRRATE = 0x2A 153 154SERIAL_DADC = 0xA0 155SERIAL_WFMPWM = 0xA1 156 157# board configurations 158CONF_UNCONF = 0 # hvpsx unconfigured device 159CONF_BIPOLAR = 1 # hvpsx Bipolar board version 1 160CONF_UNIPOLAR = 2 # hvpsx Unipolar board version 1 161 162# setting types 163SETTINGS_CURRENT = 0 164SETTINGS_BACKUP = 1 165SETTINGS_FACTORY = 2 166 167# calibration methods 168CALMETH_POLYNOMIAL = 0 169CALMETH_LOOKUP = 1 170 171# DCDC converter 172DCDC_POS = 1 # positive DC/DC converter 173DCDC_NEG = 2 # negative DC/DC converter 174DCDC_BOTH = 3 # Both DC/DC converters 175 176VNOW_POS = 0x00 # Voltage reading from positive DC/DC 177VNOW_NEG = 0x01 # Voltage reading from negative DC/DC 178VNOW_OUT = 0x02 # Voltage reading at the hvps-x output 179 180# gains of the different PIDs 181PID_KPP = 0x0 # P for positive voltage DCDC converter 182PID_KIP = 0x1 183PID_KDP = 0x2 184PID_KPN = 0x3 # N for negative voltage DCDC converter 185PID_KIN = 0x4 186PID_KDN = 0x5 187PID_KPO = 0x6 # O for output waveform PID 188PID_KIO = 0x7 189PID_KDO = 0x8 190 191# Calibration constants 192CAL_C0P = 0x00 # P for positive voltage measurement 193CAL_C1P = 0x01 194CAL_C2P = 0x02 195CAL_C0N = 0x03 # N for negative voltage measurement 196CAL_C1N = 0x04 197CAL_C2N = 0x05 198CAL_C0O = 0x06 # O for output waveform measurement 199CAL_C1O = 0x07 200CAL_C2O = 0x08 201 202memorymap = { 203 'Fw-1.0': [(1, 'hw_major', 'B'), (1, 'hw_minor', 'B'), (1, 'fw_major', 'B'), (1, 'fw_minor', 'B'), 204 (1, 'setting_type', 'B'), (1, 'conf', 'B'), (1, 'cal_meth_p', 'B'), (1, 'cal_meth_o', 'B'), 205 (1, 'cal_meth_n', 'B'), (1, 'bttncfg', 'B'), (1, 'SwMode', 'B'), (1, 'SwSrc', 'B'), (1, 'VMode', 'B'), 206 (1, 'Padding', 'B'), (1, 'pid', 'H'), (1, 'Vmax', 'H'), (1, 'Vsetp_raw', 'H'), (1, 'Vsetn_raw', 'H'), 207 (1, 'Duty', 'H'), (12, 'Name', 's'), (21, 'lookup_ADC_p', 'h'), (21, 'lookup_ADC_n', 'h'), 208 (21, 'lookup_ADC_o', 'h'), (21, 'lookup_Vout', 'h'), (3, 'cp', 'f'), (3, 'cn', 'f'), (3, 'co', 'f'), 209 (1, 'Freq', 'f'), (1, 'kpp', 'f'), (1, 'kip', 'f'), (1, 'kdp', 'f')], 210 'Fw-1.1': [(1, 'hw_major', 'B'), (1, 'hw_minor', 'B'), (1, 'fw_major', 'B'), (1, 'fw_minor', 'B'), 211 (1, 'setting_type', 'B'), (1, 'conf', 'B'), (1, 'cal_meth_p', 'B'), (1, 'cal_meth_o', 'B'), 212 (1, 'cal_meth_n', 'B'), (1, 'bttncfg', 'B'), (1, 'SwMode', 'B'), (1, 'SwSrc', 'B'), (1, 'VMode', 'B'), 213 (1, 'Padding', 'B'), (1, 'pid', 'H'), (1, 'Vmax', 'H'), (1, 'Vsetp_raw', 'H'), (1, 'Vsetn_raw', 'H'), 214 (1, 'Duty', 'H'), (12, 'Name', 's'), (21, 'lookup_ADC_p', 'h'), (21, 'lookup_ADC_n', 'h'), 215 (21, 'lookup_ADC_o', 'h'), (21, 'lookup_Vout', 'h'), (3, 'cp', 'f'), (3, 'cn', 'f'), (3, 'co', 'f'), 216 (1, 'Freq', 'f'), (1, 'kpp', 'f'), (1, 'kip', 'f'), (1, 'kdp', 'f')] 217} 218memory_string = { 219 'Fw-1.0': '<14B5H12s84h13f', 220 'Fw-1.1': '<14B5H12s84h13f' 221} 222 223 224class HVPS: 225 """ Class to control a petapicovoltron hvps-x 226 227 The class implements the low-level functions to interface the hvps-x firmware 228 for easy communication with the hvps-x. In addition, a few higher-level functions are provided.""" 229 230 # ====Communication functions and class constructor==== 231 def __init__(self, port, init=True): 232 """ Initialises a HVPS object: dev = HVPS('/path/to/serial/port'). 233 234 Input: COM port to which the HVPS is connected.""" 235 self.name = '' 236 self.vmax = 0 237 self.swmode = 0 238 self.vsetp = 0 239 self.vsetn = 0 # negative voltage setpoint (for bipolar configuration) 240 self.vnowp = 0 241 self.vnown = 0 242 self.vnowo = 0 243 self.f = 0 244 self.duty = 0.5 245 self.cycles = 0 246 self.cycle_n = 0 # the current cycle number 247 self.swsrc = 0 248 self.vmode = 0 249 self.bttncfg = 0 # latching behaviour of the button 250 self.err = 0 251 self.stmode = 0 # Strobe mode 252 self.stpos = 0 # Strobe position 253 self.stdur = 0 # strobe duration 254 self.stsweep = 5000 # Strobe sweep time (default when HVPS starts) 255 self.ser = serial.Serial() # the serial connection to the HVPS 256 self.waveform_pts = [] # list to store the set points of the custom waveform 257 self.waveform_meas = [] # list to store the measured points of the custom waveform. 258 self.lookup_adc_p = [] # lookup tables for linear interpolation 259 self.lookup_adc_o = [] 260 self.lookup_v_out = [] 261 self.cp = [] # calibration coefficients. 262 self.co = [] 263 self.calmeth_p = -1 264 self.calmeth_n = -1 265 self.calmeth_o = -1 266 267 self.firmware = 0 268 self.hardware = 0 269 self.conf = 0 # configuration of the hvps-x 270 self.is_hvpsx = 0 271 272 try: 273 self.ser = serial.Serial(port, 115200, timeout=3) 274 except serial.SerialException: 275 self.err = self.err | ERR_PORT 276 if DEBUG: 277 print("Cannot connect to serial port. Probably busy.") 278 else: 279 self.ser.reset_input_buffer() 280 if DEBUG: 281 text = "connecting to " + port 282 print(text) 283 print("Serial port open") 284 z = self._send_receive_4bytes(SERIAL_QCONF) 285 if z == 0: # no response: the device connected doesn't talk using this protocol 286 self.err = self.err | ERR_COM 287 if DEBUG: 288 print("Communication error: the connected device doesn't reply to commands") 289 z_hvps = z & 0xFFFF 290 z_conf = (z & 0xFFFF0000) >> 16 291 if z_hvps == HVPSX_PID: 292 self.is_hvpsx = 1 293 if z_conf != CONF_UNCONF: 294 self.conf = z_conf 295 else: 296 self.err = self.err | ERR_CONF # the hvps-x is not configured 297 if DEBUG: 298 print("The hvps is not configured") 299 z = self._send_4bytes_receive_nbytes(SERIAL_QNAME, param1=0, param2=0, fstring='<12s') 300 z = z[0].split(b'\x00', 1)[0] 301 self.name = z 302 else: 303 self.err = self.err | ERR_TYPE # The connected device replied with Err! but is not a hvps-x 304 if DEBUG: 305 print("Type error: the device replies to commands, but is not a hvps-x.") 306 if init and not self.err: 307 self._initialise_hvpsx() 308 309 def close(self, zero=True): # closes connection with the HVPS 310 """Closes the connection to the HVPS. Sets voltage to 0 if board was connected 311 312 Input (optional): zero. If True, will set the voltage to 0 and the optocouplers to off before closing the 313 communication. If False, the hvps-x is left in its current state""" 314 315 if self.ser.is_open: 316 if not (self.err & ERR_COM): # if connected board is not running the firmware, do not send command 317 if zero: 318 self.s_vset(0, DCDC_BOTH) # set the voltage to 0 as a safety measure 319 self.s_sw_mode(SWMODE_OFF) # for hvps-x, this will disable both optocouplers. 320 self.ser.close() 321 322 if DEBUG: 323 print("Serial port closed") 324 325 def _read_4bytes_uint(self): 326 ans = self.ser.read(4) 327 z = int.from_bytes(ans, byteorder='little') 328 return z 329 330 def _read_4bytes_int16(self): # Read 4 bytes with a 16-bit signed int in bytes 0 and 1 (and ignore bytes 2,3) 331 ans1 = self.ser.read(2) 332 self.ser.read(2) # discard the last 2 bytes 333 z = int.from_bytes(ans1, byteorder='little', signed=True) 334 return z 335 336 def _read_4bytes_float(self): 337 ans = self.ser.read(4) 338 z = struct.unpack('<f', ans) # little endian: string starts with the low index of the array, which 339 # represents the low bits of the float 340 return z[0] 341 342 def _send_receive_4bytes(self, cmd, param1=0, param2=0, typ='uint'): 343 param2 = round(param2) 344 if param2 < 0: 345 param2 = param2 & 0xFFFF # represent is as two's complement 346 cmd = cmd + (param1 << 8) + (param2 << 16) 347 cmd_b = cmd.to_bytes(4, 'little') 348 self.ser.write(cmd_b) 349 if typ == 'uint': 350 z = self._read_4bytes_uint() 351 elif typ == 'int16': 352 z = self._read_4bytes_int16() 353 else: 354 z = self._read_4bytes_float() 355 return z 356 357 def _send_4bytes_receive_nbytes(self, cmd, param1=0, param2=0, fstring='<2B'): 358 n = struct.calcsize(fstring) 359 param2 = round(param2) 360 if param2 < 0: 361 param2 = param2 & 0xFFFF # represent it as two's complement 362 cmd = cmd + (param1 << 8) + (param2 << 16) 363 cmd_b = cmd.to_bytes(4, 'little') 364 self.ser.write(cmd_b) 365 waiting = self.ser.inWaiting() 366 x = self.ser.read(n) 367 z = struct.unpack(fstring, x) 368 return z # returns unpacked tuples 369 370 def _send_nbytes_receive_4bytes(self, cmd, packet, typ='uint'): 371 packet = bytes([cmd]) + packet 372 self.ser.write(packet) 373 if typ == 'uint': 374 z = self._read_4bytes_uint() 375 elif typ == 'int16': 376 z = self._read_4bytes_int16() 377 else: 378 z = self._read_4bytes_float() 379 return z 380 381 def _initialise_hvpsx(self): 382 self.check_version_compatibility() 383 self.q_vmax() 384 self.q_vset(DCDC_POS) 385 self.q_vset(DCDC_NEG) 386 self.q_duty() 387 self.q_f() 388 self.q_bttn_cfg() 389 self.q_sw_mode() 390 self.q_sw_src() 391 self.q_v_mode() 392 393 def convert_raw_to_voltage(self, vraw_main, vraw_out, which): 394 if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT: 395 which = VNOW_POS 396 # Download calibration values the first time the function is used 397 398 if self.calmeth_p == -1: # calibration method not yet fetched from HVPS 399 self.q_calibration_method(which=VNOW_POS) 400 if self.calmeth_n == -1: # calibration method not yet fetched from HVPS 401 self.q_calibration_method(which=VNOW_NEG) 402 if self.calmeth_o == -1: # calibration method not yet fetched from HVPS 403 self.q_calibration_method(which=VNOW_OUT) 404 405 if (self.calmeth_o == CALMETH_LOOKUP or self.calmeth_p == CALMETH_LOOKUP) and not self.lookup_v_out: 406 self.lookup_v_out = self.q_lookup(0) 407 if self.calmeth_p == CALMETH_LOOKUP and not self.lookup_adc_p: 408 self.lookup_adc_p = self.q_lookup(1) 409 if self.calmeth_o == CALMETH_LOOKUP and not self.lookup_adc_o: 410 self.lookup_adc_o = self.q_lookup(2) 411 412 if self.calmeth_p == CALMETH_POLYNOMIAL and not self.cp: # if we don't know what are the calibration values 413 self.cp.append(self.q_cal(CAL_C0P)) 414 self.cp.append(self.q_cal(CAL_C1P)) 415 self.cp.append(self.q_cal(CAL_C2P)) 416 417 if self.calmeth_o == CALMETH_POLYNOMIAL and not self.co: # if we don't know what are the calibration values 418 self.co.append(self.q_cal(CAL_C0O)) 419 self.co.append(self.q_cal(CAL_C1O)) 420 self.co.append(self.q_cal(CAL_C2O)) 421 422 if self.calmeth_o == CALMETH_LOOKUP: 423 v_out = np.interp(vraw_out, self.lookup_adc_o, self.lookup_v_out, left=None, right=None, period=None) 424 else: 425 v_out = self.co[0] + self.co[1] * vraw_out + self.co[2] * np.square(vraw_out) 426 427 if which == VNOW_POS: 428 if self.calmeth_p == CALMETH_LOOKUP: 429 v_main = np.interp(vraw_main, self.lookup_adc_p, self.lookup_v_out, left=None, right=None, period=None) 430 else: 431 v_main = self.cp[0] + self.cp[1] * vraw_main + self.cp[2] * np.square(vraw_main) 432 elif which == VNOW_NEG: 433 v_main = 0 434 else: 435 v_main = v_out 436 return v_main, v_out 437 438 def is_bipolar(self): # return true if connected device is hvpsx and bipolar unit 439 """ Returns true if the hvps-x configuration is bipolar.""" 440 if self.conf & 1: 441 return True 442 else: 443 return False 444 445 # ====Commands related to voltage and frequency==== 446 def s_vset(self, x, polarity=DCDC_POS): # sets the output voltage 447 """Sets the output voltage of the HVPS. The new parameter remains valid until a new call to this command, or 448 when the HVPS is powered off. Using the 449 save() command enables to save this parameter in memory\n 450 Although this command can be used at any time, it is mainly useful when the HVPS voltage control mode is 451 regulated (i.e. closed loop) (VMODE_R). In this case the hvps-x will adjust the control voltage to keep the 452 output to the set point; see s_vmode() command. 453 :param x: voltage set point in volt (int) 454 :param polarity: which DC/DC converter to address (DCDC_POS, DCDC_NEG (bipolar config), DCDC_BOTH) 455 :return: voltage set point accepted by hvps-x 456 """ 457 x = constrain(abs(x), 0, self.vmax) 458 x = int(x) 459 if polarity != DCDC_POS and polarity != DCDC_NEG and polarity != DCDC_BOTH: 460 polarity = DCDC_POS 461 z = self._send_receive_4bytes(SERIAL_SVSET, param1=polarity, param2=x) 462 if x == 0: 463 z = 0 # avoids calibration issue near 0 that would lead to a negative number which is saturated to 0 in 464 # the STM32 (as the stored raw value is uint), leading to a possible large offset error returned value of a 465 # few 10s of volts when back calculated in volts 466 if polarity == DCDC_POS: 467 self.vsetp = z 468 elif polarity == DCDC_NEG: 469 self.vsetn = z 470 else: # DCDC_Both: i.e. set the positive and negative voltage to the same level 471 self.vsetp = z 472 self.vsetn = z 473 if DEBUG: 474 y = "s_vset(" + str(x) + ") -> " + str(z) 475 print(y) 476 return z 477 478 def q_vset(self, polarity=DCDC_POS): # queries the voltage setpoint 479 """Queries the voltage set point. The returned value is in volts. 480 :param polarity: DCDC converter to be queried (DCDC_POS or DCDC_NEG). DCDC_NEG will return 0 for 481 unipolar configs 482 :return: Voltage set point in volts 483 """ 484 if polarity != DCDC_POS and polarity != DCDC_NEG: 485 polarity = DCDC_POS 486 z = self._send_receive_4bytes(SERIAL_QVSET, param1=polarity) 487 if DEBUG: 488 y = "q_vset -> " + str(z) 489 print(y) 490 if polarity == DCDC_NEG: 491 self.vsetn = z 492 else: 493 self.vsetp = z 494 return z 495 496 def q_vnow(self, which=VNOW_POS): # queries the voltage output 497 """Queries the feedback voltage of the HVPS. 498 :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the 499 positive DCDC converter, negative DCDC converter, or output of the HVPS 500 :return: voltage value in volts 501 """ 502 503 if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT: 504 which = VNOW_POS 505 z = self._send_4bytes_receive_nbytes(SERIAL_QVNOW, param1=which, fstring='<2h') 506 z_out = z[1] 507 z_main = z[0] 508 if DEBUG: 509 y = "q_vnow -> " + str(z_main) + " / " + str(z_out) 510 print(y) 511 self.vnowo = z_out 512 if which == VNOW_OUT: 513 self.vnowo = z_main 514 elif which == VNOW_NEG: 515 self.vnown = z_main 516 else: 517 self.vnowp = z_main 518 return z_main 519 520 def q_vnow_raw(self, which=VNOW_POS): # queries a raw value of the voltage output 521 """Queries the current feedback voltage of the HVPS. The returned value is a raw 12bit ADC value. This avoids 522 running slow floating point calculations on the 523 microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to 524 execute than q_vnow(). Useful for streaming voltage values 525 :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the 526 positive DCDC converter, negative DCDC converter, or output of the HVPS. 527 :return: a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT 528 (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is 529 a 12 bit value that can be converted to voltage using the unit's calibration values (see q_vnow_fast() for a 530 way to do this automatically""" 531 532 if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT: 533 which = VNOW_POS 534 z = self._send_receive_4bytes(SERIAL_QVNOWRAW, param1=which, typ='uint') 535 z_out = z & 0xFFFF 536 z_main = (z & 0xFFFF0000) >> 16 537 if DEBUG: 538 y = "q_vnow_raw -> " + str(z_main) + " , " + str(z_out) 539 print(y) 540 return z_main, z_out 541 542 def q_vnow_fast(self, which=VNOW_POS): # queries the voltage output 543 """Queries the current feedback voltage of the HVPS in a raw format and convert it to a calibrated voltage 544 This avoids running slow floating point calculations on the 545 microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to 546 execute than q_vnow(). Useful for streaming voltage values. This method is similar to q_vnow(), except that 547 the conversion from a raw value to a calibrated value is done 548 on the computer rather than on the microcontroller. It can take up to 300us to convert a value on the MUC 549 (it depends on the method used (linear, quadratic, lookup table) 550 :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the 551 positive DCDC converter, negative DCDC converter, or output of the HVPS. 552 :return: a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT 553 (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is 554 a 12 bit value that can be converted to voltage using the unit's calibration values The returned values 555 are calibrated voltages in Volt""" 556 557 z_main, z_out = self.q_vnow_raw(which=which) 558 v_main, v_out = self.convert_raw_to_voltage(z_main, z_out, which=which) 559 560 return v_main, v_out 561 562 def s_f(self, x): # sets the frequency 563 """Sets the frequency of the signal when the HVPS is in switching mode (SWMODE_SW).\n 564 The value returned is the new frequency, taking quantification into account. 565 :param x: frequency in Hz between 0.001 and 1000 566 :return: frequency accepted by hvps-x in Hz 567 """ 568 x = constrain(x, 0.001, 1000.0) 569 570 # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction 571 # Ex: 10.0: 0x41 0x20 0x00 0x00. 572 # 0 10000010 01000000000000000000000 573 # S=0 Positive 574 # Exp=130-127 (offset)=2^3=8 575 # Fraction=1+2^-2=1.25 576 # number=+1.25*8=10 577 f_byte = struct.pack('<f', x) 578 z = self._send_nbytes_receive_4bytes(SERIAL_SF, f_byte, typ='float') 579 580 if DEBUG: 581 y = "s_f(" + str(x) + ") -> " + str(z) 582 print(y) 583 self.f = z 584 return z 585 586 def q_f(self): # queries the frequency 587 """Queries the switching frequency. The returned value is in Hz.""" 588 z = self._send_receive_4bytes(SERIAL_QF, param1=0, param2=0, typ='float') 589 590 if DEBUG: 591 y = "q_f -> " + str(z) 592 print(y) 593 self.f = z 594 return z 595 596 def s_duty(self, x): # sets the duty cycle of the switching signal 597 """Sets the duty cycle of the switching signal 598 :param x: the duty cycle (float in the range 0-1) 599 :return: the current duty cycle (float between 0 and 1) 600 """ 601 duty = int(x * 1000) # hvps-x is coding duty cycle on a 0-1000 scale 602 603 z = self._send_receive_4bytes(SERIAL_SDUTY, param1=0, param2=duty, typ='uint') 604 z = z / 1000 605 if DEBUG: 606 y = "s_duty(" + str(x) + ") -> " + str(z) 607 print(y) 608 self.duty = z 609 return z 610 611 def q_duty(self): # queries the duty cycle of the switching signal 612 """queries the duty cycle of the switching signal 613 :return: the current duty cycle (float between 0 and 1) 614 """ 615 616 z = self._send_receive_4bytes(SERIAL_QDUTY, param1=0, param2=0, typ='uint') 617 z = float(z) / 1000.0 618 if DEBUG: 619 y = "q_duty() -> " + str(z) 620 print(y) 621 self.duty = z 622 return z 623 624 # ===Commands to change the voltage control and switching behaviour (mode and source)==== 625 def s_sw_mode(self, x): # sets the switching mode 626 """Sets the switching mode of the hvps-x. \n 627 SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off. 628 This effectively 629 connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV DC/DC converter. 630 SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. For unipolar hvps-x, this connects J4 to ground and 631 is equivalent to the DC mode of the shvps. For bipolar hvps-x, this connects J4 to the negative high voltage 632 produced by the negative board. The other modes behaves in a similar manner compared to the shvps. SWMODE_SW: 633 the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency. 634 The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the 635 save() command enables to save this parameter in memory 636 :param x: SWMODE_OFF, SWMODE_HIGH, SWMODE_LOW, SWMODE_SW 637 :return: Switching mode set by the HVPS 638 """ 639 640 if x > SWMODE_LOW: 641 x = SWMODE_OFF 642 643 z = self._send_receive_4bytes(SERIAL_SSWMODE, param1=x, param2=0, typ='uint') 644 if DEBUG: 645 y = "s_sw_mode(" + str(x) + ") -> " + str(z) 646 print(y) 647 self.swmode = z 648 return z 649 650 def q_sw_mode(self): # queries the switching mode 651 """queries the switching mode of the hvps-x. \n 652 SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off. 653 This effectively 654 connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV DC/DC converter. 655 SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. For unipolar hvps-x, this connects J4 to ground and 656 is equivalent to the DC mode of the shvps. For bipolar hvps-x, this connects J4 to the negative high voltage 657 produced by the negative board. The other modes behaves in a similar manner compared to the shvps. SWMODE_SW: 658 the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency. 659 :return: Switching mode set by the HVPS 660 """ 661 z = self._send_receive_4bytes(SERIAL_QSWMODE, param1=0, param2=0, typ='uint') 662 if DEBUG: 663 y = "q_sw_mode -> " + str(z) 664 print(y) 665 self.swmode = z 666 return z 667 668 def s_sw_src(self, x): # sets the switching source 669 """Sets the source of the switching signal. 670 671 Sets the source of the switching signal. Accepted values are: SWSRC_TMR for onboard 672 switching (from internal clock of the board), or SWSRC_BTTN for the push button.\n 673 Using the save() command enables to save this parameter in memory 674 :param x: SWSRC_TMR, or SWSRC_BTTN 675 :return: SWSRC_TMR, or SWSRC_BTTN 676 """ 677 if x > SWSRC_BTTN: 678 x = SWSRC_TMR 679 z = self._send_receive_4bytes(SERIAL_SSWSRC, param1=x, param2=0, typ='uint') 680 if DEBUG: 681 y = "s_sw_src(" + str(x) + ") -> " + str(z) 682 print(y) 683 self.swsrc = z 684 return z 685 686 def q_sw_src(self): # queries the switching source 687 """queries the source of the switching signal. 688 689 queries the source of the switching signal. Output values are: SWSRC_TMR for onboard 690 switching (from internal clock of the board), or SWSRC_BTTN for the push button.\n 691 Using the save() command enables to save this parameter in memory 692 :return: SWSRC_TMR, or SWSRC_BTTN 693 """ 694 695 z = self._send_receive_4bytes(SERIAL_QSWSRC, param1=0, param2=0, typ='uint') 696 697 if DEBUG: 698 y = "q_sw_src -> " + str(z) 699 print(y) 700 self.swsrc = z 701 return z 702 703 def s_bttn_cfg(self, x): # sets the configuration of the push button 704 """Defines the behaviour of the push button 705 706 The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the 707 save() command enables to save this parameter in memory 708 :param x: 2-bit value (0x0 to 0x3). bit 0: Defines the behaviour of the push button, when the switching source 709 of the HVPS is set to the push button 710 (SWSRC_BTTN, c.f. s_sw_src() command above). Accepted values are 0 and 1: 0 for a push button behaviour 711 (i.e. the high voltage is turned on as long as the button is pressed), 712 and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time 713 to turn it off).\n 714 bit 1: State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW 715 :return: the 2-bit button config value 716 """ 717 if x > 3: 718 x = 3 719 z = self._send_receive_4bytes(SERIAL_SBTTNCFG, param1=x, param2=0, typ='uint') 720 if DEBUG: 721 y = "s_bttn_cfg(" + str(x) + ") -> " + str(z) 722 print(y) 723 self.bttncfg = z 724 return z 725 726 def q_bttn_cfg(self): # queries the latch mode of the push button 727 """Queries the behaviour of the push button 728 729 :return: the 2-bit button config value. bit 0: Defines the behaviour of the push button, when the switching 730 source of the HVPS is set to the push button (SWSRC_BTTN, c.f. s_sw_src() command above). 731 Values are 0 for a push button behaviour (i.e. the high voltage is turned on as long as the button is pressed), 732 and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time 733 to turn it off).\n 734 bit 1: State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW 735 """ 736 z = self._send_receive_4bytes(SERIAL_QBTTNCFG, param1=0, param2=0, typ='uint') 737 if DEBUG: 738 y = "q_bttn_cfg -> " + str(z) 739 print(y) 740 self.bttncfg = z 741 return z 742 743 def q_kill(self): # queries the state of the kill button. Kill=1 means HV is disabled 744 """Queries whether HV is disabled (Kill=1) or enabled (kill=0). When kill = 1 there will not be a HV present at 745 the hvps-x output, irrespective of any software setting. 746 :return: 1 if the Switch S1 on the board is set to 0 (HV output is killed), 0 otherwise. 747 """ 748 z = self._send_receive_4bytes(SERIAL_QKILL, param1=0, param2=0, typ='uint') 749 750 if DEBUG: 751 y = "q_kill -> " + str(z) 752 print(y) 753 return z 754 755 def s_v_mode(self, x): # sets the voltage control mode 756 """Sets the voltage control mode 757 758 Sets the voltage control mode (i.e. how is the value of the output voltage controlled):\n 759 VMODE_R for internal voltage regulator (regulates the voltage to the value defined with the Vset command).\n 760 VMODE_O (that's an O like in open) internal open loop control (on-board regulator disconnected).\n 761 The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the 762 save() command enables to save this parameter in memory\n 763 VMODE_O has an internal safeguard that will decrease the setting if the sensing circuit saturates. However, 764 the voltage can still be slightly higher than the DCDC converter upper limit. User must check that the output 765 voltage remains within the allowed range 766 :param x: VMODE_R, VMODE_O 767 :return: VMODE_R, VMODE_O 768 """ 769 770 if x > VMODE_O: 771 x = VMODE_R 772 773 z = self._send_receive_4bytes(SERIAL_SVMODE, param1=x, param2=0, typ='uint') 774 if DEBUG: 775 y = "s_v_mode(" + str(x) + ") -> " + str(z) 776 print(y) 777 self.vmode = z 778 return z 779 780 def q_v_mode(self): # queries the switching source 781 """Queries the voltage control mode 782 783 :return: VMODE_R internal voltage regulator, VMODE_O internal 784 open loop control (on-board regulator disconnected). 785 """ 786 787 z = self._send_receive_4bytes(SERIAL_QVMODE, param1=0, param2=0, typ='uint') 788 789 if DEBUG: 790 y = "q_v_mode -> " + str(z) 791 print(y) 792 self.vmode = z 793 return z 794 795 # ====Functions to set configuration parameters==== 796 def s_pid(self, x, pid=PID_KPP): 797 """Sets the gains of the PIDs 798 Use save() command to save the new values to memory 799 :param x: the value (float) of the gain. 800 :param pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,N,O}, 801 for positive voltage PID, negative voltage PID, output voltage PID. (in Lib V1, Y can only take the value P) 802 :return: the gain value 803 """ 804 # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction 805 # Ex: 10.0: 0x41 0x20 0x00 0x00. 806 # 0 10000010 01000000000000000000000 807 # S=0 Positive 808 # Exp=130-127 (offset)=2^3=8 809 # Fraction=1+2^-2=1.25 810 # number=+1.25*8=10 811 pid_byte = struct.pack('<Bf', pid, x) 812 z = self._send_nbytes_receive_4bytes(SERIAL_SPID, pid_byte, typ='float') 813 814 if DEBUG: 815 y = "s_pid(" + str(x) + "," + str(pid) + ") -> " + str(z) 816 print(y) 817 return z 818 819 def q_pid(self, pid=PID_KPP): # queries the frequency 820 """returns the gains of the PIDs 821 822 :param pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,N,O}, 823 for positive voltage PID, negative voltage PID, output voltage PID. (in Lib V1, Y can only take the value P) 824 :return: the gain value of the chosen parameter 825 """ 826 827 z = self._send_receive_4bytes(SERIAL_QPID, param1=pid, param2=0, typ='float') 828 829 if DEBUG: 830 y = "q_pid(" + str(pid) + ") -> " + str(z) 831 print(y) 832 833 return z 834 835 def s_cal(self, x, cal=CAL_C1P): 836 """Sets the calibration constants of the analogue input signals (conversion to calibrated voltage values) 837 Use save() to commit the setting to memory 838 :param x: the value of the calibration coefficient (float) 839 :param cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,N,O}, for positive 840 voltage measurement, negative voltage measurement, output voltage measurement 841 :return: the calibration value 842 """ 843 844 # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction 845 # Ex: 10.0: 0x41 0x20 0x00 0x00. 846 # 0 10000010 01000000000000000000000 847 # S=0 Positive 848 # Exp=130-127 (offset)=2^3=8 849 # Fraction=1+2^-2=1.25 850 # number=+1.25*8=10 851 cal_byte = struct.pack('<Bf', cal, x) 852 z = self._send_nbytes_receive_4bytes(SERIAL_SCAL, cal_byte, typ='float') 853 854 if DEBUG: 855 y = "s_cal(" + str(x) + "," + str(cal) + ") -> " + str(z) 856 print(y) 857 return z 858 859 def q_cal(self, cal=CAL_C1P): # queries the frequency 860 """queries the calibration constants of the analogue input signals (conversion to calibrated voltage values) 861 862 :param cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,N,O}, for positive 863 voltage measurement, negative voltage measurement, output voltage measurement 864 :return: the calibration value 865 """ 866 867 z = self._send_receive_4bytes(SERIAL_QCAL, param1=cal, param2=0, typ='float') 868 869 if DEBUG: 870 y = "q_cal(" + str(cal) + ") -> " + str(z) 871 print(y) 872 self.f = z 873 return z 874 875 def s_calibration_method(self, which=VNOW_POS, calmeth=CALMETH_POLYNOMIAL): 876 if not (calmeth == CALMETH_POLYNOMIAL or calmeth == CALMETH_LOOKUP): 877 calmeth = CALMETH_POLYNOMIAL 878 if not (which == VNOW_POS or which == VNOW_OUT or which == VNOW_NEG): 879 which = VNOW_POS 880 z = self._send_receive_4bytes(SERIAL_SCALMETH, param1=which, param2=calmeth, typ='uint') 881 882 if DEBUG: 883 y = "s_calibration_method({0}, {1}) -> {2}".format(which, calmeth, z) 884 print(y) 885 if which == VNOW_POS: 886 self.calmeth_p = z 887 elif which == VNOW_OUT: 888 self.calmeth_o = z 889 else: 890 self.calmeth_n = z 891 892 def q_calibration_method(self, which=VNOW_POS): 893 if not (which == VNOW_POS or which == VNOW_OUT or which == VNOW_NEG): 894 which = VNOW_POS 895 z = self._send_receive_4bytes(SERIAL_QCALMETH, param1=which, param2=0, typ='uint') 896 897 if DEBUG: 898 y = "q_calibration_method({0}) -> {1}".format(which, z) 899 print(y) 900 if which == VNOW_POS: 901 self.calmeth_p = z 902 elif which == VNOW_NEG: 903 self.calmeth_n = z 904 else: 905 self.calmeth_o = z 906 907 def s_lookup(self, x, n, table=LOOKUP_ADC_P): 908 if n > 20: 909 n = 20 910 n = n << 2 # shift left 2 bits 911 if table == LOOKUP_ADC_P: 912 n = n | 0b01 913 elif table == LOOKUP_ADC_O: 914 n = n | 0b10 915 x = int(x) 916 z = self._send_receive_4bytes(SERIAL_SLKUP, param1=n, param2=x, typ='int16') 917 if DEBUG: 918 y = "s_lookup({0},{1},{2}) - > {3}".format(x, n, table, z) 919 print(y) 920 921 def q_lookup(self, which): # which=0: vout, which=1: ADC_p, which=2: ADC_o 922 format_string = f'<21h' 923 z = self._send_4bytes_receive_nbytes(SERIAL_QLKUP, param1=which, param2=0, fstring=format_string) 924 lookup_list = list(z) 925 if DEBUG: 926 y = "q_lookup({0}) - > {1}".format(which, lookup_list) 927 print(y) 928 return lookup_list 929 930 # ====User Waveform Functions==== 931 # def clear_waveform(self): # clear the stored waveform 932 # """Clear the current user-defined waveform 933 # 934 # Clear the current user-defined waveform from the HVPS memory.""" 935 # 936 # z = 0 937 # if DEBUG: 938 # y = "clear_waveform -> " + str(z) 939 # print(y) 940 # return z 941 # 942 # def q_waveform_num_pts(self): # queries the number of points saved for the waveform 943 # """Queries how many data point are currently stored in the waveform""" 944 # 945 # z = 0 946 # if DEBUG: 947 # y = "q_waveform_num_pts -> " + str(z) 948 # print(y) 949 # return z 950 # 951 # def s_waveform_point(self, x): # Add a point to the waveform 952 # """Add a point to the user waveform. 953 # 954 # Add a point to the user waveform. Usage: s_waveform_point(xxx), with xxx between 0 and 255 representing 955 # 0 to 100% of the voltage setpoint.\n 956 # A new waveform is defined by issuing clear_waveform() (to clear the previous waveform), followed by a series of 957 # s_waveform_point(xxx) to define the points of the new waveform. The maximal number of allowed points is 255. 958 # This is a low-level function provided to match the 'SP' command of the HVPS communication protocol. However, 959 # It is easier to use upload_waveform() to upload a complete waveform to the HVPS in one go, or to use 960 # upload_std_waveform() to upload some customisable standard waveforms.\n 961 # The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the 962 # save() command enables to save this parameter in memory\n 963 # input: x: set point to add to the current waveform. 0-255 representing 0-100% of voltage set point.\n 964 # output: accepted set point (or -1 if waveform full)""" 965 # pass 966 # 967 # def q_waveform_set_pts(self, x): # queries the xth point of the waveform (x starts at 0) 968 # """queries the waveform set point number x 969 # 970 # queries the waveform set point number x (0<=x<=255) returns a value between 0 and 255 representing 0 to 100% 971 # of current voltage set point. This is a low-level function provided to match the QP command of the HVPS 972 # communication protocol. It is easier to use download_waveform_set_pts() to download the whole waveform from 973 # the HVPS""" 974 # 975 # z = 0 976 # if DEBUG: 977 # y = "q_waveform_set_point(" + str(x) + ") -> " + str(z) 978 # print(y) 979 # return z 980 # 981 # def q_waveform_meas_pts(self, x): # queries the measured voltage of the xth point of the 982 # # waveform (x starts at 0) 983 # """queries the waveform measured point number x 984 # 985 # queries the waveform measured point number x. Same as q_waveform_set_pts(), but instead of returning the set 986 # point value, it returns the voltage value read by the SHVPS internally. In order for QR to return meaningful 987 # values, the SHVPS must have been in Waveform mode (SWMODE_WFRM) for at least one cycle.\n 988 # Queries the waveform point number x (0<=x<=255). returns a value between 0 and 255 representing 0 to 100% 989 # of current voltage set point. This is a low-level function provided to match the QR command of the HVPS 990 # communication protocol. It is easier to use download_waveform_meas_pts() to download the whole waveform from 991 # the HVPS""" 992 # 993 # z = 0 994 # if DEBUG: 995 # y = "q_waveform_meas_point(" + str(x) + ") -> " + str(z) 996 # print(y) 997 # return z 998 # 999 # def upload_waveform(self, x): # upload a list (x) of waveform set points to the HVPS 1000 # """upload a user-defined waveform to the HVPS 1001 # 1002 # upload a user-defined waveform to the HVPS. It starts by clearing the current waveform and then it upload 1003 # an new list of points. It also updates the member variable self.waveform_pts with the new list of points.\n 1004 # The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the 1005 # save() command enables to save this parameter in memory\n 1006 # input: x: [p1, p2, p3, ..., pn] where pn is the nth point of the waveform, between 0 and 255, representing 0 to 1007 # 100% of the current voltage set point. n is limited to 255.\n 1008 # output: none""" 1009 # pass 1010 # 1011 # def download_waveform_set_pts(self): # download the waveform set points stored in the HVPS 1012 # """Download the current waveform from the HVPS 1013 # 1014 # Download the current waveform from the HVPS. The output is a list of set points between 0 and 255 representing 1015 # 0 to 100% of the voltage set point. the downloaded points are stored in the member list waveform_pts""" 1016 # pass 1017 # 1018 # def download_waveform_meas_pts(self): # download the waveform set points stored in the HVPS 1019 # """Download the measured waveform (last performed cycle) from the HVPS 1020 # 1021 # Download the current waveform from the HVPS. The output is a list of set points between 0 and 255 representing 1022 # 0 to 100% of the voltage set point. the SHVPS must have been in Waveform mode (SWMODE_WFRM) for at least one 1023 # cycle to obtain meaningful values. member list waveform_meas""" 1024 # pass 1025 # 1026 # def upload_std_waveform(self, func=FUNC_SINE, sr=False, n=100, b=0.15): 1027 # """Upload a customisable standard waveform to the HVPS 1028 # 1029 # inputs:\n 1030 # func: FUNC_SINE (a sine wave with an offset to be between 0 and Voltage set point), FUNC_TRI 1031 # (a triangle function), FUNC_TRAP (a Trapezoid function), FUNC_CSTM (a custom waveform. Points (a maximum number 1032 # of 255 points) should be defined in a file named waveform.txt located alongside this library. There should be 1033 # 1 point per line, each point between 0 and 1, representing 0 to 100% of the voltage set point)\n 1034 # sr: (square root) True or False. In case the HVPS is used to drive dielectric elastomer actuators, there is 1035 # a quadratic relationship between voltage and actuation strain. True: a square root correction is applied so 1036 # that the actuation strain will roughly have the chosen profile. False: No correction applied/n 1037 # n: number of point in the waveform. Max is 255. It depends on the frequency at which the waveform will be 1038 # produced, For a 1Hz signal, 100 points are adequate. Reduce the number of points for higher frequencies 1039 # b: This applies only for the FUNC_TRAP function and defines the percentage of the period that the raising 1040 # (and falling) edge should take. The value should be smaller than 0.5 (at which point the waveform becomes a 1041 # triangle).\n 1042 # The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the 1043 # save() command enables to save this parameter in memory\n""" 1044 # # pts = [] 1045 # # n = constrain(n, 1, 255) # n must be between 1 and 255 points 1046 # # b = constrain(b, 0, 0.5) # b must be between 0 and 0.5 1047 # # if sr: # if we want the square root of the signal (because of the quadratic relationship between voltage 1048 # # # and strain for DEAs) 1049 # # power = 0.5 1050 # # else: 1051 # # power = 1.0 1052 # # 1053 # # if func == FUNC_CSTM: # custom user waveform 1054 # # try: 1055 # # fp = open('./waveform.txt', 'r') 1056 # # except FileNotFoundError: 1057 # # if DEBUG: 1058 # # print("Custom waveform must be in ./waveform.txt, but file is not found") 1059 # # fp = 0 1060 # # if fp: 1061 # # list_of_points = fp.readlines() 1062 # # for point in list_of_points: 1063 # # try: 1064 # # point = int(255 * (float(point) ** power)) 1065 # # except ValueError: 1066 # # point = 0 1067 # # if DEBUG: 1068 # # print("Error when reading point for custom waveform. Each line in the file ./waveform.txt " 1069 # # "must contain a single floating point number") 1070 # # point = constrain(point, 0, 255) # points should be between 0 and 255 1071 # # pts.append(point) 1072 # # fp.close() 1073 # # else: # if other standard functions are chosen 1074 # # for i in range(n): 1075 # # p = 0 # p is between 0 to 1 representing percentage of voltage set point 1076 # # if func == FUNC_SINE: # Sine + offset waveform 1077 # # p = (0.5 + 0.5 * sin(2 * pi / n * i)) ** power 1078 # # elif func == FUNC_TRAP: # trapeze waveform 1079 # # if i <= b * n: # the ramp up of the trapeze 1080 # # p = (i / b / n) ** power 1081 # # elif i <= n / 2: # holding time 1082 # # p = 1 1083 # # elif i <= (n / 2 + b * n): # ramp down 1084 # # p = (1 - (i - n / 2) / b / n) ** power 1085 # # else: 1086 # # p = 0 1087 # # elif func == FUNC_TRI: 1088 # # if i <= n / 2: # Raising edge 1089 # # p = (2 / n * i) ** power 1090 # # else: 1091 # # p = (1 - 2 / n * (i - n / 2)) ** power 1092 # # p = int(p * 255) 1093 # # pts.append(p) 1094 # # 1095 # # self.upload_waveform(pts) # uploads the waveform to the HVPS 1096 # pass 1097 1098 # Miscellaneous functions 1099 def save(self): # save current HVPS parameters into the memory 1100 """save active HVPS parameters into the memory 1101 1102 This command saves the active parameters as \'current\' settings. Current setting are the settings that are 1103 loaded when power is applied to the hvpsx 1104 1105 :return: SERIAL_OK or SERIAL_ERROR 1106 """ 1107 1108 z = self._send_receive_4bytes(SERIAL_SAVE) 1109 1110 if DEBUG: 1111 y = "save -> " + str(z) 1112 print(y) 1113 return z 1114 1115 def save_memory_to_file(self, settings=SETTINGS_CURRENT): 1116 """Dumps the content of the memory into a JSON file. 1117 1118 Dumps the content of the memory into a file. This is useful to keep a backup of the parameters on file. 1119 Files will be created in the interface folder and have the following format: Name_settings_type_date_time.json\n 1120 json files with settings can be transferred back to the hvps-x with the transfer_settings() method of the HVPS 1121 class, or the higher-level function transfer_file_to_memory() 1122 :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup 1123 :return: nothing 1124 """ 1125 1126 memory = self.q_mem(settings) 1127 now = datetime.now() # current date and time 1128 date_time = now.strftime("%Y_%m_%d_%H_%M_%S") 1129 if settings == SETTINGS_CURRENT: 1130 st_string = '_current_settings_' 1131 else: 1132 st_string = '_backup_settings_' 1133 file_name = self.name.decode("utf-8") + st_string + date_time + '.json' 1134 with open(file_name, "w") as write_file: 1135 json.dump(memory, write_file, indent=4) 1136 1137 def s_settings(self, 1138 settings=SETTINGS_CURRENT): # sets the active setting to a particular type (useful before saving) 1139 if not (settings == SETTINGS_CURRENT or settings == SETTINGS_FACTORY or settings == SETTINGS_BACKUP): 1140 settings = SETTINGS_CURRENT 1141 x = self._send_receive_4bytes(SERIAL_SST, param1=settings) 1142 if DEBUG: 1143 print('s_settings({0}) -> {1}'.format(settings, x)) 1144 return x 1145 1146 def load_settings(self, settings=SETTINGS_CURRENT): # Load a setting set from memory to the active setting 1147 """This function loads one of the two set of settings (current or backup) as active settings used by the hvps-x 1148 1149 :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup 1150 :return: 1151 """ 1152 1153 if not (settings == SETTINGS_CURRENT or settings == SETTINGS_BACKUP): 1154 settings = SETTINGS_CURRENT 1155 x = self._send_receive_4bytes(SERIAL_LST, param1=settings) 1156 if DEBUG: 1157 print('load_settings({0}) -> {1}'.format(settings, x)) 1158 1159 self._initialise_hvpsx() # need to reread all parameters 1160 1161 return x 1162 1163 def transfer_settings(self, dict_settings): 1164 """ transfer a setting dictionary from the computer to the hvps-x memory 1165 :param dict_settings: a dictionary containing the settings values. 1166 :return: SERIAL_OK or SERIAL_ERROR 1167 The dictionary of settings should be read from a file dumped using the function save_memory_to_file(). Together 1168 there two functions make it possible to backup the settings (this includes calibration and PID settings in a 1169 file, and gives the opportunity to restore the settings""" 1170 1171 error = False 1172 fw_string = 'Fw-{0:.1f}'.format(self.firmware) 1173 siz = struct.calcsize(memory_string[fw_string]) 1174 buffer = ctypes.create_string_buffer(siz) 1175 memmap = memorymap[fw_string] 1176 counter = 0 1177 for x in memmap: 1178 n = x[0] 1179 key = x[1] 1180 data_format = x[2] 1181 if data_format == 'B' or data_format == 's': 1182 bytelength = 1 1183 elif data_format == 'h' or data_format == 'H': 1184 bytelength = 2 1185 elif data_format == 'f': 1186 bytelength = 4 1187 else: 1188 bytelength = 1 1189 data = dict_settings[key] 1190 if key == 'Name': 1191 data = bytes(data, 'utf-8') 1192 if len(data) > 12: 1193 error = True 1194 if DEBUG: 1195 print("Error: name should not exceed 12 characters") 1196 else: 1197 format_str = '<{0}{1}'.format(n, data_format) 1198 struct.pack_into(format_str, buffer, counter, data) 1199 else: 1200 format_str = '<{0}'.format(data_format) 1201 if n == 1: 1202 struct.pack_into(format_str, buffer, counter, data) 1203 else: 1204 for i in range(n): 1205 try: 1206 struct.pack_into(format_str, buffer, counter + i * bytelength, data[i]) 1207 except IndexError: 1208 error = True 1209 if DEBUG: 1210 print('setting dictionary does not fit the expected format') 1211 1212 counter = counter + n * bytelength 1213 data_fw = 'Fw-{0}.{1}'.format(dict_settings['fw_major'], dict_settings['fw_minor']) 1214 if data_fw != fw_string: 1215 error = True 1216 if DEBUG: 1217 print('Error: JSON file firmware version does not match firware currently on hvps-x. Exiting') 1218 exit(0) 1219 if not error: 1220 buffer = b'xxx' + buffer # Adds 3 random bytes. CMD + 3 random bytes means that when mapping the transfer 1221 # buffer to a structure, it will on an aligned memory address 1222 x = self._send_nbytes_receive_4bytes(SERIAL_XFERST, buffer) 1223 else: 1224 x = SERIAL_ERROR 1225 1226 if DEBUG: 1227 print("transfer_settings(...) -> {0}".format(x)) 1228 return x 1229 1230 def copy_settings(self, src, dst): 1231 """ 1232 :param src: the settings to copy (CURRENT_SETTINGS or BACKUP_SETTINGS) 1233 :param dst: the destination settings (CURRENT_SETTINGS or BACKUP_SETTINGS) (destination will be overwritten by 1234 the source 1235 :return: nothing 1236 Copies one set of settings to another location:\n 1237 Copying BACKUP_SETTINGS to CURRENT_SETTINGS is useful to restore the backup settings as current settings 1238 (for example if some temporary settings were saved as current settings, for example to experiment with new PID 1239 gain values)\ 1240 Copying CURRENT_SETTINGS to BACKUP_SETTINGS is useful after a new calibration of the HVPS to save the new 1241 calibration as a set of back-up settings""" 1242 if (src == SETTINGS_CURRENT or src == SETTINGS_BACKUP) and (dst == SETTINGS_CURRENT or dst == SETTINGS_BACKUP): 1243 x = self._send_receive_4bytes(SERIAL_CPYST, param1=src, param2=dst) 1244 else: 1245 x = SERIAL_ERROR 1246 1247 if DEBUG: 1248 print('copy_settings({0},{1}) -> {2}'.format(src, dst, x)) 1249 1250 def q_mem(self, settings=SETTINGS_CURRENT): # queries the content of the memory 1251 """ 1252 :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup 1253 :return: A dictionary with the content of the memory. This is similar to the function save_memory_to_file() 1254 except that is doesn't save the content to a file""" 1255 fw_string = 'Fw-{0:.1f}'.format(self.firmware) 1256 memmap = memorymap[fw_string] 1257 dict_mem = {} 1258 1259 z = self._send_4bytes_receive_nbytes(SERIAL_QMEM, settings, param2=0, fstring=memory_string[fw_string]) 1260 bytecount = 0 1261 for i in range(len(memmap)): 1262 length = memmap[i][0] 1263 field = memmap[i][1] 1264 if field == 'Name': 1265 length = 1 1266 y = z[bytecount:bytecount + length] 1267 # if field != 'Padding': 1268 if length == 1: 1269 dict_mem[field] = y[0] 1270 else: 1271 dict_mem[field] = y 1272 bytecount = bytecount + length 1273 1274 dict_mem['Name'] = dict_mem['Name'].decode('UTF-8') 1275 dict_mem['Name'] = dict_mem['Name'].split("\0")[0] 1276 1277 if DEBUG: 1278 print(dict_mem) 1279 return dict_mem 1280 1281 def q_ver(self): # queries the firmware version 1282 """returns the current version of the firmware / hardware running on the board.""" 1283 z = self._send_receive_4bytes(SERIAL_QVER) 1284 Firm_minor = z & 0xFF 1285 Firm_major = (z >> 8) & 0xFF 1286 Hard_minor = (z >> 16) & 0xFF 1287 Hard_major = (z >> 24) 1288 self.firmware = Firm_major + Firm_minor / 10 1289 self.hardware = Hard_major + Hard_minor / 10 1290 if DEBUG: 1291 y = "q_ver -> {0} / {1}".format(self.firmware, self.hardware) 1292 print(y) 1293 return self.firmware, self.hardware 1294 1295 # Configuration functions 1296 def q_conf(self): # queries the configuration 1297 """returns the configuration of the board. 1298 :return: the configuration of the board in the high 2 bytes (CONF_UNCONF, CONF_UNIPOLAR, or CONF_BIPOLAR) 1299 and the PID (product ID) in the low 2 bytes. This is 0x0632 for a hvps-x unit. 1300 """ 1301 1302 z = self._send_receive_4bytes(SERIAL_QCONF) 1303 z_hvps = z & 0xFFFF 1304 z_conf = (z & 0xFFFF0000) >> 16 1305 if DEBUG: 1306 y = "q_conf -> " + hex(z_conf) + " / " + hex(z_hvps) 1307 print(y) 1308 self.conf = z_conf 1309 return z 1310 1311 def s_conf(self, bipolar): 1312 """Sets the configuration of the board 1313 :param bipolar: boolean. Is board bipolar (True), or unipolar (false) 1314 :return: bipolar: boolean 1315 """ 1316 if bipolar: 1317 param = CONF_BIPOLAR 1318 else: 1319 param = CONF_UNIPOLAR 1320 z = self._send_receive_4bytes(SERIAL_SCONF, param1=param) 1321 if DEBUG: 1322 y = "s_conf -> " + hex(z) 1323 print(y) 1324 if z == SERIAL_ERROR: 1325 print("Error: this configuration is not recognised") 1326 self.conf = z 1327 return z 1328 1329 def s_vmax(self, x): # sets the maximum voltage rating of the board 1330 """ sets the voltage rating of the hvps-x. Must match the EMCO DC/DC converter rating. 1331 :param x: Voltage rating of hvps-x in Volt 1332 :return: Voltage rating of hvps-x in Volt 1333 """ 1334 1335 x = constrain(x, 0, 6000) 1336 z = self._send_receive_4bytes(SERIAL_SVMAX, param2=x) 1337 if DEBUG: 1338 y = "s_vmax(" + str(x) + ") -> " + str(z) 1339 print(y) 1340 self.vmax = z 1341 return z 1342 1343 def q_vmax(self): # queries the voltage rating of the board 1344 """ Queries the maximal voltage of the board. The returned value is in volts. 1345 :return: board maximal voltage (V) 1346 """ 1347 z = self._send_receive_4bytes(SERIAL_QVMAX) 1348 if DEBUG: 1349 y = "q_vmax -> " + str(z) 1350 print(y) 1351 self.vmax = z 1352 return z 1353 1354 def s_name(self, x): # set the name of the HVPS 1355 """ Sets the name of the HVPS. 1356 :param x: Name of the hvps-x. 11 characters maximum 1357 :return: name accepted by hvps-x 1358 """ 1359 ll = len(x) 1360 if ll <= 12: 1361 x = bytearray(x, 'utf-8') 1362 for i in range(12 - ll): # pad the string with 0s 1363 x = x + b'\0' 1364 self._send_nbytes_receive_4bytes(SERIAL_SNAME, x, typ='uint') 1365 z = self.q_name() 1366 else: 1367 z = 'too long' 1368 if DEBUG: 1369 y = "s_name(" + str(x) + ") -> " + str(z) 1370 print(y) 1371 return z 1372 1373 def q_name(self): # queries the name of the board 1374 """queries the name of the board 1375 :return: Name of the board 1376 """ 1377 1378 # x = self._send_receive_4bytes(SERIAL_QNAME, param1=0) 1379 # x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=4) << 32) 1380 # x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=8) << 64) 1381 # x = x.to_bytes(12, 'little') 1382 1383 z = self._send_4bytes_receive_nbytes(SERIAL_QNAME, param1=0, param2=0, fstring='<12s') 1384 z = z[0].split(b'\x00', 1)[0] # take first (only) element of the tuple and remove the 0s at the end of string 1385 1386 self.name = z 1387 if DEBUG: 1388 y = "q_name -> " + str(z) 1389 print(y) 1390 return z 1391 1392 def set_hardware_version(self, hw_major, hw_minor): 1393 param = (hw_major << 8) + hw_minor 1394 self._send_receive_4bytes(SERIAL_SHW, param2=param) 1395 1396 def check_version_compatibility(self): 1397 self.q_ver() 1398 lib_string = 'Lib-' + LIB_VER 1399 fw_string = 'Fw-{0:.1f}'.format(self.firmware) 1400 hw_string = 'Hw-{0:.1f}'.format(self.hardware) 1401 1402 list_compatible_fw = compatibility_dict[lib_string] 1403 if fw_string in list_compatible_fw: 1404 list_compatible_hw = compatibility_dict[fw_string] 1405 if hw_string not in list_compatible_hw: 1406 self.err |= ERR_FIRM 1407 else: 1408 self.err |= ERR_FIRM 1409 1410 def diagnostic_ADC(self, which_vnow=VNOW_POS): 1411 x = self._send_4bytes_receive_nbytes(SERIAL_DADC, which_vnow, 0, '<40H') 1412 df = pd.DataFrame(x) 1413 df.to_excel('diagnostic_adc.xlsx') 1414 1415 def q_trace(self, which_vnow=VNOW_POS): 1416 x = self._send_4bytes_receive_nbytes(SERIAL_QTRACE, which_vnow, 0, '<100H') 1417 x = np.array(x) 1418 if DEBUG: 1419 y = "trace({0}) -> {1}".format(which_vnow, x) 1420 print(y) 1421 return x 1422 1423 def s_trace_rate(self, rate=5): 1424 """Sets the rate at which trace points are stored in memory 1425 :param rate: time in ms between point acquisition. This is limited by the time at which the hvps-x is 1426 calculating the voltages; currently evey 5ms. 1427 :return the rate accepted by the hvps-x""" 1428 1429 rate = int(rate) 1430 if rate < 2: 1431 rate = 2 1432 x = self._send_receive_4bytes(SERIAL_STRRATE, param1=0, param2=rate, typ='uint') 1433 x = x / 10 # the function returns the number of ticks of the counter, which counts at 10kHz. 1434 if DEBUG: 1435 y = "s_trace_rate({0}) -> {1}".format(rate, x) 1436 print(y) 1437 return x 1438 1439 def q_trace_rate(self): 1440 """queries the rate at which trace points are stored in memory 1441 :param none 1442 :return the rate at which trace points are stored in memory (in ms)""" 1443 1444 x = self._send_receive_4bytes(SERIAL_QTRRATE, param1=0, param2=0, typ='uint') 1445 x = x / 10 # the function returns the number of ticks of the counter, which counts at 10kHz. 1446 if DEBUG: 1447 y = "q_trace_rate() -> {0}".format(x) 1448 print(y) 1449 return x 1450 1451 def s_waveform_pwm(self, value): 1452 1453 value = constrain(value, -4095, 4095) 1454 value = int(value) 1455 x = self._send_receive_4bytes(SERIAL_WFMPWM, param1=0, param2=value, typ='uint') 1456 if DEBUG: 1457 y = "s_waveform_pwm({0}) -> {1}".format(value, x) 1458 print(y) 1459 return x 1460 1461 1462def list_hvps(list_all=False): 1463 """lists the hvps-x connected to PC in a dictionary.The key is the name of the HVPS, and the parameter is 1464 the associated serial port 1465 :param list_all: if True list all connected HVPS. 1466 if list_all is false, it will only list device that are configured. Using True enable to list unconfigured 1467 devices and give the opportunity to configure them 1468 :return: dictionary of connected hvps-x""" 1469 1470 hvpsx_ports = [ # creates a list with all STM32 VCP devices connected 1471 p.device 1472 for p in serial.tools.list_ports.comports() 1473 if (p.pid == 0x5740 and p.vid == 0x483) 1474 ] 1475 dict_hvps = {} 1476 for port in hvpsx_ports: 1477 dev = HVPS(port, init=False) # Attempts to connect to HVPS but without initialisation 1478 if dev.is_hvpsx: 1479 if dev.conf != CONF_UNCONF or list_all: 1480 dict_hvps[dev.name.decode()] = port # add an entry in the dictionary with the name of the 1481 # HVPS and the port 1482 dev.close(zero=False) 1483 1484 return dict_hvps 1485 1486 1487def connect_to_hvps(unconf=False): 1488 """Scan for connected hvps-x and connects to it if a single hvps-x is found. User can chose to which hvps-x to 1489 connect if more than one is detected. 1490 :param unconf: True/False. If true will connect to an unconfigured hvps-x. If False (default) only connects to a 1491 configured device 1492 :return: a HVPS object (or None if could not connect to a valid hvps-x 1493 If more than 1 hvps-x is connected to the computed, a list of names is displayed and the user must choose which one 1494 they want to connect to""" 1495 cont = False # flag to decide whether to continue the programme or close the COM port 1496 dev = None 1497 1498 ports = list_hvps(list_all=unconf) 1499 keys = list(ports.keys()) 1500 if len(ports) == 0: 1501 print("No HVPS connected to the device. Terminating programme") 1502 exit() 1503 elif len(ports) == 1: 1504 dev = HVPS(ports[keys[0]]) # connects to the only available HVPS 1505 else: 1506 print("List of connected HVPS:") 1507 for i in range(len(ports)): 1508 print(str(i) + ": " + keys[i]) 1509 print("Enter the number of the board you want to connect to") 1510 connect_to = 0 1511 try: 1512 connect_to = int(input()) 1513 except ValueError: 1514 print("Invalid entry. Terminating programme") 1515 exit() 1516 if connect_to >= len(ports): 1517 print("Invalid entry. Terminating programme") 1518 exit() 1519 else: 1520 dev = HVPS(ports[keys[connect_to]]) 1521 error = dev.err 1522 if error & ERR_PORT: 1523 print("Cannot open COM port. Probably busy with another process. Terminating Programme") 1524 if error & ERR_COM: 1525 print("Cannot communicate with hvps-x. Has firmware been flashed?. Terminating programme") 1526 if error & ERR_TYPE: 1527 print("Device connected this port is not recognised. Terminating programme") 1528 if error & ERR_CONF: 1529 print("This hvps-x is not configured. Please configure unit before using it. " 1530 "Terminating programme.") 1531 cont = True 1532 elif error & ERR_FIRM: 1533 print("Warning: your hvps-x library is not optimal for the firmware or the firmware is not optimal for your " 1534 "hardware. Refer to the website for a compatibility table") 1535 cont = True 1536 if error == 0: 1537 cont = True 1538 if cont: 1539 return dev 1540 else: 1541 return None 1542 1543 1544def configure(dev): 1545 """Script to use to for the initial configuration (or re-configuration) of an hvps-x. Follow prompt on the console. 1546 :param dev: an HVPS object 1547 :return: nothing 1548 """ 1549 if dev != -1: 1550 config = configparser.RawConfigParser() 1551 config.read('config.ini') 1552 1553 if dev.conf != CONF_UNCONF: 1554 answer = input("It appears this hvps-x is already configured. (C)ontinue with configuration or (A)bort?") 1555 if answer == 'a' or answer == 'A': 1556 print("exiting configuration") 1557 dev.close() 1558 exit(-1) 1559 answer = input("Is the hvps-x (U)nipolar or (B)ipolar? ") 1560 print("Setting configuration") 1561 if answer == 'B' or answer == 'b': 1562 dev.s_conf(bipolar=True) 1563 else: 1564 dev.s_conf(bipolar=False) 1565 answer = input("Enter the version of your hvps-x PCB (hardware version), as printed on the PCB " 1566 "(format: X.Y; e.g. 1.2): ") 1567 hw_list = answer.split('.') 1568 if len(hw_list) != 2: 1569 print("Error, the format of the hardware string must be X.Y, e.g. 1.2. Exiting") 1570 exit(0) 1571 dev.set_hardware_version(int(hw_list[0]), int(hw_list[1])) 1572 print("The current name of the hvps-x is " + dev.q_name().decode("utf-8")) 1573 answer = input("Do you want to set/change the name of this hvps-x? (Y)/(N)?") 1574 if answer == 'y' or answer == 'Y': 1575 name = input("Enter hvps-x name (12 char max): ") 1576 if len(name) > 12: 1577 print("Name too long. Ignoring name change") 1578 else: 1579 dev.s_name(name) 1580 else: 1581 print("Leaving name unchanged") 1582 print("\nThe current maximal voltage rating of this HVPS is " + str(dev.q_vmax()) + " V") 1583 answer = input("Do you want to set the maximal voltage of the HVPS? (Y)/(N)") 1584 if answer == 'y' or answer == 'Y': 1585 answer = input("Enter the maximal voltage of the hvps-x in Volt. It must match the voltage rating of the " 1586 "Emco DC/DC converter:") 1587 print("Setting Vmax to " + answer + "V") 1588 dev.s_vmax(int(answer)) 1589 else: 1590 print("Leaving Vmax unchanged") 1591 vmax = dev.q_vmax() 1592 conf_section = 'hvps-x-' + str(vmax) 1593 if config.has_section(conf_section): 1594 print("Default values for this voltage found in configuration file\n") 1595 answer = input("Do you want to replace the values stored in the hvps-x by the one from the config file " 1596 "(Y/N) (choose Y if configuring a new hvps-x)") 1597 if answer == 'Y' or answer == 'y': 1598 c0p = config.getfloat(conf_section, 'C0P') 1599 c1p = config.getfloat(conf_section, 'C1P') 1600 c2p = config.getfloat(conf_section, 'C2P') 1601 c0o = config.getfloat(conf_section, 'C0O') 1602 c1o = config.getfloat(conf_section, 'C1O') 1603 c2o = config.getfloat(conf_section, 'C2O') 1604 1605 dev.s_cal(c0p, CAL_C0P) 1606 dev.s_cal(c1p, CAL_C1P) 1607 dev.s_cal(c2p, CAL_C2P) 1608 dev.s_cal(c0o, CAL_C0O) 1609 dev.s_cal(c1o, CAL_C1O) 1610 dev.s_cal(c2o, CAL_C2O) 1611 print("Voltage calibration values set...\n") 1612 answer = input( 1613 "Reset PID values to their default values? (Y)/(N) (choose (Y) when configuring a board for the first " 1614 "time)") 1615 if answer == 'Y' or answer == 'y': 1616 kpp = config.getfloat(conf_section, 'KPP') 1617 kip = config.getfloat(conf_section, 'KIP') 1618 kdp = config.getfloat(conf_section, 'KDP') 1619 1620 dev.s_pid(kpp, PID_KPP) 1621 dev.s_pid(kip, PID_KIP) 1622 dev.s_pid(kdp, PID_KDP) 1623 print("PID values set...\n") 1624 1625 print("hvps-x configured. It is recommended to perform a calibration of the voltage readout circuit!") 1626 print("Saving information to hvps-x memory. Backup-settings and default settings") 1627 dev.s_settings(SETTINGS_BACKUP) 1628 dev.save() 1629 dev.s_settings(SETTINGS_CURRENT) 1630 dev.save() 1631 else: 1632 print("Cannot find a section in the config file for a {0} V hvps-x. You need to enter values for voltage " 1633 "calibration and PID manually ".format(vmax)) 1634 dev.save() # saves previous parameters anyway 1635 1636 1637def check_functionality(dev): 1638 """This script can be used after configuration to test the hvps-x functionality. Follow prompt on the console 1639 1640 :param dev: an HVPS object 1641 :return: nothing 1642 """ 1643 1644 print("Important:\n" 1645 "This function is experimental and the threshold values are based on a limited number of assembled boards." 1646 "In particular:\n" 1647 "- If the test fails with a value very close to the threshold value, then your board is most likely fine.\n" 1648 "- This version of the script must be used with the bill of material revision 1. If you assembled your" 1649 "board with the original bill of material, this script will not return the correct output. You can use the" 1650 "script included with the library v1.0. Otherwise the website describe how the high-voltage functionality " 1651 "can be manually tested.\n" 1652 "- If unsure, contact Peta-pico-Voltron with test results (be sure to specify hardware version of PCB).\n\n") 1653 if not dev.q_kill(): 1654 input("Place HV safety switch (S1) on position 0 and press any key.") 1655 if not dev.q_kill(): 1656 input("Switch S1 appears not to be working. Check Switch S1 functionality and restart the test") 1657 dev.close() 1658 exit() 1659 print("During the course of this test, a moderate (~20% of full scale) voltage will be applied to the output of\n " 1660 "the hvps-x. Make sure that it nothing is connected to the output and that you are not touching the\n " 1661 "instrument\n") 1662 print("Voltage set point 0V. Output off") 1663 dev.s_vset(0, DCDC_BOTH) 1664 dev.s_sw_mode(SWMODE_OFF) 1665 dev.s_v_mode(VMODE_O) 1666 dev.s_sw_src(SWSRC_TMR) 1667 1668 print("---Testing HV enable switch (S1) ---") 1669 input("Place HV enable switch (S1) on position 1 and press any key") 1670 if dev.q_kill(): 1671 print("S1 switch still reads off state. Check functionality of switch S1.\n Test FAILED. Exiting script...") 1672 dev.close() 1673 exit() 1674 else: 1675 print("***PASS\n") 1676 1677 print("---Testing Voltage monitoring at set point 0 ---") 1678 vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS) 1679 print("Raw reading at the output of the converter is {0} ".format(vnow_p)) 1680 if vnow_p < 5: 1681 print("This is within the tolerance range V < 5. Continuing...") 1682 1683 else: 1684 print("This is outside of the tolerance range V < 5. Something appears to be wrong with the positive DCDC " 1685 "voltage monitoring circuit.\n Test FAILED. Exiting script...") 1686 dev.close() 1687 exit() 1688 1689 print("Raw reading at the output of optocouplers {0} ".format(vnow_o)) 1690 if vnow_o < 5: 1691 print("This is within the tolerance range V < 5. Continuing...") 1692 else: 1693 print("This is outside of the tolerance range V < 5. Something appears to be wrong with the output " 1694 "voltage monitoring circuit.\n Test FAILED. Exiting script...") 1695 dev.close() 1696 exit() 1697 print("***PASS\n") 1698 1699 print("---Testing Voltage monitoring at set point 20% FS Output off ---") 1700 print("Applying 20% PWM value") 1701 dev.s_vset(int(0.2 * dev.vmax), DCDC_POS) 1702 sleep(0.2) 1703 vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS) 1704 print("Raw reading at the output of the converter is {0} ".format(vnow_p)) 1705 if vnow_p > 900: 1706 print("This is within the tolerance range V > 900. Continuing...") 1707 else: 1708 print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC " 1709 "voltage monitoring circuit.\n Test FAILED. Exiting script...") 1710 dev.close() 1711 exit() 1712 1713 print("Raw reading at the output of optocouplers {0} ".format(vnow_o)) 1714 if vnow_o < 5: 1715 print("This is within the tolerance range V < 5. Continuing...") 1716 else: 1717 print("This is outside of the tolerance range V < 5. Something appears to be wrong with the output " 1718 "voltage monitoring circuit.\n Test FAILED. Exiting script...") 1719 dev.close() 1720 exit() 1721 print("***PASS\n") 1722 1723 print("---Testing Voltage monitoring at set point 20% FS Output LOW ---") 1724 print("Output LOW") 1725 dev.s_sw_mode(SWMODE_LOW) 1726 sleep(0.2) 1727 vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS) 1728 print("Raw reading at the output of the converter is {0} ".format(vnow_p)) 1729 if vnow_p > 900: 1730 print("This is within the tolerance range V > 900. Continuing...") 1731 else: 1732 print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC " 1733 "voltage monitoring circuit.\n Test Failed. Exiting script...") 1734 dev.close() 1735 exit() 1736 1737 print("Raw reading at the output of optocouplers {0} ".format(vnow_o)) 1738 if vnow_o < 5: 1739 print("This is within the tolerance range V < 5. Continuing...") 1740 else: 1741 print("This is outside of the tolerance range V < 5. Something appears to be wrong with the output " 1742 "voltage monitoring circuit.\n Test FAILED. Exiting script...") 1743 dev.close() 1744 exit() 1745 print("***PASS\n") 1746 1747 print("---Testing Voltage monitoring at set point 20% FS Output HIGH ---") 1748 print("Output HIGH") 1749 dev.s_sw_mode(SWMODE_HIGH) 1750 sleep(0.2) 1751 vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS) 1752 print("Raw reading at the output of the converter is {0} ".format(vnow_p)) 1753 if vnow_p > 900: 1754 print("This is within the tolerance range V > 900. Continuing...") 1755 else: 1756 print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC " 1757 "voltage monitoring circuit.\n Test FAILED. Exiting script...") 1758 dev.close() 1759 exit() 1760 1761 print("Raw reading at the output of optocouplers {0} ".format(vnow_o)) 1762 if vnow_o > 900: 1763 print("This is within the tolerance range V > 900. Continuing...") 1764 else: 1765 print("This is outside of the tolerance range V > 900. Something appears to be wrong with the output " 1766 "voltage monitoring circuit.\n Test FAILED. Exiting script...") 1767 dev.close() 1768 exit() 1769 print("***PASS\n") 1770 print("***hvps-x passes all tests***") 1771 1772 dev.s_vset(0, DCDC_BOTH) 1773 dev.close() 1774 1775 1776def transfer_file_to_memory(dev: HVPS): 1777 """Displays a list of json file in the interface folder and gives the user the option to transfer one to the hvps-x 1778 memory 1779 :param dev: an HVPS object 1780 :return: nothing 1781 """ 1782 json_files = list_json_files() 1783 if len(json_files) > 0 and dev != -1: 1784 selected_file = select_file(json_files) 1785 with open(selected_file) as file: 1786 data_dict = json.load(file) 1787 dev.transfer_settings(data_dict) 1788 else: 1789 print("No .json files in the interface root folder") 1790 1791 1792def list_json_files(): 1793 json_files = [] 1794 for file in os.listdir('.'): 1795 if file.endswith('.json'): 1796 json_files.append(file) 1797 return json_files 1798 1799 1800def select_file(files): 1801 print("Available Files:") 1802 for i, file in enumerate(files): 1803 print(f"{i + 1}. {file}") 1804 while True: 1805 try: 1806 choice = int(input("Enter the number corresponding to the file you want to open: ")) 1807 if 1 <= choice <= len(files): 1808 return files[choice - 1] 1809 else: 1810 print("Invalid choice. Please enter a valid number.") 1811 except ValueError: 1812 print("Invalid choice. Please enter a valid number.") 1813 1814 1815def main(): 1816 dev = connect_to_hvps(unconf=True) 1817 if dev: 1818 print("Name: {0}".format(dev.q_name())) 1819 print("Vmax: {0} V".format(dev.vmax)) 1820 print("\n Your choices:") 1821 print("[1] Initial configuration (to be performed after assembling the low-voltage components)") 1822 print("[2] Basic functionality test (to be performed after assembling the high-voltage components)") 1823 print("[3] Transfer a json file to memory") 1824 print("[q] quit\n") 1825 answer = input("Your choice: ") 1826 if answer == '1': 1827 configure(dev) 1828 elif answer == '2': 1829 check_functionality(dev) 1830 elif answer == '3': 1831 transfer_file_to_memory(dev) 1832 dev.close(zero=False) 1833 1834 1835def constrain(val, min_val, max_val): 1836 """A simple implementation to constrain a value between two boundaries""" 1837 return min(max_val, max(min_val, val)) 1838 1839 1840if __name__ == "__main__": # if the library is executed as the main programme 1841 main()
225class HVPS: 226 """ Class to control a petapicovoltron hvps-x 227 228 The class implements the low-level functions to interface the hvps-x firmware 229 for easy communication with the hvps-x. In addition, a few higher-level functions are provided.""" 230 231 # ====Communication functions and class constructor==== 232 def __init__(self, port, init=True): 233 """ Initialises a HVPS object: dev = HVPS('/path/to/serial/port'). 234 235 Input: COM port to which the HVPS is connected.""" 236 self.name = '' 237 self.vmax = 0 238 self.swmode = 0 239 self.vsetp = 0 240 self.vsetn = 0 # negative voltage setpoint (for bipolar configuration) 241 self.vnowp = 0 242 self.vnown = 0 243 self.vnowo = 0 244 self.f = 0 245 self.duty = 0.5 246 self.cycles = 0 247 self.cycle_n = 0 # the current cycle number 248 self.swsrc = 0 249 self.vmode = 0 250 self.bttncfg = 0 # latching behaviour of the button 251 self.err = 0 252 self.stmode = 0 # Strobe mode 253 self.stpos = 0 # Strobe position 254 self.stdur = 0 # strobe duration 255 self.stsweep = 5000 # Strobe sweep time (default when HVPS starts) 256 self.ser = serial.Serial() # the serial connection to the HVPS 257 self.waveform_pts = [] # list to store the set points of the custom waveform 258 self.waveform_meas = [] # list to store the measured points of the custom waveform. 259 self.lookup_adc_p = [] # lookup tables for linear interpolation 260 self.lookup_adc_o = [] 261 self.lookup_v_out = [] 262 self.cp = [] # calibration coefficients. 263 self.co = [] 264 self.calmeth_p = -1 265 self.calmeth_n = -1 266 self.calmeth_o = -1 267 268 self.firmware = 0 269 self.hardware = 0 270 self.conf = 0 # configuration of the hvps-x 271 self.is_hvpsx = 0 272 273 try: 274 self.ser = serial.Serial(port, 115200, timeout=3) 275 except serial.SerialException: 276 self.err = self.err | ERR_PORT 277 if DEBUG: 278 print("Cannot connect to serial port. Probably busy.") 279 else: 280 self.ser.reset_input_buffer() 281 if DEBUG: 282 text = "connecting to " + port 283 print(text) 284 print("Serial port open") 285 z = self._send_receive_4bytes(SERIAL_QCONF) 286 if z == 0: # no response: the device connected doesn't talk using this protocol 287 self.err = self.err | ERR_COM 288 if DEBUG: 289 print("Communication error: the connected device doesn't reply to commands") 290 z_hvps = z & 0xFFFF 291 z_conf = (z & 0xFFFF0000) >> 16 292 if z_hvps == HVPSX_PID: 293 self.is_hvpsx = 1 294 if z_conf != CONF_UNCONF: 295 self.conf = z_conf 296 else: 297 self.err = self.err | ERR_CONF # the hvps-x is not configured 298 if DEBUG: 299 print("The hvps is not configured") 300 z = self._send_4bytes_receive_nbytes(SERIAL_QNAME, param1=0, param2=0, fstring='<12s') 301 z = z[0].split(b'\x00', 1)[0] 302 self.name = z 303 else: 304 self.err = self.err | ERR_TYPE # The connected device replied with Err! but is not a hvps-x 305 if DEBUG: 306 print("Type error: the device replies to commands, but is not a hvps-x.") 307 if init and not self.err: 308 self._initialise_hvpsx() 309 310 def close(self, zero=True): # closes connection with the HVPS 311 """Closes the connection to the HVPS. Sets voltage to 0 if board was connected 312 313 Input (optional): zero. If True, will set the voltage to 0 and the optocouplers to off before closing the 314 communication. If False, the hvps-x is left in its current state""" 315 316 if self.ser.is_open: 317 if not (self.err & ERR_COM): # if connected board is not running the firmware, do not send command 318 if zero: 319 self.s_vset(0, DCDC_BOTH) # set the voltage to 0 as a safety measure 320 self.s_sw_mode(SWMODE_OFF) # for hvps-x, this will disable both optocouplers. 321 self.ser.close() 322 323 if DEBUG: 324 print("Serial port closed") 325 326 def _read_4bytes_uint(self): 327 ans = self.ser.read(4) 328 z = int.from_bytes(ans, byteorder='little') 329 return z 330 331 def _read_4bytes_int16(self): # Read 4 bytes with a 16-bit signed int in bytes 0 and 1 (and ignore bytes 2,3) 332 ans1 = self.ser.read(2) 333 self.ser.read(2) # discard the last 2 bytes 334 z = int.from_bytes(ans1, byteorder='little', signed=True) 335 return z 336 337 def _read_4bytes_float(self): 338 ans = self.ser.read(4) 339 z = struct.unpack('<f', ans) # little endian: string starts with the low index of the array, which 340 # represents the low bits of the float 341 return z[0] 342 343 def _send_receive_4bytes(self, cmd, param1=0, param2=0, typ='uint'): 344 param2 = round(param2) 345 if param2 < 0: 346 param2 = param2 & 0xFFFF # represent is as two's complement 347 cmd = cmd + (param1 << 8) + (param2 << 16) 348 cmd_b = cmd.to_bytes(4, 'little') 349 self.ser.write(cmd_b) 350 if typ == 'uint': 351 z = self._read_4bytes_uint() 352 elif typ == 'int16': 353 z = self._read_4bytes_int16() 354 else: 355 z = self._read_4bytes_float() 356 return z 357 358 def _send_4bytes_receive_nbytes(self, cmd, param1=0, param2=0, fstring='<2B'): 359 n = struct.calcsize(fstring) 360 param2 = round(param2) 361 if param2 < 0: 362 param2 = param2 & 0xFFFF # represent it as two's complement 363 cmd = cmd + (param1 << 8) + (param2 << 16) 364 cmd_b = cmd.to_bytes(4, 'little') 365 self.ser.write(cmd_b) 366 waiting = self.ser.inWaiting() 367 x = self.ser.read(n) 368 z = struct.unpack(fstring, x) 369 return z # returns unpacked tuples 370 371 def _send_nbytes_receive_4bytes(self, cmd, packet, typ='uint'): 372 packet = bytes([cmd]) + packet 373 self.ser.write(packet) 374 if typ == 'uint': 375 z = self._read_4bytes_uint() 376 elif typ == 'int16': 377 z = self._read_4bytes_int16() 378 else: 379 z = self._read_4bytes_float() 380 return z 381 382 def _initialise_hvpsx(self): 383 self.check_version_compatibility() 384 self.q_vmax() 385 self.q_vset(DCDC_POS) 386 self.q_vset(DCDC_NEG) 387 self.q_duty() 388 self.q_f() 389 self.q_bttn_cfg() 390 self.q_sw_mode() 391 self.q_sw_src() 392 self.q_v_mode() 393 394 def convert_raw_to_voltage(self, vraw_main, vraw_out, which): 395 if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT: 396 which = VNOW_POS 397 # Download calibration values the first time the function is used 398 399 if self.calmeth_p == -1: # calibration method not yet fetched from HVPS 400 self.q_calibration_method(which=VNOW_POS) 401 if self.calmeth_n == -1: # calibration method not yet fetched from HVPS 402 self.q_calibration_method(which=VNOW_NEG) 403 if self.calmeth_o == -1: # calibration method not yet fetched from HVPS 404 self.q_calibration_method(which=VNOW_OUT) 405 406 if (self.calmeth_o == CALMETH_LOOKUP or self.calmeth_p == CALMETH_LOOKUP) and not self.lookup_v_out: 407 self.lookup_v_out = self.q_lookup(0) 408 if self.calmeth_p == CALMETH_LOOKUP and not self.lookup_adc_p: 409 self.lookup_adc_p = self.q_lookup(1) 410 if self.calmeth_o == CALMETH_LOOKUP and not self.lookup_adc_o: 411 self.lookup_adc_o = self.q_lookup(2) 412 413 if self.calmeth_p == CALMETH_POLYNOMIAL and not self.cp: # if we don't know what are the calibration values 414 self.cp.append(self.q_cal(CAL_C0P)) 415 self.cp.append(self.q_cal(CAL_C1P)) 416 self.cp.append(self.q_cal(CAL_C2P)) 417 418 if self.calmeth_o == CALMETH_POLYNOMIAL and not self.co: # if we don't know what are the calibration values 419 self.co.append(self.q_cal(CAL_C0O)) 420 self.co.append(self.q_cal(CAL_C1O)) 421 self.co.append(self.q_cal(CAL_C2O)) 422 423 if self.calmeth_o == CALMETH_LOOKUP: 424 v_out = np.interp(vraw_out, self.lookup_adc_o, self.lookup_v_out, left=None, right=None, period=None) 425 else: 426 v_out = self.co[0] + self.co[1] * vraw_out + self.co[2] * np.square(vraw_out) 427 428 if which == VNOW_POS: 429 if self.calmeth_p == CALMETH_LOOKUP: 430 v_main = np.interp(vraw_main, self.lookup_adc_p, self.lookup_v_out, left=None, right=None, period=None) 431 else: 432 v_main = self.cp[0] + self.cp[1] * vraw_main + self.cp[2] * np.square(vraw_main) 433 elif which == VNOW_NEG: 434 v_main = 0 435 else: 436 v_main = v_out 437 return v_main, v_out 438 439 def is_bipolar(self): # return true if connected device is hvpsx and bipolar unit 440 """ Returns true if the hvps-x configuration is bipolar.""" 441 if self.conf & 1: 442 return True 443 else: 444 return False 445 446 # ====Commands related to voltage and frequency==== 447 def s_vset(self, x, polarity=DCDC_POS): # sets the output voltage 448 """Sets the output voltage of the HVPS. The new parameter remains valid until a new call to this command, or 449 when the HVPS is powered off. Using the 450 save() command enables to save this parameter in memory\n 451 Although this command can be used at any time, it is mainly useful when the HVPS voltage control mode is 452 regulated (i.e. closed loop) (VMODE_R). In this case the hvps-x will adjust the control voltage to keep the 453 output to the set point; see s_vmode() command. 454 :param x: voltage set point in volt (int) 455 :param polarity: which DC/DC converter to address (DCDC_POS, DCDC_NEG (bipolar config), DCDC_BOTH) 456 :return: voltage set point accepted by hvps-x 457 """ 458 x = constrain(abs(x), 0, self.vmax) 459 x = int(x) 460 if polarity != DCDC_POS and polarity != DCDC_NEG and polarity != DCDC_BOTH: 461 polarity = DCDC_POS 462 z = self._send_receive_4bytes(SERIAL_SVSET, param1=polarity, param2=x) 463 if x == 0: 464 z = 0 # avoids calibration issue near 0 that would lead to a negative number which is saturated to 0 in 465 # the STM32 (as the stored raw value is uint), leading to a possible large offset error returned value of a 466 # few 10s of volts when back calculated in volts 467 if polarity == DCDC_POS: 468 self.vsetp = z 469 elif polarity == DCDC_NEG: 470 self.vsetn = z 471 else: # DCDC_Both: i.e. set the positive and negative voltage to the same level 472 self.vsetp = z 473 self.vsetn = z 474 if DEBUG: 475 y = "s_vset(" + str(x) + ") -> " + str(z) 476 print(y) 477 return z 478 479 def q_vset(self, polarity=DCDC_POS): # queries the voltage setpoint 480 """Queries the voltage set point. The returned value is in volts. 481 :param polarity: DCDC converter to be queried (DCDC_POS or DCDC_NEG). DCDC_NEG will return 0 for 482 unipolar configs 483 :return: Voltage set point in volts 484 """ 485 if polarity != DCDC_POS and polarity != DCDC_NEG: 486 polarity = DCDC_POS 487 z = self._send_receive_4bytes(SERIAL_QVSET, param1=polarity) 488 if DEBUG: 489 y = "q_vset -> " + str(z) 490 print(y) 491 if polarity == DCDC_NEG: 492 self.vsetn = z 493 else: 494 self.vsetp = z 495 return z 496 497 def q_vnow(self, which=VNOW_POS): # queries the voltage output 498 """Queries the feedback voltage of the HVPS. 499 :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the 500 positive DCDC converter, negative DCDC converter, or output of the HVPS 501 :return: voltage value in volts 502 """ 503 504 if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT: 505 which = VNOW_POS 506 z = self._send_4bytes_receive_nbytes(SERIAL_QVNOW, param1=which, fstring='<2h') 507 z_out = z[1] 508 z_main = z[0] 509 if DEBUG: 510 y = "q_vnow -> " + str(z_main) + " / " + str(z_out) 511 print(y) 512 self.vnowo = z_out 513 if which == VNOW_OUT: 514 self.vnowo = z_main 515 elif which == VNOW_NEG: 516 self.vnown = z_main 517 else: 518 self.vnowp = z_main 519 return z_main 520 521 def q_vnow_raw(self, which=VNOW_POS): # queries a raw value of the voltage output 522 """Queries the current feedback voltage of the HVPS. The returned value is a raw 12bit ADC value. This avoids 523 running slow floating point calculations on the 524 microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to 525 execute than q_vnow(). Useful for streaming voltage values 526 :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the 527 positive DCDC converter, negative DCDC converter, or output of the HVPS. 528 :return: a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT 529 (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is 530 a 12 bit value that can be converted to voltage using the unit's calibration values (see q_vnow_fast() for a 531 way to do this automatically""" 532 533 if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT: 534 which = VNOW_POS 535 z = self._send_receive_4bytes(SERIAL_QVNOWRAW, param1=which, typ='uint') 536 z_out = z & 0xFFFF 537 z_main = (z & 0xFFFF0000) >> 16 538 if DEBUG: 539 y = "q_vnow_raw -> " + str(z_main) + " , " + str(z_out) 540 print(y) 541 return z_main, z_out 542 543 def q_vnow_fast(self, which=VNOW_POS): # queries the voltage output 544 """Queries the current feedback voltage of the HVPS in a raw format and convert it to a calibrated voltage 545 This avoids running slow floating point calculations on the 546 microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to 547 execute than q_vnow(). Useful for streaming voltage values. This method is similar to q_vnow(), except that 548 the conversion from a raw value to a calibrated value is done 549 on the computer rather than on the microcontroller. It can take up to 300us to convert a value on the MUC 550 (it depends on the method used (linear, quadratic, lookup table) 551 :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the 552 positive DCDC converter, negative DCDC converter, or output of the HVPS. 553 :return: a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT 554 (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is 555 a 12 bit value that can be converted to voltage using the unit's calibration values The returned values 556 are calibrated voltages in Volt""" 557 558 z_main, z_out = self.q_vnow_raw(which=which) 559 v_main, v_out = self.convert_raw_to_voltage(z_main, z_out, which=which) 560 561 return v_main, v_out 562 563 def s_f(self, x): # sets the frequency 564 """Sets the frequency of the signal when the HVPS is in switching mode (SWMODE_SW).\n 565 The value returned is the new frequency, taking quantification into account. 566 :param x: frequency in Hz between 0.001 and 1000 567 :return: frequency accepted by hvps-x in Hz 568 """ 569 x = constrain(x, 0.001, 1000.0) 570 571 # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction 572 # Ex: 10.0: 0x41 0x20 0x00 0x00. 573 # 0 10000010 01000000000000000000000 574 # S=0 Positive 575 # Exp=130-127 (offset)=2^3=8 576 # Fraction=1+2^-2=1.25 577 # number=+1.25*8=10 578 f_byte = struct.pack('<f', x) 579 z = self._send_nbytes_receive_4bytes(SERIAL_SF, f_byte, typ='float') 580 581 if DEBUG: 582 y = "s_f(" + str(x) + ") -> " + str(z) 583 print(y) 584 self.f = z 585 return z 586 587 def q_f(self): # queries the frequency 588 """Queries the switching frequency. The returned value is in Hz.""" 589 z = self._send_receive_4bytes(SERIAL_QF, param1=0, param2=0, typ='float') 590 591 if DEBUG: 592 y = "q_f -> " + str(z) 593 print(y) 594 self.f = z 595 return z 596 597 def s_duty(self, x): # sets the duty cycle of the switching signal 598 """Sets the duty cycle of the switching signal 599 :param x: the duty cycle (float in the range 0-1) 600 :return: the current duty cycle (float between 0 and 1) 601 """ 602 duty = int(x * 1000) # hvps-x is coding duty cycle on a 0-1000 scale 603 604 z = self._send_receive_4bytes(SERIAL_SDUTY, param1=0, param2=duty, typ='uint') 605 z = z / 1000 606 if DEBUG: 607 y = "s_duty(" + str(x) + ") -> " + str(z) 608 print(y) 609 self.duty = z 610 return z 611 612 def q_duty(self): # queries the duty cycle of the switching signal 613 """queries the duty cycle of the switching signal 614 :return: the current duty cycle (float between 0 and 1) 615 """ 616 617 z = self._send_receive_4bytes(SERIAL_QDUTY, param1=0, param2=0, typ='uint') 618 z = float(z) / 1000.0 619 if DEBUG: 620 y = "q_duty() -> " + str(z) 621 print(y) 622 self.duty = z 623 return z 624 625 # ===Commands to change the voltage control and switching behaviour (mode and source)==== 626 def s_sw_mode(self, x): # sets the switching mode 627 """Sets the switching mode of the hvps-x. \n 628 SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off. 629 This effectively 630 connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV DC/DC converter. 631 SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. For unipolar hvps-x, this connects J4 to ground and 632 is equivalent to the DC mode of the shvps. For bipolar hvps-x, this connects J4 to the negative high voltage 633 produced by the negative board. The other modes behaves in a similar manner compared to the shvps. SWMODE_SW: 634 the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency. 635 The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the 636 save() command enables to save this parameter in memory 637 :param x: SWMODE_OFF, SWMODE_HIGH, SWMODE_LOW, SWMODE_SW 638 :return: Switching mode set by the HVPS 639 """ 640 641 if x > SWMODE_LOW: 642 x = SWMODE_OFF 643 644 z = self._send_receive_4bytes(SERIAL_SSWMODE, param1=x, param2=0, typ='uint') 645 if DEBUG: 646 y = "s_sw_mode(" + str(x) + ") -> " + str(z) 647 print(y) 648 self.swmode = z 649 return z 650 651 def q_sw_mode(self): # queries the switching mode 652 """queries the switching mode of the hvps-x. \n 653 SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off. 654 This effectively 655 connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV DC/DC converter. 656 SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. For unipolar hvps-x, this connects J4 to ground and 657 is equivalent to the DC mode of the shvps. For bipolar hvps-x, this connects J4 to the negative high voltage 658 produced by the negative board. The other modes behaves in a similar manner compared to the shvps. SWMODE_SW: 659 the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency. 660 :return: Switching mode set by the HVPS 661 """ 662 z = self._send_receive_4bytes(SERIAL_QSWMODE, param1=0, param2=0, typ='uint') 663 if DEBUG: 664 y = "q_sw_mode -> " + str(z) 665 print(y) 666 self.swmode = z 667 return z 668 669 def s_sw_src(self, x): # sets the switching source 670 """Sets the source of the switching signal. 671 672 Sets the source of the switching signal. Accepted values are: SWSRC_TMR for onboard 673 switching (from internal clock of the board), or SWSRC_BTTN for the push button.\n 674 Using the save() command enables to save this parameter in memory 675 :param x: SWSRC_TMR, or SWSRC_BTTN 676 :return: SWSRC_TMR, or SWSRC_BTTN 677 """ 678 if x > SWSRC_BTTN: 679 x = SWSRC_TMR 680 z = self._send_receive_4bytes(SERIAL_SSWSRC, param1=x, param2=0, typ='uint') 681 if DEBUG: 682 y = "s_sw_src(" + str(x) + ") -> " + str(z) 683 print(y) 684 self.swsrc = z 685 return z 686 687 def q_sw_src(self): # queries the switching source 688 """queries the source of the switching signal. 689 690 queries the source of the switching signal. Output values are: SWSRC_TMR for onboard 691 switching (from internal clock of the board), or SWSRC_BTTN for the push button.\n 692 Using the save() command enables to save this parameter in memory 693 :return: SWSRC_TMR, or SWSRC_BTTN 694 """ 695 696 z = self._send_receive_4bytes(SERIAL_QSWSRC, param1=0, param2=0, typ='uint') 697 698 if DEBUG: 699 y = "q_sw_src -> " + str(z) 700 print(y) 701 self.swsrc = z 702 return z 703 704 def s_bttn_cfg(self, x): # sets the configuration of the push button 705 """Defines the behaviour of the push button 706 707 The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the 708 save() command enables to save this parameter in memory 709 :param x: 2-bit value (0x0 to 0x3). bit 0: Defines the behaviour of the push button, when the switching source 710 of the HVPS is set to the push button 711 (SWSRC_BTTN, c.f. s_sw_src() command above). Accepted values are 0 and 1: 0 for a push button behaviour 712 (i.e. the high voltage is turned on as long as the button is pressed), 713 and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time 714 to turn it off).\n 715 bit 1: State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW 716 :return: the 2-bit button config value 717 """ 718 if x > 3: 719 x = 3 720 z = self._send_receive_4bytes(SERIAL_SBTTNCFG, param1=x, param2=0, typ='uint') 721 if DEBUG: 722 y = "s_bttn_cfg(" + str(x) + ") -> " + str(z) 723 print(y) 724 self.bttncfg = z 725 return z 726 727 def q_bttn_cfg(self): # queries the latch mode of the push button 728 """Queries the behaviour of the push button 729 730 :return: the 2-bit button config value. bit 0: Defines the behaviour of the push button, when the switching 731 source of the HVPS is set to the push button (SWSRC_BTTN, c.f. s_sw_src() command above). 732 Values are 0 for a push button behaviour (i.e. the high voltage is turned on as long as the button is pressed), 733 and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time 734 to turn it off).\n 735 bit 1: State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW 736 """ 737 z = self._send_receive_4bytes(SERIAL_QBTTNCFG, param1=0, param2=0, typ='uint') 738 if DEBUG: 739 y = "q_bttn_cfg -> " + str(z) 740 print(y) 741 self.bttncfg = z 742 return z 743 744 def q_kill(self): # queries the state of the kill button. Kill=1 means HV is disabled 745 """Queries whether HV is disabled (Kill=1) or enabled (kill=0). When kill = 1 there will not be a HV present at 746 the hvps-x output, irrespective of any software setting. 747 :return: 1 if the Switch S1 on the board is set to 0 (HV output is killed), 0 otherwise. 748 """ 749 z = self._send_receive_4bytes(SERIAL_QKILL, param1=0, param2=0, typ='uint') 750 751 if DEBUG: 752 y = "q_kill -> " + str(z) 753 print(y) 754 return z 755 756 def s_v_mode(self, x): # sets the voltage control mode 757 """Sets the voltage control mode 758 759 Sets the voltage control mode (i.e. how is the value of the output voltage controlled):\n 760 VMODE_R for internal voltage regulator (regulates the voltage to the value defined with the Vset command).\n 761 VMODE_O (that's an O like in open) internal open loop control (on-board regulator disconnected).\n 762 The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the 763 save() command enables to save this parameter in memory\n 764 VMODE_O has an internal safeguard that will decrease the setting if the sensing circuit saturates. However, 765 the voltage can still be slightly higher than the DCDC converter upper limit. User must check that the output 766 voltage remains within the allowed range 767 :param x: VMODE_R, VMODE_O 768 :return: VMODE_R, VMODE_O 769 """ 770 771 if x > VMODE_O: 772 x = VMODE_R 773 774 z = self._send_receive_4bytes(SERIAL_SVMODE, param1=x, param2=0, typ='uint') 775 if DEBUG: 776 y = "s_v_mode(" + str(x) + ") -> " + str(z) 777 print(y) 778 self.vmode = z 779 return z 780 781 def q_v_mode(self): # queries the switching source 782 """Queries the voltage control mode 783 784 :return: VMODE_R internal voltage regulator, VMODE_O internal 785 open loop control (on-board regulator disconnected). 786 """ 787 788 z = self._send_receive_4bytes(SERIAL_QVMODE, param1=0, param2=0, typ='uint') 789 790 if DEBUG: 791 y = "q_v_mode -> " + str(z) 792 print(y) 793 self.vmode = z 794 return z 795 796 # ====Functions to set configuration parameters==== 797 def s_pid(self, x, pid=PID_KPP): 798 """Sets the gains of the PIDs 799 Use save() command to save the new values to memory 800 :param x: the value (float) of the gain. 801 :param pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,N,O}, 802 for positive voltage PID, negative voltage PID, output voltage PID. (in Lib V1, Y can only take the value P) 803 :return: the gain value 804 """ 805 # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction 806 # Ex: 10.0: 0x41 0x20 0x00 0x00. 807 # 0 10000010 01000000000000000000000 808 # S=0 Positive 809 # Exp=130-127 (offset)=2^3=8 810 # Fraction=1+2^-2=1.25 811 # number=+1.25*8=10 812 pid_byte = struct.pack('<Bf', pid, x) 813 z = self._send_nbytes_receive_4bytes(SERIAL_SPID, pid_byte, typ='float') 814 815 if DEBUG: 816 y = "s_pid(" + str(x) + "," + str(pid) + ") -> " + str(z) 817 print(y) 818 return z 819 820 def q_pid(self, pid=PID_KPP): # queries the frequency 821 """returns the gains of the PIDs 822 823 :param pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,N,O}, 824 for positive voltage PID, negative voltage PID, output voltage PID. (in Lib V1, Y can only take the value P) 825 :return: the gain value of the chosen parameter 826 """ 827 828 z = self._send_receive_4bytes(SERIAL_QPID, param1=pid, param2=0, typ='float') 829 830 if DEBUG: 831 y = "q_pid(" + str(pid) + ") -> " + str(z) 832 print(y) 833 834 return z 835 836 def s_cal(self, x, cal=CAL_C1P): 837 """Sets the calibration constants of the analogue input signals (conversion to calibrated voltage values) 838 Use save() to commit the setting to memory 839 :param x: the value of the calibration coefficient (float) 840 :param cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,N,O}, for positive 841 voltage measurement, negative voltage measurement, output voltage measurement 842 :return: the calibration value 843 """ 844 845 # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction 846 # Ex: 10.0: 0x41 0x20 0x00 0x00. 847 # 0 10000010 01000000000000000000000 848 # S=0 Positive 849 # Exp=130-127 (offset)=2^3=8 850 # Fraction=1+2^-2=1.25 851 # number=+1.25*8=10 852 cal_byte = struct.pack('<Bf', cal, x) 853 z = self._send_nbytes_receive_4bytes(SERIAL_SCAL, cal_byte, typ='float') 854 855 if DEBUG: 856 y = "s_cal(" + str(x) + "," + str(cal) + ") -> " + str(z) 857 print(y) 858 return z 859 860 def q_cal(self, cal=CAL_C1P): # queries the frequency 861 """queries the calibration constants of the analogue input signals (conversion to calibrated voltage values) 862 863 :param cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,N,O}, for positive 864 voltage measurement, negative voltage measurement, output voltage measurement 865 :return: the calibration value 866 """ 867 868 z = self._send_receive_4bytes(SERIAL_QCAL, param1=cal, param2=0, typ='float') 869 870 if DEBUG: 871 y = "q_cal(" + str(cal) + ") -> " + str(z) 872 print(y) 873 self.f = z 874 return z 875 876 def s_calibration_method(self, which=VNOW_POS, calmeth=CALMETH_POLYNOMIAL): 877 if not (calmeth == CALMETH_POLYNOMIAL or calmeth == CALMETH_LOOKUP): 878 calmeth = CALMETH_POLYNOMIAL 879 if not (which == VNOW_POS or which == VNOW_OUT or which == VNOW_NEG): 880 which = VNOW_POS 881 z = self._send_receive_4bytes(SERIAL_SCALMETH, param1=which, param2=calmeth, typ='uint') 882 883 if DEBUG: 884 y = "s_calibration_method({0}, {1}) -> {2}".format(which, calmeth, z) 885 print(y) 886 if which == VNOW_POS: 887 self.calmeth_p = z 888 elif which == VNOW_OUT: 889 self.calmeth_o = z 890 else: 891 self.calmeth_n = z 892 893 def q_calibration_method(self, which=VNOW_POS): 894 if not (which == VNOW_POS or which == VNOW_OUT or which == VNOW_NEG): 895 which = VNOW_POS 896 z = self._send_receive_4bytes(SERIAL_QCALMETH, param1=which, param2=0, typ='uint') 897 898 if DEBUG: 899 y = "q_calibration_method({0}) -> {1}".format(which, z) 900 print(y) 901 if which == VNOW_POS: 902 self.calmeth_p = z 903 elif which == VNOW_NEG: 904 self.calmeth_n = z 905 else: 906 self.calmeth_o = z 907 908 def s_lookup(self, x, n, table=LOOKUP_ADC_P): 909 if n > 20: 910 n = 20 911 n = n << 2 # shift left 2 bits 912 if table == LOOKUP_ADC_P: 913 n = n | 0b01 914 elif table == LOOKUP_ADC_O: 915 n = n | 0b10 916 x = int(x) 917 z = self._send_receive_4bytes(SERIAL_SLKUP, param1=n, param2=x, typ='int16') 918 if DEBUG: 919 y = "s_lookup({0},{1},{2}) - > {3}".format(x, n, table, z) 920 print(y) 921 922 def q_lookup(self, which): # which=0: vout, which=1: ADC_p, which=2: ADC_o 923 format_string = f'<21h' 924 z = self._send_4bytes_receive_nbytes(SERIAL_QLKUP, param1=which, param2=0, fstring=format_string) 925 lookup_list = list(z) 926 if DEBUG: 927 y = "q_lookup({0}) - > {1}".format(which, lookup_list) 928 print(y) 929 return lookup_list 930 931 # ====User Waveform Functions==== 932 # def clear_waveform(self): # clear the stored waveform 933 # """Clear the current user-defined waveform 934 # 935 # Clear the current user-defined waveform from the HVPS memory.""" 936 # 937 # z = 0 938 # if DEBUG: 939 # y = "clear_waveform -> " + str(z) 940 # print(y) 941 # return z 942 # 943 # def q_waveform_num_pts(self): # queries the number of points saved for the waveform 944 # """Queries how many data point are currently stored in the waveform""" 945 # 946 # z = 0 947 # if DEBUG: 948 # y = "q_waveform_num_pts -> " + str(z) 949 # print(y) 950 # return z 951 # 952 # def s_waveform_point(self, x): # Add a point to the waveform 953 # """Add a point to the user waveform. 954 # 955 # Add a point to the user waveform. Usage: s_waveform_point(xxx), with xxx between 0 and 255 representing 956 # 0 to 100% of the voltage setpoint.\n 957 # A new waveform is defined by issuing clear_waveform() (to clear the previous waveform), followed by a series of 958 # s_waveform_point(xxx) to define the points of the new waveform. The maximal number of allowed points is 255. 959 # This is a low-level function provided to match the 'SP' command of the HVPS communication protocol. However, 960 # It is easier to use upload_waveform() to upload a complete waveform to the HVPS in one go, or to use 961 # upload_std_waveform() to upload some customisable standard waveforms.\n 962 # The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the 963 # save() command enables to save this parameter in memory\n 964 # input: x: set point to add to the current waveform. 0-255 representing 0-100% of voltage set point.\n 965 # output: accepted set point (or -1 if waveform full)""" 966 # pass 967 # 968 # def q_waveform_set_pts(self, x): # queries the xth point of the waveform (x starts at 0) 969 # """queries the waveform set point number x 970 # 971 # queries the waveform set point number x (0<=x<=255) returns a value between 0 and 255 representing 0 to 100% 972 # of current voltage set point. This is a low-level function provided to match the QP command of the HVPS 973 # communication protocol. It is easier to use download_waveform_set_pts() to download the whole waveform from 974 # the HVPS""" 975 # 976 # z = 0 977 # if DEBUG: 978 # y = "q_waveform_set_point(" + str(x) + ") -> " + str(z) 979 # print(y) 980 # return z 981 # 982 # def q_waveform_meas_pts(self, x): # queries the measured voltage of the xth point of the 983 # # waveform (x starts at 0) 984 # """queries the waveform measured point number x 985 # 986 # queries the waveform measured point number x. Same as q_waveform_set_pts(), but instead of returning the set 987 # point value, it returns the voltage value read by the SHVPS internally. In order for QR to return meaningful 988 # values, the SHVPS must have been in Waveform mode (SWMODE_WFRM) for at least one cycle.\n 989 # Queries the waveform point number x (0<=x<=255). returns a value between 0 and 255 representing 0 to 100% 990 # of current voltage set point. This is a low-level function provided to match the QR command of the HVPS 991 # communication protocol. It is easier to use download_waveform_meas_pts() to download the whole waveform from 992 # the HVPS""" 993 # 994 # z = 0 995 # if DEBUG: 996 # y = "q_waveform_meas_point(" + str(x) + ") -> " + str(z) 997 # print(y) 998 # return z 999 # 1000 # def upload_waveform(self, x): # upload a list (x) of waveform set points to the HVPS 1001 # """upload a user-defined waveform to the HVPS 1002 # 1003 # upload a user-defined waveform to the HVPS. It starts by clearing the current waveform and then it upload 1004 # an new list of points. It also updates the member variable self.waveform_pts with the new list of points.\n 1005 # The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the 1006 # save() command enables to save this parameter in memory\n 1007 # input: x: [p1, p2, p3, ..., pn] where pn is the nth point of the waveform, between 0 and 255, representing 0 to 1008 # 100% of the current voltage set point. n is limited to 255.\n 1009 # output: none""" 1010 # pass 1011 # 1012 # def download_waveform_set_pts(self): # download the waveform set points stored in the HVPS 1013 # """Download the current waveform from the HVPS 1014 # 1015 # Download the current waveform from the HVPS. The output is a list of set points between 0 and 255 representing 1016 # 0 to 100% of the voltage set point. the downloaded points are stored in the member list waveform_pts""" 1017 # pass 1018 # 1019 # def download_waveform_meas_pts(self): # download the waveform set points stored in the HVPS 1020 # """Download the measured waveform (last performed cycle) from the HVPS 1021 # 1022 # Download the current waveform from the HVPS. The output is a list of set points between 0 and 255 representing 1023 # 0 to 100% of the voltage set point. the SHVPS must have been in Waveform mode (SWMODE_WFRM) for at least one 1024 # cycle to obtain meaningful values. member list waveform_meas""" 1025 # pass 1026 # 1027 # def upload_std_waveform(self, func=FUNC_SINE, sr=False, n=100, b=0.15): 1028 # """Upload a customisable standard waveform to the HVPS 1029 # 1030 # inputs:\n 1031 # func: FUNC_SINE (a sine wave with an offset to be between 0 and Voltage set point), FUNC_TRI 1032 # (a triangle function), FUNC_TRAP (a Trapezoid function), FUNC_CSTM (a custom waveform. Points (a maximum number 1033 # of 255 points) should be defined in a file named waveform.txt located alongside this library. There should be 1034 # 1 point per line, each point between 0 and 1, representing 0 to 100% of the voltage set point)\n 1035 # sr: (square root) True or False. In case the HVPS is used to drive dielectric elastomer actuators, there is 1036 # a quadratic relationship between voltage and actuation strain. True: a square root correction is applied so 1037 # that the actuation strain will roughly have the chosen profile. False: No correction applied/n 1038 # n: number of point in the waveform. Max is 255. It depends on the frequency at which the waveform will be 1039 # produced, For a 1Hz signal, 100 points are adequate. Reduce the number of points for higher frequencies 1040 # b: This applies only for the FUNC_TRAP function and defines the percentage of the period that the raising 1041 # (and falling) edge should take. The value should be smaller than 0.5 (at which point the waveform becomes a 1042 # triangle).\n 1043 # The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the 1044 # save() command enables to save this parameter in memory\n""" 1045 # # pts = [] 1046 # # n = constrain(n, 1, 255) # n must be between 1 and 255 points 1047 # # b = constrain(b, 0, 0.5) # b must be between 0 and 0.5 1048 # # if sr: # if we want the square root of the signal (because of the quadratic relationship between voltage 1049 # # # and strain for DEAs) 1050 # # power = 0.5 1051 # # else: 1052 # # power = 1.0 1053 # # 1054 # # if func == FUNC_CSTM: # custom user waveform 1055 # # try: 1056 # # fp = open('./waveform.txt', 'r') 1057 # # except FileNotFoundError: 1058 # # if DEBUG: 1059 # # print("Custom waveform must be in ./waveform.txt, but file is not found") 1060 # # fp = 0 1061 # # if fp: 1062 # # list_of_points = fp.readlines() 1063 # # for point in list_of_points: 1064 # # try: 1065 # # point = int(255 * (float(point) ** power)) 1066 # # except ValueError: 1067 # # point = 0 1068 # # if DEBUG: 1069 # # print("Error when reading point for custom waveform. Each line in the file ./waveform.txt " 1070 # # "must contain a single floating point number") 1071 # # point = constrain(point, 0, 255) # points should be between 0 and 255 1072 # # pts.append(point) 1073 # # fp.close() 1074 # # else: # if other standard functions are chosen 1075 # # for i in range(n): 1076 # # p = 0 # p is between 0 to 1 representing percentage of voltage set point 1077 # # if func == FUNC_SINE: # Sine + offset waveform 1078 # # p = (0.5 + 0.5 * sin(2 * pi / n * i)) ** power 1079 # # elif func == FUNC_TRAP: # trapeze waveform 1080 # # if i <= b * n: # the ramp up of the trapeze 1081 # # p = (i / b / n) ** power 1082 # # elif i <= n / 2: # holding time 1083 # # p = 1 1084 # # elif i <= (n / 2 + b * n): # ramp down 1085 # # p = (1 - (i - n / 2) / b / n) ** power 1086 # # else: 1087 # # p = 0 1088 # # elif func == FUNC_TRI: 1089 # # if i <= n / 2: # Raising edge 1090 # # p = (2 / n * i) ** power 1091 # # else: 1092 # # p = (1 - 2 / n * (i - n / 2)) ** power 1093 # # p = int(p * 255) 1094 # # pts.append(p) 1095 # # 1096 # # self.upload_waveform(pts) # uploads the waveform to the HVPS 1097 # pass 1098 1099 # Miscellaneous functions 1100 def save(self): # save current HVPS parameters into the memory 1101 """save active HVPS parameters into the memory 1102 1103 This command saves the active parameters as \'current\' settings. Current setting are the settings that are 1104 loaded when power is applied to the hvpsx 1105 1106 :return: SERIAL_OK or SERIAL_ERROR 1107 """ 1108 1109 z = self._send_receive_4bytes(SERIAL_SAVE) 1110 1111 if DEBUG: 1112 y = "save -> " + str(z) 1113 print(y) 1114 return z 1115 1116 def save_memory_to_file(self, settings=SETTINGS_CURRENT): 1117 """Dumps the content of the memory into a JSON file. 1118 1119 Dumps the content of the memory into a file. This is useful to keep a backup of the parameters on file. 1120 Files will be created in the interface folder and have the following format: Name_settings_type_date_time.json\n 1121 json files with settings can be transferred back to the hvps-x with the transfer_settings() method of the HVPS 1122 class, or the higher-level function transfer_file_to_memory() 1123 :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup 1124 :return: nothing 1125 """ 1126 1127 memory = self.q_mem(settings) 1128 now = datetime.now() # current date and time 1129 date_time = now.strftime("%Y_%m_%d_%H_%M_%S") 1130 if settings == SETTINGS_CURRENT: 1131 st_string = '_current_settings_' 1132 else: 1133 st_string = '_backup_settings_' 1134 file_name = self.name.decode("utf-8") + st_string + date_time + '.json' 1135 with open(file_name, "w") as write_file: 1136 json.dump(memory, write_file, indent=4) 1137 1138 def s_settings(self, 1139 settings=SETTINGS_CURRENT): # sets the active setting to a particular type (useful before saving) 1140 if not (settings == SETTINGS_CURRENT or settings == SETTINGS_FACTORY or settings == SETTINGS_BACKUP): 1141 settings = SETTINGS_CURRENT 1142 x = self._send_receive_4bytes(SERIAL_SST, param1=settings) 1143 if DEBUG: 1144 print('s_settings({0}) -> {1}'.format(settings, x)) 1145 return x 1146 1147 def load_settings(self, settings=SETTINGS_CURRENT): # Load a setting set from memory to the active setting 1148 """This function loads one of the two set of settings (current or backup) as active settings used by the hvps-x 1149 1150 :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup 1151 :return: 1152 """ 1153 1154 if not (settings == SETTINGS_CURRENT or settings == SETTINGS_BACKUP): 1155 settings = SETTINGS_CURRENT 1156 x = self._send_receive_4bytes(SERIAL_LST, param1=settings) 1157 if DEBUG: 1158 print('load_settings({0}) -> {1}'.format(settings, x)) 1159 1160 self._initialise_hvpsx() # need to reread all parameters 1161 1162 return x 1163 1164 def transfer_settings(self, dict_settings): 1165 """ transfer a setting dictionary from the computer to the hvps-x memory 1166 :param dict_settings: a dictionary containing the settings values. 1167 :return: SERIAL_OK or SERIAL_ERROR 1168 The dictionary of settings should be read from a file dumped using the function save_memory_to_file(). Together 1169 there two functions make it possible to backup the settings (this includes calibration and PID settings in a 1170 file, and gives the opportunity to restore the settings""" 1171 1172 error = False 1173 fw_string = 'Fw-{0:.1f}'.format(self.firmware) 1174 siz = struct.calcsize(memory_string[fw_string]) 1175 buffer = ctypes.create_string_buffer(siz) 1176 memmap = memorymap[fw_string] 1177 counter = 0 1178 for x in memmap: 1179 n = x[0] 1180 key = x[1] 1181 data_format = x[2] 1182 if data_format == 'B' or data_format == 's': 1183 bytelength = 1 1184 elif data_format == 'h' or data_format == 'H': 1185 bytelength = 2 1186 elif data_format == 'f': 1187 bytelength = 4 1188 else: 1189 bytelength = 1 1190 data = dict_settings[key] 1191 if key == 'Name': 1192 data = bytes(data, 'utf-8') 1193 if len(data) > 12: 1194 error = True 1195 if DEBUG: 1196 print("Error: name should not exceed 12 characters") 1197 else: 1198 format_str = '<{0}{1}'.format(n, data_format) 1199 struct.pack_into(format_str, buffer, counter, data) 1200 else: 1201 format_str = '<{0}'.format(data_format) 1202 if n == 1: 1203 struct.pack_into(format_str, buffer, counter, data) 1204 else: 1205 for i in range(n): 1206 try: 1207 struct.pack_into(format_str, buffer, counter + i * bytelength, data[i]) 1208 except IndexError: 1209 error = True 1210 if DEBUG: 1211 print('setting dictionary does not fit the expected format') 1212 1213 counter = counter + n * bytelength 1214 data_fw = 'Fw-{0}.{1}'.format(dict_settings['fw_major'], dict_settings['fw_minor']) 1215 if data_fw != fw_string: 1216 error = True 1217 if DEBUG: 1218 print('Error: JSON file firmware version does not match firware currently on hvps-x. Exiting') 1219 exit(0) 1220 if not error: 1221 buffer = b'xxx' + buffer # Adds 3 random bytes. CMD + 3 random bytes means that when mapping the transfer 1222 # buffer to a structure, it will on an aligned memory address 1223 x = self._send_nbytes_receive_4bytes(SERIAL_XFERST, buffer) 1224 else: 1225 x = SERIAL_ERROR 1226 1227 if DEBUG: 1228 print("transfer_settings(...) -> {0}".format(x)) 1229 return x 1230 1231 def copy_settings(self, src, dst): 1232 """ 1233 :param src: the settings to copy (CURRENT_SETTINGS or BACKUP_SETTINGS) 1234 :param dst: the destination settings (CURRENT_SETTINGS or BACKUP_SETTINGS) (destination will be overwritten by 1235 the source 1236 :return: nothing 1237 Copies one set of settings to another location:\n 1238 Copying BACKUP_SETTINGS to CURRENT_SETTINGS is useful to restore the backup settings as current settings 1239 (for example if some temporary settings were saved as current settings, for example to experiment with new PID 1240 gain values)\ 1241 Copying CURRENT_SETTINGS to BACKUP_SETTINGS is useful after a new calibration of the HVPS to save the new 1242 calibration as a set of back-up settings""" 1243 if (src == SETTINGS_CURRENT or src == SETTINGS_BACKUP) and (dst == SETTINGS_CURRENT or dst == SETTINGS_BACKUP): 1244 x = self._send_receive_4bytes(SERIAL_CPYST, param1=src, param2=dst) 1245 else: 1246 x = SERIAL_ERROR 1247 1248 if DEBUG: 1249 print('copy_settings({0},{1}) -> {2}'.format(src, dst, x)) 1250 1251 def q_mem(self, settings=SETTINGS_CURRENT): # queries the content of the memory 1252 """ 1253 :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup 1254 :return: A dictionary with the content of the memory. This is similar to the function save_memory_to_file() 1255 except that is doesn't save the content to a file""" 1256 fw_string = 'Fw-{0:.1f}'.format(self.firmware) 1257 memmap = memorymap[fw_string] 1258 dict_mem = {} 1259 1260 z = self._send_4bytes_receive_nbytes(SERIAL_QMEM, settings, param2=0, fstring=memory_string[fw_string]) 1261 bytecount = 0 1262 for i in range(len(memmap)): 1263 length = memmap[i][0] 1264 field = memmap[i][1] 1265 if field == 'Name': 1266 length = 1 1267 y = z[bytecount:bytecount + length] 1268 # if field != 'Padding': 1269 if length == 1: 1270 dict_mem[field] = y[0] 1271 else: 1272 dict_mem[field] = y 1273 bytecount = bytecount + length 1274 1275 dict_mem['Name'] = dict_mem['Name'].decode('UTF-8') 1276 dict_mem['Name'] = dict_mem['Name'].split("\0")[0] 1277 1278 if DEBUG: 1279 print(dict_mem) 1280 return dict_mem 1281 1282 def q_ver(self): # queries the firmware version 1283 """returns the current version of the firmware / hardware running on the board.""" 1284 z = self._send_receive_4bytes(SERIAL_QVER) 1285 Firm_minor = z & 0xFF 1286 Firm_major = (z >> 8) & 0xFF 1287 Hard_minor = (z >> 16) & 0xFF 1288 Hard_major = (z >> 24) 1289 self.firmware = Firm_major + Firm_minor / 10 1290 self.hardware = Hard_major + Hard_minor / 10 1291 if DEBUG: 1292 y = "q_ver -> {0} / {1}".format(self.firmware, self.hardware) 1293 print(y) 1294 return self.firmware, self.hardware 1295 1296 # Configuration functions 1297 def q_conf(self): # queries the configuration 1298 """returns the configuration of the board. 1299 :return: the configuration of the board in the high 2 bytes (CONF_UNCONF, CONF_UNIPOLAR, or CONF_BIPOLAR) 1300 and the PID (product ID) in the low 2 bytes. This is 0x0632 for a hvps-x unit. 1301 """ 1302 1303 z = self._send_receive_4bytes(SERIAL_QCONF) 1304 z_hvps = z & 0xFFFF 1305 z_conf = (z & 0xFFFF0000) >> 16 1306 if DEBUG: 1307 y = "q_conf -> " + hex(z_conf) + " / " + hex(z_hvps) 1308 print(y) 1309 self.conf = z_conf 1310 return z 1311 1312 def s_conf(self, bipolar): 1313 """Sets the configuration of the board 1314 :param bipolar: boolean. Is board bipolar (True), or unipolar (false) 1315 :return: bipolar: boolean 1316 """ 1317 if bipolar: 1318 param = CONF_BIPOLAR 1319 else: 1320 param = CONF_UNIPOLAR 1321 z = self._send_receive_4bytes(SERIAL_SCONF, param1=param) 1322 if DEBUG: 1323 y = "s_conf -> " + hex(z) 1324 print(y) 1325 if z == SERIAL_ERROR: 1326 print("Error: this configuration is not recognised") 1327 self.conf = z 1328 return z 1329 1330 def s_vmax(self, x): # sets the maximum voltage rating of the board 1331 """ sets the voltage rating of the hvps-x. Must match the EMCO DC/DC converter rating. 1332 :param x: Voltage rating of hvps-x in Volt 1333 :return: Voltage rating of hvps-x in Volt 1334 """ 1335 1336 x = constrain(x, 0, 6000) 1337 z = self._send_receive_4bytes(SERIAL_SVMAX, param2=x) 1338 if DEBUG: 1339 y = "s_vmax(" + str(x) + ") -> " + str(z) 1340 print(y) 1341 self.vmax = z 1342 return z 1343 1344 def q_vmax(self): # queries the voltage rating of the board 1345 """ Queries the maximal voltage of the board. The returned value is in volts. 1346 :return: board maximal voltage (V) 1347 """ 1348 z = self._send_receive_4bytes(SERIAL_QVMAX) 1349 if DEBUG: 1350 y = "q_vmax -> " + str(z) 1351 print(y) 1352 self.vmax = z 1353 return z 1354 1355 def s_name(self, x): # set the name of the HVPS 1356 """ Sets the name of the HVPS. 1357 :param x: Name of the hvps-x. 11 characters maximum 1358 :return: name accepted by hvps-x 1359 """ 1360 ll = len(x) 1361 if ll <= 12: 1362 x = bytearray(x, 'utf-8') 1363 for i in range(12 - ll): # pad the string with 0s 1364 x = x + b'\0' 1365 self._send_nbytes_receive_4bytes(SERIAL_SNAME, x, typ='uint') 1366 z = self.q_name() 1367 else: 1368 z = 'too long' 1369 if DEBUG: 1370 y = "s_name(" + str(x) + ") -> " + str(z) 1371 print(y) 1372 return z 1373 1374 def q_name(self): # queries the name of the board 1375 """queries the name of the board 1376 :return: Name of the board 1377 """ 1378 1379 # x = self._send_receive_4bytes(SERIAL_QNAME, param1=0) 1380 # x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=4) << 32) 1381 # x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=8) << 64) 1382 # x = x.to_bytes(12, 'little') 1383 1384 z = self._send_4bytes_receive_nbytes(SERIAL_QNAME, param1=0, param2=0, fstring='<12s') 1385 z = z[0].split(b'\x00', 1)[0] # take first (only) element of the tuple and remove the 0s at the end of string 1386 1387 self.name = z 1388 if DEBUG: 1389 y = "q_name -> " + str(z) 1390 print(y) 1391 return z 1392 1393 def set_hardware_version(self, hw_major, hw_minor): 1394 param = (hw_major << 8) + hw_minor 1395 self._send_receive_4bytes(SERIAL_SHW, param2=param) 1396 1397 def check_version_compatibility(self): 1398 self.q_ver() 1399 lib_string = 'Lib-' + LIB_VER 1400 fw_string = 'Fw-{0:.1f}'.format(self.firmware) 1401 hw_string = 'Hw-{0:.1f}'.format(self.hardware) 1402 1403 list_compatible_fw = compatibility_dict[lib_string] 1404 if fw_string in list_compatible_fw: 1405 list_compatible_hw = compatibility_dict[fw_string] 1406 if hw_string not in list_compatible_hw: 1407 self.err |= ERR_FIRM 1408 else: 1409 self.err |= ERR_FIRM 1410 1411 def diagnostic_ADC(self, which_vnow=VNOW_POS): 1412 x = self._send_4bytes_receive_nbytes(SERIAL_DADC, which_vnow, 0, '<40H') 1413 df = pd.DataFrame(x) 1414 df.to_excel('diagnostic_adc.xlsx') 1415 1416 def q_trace(self, which_vnow=VNOW_POS): 1417 x = self._send_4bytes_receive_nbytes(SERIAL_QTRACE, which_vnow, 0, '<100H') 1418 x = np.array(x) 1419 if DEBUG: 1420 y = "trace({0}) -> {1}".format(which_vnow, x) 1421 print(y) 1422 return x 1423 1424 def s_trace_rate(self, rate=5): 1425 """Sets the rate at which trace points are stored in memory 1426 :param rate: time in ms between point acquisition. This is limited by the time at which the hvps-x is 1427 calculating the voltages; currently evey 5ms. 1428 :return the rate accepted by the hvps-x""" 1429 1430 rate = int(rate) 1431 if rate < 2: 1432 rate = 2 1433 x = self._send_receive_4bytes(SERIAL_STRRATE, param1=0, param2=rate, typ='uint') 1434 x = x / 10 # the function returns the number of ticks of the counter, which counts at 10kHz. 1435 if DEBUG: 1436 y = "s_trace_rate({0}) -> {1}".format(rate, x) 1437 print(y) 1438 return x 1439 1440 def q_trace_rate(self): 1441 """queries the rate at which trace points are stored in memory 1442 :param none 1443 :return the rate at which trace points are stored in memory (in ms)""" 1444 1445 x = self._send_receive_4bytes(SERIAL_QTRRATE, param1=0, param2=0, typ='uint') 1446 x = x / 10 # the function returns the number of ticks of the counter, which counts at 10kHz. 1447 if DEBUG: 1448 y = "q_trace_rate() -> {0}".format(x) 1449 print(y) 1450 return x 1451 1452 def s_waveform_pwm(self, value): 1453 1454 value = constrain(value, -4095, 4095) 1455 value = int(value) 1456 x = self._send_receive_4bytes(SERIAL_WFMPWM, param1=0, param2=value, typ='uint') 1457 if DEBUG: 1458 y = "s_waveform_pwm({0}) -> {1}".format(value, x) 1459 print(y) 1460 return x
Class to control a petapicovoltron hvps-x
The class implements the low-level functions to interface the hvps-x firmware for easy communication with the hvps-x. In addition, a few higher-level functions are provided.
232 def __init__(self, port, init=True): 233 """ Initialises a HVPS object: dev = HVPS('/path/to/serial/port'). 234 235 Input: COM port to which the HVPS is connected.""" 236 self.name = '' 237 self.vmax = 0 238 self.swmode = 0 239 self.vsetp = 0 240 self.vsetn = 0 # negative voltage setpoint (for bipolar configuration) 241 self.vnowp = 0 242 self.vnown = 0 243 self.vnowo = 0 244 self.f = 0 245 self.duty = 0.5 246 self.cycles = 0 247 self.cycle_n = 0 # the current cycle number 248 self.swsrc = 0 249 self.vmode = 0 250 self.bttncfg = 0 # latching behaviour of the button 251 self.err = 0 252 self.stmode = 0 # Strobe mode 253 self.stpos = 0 # Strobe position 254 self.stdur = 0 # strobe duration 255 self.stsweep = 5000 # Strobe sweep time (default when HVPS starts) 256 self.ser = serial.Serial() # the serial connection to the HVPS 257 self.waveform_pts = [] # list to store the set points of the custom waveform 258 self.waveform_meas = [] # list to store the measured points of the custom waveform. 259 self.lookup_adc_p = [] # lookup tables for linear interpolation 260 self.lookup_adc_o = [] 261 self.lookup_v_out = [] 262 self.cp = [] # calibration coefficients. 263 self.co = [] 264 self.calmeth_p = -1 265 self.calmeth_n = -1 266 self.calmeth_o = -1 267 268 self.firmware = 0 269 self.hardware = 0 270 self.conf = 0 # configuration of the hvps-x 271 self.is_hvpsx = 0 272 273 try: 274 self.ser = serial.Serial(port, 115200, timeout=3) 275 except serial.SerialException: 276 self.err = self.err | ERR_PORT 277 if DEBUG: 278 print("Cannot connect to serial port. Probably busy.") 279 else: 280 self.ser.reset_input_buffer() 281 if DEBUG: 282 text = "connecting to " + port 283 print(text) 284 print("Serial port open") 285 z = self._send_receive_4bytes(SERIAL_QCONF) 286 if z == 0: # no response: the device connected doesn't talk using this protocol 287 self.err = self.err | ERR_COM 288 if DEBUG: 289 print("Communication error: the connected device doesn't reply to commands") 290 z_hvps = z & 0xFFFF 291 z_conf = (z & 0xFFFF0000) >> 16 292 if z_hvps == HVPSX_PID: 293 self.is_hvpsx = 1 294 if z_conf != CONF_UNCONF: 295 self.conf = z_conf 296 else: 297 self.err = self.err | ERR_CONF # the hvps-x is not configured 298 if DEBUG: 299 print("The hvps is not configured") 300 z = self._send_4bytes_receive_nbytes(SERIAL_QNAME, param1=0, param2=0, fstring='<12s') 301 z = z[0].split(b'\x00', 1)[0] 302 self.name = z 303 else: 304 self.err = self.err | ERR_TYPE # The connected device replied with Err! but is not a hvps-x 305 if DEBUG: 306 print("Type error: the device replies to commands, but is not a hvps-x.") 307 if init and not self.err: 308 self._initialise_hvpsx()
Initialises a HVPS object: dev = HVPS('/path/to/serial/port').
Input: COM port to which the HVPS is connected.
310 def close(self, zero=True): # closes connection with the HVPS 311 """Closes the connection to the HVPS. Sets voltage to 0 if board was connected 312 313 Input (optional): zero. If True, will set the voltage to 0 and the optocouplers to off before closing the 314 communication. If False, the hvps-x is left in its current state""" 315 316 if self.ser.is_open: 317 if not (self.err & ERR_COM): # if connected board is not running the firmware, do not send command 318 if zero: 319 self.s_vset(0, DCDC_BOTH) # set the voltage to 0 as a safety measure 320 self.s_sw_mode(SWMODE_OFF) # for hvps-x, this will disable both optocouplers. 321 self.ser.close() 322 323 if DEBUG: 324 print("Serial port closed")
Closes the connection to the HVPS. Sets voltage to 0 if board was connected
Input (optional): zero. If True, will set the voltage to 0 and the optocouplers to off before closing the communication. If False, the hvps-x is left in its current state
394 def convert_raw_to_voltage(self, vraw_main, vraw_out, which): 395 if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT: 396 which = VNOW_POS 397 # Download calibration values the first time the function is used 398 399 if self.calmeth_p == -1: # calibration method not yet fetched from HVPS 400 self.q_calibration_method(which=VNOW_POS) 401 if self.calmeth_n == -1: # calibration method not yet fetched from HVPS 402 self.q_calibration_method(which=VNOW_NEG) 403 if self.calmeth_o == -1: # calibration method not yet fetched from HVPS 404 self.q_calibration_method(which=VNOW_OUT) 405 406 if (self.calmeth_o == CALMETH_LOOKUP or self.calmeth_p == CALMETH_LOOKUP) and not self.lookup_v_out: 407 self.lookup_v_out = self.q_lookup(0) 408 if self.calmeth_p == CALMETH_LOOKUP and not self.lookup_adc_p: 409 self.lookup_adc_p = self.q_lookup(1) 410 if self.calmeth_o == CALMETH_LOOKUP and not self.lookup_adc_o: 411 self.lookup_adc_o = self.q_lookup(2) 412 413 if self.calmeth_p == CALMETH_POLYNOMIAL and not self.cp: # if we don't know what are the calibration values 414 self.cp.append(self.q_cal(CAL_C0P)) 415 self.cp.append(self.q_cal(CAL_C1P)) 416 self.cp.append(self.q_cal(CAL_C2P)) 417 418 if self.calmeth_o == CALMETH_POLYNOMIAL and not self.co: # if we don't know what are the calibration values 419 self.co.append(self.q_cal(CAL_C0O)) 420 self.co.append(self.q_cal(CAL_C1O)) 421 self.co.append(self.q_cal(CAL_C2O)) 422 423 if self.calmeth_o == CALMETH_LOOKUP: 424 v_out = np.interp(vraw_out, self.lookup_adc_o, self.lookup_v_out, left=None, right=None, period=None) 425 else: 426 v_out = self.co[0] + self.co[1] * vraw_out + self.co[2] * np.square(vraw_out) 427 428 if which == VNOW_POS: 429 if self.calmeth_p == CALMETH_LOOKUP: 430 v_main = np.interp(vraw_main, self.lookup_adc_p, self.lookup_v_out, left=None, right=None, period=None) 431 else: 432 v_main = self.cp[0] + self.cp[1] * vraw_main + self.cp[2] * np.square(vraw_main) 433 elif which == VNOW_NEG: 434 v_main = 0 435 else: 436 v_main = v_out 437 return v_main, v_out
439 def is_bipolar(self): # return true if connected device is hvpsx and bipolar unit 440 """ Returns true if the hvps-x configuration is bipolar.""" 441 if self.conf & 1: 442 return True 443 else: 444 return False
Returns true if the hvps-x configuration is bipolar.
447 def s_vset(self, x, polarity=DCDC_POS): # sets the output voltage 448 """Sets the output voltage of the HVPS. The new parameter remains valid until a new call to this command, or 449 when the HVPS is powered off. Using the 450 save() command enables to save this parameter in memory\n 451 Although this command can be used at any time, it is mainly useful when the HVPS voltage control mode is 452 regulated (i.e. closed loop) (VMODE_R). In this case the hvps-x will adjust the control voltage to keep the 453 output to the set point; see s_vmode() command. 454 :param x: voltage set point in volt (int) 455 :param polarity: which DC/DC converter to address (DCDC_POS, DCDC_NEG (bipolar config), DCDC_BOTH) 456 :return: voltage set point accepted by hvps-x 457 """ 458 x = constrain(abs(x), 0, self.vmax) 459 x = int(x) 460 if polarity != DCDC_POS and polarity != DCDC_NEG and polarity != DCDC_BOTH: 461 polarity = DCDC_POS 462 z = self._send_receive_4bytes(SERIAL_SVSET, param1=polarity, param2=x) 463 if x == 0: 464 z = 0 # avoids calibration issue near 0 that would lead to a negative number which is saturated to 0 in 465 # the STM32 (as the stored raw value is uint), leading to a possible large offset error returned value of a 466 # few 10s of volts when back calculated in volts 467 if polarity == DCDC_POS: 468 self.vsetp = z 469 elif polarity == DCDC_NEG: 470 self.vsetn = z 471 else: # DCDC_Both: i.e. set the positive and negative voltage to the same level 472 self.vsetp = z 473 self.vsetn = z 474 if DEBUG: 475 y = "s_vset(" + str(x) + ") -> " + str(z) 476 print(y) 477 return z
Sets the output voltage of the HVPS. The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the save() command enables to save this parameter in memory
Although this command can be used at any time, it is mainly useful when the HVPS voltage control mode is regulated (i.e. closed loop) (VMODE_R). In this case the hvps-x will adjust the control voltage to keep the output to the set point; see s_vmode() command.
Parameters
- x: voltage set point in volt (int)
- polarity: which DC/DC converter to address (DCDC_POS, DCDC_NEG (bipolar config), DCDC_BOTH)
Returns
voltage set point accepted by hvps-x
479 def q_vset(self, polarity=DCDC_POS): # queries the voltage setpoint 480 """Queries the voltage set point. The returned value is in volts. 481 :param polarity: DCDC converter to be queried (DCDC_POS or DCDC_NEG). DCDC_NEG will return 0 for 482 unipolar configs 483 :return: Voltage set point in volts 484 """ 485 if polarity != DCDC_POS and polarity != DCDC_NEG: 486 polarity = DCDC_POS 487 z = self._send_receive_4bytes(SERIAL_QVSET, param1=polarity) 488 if DEBUG: 489 y = "q_vset -> " + str(z) 490 print(y) 491 if polarity == DCDC_NEG: 492 self.vsetn = z 493 else: 494 self.vsetp = z 495 return z
Queries the voltage set point. The returned value is in volts.
Parameters
- polarity: DCDC converter to be queried (DCDC_POS or DCDC_NEG). DCDC_NEG will return 0 for unipolar configs
Returns
Voltage set point in volts
497 def q_vnow(self, which=VNOW_POS): # queries the voltage output 498 """Queries the feedback voltage of the HVPS. 499 :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the 500 positive DCDC converter, negative DCDC converter, or output of the HVPS 501 :return: voltage value in volts 502 """ 503 504 if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT: 505 which = VNOW_POS 506 z = self._send_4bytes_receive_nbytes(SERIAL_QVNOW, param1=which, fstring='<2h') 507 z_out = z[1] 508 z_main = z[0] 509 if DEBUG: 510 y = "q_vnow -> " + str(z_main) + " / " + str(z_out) 511 print(y) 512 self.vnowo = z_out 513 if which == VNOW_OUT: 514 self.vnowo = z_main 515 elif which == VNOW_NEG: 516 self.vnown = z_main 517 else: 518 self.vnowp = z_main 519 return z_main
Queries the feedback voltage of the HVPS.
Parameters
- which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the positive DCDC converter, negative DCDC converter, or output of the HVPS
Returns
voltage value in volts
521 def q_vnow_raw(self, which=VNOW_POS): # queries a raw value of the voltage output 522 """Queries the current feedback voltage of the HVPS. The returned value is a raw 12bit ADC value. This avoids 523 running slow floating point calculations on the 524 microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to 525 execute than q_vnow(). Useful for streaming voltage values 526 :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the 527 positive DCDC converter, negative DCDC converter, or output of the HVPS. 528 :return: a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT 529 (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is 530 a 12 bit value that can be converted to voltage using the unit's calibration values (see q_vnow_fast() for a 531 way to do this automatically""" 532 533 if which != VNOW_POS and which != VNOW_NEG and which != VNOW_OUT: 534 which = VNOW_POS 535 z = self._send_receive_4bytes(SERIAL_QVNOWRAW, param1=which, typ='uint') 536 z_out = z & 0xFFFF 537 z_main = (z & 0xFFFF0000) >> 16 538 if DEBUG: 539 y = "q_vnow_raw -> " + str(z_main) + " , " + str(z_out) 540 print(y) 541 return z_main, z_out
Queries the current feedback voltage of the HVPS. The returned value is a raw 12bit ADC value. This avoids running slow floating point calculations on the microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to execute than q_vnow(). Useful for streaming voltage values
Parameters
- which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the positive DCDC converter, negative DCDC converter, or output of the HVPS.
Returns
a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is a 12 bit value that can be converted to voltage using the unit's calibration values (see q_vnow_fast() for a way to do this automatically
543 def q_vnow_fast(self, which=VNOW_POS): # queries the voltage output 544 """Queries the current feedback voltage of the HVPS in a raw format and convert it to a calibrated voltage 545 This avoids running slow floating point calculations on the 546 microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to 547 execute than q_vnow(). Useful for streaming voltage values. This method is similar to q_vnow(), except that 548 the conversion from a raw value to a calibrated value is done 549 on the computer rather than on the microcontroller. It can take up to 300us to convert a value on the MUC 550 (it depends on the method used (linear, quadratic, lookup table) 551 :param which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the 552 positive DCDC converter, negative DCDC converter, or output of the HVPS. 553 :return: a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT 554 (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is 555 a 12 bit value that can be converted to voltage using the unit's calibration values The returned values 556 are calibrated voltages in Volt""" 557 558 z_main, z_out = self.q_vnow_raw(which=which) 559 v_main, v_out = self.convert_raw_to_voltage(z_main, z_out, which=which) 560 561 return v_main, v_out
Queries the current feedback voltage of the HVPS in a raw format and convert it to a calibrated voltage This avoids running slow floating point calculations on the microcontroller to convert ADC readings into a calibrated voltage, and this command is therefore faster to execute than q_vnow(). Useful for streaming voltage values. This method is similar to q_vnow(), except that the conversion from a raw value to a calibrated value is done on the computer rather than on the microcontroller. It can take up to 300us to convert a value on the MUC (it depends on the method used (linear, quadratic, lookup table)
Parameters
- which: which output voltage to read {VNOW_POS, VNOW_NEG, VNOW_OUT}. Voltage at the output of the positive DCDC converter, negative DCDC converter, or output of the HVPS.
Returns
a tuple with two values: the first element is the requested value, and the second is always VNOW_OUT (i.e. you get VNOW_OUT for free when reading the voltage of one of the DCDC converters.). The returned value is a 12 bit value that can be converted to voltage using the unit's calibration values The returned values are calibrated voltages in Volt
563 def s_f(self, x): # sets the frequency 564 """Sets the frequency of the signal when the HVPS is in switching mode (SWMODE_SW).\n 565 The value returned is the new frequency, taking quantification into account. 566 :param x: frequency in Hz between 0.001 and 1000 567 :return: frequency accepted by hvps-x in Hz 568 """ 569 x = constrain(x, 0.001, 1000.0) 570 571 # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction 572 # Ex: 10.0: 0x41 0x20 0x00 0x00. 573 # 0 10000010 01000000000000000000000 574 # S=0 Positive 575 # Exp=130-127 (offset)=2^3=8 576 # Fraction=1+2^-2=1.25 577 # number=+1.25*8=10 578 f_byte = struct.pack('<f', x) 579 z = self._send_nbytes_receive_4bytes(SERIAL_SF, f_byte, typ='float') 580 581 if DEBUG: 582 y = "s_f(" + str(x) + ") -> " + str(z) 583 print(y) 584 self.f = z 585 return z
Sets the frequency of the signal when the HVPS is in switching mode (SWMODE_SW).
The value returned is the new frequency, taking quantification into account.
Parameters
- x: frequency in Hz between 0.001 and 1000
Returns
frequency accepted by hvps-x in Hz
587 def q_f(self): # queries the frequency 588 """Queries the switching frequency. The returned value is in Hz.""" 589 z = self._send_receive_4bytes(SERIAL_QF, param1=0, param2=0, typ='float') 590 591 if DEBUG: 592 y = "q_f -> " + str(z) 593 print(y) 594 self.f = z 595 return z
Queries the switching frequency. The returned value is in Hz.
597 def s_duty(self, x): # sets the duty cycle of the switching signal 598 """Sets the duty cycle of the switching signal 599 :param x: the duty cycle (float in the range 0-1) 600 :return: the current duty cycle (float between 0 and 1) 601 """ 602 duty = int(x * 1000) # hvps-x is coding duty cycle on a 0-1000 scale 603 604 z = self._send_receive_4bytes(SERIAL_SDUTY, param1=0, param2=duty, typ='uint') 605 z = z / 1000 606 if DEBUG: 607 y = "s_duty(" + str(x) + ") -> " + str(z) 608 print(y) 609 self.duty = z 610 return z
Sets the duty cycle of the switching signal
Parameters
- x: the duty cycle (float in the range 0-1)
Returns
the current duty cycle (float between 0 and 1)
612 def q_duty(self): # queries the duty cycle of the switching signal 613 """queries the duty cycle of the switching signal 614 :return: the current duty cycle (float between 0 and 1) 615 """ 616 617 z = self._send_receive_4bytes(SERIAL_QDUTY, param1=0, param2=0, typ='uint') 618 z = float(z) / 1000.0 619 if DEBUG: 620 y = "q_duty() -> " + str(z) 621 print(y) 622 self.duty = z 623 return z
queries the duty cycle of the switching signal
Returns
the current duty cycle (float between 0 and 1)
626 def s_sw_mode(self, x): # sets the switching mode 627 """Sets the switching mode of the hvps-x. \n 628 SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off. 629 This effectively 630 connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV DC/DC converter. 631 SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. For unipolar hvps-x, this connects J4 to ground and 632 is equivalent to the DC mode of the shvps. For bipolar hvps-x, this connects J4 to the negative high voltage 633 produced by the negative board. The other modes behaves in a similar manner compared to the shvps. SWMODE_SW: 634 the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency. 635 The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the 636 save() command enables to save this parameter in memory 637 :param x: SWMODE_OFF, SWMODE_HIGH, SWMODE_LOW, SWMODE_SW 638 :return: Switching mode set by the HVPS 639 """ 640 641 if x > SWMODE_LOW: 642 x = SWMODE_OFF 643 644 z = self._send_receive_4bytes(SERIAL_SSWMODE, param1=x, param2=0, typ='uint') 645 if DEBUG: 646 y = "s_sw_mode(" + str(x) + ") -> " + str(z) 647 print(y) 648 self.swmode = z 649 return z
Sets the switching mode of the hvps-x.
SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off. This effectively connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV DC/DC converter. SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. For unipolar hvps-x, this connects J4 to ground and is equivalent to the DC mode of the shvps. For bipolar hvps-x, this connects J4 to the negative high voltage produced by the negative board. The other modes behaves in a similar manner compared to the shvps. SWMODE_SW: the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency. The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the save() command enables to save this parameter in memory
Parameters
- x: SWMODE_OFF, SWMODE_HIGH, SWMODE_LOW, SWMODE_SW
Returns
Switching mode set by the HVPS
651 def q_sw_mode(self): # queries the switching mode 652 """queries the switching mode of the hvps-x. \n 653 SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off. 654 This effectively 655 connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV DC/DC converter. 656 SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. For unipolar hvps-x, this connects J4 to ground and 657 is equivalent to the DC mode of the shvps. For bipolar hvps-x, this connects J4 to the negative high voltage 658 produced by the negative board. The other modes behaves in a similar manner compared to the shvps. SWMODE_SW: 659 the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency. 660 :return: Switching mode set by the HVPS 661 """ 662 z = self._send_receive_4bytes(SERIAL_QSWMODE, param1=0, param2=0, typ='uint') 663 if DEBUG: 664 y = "q_sw_mode -> " + str(z) 665 print(y) 666 self.swmode = z 667 return z
queries the switching mode of the hvps-x.
SWMODE_OFF Both optocouplers turned off. SWMODE_HIGH: Optocoupler 1 is on and Optocoupler 2 is off. This effectively connects the red output (J4) of the hvps-x to the high voltage produced by the main board HV DC/DC converter. SWMODE_LOW: Optocoupler 2 is on and Optocoupler 1 is off. For unipolar hvps-x, this connects J4 to ground and is equivalent to the DC mode of the shvps. For bipolar hvps-x, this connects J4 to the negative high voltage produced by the negative board. The other modes behaves in a similar manner compared to the shvps. SWMODE_SW: the optocouplers are switching between SWMODE_HIGH and SWMODE_LOW at the specified frequency.
Returns
Switching mode set by the HVPS
669 def s_sw_src(self, x): # sets the switching source 670 """Sets the source of the switching signal. 671 672 Sets the source of the switching signal. Accepted values are: SWSRC_TMR for onboard 673 switching (from internal clock of the board), or SWSRC_BTTN for the push button.\n 674 Using the save() command enables to save this parameter in memory 675 :param x: SWSRC_TMR, or SWSRC_BTTN 676 :return: SWSRC_TMR, or SWSRC_BTTN 677 """ 678 if x > SWSRC_BTTN: 679 x = SWSRC_TMR 680 z = self._send_receive_4bytes(SERIAL_SSWSRC, param1=x, param2=0, typ='uint') 681 if DEBUG: 682 y = "s_sw_src(" + str(x) + ") -> " + str(z) 683 print(y) 684 self.swsrc = z 685 return z
Sets the source of the switching signal.
Sets the source of the switching signal. Accepted values are: SWSRC_TMR for onboard switching (from internal clock of the board), or SWSRC_BTTN for the push button.
Using the save() command enables to save this parameter in memory
Parameters
- x: SWSRC_TMR, or SWSRC_BTTN
Returns
SWSRC_TMR, or SWSRC_BTTN
687 def q_sw_src(self): # queries the switching source 688 """queries the source of the switching signal. 689 690 queries the source of the switching signal. Output values are: SWSRC_TMR for onboard 691 switching (from internal clock of the board), or SWSRC_BTTN for the push button.\n 692 Using the save() command enables to save this parameter in memory 693 :return: SWSRC_TMR, or SWSRC_BTTN 694 """ 695 696 z = self._send_receive_4bytes(SERIAL_QSWSRC, param1=0, param2=0, typ='uint') 697 698 if DEBUG: 699 y = "q_sw_src -> " + str(z) 700 print(y) 701 self.swsrc = z 702 return z
queries the source of the switching signal.
queries the source of the switching signal. Output values are: SWSRC_TMR for onboard switching (from internal clock of the board), or SWSRC_BTTN for the push button.
Using the save() command enables to save this parameter in memory
Returns
SWSRC_TMR, or SWSRC_BTTN
704 def s_bttn_cfg(self, x): # sets the configuration of the push button 705 """Defines the behaviour of the push button 706 707 The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the 708 save() command enables to save this parameter in memory 709 :param x: 2-bit value (0x0 to 0x3). bit 0: Defines the behaviour of the push button, when the switching source 710 of the HVPS is set to the push button 711 (SWSRC_BTTN, c.f. s_sw_src() command above). Accepted values are 0 and 1: 0 for a push button behaviour 712 (i.e. the high voltage is turned on as long as the button is pressed), 713 and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time 714 to turn it off).\n 715 bit 1: State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW 716 :return: the 2-bit button config value 717 """ 718 if x > 3: 719 x = 3 720 z = self._send_receive_4bytes(SERIAL_SBTTNCFG, param1=x, param2=0, typ='uint') 721 if DEBUG: 722 y = "s_bttn_cfg(" + str(x) + ") -> " + str(z) 723 print(y) 724 self.bttncfg = z 725 return z
Defines the behaviour of the push button
The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the save() command enables to save this parameter in memory
Parameters
- x: 2-bit value (0x0 to 0x3). bit 0: Defines the behaviour of the push button, when the switching source of the HVPS is set to the push button (SWSRC_BTTN, c.f. s_sw_src() command above). Accepted values are 0 and 1: 0 for a push button behaviour (i.e. the high voltage is turned on as long as the button is pressed), and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time to turn it off).
bit 1: State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW
Returns
the 2-bit button config value
727 def q_bttn_cfg(self): # queries the latch mode of the push button 728 """Queries the behaviour of the push button 729 730 :return: the 2-bit button config value. bit 0: Defines the behaviour of the push button, when the switching 731 source of the HVPS is set to the push button (SWSRC_BTTN, c.f. s_sw_src() command above). 732 Values are 0 for a push button behaviour (i.e. the high voltage is turned on as long as the button is pressed), 733 and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time 734 to turn it off).\n 735 bit 1: State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW 736 """ 737 z = self._send_receive_4bytes(SERIAL_QBTTNCFG, param1=0, param2=0, typ='uint') 738 if DEBUG: 739 y = "q_bttn_cfg -> " + str(z) 740 print(y) 741 self.bttncfg = z 742 return z
Queries the behaviour of the push button
Returns
the 2-bit button config value. bit 0: Defines the behaviour of the push button, when the switching source of the HVPS is set to the push button (SWSRC_BTTN, c.f. s_sw_src() command above). Values are 0 for a push button behaviour (i.e. the high voltage is turned on as long as the button is pressed), and 1 for a latching switch behaviour (i.e. press once to turn the high voltage on, and press a second time to turn it off).
bit 1: State of the button when it is not activated: 0: SWMODE_OFF, 1: SWMODE_LOW
744 def q_kill(self): # queries the state of the kill button. Kill=1 means HV is disabled 745 """Queries whether HV is disabled (Kill=1) or enabled (kill=0). When kill = 1 there will not be a HV present at 746 the hvps-x output, irrespective of any software setting. 747 :return: 1 if the Switch S1 on the board is set to 0 (HV output is killed), 0 otherwise. 748 """ 749 z = self._send_receive_4bytes(SERIAL_QKILL, param1=0, param2=0, typ='uint') 750 751 if DEBUG: 752 y = "q_kill -> " + str(z) 753 print(y) 754 return z
Queries whether HV is disabled (Kill=1) or enabled (kill=0). When kill = 1 there will not be a HV present at the hvps-x output, irrespective of any software setting.
Returns
1 if the Switch S1 on the board is set to 0 (HV output is killed), 0 otherwise.
756 def s_v_mode(self, x): # sets the voltage control mode 757 """Sets the voltage control mode 758 759 Sets the voltage control mode (i.e. how is the value of the output voltage controlled):\n 760 VMODE_R for internal voltage regulator (regulates the voltage to the value defined with the Vset command).\n 761 VMODE_O (that's an O like in open) internal open loop control (on-board regulator disconnected).\n 762 The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the 763 save() command enables to save this parameter in memory\n 764 VMODE_O has an internal safeguard that will decrease the setting if the sensing circuit saturates. However, 765 the voltage can still be slightly higher than the DCDC converter upper limit. User must check that the output 766 voltage remains within the allowed range 767 :param x: VMODE_R, VMODE_O 768 :return: VMODE_R, VMODE_O 769 """ 770 771 if x > VMODE_O: 772 x = VMODE_R 773 774 z = self._send_receive_4bytes(SERIAL_SVMODE, param1=x, param2=0, typ='uint') 775 if DEBUG: 776 y = "s_v_mode(" + str(x) + ") -> " + str(z) 777 print(y) 778 self.vmode = z 779 return z
Sets the voltage control mode
Sets the voltage control mode (i.e. how is the value of the output voltage controlled):
VMODE_R for internal voltage regulator (regulates the voltage to the value defined with the Vset command).
VMODE_O (that's an O like in open) internal open loop control (on-board regulator disconnected).
The new parameter remains valid until a new call to this command, or when the HVPS is powered off. Using the save() command enables to save this parameter in memory
VMODE_O has an internal safeguard that will decrease the setting if the sensing circuit saturates. However, the voltage can still be slightly higher than the DCDC converter upper limit. User must check that the output voltage remains within the allowed range
Parameters
- x: VMODE_R, VMODE_O
Returns
VMODE_R, VMODE_O
781 def q_v_mode(self): # queries the switching source 782 """Queries the voltage control mode 783 784 :return: VMODE_R internal voltage regulator, VMODE_O internal 785 open loop control (on-board regulator disconnected). 786 """ 787 788 z = self._send_receive_4bytes(SERIAL_QVMODE, param1=0, param2=0, typ='uint') 789 790 if DEBUG: 791 y = "q_v_mode -> " + str(z) 792 print(y) 793 self.vmode = z 794 return z
Queries the voltage control mode
Returns
VMODE_R internal voltage regulator, VMODE_O internal open loop control (on-board regulator disconnected).
797 def s_pid(self, x, pid=PID_KPP): 798 """Sets the gains of the PIDs 799 Use save() command to save the new values to memory 800 :param x: the value (float) of the gain. 801 :param pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,N,O}, 802 for positive voltage PID, negative voltage PID, output voltage PID. (in Lib V1, Y can only take the value P) 803 :return: the gain value 804 """ 805 # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction 806 # Ex: 10.0: 0x41 0x20 0x00 0x00. 807 # 0 10000010 01000000000000000000000 808 # S=0 Positive 809 # Exp=130-127 (offset)=2^3=8 810 # Fraction=1+2^-2=1.25 811 # number=+1.25*8=10 812 pid_byte = struct.pack('<Bf', pid, x) 813 z = self._send_nbytes_receive_4bytes(SERIAL_SPID, pid_byte, typ='float') 814 815 if DEBUG: 816 y = "s_pid(" + str(x) + "," + str(pid) + ") -> " + str(z) 817 print(y) 818 return z
Sets the gains of the PIDs Use save() command to save the new values to memory
Parameters
- x: the value (float) of the gain.
- pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,N,O}, for positive voltage PID, negative voltage PID, output voltage PID. (in Lib V1, Y can only take the value P)
Returns
the gain value
820 def q_pid(self, pid=PID_KPP): # queries the frequency 821 """returns the gains of the PIDs 822 823 :param pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,N,O}, 824 for positive voltage PID, negative voltage PID, output voltage PID. (in Lib V1, Y can only take the value P) 825 :return: the gain value of the chosen parameter 826 """ 827 828 z = self._send_receive_4bytes(SERIAL_QPID, param1=pid, param2=0, typ='float') 829 830 if DEBUG: 831 y = "q_pid(" + str(pid) + ") -> " + str(z) 832 print(y) 833 834 return z
returns the gains of the PIDs
Parameters
- pid: PID_KXY, with X={P,I,D} for the gains Kp, Ki, and Kd, and Y={P,N,O}, for positive voltage PID, negative voltage PID, output voltage PID. (in Lib V1, Y can only take the value P)
Returns
the gain value of the chosen parameter
836 def s_cal(self, x, cal=CAL_C1P): 837 """Sets the calibration constants of the analogue input signals (conversion to calibrated voltage values) 838 Use save() to commit the setting to memory 839 :param x: the value of the calibration coefficient (float) 840 :param cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,N,O}, for positive 841 voltage measurement, negative voltage measurement, output voltage measurement 842 :return: the calibration value 843 """ 844 845 # 4-bytes floats: sign-exponent-fraction. Sign is bit 31. must add 1 to fraction 846 # Ex: 10.0: 0x41 0x20 0x00 0x00. 847 # 0 10000010 01000000000000000000000 848 # S=0 Positive 849 # Exp=130-127 (offset)=2^3=8 850 # Fraction=1+2^-2=1.25 851 # number=+1.25*8=10 852 cal_byte = struct.pack('<Bf', cal, x) 853 z = self._send_nbytes_receive_4bytes(SERIAL_SCAL, cal_byte, typ='float') 854 855 if DEBUG: 856 y = "s_cal(" + str(x) + "," + str(cal) + ") -> " + str(z) 857 print(y) 858 return z
Sets the calibration constants of the analogue input signals (conversion to calibrated voltage values) Use save() to commit the setting to memory
Parameters
- x: the value of the calibration coefficient (float)
- cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,N,O}, for positive voltage measurement, negative voltage measurement, output voltage measurement
Returns
the calibration value
860 def q_cal(self, cal=CAL_C1P): # queries the frequency 861 """queries the calibration constants of the analogue input signals (conversion to calibrated voltage values) 862 863 :param cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,N,O}, for positive 864 voltage measurement, negative voltage measurement, output voltage measurement 865 :return: the calibration value 866 """ 867 868 z = self._send_receive_4bytes(SERIAL_QCAL, param1=cal, param2=0, typ='float') 869 870 if DEBUG: 871 y = "q_cal(" + str(cal) + ") -> " + str(z) 872 print(y) 873 self.f = z 874 return z
queries the calibration constants of the analogue input signals (conversion to calibrated voltage values)
Parameters
- cal: CAL_CXY, with X={0,1,2} for the calibration coefficient of order X, and Y={P,N,O}, for positive voltage measurement, negative voltage measurement, output voltage measurement
Returns
the calibration value
876 def s_calibration_method(self, which=VNOW_POS, calmeth=CALMETH_POLYNOMIAL): 877 if not (calmeth == CALMETH_POLYNOMIAL or calmeth == CALMETH_LOOKUP): 878 calmeth = CALMETH_POLYNOMIAL 879 if not (which == VNOW_POS or which == VNOW_OUT or which == VNOW_NEG): 880 which = VNOW_POS 881 z = self._send_receive_4bytes(SERIAL_SCALMETH, param1=which, param2=calmeth, typ='uint') 882 883 if DEBUG: 884 y = "s_calibration_method({0}, {1}) -> {2}".format(which, calmeth, z) 885 print(y) 886 if which == VNOW_POS: 887 self.calmeth_p = z 888 elif which == VNOW_OUT: 889 self.calmeth_o = z 890 else: 891 self.calmeth_n = z
893 def q_calibration_method(self, which=VNOW_POS): 894 if not (which == VNOW_POS or which == VNOW_OUT or which == VNOW_NEG): 895 which = VNOW_POS 896 z = self._send_receive_4bytes(SERIAL_QCALMETH, param1=which, param2=0, typ='uint') 897 898 if DEBUG: 899 y = "q_calibration_method({0}) -> {1}".format(which, z) 900 print(y) 901 if which == VNOW_POS: 902 self.calmeth_p = z 903 elif which == VNOW_NEG: 904 self.calmeth_n = z 905 else: 906 self.calmeth_o = z
908 def s_lookup(self, x, n, table=LOOKUP_ADC_P): 909 if n > 20: 910 n = 20 911 n = n << 2 # shift left 2 bits 912 if table == LOOKUP_ADC_P: 913 n = n | 0b01 914 elif table == LOOKUP_ADC_O: 915 n = n | 0b10 916 x = int(x) 917 z = self._send_receive_4bytes(SERIAL_SLKUP, param1=n, param2=x, typ='int16') 918 if DEBUG: 919 y = "s_lookup({0},{1},{2}) - > {3}".format(x, n, table, z) 920 print(y)
922 def q_lookup(self, which): # which=0: vout, which=1: ADC_p, which=2: ADC_o 923 format_string = f'<21h' 924 z = self._send_4bytes_receive_nbytes(SERIAL_QLKUP, param1=which, param2=0, fstring=format_string) 925 lookup_list = list(z) 926 if DEBUG: 927 y = "q_lookup({0}) - > {1}".format(which, lookup_list) 928 print(y) 929 return lookup_list
1100 def save(self): # save current HVPS parameters into the memory 1101 """save active HVPS parameters into the memory 1102 1103 This command saves the active parameters as \'current\' settings. Current setting are the settings that are 1104 loaded when power is applied to the hvpsx 1105 1106 :return: SERIAL_OK or SERIAL_ERROR 1107 """ 1108 1109 z = self._send_receive_4bytes(SERIAL_SAVE) 1110 1111 if DEBUG: 1112 y = "save -> " + str(z) 1113 print(y) 1114 return z
save active HVPS parameters into the memory
This command saves the active parameters as 'current' settings. Current setting are the settings that are loaded when power is applied to the hvpsx
Returns
SERIAL_OK or SERIAL_ERROR
1116 def save_memory_to_file(self, settings=SETTINGS_CURRENT): 1117 """Dumps the content of the memory into a JSON file. 1118 1119 Dumps the content of the memory into a file. This is useful to keep a backup of the parameters on file. 1120 Files will be created in the interface folder and have the following format: Name_settings_type_date_time.json\n 1121 json files with settings can be transferred back to the hvps-x with the transfer_settings() method of the HVPS 1122 class, or the higher-level function transfer_file_to_memory() 1123 :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup 1124 :return: nothing 1125 """ 1126 1127 memory = self.q_mem(settings) 1128 now = datetime.now() # current date and time 1129 date_time = now.strftime("%Y_%m_%d_%H_%M_%S") 1130 if settings == SETTINGS_CURRENT: 1131 st_string = '_current_settings_' 1132 else: 1133 st_string = '_backup_settings_' 1134 file_name = self.name.decode("utf-8") + st_string + date_time + '.json' 1135 with open(file_name, "w") as write_file: 1136 json.dump(memory, write_file, indent=4)
Dumps the content of the memory into a JSON file.
Dumps the content of the memory into a file. This is useful to keep a backup of the parameters on file. Files will be created in the interface folder and have the following format: Name_settings_type_date_time.json
json files with settings can be transferred back to the hvps-x with the transfer_settings() method of the HVPS class, or the higher-level function transfer_file_to_memory()
Parameters
- settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup
Returns
nothing
1138 def s_settings(self, 1139 settings=SETTINGS_CURRENT): # sets the active setting to a particular type (useful before saving) 1140 if not (settings == SETTINGS_CURRENT or settings == SETTINGS_FACTORY or settings == SETTINGS_BACKUP): 1141 settings = SETTINGS_CURRENT 1142 x = self._send_receive_4bytes(SERIAL_SST, param1=settings) 1143 if DEBUG: 1144 print('s_settings({0}) -> {1}'.format(settings, x)) 1145 return x
1147 def load_settings(self, settings=SETTINGS_CURRENT): # Load a setting set from memory to the active setting 1148 """This function loads one of the two set of settings (current or backup) as active settings used by the hvps-x 1149 1150 :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup 1151 :return: 1152 """ 1153 1154 if not (settings == SETTINGS_CURRENT or settings == SETTINGS_BACKUP): 1155 settings = SETTINGS_CURRENT 1156 x = self._send_receive_4bytes(SERIAL_LST, param1=settings) 1157 if DEBUG: 1158 print('load_settings({0}) -> {1}'.format(settings, x)) 1159 1160 self._initialise_hvpsx() # need to reread all parameters 1161 1162 return x
This function loads one of the two set of settings (current or backup) as active settings used by the hvps-x
Parameters
- settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup
Returns
1164 def transfer_settings(self, dict_settings): 1165 """ transfer a setting dictionary from the computer to the hvps-x memory 1166 :param dict_settings: a dictionary containing the settings values. 1167 :return: SERIAL_OK or SERIAL_ERROR 1168 The dictionary of settings should be read from a file dumped using the function save_memory_to_file(). Together 1169 there two functions make it possible to backup the settings (this includes calibration and PID settings in a 1170 file, and gives the opportunity to restore the settings""" 1171 1172 error = False 1173 fw_string = 'Fw-{0:.1f}'.format(self.firmware) 1174 siz = struct.calcsize(memory_string[fw_string]) 1175 buffer = ctypes.create_string_buffer(siz) 1176 memmap = memorymap[fw_string] 1177 counter = 0 1178 for x in memmap: 1179 n = x[0] 1180 key = x[1] 1181 data_format = x[2] 1182 if data_format == 'B' or data_format == 's': 1183 bytelength = 1 1184 elif data_format == 'h' or data_format == 'H': 1185 bytelength = 2 1186 elif data_format == 'f': 1187 bytelength = 4 1188 else: 1189 bytelength = 1 1190 data = dict_settings[key] 1191 if key == 'Name': 1192 data = bytes(data, 'utf-8') 1193 if len(data) > 12: 1194 error = True 1195 if DEBUG: 1196 print("Error: name should not exceed 12 characters") 1197 else: 1198 format_str = '<{0}{1}'.format(n, data_format) 1199 struct.pack_into(format_str, buffer, counter, data) 1200 else: 1201 format_str = '<{0}'.format(data_format) 1202 if n == 1: 1203 struct.pack_into(format_str, buffer, counter, data) 1204 else: 1205 for i in range(n): 1206 try: 1207 struct.pack_into(format_str, buffer, counter + i * bytelength, data[i]) 1208 except IndexError: 1209 error = True 1210 if DEBUG: 1211 print('setting dictionary does not fit the expected format') 1212 1213 counter = counter + n * bytelength 1214 data_fw = 'Fw-{0}.{1}'.format(dict_settings['fw_major'], dict_settings['fw_minor']) 1215 if data_fw != fw_string: 1216 error = True 1217 if DEBUG: 1218 print('Error: JSON file firmware version does not match firware currently on hvps-x. Exiting') 1219 exit(0) 1220 if not error: 1221 buffer = b'xxx' + buffer # Adds 3 random bytes. CMD + 3 random bytes means that when mapping the transfer 1222 # buffer to a structure, it will on an aligned memory address 1223 x = self._send_nbytes_receive_4bytes(SERIAL_XFERST, buffer) 1224 else: 1225 x = SERIAL_ERROR 1226 1227 if DEBUG: 1228 print("transfer_settings(...) -> {0}".format(x)) 1229 return x
transfer a setting dictionary from the computer to the hvps-x memory
Parameters
- dict_settings: a dictionary containing the settings values.
Returns
SERIAL_OK or SERIAL_ERROR The dictionary of settings should be read from a file dumped using the function save_memory_to_file(). Together there two functions make it possible to backup the settings (this includes calibration and PID settings in a file, and gives the opportunity to restore the settings
1231 def copy_settings(self, src, dst): 1232 """ 1233 :param src: the settings to copy (CURRENT_SETTINGS or BACKUP_SETTINGS) 1234 :param dst: the destination settings (CURRENT_SETTINGS or BACKUP_SETTINGS) (destination will be overwritten by 1235 the source 1236 :return: nothing 1237 Copies one set of settings to another location:\n 1238 Copying BACKUP_SETTINGS to CURRENT_SETTINGS is useful to restore the backup settings as current settings 1239 (for example if some temporary settings were saved as current settings, for example to experiment with new PID 1240 gain values)\ 1241 Copying CURRENT_SETTINGS to BACKUP_SETTINGS is useful after a new calibration of the HVPS to save the new 1242 calibration as a set of back-up settings""" 1243 if (src == SETTINGS_CURRENT or src == SETTINGS_BACKUP) and (dst == SETTINGS_CURRENT or dst == SETTINGS_BACKUP): 1244 x = self._send_receive_4bytes(SERIAL_CPYST, param1=src, param2=dst) 1245 else: 1246 x = SERIAL_ERROR 1247 1248 if DEBUG: 1249 print('copy_settings({0},{1}) -> {2}'.format(src, dst, x))
Parameters
- src: the settings to copy (CURRENT_SETTINGS or BACKUP_SETTINGS)
- dst: the destination settings (CURRENT_SETTINGS or BACKUP_SETTINGS) (destination will be overwritten by the source
Returns
nothing Copies one set of settings to another location:
Copying BACKUP_SETTINGS to CURRENT_SETTINGS is useful to restore the backup settings as current settings (for example if some temporary settings were saved as current settings, for example to experiment with new PID gain values) Copying CURRENT_SETTINGS to BACKUP_SETTINGS is useful after a new calibration of the HVPS to save the new calibration as a set of back-up settings
1251 def q_mem(self, settings=SETTINGS_CURRENT): # queries the content of the memory 1252 """ 1253 :param settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup 1254 :return: A dictionary with the content of the memory. This is similar to the function save_memory_to_file() 1255 except that is doesn't save the content to a file""" 1256 fw_string = 'Fw-{0:.1f}'.format(self.firmware) 1257 memmap = memorymap[fw_string] 1258 dict_mem = {} 1259 1260 z = self._send_4bytes_receive_nbytes(SERIAL_QMEM, settings, param2=0, fstring=memory_string[fw_string]) 1261 bytecount = 0 1262 for i in range(len(memmap)): 1263 length = memmap[i][0] 1264 field = memmap[i][1] 1265 if field == 'Name': 1266 length = 1 1267 y = z[bytecount:bytecount + length] 1268 # if field != 'Padding': 1269 if length == 1: 1270 dict_mem[field] = y[0] 1271 else: 1272 dict_mem[field] = y 1273 bytecount = bytecount + length 1274 1275 dict_mem['Name'] = dict_mem['Name'].decode('UTF-8') 1276 dict_mem['Name'] = dict_mem['Name'].split("\0")[0] 1277 1278 if DEBUG: 1279 print(dict_mem) 1280 return dict_mem
Parameters
- settings: which of the two sets of settings to save. Either SETTINGS_CURRENT or SETTINGS_Backup
Returns
A dictionary with the content of the memory. This is similar to the function save_memory_to_file() except that is doesn't save the content to a file
1282 def q_ver(self): # queries the firmware version 1283 """returns the current version of the firmware / hardware running on the board.""" 1284 z = self._send_receive_4bytes(SERIAL_QVER) 1285 Firm_minor = z & 0xFF 1286 Firm_major = (z >> 8) & 0xFF 1287 Hard_minor = (z >> 16) & 0xFF 1288 Hard_major = (z >> 24) 1289 self.firmware = Firm_major + Firm_minor / 10 1290 self.hardware = Hard_major + Hard_minor / 10 1291 if DEBUG: 1292 y = "q_ver -> {0} / {1}".format(self.firmware, self.hardware) 1293 print(y) 1294 return self.firmware, self.hardware
returns the current version of the firmware / hardware running on the board.
1297 def q_conf(self): # queries the configuration 1298 """returns the configuration of the board. 1299 :return: the configuration of the board in the high 2 bytes (CONF_UNCONF, CONF_UNIPOLAR, or CONF_BIPOLAR) 1300 and the PID (product ID) in the low 2 bytes. This is 0x0632 for a hvps-x unit. 1301 """ 1302 1303 z = self._send_receive_4bytes(SERIAL_QCONF) 1304 z_hvps = z & 0xFFFF 1305 z_conf = (z & 0xFFFF0000) >> 16 1306 if DEBUG: 1307 y = "q_conf -> " + hex(z_conf) + " / " + hex(z_hvps) 1308 print(y) 1309 self.conf = z_conf 1310 return z
returns the configuration of the board.
Returns
the configuration of the board in the high 2 bytes (CONF_UNCONF, CONF_UNIPOLAR, or CONF_BIPOLAR) and the PID (product ID) in the low 2 bytes. This is 0x0632 for a hvps-x unit.
1312 def s_conf(self, bipolar): 1313 """Sets the configuration of the board 1314 :param bipolar: boolean. Is board bipolar (True), or unipolar (false) 1315 :return: bipolar: boolean 1316 """ 1317 if bipolar: 1318 param = CONF_BIPOLAR 1319 else: 1320 param = CONF_UNIPOLAR 1321 z = self._send_receive_4bytes(SERIAL_SCONF, param1=param) 1322 if DEBUG: 1323 y = "s_conf -> " + hex(z) 1324 print(y) 1325 if z == SERIAL_ERROR: 1326 print("Error: this configuration is not recognised") 1327 self.conf = z 1328 return z
Sets the configuration of the board
Parameters
- bipolar: boolean. Is board bipolar (True), or unipolar (false)
Returns
bipolar: boolean
1330 def s_vmax(self, x): # sets the maximum voltage rating of the board 1331 """ sets the voltage rating of the hvps-x. Must match the EMCO DC/DC converter rating. 1332 :param x: Voltage rating of hvps-x in Volt 1333 :return: Voltage rating of hvps-x in Volt 1334 """ 1335 1336 x = constrain(x, 0, 6000) 1337 z = self._send_receive_4bytes(SERIAL_SVMAX, param2=x) 1338 if DEBUG: 1339 y = "s_vmax(" + str(x) + ") -> " + str(z) 1340 print(y) 1341 self.vmax = z 1342 return z
sets the voltage rating of the hvps-x. Must match the EMCO DC/DC converter rating.
Parameters
- x: Voltage rating of hvps-x in Volt
Returns
Voltage rating of hvps-x in Volt
1344 def q_vmax(self): # queries the voltage rating of the board 1345 """ Queries the maximal voltage of the board. The returned value is in volts. 1346 :return: board maximal voltage (V) 1347 """ 1348 z = self._send_receive_4bytes(SERIAL_QVMAX) 1349 if DEBUG: 1350 y = "q_vmax -> " + str(z) 1351 print(y) 1352 self.vmax = z 1353 return z
Queries the maximal voltage of the board. The returned value is in volts.
Returns
board maximal voltage (V)
1355 def s_name(self, x): # set the name of the HVPS 1356 """ Sets the name of the HVPS. 1357 :param x: Name of the hvps-x. 11 characters maximum 1358 :return: name accepted by hvps-x 1359 """ 1360 ll = len(x) 1361 if ll <= 12: 1362 x = bytearray(x, 'utf-8') 1363 for i in range(12 - ll): # pad the string with 0s 1364 x = x + b'\0' 1365 self._send_nbytes_receive_4bytes(SERIAL_SNAME, x, typ='uint') 1366 z = self.q_name() 1367 else: 1368 z = 'too long' 1369 if DEBUG: 1370 y = "s_name(" + str(x) + ") -> " + str(z) 1371 print(y) 1372 return z
Sets the name of the HVPS.
Parameters
- x: Name of the hvps-x. 11 characters maximum
Returns
name accepted by hvps-x
1374 def q_name(self): # queries the name of the board 1375 """queries the name of the board 1376 :return: Name of the board 1377 """ 1378 1379 # x = self._send_receive_4bytes(SERIAL_QNAME, param1=0) 1380 # x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=4) << 32) 1381 # x = x + (self._send_receive_4bytes(SERIAL_QNAME, param1=8) << 64) 1382 # x = x.to_bytes(12, 'little') 1383 1384 z = self._send_4bytes_receive_nbytes(SERIAL_QNAME, param1=0, param2=0, fstring='<12s') 1385 z = z[0].split(b'\x00', 1)[0] # take first (only) element of the tuple and remove the 0s at the end of string 1386 1387 self.name = z 1388 if DEBUG: 1389 y = "q_name -> " + str(z) 1390 print(y) 1391 return z
queries the name of the board
Returns
Name of the board
1397 def check_version_compatibility(self): 1398 self.q_ver() 1399 lib_string = 'Lib-' + LIB_VER 1400 fw_string = 'Fw-{0:.1f}'.format(self.firmware) 1401 hw_string = 'Hw-{0:.1f}'.format(self.hardware) 1402 1403 list_compatible_fw = compatibility_dict[lib_string] 1404 if fw_string in list_compatible_fw: 1405 list_compatible_hw = compatibility_dict[fw_string] 1406 if hw_string not in list_compatible_hw: 1407 self.err |= ERR_FIRM 1408 else: 1409 self.err |= ERR_FIRM
1424 def s_trace_rate(self, rate=5): 1425 """Sets the rate at which trace points are stored in memory 1426 :param rate: time in ms between point acquisition. This is limited by the time at which the hvps-x is 1427 calculating the voltages; currently evey 5ms. 1428 :return the rate accepted by the hvps-x""" 1429 1430 rate = int(rate) 1431 if rate < 2: 1432 rate = 2 1433 x = self._send_receive_4bytes(SERIAL_STRRATE, param1=0, param2=rate, typ='uint') 1434 x = x / 10 # the function returns the number of ticks of the counter, which counts at 10kHz. 1435 if DEBUG: 1436 y = "s_trace_rate({0}) -> {1}".format(rate, x) 1437 print(y) 1438 return x
Sets the rate at which trace points are stored in memory
Parameters
- rate: time in ms between point acquisition. This is limited by the time at which the hvps-x is calculating the voltages; currently evey 5ms. :return the rate accepted by the hvps-x
1440 def q_trace_rate(self): 1441 """queries the rate at which trace points are stored in memory 1442 :param none 1443 :return the rate at which trace points are stored in memory (in ms)""" 1444 1445 x = self._send_receive_4bytes(SERIAL_QTRRATE, param1=0, param2=0, typ='uint') 1446 x = x / 10 # the function returns the number of ticks of the counter, which counts at 10kHz. 1447 if DEBUG: 1448 y = "q_trace_rate() -> {0}".format(x) 1449 print(y) 1450 return x
queries the rate at which trace points are stored in memory :param none :return the rate at which trace points are stored in memory (in ms)
1463def list_hvps(list_all=False): 1464 """lists the hvps-x connected to PC in a dictionary.The key is the name of the HVPS, and the parameter is 1465 the associated serial port 1466 :param list_all: if True list all connected HVPS. 1467 if list_all is false, it will only list device that are configured. Using True enable to list unconfigured 1468 devices and give the opportunity to configure them 1469 :return: dictionary of connected hvps-x""" 1470 1471 hvpsx_ports = [ # creates a list with all STM32 VCP devices connected 1472 p.device 1473 for p in serial.tools.list_ports.comports() 1474 if (p.pid == 0x5740 and p.vid == 0x483) 1475 ] 1476 dict_hvps = {} 1477 for port in hvpsx_ports: 1478 dev = HVPS(port, init=False) # Attempts to connect to HVPS but without initialisation 1479 if dev.is_hvpsx: 1480 if dev.conf != CONF_UNCONF or list_all: 1481 dict_hvps[dev.name.decode()] = port # add an entry in the dictionary with the name of the 1482 # HVPS and the port 1483 dev.close(zero=False) 1484 1485 return dict_hvps
lists the hvps-x connected to PC in a dictionary.The key is the name of the HVPS, and the parameter is the associated serial port
Parameters
- list_all: if True list all connected HVPS. if list_all is false, it will only list device that are configured. Using True enable to list unconfigured devices and give the opportunity to configure them
Returns
dictionary of connected hvps-x
1488def connect_to_hvps(unconf=False): 1489 """Scan for connected hvps-x and connects to it if a single hvps-x is found. User can chose to which hvps-x to 1490 connect if more than one is detected. 1491 :param unconf: True/False. If true will connect to an unconfigured hvps-x. If False (default) only connects to a 1492 configured device 1493 :return: a HVPS object (or None if could not connect to a valid hvps-x 1494 If more than 1 hvps-x is connected to the computed, a list of names is displayed and the user must choose which one 1495 they want to connect to""" 1496 cont = False # flag to decide whether to continue the programme or close the COM port 1497 dev = None 1498 1499 ports = list_hvps(list_all=unconf) 1500 keys = list(ports.keys()) 1501 if len(ports) == 0: 1502 print("No HVPS connected to the device. Terminating programme") 1503 exit() 1504 elif len(ports) == 1: 1505 dev = HVPS(ports[keys[0]]) # connects to the only available HVPS 1506 else: 1507 print("List of connected HVPS:") 1508 for i in range(len(ports)): 1509 print(str(i) + ": " + keys[i]) 1510 print("Enter the number of the board you want to connect to") 1511 connect_to = 0 1512 try: 1513 connect_to = int(input()) 1514 except ValueError: 1515 print("Invalid entry. Terminating programme") 1516 exit() 1517 if connect_to >= len(ports): 1518 print("Invalid entry. Terminating programme") 1519 exit() 1520 else: 1521 dev = HVPS(ports[keys[connect_to]]) 1522 error = dev.err 1523 if error & ERR_PORT: 1524 print("Cannot open COM port. Probably busy with another process. Terminating Programme") 1525 if error & ERR_COM: 1526 print("Cannot communicate with hvps-x. Has firmware been flashed?. Terminating programme") 1527 if error & ERR_TYPE: 1528 print("Device connected this port is not recognised. Terminating programme") 1529 if error & ERR_CONF: 1530 print("This hvps-x is not configured. Please configure unit before using it. " 1531 "Terminating programme.") 1532 cont = True 1533 elif error & ERR_FIRM: 1534 print("Warning: your hvps-x library is not optimal for the firmware or the firmware is not optimal for your " 1535 "hardware. Refer to the website for a compatibility table") 1536 cont = True 1537 if error == 0: 1538 cont = True 1539 if cont: 1540 return dev 1541 else: 1542 return None
Scan for connected hvps-x and connects to it if a single hvps-x is found. User can chose to which hvps-x to connect if more than one is detected.
Parameters
- unconf: True/False. If true will connect to an unconfigured hvps-x. If False (default) only connects to a configured device
Returns
a HVPS object (or None if could not connect to a valid hvps-x If more than 1 hvps-x is connected to the computed, a list of names is displayed and the user must choose which one they want to connect to
1545def configure(dev): 1546 """Script to use to for the initial configuration (or re-configuration) of an hvps-x. Follow prompt on the console. 1547 :param dev: an HVPS object 1548 :return: nothing 1549 """ 1550 if dev != -1: 1551 config = configparser.RawConfigParser() 1552 config.read('config.ini') 1553 1554 if dev.conf != CONF_UNCONF: 1555 answer = input("It appears this hvps-x is already configured. (C)ontinue with configuration or (A)bort?") 1556 if answer == 'a' or answer == 'A': 1557 print("exiting configuration") 1558 dev.close() 1559 exit(-1) 1560 answer = input("Is the hvps-x (U)nipolar or (B)ipolar? ") 1561 print("Setting configuration") 1562 if answer == 'B' or answer == 'b': 1563 dev.s_conf(bipolar=True) 1564 else: 1565 dev.s_conf(bipolar=False) 1566 answer = input("Enter the version of your hvps-x PCB (hardware version), as printed on the PCB " 1567 "(format: X.Y; e.g. 1.2): ") 1568 hw_list = answer.split('.') 1569 if len(hw_list) != 2: 1570 print("Error, the format of the hardware string must be X.Y, e.g. 1.2. Exiting") 1571 exit(0) 1572 dev.set_hardware_version(int(hw_list[0]), int(hw_list[1])) 1573 print("The current name of the hvps-x is " + dev.q_name().decode("utf-8")) 1574 answer = input("Do you want to set/change the name of this hvps-x? (Y)/(N)?") 1575 if answer == 'y' or answer == 'Y': 1576 name = input("Enter hvps-x name (12 char max): ") 1577 if len(name) > 12: 1578 print("Name too long. Ignoring name change") 1579 else: 1580 dev.s_name(name) 1581 else: 1582 print("Leaving name unchanged") 1583 print("\nThe current maximal voltage rating of this HVPS is " + str(dev.q_vmax()) + " V") 1584 answer = input("Do you want to set the maximal voltage of the HVPS? (Y)/(N)") 1585 if answer == 'y' or answer == 'Y': 1586 answer = input("Enter the maximal voltage of the hvps-x in Volt. It must match the voltage rating of the " 1587 "Emco DC/DC converter:") 1588 print("Setting Vmax to " + answer + "V") 1589 dev.s_vmax(int(answer)) 1590 else: 1591 print("Leaving Vmax unchanged") 1592 vmax = dev.q_vmax() 1593 conf_section = 'hvps-x-' + str(vmax) 1594 if config.has_section(conf_section): 1595 print("Default values for this voltage found in configuration file\n") 1596 answer = input("Do you want to replace the values stored in the hvps-x by the one from the config file " 1597 "(Y/N) (choose Y if configuring a new hvps-x)") 1598 if answer == 'Y' or answer == 'y': 1599 c0p = config.getfloat(conf_section, 'C0P') 1600 c1p = config.getfloat(conf_section, 'C1P') 1601 c2p = config.getfloat(conf_section, 'C2P') 1602 c0o = config.getfloat(conf_section, 'C0O') 1603 c1o = config.getfloat(conf_section, 'C1O') 1604 c2o = config.getfloat(conf_section, 'C2O') 1605 1606 dev.s_cal(c0p, CAL_C0P) 1607 dev.s_cal(c1p, CAL_C1P) 1608 dev.s_cal(c2p, CAL_C2P) 1609 dev.s_cal(c0o, CAL_C0O) 1610 dev.s_cal(c1o, CAL_C1O) 1611 dev.s_cal(c2o, CAL_C2O) 1612 print("Voltage calibration values set...\n") 1613 answer = input( 1614 "Reset PID values to their default values? (Y)/(N) (choose (Y) when configuring a board for the first " 1615 "time)") 1616 if answer == 'Y' or answer == 'y': 1617 kpp = config.getfloat(conf_section, 'KPP') 1618 kip = config.getfloat(conf_section, 'KIP') 1619 kdp = config.getfloat(conf_section, 'KDP') 1620 1621 dev.s_pid(kpp, PID_KPP) 1622 dev.s_pid(kip, PID_KIP) 1623 dev.s_pid(kdp, PID_KDP) 1624 print("PID values set...\n") 1625 1626 print("hvps-x configured. It is recommended to perform a calibration of the voltage readout circuit!") 1627 print("Saving information to hvps-x memory. Backup-settings and default settings") 1628 dev.s_settings(SETTINGS_BACKUP) 1629 dev.save() 1630 dev.s_settings(SETTINGS_CURRENT) 1631 dev.save() 1632 else: 1633 print("Cannot find a section in the config file for a {0} V hvps-x. You need to enter values for voltage " 1634 "calibration and PID manually ".format(vmax)) 1635 dev.save() # saves previous parameters anyway
Script to use to for the initial configuration (or re-configuration) of an hvps-x. Follow prompt on the console.
Parameters
- dev: an HVPS object
Returns
nothing
1638def check_functionality(dev): 1639 """This script can be used after configuration to test the hvps-x functionality. Follow prompt on the console 1640 1641 :param dev: an HVPS object 1642 :return: nothing 1643 """ 1644 1645 print("Important:\n" 1646 "This function is experimental and the threshold values are based on a limited number of assembled boards." 1647 "In particular:\n" 1648 "- If the test fails with a value very close to the threshold value, then your board is most likely fine.\n" 1649 "- This version of the script must be used with the bill of material revision 1. If you assembled your" 1650 "board with the original bill of material, this script will not return the correct output. You can use the" 1651 "script included with the library v1.0. Otherwise the website describe how the high-voltage functionality " 1652 "can be manually tested.\n" 1653 "- If unsure, contact Peta-pico-Voltron with test results (be sure to specify hardware version of PCB).\n\n") 1654 if not dev.q_kill(): 1655 input("Place HV safety switch (S1) on position 0 and press any key.") 1656 if not dev.q_kill(): 1657 input("Switch S1 appears not to be working. Check Switch S1 functionality and restart the test") 1658 dev.close() 1659 exit() 1660 print("During the course of this test, a moderate (~20% of full scale) voltage will be applied to the output of\n " 1661 "the hvps-x. Make sure that it nothing is connected to the output and that you are not touching the\n " 1662 "instrument\n") 1663 print("Voltage set point 0V. Output off") 1664 dev.s_vset(0, DCDC_BOTH) 1665 dev.s_sw_mode(SWMODE_OFF) 1666 dev.s_v_mode(VMODE_O) 1667 dev.s_sw_src(SWSRC_TMR) 1668 1669 print("---Testing HV enable switch (S1) ---") 1670 input("Place HV enable switch (S1) on position 1 and press any key") 1671 if dev.q_kill(): 1672 print("S1 switch still reads off state. Check functionality of switch S1.\n Test FAILED. Exiting script...") 1673 dev.close() 1674 exit() 1675 else: 1676 print("***PASS\n") 1677 1678 print("---Testing Voltage monitoring at set point 0 ---") 1679 vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS) 1680 print("Raw reading at the output of the converter is {0} ".format(vnow_p)) 1681 if vnow_p < 5: 1682 print("This is within the tolerance range V < 5. Continuing...") 1683 1684 else: 1685 print("This is outside of the tolerance range V < 5. Something appears to be wrong with the positive DCDC " 1686 "voltage monitoring circuit.\n Test FAILED. Exiting script...") 1687 dev.close() 1688 exit() 1689 1690 print("Raw reading at the output of optocouplers {0} ".format(vnow_o)) 1691 if vnow_o < 5: 1692 print("This is within the tolerance range V < 5. Continuing...") 1693 else: 1694 print("This is outside of the tolerance range V < 5. Something appears to be wrong with the output " 1695 "voltage monitoring circuit.\n Test FAILED. Exiting script...") 1696 dev.close() 1697 exit() 1698 print("***PASS\n") 1699 1700 print("---Testing Voltage monitoring at set point 20% FS Output off ---") 1701 print("Applying 20% PWM value") 1702 dev.s_vset(int(0.2 * dev.vmax), DCDC_POS) 1703 sleep(0.2) 1704 vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS) 1705 print("Raw reading at the output of the converter is {0} ".format(vnow_p)) 1706 if vnow_p > 900: 1707 print("This is within the tolerance range V > 900. Continuing...") 1708 else: 1709 print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC " 1710 "voltage monitoring circuit.\n Test FAILED. Exiting script...") 1711 dev.close() 1712 exit() 1713 1714 print("Raw reading at the output of optocouplers {0} ".format(vnow_o)) 1715 if vnow_o < 5: 1716 print("This is within the tolerance range V < 5. Continuing...") 1717 else: 1718 print("This is outside of the tolerance range V < 5. Something appears to be wrong with the output " 1719 "voltage monitoring circuit.\n Test FAILED. Exiting script...") 1720 dev.close() 1721 exit() 1722 print("***PASS\n") 1723 1724 print("---Testing Voltage monitoring at set point 20% FS Output LOW ---") 1725 print("Output LOW") 1726 dev.s_sw_mode(SWMODE_LOW) 1727 sleep(0.2) 1728 vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS) 1729 print("Raw reading at the output of the converter is {0} ".format(vnow_p)) 1730 if vnow_p > 900: 1731 print("This is within the tolerance range V > 900. Continuing...") 1732 else: 1733 print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC " 1734 "voltage monitoring circuit.\n Test Failed. Exiting script...") 1735 dev.close() 1736 exit() 1737 1738 print("Raw reading at the output of optocouplers {0} ".format(vnow_o)) 1739 if vnow_o < 5: 1740 print("This is within the tolerance range V < 5. Continuing...") 1741 else: 1742 print("This is outside of the tolerance range V < 5. Something appears to be wrong with the output " 1743 "voltage monitoring circuit.\n Test FAILED. Exiting script...") 1744 dev.close() 1745 exit() 1746 print("***PASS\n") 1747 1748 print("---Testing Voltage monitoring at set point 20% FS Output HIGH ---") 1749 print("Output HIGH") 1750 dev.s_sw_mode(SWMODE_HIGH) 1751 sleep(0.2) 1752 vnow_p, vnow_o = dev.q_vnow_raw(VNOW_POS) 1753 print("Raw reading at the output of the converter is {0} ".format(vnow_p)) 1754 if vnow_p > 900: 1755 print("This is within the tolerance range V > 900. Continuing...") 1756 else: 1757 print("This is outside of the tolerance range V > 900. Something appears to be wrong with the positive DCDC " 1758 "voltage monitoring circuit.\n Test FAILED. Exiting script...") 1759 dev.close() 1760 exit() 1761 1762 print("Raw reading at the output of optocouplers {0} ".format(vnow_o)) 1763 if vnow_o > 900: 1764 print("This is within the tolerance range V > 900. Continuing...") 1765 else: 1766 print("This is outside of the tolerance range V > 900. Something appears to be wrong with the output " 1767 "voltage monitoring circuit.\n Test FAILED. Exiting script...") 1768 dev.close() 1769 exit() 1770 print("***PASS\n") 1771 print("***hvps-x passes all tests***") 1772 1773 dev.s_vset(0, DCDC_BOTH) 1774 dev.close()
This script can be used after configuration to test the hvps-x functionality. Follow prompt on the console
Parameters
- dev: an HVPS object
Returns
nothing
1777def transfer_file_to_memory(dev: HVPS): 1778 """Displays a list of json file in the interface folder and gives the user the option to transfer one to the hvps-x 1779 memory 1780 :param dev: an HVPS object 1781 :return: nothing 1782 """ 1783 json_files = list_json_files() 1784 if len(json_files) > 0 and dev != -1: 1785 selected_file = select_file(json_files) 1786 with open(selected_file) as file: 1787 data_dict = json.load(file) 1788 dev.transfer_settings(data_dict) 1789 else: 1790 print("No .json files in the interface root folder")
Displays a list of json file in the interface folder and gives the user the option to transfer one to the hvps-x memory
Parameters
- dev: an HVPS object
Returns
nothing
1801def select_file(files): 1802 print("Available Files:") 1803 for i, file in enumerate(files): 1804 print(f"{i + 1}. {file}") 1805 while True: 1806 try: 1807 choice = int(input("Enter the number corresponding to the file you want to open: ")) 1808 if 1 <= choice <= len(files): 1809 return files[choice - 1] 1810 else: 1811 print("Invalid choice. Please enter a valid number.") 1812 except ValueError: 1813 print("Invalid choice. Please enter a valid number.")
1816def main(): 1817 dev = connect_to_hvps(unconf=True) 1818 if dev: 1819 print("Name: {0}".format(dev.q_name())) 1820 print("Vmax: {0} V".format(dev.vmax)) 1821 print("\n Your choices:") 1822 print("[1] Initial configuration (to be performed after assembling the low-voltage components)") 1823 print("[2] Basic functionality test (to be performed after assembling the high-voltage components)") 1824 print("[3] Transfer a json file to memory") 1825 print("[q] quit\n") 1826 answer = input("Your choice: ") 1827 if answer == '1': 1828 configure(dev) 1829 elif answer == '2': 1830 check_functionality(dev) 1831 elif answer == '3': 1832 transfer_file_to_memory(dev) 1833 dev.close(zero=False)
1836def constrain(val, min_val, max_val): 1837 """A simple implementation to constrain a value between two boundaries""" 1838 return min(max_val, max(min_val, val))
A simple implementation to constrain a value between two boundaries