From 648457e587f773f3e4e46e1867bd93f063f680ce Mon Sep 17 00:00:00 2001 From: Andrey Kruchinkin Date: Mon, 31 Aug 2026 01:06:16 +0300 Subject: [PATCH] protocan-boot: support single-slot targets --- c/protocan-boot/README.md | 7 ++++- c/protocan-boot/include/pcan_boot.h | 4 +++ c/protocan-boot/src/pcan_boot.c | 23 +++++++++++---- c/protocan-boot/tests/test_pcan_boot.c | 39 ++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 6 deletions(-) diff --git a/c/protocan-boot/README.md b/c/protocan-boot/README.md index 3bf5c26..589f243 100644 --- a/c/protocan-boot/README.md +++ b/c/protocan-boot/README.md @@ -2,7 +2,7 @@ `protocan-boot` — переносимое C99-ядро адресной прошивки приборов по classic CAN 2.0B и 29-битному ProtoCAN ID. Оно реализует сессию обновления, два -логических слота по 512 КиБ, последовательную запись 8-байтовых блоков, +логических слота по 512 КиБ либо single-slot обновление, последовательную запись 8-байтовых блоков, CRC32, проверку совместимости, продолжение по номеру следующего блока и безопасный выбор неактивного слота. @@ -10,6 +10,11 @@ CRC32, проверку совместимости, продолжение по linker script, обработчиков прерываний, криптографии и перехода в приложение. Эти операции предоставляет проект через таблицу callbacks. +Для STM32G474VET6 из `candleLight_fw` задаются `slot_count = 1` и +`max_image_size = 480 * 1024`. В этом режиме запись идёт в единственную +область приложения, callback `set_pending_slot` не требуется, а аппаратный +rollback недоступен. Значение `slot_count = 0` сохраняет прежний A/B-режим. + ## Слои ```text diff --git a/c/protocan-boot/include/pcan_boot.h b/c/protocan-boot/include/pcan_boot.h index 7868ef0..f984296 100644 --- a/c/protocan-boot/include/pcan_boot.h +++ b/c/protocan-boot/include/pcan_boot.h @@ -79,6 +79,10 @@ typedef struct { uint8_t hardware_revision; uint32_t firmware_version; uint8_t active_slot; + /** 1 for an in-place/single-slot target, 2 for A/B. Zero keeps legacy A/B. */ + uint8_t slot_count; + /** Writable image capacity. Zero uses PCAN_BOOT_SLOT_SIZE. */ + uint32_t max_image_size; uint8_t ack_window; /**< 1 — ACK каждого блока, 16 — ACK каждых 16 блоков. */ } pcan_boot_config_t; diff --git a/c/protocan-boot/src/pcan_boot.c b/c/protocan-boot/src/pcan_boot.c index 9cefb52..fa4c918 100644 --- a/c/protocan-boot/src/pcan_boot.c +++ b/c/protocan-boot/src/pcan_boot.c @@ -115,17 +115,27 @@ void pcan_boot_abort(pcan_boot_t *boot) bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config, const pcan_boot_port_t *port, void *port_user) { + uint8_t slot_count; if ((boot == NULL) || (config == NULL) || (port == NULL) || (config->device_type > 7U) || (config->device_id > 15U) || (config->active_slot > 1U) || (port->send == NULL) || (port->erase_slot == NULL) - || (port->write_slot == NULL) || (port->set_pending_slot == NULL)) { + || (port->write_slot == NULL)) { + return false; + } + slot_count = config->slot_count == 0U ? 2U : config->slot_count; + if ((slot_count > 2U) || (config->active_slot >= slot_count) + || ((slot_count == 2U) && (port->set_pending_slot == NULL))) { return false; } (void)memset(boot, 0, sizeof(*boot)); boot->config = *config; boot->port = *port; boot->port_user = port_user; + boot->config.slot_count = slot_count; + if (boot->config.max_image_size == 0U) { + boot->config.max_image_size = PCAN_BOOT_SLOT_SIZE; + } if (boot->config.ack_window == 0U) { boot->config.ack_window = 1U; } @@ -140,7 +150,7 @@ static bool metadata_complete(pcan_boot_t *boot, uint8_t command) return send_status(boot, command, PCAN_BOOT_STATUS_OK); } if ((boot->manifest.image_size == 0U) - || (boot->manifest.image_size > PCAN_BOOT_SLOT_SIZE)) { + || (boot->manifest.image_size > boot->config.max_image_size)) { boot->state = PCAN_BOOT_STATE_FAILED; return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_SIZE); } @@ -155,7 +165,9 @@ static bool metadata_complete(pcan_boot_t *boot, uint8_t command) boot->state = PCAN_BOOT_STATE_FAILED; return send_status(boot, command, PCAN_BOOT_STATUS_SIGNATURE_ERROR); } - boot->target_slot = (uint8_t)(boot->config.active_slot ^ 1U); + boot->target_slot = boot->config.slot_count == 1U + ? boot->config.active_slot + : (uint8_t)(boot->config.active_slot ^ 1U); boot->state = PCAN_BOOT_STATE_READY_TO_ERASE; return send_status(boot, command, PCAN_BOOT_STATUS_OK); } @@ -239,8 +251,9 @@ static bool process_control(pcan_boot_t *boot, uint16_t body, if (boot->state != PCAN_BOOT_STATE_VERIFIED) { return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); } - if (!boot->port.set_pending_slot(boot->port_user, boot->target_slot, - &boot->manifest)) { + if ((boot->config.slot_count == 2U) + && !boot->port.set_pending_slot(boot->port_user, boot->target_slot, + &boot->manifest)) { return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR); } return send_status(boot, command, PCAN_BOOT_STATUS_OK); diff --git a/c/protocan-boot/tests/test_pcan_boot.c b/c/protocan-boot/tests/test_pcan_boot.c index fc0aba7..1ccd603 100644 --- a/c/protocan-boot/tests/test_pcan_boot.c +++ b/c/protocan-boot/tests/test_pcan_boot.c @@ -255,6 +255,44 @@ static void test_rejects_wrong_session_and_compatibility(void) assert(boot.state == PCAN_BOOT_STATE_FAILED); } +static void test_single_slot_update(void) +{ + static const uint8_t image[8] = { 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U }; + pcan_boot_t boot; + fake_t fake; + pcan_boot_config_t config; + pcan_boot_port_t port; + + (void)memset(&fake, 0, sizeof(fake)); + (void)memset(&config, 0, sizeof(config)); + (void)memset(&port, 0, sizeof(port)); + fake.pending_slot = PCAN_BOOT_SLOT_NONE; + fake.authorized = true; + fake.verified = true; + config.device_type = 3U; + config.device_id = 5U; + config.product_type = 0x1234U; + config.hardware_revision = 2U; + config.slot_count = 1U; + config.max_image_size = sizeof(fake.flash[0]); + config.ack_window = 16U; + port.send = fake_send; + port.erase_slot = fake_erase; + port.write_slot = fake_write; + port.authorize = fake_authorize; + port.verify_image = fake_verify; + port.reboot = fake_reboot; + assert(pcan_boot_init(&boot, &config, &port, &fake)); + begin_update(&boot, image, sizeof(image)); + assert(boot.target_slot == 0U); + assert(command(&boot, 7U, PCAN_BOOT_CMD_ERASE, NULL, 0U)); + assert(pcan_boot_process(&boot, request_id(PCAN_BOOT_MSG_DATA_A, 0U), image, 8U)); + assert(command(&boot, 7U, PCAN_BOOT_CMD_VERIFY, NULL, 0U)); + assert(command(&boot, 7U, PCAN_BOOT_CMD_COMMIT, NULL, 0U)); + assert(fake.pending_slot == PCAN_BOOT_SLOT_NONE); + assert(memcmp(fake.flash[0], image, sizeof(image)) == 0); +} + int main(void) { test_id_layout(); @@ -262,6 +300,7 @@ int main(void) test_complete_update(); test_rejects_wrong_address_and_sequence(); test_rejects_wrong_session_and_compatibility(); + test_single_slot_update(); puts("pcan_boot tests: OK"); return 0; }