Создание Bootloader’a

Введение
На написание данной статьи меня сподвигло практически полное отсутствие какой либо вменяемой информации по теме бутлоадеров на русском языке, и конкретно для чипов основанных на архитектуре AVR.

В общем то DI как то писал о вкусностях этих тулз для пользователей будь то мобила, либо девайс в труднодоступном месте, но процесс работы самого кода подробно не был рассмотрен.

И в один прекрасный день мне на работе дали партийное задание — разработать систему позволяющую дистанционно обновлять прошивку кое-каких устройств, сами железки стоят под взрывозащитными кожухами в шахтах на значительной глубине. Лазить туда и разбирать каждый девайс чтобы воткнуть шлейф ISP понятное дело не самая лучшая идея, однако устройства соединены интерфейсом RS485 это позволяет использовать бутлоадер в проекте.

Конечно можно взять один из OVER чем 9000 готовых бутлоадеров на Сях и доработать напильником, переделать под задачу, но мне давно было интересно разобраться в теме самопрошивки МК. И, думаю, не только мне, поэтому вооружившись даташитом и найдя скудную документацию на утилиту AVRprog я сел за AVR Studio изобетать велосипед — писать свой загрузчик. Естественно на асме (под 8ми битки только на асме пишу).

Так, для разгрева, разработаем проект бутлоадера с прошивкой по RS232 и поддержкой протокола AVRprog v1.4, а дальше можно его заточить хоть под I2C или SPI, RS485 и т. д.

WARNING
Для того чтобы полностью вкурить о чём тут говорится — рекомендуется к прочтению статья DI HALT о использовании бутлоадеров.

Поехали!
Итак, первое, что нам надо сделать, кроме создания нового прожекта, это в меню 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 		; рапортуем об успешной установке адреса

Злоключение или это ещё не конец?
Таким образом мы рассмотрели наиболее ключевые моменты. На первый взгляд процедура самозаливки кода может показаться сложной, но на самом деле всё очень просто и сводится к реализации интерфейса для программатора — ожидать и выполнять команды. Также следует стараться уделять особое внимание некоторым неочевидным моментам (подводные камни) с которыми возможно столкнуться при написании своего проекта, для этого писалась статья и откомментирован исходник.

BootExample.zip

Кстати за кадром осталась работа с прерываниями в бутлоадере (которые могут быть перемещены в секцию бутлоадера, если вы конечно их используете), установке фьюз битов, ну или перезаписи самого бутлоадера самим собой (до конца не разобрался, но такое извращение судя по всему поддерживается) –- это оставляю в качестве домашнего задания читателю. А если и возникнут сложности, либо найдутся глюки в программе, (что весьма вероятно т. к. код толком не обкатан, но как говорится – отладчик всему голова) то, возможно, распишу это всё в отдельной статье.

Ну и вопросы, предложения, критика принимается на мыло exp10der (ящик на mail.ru) или в комменты.

Гринёв Роман aka Exp10der.

38 thoughts on “Создание Bootloader’a”

  1. Спасибо за статейку, надеюсь в будущем пригодиться, т.к. прошивать по отличному от spi интерфейсу, есть весьма полезная функция.

  2. Подробненько и понятно. Хоть до этого только слышал о такой возможности как бутлоадер. Только вот не понял, а как должны быть настроены «клиенты»? Там тоже определенный код писать, или ненадо?

    1. Тут совместимость с AVRProg — утилитка в составе AVRStudio.

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

  3. Хочу еще добавить, что если не планируется позволять бутлоадеру перешивать самого себя, то лучше запретить запись в область бутлоадера через фьюзы. Иначе ошибка адресации может привести к тому, что бутлоадер накроется медным тазом, что не есть хорошо.

    1. Можно конечно и вручную смотреть куда и что пишешь, но для надёжности вообще это имеет смысл.

  4. Приветствую.
    Это всё конечно хорошо, со студии загрузить прошивку.
    Только для коммерческих девайсов не применимо. Прошивку в открытом виде не даш для обновления. Могут и девайс потом целиком скопировать.
    В своё время карячились с загрузчиком и программой для обновления на винды.
    В итоге — транспорт между девайсом и компом криптованный. Ключ расшифровки вычисляется на лету из данных в пакете. Сама прошивка тоже криптованая. В хэксе выглядит как набор мусора.Алгоритм расшифровки только у автора прошивки и в самом загрузчике, который не прочитаеш из-за локов. Как в прочем и весь камень целиком. С клиентовской программы-загрузчика тупо валим пакеты из криптованного файла прошивки и загрузчик , раскриптовывает сначала транспорт, потом сами данные пакета по своему алго.
    Получилось более-менее приемлимо для коммерческого использования. Ни скан , ни прямой анализ результата уже не дают. Главное не забывать локи ставить :-)
    Разработать бы такое в более применимом варианте под разные процы, усложнить криптование — было бы очень удобно , для прошедших этап работы с загрузчиком и желающим реально применить его в коммерческом девайсе.

    1. Цель статьи, объяснить как происходит в принципе селф-прог, а навороты вроде шифрования, утилит для прошивки и т. п. уже на совести читателей :)

    2. Кстати, у мастеркита прошивки тоже криптованные. По крайней мере на набор NM8036.
      Отсюда вопрос — а с помощью, например, jtag можно посмотреть содержимое бутлоадера?

  5. Не совсем понял в каких случаях он необходим? Когда программа в МК повреждается? Чтобы обновить или зашить другую?
    Еще вопрос, возможно ли из МК считать прошивку, если в нем установлены фьюзы, которые не дают это сделать? Какова их надежность для «пытливых» умов?

      1. Зато по обычному ISP(spi) они меняются на ура =) (типа как вариант извращённого бутлоадера на отдельном контроллере который шьёт основной по ISP) думаю в следующей статье расписать прошивку чз ISP кстати..

        1. Если при передачи прошивки по 485 каналу по дороге пару байтиков запорется от какой нибудь наводки. Бутак так и запишет во флеш что принял? Есть ли необходимость проверки прошиви на правильность ее принятия? Как это лучше сделать? через бит четности каждого байта? А есть ли возможность у avrPROG или у AVRDUDE принять команду от бутака что типа байт пришел кривой пошли мне его заново. Вообще поддерживается ли этими прогами кака я то система проверки правильности принятия прошивки. Представляю что будет если в прошивке хотябы один байтик не тот запишется, этож тогда будет полный армагедец.

          1. Автоматом идет чтение записанного и сравнение с исходной HEX-иной (верификация). Что там будет если не совпало — хз, но, как минимум, сообщение выдать должно.

  6. Я тоже, по долгу службы, делал бутлоадер для mega64. Лежит вот здесь: http://files.mail.ru/GYDNK3
    Это загрузчик, который я когда-то нашел на просторах сети, для mega16. Я его несколько допилил для mega64 и облагородил. Для передачи он использует протокол XModem, который поддерживается большинством гипертерминалов (в том числе и встроенным Windows HyperTerminal). Для нормальной работы вам остается задать условия запуска (все равно они всегда у всех свои индивидуальные), настроить скорость RS и… наверно все. Все это легко найти в исходниках, в самом начале.
    Загрузчик, конечно, не слишком-то оптимизирован, но все же в 512-байтовую boot-область влазит без проблем.
    Если будут вопросы — рад помочь, или вместе попереживать =) itis0q@mail.ru

  7. DI привет, заметил что AVR prog делает верификацию после прошивки МК, что это и счем ее едят? И как это работает? Какая то система проверки прошивки на правильность ее записи? По какому принципу проверка происходит?

  8. В BootLoader можно войти и из основной программы по какому-нибудь условию.
    Например, при какой-то определённой последовательности или команде по UART.
    У меня BootLoader на 512 слов. Его адрес начала есть в *****def.inc
    Числится, как SECONDBOOTSTART (для 512-словного бута)
    Я сделал так:
    .equ BOOTLOAD_ADDR = SECONDBOOTSTART
    ;………
    ;……
    ; Условие….
    jmp BOOTLOAD_ADDR

    1. Теоретически можно, но если основная программа будет написана неправильно (кто даст гарантию) тогда войти в бутлоадер будет невозможно. Имеет смысл оставлять «черный ход» для входа в бутлоадер. Тогда основная программа може тсчитать что контроллер в ее единоличном распоряжении.

    1. Ага я тоже об этом мечтаю :) На самом деле скорей нет чем да. Перезапись большими блоками да еще куча гемора для этого.

  9. Вопрос к DI и к Exp10der. Мужики есть ли у вас готовый бутак под мегу 16. Только чтоб он работал через RS485 интерфейс. Тот который предлагал DI, классный, я его постоянно использую. Но он под RS232. Можно ли его допились под 485 я не знаю. Очень надо!!!

    1. Мой комент с вопросом куда-то не туда вставился…))
      Но и на твой вопрос ответить хотелось. Rs232 и 485 это всего лишь разные интерфейсы, но не протоколы общения между кристаллом и ПК (это не стоит путать!). Один можно преобразовать в другой при помощи микросхемы ST485. Таким образом тебе как раз нужна программа написанная под RS232, а меги 485 на прямую не поддерживают. И если бы даже поддерживали, то программы отличались бы разве что регистром приема/передачи данных UDR.

  10. Я сейчас сам пишу бутлоадер. Пишу его одновременно на С++ вперемешку с С. Но основные функции, такие как стирание и запись страницы сделать там не могу, поэтому хочу подключить часть кода из асма. На нем я никогда не писал, хотя и неоднократно сталкивался. Решил от простого к сложному. Ниже код для стирания страницы. Вроде все как и у других, но компилятор IAR говорит, что инструкция spm не поддерживается (Instruction SPM not supported! ). Зато все остальное работает. В чем может быть дело? Спасибо.

    EraseSPM:
    ldi r31, $F0
    clr r31

    ldi r16, (1<<PGERS)|(1<<SPMEN) //â ðåãèñòð çàãðóæàåì ÷èñëî
    sts SPMCSR, r16 //ïîòîì ýòî ÷èñëî çàãðóæàåì â ðåãèñòð SPMCSR

    spm

    ret

  11. насколько я помню инструкцию SPM можно выполнить только в области загрузчика..
    как вы средствами СИ обеспечиваете это?

    p.s. я сейчас тоже пишу небольшой загрузчик с поддержкой avrprog, но у меню любимый язык асм :-)

  12. К слову, а код который здесь приведен рабочий ?
    чтото я внимательно просмотрел его… у меня большие сомнения на этот счет :-(

    по крайней мере судя по коду… например при инкременте адреса для записи мы Z увеличиваем на PAGESIZE, в самой процедуре записи работаем с PAGESIZE*2.. код точно верен ?
    тоже самое в процедуре стирания страницы (опять адресуем страницы с инкрементом PAGESIZE)

  13. только зарегистрировался и хотел бы сразу сказать огромное спасибо за столь хорошие материалы и учебники для начинающих. пишу именно в данной теме так как есть вопрос. у меня есть задача сравнить два загрузчика. один с исходниками, второй без. естественно залез в машинный код из *.hex от atmega2560 и обнаружил, что такое можно только в старых AVR Studio, но никак не в Atmel Studio. сейчас чтоб облегчить труд хочу научится машинный код переводить в ассемблер без каких либо меток как должно быть в исходниках. нужно только для дальнейшего сравнения такого кода с помощью WinMerge. какими средствами это можно сделать или может есть статья про дизассемблер AVR?

Добавить комментарий

Ваш e-mail не будет опубликован.

Перед отправкой формы:
Human test by Not Captcha