Программный переход в бутлоадер на произвольном STM32

В продолжение темы про способ определения валидности произвольного адреса в памяти на Cortex-M — пример практического использования.

В конкретном изделии иногда хочется программно прыгнуть на штатный фабричный бутлоадер STM32 — чтобы перейти в режим обновления прошивки по UART или USB, не утруждая себя написанием своего бутлоадера.

Бутлоадер у STM32 лежит в области под названием System Memory, на которую и надо перейти, но есть одна проблема — у этой области разные адреса не то что на разных сериях процессоров, а на разных моделях одной серии (с эпической табличкой можно ознакомиться в AN2606 на страницах с 22 по 26). При внесении же соответствующего функционала в платформу вообще, а не просто в конкретное изделие, хочется универсальности.

В файлах CMSIS адрес начала System Memory также отсутствует. Определить его по Bootloader ID не представляется возможным, т.к. это проблема курицы и яйца — Bootloader ID лежит в последнем байте System Memory, что возвращает нас к вопросу об адресе.

Однако, если мы посмотрим на карту памяти STM32, то увидим примерно такую картину:

Нас в данном случае интересует окружение System Memory — например, сверху лежит однократно программируемая область (есть не во всех STM32) и Option bytes (есть во всех). Эта структура наблюдается не то что в разных моделях, а в разных линейках STM32, с разницей лишь в наличии OTP и наличии зазора в адресах между системной памятью и опциями.

Но для нас в данном случае важнее всего то, что адрес начала Option Bytes есть в штатных заголовках CMSIS — он там называется OB_BASE.

Дальнейшее просто. Пишем функцию поиска первого валидного или невалидного адреса вниз или вверх от указанного:

char *cpu_find_next_valid_address(char *start, char *stop, bool valid) {
    char *address = start;
    while (true) {       
        if (address == stop) {
            return NULL;
        }
        
        if (cpu_check_address(address) == valid) {
            return address;
        }
        
        if (stop > start) {
            address++;
        } else {
            address--;
        }
    };

    return NULL;
}

И ищем вниз от Option bytes сначала конец то ли системной памяти, то ли примыкающих к ней OTP, а потом и начало системной памяти — в два захода:

/* System memory is the valid area next _below_ Option bytes */
char *a, *b, *c;
a = (char *)(OB_BASE - 1);
b = 0;

/* Here we have System memory top address */
c = cpu_find_next_valid_address(a, b, true);

/* Here we have System memory bottom address */
c = cpu_find_next_valid_address(c, b, false) + 1;

И без особого труда оформляем это в функцию, находящую начало системной памяти и прыгающую на него, то есть — запускающую бутлоадер:

static void jump_to_bootloader(void) __attribute__ ((noreturn));

/* Sets up and jumps to the bootloader */
static void jump_to_bootloader(void) {
    /* System memory is the valid area next _below_ Option bytes */
    char *a, *b, *c;
    a = (char *)(OB_BASE - 1);
    b = 0;
    
    /* Here we have System memory top address */
    c = cpu_find_next_valid_address(a, b, true);
    
    /* Here we have System memory bottom address */
    c = cpu_find_next_valid_address(c, b, false) + 1;
    
    if (!c) {
        NVIC_SystemReset();
    }
    
    uint32_t boot_addr = (uint32_t)c;
    
    uint32_t boot_stack_ptr = *(uint32_t*)(boot_addr);
    uint32_t dfu_reset_addr = *(uint32_t*)(boot_addr+4);

    void (*dfu_bootloader)(void) = (void (*))(dfu_reset_addr);

    /* Reset the stack pointer */
    __set_MSP(boot_stack_ptr);

    dfu_bootloader();
    while (1);
}

От конкретной модели процессора тут зависит… да ничего не зависит. Логика не будет работать на моделях, у которых между OTP и системной памятью есть дырка — но я не проверял, есть ли такие вообще. Будете активно работать с OTP — проверьте.

Прочие же хитрости относятся лишь к обычной процедуре вызова бутлоадера из вашего кода — не забудьте сброить указатель стека и вызывайте процедуру ухода в бутлоадер до того, как инициализируете в процессоре периферию, тактовые частоты и т.д.: в силу своего минимализма бутлоадер может забивать на инициализацию периферии и ожидать, что она находится в состоянии по умолчанию. Неплохим вариантом вызова бутлодера из произвольного места вашей программы является запись в RTC Backup Register или просто в известный адрес в памяти магического числа, программная перезагрузка и проверка на первых этапах инициализации этого числа.

P.S. Так как все адреса в карте памяти процессора выровнены в худшем случае по 4, описанную выше процедуру кратно ускорит идея шагать по ним с шагом 4 байта вместо одного.

Эта заметка в LiveJournal. Текст тот же, но часто там бывает много комментариев.