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 и раскладка флеш-памяти).
Что умеет загрузчик, одинаково для всех семейств: принять образ по одному из интерфейсов, стереть и записать флеш-память, прочитать её, включить и снять защиту чтения и записи, перейти на адрес. Чего он не умеет: проверить, чей образ (подписи нет), откатиться на прежнюю версию, принять образ по радио.
| Когда | Заводской | Свой (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);
}
Почему так:
- Признак — в последних 16 байтах ОЗУ, по одному адресу у приложения и стартового кода (48.6): секция компоновщика укорачивается на 16 байт. Загрузчик ST сам занимает начало ОЗУ — 512 байт у F10xxx (AN2606, таблица 6), 8 КБ у F40xxx V3.x и 12 КБ у V9.x (таблицы 22 и 24), — до конца ОЗУ он не доходит. Прочитали — стёрли, иначе прибор навсегда в загрузчике.
- Стек — MSP,
CONTROL= 0. Если приложение работает под ОС на PSP, загрузчик начал бы на чужом стеке. Тот жеCONTROL= 0 снимает след плавающей точки на F4 и G4. Вызывать прыжок надо из обычного кода, не из обработчика прерывания: иначе загрузчик окажется внутри исключения. PRIMASK= 0 передbx. После сброса прерывания разрешены; NVIC к этому моменту весь выключен, SysTick стоит — пустить прерывания безопасно.- На F4 и G4 по адресу 0 ставится системная память (
SYSCFG_MEMRMP,MEM_MODE= 01), аVTOR— на её начало. AN2606 (3.1) советует это для кристаллов с двумя банками, где прыжок иначе вернётся в приложение; у F1 такого регистра нет, там толькоVTOR. - Сброс периферии — импульсом в регистрах
…RSTR, такты — значениями после сброса из справочников: у F103AHBENR=0x14(тактирование SRAM и интерфейса флеш-памяти в режиме сна), у F407AHB1ENR=0x00100000, у G474AHB1ENR=0x100иAPB1ENR1=0x400. Каналы DMA у F103 гасятся битомEN— сбросить DMA через RCC у него нечем (48.5). У G4 вAHB1RSTRне трогается битFLASHRST: из флеш-памяти идёт этот же код. - Ожидания — с пределом. Если HSI не поднимается, прыжок всё равно состоится, а не повиснет.
Размеры (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, сброс |
Ограничения и подробности, о которые спотыкаются:
- Не больше 256 байт за одно чтение и одну запись: длина идёт байтом N − 1 (AN3155, 3.4 и 3.6). Адрес записи кратен 4, длина кратна 4 (AN2606, 3.4).
- Erase или Extended Erase — одна из двух. Extended Erase появилась в
версии протокола 3.0 и вытеснила Erase (AN3155, таблица 3). У F103 (V2.2)
— Erase
0x43, у F407 (V3.1) — Extended Erase0x44. Утилита смотрит, какой код вернула Get. Номера в Extended Erase двухбайтовые и ещё0xFFFF— всё,0xFFFEи0xFFFD— банк 1 и банк 2 (AN3155, 3.8). Для F103 «страница» — 1 КБ (RM0008, таблица 5), для F407 — сектор 16, 64 или 128 КБ (RM0090, таблица 5). - При защите чтения работают только Get, Get Version, Get ID и Readout Unprotect, остальное — NACK (AN3155, таблица 2). Снятие защиты стирает всю флеш-память, загрузчик V2.2 ещё и обнуляет ОЗУ (AN2606, таблица 7).
- Write Protect, Write Unprotect, Readout Protect и Readout Unprotect
кончаются сбросом кристалла — после них снова
0x7F. Вторая Write Protect заменяет первую, а не добавляет к ней (AN3155, 3.9). - Запись и стирание защищённых секторов проходят без ошибки — ACK есть, а флеш-память не изменилась (AN3155, 3.6–3.8). Без сверки чтением этого не увидеть.
- Go берёт стек по адресу и переходит по адресу + 4 (AN3155, 3.5). Адрес в системной памяти и байтах конфигурации — NACK (с версии 2.1).
0xFFFF0000у F2 и F4 — не память, а настройка: напряжение питания для записи (AN2606, таблица 4). По умолчанию F4 пишет флеш-память по байту; запись03 FF FF FFразрешает запись словами при питании 2,7–3,6 В.
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 Итоги
- Заводской загрузчик лежит в системной памяти и не стирается: F103 —
0x1FFFF000, 2 КБ; F407 —0x1FFF0000…0x1FFF77FF; G474 —0x1FFF0000, 28 КБ. Подписи и отката у него нет; места во флеш-памяти он не занимает. - Вход:
BOOT0= 1 иBOOT1= 0 на F1 и F4; на G4 —PB8или битnBOOT0в зависимости отnSWBOOT0, плюсnBOOT1= 1. Из прошивки — признак в ОЗУ и сброс либо прыжок с возвратом камня к состоянию после сброса: 436 байт кода у F1, 504 у F4, 464 у G4; в QEMU загрузчик получилCONTROL,PRIMASK, SysTick и NVIC в нулях. - F103C8: только
USART1наPA9/PA10, протокол V2.2, PID0x410, Erase. F407:USART1,USART3,CAN2, USB DFU (у V9.x ещё I²C и SPI), V3.1, PID0x413, Extended Erase. G474: USART, I²C, SPI, USB DFU, DEV_ID0x469. - Протокол:
0x7Fдля измерения скорости, 8E1, ACK0x79/ NACK0x1F, код с дополнением, XOR-сумма блоков, не больше 256 байт за раз. - Своя утилита на Python — все 12 команд AN3155 поверх любого порта; против модели записала и сверила 20 000, 65 536 и 70 000 байт; через pyserial 3.5 — то же. Блок 256 байт — 268 байт в линии, 25,6 мс при 115 200 бит/с; 64 КБ — 6,56 с записи по проводу и столько же сверки.
- Защита записи молчит: ACK есть, данных нет — спасает только сверка. F407 V3.1 под защитой чтения отвечает двумя NACK. 8N1 вместо 8E1 проходит синхронизацию и ломается на первой команде.
Дальше: раздел 39 «Как читать документацию ST».