Например TDA7294

РадиоКот >Статьи >

Бутлоадеры, или как прошить STM32 с помощью AvrProg

Автор: Aheir
Опубликовано 20.01.2016.
Создано при помощи КотоРед.


Если взрослого мыша
Взять и, бережно держа,
Напихать в него иголок,
Вы получите ежа…

Да, именно так, сегодня будем прошивать микроконтроллер из семейства STM32F100 через программу AvrProg из состава AtmelStudio, предназначенную, соответственно, для микроконтроллеров Atmel. Но – чуть позже. А пока – немного предыстории и железа, а то совсем без «попаять» скучно же…
В свое время в моей практике была необходимость достаточно плотного общения с разнообразными устройствами по протоколу RS485. В дебри описания углубляться не буду, информации море при поиске по ключевым словам (в двух словах, там вся заморочка в том, что интерфейс полудуплексный, и, в отличие от того же RS232, необходимо не только передавать/принимать данные, но и управлять направлением потока этих самых данных: на прием или передачу). Для подключения подобных железяк к компу часто применяются USB-RS485 конвертеры (легко получаются из USB-RS232 преобразователей (см., например, здесь) путем добавления микросхемы приемопередатчика RS485), но стандартные решения меня не очень устраивали, потому как иногда требовалось осуществлять дополнительную обработку или буферизацию данных и т.п., поэтому был разработан свой конвертер на микроконтроллере.
Схема P-Cad доступна для скачивания в конце, разберем ее по кусочкам, хотя ничего волшебного там нет.

Устройство подключается к компьютеру через USB с обеспечением полной гальванической развязки, как по линиям данных (микросхема U2 на схеме ADUM4160), так и по питанию 5В (DC/DC конвертер U6). USB-выход этого узла поступает на преобразователь USB-RS232 по классической уже схеме на FT232B (см. здесь), так что ее приводить не буду (да, знаю, на сегодняшний день есть более простые схемотехнические решения, но здесь как всегда работаем по принципу «из наличия»):


Полученный поток данных RS232 отдаем на первый USART микроконтроллера STM32F100C8. У этого микроконтроллера два интерфейса USART и ко второму из них подключена микросхема-драйвер RS485, а именно MAX485:

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

Обычный ключ на полевике, в качестве входного и выходного разъема используются DC на 5,5мм, а U1 – разъем 8P8C (под вилку RJ-45), потому как для подключения устройств удобно использовать витую пару. В его распиновке постарался учесть и варианты PoE, и более-менее стандартную распиновку китайских DMX-контроллеров, и свои предпочтения…
Таким образом, устройство, условно, состоит из трех блоков, расположенных последовательно: (USB<->RS232) <-> (микроконтроллер) <-> (RS232<->RS485). Что происходит с данными в микроконтроллере, какой функционал туда заложен – определяется исключительно прошивкой, которая неоднократно менялась в разных ситуациях. Это может быть простой транслятор данных туда-сюда, можно реализовывать какие-то более сложные алгоритмы (по приходу команды с ПК сбросить подключенное устройство по питанию, затем послать ему какую-либо последовательность, чего-то подождать, сконфигурировать, после этого доложиться компу и включить прозрачный обмен, например, или по нажатию кнопки делать посылки фиксированных данных и т.п.). Тут все зависит от текущих желаний и потребностей.
Для истории – фото собранной платы (LM7805 была добавлена позже, навесом, служит для возможности питания контроллера не только от USB, но и от напряжения питания целевого устройства) и внешнего вида устройства. Вроде даже симпатишно…

Какое-то время этот конвертер использовался по прямому и не очень назначению, но в определенный момент я закономерно пришел к необходимости применения в своих устройствах такой вещи, как бутлоадер для микроконтроллеров. Опять же, отправлю в Гугл, а если коротко, то это специальная прошивка для микроконтроллера, которая может каким-либо образом (интерфейсом) принимать данные (прошивку) и осуществлять самопрограммирование микроконтроллера полученными данными.
Т.е. в общем случае алгоритм такой. Разрабатываем прошивку бутлоадера, далее с помощью программатора или каким-то иным доступным для интересующего нас микроконтроллера образом прошиваем ее. Получаем микроконтроллер, в который прошит только бутлоадер. Теперь, при включении такого микроконтроллера, первоначально загружается бутлоадер и отслеживает некие условия (нажатие кнопки, приход определенного символа через UART, наличие определенного файла на подключенной карте памяти, простой таймаут – что мы напрограммировали в бутлоадере, то он и делает). Если условие выполнено, бутлоадер переходит к приему данных и прошивает их во flash-память микроконтроллера (куда, как – определяем мы в прошивке бутлоадера: может, там расшифровывать что-то надо, или еще чего…). Если условие при включении не выполняется, то бутлоадер завершает свою работу и передает управление основной программе. Такая возможность имеется в подавляющем большинстве современных микроконтроллеров на основе flash-памяти. Более того, часто эти микроконтроллеры поставляются уже со встроенным бутлоадером, что позволяет осуществлять их прошивку без специального программатора. Но тема все равно интересная, поскольку свой бутлоадер дает практически неограниченные возможности по способу ввода целевой прошивки в микроконтроллер.
В микроконтроллерах Atmel тоже есть такая возможность. Здесь для бутлоадера отведена специальная область памяти, ее размер и необходимость при включении загружаться с бутлоадера в семействе AVR определяется соответствующими fuse-битами. Например, для Atmega88:

Последние три fuse’a говорят нам, что при старте нужно загружаться с адреса бутлоадера (BOOTRST=0), а размер бутлоадера выбран максимальным (BOOTSZ0=0, BOOTSZ0=1 – см. даташит). Вообще, даташит стоит посмотреть ради карты распределения памяти и конкретных характеристик ее объема для отдельно взятого микроконтроллера.
Как вы понимаете, первая часть статьи была написана неспроста, и мне понадобился бутлоадер именно под RS485, причем для Atmega88.
Немного поисков дали по всем параметрам подходящий результат. Это тоже не первоначальный, а доработанный вариант более ранней разработки Мартина Томаса.
Из плюсов: полностью мне подходил, универсален, бесплатен и открыт, использует стандартное ПО на стороне ПК.
Из минусов: здоровый (2кБ – четверть памяти той же Atmega88)
Впрочем, меня такой расход памяти устраивал, так что все вполне подошло. Опять же, бутлоадер стал весить более 512 слов после доработки для совместимости с RS485 и добавления светодиода (а это весьма небольшой объем добавочного кода), так что теперь там есть еще масса места под всякие надобности. Я из плюшек добавил возможность мигать светодиодом: светик мигает, пока бутлоадер ждет входное условие, горит, когда мы находимся в режиме бутлоадера и помаргивает, когда мы читаем или пишем микроконтроллер.
Да, если хочется видеть размер полученной пошивки бутлоадера, в makefile нужно добавить еще один target:

size: ${TARGET}
@echo
@avr-size -C --mcu=${MCU} ${TARGET}

Ну и запустить его, само собой:

# Default target.
all: begin gccversion sizebefore build sizeafter finished size end

В архиве в конце статьи есть исходники этого проекта для AtmelStudio4, в том числе содержащие и дополненный makefile под названием «makefile_target_size»
В этом бутлоадере из актуальных для нас доступны способы входа по нажатию кнопки или по приходу через USART заданного символа. Само собой, меня интересовал последний вариант, иначе зачем тогда вся возня с RS485. Поскольку в качестве оболочки со стороны ПК используется утилита AvrProg, а она при поиске программатора шлет в первые 4 СОМ-порта символ ‘S’, то именно его мы и задаем в качестве условного при настройке бутлоадера в файле main.c
Под эту лавочку была доработана и прошивка конвертера RS485. В текущем виде он умеет работать в трех режимах: Data, Boot, DMX. Переключение между режимами – по долгому нажатию единственной кнопки.
Data – простой конвертер, короткие нажатия на кнопку переключают скорость обмена 9600-19200-57600-115200, формат 8N1
DMX – передача в линию пакета данных от компа (пакет должен начинаться с преамбулы “ABCDEF”, далее – 512 байт данных) в формате DMX, короткие нажатия на кнопку – ничего не делают (можно добавить какие-то встроенные эффекты, например)
Boot – работа с бутлоадером на скорости 19200, по короткому нажатию происходят поочередно 2 сценария: 1. сброс целевого устройства по питанию или 2. сброс целевого устройства по питанию и через 500мс передача в линию символа ‘S’ для входа в бутлоадер.
Таким образом, в режиме Boot нет необходимости торопиться и подгадывать время запуска AvrProg на компе или увеличивать время ожидания в бутлоадере. Достаточно нажать на кнопку на конвертере для входа в бутлоадер, после чего спокойно запускать AvrProg и соединяться с целевым устройством. Еще раз обращаю внимание: AvrProg сканирует только несколько (4, если не ошибаюсь) первых СОМ-портов, так что конвертер обязан висеть на каком-то из них!
В остальном, бутлоадер работает совершенно так же, как написано в источнике, с теми же warnings при компиляции и ошибками верификации. Подтверждаю, прошивает при этом все верно.
Если AvrProg не смог подключиться к устройству, мы увидим следующее:

В этом случае, нужно проверить… все.
Если соединение прошло успешно, увидим:

Верхняя часть – выбор файла прошивки (кстати, запоминает его при закрытии, иногда это удобно, иногда – прошиваешь не то, что хотел). Внизу видим, к какому устройству подключились (эта сигнатура определяется параметром DEVTYPE в main.c бутлоадера. По какому принципу они присваиваются – без понятия, на всякий случай, то, что удалось найти, лежит в файле chipdef.h в конце статьи.
Если жмакнуть на «Advanced», увидим следующее:

Закрываем и жмакаем на Program, начинается процесс прошивки:

При верификации вылезает обещанная ошибка:

Однако, перезагружаемся, ждем, пока бутлоадер отсчитает таймаут, и… все работает, согласно записанной прошивке. Если посмотреть дамп памяти с прошитого контроллера, можно убедиться, что все прошито верно, несмотря на ошибку верификации.
В AVR бутлоадер располагается в самом конце области памяти flash, а адрес загрузки контроллера определяется только состоянием fuse-битов, поэтому каких-либо дополнительных настроек компилятора при написании прошивки, которая потом будет заливаться через бутлоадер, не требуется. Просто пишем, как обычно. Главное, следить за тем, чтобы размер прошивки не превысил объем свободной после записи бутлоадера памяти. В случае Atmega88 – это 6кБ. Эту же прошивку можно заливать любым другим программатором – будет работать. Но не забываем про fuse'ы.
В общем, вполне рабочий вариант, можно пользоваться.
Прошло еще некоторое время, я пришел к необходимости бутлоадера для STM32. У этих контроллеров есть встроенный бутлоадер для заливки прошивки через USART0, но там достаточно много ограничений (только USART0 и только RS232, исходников не дают, как я понял, хотя есть соответствующий аппнот, свой собственный софт). В общем, пошел копать в направлении самописных бутов и с удивлением обнаружил, что их как бы почти и нет… Нашел интересный вариант, что-то обсуждается на форумах, что-то видел на GitHub’e, но практики мало. Возможно, все пользуются штатным, возможно — все пишут свои и помалкивают, возможно, это просто никому не надо.. Не знаю. Мне вот стало надо)
Решил наваять что-то свое на основе, так сказать, и неожиданно вспомнил про все тот же AvrProg, к которому успел привыкнуть. Ну а почему нет? Работает вроде как со стандартным hex-файлом, умеет его отдавать через последовательный порт, проверен с RS485… Кандидат, однако)
Разумеется, первый вопрос – как писать в память. Конечно, STM32 гораздо более сложные контроллеры, но с точки зрения пользователя, здесь все даже проще, потому как о нас уже позаботились. Все нужное описано в файле stm32f10x_flash.h из состава STM-овской Standard Peripheral Library, работа с flash в данном случае сводится всего к нескольким функциям. Сначала нужно разблокировать flash для внесения изменений (FLASH_Unlock()), потом произвести какие-то действия, желательно осмысленные (стирание, запись информации), после чего вновь включить блокировку (FLASH_Lock()).
В остальном, перенос исходников бута с AVR на STM32 существенных проблем не доставил. Аппаратно зависимые там только, само собой, процедуры инициализации периферии и самой записи-чтения. Кроме этого, для корректной работы с AvrProg надо качественно прикинуться AVR’кой, что сводится к отдаче правильных DEVTYPE и цифровой подписи микроконтроллера. Я решил прикидываться Atmega128 из соображений большого объема памяти. Впрочем, не сильно помогло, но об этом позже.
Прошить хотелось STM32F100C8. Начал с анализа работы AvrProg. Снял лог обмена с целевой платой при прошивке, убедился, что AvrProg корректно реагирует на размер буфера в 1024 байта и исправно отдает данные такими порциями (это удобно, т.к. размер страницы памяти в STM32F100C8 именно такой и не надо лишний раз мудрить расчетами адресов). Реализуем необходимый минимум: только запись во flash, а вот чтение, работу с ЕЕПРОМ и прочие не актуальные вещи просто заменяем заглушками. В общем, все складывалось неплохо, так что буквально через час возни AvrProg смог прицепиться к моей STM’ке с самописным бутом:

Надпись «Atmega128 BOOT» следует читать как «STM32 Board» )))
Под кнопкой «Advanced» особых изменений нет:

Здесь, видимо, уже пора рассказать об особенностях написания основной прошивки STM32 для загрузки бутлоадером. В отличии от AVR здесь предопределенной области для бутлоадера не предусмотрено. Стартуем с нулевого адреса, а уж что там окажется – заранее неизвестно. Поэтому делают так: бут подготавливается для старта с нулевого адреса (как обычная прошивка для записи программатором), а уже в нем задается адрес, по которому размещается основная программа, которую нужно запустить. В моем исходнике за это отвечает параметр PROGRAM_START_ADDR. 32-битная переменная вида 0x08003000, задающая стартовый адрес основной программы в абсолютных адресах. Очевидно, что 0x3000 – на единичку больше максимального размера бутлоадера. Этот параметр можно произвольно изменять, оставаясь в рамках доступной flash и не забывая, что основной программе все же тоже нужно место.
Теперь конкретно по основной программе. В компиляторе линковщику нужно сообщить адрес памяти, с которого программа должна начинаться. В Keil это делается в меню Project -> Options for Target на закладке Target:

Знакомое же значение верно? Эта настройка необходима, но не достаточна.
В исходнике основной программы первым делом, в самом начале функции main, необходимо перенести на этот адрес таблицу векторов прерываний.
Делается это следующим нехитрым образом:

#ifdef START_ADDRESS_WITH_BOOTLOADER

__set_PRIMASK(1);

NVIC_SetVectorTable(NVIC_VectTab_FLASH, START_ADDRESS_WITH_BOOTLOADER);

__set_PRIMASK(0);

__enable_irq();

#endif



Как вы понимаете, выше, в начале листинга, должно быть магическое число:

#define START_ADDRESS_WITH_BOOTLOADER 0x08003000

тоже, подозреваю, знакомое). Т.е. если этот дефайн определен, то подготавливаем прошивку с указанным смещение, если нет - обычную.
Теперь в результате компиляции мы получим hex-файл с необходимыми параметрами адресации. Ну что ж, рискнем) Этот самый hex под названием fw_offset.hex можно увидеть парой картинок выше в окне AvrProg’а. Нажимаем на «Program» и начинаем огребать:

Однако, странно, у Atmega128 flash больше, чем у нашей STM'ки. С чего тогда? Ладно, ОК:

Не удивил, с учетом предыдущего сообщения. Продолжаем:

AvrProg в шоке… И тем не менее, ДА, и процесс записи прошивки начинается, конвертер и целевая плата весело моргают светодиодами, т.е. какой-то обмен между ними идет. Более того, процесс даже завершается, правда с ошибкой верификации:

Правда ничего не повисает, бутлоадер продолжает отзываться на команды и т.д.
Хорошо, перезагружаем целевую плату и вот тут-то как раз полная тишина в эфире… Цепляемся к камню STLink’ом – полная чушь в памяти, кроме адресов бутлоадера, конечно. Здесь все на месте.
Копаем дальше) Для сравнения открываем hex для AVR (сверху) и для STM (снизу) (простым блокнотом, файл-то текстовый, на самом деле…):

Конец файлов:

Видим, что разница есть, но пока что ни черта в ней не понимаем. Здесь придется сделать небольшое отступление в область структуры hex-файлов. Все весьма доступно описано. Если коротко, то каждая строка начинается с двоеточия, далее идет два символа, задающие количество байт данных в строке; потом 4 символа адреса; потом 2 символа типа записи; поле данных; 2 символа контрольной суммы. Все пары символов следует читать как 16-тиричные значения. Стырю картинку из указанного чуть выше источника:

Таким образом, теперь понятно, что в STM’овском hex есть группы данных, которые в AVR отсутствуют. В порядке общего развития можно вникнуть за что они отвечают, после чего придти к выводу, что их можно попробовать удалить. В том же блокноте приводим hex к следующему виду, т.е. удаляем все, кроме записей данных и признака конца файла (середину вырезал для наглядности) и сохраняем под именем fw_cutted.hex:

Кстати, можно заметить, что адресация данных начинается со смещением 0x3000. Знакомое число)
В итоге, AvrProg перестает пытаться залить несуществующие данные (откуда он их только берет-то, ведь что-то же заливает) и начинает сразу с того, что нужно.
Пробуем прошить, теперь процесс идет гораздо шустрее:

Верификация:

Ошибка:

Здесь ошибка вполне ожидаема, потому как процедура чтения flash не реализована, бутлоадер отдает вместо данных 0xFF. А вот то, что оно ожидает получить 0x0708 – отрадно, потому как это первые байты нашей прошивки! Адрес доступа странен, но AvrProg читает данные исключительно порциями по 256 байт, а не по 1024, как адресуемся мы в STM32. Короче говоря, предпосылки неплохие. Перезагружаемcя… Работает! Основная прошивка запустилась после бута, все в порядке.
Полшага в сторону. В исходнике бута есть возможность включить реальное чтение flash (в порядке эксперимента я таки написал этот функционал) и передачу этих данных в AvrProg. Более того, оно работает. Однако, при верификации оно все равно вываливается с ошибкой, причем в одном и том же месте для одной и той же прошивки. Дамп памяти в этом месте не выявляет никаких расхождений, все записано точно. В общем, ситуация, аналогичная Atmega88. На практике это приводит только к увеличению времени прошивки, потому как при отдаче левых данных ошибка вываливается сразу, а с корректными данными оно еще секунд 10-20 лопатит файл. Так что я решил до выяснения обстоятельств на это пока внимания не обращать.
Так, вроде разобрались, один неприятный момент: ну не будешь же каждый раз ручками в hex лазить, верно? Ваяем bat-ник format_hex.bat следующего содержания:

type output*.hex > fw_offset.hex
type output*.hex | findstr /b /v :02000004 | findstr /b /v :04000005 > fw_cutted.hex

При размещении его в папке проекта Keil, если не менялась структура папок проекта по умолчанию, после его запуска в папке появятся два файла: fw_offset.hex (просто копия hex из папки output) и fw_cutted.hex (отредактированный файл). Редактирование сводится к вырезанию из файла строк, совпадающих с образцом, с помощью команды findstr (линуксоиды курят маны к sed). Копирование (первая строка батника) выполнена через type с перенаправлением вывода, потому что простое copy дает лишний байт в конце файла:

Вроде ни на что не влияет, но неаккуратненько как-то…
Здорово, но не вручную же его запускать, в самом деле. В Keil в меню Project -> Options for Target на закладке User дописываем наш батник:

Теперь, после компиляции, увидим следующее:

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

Так, уже легче, но все равно есть необходимость помнить, какая конфигурация распределения памяти на данный момент настроена в свойствах проекта. А это не всегда удобно. Попробуем автоматизировать и этот момент. Идем в свойствах проекта на закладку Linker:

Нас интересует галочка в районе левого верхнего угла панели, а именно «Use Memory Layout from Target Dialog». Т.е. если «галка» стоит, то в качестве исходных настроек распределения памяти для линкера берутся данные с закладки «Target», которые мы уже меняли чуть ранее парой экранов выше. Снимаем эту галку (теперь настройки на закладке «Target» игнорируются), становится интереснее:

У нас появляется возможность задать настройки непосредственно здесь, либо же подгрузить некий загадочный «scatter file». Настройки в явном виде менять смысла нет, уже проходили, а вот насчет файла стоит задуматься. Собственно говоря, файл содержит те же самые настройки и генерируется системой перед каждым актом генерации прошивки – build. Размещение и название файла по умолчанию видно нас скриншоте, внутри там примерно следующее (открывается в самом Keil’e, если нажать на кнопку «Edit»):

Видим все те же уже знакомые значения (здесь – по умолчанию, для конфига без бутлоадера), если начать менять настройки распределения памяти, начнет изменяться и этот файл. Синтаксис тоже понятен, комментарии и разбивка на группы скобками. Что ж, попробуем сгенерить такое самостоятельно, будем создавать файл bootloader.sct для размещения в основной папке проекта:

Файл будем делать с помощью еще одного батника, назовем его, например, pre_build.bat и поместим также в папку проекта. Содержание следующее:

:: режим расширенных команд

SetLocal EnableExtensions

:: ищем в исходнике незакомментированную строку включения бутлоадера и выводим ее в файл

findstr /b /c:"#define START_ADDRESS_WITH_BOOTLOADER" sourcemain.c > temp.txt

:: определяем размер полученного файла, заносим его в переменную size

FOR %%i in (temp.txt) do (set /a size=%%~Zi)

:: если размер не 0, то делаем прошивку с бутлоадером, если 0 - обычную

IF "%size%"=="0" (goto NOBOOT) else (goto BOOT)

:BOOT

:: выделяем из файла строку с адресом

FOR /F "tokens=3" %%a in (temp.txt) do (set address=%%a)

:: выводим в scatter

echo LR_IROM1 %address% 0x00010000 {ER_IROM1 %address% 0x00010000 {*.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO)} RW_IRAM1 0x20000000 0x00002000 {.ANY (+RW +ZI)}} > bootloader.sct

:: прыгаем в конец

goto END

:NOBOOT

:: если без бутлоадера - выводим scatter "по умолчанию"

echo LR_IROM1 0x08000000 0x00010000 {ER_IROM1 0x08000000 0x00010000 {*.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO)} RW_IRAM1 0x20000000 0x00002000 {.ANY (+RW +ZI)}} > bootloader.sct

:END

:: удаляем временный файл

del temp.txt

В принципе, вроде как откомментировал и должно быть понятно, что там происходит. Анализируем main.c, если в нем есть активный define конфигурации с бутлоадером и смещение для нее, генерируем соответствующий scatter. Вывод в файл сделан одной строкой (так проще), на работоспособность файла это никак не влияет. Запускать его вручную мы, опять же, не станем, идем в свойства проекта на закладку «User» и добавляем следующее:

Т.е. теперь при каждой компиляции у нас сначала будет создаваться нужный на данный момент scatter, потом генерироваться прошивка и, наконец, произойдет подготовка hex’а для совместимости с AvrProg. И все это в полностью автоматическом режиме. Таким образом, тип прошивки определяется одним define’ом в исходнике, при разработке и отладке используем обычную конфигурацию, при окончательной подготовке под бутлоадер - просто раскомментируем одну строчку.

Уж не знаю, много ли здесь практической ценности и можно ли все это использовать прямо в таком виде – наверняка, все можно сделать гораздо проще - но меня эта связка иногда выручает. Кроме того, появились наработки по бутлоадеру STM32, так что теперь его можно будет написать в любом необходимом виде и с любым нужным функционалом.
Ну а вопросы, пожелания и предложения, как всегда, в Форум


Файлы:
AVR_bootloader
RS485_Converter_Code
RS485_Converter_board
STM_bootloader
chipdef.h
format_hex.bat
pre_build.bat


Все вопросы в Форум.


Как вам эта статья?

Заработало ли это устройство у вас?

18 11 4