diff --git a/README.md b/README.md index 55dbe63..2ce178c 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,9 @@ templates/ | [`c/eeprom-ft24c256`](c/eeprom-ft24c256) | EEPROM 24Cxx по I²C с нарезкой записи по страницам | `stdint.h` | две I²C-транзакции, задержка | | [`c/can-sensor`](c/can-sensor) | однокадровые SETCAN SETTINGS для 64-битных ROM | ядро: `stdint.h`; порт F1: CMSIS | callbacks либо готовый bxCAN STM32F1 | | [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порты STM32F103, STM32G431 и STM32G474 в комплекте** | -| [`c/set-protocol`](c/set-protocol) | единое ядро SETProtocol: SET v2, совместимые ProtoCAN/GUI v1, GAS, телеметрия, firmware flow и стабильный host ABI | C99 | COM/SLCAN/SocketCAN/USB/Ethernet или callbacks — **Windows, Android и STM32F4-порты в комплекте** | +| [`c/set-protocol`](c/set-protocol) | единое ядро SETProtocol: SET v2, совместимые ProtoCAN/GUI v1, GAS, телеметрия, firmware flow и стабильный host ABI | C99 | COM/SLCAN/SocketCAN/USB/Ethernet или callbacks — **Windows, Android, STM32F4 и TMS320F2812-порты в комплекте** | | [`c/set-protocol/ports/stm32-bxcan`](c/set-protocol/ports/stm32-bxcan) | порт прикладного ProtoCAN для STM32, бывший SETCAN; сохранён API `PROTOCAN_*` | STM32 HAL CAN/RTC/TIM + общее ядро `pcan_id` | classic bxCAN; настройки платы предоставляет прошивка | +| [`c/set-protocol/ports/tms320f2812`](c/set-protocol/ports/tms320f2812) | SETProtocol v2 firmware service по segmented classic CAN | `set_protocol`, `set_can`, `set_firmware` | CAN TX, Flash erase/write/read, optional signature policy и reboot | | [`c/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | CAN TX, erase/write Flash, boot metadata, проверка образа и reboot | | [`c/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** | | [`c/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** | diff --git a/c/set-protocol/CMakeLists.txt b/c/set-protocol/CMakeLists.txt index f757905..f35443c 100644 --- a/c/set-protocol/CMakeLists.txt +++ b/c/set-protocol/CMakeLists.txt @@ -107,6 +107,13 @@ if(SETP_BUILD_TESTS) add_executable(test_tms2812 tests/test_tms2812.c) target_link_libraries(test_tms2812 PRIVATE setprotocol_static) add_test(NAME shared_tms2812 COMMAND test_tms2812) + add_executable(test_tms2812_boot + tests/test_tms2812_boot.c + ports/tms320f2812/setp_tms2812_boot.c + ) + target_include_directories(test_tms2812_boot PRIVATE ports/tms320f2812) + target_link_libraries(test_tms2812_boot PRIVATE setprotocol_static) + add_test(NAME setp_tms2812_boot COMMAND test_tms2812_boot) add_executable(test_trends tests/test_trends.c) target_link_libraries(test_trends PRIVATE setprotocol_static) add_test(NAME shared_trends COMMAND test_trends) diff --git a/c/set-protocol/PORTING.md b/c/set-protocol/PORTING.md index 6373c96..8c1c206 100644 --- a/c/set-protocol/PORTING.md +++ b/c/set-protocol/PORTING.md @@ -43,6 +43,11 @@ multicast запрещены WRITE, FW_BEGIN и FW_ACTIVATE. В проект устройства добавляются `ports/stm32-bxcan/protocan.c` и `src/pcan_id.c`, а в include paths — `ports/stm32-bxcan` и `include`. +Для TMS320F2812 доступен SETProtocol v2 firmware service +[`ports/tms320f2812`](ports/tms320f2812/README.md). Он использует общую +сегментацию `set_can`, собирается без TI headers и получает eCAN, Flash и reset +через callbacks проекта платы. + Не вызывайте прикладной router из ISR. ISR принимает CAN-кадры в очередь, сегментация собирается в главном цикле, и только полный SETP-кадр передаётся parser/router. Сборка должна иметь timeout, контроль номера сегмента и одного diff --git a/c/set-protocol/ports/tms320f2812/README.md b/c/set-protocol/ports/tms320f2812/README.md new file mode 100644 index 0000000..6794ad0 --- /dev/null +++ b/c/set-protocol/ports/tms320f2812/README.md @@ -0,0 +1,23 @@ +# SETProtocol v2 firmware port for TMS320F2812 + +`setp_tms2812_boot.c` implements the shared SETP v2 CAN firmware service for +F2812 projects. It owns CAN reassembly, request/response framing, firmware state, +idempotent blocks, CRC32 and SHA-256 verification. It does not include TI or +board headers. + +The target application provides callbacks for classic-CAN transmission, Flash +erase/program/read, optional signature authorization, and reset. Receive ISR +code must only copy frames into a queue; call `setp_tms2812_boot_process()` from +task context. + +Add these sources to a CCS project: + +- `c/set-protocol/src/set_protocol.c` +- `c/set-protocol/src/set_can.c` +- `c/set-protocol/src/set_firmware.c` +- `c/set-protocol/ports/tms320f2812/setp_tms2812_boot.c` + +Add `c/set-protocol/include` and this directory to include paths. The port uses +extended 29-bit SETP CAN identifiers and supports one firmware slot. A single +slot has no power-loss rollback; production hardware should provide staging or +A/B storage. diff --git a/c/set-protocol/ports/tms320f2812/setp_tms2812_boot.c b/c/set-protocol/ports/tms320f2812/setp_tms2812_boot.c new file mode 100644 index 0000000..fe89157 --- /dev/null +++ b/c/set-protocol/ports/tms320f2812/setp_tms2812_boot.c @@ -0,0 +1,407 @@ +#include "setp_tms2812_boot.h" + +#include + +#define SHA256_BLOCK_SIZE 64U + +typedef struct { + uint32_t state[8]; + uint64_t bit_count; + uint8_t block[SHA256_BLOCK_SIZE]; + uint16_t block_length; +} sha256_ctx_t; + +static uint32_t rotr32(uint32_t value, uint8_t bits) +{ + return (value >> bits) | (value << (32U - bits)); +} + +static void sha256_transform(sha256_ctx_t *ctx, const uint8_t *block) +{ + static const uint32_t k[64] = { + 0x428A2F98UL, 0x71374491UL, 0xB5C0FBCFUL, 0xE9B5DBA5UL, + 0x3956C25BUL, 0x59F111F1UL, 0x923F82A4UL, 0xAB1C5ED5UL, + 0xD807AA98UL, 0x12835B01UL, 0x243185BEUL, 0x550C7DC3UL, + 0x72BE5D74UL, 0x80DEB1FEUL, 0x9BDC06A7UL, 0xC19BF174UL, + 0xE49B69C1UL, 0xEFBE4786UL, 0x0FC19DC6UL, 0x240CA1CCUL, + 0x2DE92C6FUL, 0x4A7484AAUL, 0x5CB0A9DCUL, 0x76F988DAUL, + 0x983E5152UL, 0xA831C66DUL, 0xB00327C8UL, 0xBF597FC7UL, + 0xC6E00BF3UL, 0xD5A79147UL, 0x06CA6351UL, 0x14292967UL, + 0x27B70A85UL, 0x2E1B2138UL, 0x4D2C6DFCUL, 0x53380D13UL, + 0x650A7354UL, 0x766A0ABBUL, 0x81C2C92EUL, 0x92722C85UL, + 0xA2BFE8A1UL, 0xA81A664BUL, 0xC24B8B70UL, 0xC76C51A3UL, + 0xD192E819UL, 0xD6990624UL, 0xF40E3585UL, 0x106AA070UL, + 0x19A4C116UL, 0x1E376C08UL, 0x2748774CUL, 0x34B0BCB5UL, + 0x391C0CB3UL, 0x4ED8AA4AUL, 0x5B9CCA4FUL, 0x682E6FF3UL, + 0x748F82EEUL, 0x78A5636FUL, 0x84C87814UL, 0x8CC70208UL, + 0x90BEFFFAUL, 0xA4506CEBUL, 0xBEF9A3F7UL, 0xC67178F2UL + }; + uint32_t w[64]; + uint32_t a, b, c, d, e, f, g, h, s0, s1, ch, maj, temp1, temp2; + uint16_t i; + for (i = 0U; i < 16U; i++) { + uint16_t p = (uint16_t)(i * 4U); + w[i] = ((uint32_t)block[p] << 24U) + | ((uint32_t)block[p + 1U] << 16U) + | ((uint32_t)block[p + 2U] << 8U) + | (uint32_t)block[p + 3U]; + } + for (i = 16U; i < 64U; i++) { + s0 = rotr32(w[i - 15U], 7U) ^ rotr32(w[i - 15U], 18U) + ^ (w[i - 15U] >> 3U); + s1 = rotr32(w[i - 2U], 17U) ^ rotr32(w[i - 2U], 19U) + ^ (w[i - 2U] >> 10U); + w[i] = w[i - 16U] + s0 + w[i - 7U] + s1; + } + a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2]; d = ctx->state[3]; + e = ctx->state[4]; f = ctx->state[5]; g = ctx->state[6]; h = ctx->state[7]; + for (i = 0U; i < 64U; i++) { + s1 = rotr32(e, 6U) ^ rotr32(e, 11U) ^ rotr32(e, 25U); + ch = (e & f) ^ ((~e) & g); + temp1 = h + s1 + ch + k[i] + w[i]; + s0 = rotr32(a, 2U) ^ rotr32(a, 13U) ^ rotr32(a, 22U); + maj = (a & b) ^ (a & c) ^ (b & c); + temp2 = s0 + maj; + h = g; g = f; f = e; e = d + temp1; + d = c; c = b; b = a; a = temp1 + temp2; + } + ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c; ctx->state[3] += d; + ctx->state[4] += e; ctx->state[5] += f; ctx->state[6] += g; ctx->state[7] += h; +} + +static void sha256_init(sha256_ctx_t *ctx) +{ + static const uint32_t initial[8] = { + 0x6A09E667UL, 0xBB67AE85UL, 0x3C6EF372UL, 0xA54FF53AUL, + 0x510E527FUL, 0x9B05688CUL, 0x1F83D9ABUL, 0x5BE0CD19UL + }; + (void)memcpy(ctx->state, initial, sizeof(initial)); + ctx->bit_count = 0U; + ctx->block_length = 0U; +} + +static void sha256_update(sha256_ctx_t *ctx, const uint8_t *data, uint16_t length) +{ + uint16_t i; + for (i = 0U; i < length; i++) { + ctx->block[ctx->block_length++] = data[i]; + ctx->bit_count += 8U; + if (ctx->block_length == SHA256_BLOCK_SIZE) { + sha256_transform(ctx, ctx->block); + ctx->block_length = 0U; + } + } +} + +static void sha256_finish(sha256_ctx_t *ctx, uint8_t digest[SETP_SHA256_SIZE]) +{ + uint16_t i; + uint64_t bits = ctx->bit_count; + ctx->block[ctx->block_length++] = 0x80U; + if (ctx->block_length > 56U) { + while (ctx->block_length < SHA256_BLOCK_SIZE) ctx->block[ctx->block_length++] = 0U; + sha256_transform(ctx, ctx->block); + ctx->block_length = 0U; + } + while (ctx->block_length < 56U) ctx->block[ctx->block_length++] = 0U; + for (i = 0U; i < 8U; i++) { + ctx->block[63U - i] = (uint8_t)(bits & 0xFFU); + bits >>= 8U; + } + sha256_transform(ctx, ctx->block); + for (i = 0U; i < 8U; i++) { + digest[i * 4U] = (uint8_t)(ctx->state[i] >> 24U); + digest[i * 4U + 1U] = (uint8_t)(ctx->state[i] >> 16U); + digest[i * 4U + 2U] = (uint8_t)(ctx->state[i] >> 8U); + digest[i * 4U + 3U] = (uint8_t)ctx->state[i]; + } +} + +static void boot_status(const setp_tms2812_boot_t *boot, setp_fw_status_t *status) +{ + status->state = boot->state; + status->active_slot = boot->config.active_slot; + status->max_block_size = boot->config.max_block_size; + status->next_offset = boot->next_offset; + status->image_size = boot->manifest.image_size; + status->last_error = boot->last_error; + status->flags = 0U; +} + +static bool boot_send_response(setp_tms2812_boot_t *boot, + const setp_frame_t *request, + const setp_can_id_t *request_id, + uint16_t status, + const uint8_t *body, uint16_t body_length) +{ + setp_frame_t response; + setp_can_id_t response_id; + size_t packet_length; + if ((uint32_t)body_length + 2U > sizeof(boot->response_payload)) return false; + setp_put_u16(boot->response_payload, status); + if (body_length != 0U) (void)memcpy(&boot->response_payload[2], body, body_length); + response.flags = SETP_FLAG_RESPONSE; + if (status != SETP_STATUS_OK) response.flags |= SETP_FLAG_ERROR; + if ((request->flags & SETP_FLAG_PRIORITY) != 0U) response.flags |= SETP_FLAG_PRIORITY; + response.message_type = request->message_type; + response.source = boot->config.node_id; + response.destination = request->source; + response.sequence = request->sequence; + response.payload_length = (uint16_t)(body_length + 2U); + response.payload = boot->response_payload; + packet_length = setp_frame_encode(&response, boot->response_packet, + sizeof(boot->response_packet)); + if (packet_length == 0U) return false; + response_id.destination = request_id->source; + response_id.source = boot->config.node_id; + response_id.priority = (response.flags & SETP_FLAG_PRIORITY) != 0U ? 1U : 0U; + response_id.channel = request_id->channel; + return setp_can_segment(boot->response_packet, (uint16_t)packet_length, + setp_can_id_pack(&response_id), boot->port.send_can, + boot->port_user); +} + +static uint16_t boot_verify_image(setp_tms2812_boot_t *boot) +{ + uint8_t data[SETP_TMS2812_MAX_BLOCK_SIZE]; + uint8_t digest[SETP_SHA256_SIZE]; + sha256_ctx_t sha; + uint32_t crc = 0xFFFFFFFFUL; + uint32_t offset = 0U; + uint16_t i; + sha256_init(&sha); + while (offset < boot->manifest.image_size) { + uint32_t remaining = boot->manifest.image_size - offset; + uint16_t length = remaining > sizeof(data) ? (uint16_t)sizeof(data) : (uint16_t)remaining; + if (!boot->port.read_image(boot->port_user, offset, data, length)) { + return SETP_STATUS_INTERNAL; + } + sha256_update(&sha, data, length); + for (i = 0U; i < length; i++) { + uint8_t bit; + crc ^= data[i]; + for (bit = 0U; bit < 8U; bit++) + crc = (crc >> 1U) ^ (((crc & 1U) != 0U) ? 0xEDB88320UL : 0U); + } + offset += length; + } + sha256_finish(&sha, digest); + crc ^= 0xFFFFFFFFUL; + if ((crc != boot->manifest.image_crc32) + || (memcmp(digest, boot->manifest.sha256, SETP_SHA256_SIZE) != 0)) { + return SETP_STATUS_VERIFY_FAILED; + } + return SETP_STATUS_OK; +} + +static uint16_t boot_fw_begin(setp_tms2812_boot_t *boot, const setp_frame_t *request) +{ + setp_fw_begin_t value; + bool same_manifest; + if (!setp_fw_begin_decode(request->payload, request->payload_length, &value)) + return SETP_STATUS_INVALID_LENGTH; + if ((value.image_size == 0U) || (value.image_size > boot->config.max_image_size) + || (value.slot != boot->config.active_slot) + || ((value.base_address != 0U) + && (value.base_address != boot->config.app_base_address)) + || (value.block_size == 0U) + || (value.block_size > boot->config.max_block_size)) + return SETP_STATUS_INVALID_ARGUMENT; + if (((value.flags & SETP_FW_FLAG_SIGNED) != 0U) || boot->config.require_signature) { + if ((boot->port.authorize == NULL) + || !boot->port.authorize(boot->port_user, &value)) + return SETP_STATUS_AUTH_FAILED; + } + same_manifest = boot->state == SETP_FW_RECEIVING + && boot->manifest.image_size == value.image_size + && boot->manifest.image_crc32 == value.image_crc32 + && boot->manifest.image_version == value.image_version + && memcmp(boot->manifest.sha256, value.sha256, SETP_SHA256_SIZE) == 0; + if (same_manifest && ((value.flags & SETP_FW_FLAG_RESUME) != 0U)) return SETP_STATUS_OK; + if (!boot->port.erase_image(boot->port_user, value.image_size)) + return SETP_STATUS_INTERNAL; + boot->manifest = value; + boot->manifest.signature = NULL; + boot->manifest.signature_length = 0U; + boot->next_offset = 0U; + boot->state = SETP_FW_RECEIVING; + return SETP_STATUS_OK; +} + +static uint16_t boot_fw_data(setp_tms2812_boot_t *boot, const setp_frame_t *request) +{ + setp_fw_data_t value; + uint8_t current[SETP_TMS2812_MAX_BLOCK_SIZE]; + if (boot->state != SETP_FW_RECEIVING) return SETP_STATUS_WRONG_STATE; + if (!setp_fw_data_decode(request->payload, request->payload_length, &value)) + return SETP_STATUS_CRC; + if ((value.data_length > boot->manifest.block_size) + || (value.data_length > boot->config.max_block_size) + || (value.offset > boot->manifest.image_size) + || ((uint32_t)value.data_length > boot->manifest.image_size - value.offset)) + return SETP_STATUS_INVALID_ARGUMENT; + if (value.offset < boot->next_offset) { + if ((value.offset + value.data_length > boot->next_offset) + || !boot->port.read_image(boot->port_user, value.offset, + current, value.data_length) + || memcmp(current, value.data, value.data_length) != 0) + return SETP_STATUS_SEQUENCE; + return SETP_STATUS_OK; + } + if (value.offset != boot->next_offset) return SETP_STATUS_SEQUENCE; + if (!boot->port.write_image(boot->port_user, value.offset, + value.data, value.data_length)) + return SETP_STATUS_INTERNAL; + boot->next_offset += value.data_length; + return SETP_STATUS_OK; +} + +static uint16_t boot_fw_end(setp_tms2812_boot_t *boot, const setp_frame_t *request) +{ + setp_fw_end_t value; + uint16_t status; + if (boot->state != SETP_FW_RECEIVING) return SETP_STATUS_WRONG_STATE; + if (!setp_fw_end_decode(request->payload, request->payload_length, &value)) + return SETP_STATUS_INVALID_LENGTH; + if ((boot->next_offset != boot->manifest.image_size) + || (value.image_size != boot->manifest.image_size) + || (value.image_crc32 != boot->manifest.image_crc32) + || (memcmp(value.sha256, boot->manifest.sha256, SETP_SHA256_SIZE) != 0)) + return SETP_STATUS_VERIFY_FAILED; + boot->state = SETP_FW_VERIFYING; + status = boot_verify_image(boot); + boot->state = status == SETP_STATUS_OK ? SETP_FW_READY : SETP_FW_FAILED; + return status; +} + +static bool boot_dispatch(setp_tms2812_boot_t *boot, const setp_frame_t *request, + const setp_can_id_t *request_id, uint32_t now_ms) +{ + uint8_t body[SETP_TMS2812_RESPONSE_PAYLOAD_SIZE - 2U]; + uint16_t body_length = 0U; + uint16_t status = SETP_STATUS_OK; + uint8_t reboot = 0U; + setp_fw_status_t fw_status; + if ((request->flags & (SETP_FLAG_RESPONSE | SETP_FLAG_EVENT)) != 0U) return false; + if ((request->source > 0xFFU) || (request->destination != boot->config.node_id)) return false; + switch (request->message_type) { + case SETP_MSG_PING: + if (request->payload_length != 0U) status = SETP_STATUS_INVALID_LENGTH; + else { setp_put_u32(body, now_ms); body_length = 4U; } + break; + case SETP_MSG_DEVICE_INFO: + if (request->payload_length != 0U) status = SETP_STATUS_INVALID_LENGTH; + else { + setp_device_info_t info; + info.schema_version = SETP_DEVICE_INFO_SCHEMA_VERSION; + info.device_class = boot->config.device_class; + info.hardware_version = boot->config.hardware_version; + info.firmware_version = boot->config.firmware_version; + info.dictionary_version = boot->config.dictionary_version; + info.serial_number = boot->config.serial_number; + info.model_length = boot->config.model_length; + info.model = boot->config.model; + body_length = (uint16_t)setp_device_info_encode(&info, body, sizeof(body)); + if (body_length == 0U) status = SETP_STATUS_INTERNAL; + } + break; + case SETP_MSG_CAPABILITIES: + if (request->payload_length != 0U) status = SETP_STATUS_INVALID_LENGTH; + else { + setp_capabilities_t caps; + caps.schema_version = SETP_CAPABILITIES_SCHEMA_VERSION; + caps.max_payload = (uint16_t)(SETP_FW_DATA_HEADER_SIZE + boot->config.max_block_size); + caps.interface_mask = SETP_IFACE_MASK(SETP_IFACE_CAN); + caps.feature_flags = SETP_FEATURE_FIRMWARE; + caps.max_read_items = 0U; caps.max_write_items = 0U; + caps.max_subscriptions = 0U; caps.max_publish_items = 0U; + body_length = (uint16_t)setp_capabilities_encode(&caps, body, sizeof(body)); + if (body_length == 0U) status = SETP_STATUS_INTERNAL; + } + break; + case SETP_MSG_FW_BEGIN: + status = boot_fw_begin(boot, request); + setp_put_u32(body, boot->next_offset); body_length = 4U; + break; + case SETP_MSG_FW_DATA: + status = boot_fw_data(boot, request); + setp_put_u32(body, boot->next_offset); body_length = 4U; + break; + case SETP_MSG_FW_END: + status = boot_fw_end(boot, request); + setp_put_u32(body, boot->next_offset); body_length = 4U; + break; + case SETP_MSG_FW_ABORT: + if (request->payload_length != 0U) status = SETP_STATUS_INVALID_LENGTH; + else setp_tms2812_boot_abort(boot); + setp_put_u32(body, boot->next_offset); body_length = 4U; + break; + case SETP_MSG_FW_STATUS: + if (request->payload_length != 0U) status = SETP_STATUS_INVALID_LENGTH; + else { + boot_status(boot, &fw_status); + body_length = (uint16_t)setp_fw_status_encode(&fw_status, body, sizeof(body)); + } + break; + case SETP_MSG_FW_ACTIVATE: + if (request->payload_length != 0U) status = SETP_STATUS_INVALID_LENGTH; + else if (boot->state != SETP_FW_READY) status = SETP_STATUS_WRONG_STATE; + else { boot->state = SETP_FW_ACTIVE; reboot = 1U; } + setp_put_u32(body, boot->next_offset); body_length = 4U; + break; + default: + status = SETP_STATUS_UNSUPPORTED; + break; + } + boot->last_error = status == SETP_STATUS_OK ? 0U : status; + if (!boot_send_response(boot, request, request_id, status, body, body_length)) return false; + if (reboot != 0U) boot->port.reboot(boot->port_user); + return true; +} + +bool setp_tms2812_boot_init(setp_tms2812_boot_t *boot, + const setp_tms2812_boot_config_t *config, + const setp_tms2812_boot_port_t *port, + void *port_user) +{ + if ((boot == NULL) || (config == NULL) || (port == NULL) + || (config->model == NULL) || (config->model_length > SETP_DEVICE_MODEL_MAX) + || (config->max_image_size == 0U) || (config->max_block_size == 0U) + || (config->max_block_size > SETP_TMS2812_MAX_BLOCK_SIZE) + || (port->send_can == NULL) || (port->erase_image == NULL) + || (port->write_image == NULL) || (port->read_image == NULL) + || (port->reboot == NULL)) return false; + (void)memset(boot, 0, sizeof(*boot)); + boot->config = *config; + boot->port = *port; + boot->port_user = port_user; + boot->state = SETP_FW_IDLE; + setp_can_rx_init(&boot->rx); + return true; +} + +void setp_tms2812_boot_abort(setp_tms2812_boot_t *boot) +{ + if (boot == NULL) return; + boot->state = SETP_FW_IDLE; + boot->next_offset = 0U; + boot->last_error = 0U; + (void)memset(&boot->manifest, 0, sizeof(boot->manifest)); +} + +bool setp_tms2812_boot_process(setp_tms2812_boot_t *boot, + const setp_can_frame_t *frame, + uint32_t now_ms) +{ + setp_can_packet_t packet; + setp_can_rx_result_t result; + setp_can_id_t can_id; + setp_frame_t request; + if ((boot == NULL) || (frame == NULL)) return false; + result = setp_can_rx_feed(&boot->rx, frame, now_ms, &packet); + if (result != SETP_CAN_RX_COMPLETE) return result == SETP_CAN_RX_NONE; + if (!setp_can_id_unpack(packet.can_id, &can_id) + || (can_id.destination != boot->config.node_id) + || !setp_frame_decode_datagram(packet.data, packet.length, &request)) return false; + return boot_dispatch(boot, &request, &can_id, now_ms); +} diff --git a/c/set-protocol/ports/tms320f2812/setp_tms2812_boot.h b/c/set-protocol/ports/tms320f2812/setp_tms2812_boot.h new file mode 100644 index 0000000..4c2c0c0 --- /dev/null +++ b/c/set-protocol/ports/tms320f2812/setp_tms2812_boot.h @@ -0,0 +1,74 @@ +#ifndef SETP_TMS2812_BOOT_H +#define SETP_TMS2812_BOOT_H + +#include "set_can.h" +#include "set_firmware.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef SETP_TMS2812_MAX_BLOCK_SIZE +#define SETP_TMS2812_MAX_BLOCK_SIZE 64U +#endif + +#define SETP_TMS2812_RESPONSE_PAYLOAD_SIZE \ + (2U + SETP_DEVICE_INFO_FIXED_SIZE + SETP_DEVICE_MODEL_MAX) + +typedef struct { + uint8_t node_id; + uint16_t device_class; + uint32_t hardware_version; + uint32_t firmware_version; + uint32_t dictionary_version; + uint64_t serial_number; + const uint8_t *model; + uint8_t model_length; + uint32_t app_base_address; + uint32_t max_image_size; + uint16_t max_block_size; + uint8_t active_slot; + uint8_t require_signature; +} setp_tms2812_boot_config_t; + +typedef struct { + setp_can_send_fn send_can; + bool (*erase_image)(void *user, uint32_t image_size); + bool (*write_image)(void *user, uint32_t offset, + const uint8_t *data, uint16_t length); + bool (*read_image)(void *user, uint32_t offset, + uint8_t *data, uint16_t length); + bool (*authorize)(void *user, const setp_fw_begin_t *manifest); + void (*reboot)(void *user); +} setp_tms2812_boot_port_t; + +typedef struct { + setp_tms2812_boot_config_t config; + setp_tms2812_boot_port_t port; + void *port_user; + setp_can_rx_t rx; + setp_fw_begin_t manifest; + uint32_t next_offset; + uint16_t last_error; + uint8_t state; + uint8_t response_payload[SETP_TMS2812_RESPONSE_PAYLOAD_SIZE]; + uint8_t response_packet[SETP_FRAME_MAX]; +} setp_tms2812_boot_t; + +bool setp_tms2812_boot_init(setp_tms2812_boot_t *boot, + const setp_tms2812_boot_config_t *config, + const setp_tms2812_boot_port_t *port, + void *port_user); + +/** Process one classic-CAN frame in task context, never from an ISR. */ +bool setp_tms2812_boot_process(setp_tms2812_boot_t *boot, + const setp_can_frame_t *frame, + uint32_t now_ms); + +void setp_tms2812_boot_abort(setp_tms2812_boot_t *boot); + +#ifdef __cplusplus +} +#endif + +#endif /* SETP_TMS2812_BOOT_H */ diff --git a/c/set-protocol/tests/test_tms2812_boot.c b/c/set-protocol/tests/test_tms2812_boot.c new file mode 100644 index 0000000..f1065b0 --- /dev/null +++ b/c/set-protocol/tests/test_tms2812_boot.c @@ -0,0 +1,225 @@ +#include +#include +#include + +#include "setp_tms2812_boot.h" + +#define NODE_ID 13U +#define IMAGE_SIZE 100U + +typedef struct { + uint8_t flash[512]; + setp_can_rx_t response_rx; + setp_can_packet_t response_packet; + uint32_t now_ms; + unsigned int response_complete; + unsigned int erased; + unsigned int rebooted; +} fake_t; + +typedef struct { + setp_tms2812_boot_t *boot; + fake_t *fake; +} request_context_t; + +static bool fake_send_response(const setp_can_frame_t *frame, void *user) +{ + fake_t *fake = (fake_t *)user; + setp_can_rx_result_t result = setp_can_rx_feed( + &fake->response_rx, frame, fake->now_ms++, &fake->response_packet); + if (result == SETP_CAN_RX_COMPLETE) fake->response_complete++; + return result == SETP_CAN_RX_NONE || result == SETP_CAN_RX_COMPLETE; +} + +static bool fake_erase(void *user, uint32_t image_size) +{ + fake_t *fake = (fake_t *)user; + if (image_size > sizeof(fake->flash)) return false; + (void)memset(fake->flash, 0xFF, sizeof(fake->flash)); + fake->erased++; + return true; +} + +static bool fake_write(void *user, uint32_t offset, + const uint8_t *data, uint16_t length) +{ + fake_t *fake = (fake_t *)user; + if (offset + length > sizeof(fake->flash)) return false; + (void)memcpy(&fake->flash[offset], data, length); + return true; +} + +static bool fake_read(void *user, uint32_t offset, + uint8_t *data, uint16_t length) +{ + fake_t *fake = (fake_t *)user; + if (offset + length > sizeof(fake->flash)) return false; + (void)memcpy(data, &fake->flash[offset], length); + return true; +} + +static void fake_reboot(void *user) +{ + ((fake_t *)user)->rebooted++; +} + +static bool send_request_frame(const setp_can_frame_t *frame, void *user) +{ + request_context_t *context = (request_context_t *)user; + return setp_tms2812_boot_process(context->boot, frame, + context->fake->now_ms++); +} + +static setp_frame_t transact(setp_tms2812_boot_t *boot, fake_t *fake, + uint16_t sequence, uint16_t type, + const uint8_t *payload, uint16_t payload_length) +{ + uint8_t packet[SETP_FRAME_MAX]; + setp_frame_t request; + setp_frame_t response; + setp_can_id_t id; + request_context_t context; + size_t packet_length; + fake->response_complete = 0U; + setp_can_rx_init(&fake->response_rx); + request.flags = SETP_FLAG_ACK_REQUIRED | SETP_FLAG_PRIORITY; + request.message_type = type; + request.source = 0U; + request.destination = NODE_ID; + request.sequence = sequence; + request.payload_length = payload_length; + request.payload = payload; + packet_length = setp_frame_encode(&request, packet, sizeof(packet)); + assert(packet_length != 0U); + id.destination = NODE_ID; + id.source = 0U; + id.priority = 1U; + id.channel = 1U; + context.boot = boot; + context.fake = fake; + assert(setp_can_segment(packet, (uint16_t)packet_length, + setp_can_id_pack(&id), send_request_frame, &context)); + assert(fake->response_complete == 1U); + assert(setp_frame_decode_datagram(fake->response_packet.data, + fake->response_packet.length, &response)); + assert(response.flags & SETP_FLAG_RESPONSE); + assert(response.message_type == type); + assert(response.sequence == sequence); + assert(response.source == NODE_ID); + assert(response.destination == 0U); + return response; +} + +static void assert_ok(const setp_frame_t *response) +{ + assert(response->payload_length >= 2U); + assert(setp_get_u16(response->payload) == SETP_STATUS_OK); + assert((response->flags & SETP_FLAG_ERROR) == 0U); +} + +int main(void) +{ + static const uint8_t model[] = "BALZAM-166"; + static const uint8_t expected_sha[SETP_SHA256_SIZE] = { + 0xBC, 0xE0, 0xAF, 0xF1, 0x9C, 0xF5, 0xAA, 0x6A, + 0x74, 0x69, 0xA3, 0x0D, 0x61, 0xD0, 0x4E, 0x43, + 0x76, 0xE4, 0xBB, 0xF6, 0x38, 0x10, 0x52, 0xEE, + 0x9E, 0x7F, 0x33, 0x92, 0x5C, 0x95, 0x4D, 0x52 + }; + uint8_t image[IMAGE_SIZE]; + uint8_t payload[SETP_MAX_PAYLOAD]; + setp_tms2812_boot_t boot; + setp_tms2812_boot_config_t config; + setp_tms2812_boot_port_t port; + setp_fw_begin_t begin; + setp_fw_end_t end; + setp_fw_status_t status; + setp_frame_t response; + size_t length; + uint32_t offset; + uint16_t sequence = 1U; + fake_t fake; + unsigned int i; + + (void)memset(&fake, 0, sizeof(fake)); + (void)memset(&config, 0, sizeof(config)); + (void)memset(&port, 0, sizeof(port)); + for (i = 0U; i < IMAGE_SIZE; i++) image[i] = (uint8_t)i; + config.node_id = NODE_ID; + config.device_class = 0x0166U; + config.hardware_version = 1U; + config.firmware_version = 2U; + config.model = model; + config.model_length = (uint8_t)(sizeof(model) - 1U); + config.app_base_address = 0x00100000UL; + config.max_image_size = sizeof(fake.flash); + config.max_block_size = 32U; + config.active_slot = 0U; + port.send_can = fake_send_response; + port.erase_image = fake_erase; + port.write_image = fake_write; + port.read_image = fake_read; + port.reboot = fake_reboot; + assert(setp_tms2812_boot_init(&boot, &config, &port, &fake)); + + response = transact(&boot, &fake, sequence++, SETP_MSG_PING, NULL, 0U); + assert_ok(&response); + assert(response.payload_length == 6U); + + (void)memset(&begin, 0, sizeof(begin)); + begin.image_size = IMAGE_SIZE; + begin.image_crc32 = setp_crc32(image, sizeof(image)); + begin.image_version = 0x01020304UL; + begin.base_address = config.app_base_address; + begin.slot = 0U; + begin.flags = SETP_FW_FLAG_RESUME | SETP_FW_FLAG_ERASE_SLOT; + begin.block_size = 32U; + (void)memcpy(begin.sha256, expected_sha, sizeof(expected_sha)); + length = setp_fw_begin_encode(&begin, payload, sizeof(payload)); + assert(length != 0U); + response = transact(&boot, &fake, sequence++, SETP_MSG_FW_BEGIN, + payload, (uint16_t)length); + assert_ok(&response); + assert(fake.erased == 1U); + + for (offset = 0U; offset < IMAGE_SIZE;) { + uint16_t chunk = (uint16_t)(IMAGE_SIZE - offset); + if (chunk > begin.block_size) chunk = begin.block_size; + length = setp_fw_data_encode(offset, 0U, &image[offset], chunk, + payload, sizeof(payload)); + response = transact(&boot, &fake, sequence++, SETP_MSG_FW_DATA, + payload, (uint16_t)length); + assert_ok(&response); + assert(setp_get_u32(&response.payload[2]) == offset + chunk); + if (offset == 0U) { + response = transact(&boot, &fake, sequence++, SETP_MSG_FW_DATA, + payload, (uint16_t)length); + assert_ok(&response); + assert(setp_get_u32(&response.payload[2]) == chunk); + } + offset += chunk; + } + assert(memcmp(fake.flash, image, sizeof(image)) == 0); + + end.image_size = begin.image_size; + end.image_crc32 = begin.image_crc32; + (void)memcpy(end.sha256, expected_sha, sizeof(expected_sha)); + length = setp_fw_end_encode(&end, payload, sizeof(payload)); + response = transact(&boot, &fake, sequence++, SETP_MSG_FW_END, + payload, (uint16_t)length); + assert_ok(&response); + assert(boot.state == SETP_FW_READY); + + response = transact(&boot, &fake, sequence++, SETP_MSG_FW_STATUS, NULL, 0U); + assert_ok(&response); + assert(setp_fw_status_decode(&response.payload[2], + (uint16_t)(response.payload_length - 2U), &status)); + assert(status.state == SETP_FW_READY); + assert(status.next_offset == IMAGE_SIZE); + + response = transact(&boot, &fake, sequence++, SETP_MSG_FW_ACTIVATE, NULL, 0U); + assert_ok(&response); + assert(fake.rebooted == 1U); + puts("SETProtocol v2 TMS320F2812 firmware port tests passed"); + return 0; +} diff --git a/python/setprotocol/__init__.py b/python/setprotocol/__init__.py index 8791acc..fc0d384 100644 --- a/python/setprotocol/__init__.py +++ b/python/setprotocol/__init__.py @@ -1,3 +1,4 @@ """Cross-platform Python facade for the canonical SETProtocol core.""" from .core import * # noqa: F401,F403 +from .can import CanAddress, CanFrame, CanReassembler, segment diff --git a/python/setprotocol/can.py b/python/setprotocol/can.py new file mode 100644 index 0000000..30c0e6d --- /dev/null +++ b/python/setprotocol/can.py @@ -0,0 +1,173 @@ +"""Classic-CAN segmentation for canonical SETProtocol v2 frames.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from .core import ( + CRC_SIZE, + FRAME_MAX, + HEADER_SIZE, + SOF, + FrameFlag, + SetProtocolError, +) + +CAN_ID_MASK = 0x1FFFFFFF +CAN_ID_PREFIX = 0x12 +CAN_ID_PREFIX_MASK = 0x1F000000 +PCI_FIRST = 0x10 +PCI_CONSECUTIVE = 0x20 +PCI_FLOW_CONTROL = 0x30 +PCI_TYPE_MASK = 0xF0 +PCI_VALUE_MASK = 0x0F +FIRST_DATA_SIZE = 5 +CONSECUTIVE_DATA_SIZE = 7 +REASSEMBLY_TIMEOUT_MS = 500 + + +@dataclass(frozen=True) +class CanAddress: + destination: int + source: int + priority: int = 0 + channel: int = 0 + + def __post_init__(self) -> None: + if not 0 <= self.destination <= 0xFF: + raise SetProtocolError("CAN destination вне диапазона u8") + if not 0 <= self.source <= 0xFF: + raise SetProtocolError("CAN source вне диапазона u8") + if self.priority not in (0, 1): + raise SetProtocolError("CAN priority должен быть 0 или 1") + if not 0 <= self.channel <= 0x7F: + raise SetProtocolError("CAN channel вне диапазона 0..127") + + def pack(self) -> int: + return ( + (CAN_ID_PREFIX << 24) + | (self.destination << 16) + | (self.source << 8) + | (self.priority << 7) + | self.channel + ) + + @classmethod + def unpack(cls, can_id: int) -> "CanAddress": + if not 0 <= can_id <= CAN_ID_MASK: + raise SetProtocolError("CAN ID вне 29-битного диапазона") + if can_id & CAN_ID_PREFIX_MASK != CAN_ID_PREFIX << 24: + raise SetProtocolError("CAN ID не принадлежит SETProtocol v2") + return cls( + destination=(can_id >> 16) & 0xFF, + source=(can_id >> 8) & 0xFF, + priority=(can_id >> 7) & 1, + channel=can_id & 0x7F, + ) + + +@dataclass(frozen=True) +class CanFrame: + can_id: int + data: bytes + + def __post_init__(self) -> None: + CanAddress.unpack(self.can_id) + if not 1 <= len(self.data) <= 8: + raise SetProtocolError("classic CAN data должен содержать 1..8 байт") + + +def segment(packet: bytes, address: CanAddress) -> list[CanFrame]: + """Split one complete encoded SETP frame into classic-CAN frames.""" + if not HEADER_SIZE + CRC_SIZE <= len(packet) <= FRAME_MAX: + raise SetProtocolError("неверная длина SETP-пакета для CAN") + can_id = address.pack() + result = [ + CanFrame( + can_id, + bytes((PCI_FIRST,)) + + len(packet).to_bytes(2, "little") + + packet[:FIRST_DATA_SIZE], + ) + ] + sequence = 1 + for offset in range(FIRST_DATA_SIZE, len(packet), CONSECUTIVE_DATA_SIZE): + chunk = packet[offset : offset + CONSECUTIVE_DATA_SIZE] + result.append(CanFrame(can_id, bytes((PCI_CONSECUTIVE | sequence,)) + chunk)) + sequence = (sequence + 1) & PCI_VALUE_MASK + return result + + +class CanReassembler: + """Reassemble one SETP packet from one active source/channel.""" + + def __init__(self, timeout_ms: int = REASSEMBLY_TIMEOUT_MS) -> None: + if timeout_ms <= 0: + raise ValueError("timeout_ms должен быть положительным") + self.timeout_ms = timeout_ms + self.reset() + + def reset(self) -> None: + self._buffer = bytearray() + self._expected_length = 0 + self._can_id: int | None = None + self._deadline_ms = 0 + self._next_sequence = 1 + + def feed(self, frame: CanFrame, now_ms: int) -> bytes | None: + if self._can_id is not None and now_ms >= self._deadline_ms: + self.reset() + raise SetProtocolError("таймаут сборки SETProtocol CAN") + pci = frame.data[0] & PCI_TYPE_MASK + if pci == PCI_FIRST: + if len(frame.data) != 8: + raise SetProtocolError("первый CAN-сегмент должен иметь DLC 8") + total = int.from_bytes(frame.data[1:3], "little") + if not HEADER_SIZE + CRC_SIZE <= total <= FRAME_MAX: + raise SetProtocolError("неверная полная длина SETP CAN") + self._buffer = bytearray(frame.data[3:]) + self._expected_length = total + self._can_id = frame.can_id + self._deadline_ms = now_ms + self.timeout_ms + self._next_sequence = 1 + return None + if pci == PCI_CONSECUTIVE: + sequence = frame.data[0] & PCI_VALUE_MASK + if ( + self._can_id is None + or frame.can_id != self._can_id + or sequence != self._next_sequence + or len(frame.data) < 2 + ): + self.reset() + raise SetProtocolError("нарушена последовательность SETP CAN") + remaining = self._expected_length - len(self._buffer) + chunk = frame.data[1:] + if len(chunk) > remaining: + self.reset() + raise SetProtocolError("CAN-сегмент длиннее остатка SETP-пакета") + self._buffer.extend(chunk) + self._next_sequence = (self._next_sequence + 1) & PCI_VALUE_MASK + self._deadline_ms = now_ms + self.timeout_ms + if len(self._buffer) != self._expected_length: + return None + packet = bytes(self._buffer) + can_id = self._can_id + self.reset() + assert can_id is not None + address = CanAddress.unpack(can_id) + if packet[:2] != SOF: + raise SetProtocolError("SETP CAN packet не содержит SOF") + source = int.from_bytes(packet[6:8], "little") + destination = int.from_bytes(packet[8:10], "little") + priority = int(bool(packet[3] & int(FrameFlag.PRIORITY))) + if ( + source != address.source + or destination != address.destination + or priority != address.priority + ): + raise SetProtocolError("SETP header не совпадает с CAN ID") + return packet + if pci == PCI_FLOW_CONTROL: + return None + raise SetProtocolError("неизвестный тип SETP CAN-сегмента") diff --git a/python/tests/test_setprotocol_can.py b/python/tests/test_setprotocol_can.py new file mode 100644 index 0000000..e288286 --- /dev/null +++ b/python/tests/test_setprotocol_can.py @@ -0,0 +1,42 @@ +from setprotocol import Frame, FrameFlag, MessageType, build_frame, decode_datagram +from setprotocol.can import CanAddress, CanFrame, CanReassembler, segment + + +def test_can_id_roundtrip(): + value = CanAddress(destination=13, source=0, priority=1, channel=7) + assert CanAddress.unpack(value.pack()) == value + + +def test_segmented_setp_frame_roundtrip(): + packet = build_frame( + Frame( + message_type=MessageType.FW_DATA, + sequence=0x1234, + payload=bytes(range(76)), + flags=FrameFlag.ACK_REQUIRED | FrameFlag.PRIORITY, + source=0, + destination=13, + ) + ) + frames = segment(packet, CanAddress(13, 0, priority=1, channel=1)) + assert len(frames) == 14 + reassembler = CanReassembler() + restored = None + for now_ms, frame in enumerate(frames): + restored = reassembler.feed(frame, now_ms) + assert restored == packet + assert decode_datagram(restored).payload == bytes(range(76)) + + +def test_reassembler_rejects_wrong_sequence(): + packet = build_frame(Frame(MessageType.PING, 1, source=0, destination=13)) + frames = segment(packet, CanAddress(13, 0)) + reassembler = CanReassembler() + assert reassembler.feed(frames[0], 0) is None + damaged = CanFrame(frames[1].can_id, bytes((0x22,)) + frames[1].data[1:]) + try: + reassembler.feed(damaged, 1) + except ValueError as error: + assert "последовательность" in str(error) + else: + raise AssertionError("wrong sequence must fail")