def __init__(self, name, serialportno=5): self.serial_handle = UART(name + " serial port", serialportno) self.soft_reset() self.comm_checksum = False self.detect_comm_csum_mode() self.softsoft_reset() self.set_checksum_validation() self.set_ipga() self.set_dc_offset_calibrations(CS5490.CALIB_V_DC_OFFSET, CS5490.CALIB_I_DC_OFFSET) self.set_ac_offset_calibrations(CS5490.CALIB_I_AC_OFFSET) self.set_gain_calibrations(CS5490.CALIB_V_GAIN, CS5490.CALIB_I_GAIN) self.set_phase_compensations() self.set_temp_calibrations() self.set_settle_time(8000) self.set_sample_count(10 * 4000) self.start_conversion() self.name = name
class CS5490(object): """ CS5490 Energy meter """ # Top level commands: REGISTER_READ = 0b00000000 REGISTER_WRITE = 0b01000000 PAGE_SELECT = 0b10000000 INSTRUCTION = 0b11000000 # Instructions: SOFT_RESET = 0b000001 STANDBY = 0b000010 WAKEUP = 0b000011 SINGLE_CONV = 0b010100 CONT_CONV = 0b010101 HALT_CONV = 0b011000 # Calibration instructions: CALIB_GAIN_IV = 0b111110 CALIB_DCOFFS_IV = 0b100110 CALIB_ACOFFS_I = 0b110001 # Do not write to unlisted registers # Page 0 registers CONFIG0 = 0 CONFIG1 = 1 MASK = 3 PC = 5 SERIALCTRL = 7 PULSEWIDTH = 8 PULSECTRL = 9 STATUS0 = 23 STATUS1 = 24 STATUS2 = 25 REGLOCK = 34 V_PEAK = 36 I_PEAK = 37 PSDC = 48 ZX_NUM = 55 # Page 16 registers CONFIG2 = 0 REGCHK = 1 I = 2 V = 3 P = 4 P_AVG = 5 I_RMS = 6 V_RMS = 7 Q_AVG = 14 Q = 15 S = 20 PF = 21 T = 27 P_SUM = 29 S_SUM = 30 Q_SUM = 31 I_DCOFF = 32 I_GAIN = 33 V_DCOFF = 34 V_GAIN = 35 P_OFF = 36 I_ACOFF = 37 # Q_OFF = 38 #????? EPSILON = 49 SAMPLECOUNT = 51 T_GAIN = 54 T_OFF = 55 T_SETTLE = 57 LOAD_MIN = 58 SYS_GAIN = 60 TIME = 61 # Page 17 registers VSAG_DUR = 0 VSAG_LEVEL = 1 IOVER_DUR = 4 IOVER_LEVEL = 5 # Page 18 registers IZX_LEVEL = 24 PULSERATE = 28 INT_GAIN = 43 VSWELL_DUR = 46 VSWELL_LEVEL = 47 VZX_LEVEL = 58 CYCLECOUNT = 62 SCALE = 63 # Hardware configuration SHUNT_R = 0.05 # Ohm VDIV_R1 = 1.5e6 # Ohm VDIV_R2 = 1.8e3 # Ohm MCLK = 4096000 # Hz clock rate # Calibration values CALIB_I_DC_OFFSET = (14, 218, 255) CALIB_V_DC_OFFSET = (31, 44, 254) CALIB_I_AC_OFFSET = (129, 153, 22) CALIB_I_GAIN = (51, 22, 89) CALIB_V_GAIN = (206, 189, 63) # Scale V_MAX = 0.25 * (VDIV_R1 + VDIV_R2) / VDIV_R2 * 2 ** -0.5 # V_RMS (147.5 volt RMS) I_MAX = 4.99 * 2 ** -0.5 # I_RMS P_MAX = V_MAX * I_MAX # P_AVG V_SCALE = 120 / 0.6 # V_RMS I_SCALE = I_MAX / 0.6 P_SCALE = V_SCALE * I_SCALE def __init__(self, name, serialportno=5): self.serial_handle = UART(name + " serial port", serialportno) self.soft_reset() self.comm_checksum = False self.detect_comm_csum_mode() self.softsoft_reset() self.set_checksum_validation() self.set_ipga() self.set_dc_offset_calibrations(CS5490.CALIB_V_DC_OFFSET, CS5490.CALIB_I_DC_OFFSET) self.set_ac_offset_calibrations(CS5490.CALIB_I_AC_OFFSET) self.set_gain_calibrations(CS5490.CALIB_V_GAIN, CS5490.CALIB_I_GAIN) self.set_phase_compensations() self.set_temp_calibrations() self.set_settle_time(8000) self.set_sample_count(10 * 4000) self.start_conversion() self.name = name def get_name(self): return self.name # -~-~-~-~-~-~-~-~-~-~ # # Basic functions # # -~-~-~-~-~-~-~-~-~-~ # def select_page(self, page_no): msg = self.add_comm_checksum((CS5490.PAGE_SELECT + page_no,)) # print "Select page cmnd: ", msg self.serial_handle.send(msg) def read_register(self, register_no, page_no): """ @return tuple with 3 bytes of the register """ self.select_page(page_no) msg = self.add_comm_checksum((CS5490.REGISTER_READ + register_no,)) # print "Read register Cmnd: ", msg self.serial_handle.send(msg) msg = self.serial_handle.read(3 + self.comm_checksum) # print "Read register returns: ", msg register = self.sub_comm_checksum(msg) return register def write_register(self, register_no, page_no, data): """ @data = tuple (or list) of 3 integers 0..256 (or 4 if checksum is True) """ if type(data) == list: data = tuple(data) if not (type(data) == tuple) or len(data) != 3: print "Cannot write to CS5490 register, data wrong format" self.select_page(page_no) msg = self.add_comm_checksum((CS5490.REGISTER_WRITE + register_no,) + data) self.serial_handle.send(msg) return def send_instruction(self, instruction): """ @instruction 6 BIT CS5490 instruction """ msg = self.add_comm_checksum((CS5490.INSTRUCTION + instruction,)) # print msg self.serial_handle.send(msg) def start_conversion(self): self.send_instruction(CS5490.CONT_CONV) def stop_conversion(self): self.send_instruction(CS5490.HALT_CONV) def single_conversion(self): self.send_instruction(CS5490.SINGLE_CONV) def soft_reset(self): msg = (0xFF,) # This one works also when in CSUM mode, without adding the csum self.serial_handle.send(msg) def softsoft_reset(self): # The official, documented, reset self.send_instruction(CS5490.SOFT_RESET) self.comm_checksum = False # -~-~-~-~-~-~-~-~-~-~ # # Check bit registers # # -~-~-~-~-~-~-~-~-~-~ # def temp_updated(self): """ @return True if TUP bit is set in status0 """ status0 = self.read_register(CS5490.STATUS0, 0) return status0[0] & 0b00100000 != 0 def rx_timeout(self): """ @return True if RX_TO bit is set in status0 """ status0 = self.read_register(CS5490.STATUS0, 0) return status0[0] & 0b00000001 != 0 def rx_checksum_err(self): """ @return True if RX_csum_err bit is set in status0 """ status0 = self.read_register(CS5490.STATUS0, 0) return status0[0] & 0b00000100 != 0 def invalid_cmnd(self): """ @return True if IC bit is set in status0 """ status0 = self.read_register(CS5490.STATUS0, 0) return status0[0] & 0b00001000 != 0 def data_ready(self): """ @return True if DRDY bit is set in status0 """ status0 = self.read_register(CS5490.STATUS0, 0) return status0[2] & 0b10000000 != 0 def pivor(self): """ @return a 3 bit number composed POR, IOR, VOR bits in the status0 register any of these bits being one indicates out-of-range of the respective quantities """ status0 = self.read_register(CS5490.STATUS0, 0) return status0[1] & 0b01010100 def ioc(self): """ @return True if I over current """ status0 = self.read_register(CS5490.STATUS0, 0) return status0[1] & 0b00000001 == 1 def tod(self): """ @return True if modulation oscillation has been detected in the temperture ADC (TOD in status1) """ status1 = self.read_register(CS5490.STATUS1, 0) return status1[0] & 0b00001000 != 0 # -~-~-~-~-~-~-~-~-~-~ # # Set bit registers # # -~-~-~-~-~-~-~-~-~-~ # # Do not write "1" to unpublished bits or bits published as "0" # Do notwrite "0" to bits published as "1" def set_ipga(self): """ set the IPGA bit for the correct current channel input gain using the settings from the class constants """ reg = self.read_register(CS5490.CONFIG0, 0) # IPGA = 00 --> 10x gain, max 250 mV_Peak # IPGA = 10 --> 50x gain, max 50 mV_Peak reg = list(reg) if (CS5490.I_MAX * 2 ** 0.5 * CS5490.SHUNT_R) <= 50 / 1000.0: reg[0] = (reg[0] | 0b00100000) & 0b00110100 elif (CS5490.I_MAX * 2 ** 0.5 * CS5490.SHUNT_R) <= 250 / 1000.0: reg[0] = reg[0] & 0b00000100 else: print "The max current is more than the current shunt resistor can handle!" reg[0] = reg[0] & 0b00000100 self.write_register(CS5490.CONFIG0, 0, reg) def set_uart_baudrate(self, baud): reg = self.read_register(CS5490.SERIALCTRL, 0) br = baud * CS5490.MCLK / 524288 brb = self.int_to_bt(br) self.write_register(CS5490.SERIALCTRL, 0, (brb[0], brb[1], reg[2] & 0b00000110)) def set_highpass_filter(self, vhpf=False, ihpf=False): """ Set the high pass filter for voltage (vhpf) and current (ihpf) channels """ reg = self.read_register(CS5490.CONFIG2, 16) reg = list(reg) if vhpf: reg[0] = (reg[0] | 2) & 27 else: reg[0] = reg[0] & 0b00011001 if ihpf: reg[0] = (reg[0] | 8) & 15 else: reg[0] = reg[0] & 0b00000111 reg[1] = reg[1] & 0b01011110 reg[2] = reg[2] & 0b01010000 self.write_register(CS5490.CONFIG2, 16, reg) # -~-~-~-~-~-~-~-~-~-~ # # Get data functions # # -~-~-~-~-~-~-~-~-~-~ # # Instantaneous quantities def get_instantaneous_voltage(self): """ @return the instantaneous voltage """ v = self.read_register(CS5490.V, 16) return self.twoscompl_to_real(v) * CS5490.V_SCALE def get_instantaneous_current(self): """ @return the instantaneous current """ i = self.read_register(CS5490.I, 16) return self.twoscompl_to_real(i) * CS5490.I_SCALE def get_instantaneous_power(self): """ @return the current power calculated from the voltage and current channels """ p = self.read_register(CS5490.P, 16) return self.twoscompl_to_real(p) * CS5490.P_SCALE def get_instantaneous_quadrature_power(self): """ @return the current quadrature power (Q) """ q = self.read_register(CS5490.Q, 16) return self.twoscompl_to_real(q) * CS5490.P_SCALE # Average and RMS quantities def get_rms_voltage(self): """ @return rms value of V calculated during each low-rate interval """ reg = self.read_register(CS5490.V_RMS, 16) return self.bt_to_int(reg) * 2 ** -24 * CS5490.V_SCALE def get_rms_current(self): """ @return rms value of I calculated during each low-rate interval """ reg = self.read_register(CS5490.I_RMS, 16) return self.bt_to_int(reg) * 2 ** -24 * CS5490.I_SCALE def get_average_power(self): """ @return power averaged over each low-rate interval (samplecount samples) """ pavg = self.read_register(CS5490.P_AVG, 16) return self.twoscompl_to_real(pavg) * CS5490.P_SCALE def get_average_reactive_power(self): """ @return reactive power (Q) averaged over each low-rate interval (samplecount samples) """ qavg = self.read_register(CS5490.Q_AVG, 16) return self.twoscompl_to_real(qavg) * CS5490.P_SCALE # Peak quantities def get_peak_voltage(self): """ @return The peak voltage """ v = self.read_register(CS5490.V_PEAK, 0) return self.twoscompl_to_real(v) * CS5490.V_SCALE def get_peak_current(self): """ @return The peak current """ i = self.read_register(CS5490.I_PEAK, 0) return self.twoscompl_to_real(i) * CS5490.I_SCALE def get_apparent_power(self): """ @return The apparent power (S) """ s = self.read_register(CS5490.S, 16) return abs(self.twoscompl_to_real(s)) * CS5490.P_SCALE def get_power_factor(self): """ @return The power factor (ratio of P_avg and S) * sign(P_avg) """ pf = self.read_register(CS5490.PF, 16) return self.twoscompl_to_real(pf) # Total sum quantities def get_sum_active_power(self): """ @return Total active power, P_sum """ p_sum = self.read_register(CS5490.P_SUM, 16) return self.twoscompl_to_real(p_sum) * CS5490.P_SCALE def get_sum_apparent_power(self): """ @return Total apparent power, S_sum """ s_sum = self.read_register(CS5490.S_SUM, 16) return abs(self.twoscompl_to_real(s_sum)) * CS5490.P_SCALE def get_sum_reactive_power(self): """ @return Total reactive power, Q_sum """ q_sum = self.read_register(CS5490.Q_SUM, 16) return self.twoscompl_to_real(q_sum) * CS5490.P_SCALE # Special quantities def get_system_time(self): """ Read onboard counter that is icreaed with 4.096 MHz """ t = self.read_register(CS5490.TIME, 16) return self.bt_to_int(t) def get_temperature(self): """ @return the on-chip temperature """ t = self.read_register(CS5490.T, 16) return self.twoscompl_to_real(t) * 2 ** 7 # -~-~-~-~-~-~-~-~-~-~ # # Set settings # # -~-~-~-~-~-~-~-~-~-~ # def set_settle_time(self, owr_samples=30): """ set the number of Output Word Rate (OWR) (OWR is 4000Hz at 50Hz and 4096MHz) samples that will be used to allow filters to settle at the beginning of converion and calibration commands """ self.write_register(CS5490.T_SETTLE, 16, self.int_to_bt(owr_samples)) def set_sample_count(self, N=100): """ Set the number of output word rate (OWR) samples to use in calculating low-rate results @N integer between 100 and 2**23-1 """ N = int(N) if N > 2 ** 23 - 1: N = 2 ** 23 - 1 if N < 100: N = 100 self.write_register(CS5490.SAMPLECOUNT, 16, self.int_to_bt(N)) return def set_calibration_scale(self, scale): """ Set the SCALE register to scale @scale a number >=0 and <1 """ scale = abs(scale) self.write_register(CS5490.SCALE, 18, self.real_to_twoscompl(scale)) # -~-~-~-~-~-~-~-~-~-~ # # Data type helper fie # # -~-~-~-~-~-~-~-~-~-~ # def twoscompl_to_real(self, twoscompl): """ Transforms three bytes in 2 complement format to a number @bt tuple (or list) of three bytes (LSB,..,MSB) @return a number >-1.0 and <1.0 """ num = self.bt_to_int(twoscompl) if twoscompl[2] & 128 != 0: num -= 2 ** 24 return num * 2 ** -23 def real_to_twoscompl(self, num): """ Transforms a number to three bytes in 2 complement format @twocompl a number >-1.0 and <1.0 @return tuple of three bytes (LSB,..,MSB) in two complements format """ if abs(num) >= 1: print "Too big number to convert in 2-complement format" return self.int_to_bt(int(num * 2 ** 23)) def int_to_bt(self, integer): """ Transforms an integer to 3 bytes Negative numbers will correctly be represented in two's complement form @return tuple of 3 bytes (LSB,..,MSB) """ return (integer & 255, integer >> 8 & 255, integer >> 16 & 255) def bt_to_int(self, bt): """ Transforms 3 bytes to an integer @bt tuple (or list) of 3 bytes (LSB,..,MSB) @return an unsigned integer """ return bt[0] + bt[1] * 2 ** 8 + bt[2] * 2 ** 16 # -~-~-~-~-~-~-~-~-~-~ # # Calibration # # -~-~-~-~-~-~-~-~-~-~ # def do_dc_offset_calibration(self): print " It's assumed no line and no voltage/current are applied to the CS5490" # set T_Settle to 2000ms and SampleCount to a large number self.set_settle_time(8000) # will add 2 sec to the time self.set_sample_count(10 * 4000) # will take 10 sec self.set_highpass_filter(False, False) # set I&V Gain to 1 offset to 0 self.set_gain_calibrations((0, 0, 64), (0, 0, 64)) self.set_dc_offset_calibrations((0, 0, 0), (0, 0, 0)) # perform iv-dc calibration self.send_instruction(CS5490.CALIB_DCOFFS_IV) time.sleep(4) while not (self.data_ready()): print "Calibration in progress..." time.sleep(3) if self.pivor() != 0: print "PIVOR! P,I or V out of range: ", self.pivor() # ToDo put high pass filters back to original state self.set_highpass_filter(False, False) # get and store I & V offset registers i_off = self.read_register(CS5490.I_DCOFF, 16) v_off = self.read_register(CS5490.V_DCOFF, 16) print "V DC OFFSET is: ", self.twoscompl_to_real(v_off), v_off, "I DC OFFSET is: ", self.twoscompl_to_real( i_off ), i_off, "remember to store these values" self.set_gain_calibrations(CS5490.CALIB_V_GAIN, CS5490.CALIB_I_GAIN) def set_dc_offset_calibrations(self, v_off, i_off): """ Set the registers I_DCOFF and V_DCOFF to the values set in the class constants """ self.write_register(CS5490.I_DCOFF, 16, i_off) self.write_register(CS5490.V_DCOFF, 16, v_off) def do_ac_offset_calibration(self): print " It's assumed no line and no voltage/current are applied to the CS5490" self.set_settle_time(8000) # will add 2 sec to the time self.set_sample_count(10 * 4000) # will take 10 sec self.set_highpass_filter(True, True) # set the iac offset channel to zero self.set_ac_offset_calibrations((0, 0, 0)) # perform AC offset calibration # AC offset register will hold the square of the RMS offset self.send_instruction(CS5490.CALIB_ACOFFS_I) time.sleep(4) while not (self.data_ready()): print "Calibration in progress..." time.sleep(3) if self.pivor() != 0: print "PIVOR! P,I or V out of range: ", self.pivor() # ToDo put high pass filters back to original state self.set_highpass_filter(False, False) # get and store I AC offset registers i_acoff = self.read_register(CS5490.I_ACOFF, 16) print "I AC OFFSET is: ", self.bt_to_int(i_acoff) * 2 ** -24, i_acoff, "remember to store this value" def set_ac_offset_calibrations(self, i_acoff): """ Set the register I_ACOFF to the value set in the class constants """ self.write_register(CS5490.I_ACOFF, 16, i_acoff) def do_gain_calibration(self, calib_vac=120.0, calib_r=5000.0): print "A load is assumed to be present: V_RMS = %.2f (V), I_RMS = %.2f (A), R = %d (Ohm), P_RMS = %.2f (W)" % ( calib_vac, 1.0 * calib_vac / calib_r, calib_r, 1.0 * calib_vac ** 2 / calib_r, ) # set I&V _Gain to 1 self.set_gain_calibrations((0, 0, 64), (0, 0, 64)) # set T_Settle to 2000ms and sample count to a big number self.set_settle_time(8000) # will add 2 sec to the time self.set_sample_count(10 * 4000) # will take 10 sec # Set scale register to 0.006 init_scale_setting = 0.6 * calib_vac / (calib_r * CS5490.I_MAX) init_scale_setting = (calib_vac / calib_r) / CS5490.I_SCALE print "INIT_SCALE_SETTING: ", init_scale_setting self.write_register(CS5490.SCALE, 18, self.real_to_twoscompl(init_scale_setting)) # perform gain calibration self.send_instruction(CS5490.CALIB_GAIN_IV) # wait till done, but poll quite often to get an idea of the process time.sleep(3) while not (self.data_ready()): print "Calibration in progress..." time.sleep(3) # check IOR and VOR status bits if self.pivor() != 0: print "PIVOR! P, I or V out of range: ", self.pivor() # get and store I & V Gain registers i_gain = self.read_register(CS5490.I_GAIN, 16) v_gain = self.read_register(CS5490.V_GAIN, 16) print "V GAIN is: ", self.bt_to_int(v_gain) * 2 ** -22, v_gain, "I GAIN is: ", self.bt_to_int( i_gain ) * 2 ** -22, i_gain, "remember to store these values" # redo AC offset calculation (ToDO) def set_gain_calibrations(self, v_gain, i_gain): """ Set the register V_GAIN and I__GAIN with the values from the class constants """ self.write_register(CS5490.V_GAIN, 16, v_gain) self.write_register(CS5490.I_GAIN, 16, i_gain) def set_phase_compensations(self): # set to zero compansation, all errors are accounted for in the gain calibration pass def set_temp_calibrations(self): self.write_register(CS5490.T_GAIN, 16, (0x00, 0x00, 0x01)) self.write_register(CS5490.T_OFF, 16, (0x00, 0x00, 0x00)) return # ToDo calibration registers checksum check # -~-~-~-~-~-~-~-~-~-~ # # Checksum # # -~-~-~-~-~-~-~-~-~-~ # def set_checksum_validation(self): """ resets bit RX_CSUM_OFF in SerialCtrl to start the checksum validation """ reg = self.read_register(CS5490.SERIALCTRL, 0) self.write_register(CS5490.SERIALCTRL, 0, (reg[0], reg[1], reg[2] & 0b00000100)) self.comm_checksum = True return def reset_checksum_validation(self): """ sets bit RX_CSUM_OFF in SerialCtrl to stop the checksum validation """ reg = self.read_register(CS5490.SERIALCTRL, 0) self.write_register(CS5490.SERIALCTRL, 0, (reg[0], reg[1], (reg[2] | 0b00000010) & 0b00000110)) self.comm_checksum = False return def detect_comm_csum_mode(self): """ figures out the current mode the CS5490 is operating in csum or not sets or resets the instance comm_checksum bit @return True if in csum communication mode """ self.comm_checksum = False # Try communication without checksum reg = self.read_register(CS5490.SERIALCTRL, 0) if len(reg) == 3: if reg[2] & 0b00000010 != 0: self.comm_checksum = False else: print "Checksum error, register: ", reg else: self.comm_checksum = True # Try communication with checksum self.soft_reset() # CS5490 sometimes 'hangs' when errorenous communications were received reg = self.read_register(CS5490.SERIALCTRL, 0) # Check if it works now if len(reg) != 3: print "Cannot figure the checksum status of the CS5490, value of the register: ", reg if reg[2] & 0b00000010 != 0: print "Cannot figure the checksum status of the CS5490, value of the register: ", reg return self.comm_checksum def add_comm_checksum(self, data): """ Adds a checksum byte if comm_checksum is set to True, otherwise leaves data as is @return tuple of 3 or 4 bytes (data + 1 checkum byte) depending on comm_checksum @data tuple (or list) of data bytes """ csum = () if self.comm_checksum: csum = 0xFF for byte in data: csum = (csum - byte) & 0xFF csum = (csum,) return tuple(data) + csum def sub_comm_checksum(self, data): """ Checks if checksum is correct (if comm_checksum = True) and removes the checksum byte if present @return tuple with 3 data bytes @data tuple (or list) of the data returned from the CS5490 """ data = tuple(data) if self.comm_checksum: csum = self.add_comm_checksum(data[0:3]) # With the anoyingly confusing semantic "0:3" when 0,1,2 is meant! if csum[3] != data[3]: print "CS5490 read data checksum error, data: ", data return data[0:3]