Создание Bootloader’a в AVR мк
Введение
На написание данной статьи меня сподвигло практически полное отсутствие какой либо вменяемой информации по теме бутлоадеров на русском языке, и конкретно для чипов основанных на архитектуре AVR. В общем то DI как то писал о вкусностях этих тулз для пользователей будь то мобила, либо девайс в труднодоступном месте, но процесс работы самого кода подробно не был рассмотрен.
И в один прекрасный день мне на работе дали партийное задание — разработать систему позволяющую дистанционно обновлять прошивку кое-каких устройств, сами железки стоят под взрывозащитными кожухами в шахтах на значительной глубине. Лазить туда и разбирать каждый девайс чтобы воткнуть шлейф ISP понятное дело не самая лучшая идея, однако устройства соединены интерфейсом RS485 это позволяет использовать бутлоадер в проекте.
Конечно можно взять один из OVER чем 9000 готовых бутлоадеров на Сях и доработать напильником, переделать под задачу, но мне давно было интересно разобраться в теме самопрошивки МК. И, думаю, не только мне, поэтому вооружившись даташитом и найдя скудную документацию на утилиту AVRprog я сел за AVR Studio изобетать велосипед — писать свой загрузчик. Естественно на асме (под 8ми битки только на асме пишу).
Так, для разгрева, разработаем проект бутлоадера с прошивкой по RS232 и поддержкой протокола AVRprog v1.4, а дальше можно его заточить хоть под I2C или SPI, RS485 и т. д.
WARNING
Для того чтобы полностью вкурить о чём тут говорится - рекомендуется к прочтению статья DI HALT о использовании бутлоадеров.
Поехали!
Итак, первое, что нам надо сделать, кроме создания нового прожекта, это в меню AVR Studio выбрать опцию отладки бутлоадера, чтобы студия стартовала с адреса первой инструкции бутлоадера, а не 0×0000 для этого (Debug->AVR Simulation Options) выставляем флажок Enable boot reset и стартовый адрес на 0×1E00 – наш загрузчик будет занимать 512 команд.
Теперь накидаем простейший исходник – скажем чтоб выплёвывал на UART 9600/8/n/1 пару символов, это будет юзерское приложение (здесь и далее огрызки кода, подробней в полном исходнике из архива).
Code
Reset:
;Тут код инициализации
…
main:
ldi R16, 0x55
rcall SendUART
ldi R16, 0xAA
rcall SendUART
rcall Delay
jmp main
Далее начнём писать сам лоадер. Создаём новый модуль пропишем простенькую функцию инициализации UART’а на скорость 19200 (тут надо прописать отдельную инициализацию УАРТа отправки и приёма – надеюсь помните, что если вы используете какую-либо уарт либу из основной области памяти – она будет заблокирована в процессе прошивки и затёрта). Так ну и незабываем про адрес старта, и условие входа в бут загрузчик, стартовый код будет выглядеть примерно так:
Code
.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 не подхватит ваш чип:
Code
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'
Далее по коду запросы на программирование, но о них позже, а пока что рассмотрим обработчики для этих событий на примере нескольких:
Code
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
Ну чтож вот мы и подошли к самому интересному – работа с флешем.
Запросы на работу с памятью выглядят так:
Code
cpi R16, 'A'
breq SetAddress ; Установка адреса страницы для чтения/записи
cpi R16, 'g'
breq ReadBlock ; Запрос на буферизированное чтение сектора
cpi R16, 'B'
breq BufferedWrite ; Буферизированная запись
cpi R16, 'e'
breq EraseChip ; Очистить чип
Теперь рассмотрим обработчики на это дело. Чтобы понять смысл следующего кода надо представлять себе принцип работы команды SPM – запись в память программ – я объясню как это работает на примере очистки чипа – дальше всё просто как 2 рубля.
Code
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 – непосредственное слово для записи в буфер флеша.
Code
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 – указатель на начало оперы 0×100
в ZH:ZL – указатель на страницу флеша
WritePage2Flash:
ldi R24, low(PAGESIZEB) ; счётчик
ldi R25, high(PAGESIZEB)
Write2FlashBuffer:
ld R0, Y+
ld R1, Y+
ldi R17, (1< ; флеша - в 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< rcall SPMnWAIT
ldi R17, (1< rcall SPMnWAIT
ret
Вот так всё на самом деле просто выглядит!
Ну и часть обработчика вызывающего этот код:
…
тут очистка памяти и приём одной страницы через 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 иногда наставляет нас на путь истинный, говоря конкретно в какую страницу писать, и, казалось бы, тут всё просто и понятно, но как всегда подводные камни они везде…
SetAddress:
rcall ReadUART
mov ZH, R16
rcall ReadUART
mov ZL, R16 ; ну всё просто сохраняем в регистры ZH:ZL адрес
;куда писать и не трогаем его больше, а н-нееет…
lsl ZL ; адрес надо преобразовать в байтовый т. е. сдвинуть на 1 битик влево =)
rol ZH
OutUART 0xD ; рапортуем об успешной установке адреса
Злоключение или это ещё не конец?
Таким образом мы рассмотрели наиболее ключевые моменты. На первый взгляд процедура самозаливки кода может показаться сложной, но на самом деле всё очень просто и сводится к реализации интерфейса для программатора - ожидать и выполнять команды. Также следует стараться уделять особое внимание некоторым неочевидным моментам (подводные камни) с которыми возможно столкнуться при написании своего проекта, для этого писалась статья и откомментирован исходник.
BootExample.zip
Кстати за кадром осталась работа с прерываниями в бутлоадере (которые могут быть перемещены в секцию бутлоадера, если вы конечно их используете), установке фьюз битов, ну или перезаписи самого бутлоадера самим собой (до конца не разобрался, но такое извращение судя по всему поддерживается) –- это оставляю в качестве домашнего задания читателю. А если и возникнут сложности, либо найдутся глюки в программе, (что весьма вероятно т. к. код толком не обкатан, но как говорится – отладчик всему голова) то, возможно, распишу это всё в отдельной статье.
Автор: Гринёв Роман aka Exp10der.
Источник: http://easyelectronics.ru