Пишем свой бутлоадер для STM32, продолжение.
Когда дописывал прошлую статью, на радиокоте наткнулся на тему где обсуждали в каком формате передавать прошивку бутлоадеру. У меня тоже возникал такой вопрос и в конце прошлой статьи описал почему передавать прошивку удобнее hex файлом, чем бинарником, кому интересно могут почитать тут.
Значит так, нам надо рассмотреть какие бывают типы записей в hex файле и дополнить код из прошлой статьи. Все описанное ниже справедливо для KEIL и по сути является вольным переводом этой статьи.
Keil генерирует прошивку в формате Intel HEX, в нём хранится шестнадцатеричное представление двоичного файла закодировано с помощью цифробуквенных символов ASCII. В файле с расширением hex может быть любой количество записей, каждая из которых имеет следующий формат:
Группы состоят из одинаковых букв, а каждая буква соответствует одному шестнадцатеричному числу.
00 — cодержит данные
01 — последняя запись в файле
02 — дополнительный адрес сегмента
04 — запись содержит дополнительный адрес, необходимы для вычисления полного адреса
05 — адрес начала приложения, а именно функции main(только для MDK-ARM)
Запись с данными.
- 10 количество байт в записи
- 2462 адрес, начала записи
- 00 тип записи
- 464C...464C данные
- 33 чек сумма записи
Напомню, что данные в hex файле хранятся в шестнадцатеричном представлении и количество байт в записи равняется 16, а не 10 как можно было бы подумать 0x10 = 16. Это относится ко всем остальным полям.
Последняя запись в файле.
- 00 количество байт в записи
- 0000 в данной записи это поле игнорируется.
- 01 тип записи
- FC чек сумма записи рассчитывается как
01h + NOT(00h + 00h + 00h + 01h).
Файл формата Intel HEX должен заканчиваться записью такого типа.
Дополнительный адрес.(HEX386)
- 02 количество байт в записи
- 0000 адресное поле, для данного типа записи всегда равно 0000
- 04 тип записи
- FFFF старшие 16 бит адреса
- FC чек сумма записи рассчитывается как
01h + NOT(02h + 00h + 00h + 04h + FFh + FFh).
Дело в том, что в записи с данными указывается только 16 бит адреса, например 0x2462, а адресное пространство у нас 32-битное. Получается для записи полного адреса не хватает ещё 16 бит, которые как раз и содержатся в этой записи. Для вычисления полного адреса, по которому будут писаться данные, надо адрес из этой записи сдвинуть на 16 бит влево и прибавить к нему адрес указанный в записи с данными.
Оставшиеся два типа записей нам не понадобятся. Запись типа 02 вообще не встретил в своем хексе, она используется в формате HEX86, а у нас, судя по всему, HEX386(чем они отличаются не разбирался). А запись типа 05(адрес начала приложения, а именно функции main) включает в себя 32-битный адрес и может находиться где угодно, а главное, она не несет полезной информации для записи флэш памяти, то есть её можно игнорировать, так написано в документации.
Теперь пользуясь полученными знаниями, обновим код из прошлой статьи.
Таким образом, мы разработали шаблон для написания собственного бутлоадера, который разбирает hex.
Получен он из бутлоадера, которым пользуюсь сам, заменой функции, читающей определенное количество символов из hex файла следующим выражением /*читаем n байт*/. Конечно, его можно оптимизировать и это не готовое решение, но на то чтобы с этим разобраться было потрачено несколько недель и выложено это с целью экономии твоего времени мой дорогой читатель)))
Кстати, при компиляции прошивки, которая будет скармливаться бутлоадеру, надо указать, что она должна располагаться не с начала памяти, а после бутлоадера.
Так как тема достаточно обширная один момент был упущен, а именно перенос векторов прерываний при переходе в основную программу. Дело в том, что в бутлоадере таблица векторов прерываний располагается по одному адресу, а в основной программе по другому(у них не может быть общий вектор прерываний так как это две отдельные программы и отдельно компилируются). Для того, чтобы при переходе в основную программу мы могли работать с векторами прерываний их надо перенести либо перед прыжком в основную программу, либо в начале основной программы.
Значит так, нам надо рассмотреть какие бывают типы записей в hex файле и дополнить код из прошлой статьи. Все описанное ниже справедливо для KEIL и по сути является вольным переводом этой статьи.
Keil генерирует прошивку в формате Intel HEX, в нём хранится шестнадцатеричное представление двоичного файла закодировано с помощью цифробуквенных символов ASCII. В файле с расширением hex может быть любой количество записей, каждая из которых имеет следующий формат:
:llaaaatt[dd...]cc
Группы состоят из одинаковых букв, а каждая буква соответствует одному шестнадцатеричному числу.
- : начало записи
- ll количество данных dd в записи
- aaaa адрес начала записи
- tt тип записи:
00 — cодержит данные
01 — последняя запись в файле
02 — дополнительный адрес сегмента
04 — запись содержит дополнительный адрес, необходимы для вычисления полного адреса
05 — адрес начала приложения, а именно функции main(только для MDK-ARM)
- dd — данные которые пишутся в память МК
- сc — чек сумма
Запись с данными.
:10246200464C5549442050524F46494C4500464C33
- 10 количество байт в записи
- 2462 адрес, начала записи
- 00 тип записи
- 464C...464C данные
- 33 чек сумма записи
Напомню, что данные в hex файле хранятся в шестнадцатеричном представлении и количество байт в записи равняется 16, а не 10 как можно было бы подумать 0x10 = 16. Это относится ко всем остальным полям.
Последняя запись в файле.
:00000001FF
- 00 количество байт в записи
- 0000 в данной записи это поле игнорируется.
- 01 тип записи
- FC чек сумма записи рассчитывается как
01h + NOT(00h + 00h + 00h + 01h).
Файл формата Intel HEX должен заканчиваться записью такого типа.
Дополнительный адрес.(HEX386)
:02000004FFFFFC
- 02 количество байт в записи
- 0000 адресное поле, для данного типа записи всегда равно 0000
- 04 тип записи
- FFFF старшие 16 бит адреса
- FC чек сумма записи рассчитывается как
01h + NOT(02h + 00h + 00h + 04h + FFh + FFh).
Дело в том, что в записи с данными указывается только 16 бит адреса, например 0x2462, а адресное пространство у нас 32-битное. Получается для записи полного адреса не хватает ещё 16 бит, которые как раз и содержатся в этой записи. Для вычисления полного адреса, по которому будут писаться данные, надо адрес из этой записи сдвинуть на 16 бит влево и прибавить к нему адрес указанный в записи с данными.
Адрес из записи с данными 2462
Адрес из этой записи FFFF
-----------
Абсолютный адрес FFFF2462
Оставшиеся два типа записей нам не понадобятся. Запись типа 02 вообще не встретил в своем хексе, она используется в формате HEX86, а у нас, судя по всему, HEX386(чем они отличаются не разбирался). А запись типа 05(адрес начала приложения, а именно функции main) включает в себя 32-битный адрес и может находиться где угодно, а главное, она не несет полезной информации для записи флэш памяти, то есть её можно игнорировать, так написано в документации.
Теперь пользуясь полученными знаниями, обновим код из прошлой статьи.
#include "stm32f10x.h"
#define APPLICATION_ADDRESS 0x08008400//адрес начала программы
//выбираем размер страницы в зависимости от модели МК
#if defined (STM32F10X_HD) || defined (STM32F10X_HD_VL) || defined (STM32F10X_CL) || defined (STM32F10X_XL)
#define FLASH_PAGE_SIZE ((uint16_t)0x800)
#else
#define FLASH_PAGE_SIZE ((uint16_t)0x400)
#endif
uint8_t buff[8];
/*функция преобразует ascii в hex*/
void Ascii_To_Hex( uint8_t* buff, uint8_t count)
{
uint8_t i;
for(i=0; i<count;i++)
{
if(buff[i] <= '9' && buff[i] >= '0' )
{
buff[i] -= 0x30;
}
else
{
buff[i] = buff[i] - 0x41 + 10;
}
}
}
void Go_To_User_App(void)
{
uint32_t app_jump_address;
typedef void(*pFunction)(void);//объявляем пользовательский тип
pFunction Jump_To_Application;//и создаём переменную этого типа
__disable_irq();//запрещаем прерывания
app_jump_address = *( uint32_t*) (APPLICATION_ADDRESS + 4); //извлекаем адрес перехода из вектора Reset
Jump_To_Application = (pFunction)app_jump_address; //приводим его к пользовательскому типу
__set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS); //устанавливаем SP приложения
Jump_To_Application(); //запускаем приложение
}
int main(void)
{
uint8_t i;
uint16_t count_page = 20;//кол-во страниц, которые надо стереть
uint16_t erase_counter = 0x00;//счетчик
uint32_t extented_linear_adress = 0;//дополнительный адрес
uint8_t size_data, type_data, check_sum;//размер, тип данных и чек сумма
uint16_t address_data;//младшие 16 бит адреса
uint32_t program_data;//слово которое пишется во флеш
uint8_t calculation_check_sum = 0;//чек-сумма
volatile FLASH_Status FLASHStatus = FLASH_COMPLETE;
//если найдена новая прошивка
if(new_firmware)
{
//разблокируем флэш
FLASH_Unlock();
//очищаем флаги
FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
//стираем страницы
for(erase_counter = 0; (erase_counter <= count_page) && (FLASHStatus == FLASH_COMPLETE); erase_counter++)
{
FLASHStatus = FLASH_ErasePage(APPLICATION_ADDRESS + (FLASH_PAGE_SIZE * erase_counter));
}
//обновляемся
while(1)
{
/*читаем по одному символу пока не найдем начало записи*/
if(buff[0] == ':')
{
/*читаем 8 симовлов с метаданными*/
Ascii_To_Hex(buff, 8);//переводим их в хекс
size_data = 2*(buff[1] + 16*buff[0]);//находим размер данных
address_data = buff[5] + 16*buff[4] + 256*buff[3] + 4096*buff[2];//адрес
type_data = buff[7] + 16*buff[6];//и тип
calculation_check_sum = size_data/2 + (uint8_t)address_data + (uint8_t)(address_data>>8) + type_data;//считаем чек сумму
if(type_data == 0x00)
{
while(size_data>0 && (FLASHStatus == FLASH_COMPLETE))
{
/*читаем 8 симовлов*/
Ascii_To_Hex(buff, 8);//переводим их в хекс
for(i=0; i<8;i=i+2)//формируем 32-битное слово для записи
{
buff[i] <<= 4;
buff[i] = buff[i] | buff[i+1];
program_data |= buff[i] <<(i*4);
}
FLASHStatus = FLASH_ProgramWord(extented_linear_adress + address_data, program_data );
calculation_check_sum += (uint8_t)program_data + (uint8_t)(program_data>>8) + (uint8_t)(program_data>>16) + (uint8_t)(program_data>>24);
size_data -=8;
address_data +=4;
program_data = 0;
}
calculation_check_sum = ~(calculation_check_sum) + 1;
/*читаем 2 байта*/
Ascii_To_Hex(buff,2);
check_sum = buff[1] + 16*buff[0];
if(calculation_check_sum != check_sum )
{
//тут можно вывести сообщение типо "Ошибка чек суммы"
}
calculation_check_sum = 0;//обнуляем чек сумму
}
else if(type_data == 0x04)//дополнительный адрес
{
/*читаем 4 байта*/
extented_linear_adress = (uint32_t)(buff[0]<<28 | buff[1]<<24 | buff[2]<<20 | buff[3]<<16 );//считаем адрес
calculation_check_sum += buff[0] + buff[1]+ buff[3] + buff[3];
calculation_check_sum = ~(calculation_check_sum) + 1;
/*читаем 2 байта*/
Ascii_To_Hex(buff,2);
check_sum = buff[1] + 16*buff[0];
if(calculation_check_sum != check_sum )
{
//тут можно вывести сообщение типо "Ошибка чек суммы"
}
calculation_check_sum = 0;//обнуляем чек сумму
}
else if(type_data == 0x01)//конец файла
{
break;
}
}
FLASH_Lock();//блокируем флэш
}
}
Go_To_User_App();//прыгаем в основную программу
}
Таким образом, мы разработали шаблон для написания собственного бутлоадера, который разбирает hex.
Получен он из бутлоадера, которым пользуюсь сам, заменой функции, читающей определенное количество символов из hex файла следующим выражением /*читаем n байт*/. Конечно, его можно оптимизировать и это не готовое решение, но на то чтобы с этим разобраться было потрачено несколько недель и выложено это с целью экономии твоего времени мой дорогой читатель)))
Кстати, при компиляции прошивки, которая будет скармливаться бутлоадеру, надо указать, что она должна располагаться не с начала памяти, а после бутлоадера.
Так как тема достаточно обширная один момент был упущен, а именно перенос векторов прерываний при переходе в основную программу. Дело в том, что в бутлоадере таблица векторов прерываний располагается по одному адресу, а в основной программе по другому(у них не может быть общий вектор прерываний так как это две отдельные программы и отдельно компилируются). Для того, чтобы при переходе в основную программу мы могли работать с векторами прерываний их надо перенести либо перед прыжком в основную программу, либо в начале основной программы.
__set_PRIMASK(1); //запрещаем прерывания
SCB->VTOR = APPLICATION_ADDRESS;//переносим начало вектора прерываний по указанному адресу
__set_PRIMASK(0);//разрешаем прерывания
Похожие статьи