В этой заметке хочу написать об околозагрузочных вещах, которые мы делали в рамках нашего проекта на основе камня от Analog Devices BF-537 (семейство Blackfin). Постараюсь писать не сильно заумно, чтобы картина была более менее понятна не только специалистам, но и особо пытливым гражданским.
Итак, у нас есть процессор, для которого надо решить задачи хранения, загрузки, исполнения и безопасной обновления программы.
Камень BF-537, что у этого зверя есть в наличии? I/O области у него нет, есть единое плоское 32-х разрядное адресное пространство, в котором наблюдается:
- область, на которую отображается внешняя SDRAM (до 512 метров)
- четыре асинхронные банка памяти по мегабайту каждый (в эти банки обычно подключают параллельную флеш память)
- boot ROM (2k), намертво вшит в процессор, именно с него происходит начало работы камня
- несколько банков, на которые отображается внутренняя RAM камня; в этих областях можно напрямую хранить данные или исполнять код, либо использовать эти регионы как кэши (в случае работы с памятью на внешней шине)
- область с множеством регистров для управления процессором и периферией (в некоторых архитектурах эти вещи размещают в I/O).
![](http://pics.livejournal.com/cd_riper/pic/000qhra1/s640x480)
Перво-наперво решался вопрос "где хранить программу". Параллельный флеш отпал практически сразу, причем не столько из-за цены вопроса, сколько из-за того, что процессор больше 4х метров этого дела адресовать не может, а это очень и очень мало. Поэтому решили использовать флеш последовательный, подключенный к SPI шине.
Как загружается наш процессор? После сброса он начинает выполнять код, зашитый во внутренний ROM, т.н. BOOT ROM (кстати, сырец этого дела прилагается производителем; есть несколько ревизий, каждая из которых отличается той или иной степень забагованности -- о том, как мы боролись с багом бутрома надо писать отдельную, большую и очень интересную историю, чуть больше чем полностью, состоящую из нецензурных выражений). Так вот, в бутроме, сразу после инициализации внутренних банков памяти и прочей мелочевки, лезут смотреть состояние специальных ножек процессора, которые выбирают режим загрузки. В числе доступных вариантов -- SPI, UART, флеш память в асинхронной области.
![](http://pics.livejournal.com/cd_riper/pic/000qk6rp)
Поток данных, который ожидается из этих разных источников имеет специальный, довольно примитивный формат. Грубо говоря, данные разбиты на блоки, каждый блок имеет заголовок, который говорит о том, что с этим блоком делать -- скопировать по такому-то адресу, а если сильно надо, то и выполнить. Собранную линкером программу (которая обычно выполняется в адресах SDRAM), с помощью специального инструмента, нужно преобразовать в такой вот специальный двоичный поток, который бутром умеет грузить по UART или с флеш памяти, подключенной по SPI. В этом деле тоже есть свои нюансы -- обычно добавляется небольшой специальный startup код, в задачи которого входит поднять скорость работы SPI шины или корректно проинициализировать контроллер SDRAM (разумеется, это надо сделать перед тем, как мы начнем грузить туда последующие блоки из потока).
Итак, положим, указали мы внешними ножками бутрому, что грузиться надо с SPI флеш, дык, встает вопрос, откуда в этой флеш памяти возьмется поток данных нашей программы? Засада!
Каждая собранная плата нашего устройства имеет специальный джампер, который переключает режим загрузки процессора: с SPI флеша или с UART.
UART, если кто не знает, это обычный последовательный порт, он же COM порт. По нему раньше к персоналке модемы подключали (если, конечно, кто знает, что такое телефонные модемы). UART -- верный друг embedded разработчика, правда, производители ПК уже много лет подряд всеми силами пытаются у него этого друга отнять (есть же USB!), дык всегда находится тот или иной способ выкрутиться.
Так вот, наши платы умеет читать загрузочный поток с обычного COM порта (читай -- с компьютера). Вот мы вплотную подобрались к первой программе, которая решают вопрос с первоначальной загрузкой процессора.
Со стороны ПК по COM порту передается в виде загрузочного потока тело программы "mini-flasher", которая загружается в SDRAM (кэп подсказывает, эта программа живет ровно до первого сброса процессора). Что умеет делать эта программа? Она по специальному протоколу принимает по UART команды, которые указывают в какие страницы флеш памяти какие данные записать (запись во флеш память это специальная операция, совсем не похожа на запись в обычную память, поэтому мы, к примеру, не может записывать данные во флеш память с помощью загрузочного потока данных, который принимает бутром).
Ага! Самые догадливые в этом месте должны закричать: "Вот! Записали таким макаром, с помощью mini-flasher'а, программу во флеш, поставили перемычку в положение SPI и вуаля -- получаем стартующую после сброса нужную нам программу!".
Все это так, если бы не одно "но", связанное с обновлением программы.
Как известно, любая современная программа должна легко, просто и надежно обновляться пользователем.
Уровень, связанный с обновлением по UART и перемычкой (добраться до которой можно только разобрав девайс), конечно же, предполагается недоступным пользователю. Обновление для пользователя осуществляется с помощью программы, работающей на ПК, и управляющей нашими устройствами. Эта программа, по сети, обнаруживает приборы, сравнивает установленную на них версию прошивки с базой данных прошивок, которые идут в комплекте к этой управляющей программе, и, если это необходимо, перед тем, как начать работать с девайсом, автоматически обновляет на них прошивку. Все это происходит автоматически, без всякого участия пользователя.
Так вот, подумаем о том, что должна представлять из себя программа, которая стартует сразу после загрузки прибора. Очевидно, что помимо функций, непосредственно связанных с ее прямыми обязанностями, в ней должен быть код, подобный mini-flasher'у, который принимает команды по Ethernet, по которым можно писать данные во флеш память -- ведь только таким макаром можно обновить версию программы.
И тут всплывает интересный нюанс. Процесс обновления программы (запись во флеш) процесс отнюдь не мгновенный, и не атомарный. И если что-то пойдет не так, или возникнет какая-то проблема (например, в процессе обновления устройство банально выключат), во флеш памяти может оказаться неработоспособная программа, что автоматически означает и невозможность что-то с ней сделать, так как она сама же должна отвечать за процесс своего обновления. Итог этой ситуации -- у пользователя на руках трупик вместо работающего устройства, вернуть к жизни который может только разработчик.
Как решается эта проблема?
Флеш должен содержать две программы.
Первая программа, назовем ее booter, всегда стартует первая, ее тело во флеш защищено от записи, она содержит в себе функционал, который позволяет записать во флеш тело второй программы и умеет ее запускать.
Вторая программа -- любая записанная с помощью booter программа, которую он, как писалось выше, умеет запускать.
Т.е. фактически мы сделали следующую вещь. Мы разбили функционал исходной монолитной программы на две независимые части -- одна часть (которая стартует первой и находится в защищенной области флеш памяти) содержит в себе функционал, позволяющий записать во флеш вторую часть, которая, собственно говоря, и выполняет полезную нагрузку.
В принципе это все, но хотелось бы еще бегло пробежаться по некоторым особенностям работы нашего бутера.
#1. На самом деле бутер умеет обновлять сам себя. Операция сделана максимально безопасно -- перед тем, как начать что-то писать во флеш, бутер загружает в память полный образ новой версии (в случае записи обычной программы, запись идет поточно) -- и, тем не менее, пользователь может убить при этой операции девайс, т.к. сам процесс записи длиться почти минуту.
#2. Бутер хранит настройки, связанные с работой Ethernet (MAC адрес, настройки IP etc.). Этими же параметрами пользуется и основная программа.
#3. Бутер может управляться не только по сети, но и по UART (запасной выход для разработчиков плюс начальные настройки прибора).
#4. Девайс имеет аппаратную кнопку сброса сетевых настроек на значения по умолчанию (подобная функциональность, к примеру, есть во всех роутерах)
#5. Вопрос загрузки целевой программы бутером решается через API бутрома, который позволяет загрузить программу из SPI флеша с любого смещения (собственно, этот же код работает при старте процессора со смещением 0, если внешними ногами указана загрузка с SPI источника). На первый взгляд, кажется плевым делом реализовать свой аналог бутром функциональности, дык главная засада во всей этой истории заключается в том, что подобный код должен быть предельно компактным, и располагаться по особым адресами во внутренней памяти процессора, т.к. во все остальные области памяти будет происходить копирование тела загружаемой программы.
#6. По факту, у нас на борту стоит два процессора BF-537 -- главный (про который я всю эту историю и рассказывал) плюс "сопроцессор" для ряда вспомогательных ресурсоемких операций, вроде эхоподавления. Так вот, бутер умеет хранить не только тело основной программы, но и прошивку для сопроцессора, для которого он фактически эмулирует режим загрузки с SPI флеша.
#7. При старте бутер некоторое время ждет к себе подключений (по UART или Ethernet) и, если подключение не происходит, то он запускает основную программу. Помимо сетевых настроек, бутер имеет информацию о версии прошивки основной программы. Основная программа, также имеет доступ к этой информации. Таким образом, управляющая программа на ПК может узнать версию прошивки устройства, с которым она общается. Если версию необходимо обновить, то программе подается команда выполнить перезагрузку процессора, после перезагрузки происходит старт бутера, к которому и подключается программа с ПК для выполнения процедуры обновления прошивки.
А вот теперь, пожалуй, все.
зы. Чтобы поднять градус гиковости, выложу часть кода по mini-flasher этапу.
Header, описывающий протокол
http://dl.dropbox.com/u/490401/src/miniflasher/MfProtocol.h Тело программы, обслуживающий протокол со стороны Blackfin
http://dl.dropbox.com/u/490401/src/miniflasher/MfServer.cpp Часть кода со стороны ПК. Примечательна тем, что обычно код мы пишем в FSM стиле, тут же код написали линейно (он предназначен для запуска в отдельном потоке, чтобы не блокировать message loop), и вместо использования интерфейса, принимающего от объекта "протокол" callback для команд соответствующего типа (см. IMfProtocolCallback в MfPcProtocol.h), используется "динамический" тип команды (класс MfPacket).
Дело в том, что код со стороны сервера (MfServer.cpp) практически stateless, т.е. пришла от клиента команда, мы ее отработали и забыли. Со стороны же клиента идет осмысленный последовательный процесс, который рвать на куски в FSM стиле выходит очень накладно -- код очень тяжело писать, а еще тяжелее понять, что он делает.
http://dl.dropbox.com/u/490401/src/miniflasher/LoadImgImpl.cpphttp://dl.dropbox.com/u/490401/src/miniflasher/LoadImgImpl.hhttp://dl.dropbox.com/u/490401/src/miniflasher/MfPacket.hhttp://dl.dropbox.com/u/490401/src/miniflasher/MfPcProtocol.h зы2. Обычно я не потакаю низменным вкусам толпы, но все-таки замечу, что количество комментариев в стиле "автор пиши исчо" или "автор выпей йаду", возможно, некоторым образом повлияет на то, буду ли я в дальнейшем писать подобные вещи или ну его на хуй.