Введение
На написание данной статьи меня сподвигло практически полное отсутствие какой либо вменяемой информации по теме бутлоадеров на русском языке, и конкретно для чипов основанных на архитектуре AVR.
В общем то DI как то писал о вкусностях этих тулз для пользователей будь то мобила, либо девайс в труднодоступном месте, но процесс работы самого кода подробно не был рассмотрен.
И в один прекрасный день мне на работе дали партийное задание — разработать систему позволяющую дистанционно обновлять прошивку кое-каких устройств, сами железки стоят под взрывозащитными кожухами в шахтах на значительной глубине. Лазить туда и разбирать каждый девайс чтобы воткнуть шлейф ISP понятное дело не самая лучшая идея, однако устройства соединены интерфейсом RS485 это позволяет использовать бутлоадер в проекте.
Конечно можно взять один из OVER чем 9000 готовых бутлоадеров на Сях и доработать напильником, переделать под задачу, но мне давно было интересно разобраться в теме самопрошивки МК. И, думаю, не только мне, поэтому вооружившись даташитом и найдя скудную документацию на утилиту AVRprog я сел за AVR Studio изобетать велосипед — писать свой загрузчик. Естественно на асме (под 8ми битки только на асме пишу).
Так, для разгрева, разработаем проект бутлоадера с прошивкой по RS232 и поддержкой протокола AVRprog v1.4, а дальше можно его заточить хоть под I2C или SPI, RS485 и т. д.
Поехали!
Итак, первое, что нам надо сделать, кроме создания нового прожекта, это в меню AVR Studio выбрать опцию отладки бутлоадера, чтобы студия стартовала с адреса первой инструкции бутлоадера, а не 0x0000 для этого (Debug->AVR Simulation Options) выставляем флажок Enable boot reset и стартовый адрес на 0x1E00 – наш загрузчик будет занимать 512 команд.
Теперь накидаем простейший исходник – скажем чтоб выплёвывал на UART 9600/8/n/1 пару символов, это будет юзерское приложение (здесь и далее огрызки кода, подробней в полном исходнике из архива).
1 2 3 4 5 6 7 8 9 10 11 | Reset: ;Тут код инициализации … main: ldi R16, 0x55 rcall SendUART ldi R16, 0xAA rcall SendUART rcall Delay jmp main |
Далее начнём писать сам лоадер. Создаём новый модуль пропишем простенькую функцию инициализации UART’а на скорость 19200 (тут надо прописать отдельную инициализацию УАРТа отправки и приёма – надеюсь помните, что если вы используете какую-либо уарт либу из основной области памяти – она будет заблокирована в процессе прошивки и затёрта). Так ну и незабываем про адрес старта, и условие входа в бут загрузчик, стартовый код будет выглядеть примерно так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | .org 0x1E00 ; Для бутака отвели 512 слов BootLoader: ldi R16, low(RAMEND) ; Никуда без стека out SPL, R16 ldi R16, high(RAMEND) out SPH, R16 ldi R16, 0xFF out DDRC, R16 ; Тут диоды у нас cbi DDRC, 4 ; А тут кнопарь cbi PORTC, 4 sbic PINC, 4 ; Собсно и есть стартовый кондишн – если отпущена кнопка бежим в основную прогу jmp Reset LED3_ON ; Макрос включения светодиода «Режим программирования» rcall InitUART |
Далее идёт цикл с ожиданием байтов от AVRprog и их обработкой – довльно рутинный процесс, но поясню на примере запросов процедур идентификации – без неё AVRprog не подхватит ваш чип:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | Wait4Command: ; Главный цикл – тут получаем команды от AVRprog rcall ReadUART ; Тут всё просто – смотрим что в буфере R16 и идём на обработчик cpi R16, 'S' ; Байтик приветствия – надо на него ответить 7и символьным ; идентификатором начинающимся на “AVRxxxx” – у меня например – “AVRm162” breq Greething cpi R16, 'a' ; Запрос автоинкремента breq AutoInc cpi R16, 't' ; Запрос типа устройства breq DeviceType cpi R16, 'V' ; Запрос версии софта – у меня 0.1 breq SoftwareVersion cpi R16, 'b' ; Запрос поддержки буферизации – отвечаем, что поддерживаем буфер ;размером в одну страницу breq SetBufferInfo cpi R16, 's' |
Далее по коду запросы на программирование, но о них позже, а пока что рассмотрим обработчики для этих событий на примере нескольких:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | AutoInc: OutUART 'y' ; OutUART - макрос вывода байта в UART rjmp Wait4Command SetupLED: rcall ReadUART OutUART 0xD ; На некоторые команды надо отвечать 0xD. rjmp Wait4Command SoftwareVersion: OutUART '0' OutUART '1' rjmp Wait4Command GetSignature: OutUART 0x1E ; байтики сигнатуры OutUART 0x94 ; OutUART 0x04 ; rjmp Wait4Command GetProgramerType: OutUART 'S' ; предствляемся последовательным прошивальщиком =) rjmp Wait4Command SetBufferInfo: OutUART 'Y' ; Ответ на запрос размера буфера (в байтах!) – одна страница OutUART high(PAGESIZEB) ; PAGESIZEB= PAGESIZE*2; OutUART low(PAGESIZEB) rjmp Wait4Command |
Ну чтож вот мы и подошли к самому интересному – работа с флешем.
Запросы на работу с памятью выглядят так:
1 2 3 4 5 6 7 8 9 10 11 | cpi R16, 'A' breq SetAddress ; Установка адреса страницы для чтения/записи cpi R16, 'g' breq ReadBlock ; Запрос на буферизированное чтение сектора cpi R16, 'B' breq BufferedWrite ; Буферизированная запись cpi R16, 'e' breq EraseChip ; Очистить чип |
Теперь рассмотрим обработчики на это дело. Чтобы понять смысл следующего кода надо представлять себе принцип работы команды SPM – запись в память программ – я объясню как это работает на примере очистки чипа – дальше всё просто как 2 рубля.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | EraseChip: ldi R18, 112 ; количество страниц на очистку – зависит от чипа clr ZH clr ZL doErase: ldi R17, (1<<PGERS) | (1<<SPMEN) ; в R17 передаётся параметр в регистр SPMCR ; SPMEN – разрешает вызов команды SPM в следующих 4х тактах; PGERS – команда на очистку ;страницы флеша rcall SPMnWAIT ldi R17, (1<<RWWSRE) | (1<<SPMEN) ; ре-инициализация страницы rcall SPMnWAIT ldi R19, PAGESIZE ; инкремент указателя на страницу add ZL, R19 clr R19 adc ZH, R19 dec R18 brne doErase OutUART 0xD rjmp Wait4Command |
Cама процедура вызова SPM (ВХОД: R17 – SPMCR; ZH:ZL – указатель на страницу флеша для выполнения операций R0:R1 – непосредственное слово для записи в буфер флеша.
1 2 3 4 5 6 7 8 9 | SPMnWAIT: out SPMCR, R17 spm ; Очищаем/пишем во флеш – зависит от параметра SPMCR WaitSPM: in R16, SPMCR sbrc R16, SPMEN rjmp WaitSPM ret |
Формат регистра SPMCR
- Bit 7 – SPMIE: SPM Interrupt Enable – разрешение прерывания по окончании записи во флеш
- Bit 6 – RWWSB: Read-While-Write Section Busy – проверка занятости секции
- Bit 5 – Res: Reserved Bit
- Bit 4 – RWWSRE: Read-While-Write Section Read Enable — реинициализация страницы после записи/очистки
- Bit 3 – BLBSET: Boot Lock Bit Set – операции над фьюзами
- Bit 2 – PGWRT: Page Write – запись странциы
- Bit 1 – PGERS: Page Erase — очистка
- Bit 0 – SPMEN: Store Program Memory Enable – разрешение на исполнение SPM в следующие 4 такта.
Итак, подведём небольшой итог:
- Запись выполняется естественно командой SPM, чтение LPM
- Параметры: указатель — ZH:ZL- куда пишем (либо что очищаем), R0:R1 – слово которое пишем по (Z)
- Регистр SPMCR разрешает и определяет поведение команды SPM
Теперь внимательно рассмотрим процедуру записи флеша:
в YH:YL – указатель на начало оперы 0x100
в ZH:ZL – указатель на страницу флеша
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | WritePage2Flash: ldi R24, low(PAGESIZEB) ; счётчик ldi R25, high(PAGESIZEB) Write2FlashBuffer: ld R0, Y+ ld R1, Y+ ldi R17, (1<<SPMEN) ; Тут будьте внимательны байты пишутся в буфер страниц ; флеша - в SPMCR устанавливаем только SPMEN!!! rcall SPMnWAIT adiw ZH:ZL, 2 ; указатель на текущий адрес записи sbiw R25:R24, 2 brne Write2FlashBuffer ; А ТЕПЕРЬ МОМЕНТ ИСТИННЫ – ЗАПИСЬ СТРАНИЦЫ ВО ФЛЕШ subi ZL, low(PAGESIZEB) ; восстановление указателя sbci ZH, high(PAGESIZEB) ldi R17, (1<<PGWRT)|(1<<SPMEN) ; Даём команду на запись страницы в один приём. rcall SPMnWAIT ldi R17, (1<<RWWSRE) | (1<<SPMEN) ; Ре-инициализируем страницу rcall SPMnWAIT ret |
Вот так всё на самом деле просто выглядит!
Ну и часть обработчика вызывающего этот код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | … тут очистка памяти и приём одной страницы через UART … ; вызываем процедуру записи страницы ldi YH, high(SRAM_START) ldi YL, low(SRAM_START) ; Буфер в начале RAM rcall WritePage2Flash ; Ну незабываем делать автоинкремент страницы – обязательно если при ; инициализации ответили ‘y’ на ‘A’ запрос. ldi R19, PAGESIZE add ZL, R19 clr R19 adc ZH, R19 OutUART 0xD ; говорим AVRprog что записали страницу и всё ок! |
Так ну и рассмотрим ещё один обработчик на этот раз последний. Адресация, AVRprog иногда наставляет нас на путь истинный, говоря конкретно в какую страницу писать, и, казалось бы, тут всё просто и понятно, но как всегда подводные камни они везде…
1 2 3 4 5 6 7 8 9 10 11 | SetAddress: rcall ReadUART mov ZH, R16 rcall ReadUART mov ZL, R16 ; ну всё просто сохраняем в регистры ZH:ZL адрес ;куда писать и не трогаем его больше, а н-нееет… lsl ZL ; адрес надо преобразовать в байтовый т. е. сдвинуть на 1 битик влево =) rol ZH OutUART 0xD ; рапортуем об успешной установке адреса |
Злоключение или это ещё не конец?
Таким образом мы рассмотрели наиболее ключевые моменты. На первый взгляд процедура самозаливки кода может показаться сложной, но на самом деле всё очень просто и сводится к реализации интерфейса для программатора — ожидать и выполнять команды. Также следует стараться уделять особое внимание некоторым неочевидным моментам (подводные камни) с которыми возможно столкнуться при написании своего проекта, для этого писалась статья и откомментирован исходник.
Кстати за кадром осталась работа с прерываниями в бутлоадере (которые могут быть перемещены в секцию бутлоадера, если вы конечно их используете), установке фьюз битов, ну или перезаписи самого бутлоадера самим собой (до конца не разобрался, но такое извращение судя по всему поддерживается) –- это оставляю в качестве домашнего задания читателю. А если и возникнут сложности, либо найдутся глюки в программе, (что весьма вероятно т. к. код толком не обкатан, но как говорится – отладчик всему голова) то, возможно, распишу это всё в отдельной статье.
Ну и вопросы, предложения, критика принимается на мыло exp10der (ящик на mail.ru) или в комменты.
Гринёв Роман aka Exp10der.
Спасибо за статейку, надеюсь в будущем пригодиться, т.к. прошивать по отличному от spi интерфейсу, есть весьма полезная функция.
Подробненько и понятно. Хоть до этого только слышал о такой возможности как бутлоадер. Только вот не понял, а как должны быть настроены «клиенты»? Там тоже определенный код писать, или ненадо?
Тут совместимость с AVRProg — утилитка в составе AVRStudio.
А код при использовании бута пишется как и без него, просто мы не можем юзать верхнюю границу флеша (бутсектор) и надо учитывать тот факт, что некоторые ноги могут быть использованы как точка входа в бут.
Это как раз таки понятно. Непонятно, нужно ли дополнительную настройку делать на клиентах?
Хочу еще добавить, что если не планируется позволять бутлоадеру перешивать самого себя, то лучше запретить запись в область бутлоадера через фьюзы. Иначе ошибка адресации может привести к тому, что бутлоадер накроется медным тазом, что не есть хорошо.
Можно конечно и вручную смотреть куда и что пишешь, но для надёжности вообще это имеет смысл.
Приветствую.
Это всё конечно хорошо, со студии загрузить прошивку.
Только для коммерческих девайсов не применимо. Прошивку в открытом виде не даш для обновления. Могут и девайс потом целиком скопировать.
В своё время карячились с загрузчиком и программой для обновления на винды.
В итоге — транспорт между девайсом и компом криптованный. Ключ расшифровки вычисляется на лету из данных в пакете. Сама прошивка тоже криптованая. В хэксе выглядит как набор мусора.Алгоритм расшифровки только у автора прошивки и в самом загрузчике, который не прочитаеш из-за локов. Как в прочем и весь камень целиком. С клиентовской программы-загрузчика тупо валим пакеты из криптованного файла прошивки и загрузчик , раскриптовывает сначала транспорт, потом сами данные пакета по своему алго.
Получилось более-менее приемлимо для коммерческого использования. Ни скан , ни прямой анализ результата уже не дают. Главное не забывать локи ставить :-)
Разработать бы такое в более применимом варианте под разные процы, усложнить криптование — было бы очень удобно , для прошедших этап работы с загрузчиком и желающим реально применить его в коммерческом девайсе.
Цель статьи, объяснить как происходит в принципе селф-прог, а навороты вроде шифрования, утилит для прошивки и т. п. уже на совести читателей :)
Кстати, у мастеркита прошивки тоже криптованные. По крайней мере на набор NM8036.
Отсюда вопрос — а с помощью, например, jtag можно посмотреть содержимое бутлоадера?
Не совсем понял в каких случаях он необходим? Когда программа в МК повреждается? Чтобы обновить или зашить другую?
Еще вопрос, возможно ли из МК считать прошивку, если в нем установлены фьюзы, которые не дают это сделать? Какова их надежность для «пытливых» умов?
Когда надо обновить прошивку на новую версию. Чтобы не лазать к железке зарытой в землю.
Стесняюсь спросить, а каким образом бут может менять фьюзы?
Легко командой spm, подробности в даташите.
Да ну?
пардон, ступил.. из софта можно только считать фузы.
Ксатти, лок-биты из бутлоадера таки можно менять :)
…не все правда
Ну лок биты и Fuse это несколько разные вещи )
Ясный хер))
Лок биты это биты защиты МК? А фузы это типа настройка МК как ему работать?
Никак, ЕМНИП. Фузы вне его сферы влияния.
Зато по обычному ISP(spi) они меняются на ура =) (типа как вариант извращённого бутлоадера на отдельном контроллере который шьёт основной по ISP) думаю в следующей статье расписать прошивку чз ISP кстати..
Если при передачи прошивки по 485 каналу по дороге пару байтиков запорется от какой нибудь наводки. Бутак так и запишет во флеш что принял? Есть ли необходимость проверки прошиви на правильность ее принятия? Как это лучше сделать? через бит четности каждого байта? А есть ли возможность у avrPROG или у AVRDUDE принять команду от бутака что типа байт пришел кривой пошли мне его заново. Вообще поддерживается ли этими прогами кака я то система проверки правильности принятия прошивки. Представляю что будет если в прошивке хотябы один байтик не тот запишется, этож тогда будет полный армагедец.
Автоматом идет чтение записанного и сравнение с исходной HEX-иной (верификация). Что там будет если не совпало — хз, но, как минимум, сообщение выдать должно.
Я тоже, по долгу службы, делал бутлоадер для mega64. Лежит вот здесь: http://files.mail.ru/GYDNK3
Это загрузчик, который я когда-то нашел на просторах сети, для mega16. Я его несколько допилил для mega64 и облагородил. Для передачи он использует протокол XModem, который поддерживается большинством гипертерминалов (в том числе и встроенным Windows HyperTerminal). Для нормальной работы вам остается задать условия запуска (все равно они всегда у всех свои индивидуальные), настроить скорость RS и… наверно все. Все это легко найти в исходниках, в самом начале.
Загрузчик, конечно, не слишком-то оптимизирован, но все же в 512-байтовую boot-область влазит без проблем.
Если будут вопросы — рад помочь, или вместе попереживать =) itis0q@mail.ru
ZeroQu — ссылка http://files.mail.ru/GYDNK3 — битая
DI привет, заметил что AVR prog делает верификацию после прошивки МК, что это и счем ее едят? И как это работает? Какая то система проверки прошивки на правильность ее записи? По какому принципу проверка происходит?
В BootLoader можно войти и из основной программы по какому-нибудь условию.
Например, при какой-то определённой последовательности или команде по UART.
У меня BootLoader на 512 слов. Его адрес начала есть в *****def.inc
Числится, как SECONDBOOTSTART (для 512-словного бута)
Я сделал так:
.equ BOOTLOAD_ADDR = SECONDBOOTSTART
;………
;……
; Условие….
jmp BOOTLOAD_ADDR
Теоретически можно, но если основная программа будет написана неправильно (кто даст гарантию) тогда войти в бутлоадер будет невозможно. Имеет смысл оставлять «черный ход» для входа в бутлоадер. Тогда основная программа може тсчитать что контроллер в ее единоличном распоряжении.
А вот такой вопрос. Выходит, что на avr можно писать полиморфный код, или я что-то не понял?
Ага я тоже об этом мечтаю :) На самом деле скорей нет чем да. Перезапись большими блоками да еще куча гемора для этого.
Вопрос к DI и к Exp10der. Мужики есть ли у вас готовый бутак под мегу 16. Только чтоб он работал через RS485 интерфейс. Тот который предлагал DI, классный, я его постоянно использую. Но он под RS232. Можно ли его допились под 485 я не знаю. Очень надо!!!
Мой комент с вопросом куда-то не туда вставился…))
Но и на твой вопрос ответить хотелось. Rs232 и 485 это всего лишь разные интерфейсы, но не протоколы общения между кристаллом и ПК (это не стоит путать!). Один можно преобразовать в другой при помощи микросхемы ST485. Таким образом тебе как раз нужна программа написанная под RS232, а меги 485 на прямую не поддерживают. И если бы даже поддерживали, то программы отличались бы разве что регистром приема/передачи данных UDR.
Я сейчас сам пишу бутлоадер. Пишу его одновременно на С++ вперемешку с С. Но основные функции, такие как стирание и запись страницы сделать там не могу, поэтому хочу подключить часть кода из асма. На нем я никогда не писал, хотя и неоднократно сталкивался. Решил от простого к сложному. Ниже код для стирания страницы. Вроде все как и у других, но компилятор IAR говорит, что инструкция spm не поддерживается (Instruction SPM not supported! ). Зато все остальное работает. В чем может быть дело? Спасибо.
EraseSPM:
ldi r31, $F0
clr r31
ldi r16, (1<<PGERS)|(1<<SPMEN) //â ðåãèñòð çàãðóæàåì ÷èñëî
sts SPMCSR, r16 //ïîòîì ýòî ÷èñëî çàãðóæàåì â ðåãèñòð SPMCSR
spm
ret
насколько я помню инструкцию SPM можно выполнить только в области загрузчика..
как вы средствами СИ обеспечиваете это?
p.s. я сейчас тоже пишу небольшой загрузчик с поддержкой avrprog, но у меню любимый язык асм :-)
К слову, а код который здесь приведен рабочий ?
чтото я внимательно просмотрел его… у меня большие сомнения на этот счет :-(
по крайней мере судя по коду… например при инкременте адреса для записи мы Z увеличиваем на PAGESIZE, в самой процедуре записи работаем с PAGESIZE*2.. код точно верен ?
тоже самое в процедуре стирания страницы (опять адресуем страницы с инкрементом PAGESIZE)
у меня заработало только при байтовой адресации записи и стирания страницы…
только зарегистрировался и хотел бы сразу сказать огромное спасибо за столь хорошие материалы и учебники для начинающих. пишу именно в данной теме так как есть вопрос. у меня есть задача сравнить два загрузчика. один с исходниками, второй без. естественно залез в машинный код из *.hex от atmega2560 и обнаружил, что такое можно только в старых AVR Studio, но никак не в Atmel Studio. сейчас чтоб облегчить труд хочу научится машинный код переводить в ассемблер без каких либо меток как должно быть в исходниках. нужно только для дальнейшего сравнения такого кода с помощью WinMerge. какими средствами это можно сделать или может есть статья про дизассемблер AVR?