Анализируем протокол управления блоком питания Finirsi DPS 150
Сегодня я начну рассказывать, как изучал протокол программного управления блоком питания Finirsi DPS 150. До подробного описания всех команд и откликов мы в этот раз, правда, не дойдём, но зато рассмотрим шаги, которые обязательно в итоге приведут нас к успеху. То есть, эта часть — не руководство, как программировать блок, а описание, как вскрывался протокол. Кому это интересно — начинаем.
Введение
В прошлой статье я продвинул идею, что для предотвращения «восстания машин», знаниями и найденными методиками надо обязательно делиться через Хабр. Но при этом сам же давно уже почти ничего не описываю, так как считаю информацию, которая сейчас пробегает через меня, не самой существенной (а существенную злые Заказчики закрывают через соглашение о неразглашении, или по-русски — NDA).
Но надо же исполнять свои слова, так что давайте, пока ещё сильны воспоминания, я упорядочу свои черновики, описав процесс постижения весьма нестандартного протокола управления блоком питания Finirsi DPS 150. Может, кому-то пригодится если не сам протокол, то именно методика, при помощи которой он вскроет что-то подобное. Ну, а может, через годы, кто-то выйдет на статью, когда будет искать сам протокол.
Почему выбрана именно эта модель блока? Мне надо было протестировать одну поделку, как она себя поведёт при разных профилях питающего напряжения. Но при этом, вести проверку надо было, среди прочего, в походных условиях, поэтому источники, работающие от сети 220 вольт, не подходили. Рассматриваемая же модель получает на вход уже постоянку либо с классического круглого разъёма 5,5×2,5 мм, либо с USB Type C по протоколу Power Delivery.
Если взять Power Bank, поддерживающий Power Delivery, можно вполне себе решить мою задачу. Короче, очень удобный источник для стоявшей задачи. Поэтому на нём и остановился. Для работы дома я эти паровозики (220→20→нужное напряжение) не люблю, для дома у меня давно куплен блок, который в розетку втыкается, но как видим, случаи-то разные бывают!
Кстати, раз уж рассматриваем фото, то отмечу забавный факт. Питание идёт по прогрессивному протоколу Power Delivery, а вот для подключения управляющей ЭВМ используется разъём MicroUSB, который нынче многие считают устаревшим. Но это так, лирическое отступление. Зато точно не перепутаем, куда подавать питание, а куда управление.
Эксперименты хотелось провести быстро, так что ждать доставку с Ali Express было некогда. На Озоне же нашёлся вариант с доставкой «прямо завтра». Ну и ладушки. Вот так именно этот блок и оказался у меня. По размеру он примерно с мой фотоаппарат, его внешний вид был представлен на вступительной картинке к статье. У него ещё эффектно откидывается экран, но фотки с откинутым экраном есть в обзорах, а у этой статьи чуть иная задача. Нам надо не внешний вид и штатный функционал изучать, а учиться управлять им из своих программ. Вот и приступаем
Источники знаний о протоколе
Хотелось бы, чтобы протокол был описан в документе от производителя. Увы, такого документа найдено не было. Может, он и существует, но не обнаружен. А жалко.
Не было обнаружено и любительских проектов, управляющих данным источником. Возможно, он слишком новый (надеюсь, именно поэтому, а не потому, что имеет какие-нибудь недостатки, из-за которых на него никто не смотрит).
Зато на сайте производителя, есть программа под Windows, которая этим источником управляет. Мало того, в «продвинутом» режиме у неё есть даже какая-то возможность гонять профили:
Но есть один нюанс: многие кнопки, при нажатии, приводят к такому результату:
Исключение вылетает даже при обычном закрытии окна программы. Так что я не буду рассуждать о том, что мне надо было задавать профиль не по таймеру, как в этой программе, а с привязкой к командам в моё устройство. То, что фирменная программа вылетает — это более весомый аргумент, почему ею нельзя пользоваться, а надо писать свою. Хотя мне всё равно она бы и не подошла. Мне действительно надо было привязывать установку напряжений к вызову функционала моего устройства. Ну, и логи анализа потребляемого тока было проще привязывать к вызванному функционалу, чем потом совмещать по времени.
В общем, фирменная программа, хоть и глючна, а всё равно это огромное подспорье при изучении протокола, по которому она общается с устройством, поэтому черпать вдохновение мы будем из работы с ней, а, как потом выяснилось, и из неё самой.
Что за протокол
Разберёмся, что же за протокол у блока. Может, он какой-то стандартный. Очень многие производители любят делать что-то, похожее на классический GPIB… Вот в том блоке, который у меня от сети 220 вольт работает, реализован именно тот протокол, хоть я его ещё и не изучал. А какой протокол у DPS 150?
При подключении кабеля MicroUSB в системе появляется COM-порт. Ну, хорошо. Запускаем свой любимый сниффер COM-порта. Лично я предпочитаю Bus Hound. Открываем, активируем анализ порта. Дальше запускаем фирменную программу, выбираем в ней порт и нажимаем кнопку «В Сети». Именно так китайские друзья перевели английское слово Online.
Сниффер начинает ловить байтики. Пока я опущу входящие данные, которые бегут непрерывно, раз в 500 мс. Они какие-то пока что непонятные. Нам же надо нажать что-то, заведомо известное и простое, и посмотреть, что улетит в устройство. По умолчанию, в программе почему-то было установлено напряжение 3.5В. Вот я нажимаю на кнопочку «Вниз», чтобы получить 3.49.
Сразу на выход подаётся строка:
Пока что не очень понятно, что это значит. Как минимум, троек в посылке нет (целая часть равна трём вольтам). Но давайте проверим, не передаётся ли напряжение в стандартном вещественном формате. Идём на Гугля и задаём поисковую строку float онлайн (второе слово именно по-русски). Я эту поисковую строку придумал, когда делал лекцию для студентов про упаковку вещественных чисел. Сегодня нам пригодится не сам процесс упаковки, а готовый результат на выходе. Попадаем на русскоязычный сайт http://floatingpoint.ru/online/float2dec.php. Там переходим на страницу Перевод из десятичной системы во float ieee-754 (черновая версия). Вбиваем 3.49, переводим, получаем:
А что у нас было в команде?
С поправкой на переставленные байты — как раз оно, как раз 0×405f5c29.
Аналогично задаётся ток (я проверил, это тоже корректное значение типа float в Little Endian)
Имея эти результаты, я поискал в сети разные протоколы… Иногда этого оказывается достаточно, и можно обнаружить документ, или исходники на ГитХабе. Но в этот раз, я ничего не нашёл. Нет ни описаний, ни каких-либо проектов под этот блок питания. Производители явно не горят желанием этот протокол публиковать, а любители — ещё не освоили его. Ну хорошо, давайте анализировать протокол чуть более детально.
Выделяем сущности протокола
Давайте теперь посмотрим на протокол более широко. Осмотрим также строки, которые идут из устройства в программу. Причём сначала во мне сработала инертность мышления. Я почему-то решил, что раз у нас USB, то и данные худо-бедно разделены на пакеты. Поэтому рассматривал их так, как они выдавались в логе анализатора. Вот последовательность, где каждая транзакция начинается со слова IN или OUT:
Видны структуры, у которых имеется некий трёхбайтный префикс (красное), затем — длина данных (жёлтое выделение), сами данные (серое выделение) и ещё байт (голубое выделение).
Уже позже, я сообразил, что мы же имеем дело с UART-ом! А там данные идут не транзакциями, а потоком! Все эти разделения на псевдопакеты происходят, когда программа обратилась к драйверу за данными. Не надо задумываться, почему в один пакет в логе объединено несколько сущностей! Нет там никаких правил, кроме времени обращения программы к порту. В очередную транзакцию попадёт то, что успело за прошедший временной промежуток накопиться в буфере. Так что сущности выделены корректно, но сам поток данных выглядит чуть проще. Запишем его в реальном виде:
При таком раскладе, голубая сущность наиболее вероятно, относится к контрольной сумме, хоть правило её вычисления пока и не ясно. Но какие правила заполнения у красной сущности? Слишком много неизвестных! А программа от производителя такая заманчивая! Она состоит из одного исполняемого файла! Её так и хочется посмотреть изнутри! Что ж! Займёмся этим!
Команды
Как я уже отмечал тут, с некоторых пор, дизассемблер/декомпилятор Гидра мне нравится больше, чем дизассемблер IDA. Но вот беда, при попытке открыть фирменную программу Гидрой, выясняем, что она .NET-овская. А Гидра .NET не любит. IDA же, не знаю, как сейчас, а в былые времена вскрывала .NET только на уровне ассемблера, что не совсем удобно.
В общем, будем разглядывать прелести программы при помощи инструмента по имени .NET Reflector. Кто не знаком с ним — не бойтесь. Я сам им пользуюсь в третий раз в жизни. Причём из первых двух случаев знаю, что это простейший и интуитивно понятный инструмент. Он относится к разряду декомпиляторов, так что мы будем получать не ассемблерный код, а исходный код на одном из выбранных языков.
Правда, сегодня я выбирать язык не буду. Что автоматически будет выбрано, тем и воспользуюсь. Какая разница, на чём логику восстанавливать? Скриншот я добавлял уже когда причёсывал статью. Оказывается, по умолчанию, был выбран C#. Вполне себе приличный язык!
Загружаем EXE-шник внутрь Рефлектора, видим в дереве объектов что-то сильно нерусское…
Страшно? Мне тоже! Но давайте посмотрим внутрь первой из этих сущностей:
Ага, уже менее страшно. Уже понятней. Даже есть классы для работы с командами (Command) и последовательным портом (Serial). Вот давайте с команд и начнём. Наверняка там найдётся что-нибудь интересное. Поля класса содержат некий список команд.
Уже видно, сколько команд используется программой (позже выяснится, что чуточку больше, но не сильно). Жаль, что не ясно, какая из них для чего. Имена у них совершенно не говорящие. Ну, да ладно. Нам бы сначала с форматом разобраться. Посмотрим на конструктор этого класса. Слева выбираем конструктор в дереве, справа видим его тело:
Уж не те ли это три байта, что идут в начале команды расположены в началах конструкторов объектов CMD_1 и т.д.? Просто зачем-то хитрые авторы размазали команду на три байта…
Мы уже видели в логах, как задавались напряжение и ток:
Очень похоже, но последовательностей 0xf1, 0xb1, 0xc1 и 0xf1, 0xb1, 0xc2 не встречается. Неужели ошибка? К счастью, программа не такая большая, гуляя по её визуальной части, находим:
То есть, именные команды именными командами, но бывают ещё и особые команды, конструируемые прямо в визуальных элементах. Будем иметь в виду, благо их мало. А пока — возвращаемся к командам в целом. Мы выяснили, что самым важным тут является класс CmdModule (Command наследуется от него, а иногда его объекты конструируются налету). Идём в него. Вот такой у этого класса конструктор:
Всё логично. Три странных байта, затем — данные. А где к этому делу добавляются длина и контрольная сумма? Осматриваясь внутри этого же класса, видим:
Прекрасно! Видно, что байт C4 — это как раз длина (считаем с единицы: байты C1, C2, C3 — константы, C4 — длина, именно это мы видели в логах). Теперь мы знаем, что первые три байта — это не совсем разные поля. С точки зрения данной программы, это именно одна 24-битная константа. Почему она такая? Пока не знаю, хоть ниже и предположу.
А вот контрольная сумма (байт C6) вычисляется весьма забавно. Байты C1 и C2 в ней не участвуют! Является ли это хитростью, чтобы не все поняли протокол, или несёт какую-то суть — на момент осмотра не знал. Знаю, что в нашем коде это надо учитывать. Контрольная сумма считается, начиная с байта C3. Опять же, ниже я выскажу предположение, почему это сделано именно так. Но чтобы то предположение сформулировать до конца, надо рассмотреть ответы от устройства, так что сначала займёмся этим.
Ответы от устройства
Что можно узнать начерно про ответы от устройства? Почему там есть ответы с длиной данных 0×04 байта, а есть — целых 0×0c байт? Давайте бегло осмотрим код.
Пробегая по дереву классов, находим наиболее вероятное хранилище:
Ну, явно же поля, пришедшие из COM-порта, тут могут храниться! Data1, Data2 и т.д. Как бы это подтвердить? Перебираем все функции класса и находим вот такую красавицу:
Давайте предположим, что на вход ей приходит буфер. В отличие от команды, где рассматривались три байта, которые именовались c1, c2, c3, тут буфер — это массив. Он индексируется с нуля. Тогда строка:
как раз изымает третий элемент (считаем от нуля) и использует его как длину. То есть, выделяет полезные данные. Отлично. А второй байт (считаем от нуля) содержит идентификатор (аналог команды, но мы же ответы анализируем, так что назовём его идентификатором). Итак, вот нам пришло:
0xf0 и 0xa1 будут проигнорированы рассматриваемым кодом, 0xc3 будет идентификатором, 0×0c — длиной, в buffer2 попадёт тело данных. А что в коде соответствует идентификатору 0xc3?
Single — это псевдоним float, вещественное число одинарной точности. В составе пакета приходит три вещественных числа, которые лягут в переменные Data4, Data5 и Data6. Хорошо. Пока не разбираясь в программе, посмотрим на эти поля:
Извините, но судя по форматированию, это нам явно передаются текущие напряжение, ток и мощность (измеряемые в Вольтах — V, Амперах — A и Ваттах — W). Да, внешнее питание отключено, сейчас в логе всё по нулям.
Проверяем себя
Чтобы убедиться, что перед нами не случайное совпадение, давайте для надёжности попробуем понять, что нам пытаются сообщить в такой посылке:
Смотрим ранее найденный код:
Вещественное число, по смещению 0. На моём любимом (том самом, что ищется по фразе float онлайн) сайте выясняем, что там закодировано +18.7819976806640625. По порядку и примерному значению, я уже понимаю, что перед нами входное напряжение, но давайте отработаем методику, ведь потом будут данные, назначение которых предсказать невозможно. Итак, эти данные попадут в поле Data1. Как описаны функции его записи и чтения?
Точно! Вольты. Но как бы понять, что это именно те самые входные вольты, а не какие-то ещё? Расскажу, как это делается в данном случае. Есть там некоторая вероятность, что методика не сработает. Но ведь у меня же сработала!
Идём в класс Data1 (просто щёлкнув по нему в исходнике). Попадаем сюда:
Видим там в функции set такой паровозик:
То есть, эта переменная попадает в объект userButton113. Щёлкаем мышкой по этому слову, попадаем сюда:
Работа с перекрёстными ссылками
Ничего, за что можно было бы зацепиться! Но сейчас я расскажу про очень мощный метод анализа: ставим курсор на элемент дерева, нажимаем правую кнопку «мыши» и выбираем пункт меню Analyze:
Справа снизу появляется интересное дерево, раскроем его:
Раскроем Used By:
Перед нами список всех мест, откуда кто-то зачем-то обращается к этому объекту. Для UserButton113, таких обращений два. Первое из них — оно не интересное. Мы как раз оттуда пришли. А вот второе… На нём нажимаем правую кнопку «Мыши» и выбираем Go To Member:
Получаем огромнейший текст инициализатора, который заполняет окно элементами. Давайте понадеемся (благо это сработает, я проверил), что перед отображением элемента будет выводиться поясняющий текст. Привычно нажимаем в окне с тем огромным кодом комбинацию Ctrl+F и ищем всё, что связано с userButton113. Сначала будет находиться скукотень, типа такой:
Но через некоторое время найдётся целый блок, настраивающий эту кнопку:
Как я сказал, нас интересует не сама кнопка, а элемент, подписывающий её. Потому что на экране, если не был изменён язык, всё выглядит так:
Вот тот текст и находится выше, элемент label9. Берём фразу输入电压 в буфер обмена и скармливаем её переводчику. Получаем »входное напряжение», что и требовалось доказать.
Поздравляю! Мы только что изучили методику работы с перекрёстными ссылками. Она является основополагающей при анализе кода. В Рефлекторе для её вызова надо вызвать меню Analyze. А затем найти в дереве ветвь Used By. Если перекрёстных ссылок много, ветвь будет большая.
Когда элемент отработан, его можно удалить, выбрав Remove в контекстном меню:
Что дальше
Учитывая, что данных в блоке case не так и много, мы можем относительно быстро вскрыть весь протокол. Желающие могут потренироваться по образу и подобию самостоятельно. Но размер статьи такой, что наверняка читатели уже устали. А уж как устал автор — кто бы знал! Поэтому упорядочивание полученных сведений произведём в следующей части.
Почему контрольная сумма такая удивительная
Напомню, что первые два байта команды не учитываются в контрольной сумме. А давайте предположим, что в составе протокола они адресные. Бывает, что передаётся адрес приёмника и адрес источника. Это характерно для протоколов, идущих по интерфейсу RS485. Там обычно на одной шине висит целая гроздь устройств, и они всегда сообщают, кому передают данные. Но в нашем случае, похоже, передаётся адрес и субадрес. Этот вывод я сделал потому, что данные от программы к устройству идут так:
А от устройства к программе — так:
Если бы были адрес приёмника и источника, то были бы «Юстас Алексу» и «Алекс Юстасу». Первые два байта бы просто менялись местами. А в реальности, второй байт содержит что-то другое.
Напомню, в коде начало списка команд, найденных в конструкторе класса Command выглядит так:
В начале всегда 0xf1. Это может быть адрес приёмника. А у второй половины старший ниббл имеет значения A, B или C, младший ниббл — от нуля до двух.
Первая пара байтов ответа не анализируется, но мы можем проверить их значения в логе. Не буду загромождать текст, первый байт всегда 0xf0 (адрес приёмника?), Второй — тоже AX, BX или CX. Субадрес?
Ну, а F0 — нулевое устройство. ПК. F1 — первое устройство. Вполне логично. Точное назначение субадреса пока не выявлено, но по крайней мере, почему первая пара байтов в посылке не учитывается в контрольной сумме, становится понятно. Они не относятся к данным. Поэтому и не участвуют в расчёте КС.
На шине USB все эти адреса скорее задают совместимость с протоколами, передаваемыми через настоящий UART (а точнее, через RS485). Контрольная сумма, кстати — тоже. На самой шине USB данные защищены при помощи CRC. Но мы не придумываем протокол, мы просто изучаем, как им пользоваться. И теперь мы примерно понимаем, почему всё сделано так, а не иначе. Детальнее нам всё равно не требуется.
Заключение
В статье рассмотрена методика разбора протокола управления программируемым блоком питания FNIRSI DPS150. От простого анализа трафика произведён переход к анализу кода сервисной программы. Выявлен формат команд и возвращаемых данных. Рассмотрена общая методика получения назначения всех возвращаемых данных.
Детализация протокола будет произведена в следующей части статьи.