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); /* Передаёт адресные команды и восьмибайтовые блоки общему автомату. */ } /* Завершает обработку допустимого формата кадра. */ } /* Возвращает управление приложению после ограниченного пакета входящих кадров. */ } /* Завершает обслуживание транспорта и таймеров. */