Демонстрация порта DOOM:
Часть 2: ещё немного про работу софтверной части, DOOM и обновления по воздуху
За месяц, прошедший с прошлой части, я изучил работу механизма обновлений наушников и выпустил утилиту для прошивки по воздуху. Обо всём по порядку.
Тайна BESOTA раскрыта!
Если у вас есть почти любые наушники soundcore / Nothing (CMF), вы могли замечать в диспетчере устройств сервис BESOTA.
На форумах пользователи иногда не на шутку напрягались, что за «бесота» завелась у них в компьютере. Ещё страшнее становилось, когда человек заходил в свойства службы и обнаруживал, что её GUID состоит из одних шестерок...
На самом деле всё намного проще: этот сервис нужен для обновления по воздуху, а его название — сокращение от BEStechnic OTA (а число 6 в китайской культуре считается счастливым).
Я создал скрипт, который успешно прошивает наушники на чипах BES примерно до 2022 года (протокол BESOTA v1). Он выложен на Github под названием besota.
В будущем я планирую добавить в скрипт поддержку BESOTA v2, тем самым обеспечив возможность прошивать по воздуху все существующие SoC BES на данный момент.
Протокол BESOTA v1 работает таким образом:
С наушниками устанавливается соединение, и в сервис
BESOTAотправляется пакет инициализации OTA. Этот пакет переводит наушники в состояние, в котором они не могут входить в режим создания пары и сбрасываться до заводских настроек через комбинации кнопок.В сервис передается информация об отправляемой прошивке: её размер и контрольная сумма CRC32 всего файла. Наушники в ответ отправляют MTU (размер пакета для отправки прошивки, по умолчанию он составляет 656 байт).
После этого отправляется пакет конфигурации, в котором передается адрес, куда нужно будет записать прошивку разделу
OTA_BOOTво время стадииCOPY_NEW_IMAGE(об этом ниже), но кроме этого в пакете можно передать новоеBTиBLEимя, которое запишется во время обновления вместо предыдущего, а также MAC‑адреса для них. В конце пакета должна лежать CRC32, чтобы наушники приняли пакет.Хост начинает отправлять прошивку, разбитую на эти пакеты, а наушники записывают эти пакеты по смещению
newImageOffset.-
После передачи файла наушники сначала проверяют CRC32 (от начала прошивки до строки
CRC32_OF_IMAGE) из метаданных в конце прошивки, а после этого проверяют CRC32 всего файла, переданную в шаге 2.
Метаданные прошивки Если все проверки прошли успешно, наушники перезагружаются, далее прошивка записывается разделом
OTA_BOOT.

Зачем нужен раздел OTA_BOOT?
OTA_BOOT — загрузчик прошивки и одновременно прошивальщик нового обновления.
На BES2300-YP он занимает 0×18000 байт (96 КБ) и находится в самом начале флэш‑памяти (это важная часть, далее будет показано почему). Разберем его работу по шагам, опираясь на реверс‑инжиниринг.
Представим, что мы уже отправили через скрипт / приложение новую прошивку в наушники.
После этого наушники записывают в специальную константу OTA_BOOT_INFO по смещению 0x1000 от начала флэш‑памяти значение 0x5a5a5a5a (в коде это значение называется COPY_NEW_IMAGE). Далее код в OTA_BOOT работает так:
Наушники запускаются начиная именно с раздела
OTA_BOOT, перед запуском проверяетсяOTA_BOOT_INFO.Если
OTA_BOOT_INFOравна0x1CEC57BE(если присмотреться, можно увидетьBESTECHNIC, написанное leet‑спиком в little‑endian), чип запускается в обычном режиме. Этот режим называетсяNORMAL_BOOT.Если же она равна
0x5a5a5a5a,OTA_BOOTкопирует новое обновление, которое находится по смещениюnewImageOffset(в нашем случае это смещение равно0x180000), по адресу, который передан скриптом (для нас это чаще всего будет0x18000).После копирования в
OTA_BOOT_INFOзаписывается0x1CEC57BE, и чип перезагружается с новой прошивкой.
Интересный факт: исходный код OTA_BOOT довольно трудно найти даже в слитых SDK.
Я нашёл около 10 версий кода для разных чипов, и только в одной нашлись исходники этого раздела.
Скорее всего, вендорам этот раздел отдают скомпилированным...
Rip & tearrrr... или портирование DOOM
Почему я рассказал про работу раздела обновлений перед портом DOOM?
Дело в том, что по каким‑то причинам без OTA_BOOT наушники не запускают прошивку (предположительно в ROM загрузчике / через eFuse стоят какие‑то проверки или загрузчик инициализирует что‑то важное).
За основу порта DOOM я взял прекрасный проект DOOMBuds, в котором игру успешно портировали на наушники‑вкладыши Pinebuds Pro (они на той же SoC, что и soundcore Q35), но я решил не просто скопировать и запустить этот проект на своих наушниках, а немного улучшить его.
Чего нет на вкладышах, что есть на полноразмерных наушниках? Правильно — кнопок.
Чтобы найти, какая кнопка к какому пину чипа подтянута, нужно немного посмотреть декомпилированный код стоковой прошивки.
К счастью, BES (как и Anker) не умеют вырезать debug‑строки из своих прошивок, но помимо этого ещё и оставляют «бедные» отладочные символы — выводят в логи название функции во время её запуска.
Ищем по строкам функции, которые считывают кнопки, в итоге находим функцию, которая настраивает подтяжку кнопок (app_key_open):

Самая важная для нас часть функции — цикл, где из массива gpio_cfg берутся и настраиваются нужные нам пины кнопок.
Чтобы легче понять работу этой части, я проанализировал код с помощью ИИ. Удалось восстановить карту кнопок и на выходе мы получили такой массив:
3c1504fc 08 00 12 HAL_KEY_CFG 01 00 01 00 00 3c1504fc 08 00 ushort 8h key_code 3c1504fe 12 db 12h pin 3c1504ff 01 db 1h iomux_func 3c150500 00 db 0h irq_type 3c150501 01 db 1h pull_mode 3c150502 00 00 ushort 0h reserved 3c150504 10 00 13 HAL_KEY_CFG 01 00 01 00 00 3c150504 10 00 ushort 10h key_code 3c150506 13 db 13h pin 3c150507 01 db 1h iomux_func 3c150508 00 db 0h irq_type 3c150509 01 db 1h pull_mode 3c15050a 00 00 ushort 0h reserved
pin — GPIO‑пин кнопки в виде hex‑числа, key_code — логическое назначение кнопки, pull_mode — подтяжка.
Но как понять, к какой кнопке какой key_code привязан, и где назначение Play & Pause?
Поискав дополнительные обработчики кнопок в прошивке, находим функцию app_key_handle_process, которая фиксирует нажатия кнопок и выводит их имя в логи:

Переменная local_10 — нужный нам key_code, который мы сопоставляем с таблицей выше и получаем пины кнопок.
Интересно, что кнопка Play & Pause — просто одновременное нажатие Volume Up и Volume Down, и плата наушников при её нажатии фактически «нажимает» эти две кнопки.
Заранее скажу, что кнопка питания на чипах BES всегда находится на одном пине — KEY_CODE_PWR, поэтому нам не нужно её назначать в прошивке, за нас уже всё сделано.
Если перевести обычные hex‑числа в номера пинов (перевести значения в десятичные и поделить на 8 с остатком), мы получим карту кнопок, которую запишем в конфиг кнопок прошивки:
Volume Up — пин
P2_2(18 / 8 = 2, остаток 2)Volume Down — пин
P2_3(19 / 8 = 2, остаток 3)Play & Pause — Vol Up & Vol Down
Теперь у нас есть всё, чтобы собрать порт DOOM для soundcore Q35.
Начнём с небольших изменений конфига.
Так как мы выяснили, что наушники не запускаются без OTA_BOOT, нужно обязательно оставить раздел в начале, а прошивке указать, что она будет загружена не в начало памяти, а по смещению 0×18000. Для этого в конфиге разработчики SDK создали макрос OTA_CODE_OFFSET, с помощью которого мы и указываем смещение:

Далее в исходники уже готового таргета, сделанного для Pinebuds, была добавлена настройка пинов кнопок и функция, которая считывает нажатие кнопок и отправляет в игру выполнение соответствующего действия:
void q35_keys_enable(void) { struct HAL_IOMUX_PIN_FUNCTION_MAP pin_voldown = { HAL_IOMUX_PIN_P2_2, HAL_IOMUX_FUNC_AS_GPIO, HAL_IOMUX_PIN_VOLTAGE_VIO, HAL_IOMUX_PIN_PULLUP_ENABLE }; hal_iomux_init(&pin_voldown, 1); hal_gpio_pin_set_dir(HAL_GPIO_PIN_P2_2, HAL_GPIO_DIR_IN, 1); struct HAL_IOMUX_PIN_FUNCTION_MAP pin_volup = { HAL_IOMUX_PIN_P2_3, HAL_IOMUX_FUNC_AS_GPIO, HAL_IOMUX_PIN_VOLTAGE_VIO, HAL_IOMUX_PIN_PULLUP_ENABLE }; hal_iomux_init(&pin_volup, 1); hal_gpio_pin_set_dir(HAL_GPIO_PIN_P2_3, HAL_GPIO_DIR_IN, 1); } void q35_poll_doom_keys() { int v_down = (hal_gpio_pin_get_val(HAL_GPIO_PIN_P2_2) == 0); int v_up = (hal_gpio_pin_get_val(HAL_GPIO_PIN_P2_3) == 0); int pwr = (hal_key_read_status(HAL_KEY_CODE_PWR) == 1); int fire = (v_down && v_up); if (fire) { v_down = 0; v_up = 0; } static int state_left = 0, state_right = 0, state_up = 0, state_fire = 0; if (v_down != state_left) { state_left = v_down; addKeyToQueue(state_left, KEY_LEFTARROW); // Vol Down - влево } if (v_up != state_right) { state_right = v_up; addKeyToQueue(state_right, KEY_RIGHTARROW); // Vol Up - вправо } if (pwr != state_up) { state_up = pwr; addKeyToQueue(state_up, KEY_UPARROW); // Power - идти вперёд } if (fire != state_fire) { state_fire = fire; addKeyToQueue(state_fire, KEY_FIRE); // Play & Pause - огонь } }
Вторая функция добавлена в основной цикл игры, а первая функция — в запуск прошивки, чтобы составить компанию разработчику оригинального порта:

Получившийся форк я назвал DOOMcore и выложил его на Github, а демонстрационное видео находится в начале поста.
Кстати, признаюсь: за всё время, которое я делал проект, я не смог победить кнопку ANC.
Дело в том, что она, во‑первых, назначена на пин P1_6, и оказалось, что это пин UART0_RX, а во‑вторых, эта кнопка срабатывает как кнопка питания. Пока что я не смог понять, как Anker обрабатывают кнопку, поэтому в проекте я не привязал её к каким‑либо действиям, так как в прошивке она залипает в одном состоянии.
Итоги и содержание следующей (финальной) части
Теперь поддерживаемые наушники без разборки можно прошивать пропатченными с помощью моего проекта openqore прошивками, но помимо этого появилась возможность даже поиграть в DOOM на soundcore Q35 (и предположительно Q30, судя по их схожести)!
В финальной части я расскажу про будущее проекта и про кастомную прошивку на основе проекта OpenPineBuds (который сделал мои проекты возможными) от комьюнити pine64.
Небольшой призыв для расширения поддержки наушников
Раз вы дочитали до этого момента, вам наверняка интересен этот проект!
Если вы хотите помочь с тестами, у вас есть наушники на SoC от BES (это, например, наушники soundcore, Nothing / CMF), UART‑адаптер и желание их разобрать, вы можете написать мне в ЛС на Хабре или Telegram (контакт есть в профиле), чтобы мы всё обсудили и смогли добавить в список поддерживаемых устройств новые модели!
При желании я, конечно же, укажу вас в описании проекта, к примеру, как «ВашНик — тестировщик для модели X».
Увидимся в финальной части!