Embedded HandbookПриложения
Все разделы › VI. Практикум
STM32F103, STM32F407, STM32G474 · уровень средний · 37 мин чтения
Открыть в интерактивном справочнике

48д. Заводской загрузчик ST в системной памяти: вход по BOOT0 и из прошивки, протокол USART, своя утилита прошивки

В разделе 4.9 заводской загрузчик появился как способ спасти плату: BOOT0 в единицу, сброс — и камень ждёт прошивку по USART1. В 48.11 он стоит первым среди готовых путей обновления. Разделы 48а–48г и 198щ строили свой загрузчик: подпись, MCUboot, разностное обновление, шифрование. Здесь — загрузчик, который уже лежит в каждом кристалле и который не нужно писать.

Раздел: что такое системная память и когда заводского загрузчика хватает; как в него попасть выводом BOOT0 на F1 и F4 и битами nBOOT0 и nSWBOOT0 на G4; как прыгнуть в него из работающей прошивки — свой код на C для трёх семейств; какие интерфейсы и выводы у F103C8, F407 и G474; протокол USART по AN3155 — кадр каждой команды; своя утилита прошивки на Python и модель загрузчика, против которой она проверена; грабли.

Опоры: AN2606 Rev 21, DocID13801 (заводской загрузчик всех семейств: таблица 2 — условия входа, 3.1 — прыжок из прошивки, таблица 3 — интерфейсы и версии, таблицы 6, 7 — F10xxx, таблицы 22–25 — F40xxx/41xxx, таблица 68 — PID и адреса, таблицы 69, 70 — время запуска), публичный адрес — st.com, AN2606; AN3155 Rev 6, DocID17066 (протокол USART: команды, суммы, версии) — st.com, AN3155; RM0008 Rev 17 (F1: таблица 9 — режимы запуска, таблица 5 — страницы), RM0090 Rev 19 (F4: таблица 2 — режимы запуска, таблица 5 — секторы, 3.7.3 — уровни RDP), RM0440 Rev 6 и даташит DS12288 Rev 5 (G4: таблица 5 — режимы запуска, 2.4 — ОЗУ, 47.6.1 — номер кристалла). Редакция AN2606, с которой сверен раздел, вышла раньше G4; более новые редакции описывают и G4, с ними раздел не сверялся — поэтому про G4 здесь только то, что есть в RM0440 и DS12288. Лицензия: свой код раздела — свободно.

Как это называется в документации

Оригинал Перевод
system memory boot mode запуск из системной памяти
bootloader activation pattern набор условий входа в загрузчик
automatic baud rate detection автоопределение скорости по первому байту
even parity (8E1) 8 бит данных, бит чётности even, 1 стоповый
XOR checksum byte байт суммы: исключающее ИЛИ всех байт блока
product ID (PID), Get ID номер семейства кристалла, команда его чтения
bootloader ID номер версии загрузчика в системной памяти
Erase / Extended Erase стирание с однобайтовыми / двухбайтовыми номерами страниц
Readout Protect / Readout Unprotect включить / снять защиту чтения
Go command команда «перейти»: стек и вход из таблицы векторов
physical remap (MEM_MODE) что видно по адресу 0: флеш-память, системная память или ОЗУ
software BOOT0 (nSWBOOT0, nBOOT0) BOOT0 из байтов конфигурации вместо вывода

48д.1 Системная память и когда заводской загрузчик лучше своего

Кроме флеш-памяти под программу у STM32 есть информационный блок: байты конфигурации и системная память с кодом ST, записанным на заводе. Стереть или переписать её нельзя: по AN2606 (таблица 5) команды Write и Erase с системной памятью не работают, только Read. У F103 это 2 КБ с адреса 0x1FFFF000, у F407 — 0x1FFF0000…0x1FFF77FF (AN2606, таблица 68), у G474 — 28 КБ с 0x1FFF0000 (RM0440, 2.6.1 и раскладка флеш-памяти).

Компьютер с утилитой через переходник USB–UART соединён с USART1 кристалла PA9 и PA10, линии DTR и RTS переходника ведут к BOOT0 и NRST; справа выбор области запуска: BOOT0 равен 1 и BOOT1 равен 0 — системная память 0x1FFFF000 у F103 и 0x1FFF0000 у F407, BOOT0 равен 0 — флеш-память 0x08000000, вход из прошивки признаком в ОЗУ и сбросом ведёт в системную память; внизу кадр Write Memory: код 31 CE, ACK, адрес с суммой XOR, ACK, длина и 256 байт данных с суммой, ACK; туда 265 байт, обратно 3, при 115 200 бит в секунду 25,6 мс на блок

Что умеет загрузчик, одинаково для всех семейств: принять образ по одному из интерфейсов, стереть и записать флеш-память, прочитать её, включить и снять защиту чтения и записи, перейти на адрес. Чего он не умеет: проверить, чей образ (подписи нет), откатиться на прежнюю версию, принять образ по радио.

Когда Заводской Свой (48, 198щ)
прошить плату на столе без программатора да —
оживить плату, где программа заняла PA13/PA14 (4.9) да —
прошивка на линии производства по UART да можно
обновление у покупателя по выведенному наружу UART или USB да, если подпись не нужна да
подпись, шифрование, откат, журнал (48а–48г, 198щ) нет да
обновление по Bluetooth, радио, сети нет да
места во флеш-памяти 0 байт 4528 байт у 198щ

Есть и ещё одна причина держать заводской загрузчик в уме, даже когда свой есть: он остаётся в кристалле всегда. Кто может поставить BOOT0 в единицу и подключиться к USART1, тот может прочитать флеш-память — если не включена защита чтения (11в), и стереть её и записать свою — всегда, кроме F4 и G4 с уровнем RDP 2: там вход в системную память закрыт (RM0090, 3.7.3; RM0440, с. 123).


48д.2 Вход по выводам: BOOT0 и BOOT1, nBOOT0 и nSWBOOT0

На F1 и F4 область запуска выбирают два вывода (RM0008, таблица 9; RM0090, таблица 2): BOOT0 = 0 — флеш-память, BOOT0 = 1 и BOOT1 = 0 — системная память, оба в единице — ОЗУ. Значения защёлкиваются на 4-м фронте такта SYSCLK после сброса и снова при выходе из Standby, поэтому держать их надо всё это время. В AN2606 это условие «pattern1» (таблица 2); у F10xxx и F40xxx/41xxx загрузчик входит именно так (4.1 и 11.1.1).

У G4 BOOT1 вывода нет, вместо него бит nBOOT1 в FLASH_OPTR, а BOOT0 можно взять не с вывода PB8, а из бита nBOOT0 (RM0440, таблица 5):

nSWBOOT0 BOOT0 откуда Системная память, когда
1 вывод PB8 PB8 = 1 и nBOOT1 = 1
0 бит nBOOT0 nBOOT0 = 0 и nBOOT1 = 1

Бит BOOT_LOCK = 1 перекрывает всё: запуск только из флеш-памяти. С nSWBOOT0 = 0 вывод PB8 освобождается под обычную ножку — но тогда в загрузчик выводом уже не попасть: только переписав байты конфигурации или прыжком из прошивки (48д.3). Даташит DS12288 называет тот же бит nBOOT_SEL, RM0440 — nSWBOOT0; это одно и то же.

Удобный приём для BluePill и F4 с переходником USB–UART: DTR ведёт BOOT0, RTS — NRST. Утилита ставит BOOT0, дёргает сброс, ждёт запуска загрузчика и начинает обмен (enter_bootloader в 48д.6). Сколько ждать — AN2606, таблица 69: у F10xxx не меньше 1,227 мс, у F40xxx/41xxx — 84,59 мс для версии V3.x и 74 мс для V9.x; утилита ждёт 0,1 с.


48д.3 Вход из работающей прошивки

Прибору в корпусе до BOOT0 не добраться. Тогда приложение само уходит в системную память — по команде из меню, по кадру с компьютера. AN2606 (3.1) перечисляет, что сделать перед прыжком: выключить такты периферии и PLL, запретить прерывания и снять висящие запросы. Загрузчик рассчитывает застать камень таким, каким его оставляет сброс, — на том же условии держится передача управления в 48.5.

Надёжнее всего ничего не возвращать руками, а сбросить камень на самом деле: приложение кладёт признак в ОЗУ (как в 48.6 и 198щ.4 — магия и её дополнение) и делает программный сброс; первой строкой Reset_Handler, до .data, .bss и настройки тактов, признак проверяется — и прыжок. Сброс вернул всё сам. Второй путь — прыжок без сброса, когда в начало Reset_Handler вставить проверку нельзя (стартовый код чужой): тогда код гасит SysTick, NVIC, такты и периферию сам.

Весь код — один файл на три семейства, адреса регистров — свои #define (сверены с заголовками CMSIS проверкой раздела):

/* sysboot48d.h — раздел 48д: вход в заводской загрузчик ST из работающей прошивки.
 * Собирать с -DSB_F1 (STM32F103), -DSB_F4 (STM32F407) или -DSB_G4 (STM32G474). */
#ifndef SYSBOOT48D_H
#define SYSBOOT48D_H
#include <stdint.h>

#if defined(SB_F1)
#define SB_SYSMEM_ADDR 0x1FFFF000u   /* AN2606, таблица 68 */
#define SB_FLAG_ADDR   0x20004FF0u   /* F103C8: 20 КБ ОЗУ, последние 16 байт (48.6) */
#elif defined(SB_F4)
#define SB_SYSMEM_ADDR 0x1FFF0000u   /* AN2606, таблица 68 */
#define SB_FLAG_ADDR   0x2001FFF0u   /* F407: 128 КБ основного ОЗУ */
#elif defined(SB_G4)
#define SB_SYSMEM_ADDR 0x1FFF0000u   /* RM0440, 2.6.1 */
#define SB_FLAG_ADDR   0x20017FF0u   /* G474: SRAM1 80 КБ + SRAM2 16 КБ (RM0440, 2.4) */
#else
#error "SB_F1, SB_F4 или SB_G4"
#endif

#ifndef SB_SYSMEM
#define SB_SYSMEM SB_SYSMEM_ADDR     /* опыт в QEMU подставляет свой адрес */
#endif

#define SB_MAGIC 0x5B007ED0u

void sysboot_request(void);    /* из приложения: признак в ОЗУ и программный сброс */
void sysboot_early(void);      /* первой строкой Reset_Handler, до .data, .bss и тактирования */
void sysboot_jump_now(void);   /* без сброса: вернуть камень к состоянию после сброса и прыгнуть */

#endif
/* sysboot48d.c — раздел 48д: вход в заводской загрузчик ST из работающей прошивки, без вывода BOOT0.
 * Два пути. Надёжный: признак в ОЗУ + программный сброс, а после сброса — прыжок первой строкой
 * Reset_Handler, пока ничего не включено. Быстрый: прыжок сразу, вернув камень к состоянию после сброса
 * так, как велит AN2606, 3.1: такты периферии выключить, PLL выключить, прерывания запретить и снять.
 * Адреса регистров — из RM0008, RM0090, RM0440; сверены с заголовками CMSIS проверкой раздела. */
#include "sysboot48d.h"

#define REG(a) (*(volatile uint32_t *)(a))

/* Ядро Cortex-M3/M4 — одинаково у всех трёх семейств */
#define SYST_CSR     REG(0xE000E010u)
#define SYST_RVR     REG(0xE000E014u)
#define SYST_CVR     REG(0xE000E018u)
#define NVIC_ICER(i) REG(0xE000E180u + 4u * (i))
#define NVIC_ICPR(i) REG(0xE000E280u + 4u * (i))
#define SCB_ICSR     REG(0xE000ED04u)
#define SCB_VTOR     REG(0xE000ED08u)
#define SCB_AIRCR    REG(0xE000ED0Cu)
#define ICSR_PENDSTCLR (1u << 25)
#define AIRCR_RESET  (0x05FA0000u | (1u << 2))   /* ключ VECTKEY и SYSRESETREQ */

#if defined(SB_F1)
#define RCC          0x40021000u
#define RCC_CR       REG(RCC + 0x00u)
#define RCC_CFGR     REG(RCC + 0x04u)
#define RCC_CIR      REG(RCC + 0x08u)
#define RCC_APB2RSTR REG(RCC + 0x0Cu)
#define RCC_APB1RSTR REG(RCC + 0x10u)
#define RCC_AHBENR   REG(RCC + 0x14u)
#define RCC_APB2ENR  REG(RCC + 0x18u)
#define RCC_APB1ENR  REG(RCC + 0x1Cu)
#define DMA1_CCR(ch) REG(0x40020008u + 20u * (ch))   /* каналы 1…7: ch = 0…6 */
#define CR_HSION     (1u << 0)
#define CR_HSIRDY    (1u << 1)
#define CFGR_SW_HSI  0x0u
#define CFGR_RESET   0x00000000u
#elif defined(SB_F4)
#define RCC          0x40023800u
#define RCC_CR       REG(RCC + 0x00u)
#define RCC_CFGR     REG(RCC + 0x08u)
#define RCC_CIR      REG(RCC + 0x0Cu)
#define RCC_AHB1RSTR REG(RCC + 0x10u)
#define RCC_AHB2RSTR REG(RCC + 0x14u)
#define RCC_AHB3RSTR REG(RCC + 0x18u)
#define RCC_APB1RSTR REG(RCC + 0x20u)
#define RCC_APB2RSTR REG(RCC + 0x24u)
#define RCC_AHB1ENR  REG(RCC + 0x30u)
#define RCC_AHB2ENR  REG(RCC + 0x34u)
#define RCC_AHB3ENR  REG(RCC + 0x38u)
#define RCC_APB1ENR  REG(RCC + 0x40u)
#define RCC_APB2ENR  REG(RCC + 0x44u)
#define SYSCFG_MEMRMP REG(0x40013800u)
#define APB2ENR_SYSCFGEN (1u << 14)
#define CR_HSION     (1u << 0)
#define CR_HSIRDY    (1u << 1)
#define CFGR_SW_HSI  0x0u
#define CFGR_RESET   0x00000000u
#elif defined(SB_G4)
#define RCC          0x40021000u
#define RCC_CR       REG(RCC + 0x00u)
#define RCC_CFGR     REG(RCC + 0x08u)
#define RCC_CIER     REG(RCC + 0x18u)
#define RCC_AHB1RSTR REG(RCC + 0x28u)
#define RCC_AHB2RSTR REG(RCC + 0x2Cu)
#define RCC_AHB3RSTR REG(RCC + 0x30u)
#define RCC_APB1RSTR1 REG(RCC + 0x38u)
#define RCC_APB1RSTR2 REG(RCC + 0x3Cu)
#define RCC_APB2RSTR REG(RCC + 0x40u)
#define RCC_AHB1ENR  REG(RCC + 0x48u)
#define RCC_AHB2ENR  REG(RCC + 0x4Cu)
#define RCC_AHB3ENR  REG(RCC + 0x50u)
#define RCC_APB1ENR1 REG(RCC + 0x58u)
#define RCC_APB1ENR2 REG(RCC + 0x5Cu)
#define RCC_APB2ENR  REG(RCC + 0x60u)
#define SYSCFG_MEMRMP REG(0x40010000u)
#define APB2ENR_SYSCFGEN (1u << 0)
#define AHB1RSTR_FLASHRST (1u << 8)      /* флеш-память не сбрасываем: из неё идёт этот код */
#define CR_HSION     (1u << 8)
#define CR_HSIRDY    (1u << 10)
#define CFGR_SW_HSI  0x1u                /* у G4 HSI16 — это 01 */
#define CFGR_RESET   0x00000005u
#endif

#define CR_HSEON     (1u << 16)
#define CR_CSSON     (1u << 19)
#define CR_PLLON     (1u << 24)
#define CFGR_SW      0x3u
#define CFGR_SWS     (0x3u << 2)
#define SB_WAIT      100000u             /* ожидания с пределом: зависший генератор не вешает прыжок */

#define SB_FLAG ((volatile uint32_t *)SB_FLAG_ADDR)

__attribute__((noreturn)) static void sb_jump(uint32_t base)
{
    uint32_t sp = REG(base);
    uint32_t pc = REG(base + 4u);

    __asm volatile("msr msp, %0\n"
                   "msr control, %1\n"   /* стек MSP, привилегированный режим, без следа FPU */
                   "isb\n"
                   "cpsie i\n"           /* после сброса PRIMASK = 0 — загрузчик ждёт именно этого */
                   "bx %2\n"
                   :
                   : "r"(sp), "r"(0u), "r"(pc)
                   : "memory");
    for (;;) {
    }
}

static void sb_remap(void)
{
#if defined(SB_F4) || defined(SB_G4)
    RCC_APB2ENR |= APB2ENR_SYSCFGEN;
    (void)RCC_APB2ENR;
    SYSCFG_MEMRMP = 0x1u;                /* MEM_MODE = 01: по адресу 0 — системная память */
#endif
    SCB_VTOR = SB_SYSMEM;
    __asm volatile("dsb\n"
                   "isb\n" ::: "memory");
}

void sysboot_request(void)
{
    SB_FLAG[0] = SB_MAGIC;
    SB_FLAG[1] = ~SB_MAGIC;
    __asm volatile("dsb" ::: "memory");
    SCB_AIRCR = AIRCR_RESET;
    __asm volatile("dsb" ::: "memory");
    for (;;) {
    }
}

void sysboot_early(void)
{
    if (SB_FLAG[0] != SB_MAGIC || SB_FLAG[1] != ~SB_MAGIC)
        return;
    SB_FLAG[0] = 0u;                     /* прочитали — стёрли, иначе загрузчик навсегда */
    SB_FLAG[1] = 0u;
    sb_remap();
    sb_jump(SB_SYSMEM);
}

static void sb_clocks_reset(void)
{
    uint32_t n;

    RCC_CR |= CR_HSION;
    for (n = 0u; n < SB_WAIT && !(RCC_CR & CR_HSIRDY); n++) {
    }
    RCC_CFGR = (RCC_CFGR & ~CFGR_SW) | CFGR_SW_HSI;
    for (n = 0u; n < SB_WAIT && (RCC_CFGR & CFGR_SWS) != (CFGR_SW_HSI << 2); n++) {
    }
    RCC_CFGR = CFGR_RESET;               /* делители и MCO — как после сброса */
    RCC_CR &= ~(CR_PLLON | CR_CSSON);
    RCC_CR &= ~CR_HSEON;
#if defined(SB_G4)
    RCC_CIER = 0u;                       /* прерывания RCC выключены */
#else
    RCC_CIR = 0u;
#endif
}

static void sb_periph_reset(void)
{
#if defined(SB_F1)
    uint32_t ch;
    RCC_AHBENR |= 1u;                    /* DMA1EN: у F103 сброса DMA через RCC нет — гасим каналы сами */
    for (ch = 0u; ch < 7u; ch++)
        DMA1_CCR(ch) = 0u;
    RCC_APB2RSTR = 0xFFFFFFFFu;
    RCC_APB2RSTR = 0u;
    RCC_APB1RSTR = 0xFFFFFFFFu;
    RCC_APB1RSTR = 0u;
    RCC_AHBENR = 0x00000014u;            /* значения после сброса (RM0008, 7.3.6–7.3.8) */
    RCC_APB2ENR = 0u;
    RCC_APB1ENR = 0u;
#elif defined(SB_F4)
    RCC_AHB1RSTR = 0xFFFFFFFFu;
    RCC_AHB1RSTR = 0u;
    RCC_AHB2RSTR = 0xFFFFFFFFu;
    RCC_AHB2RSTR = 0u;
    RCC_AHB3RSTR = 0xFFFFFFFFu;
    RCC_AHB3RSTR = 0u;
    RCC_APB1RSTR = 0xFFFFFFFFu;
    RCC_APB1RSTR = 0u;
    RCC_APB2RSTR = 0xFFFFFFFFu;
    RCC_APB2RSTR = 0u;
    RCC_AHB1ENR = 0x00100000u;           /* значения после сброса (RM0090, 6.3.10–6.3.14) */
    RCC_AHB2ENR = 0u;
    RCC_AHB3ENR = 0u;
    RCC_APB1ENR = 0u;
    RCC_APB2ENR = 0u;
#elif defined(SB_G4)
    RCC_AHB1RSTR = ~AHB1RSTR_FLASHRST;
    RCC_AHB1RSTR = 0u;
    RCC_AHB2RSTR = 0xFFFFFFFFu;
    RCC_AHB2RSTR = 0u;
    RCC_AHB3RSTR = 0xFFFFFFFFu;
    RCC_AHB3RSTR = 0u;
    RCC_APB1RSTR1 = 0xFFFFFFFFu;
    RCC_APB1RSTR1 = 0u;
    RCC_APB1RSTR2 = 0xFFFFFFFFu;
    RCC_APB1RSTR2 = 0u;
    RCC_APB2RSTR = 0xFFFFFFFFu;
    RCC_APB2RSTR = 0u;
    RCC_AHB1ENR = 0x00000100u;           /* значения после сброса (RM0440, 7.4.14–7.4.19) */
    RCC_AHB2ENR = 0u;
    RCC_AHB3ENR = 0u;
    RCC_APB1ENR1 = 0x00000400u;
    RCC_APB1ENR2 = 0u;
    RCC_APB2ENR = 0u;
#endif
}

void sysboot_jump_now(void)
{
    uint32_t i;

    __asm volatile("cpsid i" ::: "memory");
    SYST_CSR = 0u;                       /* SysTick стоп: загрузчик меряет им скорость сам */
    SYST_RVR = 0u;
    SYST_CVR = 0u;
    SCB_ICSR = ICSR_PENDSTCLR;
    for (i = 0u; i < 8u; i++) {
        NVIC_ICER(i) = 0xFFFFFFFFu;      /* запретить все прерывания */
        NVIC_ICPR(i) = 0xFFFFFFFFu;      /* и снять висящие запросы */
    }
    sb_clocks_reset();
    sb_periph_reset();
    for (i = 0u; i < 8u; i++)
        NVIC_ICPR(i) = 0xFFFFFFFFu;      /* сброс периферии мог поднять запросы ещё раз */
    sb_remap();
    sb_jump(SB_SYSMEM);
}

Почему так:

Размеры (arm-none-eabi-gcc 12.2.1, -Os): весь файл — 436 байт кода у F1, 504 у F4 и 464 у G4, данных и .bss нет; из них sysboot_jump_now — 276, 304 и 272, sysboot_early — 76, 56 и 56 (у F4 и G4 с ней работает sb_remap — 60 и 52 байт, у F1 он встроен), sysboot_request — 52, sb_jump — 32 байта. В QEMU 7.2.22 (netduinoplus2 — это STM32F405) вместо системной памяти ST стоит своя таблица векторов с 0x08008000, её «загрузчик» печатает, каким получил камень. Прыжок без сброса — из режима с PSP, при работающем SysTick, с разрешённым и висящим прерыванием USART1:

APP before: systick=irq iser1=0x00000020 ispr1=0x00000020
BOOT msp=0x2000eff8 control=0x00000000 primask=0x00000000 ipsr=0x00000000 syst_csr=0x00000000 iser1=0x00000000 ispr1=0x00000000 vtor=0x08008000 memrmp=0x00000000 flag0=0x00000000

CONTROL, PRIMASK, IPSR — нули: обычный режим, стек MSP, прерывания разрешены. SysTick стоит, прерывание USART1 и выключено, и снято (было 0x20 в обоих регистрах). Указатель стека — 0x2000F000 из таблицы минус 8 байт, которые положила сама функция «загрузчика». Путь через сброс дал то же самое, признак после прыжка стёрт. SYSCFG_MEMRMP в QEMU читается нулём — модель QEMU этот регистр не хранит, отображение памяти здесь не проверено.


48д.4 Интерфейсы по семействам и автоопределение скорости

STM32F103C8 STM32F407 STM32G474
версия USART-протокола V2.2 V3.1 —
USART USART1: PA9 TX, PA10 RX USART1: PA9/PA10; USART3: PB10/PB11 или PC10/PC11 USART (DS12288)
USB DFU нет PA11/PA12, OTG FS есть (DS12288)
CAN нет CAN2: PB5 RX, PB13 TX, 125 кбит/с —
I²C нет у V9.x: I2C1 PB6/PB7, I2C2 PF1/PF0, I2C3 PA8/PC9 есть (DS12288)
SPI нет у V9.x: SPI1 PA4–PA7, SPI2 PI0–PI3 есть (DS12288)
PID 0x410 0x413 0x469 (DEV_ID, RM0440, 47.6.1)
системная память 0x1FFFF000, 2 КБ 0x1FFF0000…0x1FFF77FF 0x1FFF0000, 28 КБ
ОЗУ загрузчика 512 байт 8 КБ (V3.x), 12 КБ (V9.x) —
такт загрузчика HSI, PLL на 24 МГц HSI, PLL на 24 МГц (V3.x) или 60 МГц (V9.x); CAN и DFU — от кварца 4–26 МГц —

Источник по F103 и F407 — AN2606: таблица 3 (интерфейсы и версии), таблицы 6, 22, 24 (выводы, такт, ОЗУ), таблица 68 (PID и адреса). У F407 загрузчик бывает двух поколений — V3.x с номером 0x31 и V9.x с номером 0x90; номер лежит в системной памяти по адресу 0x1FFF77DE и читается командой Read Memory. По G474 — только то, что есть в DS12288 (3.7: USART, I²C, SPI, USB DFU) и RM0440; G474 во всех объёмах — категория 3 (RM0440, таблица 1), у неё DEV_ID 0x469. Выводы интерфейсов G4 и их версии — в новых редакциях AN2606, с которыми раздел не сверялся.

У F407 для CAN и DFU нужен кварц кратный 1 МГц от 4 до 26 МГц, загрузчик сам меряет его частоту таймером TIM11 (AN2606, таблица 22). USART работает от внутреннего HSI, кварц не нужен.

Автоопределение скорости. Загрузчик не знает скорость заранее. Он ждёт на RX кадр 0x7F — стартовый бит, 0x7F, бит чётности, стоповый — и меряет его длительность таймером SysTick; по ней настраивает USART и отвечает 0x79 (AN3155, 1). 0x7F выбран не случайно: младшие 7 бит — единицы, старший — ноль; между спадом стартового бита и спадом бита 7 ровно 8 битовых интервалов. Проверены скорости от 1200 до 115 200 бит/с, расхождение скоростей должно быть меньше 2,5 % (AN3155, 2). Какие именно фронты меряет загрузчик, AN3155 не уточняет; модель раздела меряет от спада до спада.


48д.5 Протокол USART по AN3155

Кадр UART — 8 бит, чётность even, 1 стоповый (AN2606, таблицы 6 и 22). Ответы — один байт: ACK 0x79 или NACK 0x1F (AN3155, 3). Три защиты от ошибок: чётность каждого байта; команда идёт парой «код и его дополнение» (XOR пары = 0xFF); блоки адреса и данных кончаются байтом суммы — XOR всех байт блока, так что XOR блока вместе с суммой = 0. Адрес — 4 байта, старший первым.

Команда Код От компьютера после ACK на код Ответ кристалла
Get 00 FF — N = 11, версия, 11 кодов команд, ACK
Get Version 01 FE — версия, 00, 00, ACK
Get ID 02 FD — N = 1, PID (2 байта, старший первым), ACK
Read Memory 11 EE адрес + сумма; N − 1 и его дополнение ACK, ACK, N байт
Go 21 DE адрес + сумма ACK, переход
Write Memory 31 CE адрес + сумма; N − 1, N байт, XOR ACK, ACK
Erase 43 BC N − 1, N номеров по байту, XOR; или FF 00 ACK
Extended Erase 44 BB N − 1 (2 байта), N номеров по 2 байта, XOR; или FF FF 00 ACK
Write Protect 63 9C N − 1, N номеров секторов, XOR ACK, сброс
Write Unprotect 73 8C — ACK, сброс
Readout Protect 82 7D — ACK, сброс
Readout Unprotect 92 6D — стирание всей памяти, ACK, сброс

Ограничения и подробности, о которые спотыкаются:


48д.6 Своя утилита прошивки и модель загрузчика

Утилита не привязана к последовательному порту: класс Stm32Boot говорит с любым объектом, у которого есть write, read (по тайм-ауту вернёт меньше) и flush_input. Порт на pyserial — класс SerialPort; модель загрузчика — ModelPort. Так один и тот же код работает с платой и проверяется на компьютере. Полностью:

# -*- coding: utf-8 -*-
"""stm32boot48d.py — раздел 48д: своя утилита прошивки через заводской загрузчик STM32 (протокол USART, AN3155).

Протокол не знает, что под ним: класс Stm32Boot работает с любым «портом» — объектом с методами
write(data), read(n) (вернёт меньше n байт по тайм-ауту), flush_input() и, по желанию, set_boot0(v),
set_reset(v). Порт на pyserial — SerialPort ниже; модель загрузчика — в model48d.py.
Запуск с настоящей платой (нужен pyserial):  python stm32boot48d.py COM5 firmware.bin [0x08000000]"""
import sys, time

ACK, NACK, SYNC = 0x79, 0x1F, 0x7F
GET, GV, GID, READ, GO, WRITE = 0x00, 0x01, 0x02, 0x11, 0x21, 0x31
ERASE, EXT_ERASE, WP, WUP, RP, RUP = 0x43, 0x44, 0x63, 0x73, 0x82, 0x92
# Раскладка флеш-памяти по номеру кристалла (PID из Get ID): F103 medium-density — страницы по 1 КБ
# (RM0008, таблица 5), F407 — секторы 16/16/16/16/64/128 × 7 КБ (RM0090, таблица 5).
LAYOUT = {
    0x410: ("страницы", [1024] * 128),
    0x413: ("секторы", [16384] * 4 + [65536] + [131072] * 7),
}


class BootError(Exception):
    pass


class Nack(BootError):
    pass


def xor(data):
    x = 0
    for b in data:
        x ^= b
    return x


class SerialPort:
    """Порт на pyserial: 8 бит, чётность even, 1 стоп (AN2606: так настроен USART загрузчика).
    DTR ведёт BOOT0, RTS — NRST; полярность зависит от переходника, поэтому её можно перевернуть."""

    def __init__(self, name, baud=115200, timeout=1.0, inv_boot0=False, inv_reset=False):
        import serial  # pyserial
        self.s = serial.Serial(name, baud, bytesize=8, parity=serial.PARITY_EVEN, stopbits=1, timeout=timeout)
        self.inv_boot0, self.inv_reset = inv_boot0, inv_reset

    def write(self, data):
        self.s.write(bytes(data))

    def read(self, n):
        return self.s.read(n)

    def flush_input(self):
        time.sleep(0.05)
        self.s.reset_input_buffer()

    def set_boot0(self, level):
        self.s.dtr = bool(level) != self.inv_boot0

    def set_reset(self, active):
        self.s.rts = bool(active) != self.inv_reset


class Stm32Boot:
    def __init__(self, port, echo=False):
        self.port = port
        self.echo = echo          # переходник возвращает каждый посланный байт (полудуплекс, один провод)
        self.tx = self.rx = 0     # байт в линии туда и обратно — для оценки времени
        self.version = None
        self.cmds = b""

    def _send(self, data):
        data = bytes(data)
        self.port.write(data)
        self.tx += len(data)
        if self.echo:
            back = self.port.read(len(data))
            if back != data:
                raise BootError("эхо не совпало: %s" % back.hex())

    def _recv(self, n):
        data = self.port.read(n)
        self.rx += len(data)
        if len(data) != n:
            raise BootError("тайм-аут: ждали %d байт, пришло %d" % (n, len(data)))
        return data

    def _ack(self, what):
        r = self._recv(1)[0]
        if r == ACK:
            return
        if r == NACK:
            self.port.flush_input()   # F407 V3.1 при защите чтения шлёт два NACK (AN2606, таблица 23)
            raise Nack("NACK: " + what)
        raise BootError("%s: ответ 0x%02X вместо 0x79" % (what, r))

    def _cmd(self, code):
        self._send((code, code ^ 0xFF))
        self._ack("команда 0x%02X" % code)

    def _addr(self, addr):
        a = addr.to_bytes(4, "big")
        self._send(a + bytes((xor(a),)))
        self._ack("адрес 0x%08X" % addr)

    def sync(self, tries=2):
        """0x7F — по нему загрузчик меряет скорость. NACK на повторный 0x7F значит: связь уже была."""
        for _ in range(tries):
            self.port.write(b"\x7f")
            self.tx += 1
            if self.echo:
                self.port.read(1)
            r = self.port.read(1)
            self.rx += len(r)
            if r == bytes((ACK,)):
                return "ack"
            if r == bytes((NACK,)):
                return "уже"
            if r == b"\x7f":
                raise BootError("эхо: вернулся свой 0x7F — включите echo=True")
            if r:
                raise BootError("ответ 0x%02X вместо 0x79: скорость или формат кадра" % r[0])
        raise BootError("нет ответа на 0x7F: BOOT0, сброс, TX/RX, питание")

    def get(self):
        self._cmd(GET)
        n = self._recv(1)[0]
        data = self._recv(n + 1)
        self._ack("Get")
        self.version, self.cmds = data[0], data[1:]
        return self.version, self.cmds

    def get_version(self):
        self._cmd(GV)
        data = self._recv(3)
        self._ack("Get Version")
        return data[0], data[1], data[2]

    def get_id(self):
        self._cmd(GID)
        n = self._recv(1)[0]
        pid = self._recv(n + 1)
        self._ack("Get ID")
        return int.from_bytes(pid, "big")

    def read_memory(self, addr, n):
        if not 1 <= n <= 256:
            raise ValueError("Read Memory: от 1 до 256 байт")
        self._cmd(READ)
        self._addr(addr)
        self._send((n - 1, (n - 1) ^ 0xFF))
        self._ack("длина %d" % n)
        return self._recv(n)

    def write_memory(self, addr, data):
        data = bytes(data)
        if not 1 <= len(data) <= 256 or len(data) % 4 or addr % 4:
            raise ValueError("Write Memory: 4…256 байт, кратно 4, адрес кратен 4")
        self._cmd(WRITE)
        self._addr(addr)
        block = bytes((len(data) - 1,)) + data
        self._send(block + bytes((xor(block),)))
        self._ack("запись 0x%08X" % addr)

    def go(self, addr):
        self._cmd(GO)
        self._addr(addr)

    def erase(self, pages):
        """Erase 0x43 (версии до 3.0): номера страниц по байту, не больше 255 за раз."""
        pages = list(pages)
        if not 1 <= len(pages) <= 255 or max(pages) > 254:
            raise ValueError("Erase: от 1 до 255 страниц с номерами до 254")
        self._cmd(ERASE)
        block = bytes([len(pages) - 1] + pages)
        self._send(block + bytes((xor(block),)))
        self._ack("стирание %d стр." % len(pages))

    def extended_erase(self, pages):
        """Extended Erase 0x44 (версии 3.0 и новее): номера по два байта, старший первым."""
        pages = list(pages)
        block = (len(pages) - 1).to_bytes(2, "big") + b"".join(p.to_bytes(2, "big") for p in pages)
        self._cmd(EXT_ERASE)
        self._send(block + bytes((xor(block),)))
        self._ack("стирание %d стр." % len(pages))

    def mass_erase(self):
        if EXT_ERASE in self.cmds:
            self._cmd(EXT_ERASE)
            self._send(b"\xff\xff\x00")
        else:
            self._cmd(ERASE)
            self._send(b"\xff\x00")
        self._ack("стирание всей памяти")

    def write_protect(self, sectors):
        sectors = list(sectors)
        self._cmd(WP)
        block = bytes([len(sectors) - 1] + sectors)
        self._send(block + bytes((xor(block),)))
        self._ack("защита записи")      # потом загрузчик сбрасывает кристалл: снова sync()

    def write_unprotect(self):
        self._cmd(WUP)
        self._ack("снятие защиты записи")

    def readout_protect(self):
        self._cmd(RP)
        self._ack("защита чтения")

    def readout_unprotect(self):
        self._cmd(RUP)
        self._ack("снятие защиты чтения")  # перед этим ACK загрузчик стирает всю флеш-память


def plan_erase(pid, base, size, flash=0x08000000):
    """Какие страницы или секторы задевает образ [base, base + size)."""
    if pid not in LAYOUT:
        raise BootError("PID 0x%03X: раскладка флеш-памяти неизвестна" % pid)
    kind, sizes = LAYOUT[pid]
    out, start = [], flash
    for i, sz in enumerate(sizes):
        if start < base + size and base < start + sz:
            out.append(i)
        start += sz
    return kind, out


def program(boot, image, base=0x08000000, verify=True, run=True, log=print):
    """Целиком: связь, что за кристалл, стирание нужного, запись по 256 байт, сверка, запуск."""
    image = bytes(image) + b"\xff" * (-len(image) % 4)
    boot.sync()
    ver, cmds = boot.get()
    pid = boot.get_id()
    log("загрузчик %d.%d, PID 0x%03X, команд %d" % (ver >> 4, ver & 15, pid, len(cmds)))
    kind, pages = plan_erase(pid, base, len(image))
    if EXT_ERASE in cmds:
        boot.extended_erase(pages)
    elif ERASE in cmds:
        boot.erase(pages)
    else:
        raise BootError("нет команды стирания")
    log("стёрто: %s %d…%d (%d)" % (kind, pages[0], pages[-1], len(pages)))
    t0 = boot.tx + boot.rx
    for off in range(0, len(image), 256):
        boot.write_memory(base + off, image[off:off + 256])
    log("записано %d байт блоками по 256: %d блоков, %d байт в линии" % (len(image), (len(image) + 255) // 256, boot.tx + boot.rx - t0))
    if verify:
        t0 = boot.tx + boot.rx
        for off in range(0, len(image), 256):
            n = min(256, len(image) - off)
            if boot.read_memory(base + off, n) != image[off:off + n]:
                raise BootError("сверка: расхождение в блоке 0x%08X" % (base + off))
        log("сверка совпала, %d байт в линии" % (boot.tx + boot.rx - t0))
    if run:
        boot.go(base)
        log("Go 0x%08X" % base)


def enter_bootloader(port, wait=0.1):
    """BOOT0 = 1 и импульс NRST: кристалл стартует из системной памяти. wait — время запуска загрузчика
    (AN2606, таблица 69: от 1,227 мс у F10xxx до 84,59 мс у F40xxx V3.x)."""
    port.set_boot0(1)
    port.set_reset(1)
    time.sleep(0.01)
    port.set_reset(0)
    time.sleep(wait)


def leave_bootloader(port):
    port.set_boot0(0)
    port.set_reset(1)
    time.sleep(0.01)
    port.set_reset(0)


def main(argv):
    if len(argv) < 3:
        print(__doc__)
        return 2
    base = int(argv[3], 0) if len(argv) > 3 else 0x08000000
    port = SerialPort(argv[1])
    image = open(argv[2], "rb").read()
    enter_bootloader(port)
    program(Stm32Boot(port), image, base)
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))

Модель загрузчика (model48d.py) повторяет AN3155 команда за командой и берёт из AN2606 то, что зависит от кристалла: PID, версию, команду стирания, занятое загрузчиком ОЗУ, адреса системной памяти, номер 0x31 у F407 и двойной NACK у F407 V3.1. Каждая команда — генератор Python: байт пришёл — шаг вперёд. Разбор кода и записи — целиком:

    def _session(self):
        while True:
            c = yield from self._rx()
            x = yield from self._rx()
            if c ^ x != 0xFF:
                self._nack()
                continue
            h = {0x00: self._get, 0x01: self._gv, 0x02: self._gid, 0x11: self._read, 0x21: self._go,
                 0x31: self._write, self.c["erase"]: self._erase if self.c["erase"] == 0x43 else self._ext_erase,
                 0x63: self._wp, 0x73: self._wup, 0x82: self._rp, 0x92: self._rup}.get(c)
            if h is None:
                self._nack()
                continue
            if self.rdp and c not in RDP_ALLOWED:
                self._nack(double=self.c["double_nack"] and c in (0x11, 0x31))
                continue
            self.out.append(ACK)
            done = yield from h()
            if done == "reset":
                self.reset(clear=False)   # ответ ушёл, потом кристалл сбрасывается
                return
            if done == "go":
                return
    def _write(self):
        addr = yield from self._addr()
        mem, off = self._region(addr, 4, "write") if addr is not None else (None, None)
        if mem is None or addr % 4:
            self._nack()
            return
        self.out.append(ACK)
        n = yield from self._rx()
        data = yield from self._rxn(n + 1)
        cks = yield from self._rx()
        mem, off = self._region(addr, n + 1, "write")
        if xor(bytes([n]) + data) != cks or (n + 1) % 4 or mem is None:
            self._nack()            # модель: длина не кратна 4 — NACK (AN3155: N+1 должно быть кратно 4)
            return
        if mem is self.vrange:
            if data[0] > 4 or data[1:] != b"\xff\xff\xff":
                self._nack()
                return
        if mem is self.flash:
            for i, b in enumerate(data):
                if self._page_of(off + i) in self.wp:
                    continue        # AN3155: запись в защищённое — без ошибки, но и без записи
                old = mem[off + i]
                if old != 0xFF and old != b:
                    self._nack()    # модель: поверх незатёртого — ошибка записи
                    return
                mem[off + i] = b
        else:
            mem[off:off + n + 1] = data
        self.out.append(ACK)

Там, где документы молчат, модель выбирает своё и помечает «модель:»: байт с ошибкой чётности отбрасывается молча; запись длиной не кратной 4 и запись поверх незатёртого — NACK; стирание банка у одно-банкового F407 — NACK.

Прогон. Утилита против модели, без платы:

GET chip=F103C8 sync=ack version=0x22 n=11 cmds=0001021121314363738292 gv=22-00-00 pid=0x410
GET chip=F407 sync=ack version=0x31 n=11 cmds=0001021121314463738292 gv=31-00-00 pid=0x413
F407 blid=0x31 vrange=3
Образ Стёрто Блоков по 256 Байт в линии: связь, стирание, запись Время линии Байт на сверку Время
F103C8, 20 000 байт страницы 0–19 79 21 000 2,01 с 20 948 2,00 с
F103C8, 65 536 байт страницы 0–63 256 68 704 6,56 с 68 608 6,55 с
F407, 70 000 байт секторы 0–4 274 73 331 7,00 с 73 288 7,00 с

Время линии — байты туда и обратно × 11 бит / 115 200 бит/с: так долго идут сами байты, без записи и стирания флеш-памяти. Один блок записи — 265 байт туда и 3 обратно, 268 × 11 / 115 200 = 25,6 мс, то есть около 10 000 байт данных в секунду; сверка блока — 9 байт туда и 259 обратно, столько же. Весь F103C8 (64 КБ) идёт по проводу 6,56 с на запись и 6,55 с на сверку. Go в каждом прогоне нашёл стек и вход из таблицы векторов образа: sp=0x20005000, pc=0x08000101.

Отказы и тонкости — сырые байты ответа:

ERR bad_complement=1f
ERR addr_bad_cks=791f
ERR write_ram_bootloader_area=791f
ERR write_ram_0x20000200=797979
ERR write_len6=79791f
ERR util_len6=Write Memory: 4…256 байт, кратно 4, адрес кратен 4
ERR go_sysmem=791f
ERR read_sysmem=00000000
ERR overwrite=NACK
ERR mass_erase_ff_01=7979 still_data=True
ERR mass_erase_ff_00=7979 erased=True

Неверное дополнение кода — NACK сразу; неверная сумма адреса — ACK на код, NACK на адрес; первые 512 байт ОЗУ F103 заняты загрузчиком — NACK, с 0x20000200 запись проходит; Go в системную память — NACK, а прочитать её можно. Утилита сама не пускает длину 6. FF 01 вместо FF 00 после Erase — ACK, но ничего не стёрто (AN3155, примечание к 3.7).

Защита чтения, защита записи, связь:

RDP chip=F103C8 after_rp_state=sync read_reply=1f util=Nack gv_rdp=22-00-00 pid=0x410 unprotect_blank=True mass_erase
RDP chip=F407 after_rp_state=sync read_reply=1f1f util=Nack gv_rdp=31-00-00 pid=0x413 unprotect_blank=True mass_erase
WP chip=F407 sectors=0 program=сверка:_расхождение_в_блоке_0x08000000 flash_first=ffffffff
WP after_unprotect ok=True
SYNC first=ack second=уже
SYNC echo_plain=эхо: вернулся свой 0x7F — включите echo=True
SYNC echo_mode sync=ack pid=0x410
SYNC boot0_low=нет ответа на 0x7F: BOOT0, сброс, TX/RX, питание
SYNC after_enter=ack

После Readout Protect кристалл сбросился (after_rp_state=sync — ждёт 0x7F). Read на F103 — один NACK, на F407 — два (1f1f): это известное ограничение F40xxx V3.1 и V9.0 (AN2606, таблицы 23 и 25). Утилита после NACK вычищает приёмный буфер, и следующая команда идёт с чистого листа. Get Version и Get ID работают и под защитой. Снятие защиты — флеш-память чистая. С защитой записи сектора 0 стирание и запись F407 прошли с ACK, а во флеш-памяти ffffffff: ошибку поймала только сверка.

Тот же код через настоящий pyserial. В stm-pio:1 (pyserial 3.5, Python 3.12.14) модель сидит на одном конце псевдотерминала Linux, утилита открывает другой как обычный последовательный порт — SerialPort с чётностью even, тайм-аутом 1 с:

PTY chip=F103C8 pyserial=3.5 parity=E dtr=OSError ok=True go=0x08000000_sp=0x20005000_pc=0x08000101
PTY   загрузчик 2.2, PID 0x410, команд 11
PTY   стёрто: страницы 0…19 (20)
PTY   записано 20000 байт блоками по 256: 79 блоков, 20948 байт в линии
PTY   сверка совпала, 20948 байт в линии
PTY   Go 0x08000000
PTY chip=F407 pyserial=3.5 parity=E dtr=OSError ok=True go=0x08000000_sp=0x20020000_pc=0x08000101
PTY   загрузчик 3.1, PID 0x413, команд 11
PTY   стёрто: секторы 0…4 (5)
PTY   записано 70000 байт блоками по 256: 274 блоков, 73288 байт в линии
PTY   сверка совпала, 73288 байт в линии
PTY   Go 0x08000000

dtr=OSError: у псевдотерминала нет линий DTR и RTS, на переходнике они есть — управление BOOT0 и NRST проверено только на модели.


48д.7 Грабли

Синхронизация прошла, а первая команда — тишина. Порт открыт в 8N1. Модель линии показывает почему: в 0x7F нечётное число единиц, бит чётности — единица, и стоповый бит 8N1 сходит за него; кристалл отвечает 0x79. А у 00 FF (Get) на месте чётности снова стоп-бит-единица, нужен ноль — ошибка чётности, байт выброшен (parity_errors=1), утилита ждёт ответа впустую:

LINE host=115200/8E1 sync=ack dev_baud=115384.6 err=0.160% get=0x22 parity_errors=0
LINE host=9600/8E1 sync=ack dev_baud=9600.0 err=0.000% get=0x22 parity_errors=0
LINE host=115200/8N1 sync=ack dev_baud=115384.6 err=0.160% get=тайм-аут:_ждали_1_байт,_пришло_0 parity_errors=1
LINE garbage_first dev_baud=7303.7 sync=ответ_0x00_вместо_0x79:_скорость_или_формат_кадра

Лечение — 8E1 в настройках порта (parity=PARITY_EVEN).

Первый байт — не 0x7F. При открытии порта или включении переходника на линию проскакивает мусор, и загрузчик меряет скорость по нему. В модели байт 0x00 за 1 мс до 0x7F: от его спада до спада 0x7F — не 8 битов, а больше сотни; кристалл настроился на 7303,7 бит/с вместо 115 200 и отвечает кашей (ответ 0x00 вместо 0x79). Поможет только сброс и чистый 0x7F. AN2606 (3.3) поэтому велит держать RX неиспользуемых интерфейсов загрузчика в известном уровне во время выбора интерфейса: плавающая линия может включить не тот.

NACK в ответ на 0x7F. Связь уже была — утилита запускалась второй раз без сброса, и загрузчик принял 0x7F за код команды. Утилита посылает 0x7F ещё раз: пара 7F 7F — не код с дополнением, NACK, значит, канал жив (second=уже в прогоне).

Получил свой же 0x7F. Эхо: полудуплексная линия, RS-485 или один провод на TX и RX — переходник возвращает каждый посланный байт. Утилита с echo=True вычитывает эхо после каждой посылки и сравнивает.

Нет ответа совсем. BOOT0 не в единице или сброса не было: камень в приложении (boot0_low в прогоне). Ещё — TX и RX перепутаны, нет общей земли, или загрузчик не успел запуститься (AN2606, таблица 69 — до 84,59 мс у F40xxx V3.x).

DTR и RTS — «наоборот». Полярность DTR и RTS на выводах переходника зависит от микросхемы и схемы платы, и при открытии порта линии могут переключиться сами — плата сбросится в момент открытия. В SerialPort полярность переворачивается флагами; проверьте её мультиметром на своём переходнике.

После Go приложение ведёт себя странно. AN3155 (3.5) пишет, что перед переходом регистры периферии загрузчика возвращаются к значениям после сброса, а AN2606 (примечание к 3.1) — что нет; документы расходятся. Надёжно — не надеяться: приложение настраивает USART1, такты и всё своё само. Если включён аппаратный сторожевой таймер, загрузчик поставил ему наибольший делитель (AN2606, таблица 6) — приложению его надо перенастроить.

Записалось с ACK, а во флеш-памяти пусто. Сектор под защитой записи: загрузчик не возвращает ошибку (AN3155, 3.6). Сверка чтением после записи — не роскошь.

F407 пишет медленно. По умолчанию загрузчик F2 и F4 пишет флеш-память по байту (AN2606, 3.4 и таблица 22); запись 03 FF FF FF по адресу 0xFFFF0000 переключает на слова при питании 2,7–3,6 В.

Известные ограничения по AN2606. У F10xxx в таблице 7 известных ограничений нет. У F40xxx V3.0 при Read или Write с неподдерживаемым адресом и верной суммой NACK не приходит, и следующие 2 байта читаются как новая команда (таблица 23; исправлено в V3.1); у V3.1 и V9.0 — два NACK при защите чтения, а через CAN не работает Write Unprotect — вместо неё пишут байты конфигурации командой Write Memory (таблицы 23 и 25).


48д.8 Итоги

Дальше: раздел 39 «Как читать документацию ST».

Открыть в интерактивном справочнике