Эта статья является продолжением AVR BootLoader в вопросах и ответах. Часть 1
Говорят: «Нет, нет и 100% определенно нет».
Хоть технически это возможно, но ответ все равно — «Нет». Ваш загрузчик будет гораздо более надежным, если он имеет нулевую зависимость от приложения. Основная цель загрузчика — стереть и перепрограммировать приложение. Вы же не хотите выполнять вызовы участков кода приложения в то время как стираете его содержимое.
Плохой практикой также считается хранить код, относящийся к загрузчику в разделе RWW. Не существует полного доказательства или способа защиты от случайного стирания и перепрограммирования этой области. Одно полное стирание RWW и загрузчик потенциально становится бесполезным.
Да, сделать это довольно легко. Особенно если код для совместного использования не имеет доступа к глобальным переменным. Только не пытайтесь достичь общего кода путем создания загрузчика и приложения в качестве одного двоичного файла. Лучше всего строить их по отдельности и использовать указатели на функции общего кода.
.section .jumps,"ax",@progbits // The gnu assembler will replace JMP with RJMP when possible .global _jumptable _jumptable: jmp shared_func1 jmp shared_func2 jmp shared_func3
JUMPSTART = 0x3FE0 # 32 bytes from the end of the AT90USB162 4kb boot section LDFLAGS += -Wl,--section-start=.jumps=$(JUMPSTART)
-ffunction-sections
совместно с флагами линковщика --gc-sections
и --relax
. Поэтому если вы не уверены, то в любом случае это не помешает добавить:LDFLAGS += -Wl,--undefined=_jumptable
typedef void (*PF_VOID)(void); typedef void (*PF_WHATEVER)(uint8_t); static __inline__ void call_func1(void) { ((PF_VOID) (0x3FE0/2))(); } static __inline__ void call_func2(void) { ((PF_VOID) (0x3FE2/2))(); } static __inline__ void call_func3(uint8_t arg) { ((PF_WHATEVER) (0x3FE2/2))(arg); }
call_func3(1);
Так как у вас два совершенно разных двоичных файла, то каждый будет иметь свое собственное распределение памяти. Позиции глобальных переменных в приложении никак не связаны с позициями глобальных переменных в
загрузчике.
Допустим, общая функция загрузчика по ошибке производит чтение и запись в глобальную переменную напрямую.
Когда загрузчик был отлинкован, пространство глобальных переменных по сути было жестко запрограммированно в его исполняемый код. Когда загрузчик исполняется, то его позиция правильная, и все работает нормально. Но когда запускается приложение, которое вызывает те же функции, то жестко заданные позиции глобального пространства уже совсем не те. Создается неправильная ситуация, т.к. когда приложение исполняется, то ее распределение памяти отличается от распределения памяти загрузчика. Таким образом, вы не можете получить непосредственный доступ к глобальным переменным из общих функций без потенциальной угрозы уничтожения данных в приложении.
// In a shared header file typedef struct { uint8_t val1; uint16_t val2; } globals_t; // The shared function void func4(globals_t *vars) { vars->val1 = 0; vars->val2 = 512; } // Globally defined in each binary globals_t g_vars; // Calling the shared function call_func4(&g_vars);
Если по каким-то причинам вы не можете передать параметр в общую функцию см. вопрос № 17 ниже.
На самом деле это проще сделать, чем организовать общие функции, поскольку векторы прерывания по умолчанию уже обеспечивают переход чтобы найти процедуру обработки прерывания (ISR). Достаточно написать и откомпилировать привычным способом в загрузчике ISR (только без доступа напрямую к глобальным переменным). После этого необходимо в дизассемблированном файле загрузчика найти таблицу прерываний. Записать адрес вектора ISR, который необходимо сделать общим. Вам не потребуются адреса обработчиков из этого вектора. Вам нужет адрес самого вектора прерываний. Его адрес будет в начале загрузчика.
// This must be declared "naked" because we want to let the // bootloader function handle all of the register push/pops // and do the RETI to end the handler. void USB_GEN_vect(void) __attribute__((naked)); ISR(USB_GEN_vect) { asm("jmp 0x302C"); }
Такая же ситуация как и в вопросе №15, но на этот раз решение не такое простое, поскольку вы не можете поместить в стек обработчика ISR указатель. Есть целый ряд возможных решений, но, поскольку это руководство уже довольно обширно об этом будет сказано не так подробно. Есть два варианта, таких как использование регистров GPIO для передачи указателей на глобальные или резервирование части SRAM с известным адресом, где расположены глобальные данные. Если вам нужен иной способ, то вам сюда: http://tinyurl.com/q3fpud . Идея с зарезервированной областью SRAM, кажется неплохой.
Да, зачастую таким образом можно сэкономить 100 и более байт загрузчика. Эта также относиться и к обычным приложениям, но такая экономия для загрузчика более существенна чем к приложению. Некоторые архитектуры AVR имеют 40 или более прерываний, каждый из которых в таблице векторов прерываний принимает по 4 байта. Вы можете не только незначительно сэкономить, но и переопределять неиспользуемые вектора как переходы на общие функции (обсуждается в вопросе № 14).
.section .blvects,"ax",@progbits .global __vector_default __vector_default: jmp __init
LDFLAGS += -T bootloader.x # Or whatever you named the linker script
.text : { *(.vectors) KEEP(*(.vectors))
Добавьте строчку DISCARD и замените «vectors», на ваше имя секции:
/DISCARD/ : { *(.vectors); } /* Discard standard vectors */ .text : { *(.blvects) /* Position and keep custom vectors */ KEEP(*(.blvects))
Если загрузчик использует таблиуц прерываний ISR, вы можете сэкономить на размере прошивки заменив полную таблицу прерываний на уменьшенную версию. Допустим, нам необходимо только 11-е прерывание (USB на AT90USB162), поэтому мы усечем таблицы и повторно задействуем слоты до прерывания 11 в качестве переходов на общую функцию. При этом убедитесь, что положение оставшихся векторов прерываний правильно. Каждый должен быть нацелен на адрес указанный для этого прерывания в даташите (‘nop’ы в примере ниже). Повторно задействуя часть таблицы векторов вы можете избежать необходимости в таблице с отдельными переходами, как рассказано в вопросе №14. Вам просто потребуются указатели на функции для повторного вызова векторов. См. комментарии ниже для более подробной информации:
.section .bootvect,"ax",@progbits ; Custom vector table that eliminates wasted space after the last used ; vector (__vector_11, usb general). Also re-purpose the unused space ; between the reset vector and the usb vector for the jumps to shared ; code. ; .global __vector_default ; There are 21 "word" spaces between __init and __vector_11. This fits ; 21 RJMPs or 10 JMPs. Since the bootloader is only "2K words" long, ; use RJMPs. ; - Don't change the order of these (unless it is before any devices ; shipped)! ; - Add new entries by replacing nop's ; - Remove entries by replace them with nop's (without reordering) __vector_default: rjmp __init ; 0x3000 !used interrupt! rjmp shared_func1 ; 0x3002 rjmp shared_func2 ; 0x3004 rjmp shared_func3 ; 0x3006 rjmp shared_func4 ; 0x3008 rjmp shared_func5 ; 0x300a nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop rjmp __vector_11 ; 0x302C !used interrupt!
По сути, загрузчик — это просто обычное приложение AVR, которое расположено в специальной области флэш-памяти. В простейшей форме, приложение можно сделать загрузчиком AVR путем указания дополнительного флага компоновщику, который необходимо добавить в свой Makefile:
LDFLAGS += -Wl,--section-start=.text=0x3000
Этот флаг указывает начальный адрес байта из загрузчика. Вы можете найти это значение в даташите для своего типа AVR. Убедитесь, что вы используете адрес байта, а не адрес слова (большинство даташитов от Atmel определяют адреса флеш-памяти списком из слов, но некоторые списками байт). Вместо жесткого указанного начального адреса загрузчика я предлагаю объявить константу в файлеMakefile:
BOOTSTART = 0x3000 LDFLAGS += -Wl,--section-start=.text=$(BOOTSTART)
Это описано в даташитах, но название этих терминов может запутать. NRWW и RWW обозначают разные области флэш-памяти микроконтроллеров и определяют что происходит с процессором в то время как к указанной области памяти происходит обращение на чтение или запись. Загрузчик всегда находится в в области no-read-while-write или NRWW (буквально не читать пока писать). Очень часто область NRWW резервируют полностью под загрузчик, но это вовсе не обязательно. Когда происходит стирание или запись в области памяти NRWW процессор останавливается, потому что задействуется режим «не-чтения» (no-read). Сами приложения, как правило, хранятся в области read-while-write (RWW) что буквально означает читать пока писать. Пока производится запись или стирание в области RWW процессор может продолжать работать до тех пор, пока процессор исполняет код расположенный в разделе NRWW (в области загрузчика). Пока область памяти RWW перепрограммируется любые попытки считать из нее данные будут возвращать 0xFF. Прежде чем область RWW снова станет доступна для чтения после программирования область необходимо переактивировать (подробнее об этом в вопрос № 3).
Каким образом загрузчик получит программу для перепрограммирования контроллера зависит от вас. Распространенными каналами связи являются UART и USB. Протокол обмена данными по каналу может быть собственный или стандартный, наподобие AVR109 или DFU. При реализации стандартных протоколов можно использовать уже существующие инструменты, такие как AVR Studio, работающий по протоколу AVR109 или Atmel’s Flip работающий по протоколу DFU. Тем не менее эти стандартные протоколы как правило немного раздуты, а при реализации простого пользовательского протокола загрузчик как правило будет меньше размером и чище. Правда в таком случае вам понадобится также разработать собственное решение для передачи данных загрузчику.
_safe
, чтобы не забыть применять ожидание готовности MCU;boot_page_fill
принимает адрес в байтах, но пишет слово за один раз. В цикле необходимоboot_rww_enable
С AVR это невозможно. Для этого вам потребуется внешний программатор.
Несмотря на свое название макрос BOOTLOADER_SECTION не является полезным при написании загрузчика. Этот макрос просто помогает вам переназначить позицию вашей функции в раздел NRWW флеш-памяти. Вероятно, для этого макроса более подходящим названием является что-то вроде NRWW_SECTION.
Так как загрузчику не просто обновить самого себя, а приложения практически никогда не изменяют загрузчик, то вам потребуется внешний программатор для записи загрузчика в микроконтроллер. В качестве примера программаторов можно привести STK500, AVRISP, AVR Dragon, JTAGICE MKII и т.д. Режимы программирования вы будете задавать в зависимости от вашего типа AVR. Вам не нужно делать ничего особенного, чтобы прошить в микроконтроллер загрузчик. Если у вас есть рабочий внешний программатор, который может заливать прошивки и менять фьюзы, то он вполне подойдет и для заливки загрузчика. Программатор просто читает файл прошивки с загрузчиком и записывает его по указанному адресу. Программатору все равно, что вам посчастливилось писать в область NRWW флеш-памяти.
Так как, надеюсь, вы создали отдельно приложение и загрузчик отдельно, то в конечном итоге у вас появится два отдельных шестнадцатеричных файла с прошивками (например, app.hex и boot.hex). Возникает вопрос: как залить их обе в AVR. Существует несколько вариантов:
В два этапа: Залейте загрузчик с помощью внешнего программатора как описано в Вопросе № 6. Возможно, вам при этом потребуется также установить требуемые значения фьюзов включаяBOOTSZ
иBOOTRST
. После этого можно просто использовать обычный механизм связи загрузчика для передачи и прошивки в микроконтроллер приложения.В один этап: Объединить файлы app.hex и boot.hex в один и использовать внешний программатор для записи объединенного файла в микроконтроллер. Возможно, вам при этом потребуется также установить требуемые значения фьюзов включаяBOOTSZ
иBOOTRST
.
srec_cat
. Этот инструмент входит в набор инструментов srecord
. Он поставляется с WinAVR и очень легко устанавливается на Unix-подобных ОС. Вот команда, которую вы должны при этом использовать:srec_cat app.hex -I boot.hex -I -o combined.hex -I
Также вы можете вручную объединить файлы app.hex и boot.hex файлов с небольшими правками:
Да, для этого достаточно сказать процессору, чтобы тот использовал вектор прерываний, расположенный в области загрузчика а не в области приложения. Для этого где-то в начале кода загрузчика (до использования прерываний), надо добавить строки вроде этих:
MCUCR = (1<<IVCE); MCUCR = (1<<IVSEL);
После чего во время прерываний программный счетчик будет переведен в соответствующее положение согласно таблице прерываний загрузчика. Для нормальной работы, приведенная выше последовательность кода должна быть скомпилирована с включенной оптимизацией, также вручную требуется удостовериться, что в результирующую сборку записаны команды, выполняемые за 4 машинных цикла.
Для лучшей работы загрузчиков необходимо чтобы они представляли собой небольшие, надежные и редко изменяемые программы. С того дня как вы отправили свое устройство в эксплуатацию вам действительно больше не захочется изменить на нем загрузчик. Приложение же наоборот требует больше свободы в плане обновления. Если в приложении возникает ошибка, приводящая к отказу или зависанию, или во время заливки прошивки произойдет сбой питания надежный загрузчик, который будет запускаться первым сможет без проблем исправить данную ситуацию.
Есть много возможностей. Если устройство имеет кнопку или другой механизм ввода, вы можете послать соответствующий сигнал нажатием. Тогда загрузчик, как правило, запустит приложение сразу. А в случает если кнопка будет не нажата во время сброса, то загрузчик продолжает исполнение. Это пример того как работает загрузчик чипа STK500.
// Bootloader code const uint8_t app_run = eeprom_read_byte(ADDR_APP_RUN); if(app_run == APP_RUN) { // In case the app is faulty, clear the eeprom byte so that // the BL will run next time. A properly running app should // set this back to APP_RUN. eeprom_write_byte(ADDR_APP_RUN, 0xFF); run_application(); } // Only run the bootloader once, then go back to the app // (comment out the next line during app development) eeprom_write_byte(ADDR_APP_RUN, APP_RUN);
Затем где-то подальше в приложении, в той части кода, при обработке которого вы уже точно уверены, что приложение работает правильно добавьте следующее:
// Application code eeprom_write_byte(ADDR_APP_RUN, APP_RUN);
Это работает следующим образом: если приложение было с успехом исполнено ранее, то загрузчик будет читать
байт APP_RUN подготавливать к переходу к исполнению приложения. Однако незадолго до запуска приложения байт APP_RUN очищается. Приложение затем снова пишет байт APP_RUN в тот момент когда она считает, что она работает правильно. Таким образом последовательность повторяется. Если с приложением до сброса произошел сбой до того момента как оно производит сброс байта APP_RUN, то после перезагрузки загрузчик будет оставаться на исполнении. Если вы закомментируете указанную выше строку в коде загрузчика, то загрузчик будет оставаться на исполнении только один раз, а в следующие разы после сброса будет запускать приложение.
Существует на самом деле только один способ, вы должны переместить программный счетчик в начало приложения (которое, как правило, является вектором сброса). Если у вас есть приложение на основе
обычной библиотеки AVR-Libc, то в начале вашего приложения будет расположен рантайм языка с, который инициализирует стек и глобальные переменные, а затем располагается ваша функция «main». Вы можете переместить счетчик программы с помощью ассемблерной команды jump или вызовом функции, расположенной по нулевому адресу. Я выбрал команду jump:
asm("jmp 0000");
Disable_interrupt(); Wdt_change_16ms(); while(1);
Disable_interrupt(); // Shutdown USB cleanly Usb_detach(); Usb_disable(); Stop_pll(); // Put interrupts back in app land MCUCR = (1<<IVCE); MCUCR = 0; // Run the applicat
Продолжение: AVR BootLoader в вопросах и ответах. Часть 2