Как обычно, ничто не предвещало веселья. Шла рутинная работа. Надо было освоить работу с ОС Zephyr в контроллере NRF52 на примере забавной платы из семейства «Сяо» (а именно XIAO BLE). Вообще, с этой платой принято работать из среды Arduino, но задача была использовать именно Zephyr, а значит — среду VS Code плагином NRF Connect Plugin. Заказанная плата приехала, к точкам для доступа по SWD был припаян разъём программатора… Потом я немножко похулиганил… В итоге, содержимое флэшки в контроллере было стёрто.

Но что нам стоит восстановить загрузчик? С сайта производителя был скачан актуальный HEX-файл, он был залит в плату… Дальше был собран типовой демо проект Blinky… И вечер перестал быть томным, так как проект не запустился на отладку.

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



Первые шаги, и сразу проблема


Итак. Был создан типовой проект на базе Blinky. Его создают десятки блогеров в своих видео. Но у них всё работает, а у меня не заработало! Уже странно. Но почему бы и нет? Осталось понять, чем мои действия отличаются от типовых. Вроде, я всё делал верно, в рамках той задачи, которую собирался решать. Собирал проект и запускал его на отладку. Но ведь у них-то всё работает!

Хорошо. Давайте присмотримся к проблеме чуть более внимательно. Вот я запускаю проект. Отладчик не выдаёт никаких сообщений об ошибках! Он сообщает, что идёт исполнение кода. Но обычно при входе в функцию main() срабатывает точка останова. А тут – нет.

Надо сказать, что я впервые столкнулся с этим контроллером, поэтому ещё не знал, должен произойти останов или нет, так что для надёжности поставил точку останова на первых строках функции main().



Нет, чуда не произошло. Останов не сработал даже с ней. Но отладчик работает, работает, работает! Мало того, если нажать на паузу, он остановится, но неизвестно где.





Кто виноват?


Я так подробно рассказываю симптомы, потому что они характерны для любого контроллера. Если отладчик работает, но не останавливается в функции main(), значит «прошивка» была запущена, но работа ведётся где-то на этапе инициализации. Так как мы перед началом работ прошивали загрузчик – значит, он запустился, но не нашёл причин, почему следует передать управление основному коду. А раз управление не было передано, то и точки останова в функции main() не сработали. Ни та, что на входе, ни та, что была поставлена мной.

С этими знаниями, осмотримся вокруг. Загрузчик создаёт виртуальный диск. Ну-ка? Ага! Вот он!



Итак, у нас загрузчик получает управление при старте контроллера даже без двойного нажатия на Reset, но не отдаёт его нашей прикладной программе. Осталось понять, почему.

Проверяем адреса


Первое, что приходит на ум – проверить правильность адресов. Допустим, загрузчик хочет передать управление на один адрес, а приложение расположено по-другому. Я с таким уже сталкивался. Давайте осмотрим map-файл нашего приложения.



Есть мнение, что оно расположено по адресу 0x2700.

Немного поиска в сети, и оказывается, что исходный код нашего загрузчика расположен вот здесь:
GitHub — adafruit/Adafruit_nRF52_Bootloader: USB-enabled bootloaders for the nRF52 BLE SoC chips

Из описания следует, что адрес приложения, в зависимости от обстоятельств, может быть 0x26000 или 0x27000. Я специально оперирую терминами запутавшегося новичка, у которого глаза разбегаются. Сейчас-то я знаю, что это за обстоятельства. Но статья же про меня времён разборок. Оцените диапазон возможных адресов, который вылили на нас:



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

:020000040002F8

:10644000B1490200D74902001C0500402005004068
:10645000001002007464020008000020E80100003F
:106460004411000098640200F0010020D8110000DF
:10647000A011000001181348140244200B440C061C
:106480004813770B1B2034041ABA0401A40213101A
:08649000327F0B744411C000BF

Первая строка читается так:

02 – два байта полезных данных в строке,
0000 – для этой строки не используются (а так – это адрес),
04 – тип строки. В ней задаются старшие 16 бит адреса,
0002 – последующие строки будут содержать данные для адресов 0x0002XXXX,
F8 – контрольная сумма строки.

В следующих строках нас интересует только начало. Рассмотрим вторую строку
10 – в строке задаётся 0x10 полезных байт,
6440 – младшие 16 бит адреса. С учётом старшей части, данная строка содержит данные для адреса 0x00026440.

Далее идут сами данные и контрольная сумма. Но для нас главное, что адрес 0x26440 занят телом загрузчика. Ну, и последняя строка соответствует адресу 0x26490.

Поэтому вопрос 0x26000 или 0x27000 закрыт. Не может приложение пользователя располагаться по адресу 0x26000. Там ещё идёт тело загрузчика… Как потом выяснилось, не совсем загрузчика, а SoftDevice, но сейчас нам не до того, нам бы наше приложение разместить. Тело какого-то штатного кода…

В общем, исходя из увиденного, автоматически предложенный нам адрес приложения 0x27000 считаем верным. Он, скорее всего, не приводит к проблемам, из-за которых приложение не получает управления. Да и если бы управление передавалось на неверный адрес, то всё бы падало, и виртуальный диск терял бы работоспособность. Если просто надолго поставить приложение на паузу в отладке – диск отвалится. Нельзя надолго USB-устройство останавливать. Он работает! Значит, просто загрузчик работает и не хочет никому управление отдавать!

Попытка понять логику кода загрузчика с треском провалилась. Он весьма увесистый. Его логика слишком сложна, чтобы пытаться постичь её за короткое время, а много времени тратить я был не готов. Хорошо. Адрес верный, остальное – пытаемся выяснить другим путём.

Успешное решение проблемы при помощи бубна


Вообще, меня очень смущало, что вот я вижу проблему, а поисковики ничего такого не находят. У подавляющего большинства авторов вопросов на форумах, не работает JLINK в принципе. Один автор получил точно такую же проблему, как у меня (ну хоть что-то), но этот автор скомпоновал приложение на адрес 0, и всё заработало. Читер несчастный! Он просто убрал загрузчик. Это не наш метод. Мало того, сейчас я знаю, что в итоге он потерял возможность работы с Bluetooth, так как затёр код SoftDevice. А без Bluetooth можно использовать и контроллеры попроще.

Но у остальных-то пользователей всё работало без проблем!!! Никто не оставался внутри загрузчика!!! КАК??? ПОЧЕМУ???

Возник и у меня соблазн на момент забыть об отладке и начать загружать приложение, как все Ардуинщики и даже некоторые Зефирщики: копируя файл с ним на виртуальный диск. Ничему же не противоречит! Я же только попробую! Вот этот файл! Почему не попробовать-то?



Копирую… Виртуальный диск тут же исчезает, а светодиод на устройстве начинает приветливо моргать, как того требует логика программы Blinky. Ну хоть что-то! Однако, как же проводить JTAG отладку?

А давайте попробуем провести её, когда приложение скопировано на диск? Ой! Пока я тут рассуждал, уже сработала точка останова на входе в функцию! Управление добралось до рабочего приложения!



Дальше начинается магия. Я пробую модифицировать программу и запускать её из среды разработки. Все модификации послушно прошиваются, и трассировка всегда работает! Работает, будто и не было неразрешимых проблем. Это точно работает не то, что я скопировал через файл, а новая залитая версия. Я специально вносил существенные изменения. Все они учитывались!

Магия! Но что это было?

Следствие пошло по неверному пути


Сначала я пошёл по пути наименьшего сопротивления. Что за файл CURRENT.UF2 на виртуальном диске? О-о-о-о! Давайте его откроем!

Вот его начало.



А вот – второй сектор.



Уже отсюда можно предположить, что по адресу 0x0C располагается адрес, на который должно копироваться тело сектора. И да, в секторе как раз 0x100 байт.



Но в целом, незачем разбирать формат интуитивно. Описание файла uf2 легко ищется:
GitHub — microsoft/uf2: UF2 file format specification

Само собой, я сделал конвертор из этого формата в обычный двоичный. Преобразовал файл, когда ничего не работает (но приложение уже залито), и файл, когда приложение уже работает. И оказалось, что различий не так и много:

Сравнение файлов data.bi~ и DATA.BIN
00032D8C: FF 00
00032D8D: FF 00
00032D8E: FF 00
00032D8F: FF 00
00032D90: FF 00
00032D91: FF 00
00032D92: FF 00
00032D93: FF 00
00032D94: FF 00
00032D95: FF 00

00032DF8: FF 00
00032DF9: FF 00
00032DFA: FF 00
00032DFB: FF 00
00032DFC: FF 00
00032DFD: FF 00
00032DFE: FF 00
00032DFF: FF 00

Собственно, последний сектор добит нулями до конца…

Так вот… Не делайте так! Не проверяйте пользовательские данные. Нули – это хорошо, но это – просто технологическая вещь. Поднимем глаза чуть вверх, и увидим, что первый сектор, который описывает данный файл, имеет смещение во флэшке 0x1000:



Что располагается до него (а сейчас я знаю, что там располагается область MBR) – из этого файла не узнать. И что расположено после тела файла – тоже узнать не удастся. А загрузчик и его параметры не входят в это тело. Так что этот файл – не для нашего случая, нам надо более сложное решение, которое накроет весь образ флэшки. Но вот как этот файл накрывает пользовательский код – мне очень понравилось. Надо будет пользоваться им в реальной жизни, тем более, это формат поддерживает сама Microsoft.

Читаем всю область флэша


Стандартный программатор для среды разработки NRF Connect – это J-LINK. Само собой, у меня к плате XIAO тоже подключён J-LINK EDU. Все, кто работал с именно этой модификацией адаптера (EDU), знают, что прекрасная утилита J-FLASH из штатной поставки от разработчика программатора с ним не дружит. Но есть одна тонкость. Она не дружит на запись! А на чтение вполне себе работает! Поэтому запускаем её, выбираем чтение области.



Задаём, скажем, первый мегабайт (то есть, от 0 до 0xfffff):



И всё читается! Считанный дамп сохраняем:



Не забыв выбрать двоичный формат:



Сохранив два дампа (с неработающим и работающим кодом, у меня это файлы full_first.bin и full_working.bin), сравниваем их штатной командой Windows, перенаправив весь вывод в файл full_cmp.txt, для чего в консоли (я использую FAR, но подойдёт и cmd) вводим строку:
fc /b full_first.bin full_working.bin >full_cmp.txt

Собственно, в рабочем файле есть участок, который в нерабочем затёрт FF-ами:



Остальное (ну, кроме того самого занулённого конца сектора пользовательского кода) у этих файлов идентично. Что же это такое?

Постигаем физику проблемы


Итак. У нас есть конкретный адрес, который надо изучить поподробнее. Давайте вобьём его на удачу в поиск по каталогу с загрузчиком. Зря что ли мы его нашли в сети? Ищем строку 0xff000 по всем файлам.



Первый же файл (makefile) показывает, что мы на верном пути, но не раскрывает деталей. У нас же как раз по адресу 0xff000 располагается DWORD 0x00000001:



Строки закомментированы… Но адрес!!! Адрес совпадает! И данные там наши! Отлично. Ищем дальше. И удача улыбается нам. Скрипт компоновщика (файл nrf52840.ld) гласит:
/** Location of bootloader setting in flash. */
BOOTLOADER_SETTINGS (rw): ORIGIN = 0xFF000, LENGTH = 0x1000

Отлично! Теперь ищем по всем файлам строку BOOTLOADER_SETTINGS. И вот она (я опустил несколько скучных косвенностей, это уже финальная):



Уже не теплее! Уже горячее! А что за тип bootloader_settings_t? Вот он!



Та-а-а-а-ак! Накладываем это дело на то, что мы уже видели!



Ещё бы константы 01 и FF раскрыть. Так вот же они! Рядом!



Прекрасно! Перед нами описание таблицы разделов! Сначала раздел «приложение» с занулённой контрольной суммой, потом – раздел «Неверное приложение». А когда флэшка стёрта, оба байта заполнены константой 0xFF. Приложений нет!

HEX-файл, скачанный с сайта, это дело не инициализирует. А при первой же записи на виртуальный диск, всё заполняется. И после этого, всё начинает работать.

Вот почему почти у всех всё работает! Сразу после получения платы, файловая система инициализирована! Это мои шаловливые ручки привели к краху файловой системы, потребовавшему её перезаписи. Если бы я хоть раз скопировал файл, оно бы починилось само… Но я не копировал. И ещё один автор не копировал, но он и разбираться не стал, просто от загрузчика отказался. Потеряв SoftDevice, а значит – возможность работы с Bluetooth. Но это его проблемы.

Теперь причина полного молчания в сети о проблеме понятна. Её получить не так просто. Но если получили – мы уже научились её локализовывать и устранять при помощи бубна. А точнее, единичного копирования файла с расширением uf2 на виртуальный диск.

Но можно ли автоматизировать устранение проблемы? Глобально – нет. Но для себя – да.

Правим фирменный HEX-файл


Почему бы нам не заставить автоматику записывать эту таблицу разделов вместе с основным HEX-файлом? HEX-файл же текстовый, мы его легко можем подправить! Если бы не контрольные суммы, то вообще в текстовом редакторе бы сформировали нужные строки. Но в целом… У нас же есть J-Flash! Давайте заставим его работать на нас!

Вот мы читали мегабайт. А давайте считаем только нужные байты!



Получаем вот такую красоту:



А теперь сохраняем не в двоичном виде, а в виде Intel Hex:



Вот он:



Последняя строчка – это маркер конца. Она нам не интересна. А вот первую и вторую берём в буфер обмена и вставляем до маркера конца в тот HEX-файл, который был скачан с сайта производителя плат XIAO. Например, сюда:



Теперь, если стереть содержимое флэшки в контроллере и прошить такой загрузчик, проблемы уже не будет… Уффф! Проблема решена? Да, но появилась новая проблема. Мы отметили, что есть приложение, но самого приложения не прошили. Но ведь мы сразу же его прошьём, правда? Вот починили плату, и сразу начали её отлаживать, пока снова не испортили. Так что это не страшно. Иначе придётся добавить в тело HEX-файла ещё и какое-то приложение-пустышку. Но это уже – творческая часть, которую каждый сделает сам, если ему это будет нужно.

Заключение


В статье рассмотрена достаточно редкая проблема, связанная с платой XIAO BLE. Однако, по ходу развития, в статье показаны методы выявления случаев не выхода за пределы начального загрузчика во время отладки, которые возникают довольно часто, так что умение замечать их является полезным. Кроме того, показаны методы быстрого и детального устранения конкретной проблемы, которые могут быть применены и в других случаях. Также в статье вскользь упомянут очень интересный файл с расширением uf2. При разработке обновлений «прошивок» через виртуальный диск этот формат позволит как удобно прошивать сектора, так и производить контрольное обратное считывание.

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


  1. datacompboy
    19.06.2024 09:11
    +1

    Если что-то может пойти не так -- оно пойдёт :)

    Спасибо, что поделились рецептом!


  1. dmitryrf
    19.06.2024 09:11
    +1

    Весьма любопытный случай, спасибо!


  1. Mizar91
    19.06.2024 09:11

    в итоге он потерял возможность работы с Bluetooth, так как затёр код SoftDevice

    А что, скачать его с сайта и залить снова уже нереально? Есть какие-то религиозные причины?


    1. EasyLy Автор
      19.06.2024 09:11
      +1

      Залить - проще простого. Я же во время опытов постоянно стирал чип и заливал HEX файл... Но у того товарища он затрёт приложение пользователя. Пока то не будет перекомпоновано на адрес 0x27000 - кто-то кого-то будет перетирать по мере заливки.


  1. KarmaCraft
    19.06.2024 09:11

    Познавательно, спасибо автору!


  1. Nick0las
    19.06.2024 09:11

    А есть какой-то смысл использовать в готовом устройстве Zephyr с этим сторонним загрузчиком всесто mcuboot?


    1. EasyLy Автор
      19.06.2024 09:11

      Это не сторонний загрузчик. Это загрузчик, сделанный на базе штатного примера от NRF52. Который учитывает все особенности этого контроллера. Включая наличие того самого SoftDevice.

      SoftDevice - это код библиотеки, обслуживающей радиомодуль и обеспечивающей работу стека BlueTooth. Все производители уносят его в заранее собранный двоичный код. Подозреваю (но не уверен на все 100%), что это делается, потому что в объектном файле всегда останутся какие-то метки и какие-то имена переменных, а так - всё точно будет скрыто от сторонних глаз. Однажды я читал высказывание на форуме, что без этого не получить сертификат на радиосовместимость. Все подкручивания должны быть скрыты от конечного пользователя. Снаружи должны быть доступны только стандартные номера каналов, и работа с ними должна идти по сертифицированным алгоритмам.

      Поэтому при работе с Bluetooth контроллером NRF52, важно не затереть эту самую "приколоченную гвоздями" библиотеку. Для новых контроллеров, эту библиотеку обязательно надо "прошить" на требуемое место.

      Собственно, та путаница с адресами вытекает как раз из того, что SoftDevcice новой версии стал больше, чем был в предыдущей. Адрес 0x27000 вбит в стандартную конфигурацию платы, которая прописана в свойствах Zephyr, установленного вместе со средой разработки NRF Connect. А сам HEX файл берётся с сайта производителя платы. Какой же он сторонний?


      1. NutsUnderline
        19.06.2024 09:11

        Да тут как минимум две причины чтобы закрывать

        1) Коммерческо-лицензионная - Bluetoth-стэк стоит денег как в разработке так и за лицензии. Никому не захочется чтобы его скопировали и сделали дешевый аналог

        2) Хакерская. Чем больше доступа к каналу тем больше возможностей для всяких шалостей. Хотя делают сейчас с Flipper Zero да и стэк ESP32 пропатчили

        Обе эти истории уже проходили с NRF24 и повторять видать никто не хочет. С Zigbee в общем то та же история, стэк не то чтобы не доступен, но мало кто в него лезет.


      1. Nick0las
        19.06.2024 09:11
        +1

        Я о другом. Zephyr использует свою имплементацию BLE, без softdevice. Поэтому zephyr на NRF52 можно грузить вообще без загрузчика или используя родственный для zephyr MCUboot, что я в свое время и делал. Насколько я понял, вы хотите использовать Zephyr с загрузчиком от Adafruit. Вот и возникает вопрос, зачем?


        1. EasyLy Автор
          19.06.2024 09:11

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

          Я так обрадовался Вашим словам, что полез гуглить... И похоже, нашёл ответ на вопрос "Что мешает?". Мешает то, что работа ведётся через NRF Connect. А про неё (и связанный с нею NRF Connect SDK) я нашёл сейчас вот такие гадости

          bluetooth - Comparison Zephyr vs SoftDevice - Stack Overflow

          Но буду очень рад, если Вы через ссылки или в режиме беседы позволите скинуть эти шоры. Я только рад, если этот SoftDevice станет не нужен. Но чтобы при этом не потерять других вкусностей от производителя. Вкусности - они же полезны.

          Как сказано в начале статьи, я пока только осваиваю это дело. И буду рад любым ПРАВИЛЬНЫМ ссылкам. А то в море информации пока плавать приходится и опытным путём постигать, что полезно, а что - нет.


          1. Nick0las
            19.06.2024 09:11
            +1

            Я работал с Zephyr начиная с версии 1.5 и последняя которую щупал была 2.4. В процессе работы даже один патч им отправил. Zephyr для NRF52 компилируется вместе с вашим приложеним и БТ стеком в один бинарник. Можно без всяких MCUBOOT его загрузить в чип через SWD. При правильно сконфигурированной системе и board файлах это делается одной cli командой "west flash". В качестве программатора можно использовать j-link или st-link. Можно бинарник прошить через сторонние инструменты, я так тоже делал. Вот с полным стиранием залоченного чипа через ST-link сложнее, но тоже делается.

            Можно сконфигурировать проект так, что он будет поддерживать MCU boot. Это дает два слота загрузки и возможность OTA обновлений. Документация, говорят, хорошая. Но так сложилась, что проект под NRF52 делал я, a MCUBOOT прикручивал уже другой человек, а на пет проектах я MCUBOOT не пробовал.

            Начтнайте с https://docs.zephyrproject.org/latest/develop/getting_started/index.html