Итак, у меня на столе устройство компании 70mai, которую многие знают в России по различным аксессуарам к автомобилю. Но и тут у них получился весьма привлекательный продукт.
Сапфировое стекло, красивые линии, шустрый микроконтроллер, отзывчивый интерфейс, автономное воспроизведение музыки на наушники, и в зависимости от рынка продаж - ключи NFC либо Mastercard для оплаты.

Взглянем немного внутрь системы, а там довольно экзотичный процессор BES2500BP. Чудо инженерной мысли великого Китая, на который конечно же нет никакой документации, как ни в 2021/22 году, так ни сейчас.

BES2500BP
BES2500BP

Собственно, это очень большая проблема китайских производителей, китайский чебурнет и практически полное отсутствие вменяемой информации и чипах, чем нас избаловали европейские и американские производители.

Но не беда, распаковываем OTA пакет и находим нужную прошивку, где "слава" разработчикам, присутствует дебаг информация и, отлично, что они не умеют ее выключать, вдвойне спасибо, если умеют, но не отключают.

Боже, что мы видим, какой то до боли знакомый BSP, STM32, а если точнее STM32U5 Cortex-M33, неужели не только мы умеем перемаркировывать чипы (ладно уж, забегу немного вперед и расскажу, что тут не тупое импортозамещение, а вполне продвинутое, просто позаимствовали ядро у европейской компании, и добавили свое audio, в чем немного преуспели, напомню, производитель BESTECHNICS), заодно видим, что точно 70mai их придумали, и что операционка у нас FreeRTOS

От STM32 еще достался UI от производителя, как видите упоминание TouchGFX, это именно та графическая библиотека, которая здесь используется для отрисовки всего интерфейса часов, в общем то довольно интересная, имеющая неплохой IDE генератор в комплекте.

Устройство прошивки

В данной упаковке BES2500BP сидит комплект ядер STM32U5, полностью заимствованная структура IP блоков, а также Audio/BT/Sensors ARM ядро best1501 - как это коррелирует с названием серий их контроллеров для меня загадка, потому что серии у них BES2500, 2600, 2700 и 2800, к слову, эти процы много где используются в китайских часах, у OPPO, HUAWEI и т.п.
Новые Xiaomi Watch 5 используют BES2800BP в качестве энергосберегающей второй системы, только там все уже покруче - все свое и на NuttX.

memory map
memory map

Не буду углубляться в структуру контроллера, она намного сложнее, нас интересует графическая и главная управляющая подсистема данных часов.
Это все находится в 4Mb встроенного флеша STM32U5 ядра, и управляется строго согласно документации STMicroelectronics.
Из OTA пакета нам доступны бинарники bootloader и application.
Именно их и предстоит изучить и изменить.

Проблема устройства

Главная проблема этих часов, что компания Xiaomi забила большой болт на мировое сообщество, и не дало в доступ редактор циферблатов, что, собственно, я решил путем реверса оных, и написанием компилятора.

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

Я и попытаюсь исправить эту ситуацию, чтобы не было все так больно владельцам часов, решивших улучшить жизнь комьюнити или себе, создав несколько циферблатов.

Диагностика проблемы

Благодаря тому, что у пациента есть довольно хорошо документированное ядро, я нашел на плате часов SWD выходы, это конечно отдельная веселая история, и разработчики использовали Segger RTT Logger, что позволяет иметь вывод подсистемы логирования прямо на SWD.

Загрузив сбойный циферблат, я получил такое

как видно из скриншота, устройство падает в Bus fault
при попытке распарсить сбойный циферблат.


Vector Table
Vector Table

Это таблица векторов данного процессора, в данном случае мы падаем в не в BusFault handler,
а в HardFault.





Вот какая картина в HardFault,

печатается backtrace, что видим выше в выводе отладки, идет запись времени события в раздел boot_info, включаются пины SWD, которые были отключены ранее, и почему их поиск был печальным событием, и выполняется остановка процессора.

Зачем было так делать, хз, в итоге мы имеем веселый труп при любой ошибке софта, в остальных обработчиках такая же картина.

void __noreturn HardFault_Handler()
{
  __get_CPSR();
  cm_backtrace_fault();
  get_datetime_and_save_config();
  enable_SWD_pins();
  mcu_halt(1);
  while ( 1 )
    ;
}

void __fastcall mcu_halt(int mode)
{
  if ( !mode || !get_boot_mode() )
  {
    hal_flash_unlock();
    hal_opt_unlock();
    __disable_irq();
    hal_flash_prefetch_buffer_enable();
    while (1)
      ;
  }
}

Загрузчик же устроен следующим образом

в функции is_ota_update_required() загружается конфиг device_config раздела, который подготавливается основным приложением при загрузчике OTA пакета, функция возвращает флаг - нужно запускать ota, если нужно, запускаются ota таски FreeRTOS в функции ota_run_update()

иначе, проверяется подпись приложения и оно запускается, надеюсь, тут тоже понятно к чему приведет загрузка невалидного приложения.

  memset(hw_ver, 0, sizeof(hw_ver));
  memset(sw_ver, 0, sizeof(sw_ver));
  sprintf(hw_ver, "%d.%d.%d", 1, 3, 119);
  sprintf(sw_ver, "%d.%d.%d", 1, 0, 0);
  setup_app("wt3001_a01_watch_stm32_bootloader", (int)sw_ver, (int)hw_ver);
  RTT_debug(0, "SW:%s HW::%s\n", (const char *)hw_ver, (const char *)sw_ver);
  psram_init();
  if (is_ota_update_required())
    ota_run_update();
  if (is_app_sign_valid())
    jump_to_app();
  RTT_debug(0, "Never Get here!\n");
  while ( 1 )
    ;

Заканчивается все ребутом и возвратом сюда же, далее выполняется проверка подписи приложения и старт. В итоге, даже если процессор выдернуть из зависания, бутлоадер прыгнет обратно в приложение и случится ровно тоже самое.

Теперь давайте посмотрим на причину зависания, циферблаты, и как они хранятся на флешке emmc, которая используется для файлов системы.

Это сами циферблаты, тут их полные копии, как они загружаются из библиотеки, а так же кешированные превью, конфиги циферблатов и их список.

А в папке выше лежит файл ui_setting.conf, он бинарный только, вместо ожидаемого текстового формата, и тут мы видим, id нашего злополучного циферблата, который роняет систему.

Создаем механизм Recovery

Пропущу скучные поиски пути решения, и остановлюсь на главных моментах.

Изучив код, я понял, что проще всего удалить файл конфигурации, так как система автоматически создаст новый, именно в нем хранится id активного циферблата, который система пытается загрузить, по дефолту она прописывает туда id родного, который поставляется в комплекте с прошивкой.

Далее, я разработал схему механизма восстановления, тут довольно все просто.

Модификация приложения

1. В обработчике удаляем код остановки процессора

void __noreturn HardFault_Handler()
{
    cm_backtrace_fault_veneer();
    system_fault_ex_handler();
    get_datetime_and_save_config();
    enable_SWD_pins();
    system_reboot();
}

2. Создаем счетчик сбоев, читаем его значение и инкрементируем при каждом сбое. Сохраняем в сектор Boot_Info счетчик сбоев, это делает родной код save_config.

3. Перегружаем процессор, я использовал сток вызов из кода приложения.

#define DEVICE_CFG_REBOOT_CNT (*(volatile uint32_t*)0x201FB3FC)

__attribute__((used, section(".text.bcode")))
int system_fault_ex_handler()
{
    int reboot_cnt = DEVICE_CFG_REBOOT_CNT;
    if (reboot_cnt == -1)
        reboot_cnt = 0;

    DEVICE_CFG_REBOOT_CNT = ++reboot_cnt;
}

Модификация загрузчика

  1. Считываем значение счетчика

  2. Если значение более 4х, удаляем файл конфигурации ui_setting.conf

Bootloader recovery code
__attribute__((used, section(".text")))
int recovery_run(void)
{
    int fault_counter = DEVICE_CFG_REBOOT_CNT;
    if (fault_counter == -1)
        fault_counter = 0;

    uint8_t fault_reason = get_sw_reset_reason();

    syslog(4, reset_reason, fault_reason, fault_counter);

    if (fault_counter > 4) {

        syslog(4, fault_detected, fault_counter);

        xTaskCreate(
            file_delete_task,           // Task function
            "InitTask",                 // Debug name
            1024,                       // Stack size
            0,                          // No parameters passed to task
            7,                          // Priority (High)
            (void**)0x201E2854          // Memory location for the handle
        );

        vTaskStartScheduler();

        syslog(1, msg_start_fail);
        while (1);      
    }
    return 0;
}

#define FLASH_SECTOR    0x083FE000
#define BLOCK_LEN       0x2000

void file_delete_task(void)
{
    uint32_t data[0x20];

    syslog(4, msg_file_started);
    
    mount(mount_root);
    delay(500);
    file_delete(file_ui_settings);

    syslog(4, msg_file_deleted);

    DEVICE_CFG_REBOOT_CNT = 0;

    platform_flash_read(FLASH_SECTOR, data, sizeof(data));
    data[3] = 0;
    platform_flash_erase(FLASH_SECTOR, FLASH_SECTOR + BLOCK_LEN - 1);
    platform_flash_write(FLASH_SECTOR, data, sizeof(data));

    system_reboot();
}

uint8_t get_sw_reset_reason(void)
{
    uint32_t csr = RCC_CSR;

    if (csr & RCC_CSR_LOWPWRF) return 6;
    if (csr & RCC_CSR_WINWTGF) return 5;
    if (csr & RCC_CSR_INDWTGF) return 4;
    if (csr & RCC_CSR_SOFTRST) return 3;
    if (csr & RCC_CSR_PINRSTF) return 2;
    if (csr & RCC_CSR_OBLRSTF) return 1;
    if (csr & RCC_CSR_BORRSTF) return 0;

    return 0xFF;
}

В итоге получается простой и эффективный механизм восстановления часов от сбойных циферблатов, до этой идеи разработчики додумались, но на более поздних моделях Xiaomi.

Recovery в деле
Recovery в деле

Некоторые версии, такие как Watch S3 и Watch S4 даже имеют бекап прошивки на борту,
но только S4 умеет так делать, а S3 попадает в бесконечный бутлуп.

Надеюсь вам было интересно, а пользователям еще будет и полезно.
Я планирую еще пару улучшений для этой модели и соберу в релиз весьма скоро.