- PVSM.RU - https://www.pvsm.ru -
C утилитой для ПК и платой — программатором,
с использованием SPL,
с полноценной системой команд и проверкой CRC32,
с гарантией доставки и переотправки сбойной или потерянной команды,
с проверками ошибок, отладочными сообщениями и урезанным printf'ом.
Оптимизирован под современные USB-UART преобразователи и потоковую передачу.
Предыстория [1]
Анализ причин низкой скорости протокола AN3155 [2]
Требования к моему загрузчику [3]
Описание протокола загрузчика [4]
Утилита загрузки для ПК [5]
UART Программатор на базе CP2103 [6]
Реализация прошивки загрузчика [7]
Оптимизация размера [8]
Итог и результаты [9]
Мы почти во всех своих устройствах используем STM32. Например в наших шлюзах [10].
В очень многих устройствах стоит STM32F405/407 и имеется USB <--> UART мост на базе CP2103, но иногда FTDI.
У всех STM32 согласно AN3155 [11] имеется встроенный загрузчик 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.
адрес | размер | содержимое |
---|---|---|
0 | 4 | Сигнатура начала команды, для передачи в устройство 0x817EA345, для приёма из устройства 0x45A37E81 |
4 | 1 | код команды, задаёт тип действия или события |
5 | 1 | побитово-инверсный код команды (для проверки) |
6 | 2 | N — размер дополнительной информации в байтах (обязан быть кратным 4) |
8 | N | дополнительная информация — зависит от кода команды |
8+N | 4 | встроенный аппаратный в STM32 CRC32 пакета с адреса 4 по N (исключая сигнатуру) |
Сигнатура начала команды для разных направлений выбрана разная для того, чтобы по ошибке не воспринять свои же данные, которые попали себе же на приём. Например когда КЗ на ножках RX и TX контроллера или сбое программатора / кабеля.
В описании команд распишу только дополнительные параметры команды, которые идут после кода и размера параметров.
Код команды дан в круглых скобках, далее константа в исходниках и название на русском
Дополнительные параметры команды из Хоста:
отсутствуют.
Ответ устройства:
размер в байтах | Описание |
---|---|
12 | Уникальный ChipID |
4 | Модель и ревизия чипа, взят из DBGMCU->IDCODE |
2 | Размер доступной для записи загрузчиком флеш-памяти в KiB (*1024 байт) |
2 | Версия загрузчика 0x0100 |
4 | Размер буфера приёма устройства (для опции -PreWrite) |
4 | Адрес начала доступной для записи памяти |
4 | Адрес таблицы векторов прерываний и место с которого берётся контекст запуска (стек + точка входа) |
пример из логов:
Дополнительные параметры команды из Хоста:
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 [12]
Выкладываю наш маленький и простенький программатор который:
настройки должны быть такие:
IO.Mode = 1100001101010100
IO.Reset = 0000110011111111
IO.Suspend = 0000111111111111
IO.EnhFxn = 10
Утилита FastTest из предыдущей главы как раз на этом программаторе разрабатывалась и отлаживалась.
Исходные файлы на программатор качать отсюда:
https://github.com/Mirn/ProgCP2103 [13]
Средства разработки и сторонние библиотеки:
-mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Os -fmessage-length=0 -ffunction-sections -fdata-sections -ffreestanding -fno-builtin -Wunused -Wuninitialized -Wall -Wextra -Wpointer-arith -Wshadow -Wlogical-op -Waggregate-return -Wfloat-equal -Wno-sign-compare
Профиль использования памяти:
Сделана максимально просто: DMA не используется, передача сделана прямой отправкой в периферию без прерываний при помощи функций SPL. Но раз основная цель ускорения протокола — непрерывный поток команд с содержимым для прошивки, то приём данных из UART сделан по прерыванию USART1_IRQHandler.
Так же в UART я реализовал контроль и учёт ошибок и проверку и коррекцию буфера при переполнении данных, если их записано больше, чем его размер.
При реализации приёма UART в прерываниях возникла проблема:
по умолчанию код находится во флеше и во время прошивки флеш памяти, шина флеша блокируется и исполнение останавливается полностью включая прерывания. И на скоростях выше 500к БОД это приводит к потере принимаемых из UART данных, т.к. время приостановки становится больше времени приёма байта. Поэтому функция обработки прерывания была вынесена в ОЗУ вот таким вот образом:
__attribute__ ((long_call, section(".data")))
void USART1_IRQHandler(void)
при этом есть важная тонкость, что если функция, лежащая в RAM, вызывает другие функции в флеш, то получим ошибку вида:
usart_mini.c: relocation truncated to fit: R_ARM_THM_CALL against symbol `demo' defined in .text.demo section in ./src/main.o
Это вызвано ограничением архитектуры 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 [14]
В первую очередь и самое главное что влияет на размер — это общая архитектура алгоритма и использованных данных. Я старался делать всё максимально просто и, даже местами, примитивно. При этом я старался самые сложные вещи в логике перекладывать на Хост. Одним словом, порядок и краткость в коде начинается с порядка в голове разработчика.
Но нужно соблюдать меру и не забывать о тех удобствах, которые помогают лучше понять что твориться в загрузчике, и поэтому остались отладочные сообщения, контроль и подсчёт ошибок и прочие мелочи и удобства. А так же я не стал всё лепить в одну функцию и разложил всё по полочкам, и разбил на модули. Хоть это и приводит к увеличению размера прошивки на пару сотен байт, мне её ещё много лет придётся поддерживать, развивать и на базе неё создавать новые. Ещё небольшой вклад в увеличение размера дала необходимость поместить часть функций в 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а ручками таким кодом:
__attribute__ ((section(".isr_vector_minimal"))) void (* const StartVectors_minimal[])(void) =
{
(void *)&_estack,
Reset_Handler,
};
__attribute__ ((section(".isr_vector_RAM"))) void (* StartVectors_RAM_actual[128])(void) = {0};
void Default_Reset_Handler(void)
{
...
StartVectors_RAM_actual[0xD4 / 4] = USART1_IRQHandler;
SCB->VTOR = (uint32_t)StartVectors_RAM_actual;
main();
}
и поправил ld файл прописав так, чтоб секция для таблицы прерываний в RAM была выровнена как и полагается по 512 байтам
.text : {
KEEP(*(.isr_vector_minimal*))
*(.text .text.* .gnu.linkonce.t.*)
*(.rodata .rodata* .gnu.linkonce.r.*)
} > rom
.bss (NOLOAD) : {
_sbss = . ;
. = ALIGN(512);
*(.isr_vector_RAM*)
*(.bss .bss.*)
*(COMMON)
. = ALIGN(4);
_ebss = . ;
} > ram
Экономия на таблице векторов вышла почти в 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 [14]
Это мой первый проект на гитхабе и опубликован с целью его изучения и вхождения в сообщество.
Я решил сделать не "хеллоу ворлд", а что-то реально полезное. Гитхаб для сообщества и глупо начинать с бесполезного всем проекта. Я вспомнил про то, до чего руки чесались, но из за лени много лет не доходили. И вдруг выдался повод: из-за кризиса у меня скоро возникнет просто уйма свободного времени, а что-то делать надо сейчас. В итоге так и родился этот турбо-загрузчик.
Автор: Mirn
Источник [15]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/stm32/163608
Ссылки в тексте:
[1] Предыстория: #History
[2] Анализ причин низкой скорости протокола AN3155: #Analiz
[3] Требования к моему загрузчику: #TZ
[4] Описание протокола загрузчика: #Protocol
[5] Утилита загрузки для ПК: #PCutil
[6] UART Программатор на базе CP2103: #MiniProg
[7] Реализация прошивки загрузчика: #SRC
[8] Оптимизация размера: #Optimize
[9] Итог и результаты: #Results
[10] Например в наших шлюзах: https://habrahabr.ru/post/262531/
[11] AN3155: https://www.google.de/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=stm32+AN3155
[12] https://github.com/Mirn/Boot_F4_fast_uart/tree/master/delphi/Release: https://github.com/Mirn/Boot_F4_fast_uart/tree/master/delphi/Release
[13] https://github.com/Mirn/ProgCP2103: https://github.com/Mirn/ProgCP2103
[14] https://github.com/Mirn/Boot_F4_fast_uart: https://github.com/Mirn/Boot_F4_fast_uart
[15] Источник: https://habrahabr.ru/post/305800/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.