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

Собственно, это очень большая проблема китайских производителей, китайский чебурнет и практически полное отсутствие вменяемой информации и чипах, чем нас избаловали европейские и американские производители.
Но не беда, распаковываем 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.

Не буду углубляться в структуру контроллера, она намного сложнее, нас интересует графическая и главная управляющая подсистема данных часов.
Это все находится в 4Mb встроенного флеша STM32U5 ядра, и управляется строго согласно документации STMicroelectronics.
Из OTA пакета нам доступны бинарники bootloader и application.
Именно их и предстоит изучить и изменить.
Проблема устройства
Главная проблема этих часов, что компания Xiaomi забила большой болт на мировое сообщество, и не дало в доступ редактор циферблатов, что, собственно, я решил путем реверса оных, и написанием компилятора.
Как результат, мы имеем возможность создавать циферблаты, но, не зная всех тонкостей устройства подсистемы циферблатов часов можем по той или иной причине перегрузить эту самую подсистему, особенно если код ее написан не безопасно, а это так и есть..
В результате чего, можно получить сбойный циферблат, который вызывает сбой всей системы и приложение часов падает в фатальное состояние, и хорошо, если это циклический ребут, хотя в этом тоже мало что хорошего в итоге.
Я и попытаюсь исправить эту ситуацию, чтобы не было все так больно владельцам часов, решивших улучшить жизнь комьюнити или себе, создав несколько циферблатов.
Диагностика проблемы
Благодаря тому, что у пациента есть довольно хорошо документированное ядро, я нашел на плате часов SWD выходы, это конечно отдельная веселая история, и разработчики использовали Segger RTT Logger, что позволяет иметь вывод подсистемы логирования прямо на SWD.
Загрузив сбойный циферблат, я получил такое

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

Это таблица векторов данного процессора, в данном случае мы падаем в не в 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; }
Модификация загрузчика
Считываем значение счетчика
Если значение более 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.

Некоторые версии, такие как Watch S3 и Watch S4 даже имеют бекап прошивки на борту,
но только S4 умеет так делать, а S3 попадает в бесконечный бутлуп.
Надеюсь вам было интересно, а пользователям еще будет и полезно.
Я планирую еще пару улучшений для этой модели и соберу в релиз весьма скоро.
Комментарии (10)

DLiner81
08.03.2026 12:03А как вернуть часы к жизни, если они уже в бутлупе?

m0tral Автор
08.03.2026 12:03Если у вас причина ребута именно циферблат и часы привязаны к MiFitness
(Привязка это крайне важно, потому что канал BLE под шифрованием, ключи при привязке часов хранятся на сервере и часах, оборвали канал = потеряли часы,(сервер часы забыл, а часы нет и поменять возможности нет), что часто пользователи и делают, тыкая во все подряд)
то я создал спец версию программы которая через системный протокол Сяоми отправляет команду перезагрузки в режим рекавери, где можно сбросить настройки к заводским и происходит очистка папки с циферблатами.
Если другая, то только разбор и отладчик

DLiner81
08.03.2026 12:03А как-то можно оживить, если они зависли на логотипе mi? Удержание кнопок и выключение не помогают

m0tral Автор
08.03.2026 12:03какая модель? какая причина смерти? осталась ли привязка в MiFitness
Если это S1 - я доходчиво вроде пояснил, где находятся часы, процессор в стопе - то есть спит - ядро не выполняет никаких операций, разбирать - цеплять программатор и шить, такая работа стоит больше чем остаточная стоимость часов.
NutsUnderline
полагаю часоделам совсем неинтересно давать открытую возможность кастомных циферблатов , очень много лишних хлопот с валидацией, опять же recovery прикручивать то да се Считают, наверное, что это мало кому нужно. Несмотря на это кучу базовых платформ расковыряли именно на предмет создания и заливки циферблатов. И создается впечатление что часы на BES раскопали меньше всего.
BES скажем 2700 любят называть "второй процессор" для часов на wear os интересно как там они совмещают графическую систему да вообще совместную работу, bes там явно кучу всего обеспечивают для режимов повышенной автономности. Но циферблаты вроде бы подсистема android отрисовывает
NutsUnderline
а вот для процессоров SF32 которые ставят в самые бюджетные часы куча открытого, хоть свою прошивку для часов пиши...
m0tral Автор
Проблема этих часов крайне плохой софт, кроме как демо он не годится, у Сяо он довольно хорошо проработан, отличная структура кода, и большая часть его сейчас лежит на GitHub open-vela.
Вообще очень нравится разработка под NuttX, успешно перенес драйвера с mb9 на mb10, тем самым полностью адаптировав прошивку под мб9.
NutsUnderline
зато циферблаты заливаются на раз два без шифроключей и модприложений
я вот тут подумал а удобно было бы циферблатчикам если бы на ПК был софт для заливки циферблатов на часы прямо по bt? без сбрасывания на телефон и применения модов/notify/gadgetbridge Благо протокол раскопан еще и создателями gadgetbridge и доступно в исходниках
я смотрел как это все делается у amazfit/huami/zepp: муторно но вполне реально, был бы ключ
другие платформы в принципе тоже раскопаны для такого, такой заливщик даже сделан для часов на dafit (собственно идея оттуда), но что то заливают циферблаты более кучерявыми способами
m0tral Автор
Я раскопал BES2700 довольно хорошо, и понимаю как там работают блоки, благо части SDK всплывают в сети, ну и уже есть опыт и понимание работы с блоками процов, в новых часах на NuttX с LUA можно читать/писать память напрямую с помощью shell.
Так я сейчас очень быстро нахожу нужные мне gpio например.
m0tral Автор
Они шарят экран очевидно, коммуникация по UART допустим, циферблат там 2х компонентный, на WearOS свое, на bes2700/2800 NuttX со своим, у меня есть OTA пакет Xiaomi Watch 5, там именно так устроено