К основному контенту

Python: Использование PyVISA для калибровки цифровых мультиметров

Python: Использование PyVISA для калибровки цифровых мультиметров

В зависимости от объема методики поверки (калибровки) у современных мультиметров бывает более 200 поверяемых точек. При наличии подходящего оборудования, с интерфейсами RS-232 или USB, возможно написать программу для автоматизации процесса калибровки, тем самым упростив эту процедуру.
В качестве калибраторы применяется Fluke 5500E (подключенный через RS-232(COM1)):
Рис.1
А в качестве примера мультиметр Keysight 34461A (подключенный через USB):
Рис.2
Для связи с приборами, поддерживающими стандарт VISA, мы будем использовать библиотеку PyVISA, позволяющую нам общаться с приборами используя различные интерфейсы, такие как GPIB, RS-232, USB и Ethernet.
Команда для установки: pip install pyvisa
Для составления протокола в excel необходима библиотека openpyxl.
Команда для установки: pip install openpyxl
Команда для установки PIL: pip install pillow
Также необходимо установить библиотеку VISA отдельно от PyVISA. Вы можете скачать эту библиотеку от National Instruments или от Keysight (IO Libraries).
В процессе идентификации, если прибор подключен по USB, Вы увидите следующий VISA адрес (например в Connect Expert от Keysight): USB0::0x0957::0x0A07::MY53002107::0::INSTR. В этой строке заложена информация о приборе:
0x0957 – производитель флешь памяти, в данном случае Agilent Technologies (в новых моделях уже значится 0x2A8D, что означает Keysight Technologies)
0x0A07 – тип прибора, в данном случает мультиметр 34401A (0x1301, 0x1401 – мультиметр 34461A, 0x1F01 – генератор N5183A, 0x5707 – генератор 33622A,0x5418 – измерители мощности N1913A и другие)
MY53002107 – заводской номер
Для удобства мы сделали пользовательский интерфейс средствами стандартной библиотеки Tkinter:

Рис.3
В первых двух полях нужно указать определившиеся приборы: мультиметр и калибратор. После нажатия Connect в полях ID выведутся идентификационные номера приборов, состоящие из типа, заводского номера и версии прошивки.
Для того чтобы программа определяла только типы приборов три раза разбиваем VISA адрес на составляющие (начало, разделитель, конец) с помощью метода partition:

# -*- coding: utf-8 -*-
import visa#подключаем библиотеку visa
rm = visa.ResourceManager()
rg1 = rm.list_resources()#запрос кортежа подключенных портов
rg2 = list(rg1)#преобразование кортежа в список
def pribor():#функция запускается при старте GUI
    v=len(rg1)#кол-во элементов кортежа
    i=-1
    while i < v-1:#цикл разбиения и формирования нового списка
        i=i+1#начинаем с 0-ого эл-та (i=0; rg[0])
        b1=rg1[i]#i-й эл-т кортежа
        b1, b2, b3 = b1.partition('::')#разбиение VISA адреса на части
        b4, b5, b6 = b3.partition('::')
        b7, b8, b9 = b6.partition('::')
        b10, b11, b12 = b9.partition('::')
        if b1 == 'ASRL1':
            rg2[i] = 'COM1'#новые эл-т списка
        if b1 == 'ASRL2':
            rg2[i] = 'COM2'
        if b7 in ('0x1301', '0x1401'):
            rg2[i] = '34461A'
    combo1.configure(values=rg2)#запись новых эл-тов списка в 1-й combobox
    combo2.configure(values=rg2)
В цикл можно добавлять сколько угодно условий для определения типов приборов, в зависимости от того какой у Вас подключен мультиметр, а также сколько угодно COM портов (в данном примере у нас COM1, к которому подключен калибратор Fluke 5500E (5522A) и COM2 к которому подключается мультиметр 34401А не имеющий USB).
Далее создадим класс для идентификации приборов:

class id:
    def __init__(self, type, adr, port, b):
        self.type = type
        self.adr = adr
        self.port = port
        self.b = b
    def connect_d(self):#функция соединение мультиметров
        global a4
        global a7
        global inst_1
        v=len(rg1)
        i=-1
        if combo1.get() == self.type:#условие проверки мультиметра
            while i < v-1:
                i=i+1
                if re.search(self.adr, rg1[i]):#поиск адресов в списке
                    inst_1 = rm.open_resource(rg1[i])
                    if self.port == 'com':#если подключение по COM порту
                        inst_1.write("SYST:REM")
                        time.sleep(1)
                    data_1 = inst_1.query("*IDN?")#запрос *IDN? мультиметра
                    a.set(data_1)
                    a1 = data_1
                    a1, a2, a3 = a1.partition(',')
                    a4, a5, a6 = a3.partition(',')
                    a7, a8, a9 = a6.partition(',')
                    f.set(a4 + ',' + a7 +',' + d + '.xlsx')
                    if a4 in ('34401A', '34410A', '34411A', '34460A', '34461A', '34465A', '34470A'):
                        a10.set('Мультиметр ' + a4 + ' ' + 'подключен')
                        lb.insert(END, a10.get())
                        lb.see(END)
    def connect_f(self):#функция соединение калибратора
        global b4
        global inst_2
        v=len(rg1)
        i=-1
        if combo2.get() == self.type:#условие проверки калибратора
            while i < v-1:
                i=i+1
                if re.search(self.adr, rg1[i]):
                    inst_2 = rm.open_resource(rg1[i], baud_rate = 9600, data_bits = 8, write_termination= '\r', read_termination= '\r')
                    data_2 = inst_2.query("*IDN?")#запрос *IDN? калибратора
                    b.set(data_2)
                    b1=data_2
                    b1, b2, b3 = b1.partition(',')
                    b4, b5, b6 = b3.partition(',')
                    b7, b8, b9 = b6.partition(',')
                    b14.set('Калибратор ' + b1 + ' ' + b4 + ' ' + 'подключен')
                    lb.insert(END, b14.get())
                    lb.see(END)
    def connect_dmm():#функция после нажатия кнопки Connect DMM
        USB1 = id('34411A', r'\b0x0A07\b', 'usb')
        USB2 = id('34461A', r'\b0x1401\b', 'usb')
        COM1 = id('COM1', r'\bASRL1\b', 'com')
        COM2 = id('COM2', r'\bASRL2\b', 'com')
        COM3 = id('COM3', r'\bASRL3\b', 'com')
        USB1.connect_d()
        USB2.connect_d()
        COM1.connect_d()
        COM2.connect_d()
        COM3.connect_d()
    def connect_fluke():#функция после нажатия кнопки Connect Fluke
        COM1 = id('COM1', r'\bASRL1\b', 'com')
        COM2 = id('COM2', r'\bASRL2\b', 'com')
        COM3 = id('COM3', r'\bASRL3\b', 'com')
        COM1.connect_f()
        COM2.connect_f()
        COM3.connect_f()
Для создания класса калибровки используем потоки:
class call(Thread):
    def __init__(self, name, d1, volt1, volt2, cell1, cell2, band, time, accurancy):
        Thread.__init__(self)
        self.name = name
        self.d1 = d1
        self.volt1 = volt1
        self.volt2 = volt2
        self.cell1 = cell1
        self.cell2 = cell2
        self.band = band
        self.time = time
        self.accurancy = accurancy
        self.start()
    def run(self):
        sem.acquire()#семафор для поочередного выполнения потоков
        inst_1.write(self.volt2)#тип измерения мультиметра (DC, AC …)
        inst_1.write(self.band)#полоса измерений мультиметра
        time.sleep(1)
        inst_2.write('*CLS')#очищение калибратора
        inst_2.write(self.volt1)#установка значения на калибраторе
        inst_2.write('OPER')#включение выхода калибратора
        time.sleep(5)
        inst_1.write('READ?')#запрос на чтение показания мультиметра
        time_1 = float(self.time)
        time.sleep(time_1)
        data_3 = inst_1.read()#чтение показания мультиметра
        data_4 = float(self.d1)
        data_5 = float(data_3)
        if self.name == 'cap':#перевод показаний емкости в мкФ для протокола
            data_c3 = (data_5 - data_c2)
            data_c4 = data_c3*10E+8
            data_6 = ((data_c3-data_4)/data_4)*100
            ws[self.cell1] = data_c4
        elif self.name == 'res2':#перевод показаний сопротивления в МОм для протокола
            data_r = data_5/10E+5
            data_6 = ((data_5-data_4)/data_4)*100
            ws[self.cell1] = data_r
        elif self.name in ('dc', 'ac', 'dci', 'aci', 'fr', 'res4'):
            data_6 = ((data_5-data_4)/data_4)*100
            ws[self.cell1] = data_5
        ws[self.cell2] = data_6
        ws['G7'] = a4
        ws['B10'] = a7
        ws['F16'] = h.get()
        ws['F17'] = k.get()
        ws['F18'] = l.get()
        ws['B11'] = m.get()
        data_7 = float(self.accurancy)
        colour = PatternFill(start_color='FFFFDAB9', end_color='FFFFDAB9', fill_type='solid')
        if data_6 > data_7:#подсветка погрешностей в протоколе
            ws[self.cell2].fill = colour
        elif data_6 < -data_7:
            ws[self.cell2].fill = colour
        if a4 == '34401A':#сохранение значений в протокол
            wb.save(protokol+'\\34401A\\'+f.get())
            ws['E97'] = n.get()
            ws['C99'] = e
        elif a4 == '34410A':
            wb.save(protokol+'\\34410A\\'+f.get())
            ws['E289'] = n.get()
            ws['C291'] = e
        elif a4 == '34411A':
            wb.save(protokol+'\\34411A\\'+f.get())
            ws['E289'] = n.get()
            ws['C291'] = e
        elif a4 == '34460A':
            wb.save(protokol+'\\34460A\\'+f.get())
            ws['E121'] = n.get()
            ws['C123'] = e
        elif a4 == '34461A':
            wb.save(protokol+'\\34461A\\'+f.get())
            ws['E124'] = n.get()
            ws['C126'] = e
        elif a4 == '34465A':
            wb.save(protokol+'\\34465A\\'+f.get())
            ws['E337'] = n.get()
            ws['C339'] = e
        elif a4 == '34470A':
            wb.save(protokol+'\\34470A\\'+f.get())
            ws['E337'] = n.get()
            ws['C339'] = e
        inst_2.write('STBY')#выключение выхода калибратора
        time.sleep(1)
        progress1.step(1)
        sem.release()
class reset(Thread):#сброс при переходе на следующий вид измерения
    def __init__(self):
        Thread.__init__(self)
        self.start()
    def run(self):
        sem.acquire()
        time.sleep(2)
        inst_2.write('*RST')
        inst_1.write('*RST')
        time.sleep(2)
        progress1.step(1)
        sem.release()
class message(Thread):#сообщения о переключениях проводов
    def __init__(self, text):
        Thread.__init__(self)
        self.text = text
        self.start()
    def run(self):
        sem.acquire()
        start_thread(q.put(( tkMessageBox.showinfo, ('ВНИМАНИЕ!', self.text), {} )))
        progress1.step(1)
        sem.release()
class cap(Thread):#компенсация проводов при измерении емкости
    def __init__(self):
        Thread.__init__(self)
        self.start()
    def run(self):
        sem.acquire()
        global data_c2
        inst_1.write('CONF:CAP')
        time.sleep(5)
        inst_1.write('READ?')
        time.sleep(5)
        data_c1 = inst_1.read()
        data_c2 = float(data_c1)
        time.sleep(1)
        progress1.step(1)
        sem.release()
Функция после нажатия кнопки старт:
def start():
    global ws
    global wb
    thread = message('Соедините провода для измерения постоянного напряжения')
    thread = reset()
    if a4 in ('34410A', '34411A'):
        wb = load_workbook(shablon+'\\34410A,34411A.xlsx')#выбор шаблона протокола
        ws = wb.active
        progress1.configure(maximum = 221)
        #0.1V
        thread = call('dc', '0.005', 'OUT 0.005 V', 'CONF:VOLT:DC 0.1', 'C28', 'D28', 'DET:BAND 20', '3', '0.075')
        thread = call('dc', '0.05', 'OUT 0.05 V', 'CONF:VOLT:DC 0.1', 'C29', 'D29', 'DET:BAND 20', '3', '0.012')
        thread = call('dc', '0.095', 'OUT 0.095 V', 'CONF:VOLT:DC 0.1', 'C30', 'D30', 'DET:BAND 20', '3', '0.009')
Полный код программы, инструкция по установке и шаблоны протоколов приведены на GitHub. На данный момент калибруются 7 типов мультиметров: Keysight (Agilent) 34401A, 34410A, 34411A, 34460A, 34461A, 34465A, 34470A.
GitHub: 
https://github.com/itllab/DMM

Комментарии