From 345c81166e5418727e1756c7060183a2ad2507d3 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 16 Oct 2026 22:50:25 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B1=D0=BB=D0=BE=D1=87=D0=BD=D0=BE=D0=B5=20=D1=87?= =?UTF-8?q?=D1=82=D0=B5=D0=BD=D0=B8=D0=B5=20EEPROM=20=D0=B2=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=88=D0=B8=D0=B2=D0=BA=D1=83=20166?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Перенести контракт FC03 из 167: до 120 слов за запрос, проверка границ окна и точная длина ответа с CRC. Использовать существующий буфер UART и разрешённые прерывания SPI. Проверить полный дамп из 137 блоков и сохранение прежних маршрутов Modbus и CAN. --- Source/Internal/message.c | 22 ++++++++++++++++++++++ tests/firmware_identity/port_test.c | 27 +++++++++++++++++++++++++++ tests/firmware_identity/run_test.py | 4 ++-- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/Source/Internal/message.c b/Source/Internal/message.c index c33bbfa..6629111 100644 --- a/Source/Internal/message.c +++ b/Source/Internal/message.c @@ -21,6 +21,7 @@ #include "ecan.h" #include "pulto.h" #include "spise2p.h" +#include "include/balsam167_eeprom_block.h" /* Использует общий с 167 контракт FC03: окно 0x8000..0xBFFF, до 120 слов AT25256B за запрос. */ #include "caliber.h" #include "log_to_mem.h" @@ -219,6 +220,27 @@ void ReceiveCommandModbus3(RS_DATA *rs_arr) // получили количество слов данных Length_MB = (rs_arr->RS_Header[4] << 8) | rs_arr->RS_Header[5]; + if (Address_MB >= BALSAM167_EEPROM_RTU_BASE) { /* Выделяет только верхнее окно EEPROM; старые регистры, логгер и паспорт обрабатываются прежним кодом ниже. */ + if (!balsam167_eeprom_block_valid_profile(CMD_MODBUS_3, Address_MB, Length_MB, 0U)) { /* Профиль 0 сохраняет контракт 167 для 32 КиБ: запрещает пустой блок, более 120 слов, M95 и выход за конец окна. */ + modbus_exception(rs_arr, CMD_MODBUS_3, 2U); /* Возвращает Illegal Data Address с CRC до любого обращения к SPI при неверном диапазоне. */ + return; /* Не допускает перехода ошибочного запроса EEPROM к backend логгера или обычным регистрам. */ + } /* Валидатор проверяет конец через вычитание, поэтому переполнение 16-битного адреса не разрешает чтение. */ + Seeprom_read(Address_MB - BALSAM167_EEPROM_RTU_BASE, &rs_arr->buffer[3], Length_MB * 2U); /* Как в 167, SPI получает адрес в 16-битных словах и длину в октетах; IRQ остаются разрешены для автомата Timer2. */ + for (i = Length_MB; i > 0U; --i) { /* Разворачивает прочитанные слова с конца в том же UART-буфере без нового массива и перезаписи ещё не обработанных слов. */ + value = rs_arr->buffer[i + 2U]; /* Временное слово сохраняет оба SPI-октета до расширения одного элемента C28x в два октета RTU. */ + rs_arr->buffer[i * 2U + 2U] = value & 0xFFU; /* Записывает младший октет слова после старшего, как требует физический дамп EEPROM в SETGUI. */ + rs_arr->buffer[i * 2U + 1U] = (value >> 8) & 0xFFU; /* Маска 0xFF выделяет восемь бит линии из 16-битного unsigned int C28x. */ + } /* Обратный обход корректен и для одного слова, и для максимального блока из 120 слов. */ + rs_arr->buffer[0] = CNTRL_ADDR; /* Отвечает собственным адресом платы, сохраняя поддержку общего адреса 16 существующим приёмником. */ + rs_arr->buffer[1] = CMD_MODBUS_3; /* Возвращает стандартную команду FC03 без введения отдельной команды BIOS или записи памяти. */ + rs_arr->buffer[2] = Length_MB * 2U; /* Число полезных октетов находится в диапазоне 2..240 и помещается в одном октете RTU. */ + crc = get_crc_16(0xFFFFU, rs_arr->buffer, Length_MB * 2U + 3U); /* Проверяет адрес, команду, длину и весь блок тем же Modbus CRC16, что остальные ответы прошивки. */ + rs_arr->buffer[Length_MB * 2U + 3U] = LOBYTE(crc); /* Передаёт младший октет CRC первым в соответствии с Modbus RTU. */ + rs_arr->buffer[Length_MB * 2U + 4U] = HIBYTE(crc); /* Завершает кадр старшим октетом CRC без устаревшего нулевого хвоста. */ + rs_arr->flag_TIMEOUT_to_Send = true; /* Использует штатное управление таймаутом UART при отправке готового ответа из main. */ + RS_Send(rs_arr, rs_arr->buffer, Length_MB * 2U + 5U); /* Отправляет ровно 5 служебных октетов плюс данные, максимум 245 элементов из существующего буфера на 400. */ + return; /* Завершает только новый маршрут EEPROM, сохраняя прежние форматы остальных ответов FC03. */ + } /* Новый маршрут не меняет PEEK, запись EEPROM, адреса параметров или драйвер SPI. */ if (!Length_MB || Length_MB > 125 || !((Address_MB < ANSWER_LEN && Length_MB <= ANSWER_LEN-Address_MB) || (Address_MB >= UMP_LOG_BASE && Address_MB <= UMP_LOG_BASE+15 && diff --git a/tests/firmware_identity/port_test.c b/tests/firmware_identity/port_test.c index 52b0995..58067ac 100644 --- a/tests/firmware_identity/port_test.c +++ b/tests/firmware_identity/port_test.c @@ -24,6 +24,14 @@ int ump_log_write(unsigned int address, unsigned int value) { (void)address; (vo static void UMP_commands_write(unsigned int value) { command = value; } /* Сохраняет наблюдение штатной команды регистра 127. */ static void ump_log_rs(void) { ++command_events; } /* Проверяет сохранение прежней регистрации команды RS. */ #include "ump_can_protocol.h" /* Тестирует реальный адаптер CAN 166, сохранивший двенадцатисловный логгер v4. */ +#include "include/balsam167_eeprom_block.h" /* Использует общий с 167 контракт FC03: окно 0x8000..0xBFFF, до 120 слов AT25256B за запрос. */ +static unsigned int spi_reads, spi_address, spi_bytes, spi_blocked; /* Наблюдает число SPI-транзакций, словный адрес, длину в октетах и маску IRQ во время чтения. */ +static void Seeprom_read(unsigned int address, unsigned int *data, unsigned int length) /* Имитирует только SPI-периферию с независимым узнаваемым содержимым всех 32 КиБ. */ +{ /* Производственный обработчик, упаковка и CRC остаются настоящими. */ + unsigned int index; /* Перебирает аппаратные 16-битные слова запрошенного блока. */ + ++spi_reads; spi_address = address; spi_bytes = length; spi_blocked |= irq_mask; /* Выявляет лишние SPI-чтения и запрет Timer2 IRQ внутри нового маршрута. */ + for (index = 0U; index < length / 2U; ++index) data[index] = (address + index) ^ 0xA55AU; /* Эталон зависит от полного адреса, поэтому обнаруживает повтор блока и ошибку смещения. */ +} /* Модель не записывает EEPROM и не подменяет поведение UART. */ /* PRODUCTION_MODBUS */ static void request(RS_DATA *rs, unsigned int address, unsigned int count) /* Формирует уже проверенный транспортом заголовок для настоящих обработчиков. */ { /* Здесь не подменяется алгоритм чтения или упаковки ответа. */ @@ -35,6 +43,25 @@ int main(void) /* Проверяет перенос паспорта и сохр { /* Эталон задан независимо от макроса общего C-инициализатора. */ const unsigned int expected[12] = {1, 1, 0, 0, 2026, 0x0A10, 0x0834, 22, 0x3132, 0x3334, 0x6162, 0x6364}; /* Формат v1, версия 1.0.0, UTC 2026-10-16 08:52:22, Build ID 1234abcd. */ RS_DATA rs; unsigned int words[UMP_CAN_MAX_WORDS], error, i, before; /* Использует те же ёмкости ответа, что действующий служебный CAN. */ + for (before = 0U; before < 16384U; before += 120U) { /* Полный дамп 32 КиБ должен занять 137 запросов вместо 16384 запросов PEEK. */ + unsigned int count = 16384U - before < 120U ? 16384U - before : 120U; /* Последний блок содержит только оставшиеся 64 слова, не выходя за физическую границу. */ + request(&rs, 0x8000U + before, count); ReceiveCommandModbus3(&rs); /* Вызывает настоящий новый маршрут FC03 по всему окну, включая последний блок. */ + CHECK(sent == count * 2U + 5U && rs.buffer[1] == 3U && rs.buffer[2] == count * 2U); /* Ответ имеет точную длину и не содержит нулей после CRC. */ + CHECK(spi_address == before && spi_bytes == count * 2U && !spi_blocked && !irq_calls); /* SPI получает правильные единицы адреса и длины при разрешённых прерываниях Timer2. */ + CHECK(get_crc_16(0xFFFFU, rs.buffer, sent) == 0U); /* Проверяет настоящую CRC каждого полного кадра независимым проходом по всем октетам. */ + for (i = 0U; i < count; ++i) CHECK(((rs.buffer[3U + i * 2U] << 8) | rs.buffer[4U + i * 2U]) == ((before + i) ^ 0xA55AU)); /* Сверяет каждое слово и порядок октетов после расширения UART-буфера на месте. */ + } /* Все 16384 слова должны быть доступны без чтения соседней памяти. */ + CHECK(spi_reads == 137U && reads == 0U && writes == 0U); /* Блочный маршрут не обращается к backend логгера и не выполняет записи. */ + request(&rs, 0xBFFFU, 1U); ReceiveCommandModbus3(&rs); CHECK(sent == 7U && spi_address == 0x3FFFU && spi_bytes == 2U); /* Последнее слово EEPROM остаётся доступно отдельным запросом. */ + before = spi_reads; /* Запоминает число разрешённых чтений до проверки всех ошибочных границ. */ + request(&rs, 0xBFFFU, 2U); ReceiveCommandModbus3(&rs); CHECK(sent == 5U && rs.buffer[1] == 0x83U && rs.buffer[2] == 2U); /* Пересечение конца EEPROM возвращает исключение до обращения к SPI. */ + request(&rs, 0x8000U, 0U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Нулевой блок не начинает зависающее аппаратное ожидание. */ + request(&rs, 0x8000U, 121U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Максимум 120 слов соответствует контракту SETGUI и безопасной длине ответа 245 октетов. */ + request(&rs, 0xC000U, 1U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Адрес сразу после окна не считается первым словом другой EEPROM. */ + request(&rs, 0xFFFFU, 120U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Переполнение 16-битного адреса не обходит ограничение диапазона. */ + request(&rs, 0x8000U, 0x8001U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Маркер M95M02 не принимается прошивкой с историческим 16-битным SPI-драйвером. */ + request(&rs, 0x7FFFU, 1U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Адрес перед окном остаётся запрещённым в старой карте регистров. */ + CHECK(spi_reads == before && get_crc_16(0xFFFFU, rs.buffer, sent) == 0U); /* Неверные запросы не читают SPI и получают корректное исключение с CRC. */ request(&rs, 0x1200U, 12U); ReceiveCommandModbus3(&rs); CHECK(sent == 29U && rs.buffer[1] == 3U && rs.buffer[2] == 24U); /* Полный паспорт отвечает стандартным FC03 без хвостовых нулей. */ for (i = 0; i < 12U; ++i) CHECK(((rs.buffer[3 + i * 2] << 8) | rs.buffer[4 + i * 2]) == expected[i]); /* Проверяет все поля, порядок символов и октетов независимо от производственной упаковки. */ CHECK(get_crc_16(0xFFFFU, rs.buffer, sent) == 0U && reads == 0U && irq_calls == 0U); /* Паспорт защищён настоящей CRC и не обращается к логгеру или маске IRQ. */ diff --git a/tests/firmware_identity/run_test.py b/tests/firmware_identity/run_test.py index 7a4440d..b3dd9b6 100644 --- a/tests/firmware_identity/run_test.py +++ b/tests/firmware_identity/run_test.py @@ -31,7 +31,7 @@ with tempfile.TemporaryDirectory(prefix="firmware166-test-") as directory: # У values.update({f"FI_ID_{i}": ord(char) for i, char in enumerate("1234abcd")}) # Даёт известную последовательность ASCII-слов для проверки порядка октетов. (temporary / "firmware_identity.h").write_text("\n".join(f"#define {name} {value}U /* Проверочная константа паспорта v1. */" for name, value in values.items()), encoding="utf-8") # Сохраняет fixture заголовка в требуемой кодировке. cfile.write_text(model, encoding="utf-8") # Сохраняет извлечённые русские исходники без изменения production-файла. - command = [compiler, "-std=c99", "-O3", "-Wall", "-Wextra", "-Werror", "-I" + str(temporary), "-I" + str(ROOT / "Source/Internal/Include"), "-I" + str(TEMPLATES / "c/firmware-info/include"), str(cfile), str(ROOT / "Source/Internal/crc16.c"), "-o", str(exe)] # Проверяет типы и реальные CRC вместе с обработчиками RS/CAN. + command = [compiler, "-std=c99", "-O3", "-Wall", "-Wextra", "-Werror", "-Wno-unused-function", "-I" + str(TEMPLATES / "c/set-protocol"), "-I" + str(temporary), "-I" + str(ROOT / "Source/Internal/Include"), "-I" + str(TEMPLATES / "c/firmware-info/include"), str(cfile), str(ROOT / "Source/Internal/crc16.c"), "-o", str(exe)] # Проверяет типы и реальные CRC вместе с обработчиками RS/CAN. subprocess.run(command, env=environment, check=True); subprocess.run([str(exe)], env=environment, check=True, timeout=10) # Ошибка типов, поведения или зависание делают тест неуспешным. metadata = {"schema": 1, "product": "Balsam-166-PM35", "target": "TMS320F28335", "version": [1, 0, 0], "build_id": "1234abcd", "built_at_utc": "2026-10-16T08:52:22+00:00", "identity_contract": 1, "identity_register_count": 12} # Фиксирует весь контракт manifest, который должен принять существующий GUI. words = [1, 1, 0, 0, 2026, 0x0A10, 0x0834, 22, 0x3132, 0x3334, 0x6162, 0x6364] # Независимый эталон совпадает с показанным протоколом, а не берётся из C-инициализатора. @@ -54,4 +54,4 @@ with tempfile.TemporaryDirectory(prefix="firmware166-test-") as directory: # У except ValueError: pass # Существующая защита GUI отличает product двух плат. else: raise AssertionError("166 firmware accepted as 167") # Ошибка product делает перенос небезопасным для выбора модели. assert prepare.read_version(ROOT / "UKSSTMS320F28335/firmware_version.h") == [1, 0, 0] # Проверяет единственный источник начальной версии текущего продукта. - print("PASS: embedded identity, stale/truncated/oversize rejection, exact manifest and unchanged SetGUI decoder") # Подтверждает проверку выпуска и совместимость без физического обмена с платой. + print("PASS: 137-block EEPROM dump, SPI IRQ, bounds, embedded identity, stale/truncated/oversize rejection, exact manifest and unchanged SetGUI decoder") # Подтверждает проверку выпуска и совместимость без физического обмена с платой.