C утилитой для ПК и платой — программатором,
с использованием SPL,
с полноценной системой команд и проверкой CRC32,
с гарантией доставки и переотправки сбойной или потерянной команды,
с проверками ошибок, отладочными сообщениями и урезанным printf'ом.
Оптимизирован под современные USB-UART преобразователи и потоковую передачу. Предыстория
Анализ причин низкой скорости протокола AN3155
Требования к моему загрузчику
Описание протокола загрузчика
Утилита загрузки для ПК
UART Программатор на базе CP2103
Реализация прошивки загрузчика
Оптимизация размера
Итог и результаты Мы почти во всех своих устройствах используем STM32. Например в наших шлюзах.
В очень многих устройствах стоит STM32F405/407 и имеется USB <--> UART мост на базе CP2103, но иногда FTDI.
У всех STM32 согласно AN3155 имеется встроенный загрузчик UART и мы его используем на всех стадиях: от разработки и производства до техподдержки наших пользователей.
Фирма STM также предлагает утилиты ПК для того чтоб воспользоваться этим протоколом.
У CP2103 есть штатные GPIO, которыми можно устройство сбросить в основную программу или в штатный загрузчик.
Ещё хорошо то, что эти GPIO винда не трогает, когда ищет Plug&Play устройства на RS232, и поэтому устройство не сбрасывается при подключении к ПК и не “улетает” в непонятный для пользователя режим. Казалось бы всё хорошо, но у этого штатного загрузчика есть ощутимая проблема: больно уж долго он прошивает: около двух минут, особенно когда отлаживаешь схемотехнику, а не свой код.
При этом обычно вносишь минимальные правки и дольше ждёшь когда прошьётся, успеваешь отвлечься и тд.
Да и не дело терять за день более четырёх часов на прошивку изделия, притом что порой разработка длится не один месяц.
Эта проблема долгой прошивки заставила искать причину проблемы и начать её решать. И решение представленное в этой статье — это вторая попытка.
Первая попытка заключалась в том, что я написал свою утилиту для ПК использующую AN3155 — "ARMkaProg".
Она вышла на порядок удобнее и умнее штатной, смогла использовать GPIO у CP2103 и прочие аппаратные удобства.
Но…
Штатная утилита и AN3155 не рекомендовали шить на скорости выше 115200 бод,
Моя же программа могла шить даже 1000к бодах, но это давало прибавку к скорости всего в 2 раза (по сравнению с 115200).
А не в 9, и в итоге вопрос скорости прошивки остался не решённым. Низкая скорость прошивки проистекает из специфики и простоты этого протокола: Рассмотрим подробнее команду записи из даташита на AN3155: где красным отмечено: (1) — подтверждение кода команды, (2) — подтверждение адреса, (3) — блока данных Смотрим на команду записи осциллографом (скорость 1000к бод)  
и реально видим эти состояния АСК: (1), (2) и (3), и их заметную задержку из пункта 9,
а так же видим, что время записи огромное и больше времени тратится на прошивку, чем на передачу данных (см пункт 3). И если глянуть на десяток команд записи  
то видно что пауз очень много и они занимают 2/3 времени. Хмм, а что если не ждать на каждую команду по три штуки ACK и просто слать данные потоком?
Но ничего хорошего из этого не выходит. Загрузчик прост настолько, что он теряет данные если их не ждёт, приёма по DMA или прерыванием в нём нет, да и ROM памяти тоже нет чтоб на каждый интерфейс сделать всё идеально. Из всех этих причин логичным образом вытекают: Эти пункты устраняют соответствующие причины низкой скорости. Но на практике нужны ещё мелочи: Решил сделать более подробное описание своего протокола на случай если кто нибудь захочет портировать программу загрузки для ПК под другие ОС нежели Windows. Сигнатура начала команды для разных направлений выбрана разная для того, чтобы по ошибке не воспринять свои же данные, которые попали себе же на приём. Например когда КЗ на ножках RX и TX контроллера или сбое программатора / кабеля. В описании команд распишу только дополнительные параметры команды, которые идут после кода и размера параметров.
Код команды дан в круглых скобках, далее константа в исходниках и название на русском Дополнительные параметры команды из Хоста:
отсутствуют. Ответ устройства: пример из логов: Дополнительные параметры команды из Хоста:
4 байта — размер стираемой области в байтах Ответ устройства: выдаётся по окончанию стирания всех страниц,
4 байта: Если успешно, то размер стёртой области равный тому, что передал Хост. Если произошёл сбой, то 0. Во время стирания устройство: пример из логов:  Хост не должен выдавать команду с таким кодом — она будет проигнорирована.
Хост во время стирания может заранее передавать данные для записи командами SFU_CMD_WRITE (“запись”) — для ускорения записи.
Но передавать команд можно не более чем размер буфера приёма устройства, иначе он переполнится и первые пакеты заменятся новыми, а последующие будут проигнорированы. Параметры команды с Устройства:
4 байта: номер стёртой страницы начиная с №1 до №11. Дополнительные параметры команды из Хоста:
4 байта — адрес, начиная с которого необходимо записывать содержимое.
X*4 байт — содержимое прошивки, где Х — количество 32 битных слова и должно быть: 1… 1023. Запись игнорируется если адрес указанный Хостом не равен текущему адресу записи в устройстве.
Устройство отвечает всегда вне зависимости от того произведена была запись или проигнорирована. Ответ устройства: 
4 байта: адрес следующего для записи блока увеличивается если запись произошла успешно, не изменяется если команда проигнорирована
4 байта: кол-во необработанных данных в буфере приёма устройства (для отладки и мониторинга). Пример ответов нескольких успешных команд "Запись" из логов:  Дополнительные параметры команды из Хоста:
4 байта: CRC32 всей записанной прошивки, начало прошивки указано в команде "Информация", конец — последнее подтверждённое устройством записанное слово командой "запись" — адрес следующего для записи блока (не включительно). Ответ устройства: 
4 байта: Адрес начала прошивки.
4 байта: Количество записанных байт (Внимание, не 32-х битных слов!), кратно четырём.
4 байта: CRC32 для сверки Хостом, вычисляется начиная с "Адрес начала прошивки", размером с "Количество записанных байт". После этой команды Устройство проверяет CRC32 и если оно совпадает с тем что дал Хост, то запускает прошивку, выполнив полную деинициализацию оборудования. Пример из логов:  Прошло более 500 мс с момента получения последней команды и таймаут истёк, устройство сбросилось в изначальное состояние Без параметров.
Хост ничего не должен на неё отвечать.
Это аварийное сообщение — команда, которую выдаёт только устройство.  Запись в флеш память завершилась с ошибкой. Такое встречается если питания недостаточно или поддельные китайские чипы типа GD32F4xx. Без параметров.
Хост ничего не должен на неё отвечать.
Это аварийное сообщение — команда, которую выдаёт только устройство.  Аппаратный сброс устройства. Устройство АППАРАТНО сбросилось в изначальное состояние — загрузчик перезапустился. Без параметров.
Хост ничего не должен на неё отвечать.
Это аварийное сообщение — команда, которую выдаёт только устройство.  Внешний вид:  
Написана для Windows на Delphi 6 (2001 года, тот что имеет 8 битный тип char и не уникоде). Скомпилировал на Delphi XE5 и проверил работоспособность. Такой старый делфи выбран потому что мне так было проще: ещё с начала 2000-ых были большие наработки по работе с CP210x, COM портами и тд.  Работа с устройством на уровне байт выделена в отдельный поток tCOMclient не зависящий от задержек визуального интерфейса. Связь с этим отдельным потоком сделана при помощи очередей на считывание и запись размером в 65536 байт.
Уровень парсинга с оформлением команд и уровень логики работы с командами разделён на два отдельных класса tSFUcmd и tSFUboot.
Обновление прошивки производится на скорости 921600 бод, без чётности, 8 бит, один стоп бит. Устройства можно указывать:
По имени COM порта, например COM123.
По серийному номеру записанному в CP210x
По системному пути WinNT, например \??\USB#VID_10C4&PID_EA60#GM18_E_0010#{a5dcbf10-6530-11d2-901f-00c04fb951ed} В любом случае, если открытое устройство CP2103, то утилита может попробовать его сбросить через GPIO1 (18 пин) выставив 0-1-0.
Платка-программатор с её схемой, также приложена и описана далее. Если запущена без параметров командной строки, то восстанавливает при запуске и сохраняет при завершения настройки из текстового файла: FastTest.exe.config
Если параметры командной строки присутствуют, то настройки из этого файла игнорируются и он не изменяется. Вместо этого настройки в визуальные компоненты берутся из командной строки и прошивка запускается если это указано. Можно использовать следующие параметры командной строки: Можно скачать отсюда:
https://github.com/Mirn/Boot_F4_fast_uart/tree/master/delphi/Release Выкладываю наш маленький и простенький программатор который:  настройки должны быть такие: Утилита FastTest из предыдущей главы как раз на этом программаторе разрабатывалась и отлаживалась. Исходные файлы на программатор качать отсюда:
https://github.com/Mirn/ProgCP2103 Средства разработки и сторонние библиотеки: Профиль использования памяти: Сделана максимально просто: DMA не используется, передача сделана прямой отправкой в периферию без прерываний при помощи функций SPL. Но раз основная цель ускорения протокола — непрерывный поток команд с содержимым для прошивки, то приём данных из UART сделан по прерыванию USART1_IRQHandler.  Так же в UART я реализовал контроль и учёт ошибок и проверку и коррекцию буфера при переполнении данных, если их записано больше, чем его размер. При реализации приёма UART в прерываниях возникла проблема:
по умолчанию код находится во флеше и во время прошивки флеш памяти, шина флеша блокируется и исполнение останавливается полностью включая прерывания. И на скоростях выше 500к БОД это приводит к потере принимаемых из UART данных, т.к. время приостановки становится больше времени приёма байта. Поэтому функция обработки прерывания была вынесена в ОЗУ вот таким вот образом: при этом есть важная тонкость, что если функция, лежащая в RAM, вызывает другие функции в флеш, то получим ошибку вида: Это вызвано ограничением архитектуры ARM инструкций Thumb2 на максимальное адресное расстояние между вызовами. А в этом случае оно больше допустимого. Я исправил это добавив к всем вызванным из RAM функциям атрибут-модификатор long_call. Принимает пакеты по протоколу описанному в этой статье и проверяет их целостность. При этом, на всех стадиях разбора пакета проверяет на наличие ошибок и подсчитывает их количество если они встретятся. Но ошибки не замалчиваются, а выводятся текстовые сообщения и раз в секунду выводится строка с всеми ошибками как UART так и уровня пакетов и случаев таймаута в 500 мс. Этот таймаут в 500 мс контролируется и вырабатывается этой же библиотекой. Обрабатывает логику команд SFU_CMD_XXX как описано выше. Стирает и прошивает флеш, при этом функция прошивки слова в флеш память также вынесена в RAM, чтоб данные по приёму из UART не терялись. В неё же реализован запуск основной прошивку, при этом проверяя что её контекст указывает на реальную флеш память и RAM память. Перед запуском основной прошивки вся периферия и тактовые полноценно деинициализируются и сбрасываются. Работоспособность прошивки проверена на моделях: STM32F405RG, STM32F405VG,
и на скоростях от 115200 до 921600 бод. все исходники прошивки доступны на моём гитхабе по ссылке:
https://github.com/Mirn/Boot_F4_fast_uart вариант для STM32F7xx:
https://github.com/Mirn/Boot_F745_SFU В первую очередь и самое главное что влияет на размер — это общая архитектура алгоритма и использованных данных. Я старался делать всё максимально просто и, даже местами, примитивно. При этом я старался самые сложные вещи в логике перекладывать на Хост. Одним словом, порядок и краткость в коде начинается с порядка в голове разработчика. Но нужно соблюдать меру и не забывать о тех удобствах, которые помогают лучше понять что твориться в загрузчике, и поэтому остались отладочные сообщения, контроль и подсчёт ошибок и прочие мелочи и удобства. А так же я не стал всё лепить в одну функцию и разложил всё по полочкам, и разбил на модули. Хоть это и приводит к увеличению размера прошивки на пару сотен байт, мне её ещё много лет придётся поддерживать, развивать и на базе неё создавать новые. Ещё небольшой вклад в увеличение размера дала необходимость поместить часть функций в RAM. Также не стоит забывать, как работает компилятор и его оптимизатор. Компилировал я естественно на -Os, но каких-то других особых ключей не использовал и даже не заморачивался с этим. Если дать побольше конкретики, то компилятор сможет получше оптимизировать: параметры подписывать const где это можно, локальные в пределах одного файла функции как static и т.д.  Так же не стоит шаманить с мелочами типа перестановки строк, вылизывание ифов с булевой оптимизацией условий в них — в это всё компиляторы давным давно умеют. Доверьтесь им. В случае чего, можно глянуть map файл, где написано какая функция, сколько занимает или просто в листинге посчитать кол-во строк. Даже не зная асма при этом сразу будет видно какая функция неожиданно монстроидально развернулась. Standard peripheral library от STM имеет очень большой потенциал уменьшения в размере. Она написано очень просто — многие функции перекладывают данные из переданных им заполненных структур, в соответствующие регистры периферии. Эти функции не содержат внутренних статичных переменных, не обращаются к глобальным переменным и обычно не требуют указатели на какие либо хранилища состояний. Они очень редко обращаются к другим своим или чужим функциям. Но у них есть недостаток: они содержат очень много дублирующегося кода, например GPIO_DeInit проверяет равенство переданного GPIO к каждому порту GPIOA, GPIOB… GPIOI, и сбрасывает каждый порт по отдельности отдельным кодом. Т.е. там внутри действительно пачка из десяти if и двадцати RCC_AHB1PeriphResetCmd. И поэтому SPL потребляет очень много флеша. На связку UART и GPIO с RCC обычно приходится около 8 килобайт.
Поэтому я скопировал код использованных функций SPL в отдельный заголовочник, объявил их как static inline и добавил к каждой такой функции суффикс _inline, например GPIO_DeInit_inline. Так же заинлайнил все вызванные ими функции. Это сразу сократило код в разы. В секции .data хранятся стартовые значения переменных, которые заданы на стадии компилирования. Они размещаются в флеше, и в коде есть цикл, который их копирует при старте в RAM.
Я написал код так, чтоб таких переменных не было вообще, и не пришлось бы писать код, который вручную выставляет им нужные параметры. В секции .ro_data хранятся все константы, в том числе и текстовые. Здесь нужно просто знать меру, и поэмы в терминал не выводить, ограничившись минимально информативным логом из одного-двух слов. А ещё у GCC есть такой баг, когда функция не используется, но её константные переменные в .ro_data и прошивку всё-таки попадают. Такие случаи я тоже закомментировал или удалил. Я взял из CoIDE готовую реализацию урезанного printf, в ней многое упрощено, а поддержки плавающей запятой вообще нет. Но она неявно использует структуру impure_data и указатель impure_ptr. Они занимают сотни байт и тянут за собой ещё много чего. Компилятор gcc скрыто от программиста помещает stderr и stdin именно в этой структуре и их и необходимо не использовать в коде. Изначально пример printf как раз содержал stderr и stdout, их упоминания я и убрал, попутно заменив на более прямые вызовы и закомментировал ненужные опции printf. И убрал не используемые варианты вывода например строк, знаковых целых, шестнадцетиричных и тд. Из CoIDE я взял самый минимальный, который нашел, код запуска и первичной инициализации. Он копирует .data из флеша в RAM, запускает кварец и настраивает частоты, обнуляет .bss и настраивает процессор: стек, плавающие запятые, CCM память и тд. Но часть этих задач уже реализованы в SPL и использованы мною. Я их заменил прямым вызовом соответствующей не инлайн функции SPLа. Также было много повторов кода, когда, например, плавающие запятые включаются аж в трёх местах.
Прибил гвоздями SystemCoreClock к дефайну и выкинул функцию SystemCoreClockUpdate.
Код запуска использовал таблицы констант для расчётов которые хранились в RAM как volatile (интересно зачем?). Перенёс в флеш, а при оптимизации компилятор часть из них заменил на прямой расчёт (там где были степени двойки, в тридцать два слова). Таблица прерываний содержит в первых двух 32-и битных ячейках контекст исполнения: адрес кода и адрес стека. А в последующих содержит указатели на все возможные прерывания. А это почти 500 байт. Так как "Остапа понесло" и я уже не мог смириться, что кода больше 4к (привет 4к демо сцене!). То я избавился и от таблицы тем, что ужал её до первых двух ячеек. А в стартап-коде перенёс вектор на таблицу в RAM, куда добавил только один обработчик UARTа ручками таким кодом: и поправил ld файл прописав так, чтоб секция для таблицы прерываний в RAM была выровнена как и полагается по 512 байтам Экономия на таблице векторов вышла почти в 400 байт. Время прошивки размером 400 килобайт. встроенным загрузчиком по AN3155 с скоростью 256000 БОД: 95 секунд
встроенным загрузчиком по AN3155 с скоростью 500000 БОД: 78 секунд
встроенным загрузчиком по AN3155 с скоростью 921600 БОД: 70 секунд
во всех случаях с разлочкой и залочкой, с полным стиранием моим загрузчиком со скоростью 921600 БОД: 9 секунд,
что быстрее в 8 раз. Видео работы нового загрузчика (в начале), и старого по AN3155, запускается после нового.   
 
Проверяем осциллографом на предмет непрерывности потока данных и отсутствия пауз по UART  
или более развёрнуто один пакет:  
пауз нет, поток непрерывен, ускорение в 8 раз получено.
Получилось всё что было запланировало и к чему стремились. Ещё раз ссылка на гитхаб:
https://github.com/Mirn/Boot_F4_fast_uart Это мой первый проект на гитхабе и опубликован с целью его изучения и вхождения в сообщество.
Я решил сделать не "хеллоу ворлд", а что-то реально полезное. Гитхаб для сообщества и глупо начинать с бесполезного всем проекта. Я вспомнил про то, до чего руки чесались, но из за лени много лет не доходили. И вдруг выдался повод: из-за кризиса у меня скоро возникнет просто уйма свободного времени, а что-то делать надо сейчас. В итоге так и родился этот турбо-загрузчик. Добавлено:
Сделан вариант SFU для STM32F7xx:
https://github.com/Mirn/Boot_F745_SFU