Очередной загрузчик STM32. Часть 1. Передача управления.

Автор: | 30.05.2019

Рано или поздно встает задача обновления прошивки на готовом устройстве. Практически все современные устройства, так или иначе умеют обновлять свою программу с минимальными усилиями со стороны пользователя. Любую программу бутлодер условно можно разделить на две части — интерфейсной и части работы с носителем с которого исполняется код, обычно это внутренняя флешь память. Задача интерфейсной части тем или иным способом получить новую прошивку. Способ получения определяется доступностью интерфейсов, допустимым размером бутлодера ну и возможно, вашей фантазией. Вторая часть записывает прошивку на носитель, осуществляет подготовку системы к передаче управления этому коду и саму передачу.

В данной части создадим проект загрузчика, который реализует только переход на целевую программу. Целевая программа будет заранее скомпилирована, подготовлена и положена во внутреннюю флешь память контроллера. Железом и «фреймворком» для проекта, как обычно, послужат плата с STM32, библиотека HAL и генератор кода CubeMX. Сам проект будет создан для IDE Keil uVision v5.

Проект предельно простой и в нем не составит труда разобраться. Благодаря этому, можно наглядно увидеть, как происходит переход от загрузчика к основной программе.

Сам загрузчик и приложение будут генерироваться из одного проекта.  Основной смысл, что мы создаем 2 цели boot и firmware с разными дефайнами и настройками линкера.

Общее описание работы

Попав в функцию main() идет проверка признака загрузки, который располагается в области Backup RAM. Это специальная область памяти, которая сохраняет свое значение до тех пор, пока присутствует основное питание или батарейное. Сам признак это просто сохраненное значение в памяти, 0 для перехода в основную прошивку без передачи управления, 0xFFFFFFFF — для передачи управления коду по определенному адресу. Запись и проверку этой отметки осуществляют две функции:

void _set_boot_mark(void) // Установить метку и сброситься
{
    uint32_t *prst = (uint32_t*)(BKP_BASE + MMNGR_BACKUPRAM_OFFSET);

    _set_mark(BOOT_MARK);
    //RST
    HAL_NVIC_SystemReset();
}

void _check_boot_mark(void)
{
    if (_get_mark() == BOOT_MARK) // Проверяем метку
    {
        // очистить метку, чтобы после сброса запустился прошивка
        __HAL_RCC_PWR_CLK_ENABLE(); //на всякий случай включаю такитрование контроллреа питания
        _set_mark(RST_BOOT_MARK);
        __HAL_RCC_PWR_CLK_DISABLE(); // отключаю

        //Деинициализация железа
        HAL_RCC_DeInit();// Clock deinit
        HAL_DeInit();
        
        // Перейти к прошивке
        typedef  void (*pFunction)(void);
        // 4 -- смещение необходимое для перехода именно на таблицу прерываний
        // первые четыре это значение стекпойнтера
        uint32_t jumpAddress = *((volatile uint32_t*)(MAIN_PROGRAM_START_ADDRESS + 4));
        pFunction Jump_To_Application = (pFunction) jumpAddress;
        Jump_To_Application();
    }
    else // Установить стекпойнтер
    {
        SCB->VTOR = FLASH_BASE | FIRMWARE_OFFST;
    }
}

static void _set_mark(uint32_t mark)
{
    volatile uint32_t *pmark = (uint32_t*)(BKP_BASE + MMNGR_BACKUPRAM_OFFSET);

    HAL_PWR_EnableBkUpAccess();
    *pmark = mark;
    HAL_PWR_DisableBkUpAccess();
}

static uint32_t _get_mark(void)
{
    return *((uint32_t*)(BKP_BASE + MMNGR_BACKUPRAM_OFFSET));
}

Некоторая хитрость  и замысловатость функции  _check_boot_mark() здесь в том, что не происходит полной деинициализации системы, она отдается на откуп МК. Т. е. подразумевается, что после программного вызова ресета, периферия будет сброшена. Сразу оговорюсь это не самый лучший подход, желательно, перевести всю систему в «первозданное» состояние. т.е. те ресурсы, что задействованы в загрузчике привести к тому виду, какими они были до использования. Подход использованный в загрузчике из статьи удовлетворительно показал себя в нескольких проектах. Так что для меня и подобный метод организации программы загрузчика имеет право на жизнь.

_set_mark и _get_mark — вспомогательные функции обертки над обращением к Backup RAM. Перед работой с этой памятью важно включить тактирование на домен RCC_PWR. Для произведения записи необходимо разблокировать память, читать можно в любой момент, удобно это делать через указатель. BKP_BASE  — начало адресации памяти, MMNGR_BACKUPRAM_OFFSET смещение от начала памяти. Можно писать естественно и по стартовому адресу, но часть начальных адресов используют HAL функции при работе с RTC (Real time clock).

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

Теперь об отличиях одной цели от другой. Самое очевидное это определение дефайнов FIRMWARE и BOOT, они влияют на ход выполнения функции main(). Второе и самое важное отличие это распределение флешь памяти. Для boot стартует с начального адреса внутреннего флеша 0x8000000 и должен уместиться до адреса 0x8010000, который является стартовым для цели «firmware«. Адреса лучше брать не случайные, а использовать таблицу распределения внутренней памяти. Как видно из таблицы ниже наш загрузчик будет занимать 4 первых сектора.

Таблица распределения флешь памяти

Настройки  адресов памяти для разных целей

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

 

 

 

 

Очередной загрузчик STM32. Часть 1. Передача управления.: 1 комментарий

  1. Уведомление: Очередной bootloader STM32. Часть 2. Спец утилиты. — Mcublog

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *