Пишем свой бутлоадер для STM32, продолжение.

Пишем свой бутлоадер для STM32, продолжение.

Когда дописывал прошлую статью, на радиокоте наткнулся на тему где обсуждали в каком формате передавать прошивку бутлоадеру. У меня тоже возникал такой вопрос и в конце прошлой статьи описал почему передавать прошивку удобнее hex файлом, чем бинарником, кому интересно могут почитать тут.

Значит так, нам надо рассмотреть какие бывают типы записей в 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 байт*/. Конечно, его можно оптимизировать и это не готовое решение, но на то чтобы с этим разобраться было потрачено несколько недель и выложено это с целью экономии твоего времени мой дорогой читатель)))

Кстати, при компиляции прошивки, которая будет скармливаться бутлоадеру, надо указать, что она должна располагаться не с начала памяти, а после бутлоадера.
Пишем свой бутлоадер для STM32, продолжение.

Так как тема достаточно обширная один момент был упущен, а именно перенос векторов прерываний при переходе в основную программу. Дело в том, что в бутлоадере таблица векторов прерываний располагается по одному адресу, а в основной программе по другому(у них не может быть общий вектор прерываний так как это две отдельные программы и отдельно компилируются). Для того, чтобы при переходе в основную программу мы могли работать с векторами прерываний их надо перенести либо перед прыжком в основную программу, либо в начале основной программы.

__set_PRIMASK(1); //запрещаем прерывания

SCB->VTOR = APPLICATION_ADDRESS;//переносим начало вектора прерываний по указанному адресу

__set_PRIMASK(0);//разрешаем прерывания
комментарии
0