В последней статье мы настроили FUSB302 на получение сообщений USB PD и успешно получили «оповещение о возможностях» от БП с USB-C. В этой же статье мы обратимся к спецификации PD, спарсим сообщение, после чего создадим ответ, который заставит БП подать максимально возможное напряжение.
Прим. пер.: Продолжение серии статей про USB-C, посвящённой всестороннему анализу этой технологии. Остальные части доступны здесь:
Напомню, как выглядело содержимое буфера:
>>> b
b'\xe0\xa1a,\x91\x01\x08,\xd1\x02\x00\x13\xc1\x03\x00\xdc\xb0\x04\x00\xa5@\x06\x00<!\xdc\xc0H\xc6\xe7\xc6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
Нули в конце могут показаться незначительными, и с вероятностью 99,99% это действительно будет так. Тем не менее не следует просто отбрасывать всю хвостовую часть. Один из байтов в начале кодирует длину сообщения. Первым делом мы будем считывать эти байты и только затем столько, сколько нужно остальных, убеждаясь, что не считываем два сообщения, интерпретируя их как одно, и что не отбрасываем нули, которые являются частью этих сообщений.
Сегодня мы напишем код, который будет парсить сообщения сразу после их считывания из буфера FIFO. Однако держите эти сообщения под рукой для справки, и если у вас нет нужных аппаратных средств, можете попрактиковать на них навыки декодирования. Для тех, кто желает повторить сегодняшний процесс, весь код лежит здесь.
▍ Парсинг заголовка
Первый байт в буфере – это
0xe0
, и по факту он не является частью сообщения PD, которое нам нужно спарсить. Это маркер «start of a message», и его можно найти на стр. 29 спецификации FUSB302 в разделе «RX tokens». Если, заглянув в неё, вы не поймёте, что такое SOP – то для наших целей SOP (без ‘ или “ на конце) означает «этот пакет предназначен для устройства на конце кабеля, а не внутри».Последующие байты уже являются смысловой частью пакета, и именно здесь нужно будет обратиться к спецификации PD.
Заголовок описывается на стр. 109 раздела 6.2.1.1 спецификации PD 3.0. Состоит он из двух байтов – в нашем случае это часть
\xa1a
представления байтового массива Python, 0xa1 0x61
в шестнадцатеричном виде и 0b10100001 0b1100001
в двоичном. Первый байт содержит биты 7-0, а второй – биты 15-8. Можно сказать, что каждая часть сообщения PD идёт наоборот. Нас в первую очередь интересует сегмент между битами 14-12 – возьмите второй байт, сместите его вправо на 4 и примените к нему маску 0b111
для получения длины сообщения. В нашем случае (0x61 >> 4) & 0b111
равно 6.Если длина сообщения равна нулю, значит это сообщение управляющее – они описаны на стр. 119 раздела 6.3 спецификации. В этом примере длина сообщения равна 6. Это не количество байт, а число объектов данных PD, также известных как PDO (объекты данных питания). Каждый из них имеет длину в четыре байта, и в нашем случае соответствует профилю PD. Кроме того, в конце сообщения присутствует CRC, также размером четыре байта. К счастью, CRC нам проверять не нужно – за нас это сделала FUSB302. Если бы CRC оказалась некорректна, микросхема вообще бы не передала сообщение в FIFO для считывания.
Тогда сколько ещё байтов нам нужно считать? Мы уже прочли три, определив, что нужно считать шесть четырёхбайтовых объектов данных, а затем четырёхбайтовую CRC. В общей сложности это сообщение имеет длину 31 байт. Давайте начнём со считывания объектов, после чего считаем CRC и отбросим её. Проще всего будет разом считать четыре байта FIFO – я считала весь PDO и затем в собственной реализации разделила его на сообщения.
▍ Получение профилей мощности
pdo_count = 6
pdos = []
for i in range(pdo_count):
pdo = i2c.readfrom_mem(0x22, 0x43, 4)
pdos.append(pdo)
_ = i2c.readfrom_mem(0x22, 0x43, 4) # отброс CRC
Теперь у нас есть список ещё не спарсенных профилей в
pdos
– для краткости я буду называть их PDO. Здесь будет нелишним написать отдельную функцию для парсинга PDO, хотя бы ради повышения читаемости. Формат сообщения данных описан на стр. 129 раздела 6.4 спецификации. Первым делом при получении PDO мы проверяем тип данных, а именно биты 30-31 или 7-6 последнего байта. Здесь возможно четыре типа – фиксированный (наиболее популярный), питание от батареи или регулируемого источника и Augmented PDO (расширенный PDO). Пока что мы можем ограничиться обработкой фиксированных PDO и безопасно проигнорировать другие типы.
Если вы уже выполнили парсинг PDO, то должны были заметить, что у нас есть пять фиксированных PDO и один расширенный PDO. Отмечу, что это соответствует маркировке на источнике питания, при использовании которого я получила это сообщение. Давайте разберём этот PDO. Откройте таблицу 6-9 на стр. 132. Эта таблица очень удобна и содержит всё, что вам может потребоваться. Теперь спарсим первый PDO.
00101100 10010001 00000001 00001000
Максимальный ток отражён битами 0-9, то есть двумя последними битами байта 1 и далее целым байтом 0. Напряжение отражено битами 19-10 – то есть четырьмя последними битами байта 2 и шестью первыми битами байта 1. Если читать такие величины неудобно, используйте следующий фрагмент кода Python, который парсит PDO. После получения значений напряжения и силы тока умножьте напряжение на 50, а ток на 10. Так вы получите милливольты и миллиамперы соответственно.
>>> 0b0100101100 * 10
3000
>>> 0b0001100100 * 50
5000
Мы получили 3000 и 5000, что, как вы можете догадаться, означает 5 В при 3 А. Соответствующая функция парсинга PDO находится здесь.
▍ Запрос профиля питания
Теперь у нас есть PDO в диапазоне от 5 В до 20 В. Чтобы запросить от БП один из профилей, нам нужно создать сообщение
Request
. Только помните – чтобы реально заставить БП подать повышенное напряжение, необходимо отправить ответ быстро, пока не истечёт время ожидания. Так что далее мы напишем функцию, которая будет создавать ответ и автоматически его отправлять. Это будет четырёхбайтовое сообщение с заголовком из двух байт. Мы создадим список из шести нулей, изменим их на месте и затем отправим. Здесь нам вполне сгодится и такой небрежный вариант:
pdo = [0 for i in range(6)]
.Для начала мы обратимся к спецификации заголовка – теперь нам фактически нужно считать поля заголовка сообщения и установить нужные. Опять же, смотрим стр. 109 раздела 6.2.1.1. Для битов 15-8 (
pdo[1]
) нам нужно только изменить число объектов данных. В нашем случае это 1 – мы отправляем сообщение данных, содержащее один запрос PDO. Для битов 7-0 (pdo[0]
) необходимо установить ревизию спецификации (байты 7-6) на 0b11
. Нам также нужно установить тип сообщения данных в байтах 4-0: смотрите таблицу 6-6 на стр. 128. В нашем случае это сообщение Request
с кодом 0b00010
. Ах да, есть ещё поле Message ID
, которое сейчас можно оставить равным 0, но для последующих сообщений инкрементировать на 1. Это всё, что нужно проделать относительно заголовка. Теперь создадим сам запрос в четырёх оставшихся байтах.Сообщения запросов описываются на стр. 141 раздела 6.4.2 – смотрите таблицу 6-21. Чтобы запросить PDO, нужно знать его индекс и инкрементировать этот индекс на 1 перед отправкой. То есть 5 В @ 3 А – это PDO 1, 9 В @ 3 А – это PDO 2 и так далее. БП с USB-C также должен будет знать максимальный и средний ток, который мы хотим потреблять. Поскольку мы экспериментируем, давайте попросим, например, 1 А, установив максимальный (биты 9-0) и рабочий (биты 19-10) ток на
0b1100100
. Также на всякий случай будет нелишним установить бит 24 (бит 0
в pdo[5]
) для отключения режима USB Suspend.И вот сообщение готово! Однако мы не можем просто отправить его в FIFO. Нам нужно добавить в начале и конце последовательности из двух байтов, которые позволят FUSB302 разобраться в происходящем. Эти последовательности называются SOP и EOP (начало и конец пакета соответственно) – смотрите стр. 29 спецификации FUSB302.
Последовательность SOP имеет длину в пять маркеров и, по сути, передаёт вступление сообщения – три маркера SOP1, один маркер SOP2 и один маркер PACKSYM. Нам нужно OR маркер PACKSYM с длиной нашего сообщения в байтах, которая в данном случае равна шести, сделав его
0x86
. Последовательность EOP – это JAM_CRC, EOP (маркер), TXOFF и TXON. Я не понимаю, почему взяты именно эти последовательности, но рада, что мне доступны опенсорсные стеки, из которых удалось скопировать это поведение. Итак, 0x12 0x12 0x12 0x13 0x86
перед пакетом и 0xff 0x14 0xfe 0xa1
после.Последовательность SOP, пакет, последовательность EOP – всё это помещаем в FIFO и получаем отправленное сообщение
Request
. В целом рабочий поток тут прост – сначала получаем информацию о возможностях БП, затем парсим эту информацию, выбираем нужный профиль, создаём сообщение Request
, отправляем его и получаем нужное напряжение. В чём здесь польза? В том, что можно выбрать нужные параметры мощности.▍ Немного отладки
Если я ничего не упустила, то проверка VBUS покажет, что вы успешно получили профиль 9 В, который мы договорились попробовать. Если у вас возникают какие-то сложности, опять же, вот пример кода Python, который можно использовать. А вот пример коммуникации по I2C для паяльника Pinecil.
На случай возникновения проблем дам ещё несколько советов. Как это обычно бывает в случае отладки, вам помогут инструкции
print()
, но лишь до определённой точки. С одной стороны, они необходимы, особенно, если вы кропотливо относитесь к преобразованию данных в двоичное или шестнадцатеричное представление, в зависимости от того, какое окажется более подходящим в каждый момент отладки. К примеру, вы можете вывести весь пакет в шестнадцатеричном виде, а затем вывести PDO в двоичном, чтобы можно было проверить код парсинга. Для значительной задержки коммуникации много операций вывода не потребуется
С другой стороны, инструкции
print()
будут сильно мешать вписаться в требования тайминга. Отправка данных через консоль занимает очень много времени – даже если это виртуальная консоль, как в случае виртуального моста UART – USB-CDC у RP2040. Я провела около двух часов за отладкой этого кода на RP2040 и постоянно выходила за пределы окна таймаута. В итоге же выяснилось, что причина тому в используемых мной двадцати инструкциях print()
, которые превратили мой код из «очень быстрого» в «слишком медленный для ответа». После того, как я закомментировала эти инструкции, код начал работать на каждом БП, с которым я его пробовала, и мне без проблем удалось добавить множество кастомных уровней напряжения и логики выбора тока.Также будет полезным проверить содержимое буфера получения. После отправки запроса проверьте состояние буфера – также, как мы делали в конце предыдущей статьи. Есть ли в нём ожидающие данные? Если да, считайте из него сообщение и проверьте заголовок – это сообщение
Accept
? Соответствующий код ищите на стр. 119. Если же после отправки сообщения запроса буфер пуст, то вы, скорее всего, нарушаете требования тайминга.С другой стороны, довольно трудно написать такой код MicroPython, который будет достаточно медленным, чтобы выйти в этой ситуации за пределы тайминга. По мере усложнения скрипта может получиться так, что вы разместите слишком много действий между получением PDO и отправкой ответа. Или вы получаете в буфере другой вид сообщения? Ваш БП может отправлять и другие требующие быстрого ответа сообщения – возможно, вы работаете с портом USB-C ноутбука, и ему нужно нечто другое.
▍ 9 вольт получено — что дальше?
Реализованное нами решение по цене сопоставимо с триггерной платой с PD, при этом оно куда более кастомизируемое, вряд ли дороже триггерной микросхемы с PD и явно на порядок круче. В добавок к этому, мы научились считывать и отправлять сообщения PD. Этот навык поможет вам в случае создания каких-то нестандартных девайсов с USB-C. Всё, что потребуется – это микросхема FUSB302 в паре с микроконтроллером, который справится с задачей взаимодействия по протоколу PD – у вас уже может иметься такой в проекте, но занятый чем-то другим.
Код написан на MicroPython. Тем не менее он в достаточной степени является псевдокодом, чтобы его можно было без проблем портировать на другой язык. Если вы используете С++ или С, то ознакомьтесь со стеком IronOS. Есть ещё подходящий для STM32, для Arduino и отличный стек от Microchip. В действии я видела только первый из перечисленных. Так что, если вам не по вкусу MicroPython, то один из этих вариантов наверняка подойдёт.
Вы также могли заметить, что мне нигде не пришлось обращаться к пугающим диаграммам конечных автоматов в USB-C. Технически в этом коде есть несколько состояний, и конечные автоматы вполне сгодились бы для его доработки в случае дальнейшего усложнения. Однако, если вас интересует лишь получение 9 В от источника питания с USB-C, то к их использованию прибегать не придётся. Хотя эти диаграммы помогут вам с отладкой таких явлений, как таймаут в 500 мс между оповещением о возможностях БП и ответом – иначе говоря, бояться не стоит.
Взяв за основу полученный нами результат, вы можете реализовать множество различных решений с USB-C, например превратить БП с круглым разъёмом в БП с USB-C, добавив немного электроники; собрать БП с нестандартными профилями; изучить скрытые возможности контроллеров с PD; получить через порты USB-C вывод DisplayPort. Чёрт возьми, да если вы увлекаетесь пентестингом, то можете даже создать вредоносные гаджеты с USB-C.
Вот вам мой личный простой хак – короткий алгоритм, выбирающий лучший PDO для статичного значения сопротивления с учётом максимальных значений силы тока. Он конкретно ориентирован на сценарий, в котором триггреная плата не справляется. Этот алгоритм отлично впишется в созданный нами код, и если вы хотите собрать высокомощное устройство с USB-C, делающее нечто аналогичное, то он может быть для вас интересен.
Вам следует подходить к работе с USB-C по-хакерски, и эта статья является отличным примером того, что не обязательно разбираться во всех сложностях стандарта USB-C PD для создания полезных решений с его применением – вполне можно обойтись десятью страницами из восьмисот и буквально сотней строк кода.
? Голосуй за нас на премии «ЦОДы РФ»!