Демонстрация порта DOOM:

Часть 2: ещё немного про работу софтверной части, DOOM и обновления по воздуху

За месяц, прошедший с прошлой части, я изучил работу механизма обновлений наушников и выпустил утилиту для прошивки по воздуху. Обо всём по порядку.

Тайна BESOTA раскрыта!

Если у вас есть почти любые наушники soundcore / Nothing (CMF), вы могли замечать в диспетчере устройств сервис BESOTA.
На форумах пользователи иногда не на шутку напрягались, что за «бесота» завелась у них в компьютере. Ещё страшнее становилось, когда человек заходил в свойства службы и обнаруживал, что её GUID состоит из одних шестерок...

На самом деле всё намного проще: этот сервис нужен для обновления по воздуху, а его название — сокращение от BEStechnic OTA (а число 6 в китайской культуре считается счастливым).

Я создал скрипт, который успешно прошивает наушники на чипах BES примерно до 2022 года (протокол BESOTA v1). Он выложен на Github под названием besota.

В будущем я планирую добавить в скрипт поддержку BESOTA v2, тем самым обеспечив возможность прошивать по воздуху все существующие SoC BES на данный момент.

Протокол BESOTA v1 работает таким образом:

  1. С наушниками устанавливается соединение, и в сервис BESOTA отправляется пакет инициализации OTA. Этот пакет переводит наушники в состояние, в котором они не могут входить в режим создания пары и сбрасываться до заводских настроек через комбинации кнопок.

  2. В сервис передается информация об отправляемой прошивке: её размер и контрольная сумма CRC32 всего файла. Наушники в ответ отправляют MTU (размер пакета для отправки прошивки, по умолчанию он составляет 656 байт).

  3. После этого отправляется пакет конфигурации, в котором передается адрес, куда нужно будет записать прошивку разделу OTA_BOOT во время стадии COPY_NEW_IMAGE (об этом ниже), но кроме этого в пакете можно передать новое BT и BLE имя, которое запишется во время обновления вместо предыдущего, а также MAC‑адреса для них. В конце пакета должна лежать CRC32, чтобы наушники приняли пакет.

  4. Хост начинает отправлять прошивку, разбитую на эти пакеты, а наушники записывают эти пакеты по смещению newImageOffset.

  5. После передачи файла наушники сначала проверяют CRC32 (от начала прошивки до строки CRC32_OF_IMAGE) из метаданных в конце прошивки, а после этого проверяют CRC32 всего файла, переданную в шаге 2.

    Метаданные прошивки
    Метаданные прошивки
  6. Если все проверки прошли успешно, наушники перезагружаются, далее прошивка записывается разделом OTA_BOOT.

Работа скрипта besota "под капотом"
Работа скрипта besota «под капотом»

Зачем нужен раздел OTA_BOOT?

OTA_BOOT — загрузчик прошивки и одновременно прошивальщик нового обновления.
На BES2300-YP он занимает 0×18000 байт (96 КБ) и находится в самом начале флэш‑памяти (это важная часть, далее будет показано почему). Разберем его работу по шагам, опираясь на реверс‑инжиниринг.

Представим, что мы уже отправили через скрипт / приложение новую прошивку в наушники.
После этого наушники записывают в специальную константу OTA_BOOT_INFO по смещению 0x1000 от начала флэш‑памяти значение 0x5a5a5a5a (в коде это значение называется COPY_NEW_IMAGE). Далее код в OTA_BOOT работает так:

  1. Наушники запускаются начиная именно с раздела OTA_BOOT, перед запуском проверяется OTA_BOOT_INFO.

  2. Если OTA_BOOT_INFO равна 0x1CEC57BE (если присмотреться, можно увидеть BESTECHNIC, написанное leet‑спиком в little‑endian), чип запускается в обычном режиме. Этот режим называется NORMAL_BOOT.

  3. Если же она равна 0x5a5a5a5a, OTA_BOOT копирует новое обновление, которое находится по смещению newImageOffset (в нашем случае это смещение равно 0x180000), по адресу, который передан скриптом (для нас это чаще всего будет 0x18000).

  4. После копирования в 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):

Часть функции app_key_open
Часть функции 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, которая фиксирует нажатия кнопок и выводит их имя в логи:

Нужная часть app_key_handle_process
Нужная часть 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 - огонь
    }
}

Вторая функция добавлена в основной цикл игры, а первая функция — в запуск прошивки, чтобы составить компанию разработчику оригинального порта:

Пасхалка в ините прошивки noapp_main.c
Пасхалка в ините прошивки noapp_main.c

Получившийся форк я назвал DOOMcore и выложил его на Github, а демонстрационное видео находится в начале поста.

Кстати, признаюсь: за всё время, которое я делал проект, я не смог победить кнопку ANC.
Дело в том, что она, во‑первых, назначена на пин P1_6, и оказалось, что это пин UART0_RX, а во‑вторых, эта кнопка срабатывает как кнопка питания. Пока что я не смог понять, как Anker обрабатывают кнопку, поэтому в проекте я не привязал её к каким‑либо действиям, так как в прошивке она залипает в одном состоянии.

Итоги и содержание следующей (финальной) части

Теперь поддерживаемые наушники без разборки можно прошивать пропатченными с помощью моего проекта openqore прошивками, но помимо этого появилась возможность даже поиграть в DOOM на soundcore Q35 (и предположительно Q30, судя по их схожести)!

В финальной части я расскажу про будущее проекта и про кастомную прошивку на основе проекта OpenPineBuds (который сделал мои проекты возможными) от комьюнити pine64.

Небольшой призыв для расширения поддержки наушников

Раз вы дочитали до этого момента, вам наверняка интересен этот проект!

Если вы хотите помочь с тестами, у вас есть наушники на SoC от BES (это, например, наушники soundcore, Nothing / CMF), UART‑адаптер и желание их разобрать, вы можете написать мне в ЛС на Хабре или Telegram (контакт есть в профиле), чтобы мы всё обсудили и смогли добавить в список поддерживаемых устройств новые модели!
При желании я, конечно же, укажу вас в описании проекта, к примеру, как «ВашНик — тестировщик для модели X».

Увидимся в финальной части!

Комментарии (0)