Итоги года: Кому-то приходится яростно сводить баланс, кто-то сваливает погреться поюжнее, а коллеги по теме жгут очередную пачку разноцветных светодиодов на платах естественного цвета ёлочки. Идиллия, вроде.
Так получилось, что в моей домашней лаборатории к концу прошлого года померло двое. Ясно. Нужно вскрытие. Точнее, они не совсем померли, а только начали подавать признаки разложения. Но это - моя опора, так что вскрытие не помешает. Хоть это и не сильно сложная задача, но многим интересно, как там устроены внутренности, и что сними ещё можно сделать. Кто не любит вскрытие? - Только те, которых вскрывают. А их кто-то спрашивал? Скальпель, паяльник, спирт, спирт, спирт, огурец, поехали!
Он сопротивлялся
Первый подопытный - это с виду простой китайский труженик, мультиметр UNI-T UT33B+. По отзывам моих заказчиков весьма надёжная серия. В питерском климате переключатель режимов уже почти 2 года чувствует себя превосходно.

По эргономике я, правда, понял, что отсутствие кнопки "ВКЛ" мне не удобно. Не спеша присматриваю замену, ибо приятная работа, как оказалось, на первом месте. Но на нулевом месте всё-таки надёжность - предыдущий пациент как-то раз внезапно отказался измерять ~230 V, и следующим пациентом мог стать я сам.
Заболел пациент, можно сказать, по естественной причине - отказался пропускать через себя больше 2 ампер вместо положенных 200 mА. Поломка понятна. Но на вскрытии пациент показал наличие тестовых точек на плате с маркировкой TXD и RXD. Я быстро это соотнёс с опытами товарища @tataranovich, и ссылкой оттуда на не менее знаменитый сайт sigrok. Там заинтересовался новым для меня решением для UART-USB в виде чуть более нового чипа CH9329, которым оказался завален китайско-российский импорт. И меня уже было не остановить.
Маленький оффтоп. Как поймать кота
‒ Как проще всего поймать кота?
‒ Положить на землю картонную коробку, открыть её, сидеть ждать.
‒ А как поймать инженера-электронщика?
‒ Нарисовать контактные площадки, подписать их TX и RX, сидеть ждать.

Надпись TXD меня действительно не обманула. Да и sigrok говорит, что большинство спец.микросхем для мультиметров сами по себе передают измеренные данные по UART. Очень похоже, что все эти чипы сейчас - просто более-менее универсальный микроконтроллер с хорошим АЦП, масочной или одноразовой прошивкой, и часто со встроенной EEPROM для калибровки.

Я понадеялся, что UNI-T в другую свою продукцию тоже ставит чипы Cyrustek, но тут я ошибся. Просмотрел всё, что у церроз-этого-тека есть, а также выборочно других кандидатов. Нашёл несколько разных протоколов передачи: кто-то передаёт отдельные цифры прямо в ASCII-тексте, кто-то вообще побитно передаёт состояние сегментов ЖК-индикатора. Но точно 10 байт - ни у кого не нашёл. Есть вероятность, что просто прошивка написана специально для UNI-T.
Ну да ладно, протокол на ноге TXD оказался в итоге максимально простым, и даже выудить оттуда получилось почти всё. А при засылании в RXD долгого BREAK мультиметр перезагружался. Может быть тоже полезно. Но вот засылать что-нибудь осмысленное в RXD я не стал - калибровку можно убить, а переключать режимы я всё равно удалённо не смогу. Пришлось пока отказаться от этой затеи.


В общем, я не буду супер-новатором в соединении мультиметра с компом через USB-UART. Однако расскажу о паре важных моментов, которые приходится учитывать. Во-первых, конечно, гальваническая развязка. Велосипедов нам лишних изобретать не надо, и рекомендованная схема с развязкой есть во всех описаниях чи��ов, что я прочитал.

"Общий" (COM) контакт мультиметра - это не VDD или GND микросхемы, а классическая виртуальная средняя точка. Можете заглянуть в описания по ссылкам. У кого-то это половина питания, у кого-то опорное напряжение. В общем, то, что удобно конкретно этой (ещё понятно, какой!) SoC для измерения биполярного сигнала. И это точно не земля UART. Так что без развязки даже измерить напряжение на какой-нибудь отладочной плате, питаемой от USB - уже не получится.
Не получится без костылей. Мультиметром, оказывается, можно точно измерить напряжение собственной батарейки. Но я не скажу, как - догадайтесь сами, пишите в комментах. Моё решение: батарейный отсек вскрывать нужно, но корпус мультиметра - нет.
Второй ахтунг разработчика я только предполагал, но по расчётам в итоге он не внёс существенного ухудшения в работу мультиметра. Суть в том, что прибор пока всё-таки питается от батареек, а их менять чаще, чем в оригинале, мне бы не хотелось. Хитрое включение, которое я приводил в предыдущей статье, бережёт энергию приёмника, а не передатчика. Я боялся, что опторазвязка по стандартной схеме сожрёт немало энергии. Но холодный рас��ёт сказал, что передачи редкие, 2 раза в секунду, и беспокоиться не стоит. В итоге даже по измеренным данным к обычному потреблению тока в 1,4 mA добавилось не больше 0,1 mA.

Третий момент связан с ограничением допустимой скорости нарастания синфазного напряжения у оптронов (dV/dt в описаниях оптронов, если оно вообще указано). В момент подключения щупов к сети, например, иногда действительно приходит битый пакет. Я даже специально это подловил - под следующим спойлером. На то и контрольная сумма. А для измерений в "летающих" цепях вся эта масса прибора с проводами сама внесёт немалую ёмкость с схему. Только синфазный фильтр, только хардкор.


Отдельная история произошла с разъёмами Type-C, которые я пытался встроить в корпус мультиметра. Ни разу они не маленькие, я вам скажу. А ведь ещё зазор между разъёмом и оригинальными потрохами прибора хотелось бы иметь. Чтобы оно меня потом внезапно не шарахнуло. Я просмотрел много вариантов, и часть из них заказал на пробу. Попадаются разные решения, в том числе без резисторов на контактах CC1 и CC2 - а значит могут не работать с хостом Type-C. Самый мелкий (третий слева на картинке) подошёл по размерам, но из партии 10 шт. по крайней мере 8 шт. оказались бракованными - их забыли пропаять. И я это выяснил, когда после монтажа залил всё клеевым пистолетом. Словарь литературного русского языка пришлось оставить на какое-то время в стороне.

В общем, не мытьём, так катанием получилось собрать связку мультиметр-развязка-UART-USB. Нужный режим микросхемы CH9329 получилось зашить через USB с помощью информации и софта, любезно слитого вот сюда (сработало через VirtualBox+Win7). Вроде ещё есть прошивальщик, работающий через зад, то есть со стороны UART. Выставил скорость 2400 bps, признак окончания пакета 100 ms. А также произвольный код PID и VID (из списка типа устаревших), чтобы потом USB-устройство найти программно.
Засекаю в списке новое устройство, и запускаю заветный sudo cat /dev/hidraw1 . При чтении устройство возвращает пакет, фактически HID-дескриптор. У микросхемы CH9329 он 64-байтный, начинается с указания длины полезных данных (10). Содержит нужные мне 10 байт сообщения от мультиметра, а остальное - мусор из буфера. Запись в порт я тоже проверил - работает.
Значит, пора включить доступ к VID/PID в /etc/udev/rules.d/ , и писать код.
Для начала - поиск нужного устройства.
#!/usr/bin/python3 import os class ch9329: ''' Работа с функцией USB-UART микросхемы CH9329 в Linux ''' default_vid = 0x1A86 default_pid = 0xE429 @classmethod def find( cls, vid=None, pid=None, single=True ): ''' Ищет похожее USB-устройство по идентификатору. Возвращает его имя для open(), или список имён при single=False ''' if vid is None: vid=cls.default_vid if pid is None: pid=cls.default_pid searchstr = 'v{0:08X}p{1:08X}'.format( vid, pid ) #print(searchstr) prefix='/sys/class/hidraw' suffix='device/modalias' devlist = [] for hidraw in os.listdir(prefix): #print(hidraw) with open( prefix + '/' + hidraw + '/' + suffix, 'rt' ) as modalias: devlisttr = modalias.readline() #print(devlisttr) if devlisttr.find( searchstr ) >= 0: devlist.append( '/dev/' + hidraw ) #print( devlist ) if single: if len(devlist)==0: return None if len(devlist)==1: return devlist[0] raise OverflowError('Много штук {0:04X}:{1:04X} !!!'.format(vid,pid)) else: return devlist def __init__( self, name=None, vid=None, pid=None ): self.name=name self.vid=vid self.pid=pid self.dev=None def __enter__( self ): if self.name is None: self.name = ch9329.find( vid=self.vid, pid=self.pid, single=True ) if self.name is None: raise FileNotFoundError('Нет устройства /dev/hidrawX') #print('Opening',self.name) self.dev=open( self.name, 'rb+' ) return self def close( self ): #print('Closing') self.dev.close() def __exit__( self, exc_type, exc_value, traceback ): self.close() self.dev=None return False @classmethod def open( cls, name=None, vid=None, pid=None ): me = ch9329( name, vid, pid ) me.__enter__() return me def read( self ): ''' Читает посылку до 63 байт. ''' raw = self.dev.read(64) if len(raw)==64: pktsize=raw[0] #print('Read',pktsize,'bytes') if pktsize>=0 and pktsize<=63: return raw[1:1+pktsize] raise IOError('Получен непонятный пакет') def write( self, bts: bytes ): ''' Отправляет байты bts. Если размер bts больше 63 байт, разбивает посылку на несколько ''' written=0 todo=len(bts) raw = bytearray(64) while todo>0: pktsize=min(todo,63) raw[0]=pktsize raw[1:1+pktsize]=bts[written:written+pktsize] todo=todo-pktsize #print('Write next',pktsize,'bytes,',todo,'remains') if self.dev.write(raw)!=64: raise IOError('Пакет не передан') #break written=written+pktsize #print('Write total',written,'bytes') return written if __name__=='__main__': print( 'default:', ch9329.find() ) print( ' ut33b:', ch9329.find( 0x0CC1, 0x2511 ) ) print( ' mouse:', ch9329.find( 0x09da, 0x8736 ) ) print( ' hub:', ch9329.find( 0x1d6b, 0x0003 ) ) with ch9329() as dev: #dev.write( bytes(range(91)) ) dev.write( bytes(b'1145\n') ) print( dev.read() ) #print( dev.read() )
А потом и декодирование сообщений с мультиметра.
#!/usr/bin/python3 from ch9329 import ch9329 from math import inf class ut33( ch9329 ): max_value = 8971 max_disp = 1999 modes = [ [ 0x0116, '=200mV', 1, 'mV=', 4, 'V'], [ 0x0114, '=2000mV', 0, 'mV=', 3, 'V'], [ 0x0115, '=20V', 2, 'V=', 2, 'V'], [ 0x0111, '=200V', 1, 'V=', 1, 'V'], [ 0x0113, '=600V', 0, 'V=', 0, 'V'], [ 0x0112, '~600V', 0, 'V~', 0, 'V'], [ 0x011a, '~200V', 1, 'V~', 1, 'V'], [ 0x011b, '=1.5V', 3, 'V=', 3, 'V'], [ 0x0119, '=9V', 2, 'V=', 2, 'V'], [ 0x0117, '=12V', 2, 'V=', 3, 'V'], [ 0x011c, '=10A', 2, 'A=', 2, 'A'], [ 0x011e, '=200mA', 1, 'mA=', 4, 'A'], [ 0x011f, '=200μA', 1, 'μA=', 7, 'A'], [ 0x011d, 'diode', 3, 'V=', 3, 'V'], [ 0x010d, '200Ω', 1, 'Ω', 1, 'Ω'], [ 0x010f, '2kΩ', 0, 'Ω', 0, 'Ω'], [ 0x010e, '20kΩ', 2, 'kΩ', -1, 'Ω'], [ 0x010b, '200kΩ', 1, 'kΩ', -2, 'Ω'], [ 0x0107, '20MΩ', 2, 'MΩ', -4, 'Ω'], ] def __init__( self, name=None, vid=0x0CC1, pid=0x2511 ): super().__init__( name, vid, pid ) def read( self, debug=False ): ''' Читает данные из мультиметра UT33B+. В режиме debug=True возвращает аннотацию каждого принятого пакета данных. В режиме debug=False ждёт и возвращает прочитанное значение и единицу измерения. ''' while True: pkt = super().read() if len(pkt)==10: #print(pkt.hex(' ')) # Формат: ab cd MM MM NN NN NN NN SS SS # ab cd - постоянная сигнатура # MM MM - режим работы # NN NN NN NN - показания АЦП (со знаком в доп.коде) # SS SS - контрольная сумма 6 байт MM и NN # все числа начинаются со старшего байта sig = int.from_bytes( pkt[0:2], byteorder='big', signed=False) mode = int.from_bytes( pkt[2:4], byteorder='big', signed=False) adc = int.from_bytes( pkt[4:8], byteorder='big', signed=True) check = int.from_bytes( pkt[8:10], byteorder='big', signed=False) #print( 'sig={0} check={3} mode={1:04x} adc={2:d}'.format( sig==0xabcd, mode, adc, check==sum(pkt[2:8]) ) ) if sig==0xabcd and check==sum(pkt[2:8]): strout = 'Неизвестный режим: {0:04x} adc={1:d} '.format(mode, adc) for sw in self.modes: if sw[0]==mode: fmt = '{0:.' + '{0:d}'.format(sw[2]) + 'f} {1}' #print(fmt) val = adc/pow(10,sw[4]) if abs(adc)>self.max_value: fmt = fmt + ' (совсем переполнение)' if adc>0: val = inf else: val = -inf elif abs(adc)>self.max_disp: fmt = fmt + ' (переполнение)' strout = fmt.format( adc/pow(10,sw[2]), sw[3] ) #print(strout) if not debug: return ( val, sw[5] ) else: strout = 'Неверные значения: sig={0:04x} check={3} mode={1:04x} adc={2:d}'.format(sig, mode, adc, check) else: strout = 'Неправильный пакет: ' + pkt.hex(' ') if debug: return strout if __name__=='__main__': with ut33() as dev: print('Девайс',dev.name,'открыт') while True: print( dev.read() ) #print( dev.read( True ) )
В выводе как раз приведу редкий случай влияния dV/dt - битого пакета при тыкании щупами, куда попало.
1 V~ 1 V~ 41 V~ 197 V~ 227 V~ 226 V~ Неверные значения: sig=abcd check=245 mode=01e2 adc=226 71 V~ 215 V~ 227 V~ 227 V~ 227 V~ 227 V~
Почему в коде два лимита показаний мультиметра? По ходу тестирования всего этого хозяйства выяснилось: у АЦП микросхемы мультиметра вовсе не 2000 дискретных уровней каждой полярности, а целых 8972. Хоть сам прибор на штатном дисплее и показывает перегрузку выше цифры 1999, но по крайней мере ещё 5000 дополнительных отсчётов сверху и снизу, получаемых через UART, похожи на линейный диапазон. Пока только грубо проверял, сравнивая с показаниями лабораторного БП. Наверное, можно придумать более точные способы измерить линейность, и в ближайшее время я попробую.
Почему производителем установлен такой лимит? Не могу сказать, но могу предположить. Например, у подопытного специфицируется погрешность не лучше 0,5%. На дисплей-то влезают показания до -9999, но столько цифр не имеют смысла. Хотя относительные измерения, где роль играет именно разрешение, тоже бывают нужны. И мультиметры на 6000 дискретов с такой же погрешностью - не редкость. В общем, пока не идентифицирован чип внутри, это загадка.

После проверки работы при питании от 2,2 до 3,4 В, и теста изоляции прибор в строю. В начале года мне уже попалась работа, где я задействовал автоматизированный сбор данных с этого мультиметра. На очереди был бы ещё один апгрейд схемы - счастливое избавление от батарейки при USB-подключении. Но это уже не так просто. И, похоже, я дозрел до покупки готового решения с большей точностью.
Он меня игнорил
Товарищ осциллограф Hantek DSO4104C. Рабочая лошадка, а точнее, рабочий ослик. До сих пор, правда, не привык к сокращению "ослик". Вот "осцилл" - да, вполне. Ну да ладно, живой язык - это по-любому лучше, чем мёртвый.


Анамнез: тело работает, но по USB перестало подключаться к ПК.
Во имя славы китайских богов (тут должна быть ещё картинка, но я и так перегрузил статью картинками) китайские товарищи много чего предусмотрели. В качестве защиты от непотребств извне товарищи не мелочились на какие-то там защитные диоды, супрессоры то бишь. А просто поставили USB-хаб на отдельной плате. Соединяется это всё с основной платой кабелем SATA - вот это неплохое решение. А супрессоры - это ж прошлый век!

Может эта защита наконец сработала, а может ещё что-то случилось. Свежепочиненный напряжёметр, например, показал на выходе регулятора 3,8 В вместо 3,3 В. Короче, плата эта сдохла полностью.

Ясен палец, в первом приближении я убрал хаб, и на свой страх и риск просто замкнул USB между разъёмом и процессором. Ну, как обычно, соплёй закорачивают предохранители. Только это чуть более высокотехнологичная сопля. И да, оно заработало. Но с особенностями.
Ещё раз воспою славу китайским богам (про картинку я уже писал, не буду повторять) - этот прибор, в отличии от многих других проприетарных поделий поддерживает общепринятый протокол управления SCPI. Через транспорт USB TMC, который аж в далёком 2003 году стандартизировали. Для тех, кто не в курсе - это как флешка и мышка, не надо никаких "спец. драйверов", чтобы ОСь увидела устройство. Управлять прибором можно как угодно, хоть через echo && cat в терминале, команды для любого осцилла будут примерно одинаковыми, а особенности документированы.
И вот, ОСь таки его увидела. Отпрыска она назвала уникальным именем /dev/usbtmc0, и в общем ничего не предвещало беды. Я запустил скрипты, которыми с этого-же осцилла снимал данные раньше, и... Нет. (тут должен быть запрещённый площадкой непереводимый русский фолклор). Не совсем.
Код максимально тупой благодаря USB TMC
#!/usr/bin/python3 class usbtmc: def __init__(self, name: str): self.dev = open( name, 'r+b', buffering=0 ) def __del__(self): try: self.dev.close() except: pass def send(self, cmd: str): print('>',cmd) self.dev.write(bytes(cmd,'ascii')) def recv(self, cmd: str) -> bytes: print('>',cmd+'?') self.dev.write(bytes(cmd+'?','ascii')) ans = self.dev.read(200) bens = ans.decode( encoding='ascii', errors='replace' ) print('<',bens) return bens try: osc = usbtmc('/dev/usbtmc0') except: osc = usbtmc('/dev/usbtmc1')
Первый - это логика подключения и общения с прибором. Второй - установка режима всех каналов. Есть ещё захват данных и загрузка паттерна в генератор. Но выглядят они уж сильно костыльно, и я их пока стесняюсь показывать. Хотя работают.
#!/usr/bin/python3 from usbtmc import * vertical = 1.0 horizontal = 1e-3 osc.recv('*IDN') # Вот он где, источник проблем osc.send('*RST') osc.recv('*OPC') for c in range(1,3): prefix = ':CHANNEL{}'.format(c) osc.send(prefix+':DISP ON') osc.send(prefix+':BWL ON') osc.send(prefix+':COUP DC') osc.send(prefix+':PROB 10') osc.send(prefix+':SCAL {}'.format(vertical)) osc.recv('*OPC') osc.send(':CHANNEL3:OFFSET -3.5') osc.send(':CHANNEL4:PROB 100') osc.recv('*OPC') osc.send(':ACQ:TYPE HRES') osc.send(':TIM:SCAL {}'.format(horizontal)) osc.send(':TRIG:MODE EDGE') osc.send(':TRIG:EDGE:SOURCE CHANNEL1') osc.send(':TRIG:EDGE:LEVEL {}'.format(vertical)) osc.recv('*OPC')
Для тех, кто не в курсе: SCPI - это текстовый протокол управления измерительными приборами, грубо говоря аналогичен HTTP. Из тех же 1980х годов. Просто вместо GET /trig/mode?=xyz там передают :TRIG:MODE xyz . Логика примерно та же. Работает поверх чего угодно - TCP, USB, RS232, ну, и через свой собственный, но уже сильно устаревший интерфейс GPIB.
SCPI-команды прибор воспринимает, да. Устанавливает режимы, захватывает сигнал, передаёт его мне, всё как надо. Но не реагирует на единственную команду - идентификация *IDN. По стандарту прибор должен отвечать на неё своими именем, фамилией и серийным номером. Но он молчит. Самое странное, что (чисто случайно) поставив между ПК и осциллографом внешний USB-хаб я таки увидел ответ. С хабом - работает, без хаба - нет.
Так вот зачем внутри осцилла нужен был USB-хаб! Кстати, действительно, а зачем? Этот момент я до конца не понял. Возможно, дело в задержке между USB-запросом и ответом, которую хаб проглатывает, а ПК - нет. Проверить у меня тогда не получилось, поскольку сам осцилл в отключке на операционном столе, а его переносной земляк и собрат от FNIRSI не так быстр для USB 2.0.
Я уж было понадеялся просто поставить обычную защиту на USB, и закрыть эту тему. Лёгкого пути не предвиделось, и я решил: Раз уж всё менять, так не поставить ли мне сразу вместе с хабом и гальваническую развязку? Дополнительные возможности, защита, и места внутри прибора столько, что ещё один такой же влезет. А что? Да начнётся настоящий танец с граблями!

Для начала я взял обычный аудиофильский USB-изолятор на основе старого доброго ADUM3160. В моём случае он не отменяет наличие хаба, но какой-то простой хаб откопать в запасниках было не сложно, да и надо с чего-то начинать. Это - цифровой изолятор, заточенный под сигналы USB 1.1 LS и FS, конкретный режим выбирается джампером. А вы чего хотели?
Для начала собрал цепочку ПК-хаб-изолятор-осцилл. Хаб и осцилл в этой цепочке стандартно начинают работать в режиме USB 1.1, и рапортуют ОСи, что они оба вообще-то 2.0. ОСь переключает их из 1.1 в 2.0, но не тут то было - изолятор не пропускает траффик 2.0 480 Мбит/с. ОСь трижды ругается в логах, что, дескать, кабель у вас - ну, не очень, но в конце догадывается оставить осцилл в режиме 1.1.
И тут в софте у осцилла обнаруживается ещё один косячок - в режиме 1.1 он рапортует ОСи, что умеет слать длинные USB-пакеты по 512 байт. Что разрешено для 2.0, но запрещено в 1.1. ОСь и хаб понимают, что эта лажа совсем не по фен-шуям, но даже как-то работают.
Переставил хаб, и цепочка стала ПК-изолятор-хаб-осцилл. Тут уже хаб возмутился: Не могу, говорит, я оставить соединение сверху 1.1, а снизу включить 2.0. Какая боль! Может быть есть другие хабы и драйвера, которые так могут, но моя связка не заработала совсем. Ясно. Нахожу изолятор 2.0 на ADUM3166. Вдруг нахожу ещё более крутую штуку на основе CH318 сразу вместе со всем фаршем!

Товарищи из WCH пока не умеют делать цифровые изоляторы в больших количествах. Но поняли, что это не единственный способ развязки. Для изоляции можно поставить и обычные сигнальные трансформаторы. А если перекодировать данные, и сразу обвешать всё это хабами, разрешив задержку - то трансформатора достаточно одного!

Дождался я заказа, запихал это всё в осциллограф, дополнительно заизолировал разъём от корпуса знаменитой синей изолентой. Проверил изоляцию тем же методом, что и у мультиметра. И в таком виде оно всё заработало без вопросов.



А, самое-то главное: я ж ещё в мультиметре убавил звук перепайкой резистора. А в осциллографе - забыл. Этот осцилл при получении команды *RST сбрасывает настройки, и при этом вспоминает про подзвучку кнопок, чтоб его. Напрашивается на повторное вскрытие.
Эпилог
Если копнуть поглубже, можно найти как мусор, так и сокровища. В общем, изучаю возможности, нахожу как откровенно плохие, так и очень интересные решения, пробую делать лучше.
Статья получилось на мой взгляд длинноватой. Может, отзывов комментариях посоветуйте, имеет ли смысл убрать что-то, хотя-бы под спойлер, или (не дай Бог!) добавить.
