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

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

В одной из прошлых статей описывал как прошить STM32 с помощью бутлоадера и объяснял принцип его работы. Для тех кто её не читал напомню, что бутлоадер, по сути, это программа, которая живёт в МК и может перезаписывать его память, то есть обновлять прошивку.

Думаю у кого-то мог возникнуть вопрос, зачем писать бутлоадер самому, если можно пользоваться встроенным?
Ответ на этот вопрос очень прост, пока своими поделками на МК пользуешься сам — писать бутлоадер нет надобности, в любой момент можно взять поделку и залить в неё новую прошивку. Но если устройство становится коммерческим, то встаёт вопрос как обновлять прошивку пользователям?

Если выложить прошивку в открытый доступ, то велика вероятность того, что кто-то скопирует устройство и организует продажи. Разработчику такой ситуации хотелось бы избежать, но в то же время хотелось бы радовать пользователей новыми фичами и исправлением старых багов, для решения этих вопросов и нужен самописный бутлоадер.

Используя самописный бутлоадер, ничто не мешает нам в нём инициализировать часть периферии, необходимой для работы устройства и не инициализировать её в прошивке. Таким образом, прошивка, отправляемая пользователю, получается неполноценной и на МК без нашего бутлоадера работать не будет. Для повышения безопасности прошивку можно шифровать, но ни один из способов не поможет если кому-то действительна понадобиться ваша прошивка. Судя по данным, найденным в интернете, есть конторы которые за определённую плату(в районе 1000$) извлекают прошивку из залоченного МК.

С теорией разобрались, предлагаю перейти к практике. Условно наш бутлоадер будет иметь следующую структуру
//если найдена новая прошивка
if(new_firmware)
{
//обновляемся
}
Go_To_Application();//прыгаем в основную программу


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

Сразу хотелось бы отметить, что наш бутлоадер будет располагаться во флэше и в дальнейших рассуждениях это подразумевается по умолчанию.
При включении питания МК всегда стартует с начала флэш памяти, а именно с адреса 0х0800000. Далее, из бутлоадера надо прыгнуть в основную программу, расположение которой зависит от размера бутлоадера. Для того чтобы вычислить адрес начала основной программы надо к 0х0800000 прибавить размер бутлоадера.

В общем перед нами стоит два вопроса:
На какой адрес необходимо прыгнуть ?
Как реализовать прыжок ?

Прыжок реализуется с помощью указателя на функцию. Как мы знаем указатель — это адрес, а в указателе на функцию хранится адрес, с которого начинается функция.
Для ответа на первый вопрос надо вспомнить как происходит загрузка МК.
Пишем свой бутлоадер для STM32.

При включении питания МК читает значение по адресу 0x08000000 и записывает его в SP (SP – регистр, который указывает на вершину стека), после чего читает значение по адресу 0x08000004 и записывает его в PC (PC – регистр, который указывает на текущую инструкцию + 4 байта).

Получается, что адрес перехода хранится в векторе Reset Handler. Функция, реализующая прыжок, выглядит следующим образом.

#define APPLICATION_ADDRESS    0x08008400//адрес начала программы

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();		                        //запускаем приложение	
}


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

Остался не раскрыт еще один вопрос, как передавать прошивку?
Изначально решал эту задачу в лоб, передавая бинарник, в начале которого указывал размер прошивки, но позже понял, что этот вариант неудобный и неправильный. Неудобный он тем, что после генерации бинарника из него надо удалять всё лишнее, то есть если основная программа начинается с адреса 0x08008400, то все пространство до этого адреса будет заполнена FF.
Пишем свой бутлоадер для STM32.

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

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

#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

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)
{
    uint16_t count_page = 20;//кол-во страниц которые надо стереть
    uint16_t erase_counter = 0x00;//счетчик
    volatile FLASH_Status FLASHStatus = FLASH_COMPLETE;//статус работы с флэш памятью

   //если найдена новая прошивка
   if(new_firmware)
   {
     // для работы с флешем используем StdPeriph, не забудь ее подключить
     //разблокируем флэш,
      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));
	}

      /*обновляемся*/

      //блокируем флэш
      FLASH_Lock();
   }
   Go_To_User_App();//прыгаем в основную программу
}
комментарии
2