Files
templates/c/protocan-boot/ports/stm32-common/pcan_boot_stm32.inc
T
Andrey c6390ae1c9 feat(protocan-boot): Добавь порты MCU и сервисы загрузки
Обеспечь единый процесс обновления для STM32F103/F407/G474 и TMS320F2812/F28335: discovery, служебные ответы, BUSY, тайм-аут сессии и подтверждение приложения. Добавь самостоятельные проекты CCS, аппаратные хранилища и Python-клиент.

Совместимость: структуры конфигурации и порта расширены, поэтому потребители должны пересобраться и обнулить новые поля. Для C28x callbacks используют pcan_boot_byte_t; периодический pcan_boot_poll обслуживает discovery и настроенный тайм-аут.
2026-10-23 14:16:19 +03:00

168 lines
26 KiB
C++

static bool send_frame(void *user, uint32_t id, const pcan_boot_byte_t *data, pcan_boot_byte_t dlc) /* Передаёт только объявленные октеты и дожидается физического завершения ACK. */
{ /* Порт владеет CAN1 без TX IRQ, поэтому флаги завершения не очищаются чужим обработчиком. */
pcan_boot_stm32_t *port = user; /* Получает состояние выделенного аппаратного порта. */
CAN_HandleTypeDef *can = port->can; /* Восстанавливает тип handle семейства HAL текущей единицы трансляции. */
CAN_TxHeaderTypeDef header = {0}; /* Не оставляет мусора в стандартном ID и timestamp-флагах. */
uint8_t bytes[8] = {0}; /* CAN 2.0 передаёт не больше восьми октетов; короткий payload дополняется безопасными нулями. */
uint32_t mailbox, mask, started = HAL_GetTick(); /* Запоминает mailbox, его TXOK-флаг и начало тайм-аута в мс. */
if (dlc > 8U || id > 0x1FFFFFFFUL || (dlc != 0U && data == NULL)) return false; /* Запрещает чтение за payload и идентификатор шире 29 бит. */
if (dlc != 0U) (void)memcpy(bytes, data, dlc); /* Не читает отсутствующие байты коротких служебных сообщений. */
while (HAL_CAN_GetTxMailboxesFreeLevel(can) == 0U) { /* Ожидает освобождения аппаратного передатчика. */
if ((uint32_t)(HAL_GetTick() - started) >= 100U) return false; /* Ограничивает ожидание 100 мс, включая переполнение счётчика времени. */
} /* Завершает ожидание свободного mailbox. */
header.ExtId = id; /* Передаёт адресный ProtoCAN ID без преобразования полей. */
header.IDE = CAN_ID_EXT; /* ProtoCAN использует только расширенные 29-битные идентификаторы. */
header.RTR = CAN_RTR_DATA; /* Все ответы загрузчика содержат обычные данные, а не remote-запрос. */
header.DLC = dlc; /* Фиксирует фактическое число объявленных октетов кадра. */
can->Instance->TSR = CAN_TSR_RQCP0 | CAN_TSR_RQCP1 | CAN_TSR_RQCP2; /* Очищает прежние TXOK/ошибки записью W1C перед новой отправкой; CAN принадлежит только этому порту. */
if (HAL_CAN_AddTxMessage(can, &header, bytes, &mailbox) != HAL_OK) return false; /* Не считает не поставленный в очередь ответ отправленным. */
while (HAL_CAN_IsTxMessagePending(can, mailbox) != 0U) { /* Сохраняет CAN до завершения ответа, включая команду REBOOT. */
if ((uint32_t)(HAL_GetTick() - started) >= 100U) { /* Не зависает навсегда при отсутствии ACK от шины. */
(void)HAL_CAN_AbortTxRequest(can, mailbox); /* Снимает просроченный запрос передачи перед возвратом ошибки. */
return false; /* Ядро не выполнит reset после неподтверждённого ответа. */
} /* Завершает обработку тайм-аута TX. */
} /* Аппаратная передача завершилась либо была отвергнута CAN. */
mask = mailbox == CAN_TX_MAILBOX0 ? CAN_TSR_TXOK0 : mailbox == CAN_TX_MAILBOX1 ? CAN_TSR_TXOK1 : CAN_TSR_TXOK2; /* Сопоставляет bitmask mailbox HAL с флагом успешной передачи bxCAN. */
return (can->Instance->TSR & mask) != 0U; /* Отличает подтверждённый ACK от снятия запроса вследствие ошибки. */
} /* Завершает синхронный транспорт служебных ответов. */
bool STM32_BOOT_VECTORS(void) /* Проверяет только структурную корректность таблицы векторов установленного приложения. */
{ /* Проверка CRC образа выполняется отдельно при VERIFY. */
const uint32_t *vectors = (const uint32_t *)(uintptr_t)STM32_BOOT_BASE; /* Читает первые два слова из фиксированной области приложения. */
uint32_t sp = vectors[0], pc = vectors[1]; /* Извлекает начальный стек и Thumb-адрес reset handler. */
return sp > 0x20000000UL && sp <= STM32_BOOT_RAM_END && (sp & 7U) == 0U && (pc & 1U) != 0U && (pc & ~1UL) >= STM32_BOOT_BASE && (pc & ~1UL) < STM32_BOOT_END; /* Требует доступный стек с выравниванием 8 байт и точку входа внутри приложения. */
} /* Завершает проверку векторов без изменения MCU. */
static bool erase_slot(void *user, pcan_boot_byte_t slot) /* Стирает единственную область после проверки manifest общим ядром. */
{ /* Сохранение старого приложения и rollback в этом профиле отсутствуют. */
pcan_boot_stm32_t *port = user; /* Получает manifest и состояние активации текущего загрузчика. */
uint32_t size = port->boot.manifest.image_size; /* Ограничивает стирание фактической длиной образа в байтах. */
bool ok, locked; /* Сохраняет результаты операции и последующего восстановления защиты Flash. */
if (slot != 0U || size < 8U || size > STM32_BOOT_END - STM32_BOOT_BASE) return false; /* Не допускает чужой слот, неполные векторы и выход за область приложения. */
port->ready = false; /* Инвалидирует разрешение reset до первого изменения Flash. */
port->written = 0U; /* Начинает последовательную запись заново после стирания. */
(void)pcan_boot_report_busy(&port->boot, PCAN_BOOT_CMD_ERASE, HAL_GetTick()); /* Предупреждает клиента перед синхронным блокированием Flash. */
if (HAL_FLASH_Unlock() != HAL_OK) return false; /* Не продолжает работу при отказе разблокировки Flash. */
ok = erase_region(port, size); /* Выполняет семейную операцию по страницам F1 либо секторам F4. */
locked = HAL_FLASH_Lock() == HAL_OK; /* Восстанавливает защиту Flash и при успехе, и при отказе стирания. */
if (ok && locked) (void)pcan_boot_report_busy(&port->boot, PCAN_BOOT_CMD_ERASE, HAL_GetTick()); /* Обновляет время после блокировки Flash, чтобы длительное стирание не погасило сессию. */
return ok && locked; /* Не подтверждает ERASE при любом аппаратном отказе. */
} /* Завершает callback стирания. */
static bool write_slot(void *user, pcan_boot_byte_t slot, uint32_t offset, const pcan_boot_byte_t *data, pcan_boot_byte_t length) /* Программирует последовательные блоки без пересечения защищённых областей. */
{ /* Крайний неполный блок дополняется единицами до аппаратной единицы программирования. */
pcan_boot_stm32_t *port = user; /* Получает длину manifest и ожидаемое следующее смещение. */
uint32_t done = 0U, size = port->boot.manifest.image_size; /* Считает только полезные байты без дополнения FF. */
bool ok = true, locked; /* Позволяет гарантированно закрыть Flash после любой ошибки. */
if (slot != 0U || data == NULL || length == 0U || length > 8U || size < 8U || size > STM32_BOOT_END - STM32_BOOT_BASE || offset != port->written || offset > size || length > size - offset || (offset % STM32_BOOT_UNIT) != 0U) return false; /* Проверяет адреса до сложения, выравнивание и принадлежность текущему образу. */
if ((length % STM32_BOOT_UNIT) != 0U && offset + length != size) return false; /* Дополнение FF допустимо только у последнего блока; повторная запись слова запрещена. */
if (HAL_FLASH_Unlock() != HAL_OK) return false; /* Не выполняет запись при заблокированной Flash. */
while (done < length) { /* Разбивает CAN-блок на аппаратные полуслова F1 либо слова F4. */
uint32_t value = 0xFFFFFFFFUL, portion = length - done; /* Стертое значение FF заполняет только хвост последней аппаратной записи. */
uint32_t address = STM32_BOOT_BASE + offset + done; /* Адрес остаётся в заранее проверенной области приложения. */
HAL_StatusTypeDef status; /* Отделяет аппаратное завершение записи от последующей синхронизации Flash-кэша. */
if (portion > STM32_BOOT_UNIT) portion = STM32_BOOT_UNIT; /* Ограничивает копирование размером аппаратной записи. */
(void)memcpy(&value, data + done, portion); /* Не читает за фактическим payload последнего блока. */
status = HAL_FLASH_Program(STM32_BOOT_PROGRAM, address, value); /* Дожидается завершения программирования выбранной аппаратной единицы. */
STM32_BOOT_SYNC(); /* На F407 сбрасывает строки Flash-кэша после записи, чтобы readback соседнего слова не получил старое FF. */
if (status != HAL_OK || memcmp((const void *)(uintptr_t)address, &value, STM32_BOOT_UNIT) != 0) { /* Проверяет результат HAL и чтение записанного значения, включая хвост FF. */
ok = false; /* Делает частично записанный образ непригодным для VERIFY/COMMIT. */
break; /* Не продолжает программирование после первого отказа. */
} /* Завершает проверку результата программирования. */
done += portion; /* Продвигает полезное смещение без включения дополнения FF в CRC. */
} /* Завершает аппаратные записи текущего CAN-блока. */
locked = HAL_FLASH_Lock() == HAL_OK; /* Обязательно восстанавливает аппаратную блокировку Flash. */
if (ok && locked) port->written += length; /* Подтверждает смещение только после полного успеха и блокировки. */
return ok && locked; /* Ошибка передаётся общему автомату как FLASH_ERROR. */
} /* Завершает callback последовательной записи. */
static pcan_boot_status_t verify_image(void *user, pcan_boot_byte_t slot, const pcan_boot_manifest_t *manifest) /* Перечитывает реальную Flash вместо доверия одному CRC входящего потока. */
{ /* Проверяет геометрию до доступа к памяти и точный размер переданных данных. */
pcan_boot_stm32_t *port = user; /* Получает фактический счётчик успешно записанных байтов. */
const uint8_t *image = (const uint8_t *)(uintptr_t)STM32_BOOT_BASE; /* Отображает фиксированную область приложения в адресное пространство MCU. */
uint32_t offset, crc = 0U; /* Начинает общий потоковый CRC32 с нуля. */
if (slot != 0U || manifest == NULL || manifest->image_size < 8U || manifest->image_size > STM32_BOOT_END - STM32_BOOT_BASE || port->written != manifest->image_size) return PCAN_BOOT_STATUS_INVALID_SIZE; /* Не читает произвольную память по недостоверному manifest. */
for (offset = 0U; offset < manifest->image_size;) { /* Проверяет весь полезный образ ограниченными порциями. */
uint32_t count = manifest->image_size - offset; /* Вычисляет остаток в байтах без выхода за границу образа. */
if (count > 1024U) count = 1024U; /* Порция 1 КиБ ограничивает объём одного вызова потокового CRC. */
crc = pcan_boot_crc32_update(crc, image + offset, count); /* Использует общий полином ProtoCAN и не включает дополнение FF. */
offset += count; /* Продвигает позицию чтения проверенного образа. */
} /* Завершает чтение Flash для независимой проверки CRC. */
if (crc != manifest->image_crc32) return PCAN_BOOT_STATUS_CRC_ERROR; /* Обнаруживает повреждение памяти после успешного приёма CAN. */
if (!STM32_BOOT_VECTORS() || ((*(const uint32_t *)(const void *)(image + 4U)) & ~1UL) >= STM32_BOOT_BASE + manifest->image_size) return PCAN_BOOT_STATUS_INVALID_SIZE; /* Не принимает reset handler вне фактически загруженного образа. */
return PCAN_BOOT_STATUS_OK; /* Реальная Flash и векторы соответствуют полученному manifest. */
} /* Завершает проверку записанного образа. */
static bool commit_image(void *user, pcan_boot_byte_t slot, const pcan_boot_manifest_t *manifest) /* Активирует только повторно проверенный single-slot образ. */
{ /* Не имитирует постоянные A/B-метаданные или защиту от потери питания. */
pcan_boot_stm32_t *port = user; /* Получает RAM-признак успешного COMMIT текущей сессии. */
port->ready = verify_image(user, slot, manifest) == PCAN_BOOT_STATUS_OK; /* Перечитывает Flash перед разрешением reset. */
return port->ready; /* Ядро перейдёт в COMMITTED только при успешной проверке. */
} /* Завершает активацию без записи служебных страниц. */
static bool can_reboot(void *user) /* Запрещает программный запуск недописанного приложения по команде протокола. */
{ /* Политика запуска после внешнего reset остаётся обязанностью проекта. */
pcan_boot_stm32_t *port = user; /* Получает состояние текущей сессии и подтверждение COMMIT. */
return port->ready && port->boot.state == PCAN_BOOT_STATE_COMMITTED; /* ABORT, новая сессия и ошибки сами по себе не дают права reset. */
} /* Завершает проверку допустимости протокольного reset. */
static void reboot(void *user) /* Выполняет reset только после синхронно отправленного ответа ядра. */
{ /* Повторно проверяет RAM-разрешение непосредственно перед сбросом. */
if (can_reboot(user)) NVIC_SystemReset(); /* Использует штатный CMSIS reset, оставляя startup и выбор приложения проекту. */
} /* Завершает callback перезапуска. */
bool STM32_BOOT_INIT(pcan_boot_stm32_t *port, CAN_HandleTypeDef *can, const pcan_boot_config_t *config) /* Подключает текущий API ядра к выделенным CAN1 и single-slot Flash. */
{ /* Тактирование, GPIO, bitrate, HAL_Init и SysTick должны быть настроены проектом заранее. */
pcan_boot_port_t callbacks = {0}; /* Новые необязательные callbacks ядра всегда остаются явно нулевыми. */
pcan_boot_config_t single; /* Копирует идентичность прибора, переопределяя геометрию профиля. */
CAN_FilterTypeDef filter = {0}; /* Подготавливает адресный фильтр 0 и discovery-фильтр 1 в FIFO0. */
uint32_t id, mask; /* Содержит идентификатор и аппаратную маску IDE/RTR сдвига bxCAN. */
if (port == NULL || can == NULL || config == NULL || can->Instance != CAN1) return false; /* Отклоняет неверные указатели и неподдерживаемый CAN2. */
single = *config; /* Сохраняет DeviceType/ID, Product, HW, FW, ack_window и тайм-аут сессии проекта. */
single.active_slot = 0U; /* Single-slot обновление всегда адресует логический слот A. */
single.slot_count = 1U; /* Исключает старую имитацию single-slot через фиктивный active_slot=1. */
single.max_image_size = STM32_BOOT_END - STM32_BOOT_BASE; /* Ядро отвергает превышение физической области ещё до erase. */
callbacks.send = send_frame; /* Подключает синхронную отправку подтверждённых CAN-ответов. */
callbacks.erase_slot = erase_slot; /* Подключает стирание только области приложения. */
callbacks.write_slot = write_slot; /* Подключает последовательное программирование с readback каждого слова. */
callbacks.verify_status = verify_image; /* Возвращает отдельные статусы CRC_ERROR и INVALID_SIZE для Flash/vectors. */
callbacks.set_pending_slot = commit_image; /* Согласует COMMIT с проверкой реального образа и RAM-признаком активации. */
callbacks.can_reboot = can_reboot; /* Не позволяет REBOOT обойти проверку завершённого образа. */
callbacks.reboot = reboot; /* Выполняет CMSIS reset после подтверждения ответа на шине. */
(void)memset(port, 0, sizeof(*port)); /* Очищает старые счётчики и разрешение активации до настройки сессии. */
port->can = can; /* Сохраняет заранее инициализированный handle HAL. */
if (!pcan_boot_init(&port->boot, &single, &callbacks, port)) { port->can = NULL; return false; } /* При ошибке адреса оставляет последующий poll безопасным. */
id = pcan_boot_make_id(0U, PCAN_BOOT_ROUTE_FROM_PM, single.device_type, single.device_id, 0U, 0U) << 3U; /* Переводит адрес прибора в регистровый формат bxCAN. */
mask = (0x0FF00000UL << 3U) | 6U; /* Проверяет Route/Type/ID, IDE=1 и RTR=0; приоритет, MsgType и Body остаются открытыми. */
id |= 4U; /* Устанавливает IDE в бите 2 аппаратного фильтра bxCAN. */
filter.FilterBank = 0U; /* Выделенный загрузчик использует первый банк фильтров. */
filter.FilterMode = CAN_FILTERMODE_IDMASK; /* Сравнивает адрес по маске вместо списка отдельных команд. */
filter.FilterScale = CAN_FILTERSCALE_32BIT; /* Сохраняет все 29 бит расширенного идентификатора. */
filter.FilterIdHigh = id >> 16U; /* Передаёт HAL старшую половину регистрового идентификатора. */
filter.FilterIdLow = id & 0xFFFFU; /* Передаёт HAL младшую половину, включая IDE/RTR. */
filter.FilterMaskIdHigh = mask >> 16U; /* Маскирует адрес прибора и направление обмена. */
filter.FilterMaskIdLow = mask & 0xFFFFU; /* Отвергает стандартные и remote-кадры до программного разбора. */
filter.FilterFIFOAssignment = CAN_FILTER_FIFO0; /* Приём выполняется опросом FIFO0 без IRQ. */
filter.FilterActivation = ENABLE; /* Включает настроенный адресный фильтр. */
filter.SlaveStartFilterBank = 14U; /* Стандартная граница банков CAN1/CAN2 в HAL; порт не использует CAN2. */
if (HAL_CAN_ConfigFilter(can, &filter) != HAL_OK) { port->can = NULL; return false; } /* Не запускает загрузчик с неподтверждённой настройкой адресного фильтра. */
id = (pcan_boot_make_id(0U, PCAN_BOOT_ROUTE_FROM_PM, 7U, 15U, PCAN_BOOT_MSG_DISCOVERY, 0U) << 3U) | 4U; /* Отдельно разрешает штатный broadcast discovery, сохраняя запрет broadcast-записи. */
mask = (0x0FFFFFFFUL << 3U) | 6U; /* Проверяет всё кроме приоритета, включая MsgType=DISCOVERY и пустой Body. */
filter.FilterBank = 1U; /* Второй выделенный банк обслуживает только обнаружение устройств. */
filter.FilterIdHigh = id >> 16U; /* Записывает старшую половину broadcast discovery ID. */
filter.FilterIdLow = id & 0xFFFFU; /* Записывает младшую половину с IDE=1 и RTR=0. */
filter.FilterMaskIdHigh = mask >> 16U; /* Ограничивает тип и адрес broadcast запроса. */
filter.FilterMaskIdLow = mask & 0xFFFFU; /* Ограничивает Body и формат кадра discovery. */
if (HAL_CAN_ConfigFilter(can, &filter) != HAL_OK || HAL_CAN_Start(can) != HAL_OK) { port->can = NULL; return false; } /* Обезвреживает poll при отказе второго фильтра либо запуска CAN. */
return true; /* Оба фильтра настроены, CAN успешно вышел из режима init. */
} /* Завершает настройку одного аппаратного порта. */
void STM32_BOOT_POLL(pcan_boot_stm32_t *port) /* Обслуживает загрузчик только из основного цикла, где допустимо синхронное стирание. */
{ /* Вызов из CAN IRQ не допускается из-за длительных операций Flash. */
CAN_HandleTypeDef *can; /* Получает аппаратный handle после проверки состояния порта. */
uint32_t count; /* Ограничивает обработку глубиной аппаратного FIFO bxCAN. */
if (port == NULL || port->can == NULL) return; /* Позволяет безопасно пропустить ещё не настроенный порт. */
can = port->can; /* Восстанавливает семейный HAL-тип handle. */
pcan_boot_poll(&port->boot, HAL_GetTick()); /* Обновляет монотонное время и тайм-аут сессии до приёма команд. */
for (count = 0U; count < 3U && HAL_CAN_GetRxFifoFillLevel(can, CAN_RX_FIFO0) != 0U; ++count) { /* Не позволяет непрерывной нагрузке CAN захватить весь главный цикл. */
CAN_RxHeaderTypeDef header = {0}; /* Принимает IDE/RTR/DLC независимо от аппаратной настройки фильтра. */
uint8_t data[8]; /* Выделяет максимальный payload одного classic CAN-кадра. */
if (HAL_CAN_GetRxMessage(can, CAN_RX_FIFO0, &header, data) != HAL_OK) break; /* Не разбирает неполученный кадр при аппаратной ошибке. */
if (header.IDE == CAN_ID_EXT && header.RTR == CAN_RTR_DATA && header.DLC <= 8U && header.ExtId <= 0x1FFFFFFFUL) { /* Повторно проверяет внешний ввод до вызова общего decoder. */
pcan_boot_poll(&port->boot, HAL_GetTick()); /* Учитывает время предыдущего синхронного erase перед следующей командой. */
(void)pcan_boot_process(&port->boot, header.ExtId, data, (uint8_t)header.DLC); /* Передаёт адресные команды и восьмибайтовые блоки общему автомату. */
} /* Завершает обработку допустимого формата кадра. */
} /* Возвращает управление приложению после ограниченного пакета входящих кадров. */
} /* Завершает обслуживание транспорта и таймеров. */