From bbd82b3a4e0d6b6748cc99b6d6686ea63cbb7fec Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 19 Oct 2026 16:33:28 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BD=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20166=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D1=83=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20M95M02=20=D0=B8=20Flash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Восстановить телеметрию CAN, обработку TESTLAMP и сохранение команд при загрузке параметров. Выбрать M95M02 основным профилем, добавить самостоятельную Flash-сборку и проверять образы до публикации. Проверки: хостовые тесты, реальные сборки TI C2000, контроль COFF/SCI8 и защита записи при активной нагрузке. --- .gitignore | 4 + Doc/EEPROM_LAYOUT.md | 20 +- Doc/FIRMWARE_IDENTITY.md | 70 +++-- Doc/GAS_167_M95M02.md | 2 +- Doc/GAS_MODBUS_MAP.md | 6 +- Doc/PERIPH28335_CAN.md | 15 ++ F28335_flash.cmd | 263 +++++++++++++++++++ Source/Internal/DAC.c | 20 ++ Source/Internal/Include/DAC.h | 3 + Source/Internal/Include/can_telemetry_impl.h | 123 +++++++++ Source/Internal/Include/ecan.h | 8 +- Source/Internal/Include/eeprom_config.h | 4 +- Source/Internal/Include/flash_rs.h | 5 + Source/Internal/Include/gas167.h | 2 +- Source/Internal/Include/package.h | 4 +- Source/Internal/bios.c | 17 ++ Source/Internal/ecan.c | 80 +----- Source/Internal/flash_codestart.asm | 12 + Source/Internal/flash_rs.c | 179 +++++++++++++ Source/Internal/gas167.c | 9 +- Source/Internal/main.c | 88 ++----- Source/Internal/measure.c | 4 +- Source/Internal/message.c | 38 ++- Source/Internal/peripher.c | 3 +- Source/Internal/pulto.c | 10 + UKSSTMS320F28335/.cproject | 8 +- UKSSTMS320F28335/.project | 5 + UKSSTMS320F28335/README.md | 107 +++++++- UKSSTMS320F28335/build_flash.py | 101 +++++++ UKSSTMS320F28335/prepare_identity.py | 61 ++++- UKSSTMS320F28335/publish_identity.py | 202 ++++++++++++++ tests/can_telemetry/model.c | 107 ++++++++ tests/can_telemetry/run_test.py | 22 ++ tests/can_timeout/run_test.py | 35 +-- tests/can_vpu_rx/run_test.py | 68 +++++ tests/firmware_identity/test_release.py | 181 +++++++++++++ tests/flash_rs/run_test.py | 47 ++++ tests/load_control/load_control_test.c | 12 + tests/load_control/parameter_commands_test.c | 62 +++++ tests/load_control/run_test.py | 44 +++- tests/testlamp/model.inc | 86 +++--- tests/testlamp/run_test.py | 4 +- 42 files changed, 1820 insertions(+), 321 deletions(-) create mode 100644 F28335_flash.cmd create mode 100644 Source/Internal/Include/can_telemetry_impl.h create mode 100644 Source/Internal/Include/flash_rs.h create mode 100644 Source/Internal/flash_codestart.asm create mode 100644 Source/Internal/flash_rs.c create mode 100644 UKSSTMS320F28335/build_flash.py create mode 100644 UKSSTMS320F28335/publish_identity.py create mode 100644 tests/can_telemetry/model.c create mode 100644 tests/can_telemetry/run_test.py create mode 100644 tests/can_vpu_rx/run_test.py create mode 100644 tests/firmware_identity/test_release.py create mode 100644 tests/flash_rs/run_test.py create mode 100644 tests/load_control/parameter_commands_test.c diff --git a/.gitignore b/.gitignore index c36d6e1..67cd935 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,7 @@ /Bin/**/logger_channels.png # Снимки предпросмотра защиты используются только при визуальной проверке интерфейса. /Bin/**/gui_preview.png +# Локальный комплект проверенных RAM/Flash-образов и их паспортов создаётся из исходников перед выпуском. +/Bin/Final167/ +# Временные объекты отдельной Flash-сборки зависят от установленного TI toolchain и не входят в исходники. +/Bin/Flash167/ diff --git a/Doc/EEPROM_LAYOUT.md b/Doc/EEPROM_LAYOUT.md index 4fbe28d..23b9348 100644 --- a/Doc/EEPROM_LAYOUT.md +++ b/Doc/EEPROM_LAYOUT.md @@ -60,16 +60,18 @@ SPI-загрузчика и драйвера, а не только правка выбор. Не задавайте противоречащий ему отдельный define только компилятору CCS: тогда компилятор, линкер и проверка BIN могут получить разные настройки. -### Сборка скриптом +### Отдельная сборка -```powershell -./build_logger.ps1 -Eeprom AT25256B -OutputDirectory ./Bin/EepromProfiles/AT25256B -ImageName UKSSTMS320F28335_AT25256B -./build_logger.ps1 -Eeprom M95M02 -OutputDirectory ./Bin/EepromProfiles/M95M02 -ImageName UKSSTMS320F28335_M95M02 -``` - -Параметр скрипта передаётся компилятору, линкеру и проверке размера; исходный -config не меняется. Скрипт по умолчанию собирает AT25256B. Рядом с BIN создаются -manifest и SHA-256; поле `eeprom` в manifest различает аппаратные варианты. +Скрипт `build_logger.ps1` отсутствует в текущем дереве. Для штатного выпуска +выберите профиль в заголовке и выполните Clean/Rebuild CCS. +`UKSSTMS320F28335/prepare_identity.py` подготавливает паспорт; +`UKSSTMS320F28335/publish_identity.py` проверяет и публикует результат TI. +При отдельной сборке явный выбор `--eeprom AT25256B` или `--eeprom M95M02` +в обоих Python-шагах должен совпадать с `EEPROM_M95M02` у компилятора и +препроцессора линкера. Защищённый postbuild сверяет профиль с символами MAP, +проверяет структуру SCI8, встроенный паспорт и предел до параметров EEPROM +перед копированием в постоянные каталоги. Рядом с BIN создаются manifest и +SHA-256; поле `eeprom` в manifest различает аппаратные варианты. UART остаётся 115200; алгоритм UART и FIFO не изменялся. | Параметр | AT25256B | M95M02-DR | diff --git a/Doc/FIRMWARE_IDENTITY.md b/Doc/FIRMWARE_IDENTITY.md index 8ccab40..ceed8a4 100644 --- a/Doc/FIRMWARE_IDENTITY.md +++ b/Doc/FIRMWARE_IDENTITY.md @@ -15,42 +15,36 @@ | `UKSSTMS320F28335/firmware_identity.json` | Product и target; версия берётся из C-заголовка | | `Source/Internal/shared_ump.c` | Подключение общей таблицы версии и привязка к UMP ПМ35 | | `UKSSTMS320F28335/prepare_identity.py` | Генерация идентичности из C-заголовка перед CCS Debug/Release | -| `build_logger.ps1` | Полный CLI build: prepare, compile, link, SCI8, size check, seal | -| `build_bin.bat` | Debug post-build: проверка SCI8 и паспорт конечного BIN | +| `UKSSTMS320F28335/publish_identity.py` | Защищённый post-build Debug/Release: SCI8, встроенный паспорт, профиль EEPROM, размер, manifest и SHA | | `tests/firmware_identity/port_test.c` | Host-проверка привязки версии к реальному порту 167 | Номер `0.1.0` — начальная версия разработки этого механизма; он не выдаётся за историческую версию предыдущих BIN. Назначайте release-номер вручную перед выпуском. -## Сборка из PowerShell +## Выпуск и отдельные проверки -Из корня 167: +Скрипты `build_logger.ps1` и `build_bin.bat` отсутствуют в текущем дереве. +Штатный выпуск выполняется через Clean/Rebuild CCS с оптимизацией +`-O3 --opt_for_speed=0`. Обе конфигурации вызывают `prepare_identity.py` +перед компиляцией и `publish_identity.py` после линковки. -```powershell -./build_logger.ps1 -OutputDirectory "$PWD/Bin/FirmwareIdentity" ` - -ImageName UKSSTMS320F28335 -``` +Prebuild сохраняет заголовок и JSON в `${BuildDirectory}/generated`. +Защищённый postbuild сначала вызывает общий конвертор во временный каталог, +затем проверяет границы EEPROM, структуру SCI8, точку входа и адреса RAM, +ровно один встроенный паспорт текущей компиляции и весь комплект manifest/SHA. +Аппаратный профиль сверяется с абсолютными символами EEPROM в карте линкера. +Ошибка любой проверки не публикует новый BIN или архив. -По умолчанию применяется `-O2 --opt_for_speed=0`. Результаты: +Проверенный выпуск сохраняется в `${BuildDirectory}/firmware` вместе с +manifest и SHA-256. Архив содержит OUT и сопроводительные файлы; его копия +передаётся в каталог, заданный аргументом `--export` в `.cproject`. +Поле `eeprom` в manifest обязательно различает AT25256B и M95M02. -```text -Bin/FirmwareIdentity/UKSSTMS320F28335.bin -Bin/FirmwareIdentity/UKSSTMS320F28335.bin.manifest.json -Bin/FirmwareIdentity/UKSSTMS320F28335.bin.sha256 -Bin/FirmwareIdentity/generated/firmware_identity.h -Bin/FirmwareIdentity/generated/firmware_identity.json -``` - -Проверка файла перед передачей: - -```powershell -python ../newProject/SETGUI/third_party/templates/c/firmware-info/tools/identity.py verify ` - --image Bin/FirmwareIdentity/UKSSTMS320F28335.bin ` - --manifest Bin/FirmwareIdentity/UKSSTMS320F28335.bin.manifest.json -``` - -CLI принимает `-TemplatesRoot`, если общий checkout расположен иначе. Все -исходники общего механизма собираются непосредственно из этого checkout. +При отдельной сборке параметры обоих Python-шагов можно посмотреть через +`--help`. Явный `--eeprom` должен совпадать с define компилятора и линкера. +Паспорт зависимости относится к фактически компилируемой копии +`SETGUI/third_party/templates`; инструменты конверсии остаются в соседнем +основном checkout `templates`. ## Сборка CCS @@ -62,7 +56,7 @@ Pre-build требует Python 3.10+ и Git в PATH процесса CCS. `Gene этому проекту: не запускайте Debug/Release одновременно. Для параллельных сборок используйте CLI с разными output-каталогами. -Штатный Debug post-build создаёт в `Bin`: +Штатный post-build Debug/Release создаёт в `${BuildDirectory}/firmware`: ```text UKSSTMS320F28335_.bin @@ -70,12 +64,9 @@ UKSSTMS320F28335_.bin.manifest.json UKSSTMS320F28335_.bin.sha256 ``` -Имя BIN исторически содержит HEAD без dirty-суффикса; достоверные признаки dirty -находятся внутри ответа версии и паспорта. Не определяйте чистоту только по имени. -Release-конфигурация получает embedded identity через pre-build, но её старый -проект не имел автоматического BIN post-build. Для готового проверенного BIN с -паспортом используйте описанный CLI либо настройте release-конверсию по общей -инструкции; нельзя запускать Debug `build_bin.bat` на Release и ожидать другой `.out`. +Идентификатор в имени BIN соответствует встроенному Build ID; знак `+` +обозначает незакоммиченные изменения приложения либо компилируемой зависимости. +Полная ревизия и состояние обоих деревьев находятся в паспорте. Если CCS пишет `Error ... (ignored)`, сборка не считается успешной: убедитесь, что последняя проверка и `seal` завершились без ошибок. Старый BIN с таким же @@ -119,14 +110,17 @@ CAN ПМ67 не переназначается. Запись функции 06 ## Влияние на память и параметры -С проверенными настройками текущий образ занимает **32 286 байт**, предел — -**32 508**, запас **222 байта**. Механизм добавил 116 байт относительно сборки -без него. Это результат конкретной сборки, не обещание для следующих изменений. +Историческая сборка при внедрении механизма занимала **32 286 байт** при +пределе AT25256B **32 508 байт**. Этот результат не относится к текущему +полному набору функций. Основной профиль теперь M95M02, его предел до +параметров — **261 884 байта**; актуальные результаты сборок приведены в +`UKSSTMS320F28335/README.md`. Postbuild проверяет размер каждого выпуска заново. Таблица версии — часть `.econst` прошивки. Её не сохраняют в область параметров; старые настройки не могут подменить версию после обновления. Размер буфера лога и размещение EEPROM-параметров не менялись. Дата не разбирается на MCU, SHA-256 -не вычисляется на MCU. Проверка размера `check_firmware.ps1` остаётся обязательной. +не вычисляется на MCU. Проверка размера встроена в `publish_identity.py` и +выполняется до публикации каждого нового BIN. ## Проверки и ограничения diff --git a/Doc/GAS_167_M95M02.md b/Doc/GAS_167_M95M02.md index 2b1ad06..1d26c5c 100644 --- a/Doc/GAS_167_M95M02.md +++ b/Doc/GAS_167_M95M02.md @@ -43,7 +43,7 @@ CLI define EEPROM_M95M02 должен передаваться и компиля | 0x5F04 | 0x5000 | Начало технологических регистров | | 0x5F05 | 128 | Количество технологических регистров | | 0x5F06 | 7 | Бит 0: RTU; бит 1: служебный CAN; бит 2: запись FC06 | -| 0x5F07 | 1 | EEPROM M95M02 | +| 0x5F07 | 1 / 2 | 1 — EEPROM M95M02 с исполнением из RAM; 2 — отдельный образ исполнения из внутренней Flash | В сборке AT25256B эти адреса отклоняются как недопустимые; сигнатуру GAS она не возвращает. diff --git a/Doc/GAS_MODBUS_MAP.md b/Doc/GAS_MODBUS_MAP.md index 21d1cc0..3be62fe 100644 --- a/Doc/GAS_MODBUS_MAP.md +++ b/Doc/GAS_MODBUS_MAP.md @@ -138,8 +138,8 @@ GAS предоставляет доступ к тем же значениям `m | `0x005D` | 93 | `0x505D` | 20573 | 5093 | 6093 | `sens_lo_edge[21]` | RW | | `0x005E` | 94 | `0x505E` | 20574 | 5094 | 6094 | `sens_lo_edge[22]` | RW | | `0x005F` | 95 | `0x505F` | 20575 | 5095 | 6095 | `sens_lo_edge[23]` | RW | -| `0x0060` | 96 | `0x5060` | 20576 | 5096 | 6096 | `Cancount[m_FAST]` — пауза быстрых посылок CAN | RW | -| `0x0061` | 97 | `0x5061` | 20577 | 5097 | 6097 | `Cancount[m_SLOW]` — пауза медленных посылок CAN | RW | +| `0x0060` | 96 | `0x5060` | 20576 | 5096 | 6096 | `Cancount[m_FAST]` — включение быстрого потока CAN: 0 выключен, иначе включён | RW | +| `0x0061` | 97 | `0x5061` | 20577 | 5097 | 6097 | `Cancount[m_SLOW]` — включение медленного потока CAN: 0 выключен, иначе включён | RW | | `0x0062` | 98 | `0x5062` | 20578 | 5098 | 6098 | `Brightness` / `Bright[0]` — яркость | RW | | `0x0063` | 99 | `0x5063` | 20579 | 5099 | 6099 | `Owncount` / `Bright[1]`; LOAD: исходное значение АЦП | RW | | `0x0064` | 100 | `0x5064` | 20580 | 5100 | 6100 | `Bright[2]`; `Current_count`: исходное значение канала 0 | RW | @@ -186,7 +186,7 @@ GAS предоставляет доступ к тем же значениям `m | `0x5F04` | 24324 | `0x5000` | База технологических GAS-регистров | | `0x5F05` | 24325 | `128` | Число технологических регистров | | `0x5F06` | 24326 | `0x0007` | Возможности: бит 0 — RTU, бит 1 — служебный CAN, бит 2 — FC06 | -| `0x5F07` | 24327 | `1` | Аппаратный вариант EEPROM M95M02 | +| `0x5F07` | 24327 | `1` / `2` | Исполнение: `1` — загрузка из M95M02 в RAM, `2` — внутренняя Flash; физическая EEPROM указывается отдельно в manifest | Все восемь слов — **RO**. В сборке AT25256B GAS-адреса, включая этот блок, отклоняются с кодом недопустимого адреса. diff --git a/Doc/PERIPH28335_CAN.md b/Doc/PERIPH28335_CAN.md index a5995c2..8bc6b19 100644 --- a/Doc/PERIPH28335_CAN.md +++ b/Doc/PERIPH28335_CAN.md @@ -69,6 +69,21 @@ UMP_CAN_service(). Ожидания передачи в цикле нет. Пе ## Формат кадров +После переноса планировщика из ветки 166 штатная телеметрия MBOX0 передаёт +изменённые трёхсловные пакеты и повторяет каждый подтверждённый пакет через +1,3 с. Изменения обнаруживаются с шагом 20 мс; потоки обслуживаются по очереди, +поэтому включённая быстрая маска не блокирует медленную. R0–R3 пульта находятся +в быстрой маске. При свободной шине и своевременном ACK обновления R3 проходят +не реже одного раза за 20 мс, что проверяется хостовой моделью. + +R96/R97 сохраняют прежние адреса, но теперь являются выключателями потоков: +ноль отключает соответствующий поток, любое ненулевое значение включает. +Значение больше не умножается на период 20 мс. Отдельный heartbeat с адресом +`0x0100` и счётчиком секунд передаётся с частотой 1 Гц независимо от этих настроек. +Кэш обновляется только после аппаратного ACK. При ошибке пакет остаётся +неподтверждённым; повтор разрешается после секундной паузы. Служебный канал +УМП/GAS продолжает использовать отдельный MBOX2. + CAN 2.0B, расширенный ID, 8 байт данных. Порядок соответствует существующей прошивке: MDL (слова 1,2), затем MDH (заголовок, слово 0). Каждое слово содержит сначала старший байт. diff --git a/F28335_flash.cmd b/F28335_flash.cmd new file mode 100644 index 0000000..bb19da8 --- /dev/null +++ b/F28335_flash.cmd @@ -0,0 +1,263 @@ +/* Отдельный образ исполнения из Flash F28335: код хранится в секторах H/G, загрузчик RS и Flash API исполняются из RAM; параметры остаются во внешней EEPROM. */ +/* +// TI File $Revision: /main/10 $ +// Checkin $Date: July 9, 2008 13:43:56 $ +//########################################################################### +// +// FILE: F28335.cmd +// +// TITLE: Linker Command File For F28335 Device +// +//########################################################################### +// $TI Release: DSP2833x/DSP2823x Header Files V1.20 $ +// $Release Date: August 1, 2008 $ +//########################################################################### +*/ + +/* ====================================================== +// For Code Composer Studio V2.2 and later +// --------------------------------------- +// In addition to this memory linker command file, +// add the header linker command file directly to the project. +// The header linker command file is required to link the +// peripheral structures to the proper locations within +// the memory map. +// +// The header linker files are found in \DSP2833x_Headers\cmd +// +// For BIOS applications add: DSP2833x_Headers_BIOS.cmd +// For nonBIOS applications add: DSP2833x_Headers_nonBIOS.cmd +========================================================= */ + +/* ====================================================== +// For Code Composer Studio prior to V2.2 +// -------------------------------------- +// 1) Use one of the following -l statements to include the +// header linker command file in the project. The header linker +// file is required to link the peripheral structures to the proper +// locations within the memory map */ + +/* Uncomment this line to include file only for non-BIOS applications */ +/* -l DSP2833x_Headers_nonBIOS.cmd */ + +/* Uncomment this line to include file only for BIOS applications */ +/* -l DSP2833x_Headers_BIOS.cmd */ + +/* 2) In your project add the path to \DSP2833x_headers\cmd to the + library search path under project->build options, linker tab, + library search path (-i). +/*========================================================= */ + +/* Define the memory block start/length for the F28335 + PAGE 0 will be used to organize program sections + PAGE 1 will be used to organize data sections + + Notes: + Memory blocks on F28335 are uniform (ie same + physical memory) in both PAGE 0 and PAGE 1. + That is the same memory region should not be + defined for both PAGE 0 and PAGE 1. + Doing so will result in corruption of program + and/or data. + + L0/L1/L2 and L3 memory blocks are mirrored - that is + they can be accessed in high memory or low memory. + For simplicity only one instance is used in this + linker file. + + Contiguous SARAM memory blocks can be combined + if required to create a larger memory block. + */ + + +/* Serial EEPROM is NOT CPU-addressable RAM/Flash. Do not add a fake MEMORY + * region at address zero: SPI boot copies its table into the RAM areas below. + * Export its physical layout as absolute byte/word symbols for .map/debugging. + * The final SCI8 image size (including boot headers) is checked after hex2000. */ +/* Подключает "Source/Internal/Include/eeprom_config.h": выбор профиля физически установленной EEPROM при сборке. */ +#include "Source/Internal/Include/eeprom_config.h" +/* Подключает "include/balsam167_eeprom_layout.h": настройки и интерфейс balsam167_eeprom_layout. */ +#include "include/balsam167_eeprom_layout.h" +/* Записывает _EEPROM_TOTAL_BYTES значением BALSAM167_EEPROM_BYTES; данные _EEPROM_TOTAL_BYTES. */ +_EEPROM_TOTAL_BYTES = BALSAM167_EEPROM_BYTES; +/* Записывает _EEPROM_FIRMWARE_LIMIT_BYTES значением BALSAM167_EEPROM_FIRMWARE_BYTES; данные _EEPROM_FIRMWARE_LIMIT_BYTES. */ +_EEPROM_FIRMWARE_LIMIT_BYTES = BALSAM167_EEPROM_FIRMWARE_BYTES; +/* Записывает _EEPROM_PARAMS_START_WORD значением BALSAM167_EEPROM_PARAMS_START_WORD; EEPROM PARAMS START слово. */ +_EEPROM_PARAMS_START_WORD = BALSAM167_EEPROM_PARAMS_START_WORD; +/* Записывает _EEPROM_PARAMS_START_BYTE значением BALSAM167_EEPROM_FIRMWARE_BYTES; данные _EEPROM_PARAMS_START_BYTE. */ +_EEPROM_PARAMS_START_BYTE = BALSAM167_EEPROM_FIRMWARE_BYTES; +/* Записывает _EEPROM_PARAMS_END_BYTE значением BALSAM167_EEPROM_PARAMS_END_WORD * 2; данные _EEPROM_PARAMS_END_BYTE. */ +_EEPROM_PARAMS_END_BYTE = BALSAM167_EEPROM_PARAMS_END_WORD * 2; + +/* Начинает описание физических блоков памяти для линкера. */ +MEMORY +/* Открывает составной блок или описание полей; следующие строки принадлежат этому уровню вложенности. */ +{ +PAGE 0: /* Program Memory */ /* Выбирает страницу адресного пространства линкера: 0 — программа, 1 — данные. */ + /* Memory (RAM/FLASH/OTP) blocks can be moved to PAGE1 for data allocation */ + + ZONE0 : origin = 0x004000, length = 0x001000 /* XINTF zone 0 */ /* Записывает origin значением 0x004000, length = 0x001000; данные origin. */ + RAML0 : origin = 0x008000, length = 0x004000 /* on-chip RAM block L0 */ /* Записывает origin значением 0x008000, length = 0x004000; данные origin. */ +/* RAML1 : origin = 0x009000, length = 0x001000 /* on-chip RAM block L1 */ +/* RAML2 : origin = 0x00A000, length = 0x001000 /* on-chip RAM block L2 */ +// RAML3 : origin = 0x00B000, length = 0x001000 /* on-chip RAM block L3 */ + RAML4 : origin = 0x00C000, length = 0x001000 /* on-chip RAM block L1 */ /* Записывает origin значением 0x00C000, length = 0x001000; данные origin. */ + + ZONE6 : origin = 0x0100000, length = 0x100000 /* XINTF zone 6 */ /* Записывает origin значением 0x0100000, length = 0x100000; данные origin. */ + FLASHH : origin = 0x300000, length = 0x008000 /* on-chip FLASH */ /* Записывает origin значением 0x300000, length = 0x008000; данные origin. */ + FLASHG : origin = 0x308000, length = 0x008000 /* on-chip FLASH */ /* Записывает origin значением 0x308000, length = 0x008000; данные origin. */ + FLASHF : origin = 0x310000, length = 0x008000 /* on-chip FLASH */ /* Записывает origin значением 0x310000, length = 0x008000; данные origin. */ + FLASHE : origin = 0x318000, length = 0x008000 /* on-chip FLASH */ /* Записывает origin значением 0x318000, length = 0x008000; данные origin. */ + FLASHD : origin = 0x320000, length = 0x008000 /* on-chip FLASH */ /* Записывает origin значением 0x320000, length = 0x008000; данные origin. */ + FLASHC : origin = 0x328000, length = 0x008000 /* on-chip FLASH */ /* Записывает origin значением 0x328000, length = 0x008000; данные origin. */ + FLASHA : origin = 0x338000, length = 0x007F80 /* on-chip FLASH */ /* Записывает origin значением 0x338000, length = 0x007F80; данные origin. */ + CSM_RSVD : origin = 0x33FF80, length = 0x000076 /* Part of FLASHA. Program with all 0x0000 when CSM is in use. */ /* Записывает origin значением 0x33FF80, length = 0x000076; данные origin. */ + BEGIN : origin = 0x33FFF6, length = 0x000002 /* Part of FLASHA. Used for "boot to Flash" bootloader mode. */ /* Записывает origin значением 0x33FFF6, length = 0x000002; данные origin. */ + CSM_PWL : origin = 0x33FFF8, length = 0x000008 /* Part of FLASHA. CSM password locations in FLASHA */ /* Записывает origin значением 0x33FFF8, length = 0x000008; данные origin. */ + OTP : origin = 0x380400, length = 0x000400 /* on-chip OTP */ /* Записывает origin значением 0x380400, length = 0x000400; данные origin. */ + ADC_CAL : origin = 0x380080, length = 0x000009 /* ADC_cal function in Reserved memory */ /* Записывает origin значением 0x380080, length = 0x000009; данные origin. */ + + IQTABLES : origin = 0x3FE000, length = 0x000b50 /* IQ Math Tables in Boot ROM */ /* Записывает origin значением 0x3FE000, length = 0x000b50; данные origin. */ + IQTABLES2 : origin = 0x3FEB50, length = 0x00008c /* IQ Math Tables in Boot ROM */ /* Записывает origin значением 0x3FEB50, length = 0x00008c; данные origin. */ + FPUTABLES : origin = 0x3FEBDC, length = 0x0006A0 /* FPU Tables in Boot ROM */ /* Записывает origin значением 0x3FEBDC, length = 0x0006A0; данные origin. */ + ROM : origin = 0x3FF27C, length = 0x000D44 /* Boot ROM */ /* Записывает origin значением 0x3FF27C, length = 0x000D44; данные origin. */ + RESET : origin = 0x3FFFC0, length = 0x000002 /* part of boot ROM */ /* Записывает origin значением 0x3FFFC0, length = 0x000002; данные origin. */ + VECTORS : origin = 0x3FFFC2, length = 0x00003E /* part of boot ROM */ /* Записывает origin значением 0x3FFFC2, length = 0x00003E; данные origin. */ + +PAGE 1 : /* Data Memory */ /* Выбирает страницу адресного пространства линкера: 0 — программа, 1 — данные. */ + /* Memory (RAM/FLASH/OTP) blocks can be moved to PAGE0 for program allocation */ + /* Registers remain on PAGE1 */ + + BOOT_RSVD : origin = 0x000000, length = 0x000050 /* Part of M0, BOOT rom will use this for stack */ /* Записывает origin значением 0x000000, length = 0x000050; данные origin. */ + RAMM0 : origin = 0x000050, length = 0x0003B0 /* on-chip RAM block M0 */ /* Записывает origin значением 0x000050, length = 0x0003B0; данные origin. */ + RAMM1 : origin = 0x000400, length = 0x000400 /* on-chip RAM block M1 */ /* Записывает origin значением 0x000400, length = 0x000400; данные origin. */ + RAML5 : origin = 0x00D000, length = 0x003000 /* on-chip RAM block L1 */ /* Записывает origin значением 0x00D000, length = 0x003000; данные origin. */ +/* RAML6 : origin = 0x00E000, length = 0x001000 /* on-chip RAM block L1 */ +/* RAML7 : origin = 0x00F000, length = 0x001000 /* on-chip RAM block L1 */ + ZONE7A : origin = 0x0200000, length = 0x00FC00 /* XINTF zone 7 - program space */ /* Записывает origin значением 0x0200000, length = 0x00FC00; данные origin. */ + ZONE7B : origin = 0x20FC00, length = 0x000400 /* XINTF zone 7 - data space */ /* Записывает origin значением 0x20FC00, length = 0x000400; данные origin. */ + FLASHB : origin = 0x330000, length = 0x008000 /* on-chip FLASH */ /* Записывает origin значением 0x330000, length = 0x008000; данные origin. */ +/* Закрывает текущий блок или список полей; следующие операторы относятся к внешнему уровню. */ +} + +/* Allocate sections to memory blocks. + Note: + codestart user defined section in DSP28_CodeStartBranch.asm used to redirect code + execution when booting to flash + ramfuncs user defined section to store functions that will be copied from Flash into RAM +*/ + +/* Начинает описание размещения секций объектных файлов для линкера. */ +SECTIONS +/* Открывает составной блок или описание полей; следующие строки принадлежат этому уровню вложенности. */ +{ + + /* Allocate program areas: */ + /* Keep initialization tables out of the code RAM block. */ + /* Записывает PAGE значением 0; данные PAGE. */ + .cinit : > FLASHG PAGE = 0 /* Таблица инициализации C хранится в секторе G и применяется ROM-моделью startup. */ + /* Записывает PAGE значением 0; данные PAGE. */ + .pinit : > FLASHG PAGE = 0 /* Конструкторы используют тот же непрерывный сектор данных, что и таблица C startup. */ + /* Записывает PAGE значением 0; данные PAGE. */ + .text : > FLASHH PAGE = 0 /* Основной код запускается непосредственно из сектора H, начиная с адреса слова 0x300000. */ + /* Записывает PAGE значением 0; данные PAGE. */ + codestart : > BEGIN PAGE = 0 + flash_rs : /* Собирает весь автономный участок, который должен пережить стирание исполняемой Flash. */ + { /* Начинает перечень функций и таблиц загрузчика RS и официального TI API. */ + *(flash_rs) /* Включает декодер, CRC, проверку образа, стирание, программирование и автономный ответ SCI. */ + -lFlash28335_API_V210.lib(.text) /* Копирует машинный код штатного Flash API v2.10 F28335 во внутреннюю RAM. */ + -lFlash28335_API_V210.lib(.econst) /* Сохраняет таблицы API в той же RAM, чтобы во время стирания не читать сектор F. */ + } LOAD = FLASHF, RUN = RAML0, /* Начальная копия хранится в секторе F; рабочий диапазон RAML0–L3 имеет 0x4000 16-битных слов. */ + LOAD_START(_FlashRsLoadStart), LOAD_END(_FlashRsLoadEnd), RUN_START(_FlashRsRunStart), PAGE = 0 /* Экспортирует адреса копирования в адресуемых словах C28x. */ + /* Записывает LOAD значением RAML4,; данные LOAD. */ + ramfuncs : LOAD = FLASHG, /* InitFlash хранится в G, но вызывается только после копирования в RAML4. */ + /* Записывает RUN значением RAML4,; данные RUN. */ + RUN = RAML4, + /* Вызывает LOAD_START с аргументами (_RamfuncsLoadStart), в текущей последовательности операций. */ + LOAD_START(_RamfuncsLoadStart), + /* Вызывает LOAD_END с аргументами (_RamfuncsLoadEnd), в текущей последовательности операций. */ + LOAD_END(_RamfuncsLoadEnd), + /* Вызывает RUN_START с аргументами (_RamfuncsRunStart), в текущей последовательности операций. */ + RUN_START(_RamfuncsRunStart), + /* Записывает PAGE значением 0; данные PAGE. */ + PAGE = 0 + + /* Записывает PAGE значением 0; данные PAGE. */ + csmpasswds : > CSM_PWL PAGE = 0 + /* Записывает PAGE значением 0; данные PAGE. */ + csm_rsvd : > CSM_RSVD PAGE = 0 + + /* Allocate uninitalized data sections: */ + /* Записывает PAGE значением 1; данные PAGE. */ + .stack : > RAMM1 PAGE = 1 + /* Записывает PAGE значением 1; данные PAGE. */ + .ebss : > RAML5 PAGE = 1 + /* Записывает PAGE значением 0; данные PAGE. */ + .esysmem : > RAML0 PAGE = 0 + + /* Записывает PAGE значением 1; данные PAGE. */ + .logg : > ZONE7A PAGE = 1 + + /* Initalized sections go in Flash */ + /* For SDFlash to program these, they must be allocated to page 0 */ + /* Записывает PAGE значением 0; данные PAGE. */ + .econst : > FLASHG PAGE = 0 /* Неизменяемые таблицы, GAS и паспорт выпуска остаются частью исполняемого образа во Flash. */ + /* Записывает PAGE значением 0; данные PAGE. */ + .switch : > FLASHG PAGE = 0 /* Таблицы переходов хранятся вместе с прочими константами, не занимая рабочую RAM. */ + + /* Allocate IQ math areas: */ + IQmath : > FLASHC PAGE = 0 /* Math Code */ /* Записывает PAGE значением 0; данные PAGE. */ + /* Записывает PAGE значением 0, TYPE = NOLOAD; данные PAGE. */ + IQmathTables : > IQTABLES, PAGE = 0, TYPE = NOLOAD + + /* Uncomment the section below if calling the IQNexp() or IQexp() + functions from the IQMath.lib library in order to utilize the + relevant IQ Math table in Boot ROM (This saves space and Boot ROM + is 1 wait-state). If this section is not uncommented, IQmathTables2 + will be loaded into other memory (SARAM, Flash, etc.) and will take + up space, but 0 wait-state is possible. + */ + /* + IQmathTables2 : > IQTABLES2, PAGE = 0, TYPE = NOLOAD + { + + IQmath.lib (IQmathTablesRam) + + } + */ + + /* Записывает PAGE значением 0, TYPE = NOLOAD; данные PAGE. */ + FPUmathTables : > FPUTABLES, PAGE = 0, TYPE = NOLOAD + + /* Allocate DMA-accessible RAM sections: * / + DMARAML4 : > RAML4, PAGE = 1 + DMARAML5 : > RAML5, PAGE = 1 +/* DMARAML6 : > RAML6, PAGE = 1 + DMARAML7 : > RAML7, PAGE = 1 +*/ + /* Allocate 0x400 of XINTF Zone 7 to storing data */ + /* Записывает PAGE значением 1; данные PAGE. */ + ZONE7DATA : > ZONE7B, PAGE = 1 + + /* .reset is a standard section used by the compiler. It contains the */ + /* the address of the start of _c_int00 for C Code. /* + /* When using the boot ROM this section and the CPU vector */ + /* table is not needed. Thus the default type is set here to */ + /* DSECT */ + /* Записывает PAGE значением 0, TYPE = DSECT; данные PAGE. */ + .reset : > RESET, PAGE = 0, TYPE = DSECT + /* Записывает PAGE значением 0, TYPE = DSECT; данные PAGE. */ + vectors : > VECTORS PAGE = 0, TYPE = DSECT + + /* Allocate ADC_cal function (pre-programmed by factory into TI reserved memory) */ + /* Записывает load значением ADC_CAL, PAGE = 0, TYPE = NOLOAD; данные load. */ + .adc_cal : load = ADC_CAL, PAGE = 0, TYPE = NOLOAD + +/* Закрывает текущий блок или список полей; следующие операторы относятся к внешнему уровню. */ +} + +/* +//=========================================================================== +// End of file. +//=========================================================================== +*/ diff --git a/Source/Internal/DAC.c b/Source/Internal/DAC.c index 7274f3d..f9722f3 100644 --- a/Source/Internal/DAC.c +++ b/Source/Internal/DAC.c @@ -65,6 +65,9 @@ static unsigned int load_state, load_attempts, load_inhibit; static unsigned int load_count_dac, load_count_ramp, load_init_count; /* Объявляет load_zero_sent — данные load_zero_sent; load_stop_asserted — признак установленного выхода STOP; load_initialized — данные load_initialized. */ static unsigned int load_zero_sent, load_stop_asserted, load_initialized; +#if defined(FLASH_BOOT) /* Дополнительный контроль записи Flash не увеличивает обычный EEPROM/RAM-образ. */ +static unsigned int load_last_dac_code; /* Хранит именно последний переданный код SPI, отдельно от доступного для записи диагностического Modbus-регистра. */ +#endif /* Приватное подтверждение передачи используется только автономным Flash-загрузчиком. */ /* Объявляет load_go — начальный ток за вычетом базовых 4 мА, ограниченный диапазоном 0..5; load_end — конечный ток за вычетом 4 мА, ограниченный сверху значением 16. */ static float load_go, load_end; /* 16-битные поля общие с CAN ISR. Снимок и увеличение возраста выполняются @@ -86,6 +89,9 @@ void Load_control_init(void) load_count_dac = 0; load_count_ramp = 0; load_init_count = 1; /* Записывает load_zero_sent значением 0; load_stop_asserted = 0; load_initialized = 0; данные load_zero_sent. */ load_zero_sent = 0; load_stop_asserted = 0; load_initialized = 0; +#if defined(FLASH_BOOT) /* Начальное значение не считается подтверждением безопасных 4 мА без load_zero_sent. */ + load_last_dac_code = 0; /* Явно очищает COFF RAM; первое подтверждение будет получено только после штатного dSEND. */ +#endif /* Завершает инициализацию дополнительного приватного контроля Flash. */ /* Записывает load_go значением 0; load_end = 0; начальный ток за вычетом базовых 4 мА, ограниченный диапазоном 0..5. */ load_go = 0; load_end = 0; /* Записывает load_voltage значением 0; load_voltage_age = 0; load_voltage_seen = 0; последнее выбранное напряжение CAN, масштаб 1 В на слово. */ @@ -205,6 +211,17 @@ static void load_start(void) /* Закрывает текущий блок или список полей; следующие операторы относятся к внешнему уровню. */ } +#if defined(FLASH_BOOT) /* Проверка доступна только отдельному варианту с внутренней Flash и официальным TI API. */ +int Load_flash_safe(void) /* Вызывается с заблокированными IRQ; не меняет команды, GPIO или алгоритм штатной остановки. */ +{ /* Фиксирует согласованное состояние автомата, потому что вызывающий FlashRs_Command не разрешает Timer1 между проверкой и erase. */ + if (Desk != dsk_LOAD) return 1; /* У других плат нет автомата аналоговой нагрузки; их PWM дополнительно блокирует автономный загрузчик. */ + return load_initialized && READY && period_dac && time_dac && load_state == LOAD_IDLE && /* Требует завершённую инициализацию и штатное состояние ожидания вместо RUN/RELOAD/FAULT. */ + load_stop_asserted && load_zero_sent && !IMPowse && /* STOP уже удержан не менее 1000 мс, а безопасный ток действительно передан штатным SPI-путём. */ + !((unsigned int)Commands & 0x03C0U) && !cInitDac && /* START, RESET, калибровка и переинициализация ЦАП не должны создавать отложенные переходы во время прошивки. */ + load_last_dac_code == ((unsigned int)DAC_min() & 0x0FFFU); /* Подтверждает программную передачу текущего кода 4 мА; это не измерение физического тока на плате. */ +} /* После успешной проверки IRQ остаются заблокированы до автономной записи, поэтому разрешение не устаревает. */ +#endif /* Обычная RAM/EEPROM-сборка не включает дополнительный публичный обработчик Flash. */ + /* Вычисляет такты ЦАП; для недопустимого сохранённого тайм-аута CAN возвращает 600 мс. */ void Setup_DAC_time(void) /* Открывает составной блок или описание полей; следующие строки принадлежат этому уровню вложенности. */ @@ -317,6 +334,9 @@ static void anal_output(long vrot, long maxx, int calibration) if(x) dSEND(out|0x0000|0x0000); /* Выполняет альтернативу, если предыдущее условие if оказалось ложным. */ else dSEND(out|0x8000|0x0000); +#if defined(FLASH_BOOT) /* Снимок обновляется только после фактической отправки слова в SPI ЦАП. */ + load_last_dac_code = (unsigned int)out; /* Приватное поле не может быть подменено обычной записью в технологический регистр 0x7B. */ +#endif /* Завершает подтверждение последнего переданного уровня для проверки перед erase. */ /* Публикует фактически отправляемый 12-битный код ЦАП в технологическом регистре 0x7B для диагностики и регистрации. */ modbus[123] = out; diff --git a/Source/Internal/Include/DAC.h b/Source/Internal/Include/DAC.h index ea35170..e6fe1c3 100644 --- a/Source/Internal/Include/DAC.h +++ b/Source/Internal/Include/DAC.h @@ -17,6 +17,9 @@ extern unsigned int period_dac, time_dac; /* Call once before IRQ; complete technological CAN packets update voltage. */ /* Сбрасывает все переменные автомата нагрузки до разрешения прерываний; COFF не обнуляет всю .ebss. */ void Load_control_init(void); +#if defined(FLASH_BOOT) /* Только Flash-загрузчик использует проверку штатного безопасного состояния нагрузки. */ +int Load_flash_safe(void); /* При запрещённых IRQ требует завершённый STOP, снятые команды и отправленный текущий код 4 мА; не выполняет аварийных записей GPIO. */ +#endif /* RAM-вариант сохраняет прежний набор обработчиков и размер кода. */ /* Принимает напряжение выбранной нагрузки из достоверных слов CAN и сбрасывает возраст только нужного измерения. */ void Load_voltage_can(unsigned int header, unsigned int a, /* Объявляет b — данные b. */ diff --git a/Source/Internal/Include/can_telemetry_impl.h b/Source/Internal/Include/can_telemetry_impl.h new file mode 100644 index 0000000..71dd485 --- /dev/null +++ b/Source/Internal/Include/can_telemetry_impl.h @@ -0,0 +1,123 @@ +#ifndef CAN_TELEMETRY_IMPL_H /* Реализация включается только в ecan.c после аппаратных типов и переменных штатного передатчика. */ +#define CAN_TELEMETRY_IMPL_H /* Не допускает повторного определения состояния и функций в одной единице трансляции. */ +#define CAN_HEARTBEAT_ADDR 0x0100U /* Адрес heartbeat из реализации 166 находится вне технологической карты R0–R127 и служебной области логгера 0x1000. */ +#define CAN_HEARTBEAT_TICKS ((Uint32)READY_FREQ) /* READY_FREQ задаёт частоту Timer1 в герцах; это ровно одна секунда его тактов. */ +#define CAN_REFRESH_TICKS ((Uint32)READY_FREQ * 13UL / 10UL) /* Тринадцать десятых секунды дают период повторения данных 1,3 с независимо от числа циклов main. */ +#define CAN_SCAN_TICKS ((Uint32)READY_FREQ * CANPOWSE / 1000UL) /* Существующие CANPOWSE=20 мс ограничивают задержку обнаружения обновлений, но не скорость выдачи уже готовой очереди. */ +#define CAN_ACK_TICKS ((Uint32)READY_FREQ / 10UL) /* Программная страховка через 100 мс отменяет зависший TX, если аппаратный timeout IRQ не был обработан. */ +static Uint32 can_last_hi[ANSWER_LEN], can_last_lo[ANSWER_LEN], can_last_at[ANSWER_LEN]; /* Последний подтверждённый пакет и время ACK хранятся отдельно для каждого начального адреса R0–R127. */ +static unsigned int can_valid[8], can_cursor[2], can_stream; /* Восемь 16-битных масок покрывают 128 адресов; два курсора и очередность обеспечивают справедливость потоков. */ +static Uint32 can_tx_at, can_failed_at, can_heartbeat_at, can_scan_at; /* Беззнаковые отметки Timer1 поддерживают вычитание через переполнение 32-битного счётчика. */ +static unsigned int can_backoff, can_started, can_heartbeat_count, can_heartbeat_due, can_scan_due; /* Отделяет секундную паузу после ошибки, старт часов, heartbeat и незавершённое сканирование масок. */ +static int can_pending_addr; /* Значение -1 обозначает heartbeat, который не должен индексировать кэш технологических регистров. */ +static volatile unsigned int can_tx_failed; /* Timeout ISR отмечает ошибку текущего mailbox до обработки ACK в основном цикле. */ +static Uint32 CAN_millis(void) /* Возвращает согласованный счётчик Timer1; при READY_FREQ=1000 один такт соответствует миллисекунде. */ +{ /* Чтение 32 бит защищается отдельно на 16-битном C28x без ожидания аппаратуры. */ + unsigned int saved = __disable_interrupts(); /* Сохраняет исходное состояние IRQ, включая вызовы из уже защищённой отправки. */ + Uint32 now = CpuTimer1.InterruptCount; /* Счётчик обновляют оба существующих обработчика Timer1 для пульта и измерительной платы. */ + __restore_interrupts(saved); /* Немедленно возвращает прежнюю маску прерываний. */ + return now; /* Наружу передаётся один целостный снимок времени. */ +} /* Длительные расчёты масок выполняются с разрешёнными прерываниями. */ +static void CAN_telemetry_init(void) /* Сбрасывает программное состояние при каждом InitCan, в том числе после запуска с грязной RAM. */ +{ /* Крупные массивы не очищаются, потому что их элементы недоступны без valid-бита. */ + unsigned int i; /* Перебирает восемь масок достоверности кэша. */ + for (i = 0U; i < 8U; ++i) can_valid[i] = 0U; /* Первый пакет каждого разрешённого адреса обязательно передаётся после запуска. */ + can_cursor[0] = can_cursor[1] = can_stream = 0U; /* Обе маски начинают обход с R0; далее потоки чередуются после реально поставленного кадра. */ + can_backoff = can_started = can_heartbeat_count = can_heartbeat_due = 0U; /* Новые часы и секундный heartbeat начинают отсчёт при первом обслуживании main. */ + can_scan_due = 1U; /* Первый снимок данных выдаётся сразу, без ожидания периода 1,3 с. */ + can_pending_addr = -1; can_tx_failed = 0U; wait = 0; /* Исключает подтверждение кадра и ошибку из предыдущего запуска программы. */ +} /* Сброс не меняет аппаратные CAN ID, маски регистров и настройки EEPROM. */ +static int CAN_tx_ready(Uint32 now) /* Обслуживает завершение штатного mailbox 0; вызывающий код уже запретил IRQ на короткое время. */ +{ /* Возвращает готовность без ожидания ACK, CANAA или освобождения шины в цикле. */ + if ((ECanaRegs.CANTRS.all | ECanaRegs.CANTRR.all) & 1UL) { /* Бит 0 относится только к штатному TX; служебный mailbox 2 не перезаписывается. */ + if (wait && !can_tx_failed && (ECanaRegs.CANTRS.all & 1UL) && now - can_tx_at >= CAN_ACK_TICKS) { /* Программный timeout страхует потерянное аппаратное прерывание спустя 100 мс. */ + can_tx_failed = 1U; ++CanTimeOutErrorTR; /* Отмечает ошибку ровно один раз, пока этот запрос ещё ожидает завершения отмены. */ + EALLOW; ECanaRegs.CANTOC.all &= ~1UL; EDIS; /* Снимает источник аппаратного timeout только для mailbox 0, сохраняя остальные биты контроля. */ + ECanaRegs.CANTRR.all = 1UL; /* Запрашивает аппаратную отмену без блокирующего ожидания CANAA. */ + } /* Данные занятого mailbox остаются неизменными до аппаратного освобождения. */ + return 0; /* Текущий адрес и признак готовности сканирования сохраняются для повторного обслуживания. */ + } /* Только свободный mailbox позволяет обработать завершение предыдущей передачи. */ + if (wait) { /* ACK или ошибка относятся ровно к одному ранее запущенному кадру. */ + if ((ECanaRegs.CANTA.all & 1UL) && !can_tx_failed) { /* Только успешный ACK позволяет подавлять повтор неизменившихся данных. */ + if (can_pending_addr >= 0) { /* Heartbeat не занимает ограниченный технологический кэш. */ + can_last_hi[can_pending_addr] = ECanaMboxes.MBOX0.MDH.all; /* Запоминает действительно доставленные адрес и первое слово из неизменявшегося mailbox. */ + can_last_lo[can_pending_addr] = ECanaMboxes.MBOX0.MDL.all; /* Сохраняет второе и третье слова того же подтверждённого кадра. */ + can_last_at[can_pending_addr] = now; /* Отсчитывает повтор через 1,3 с от последнего подтверждения, а не от неудачной попытки. */ + can_valid[can_pending_addr >> 4] |= 1U << (can_pending_addr & 15); /* Устанавливает один бит достоверности начального адреса пакета. */ + } /* Перекрывающиеся пакеты разных масок имеют независимые начальные адреса. */ + } else { /* Отмена, ошибка или свободный TX без CANAA не подтверждают доставку текущих данных. */ + can_failed_at = now; can_backoff = 1U; /* Ограничивает новые попытки одной секундой при отсутствии получателя и CAN ACK. */ + } /* Старый подтверждённый кэш сохраняется, поэтому неудачная передача не теряет обновление. */ + wait = 0; can_tx_failed = 0U; can_pending_addr = -1; /* Завершение обрабатывается один раз даже без отдельного CANAA после отмены. */ + } /* Проверяет паузу после ошибки до подготовки следующего кадра. */ + if (can_backoff && now - can_failed_at < CAN_HEARTBEAT_TICKS) return 0; /* Во время секундной паузы данные и heartbeat остаются ожидающими. */ + can_backoff = 0U; /* Снятие паузы не требует изменения таймеров технологических данных. */ + return 1; /* Mailbox свободен и может принять один новый актуальный кадр. */ +} /* Чужие mailbox, аппаратный CAN ID и получаемые команды не меняются. */ +int CAN_send(int Port, int data[], int Addr) /* Возвращает 0 при занятости, 1 после запуска TX и 2 при неизменном пакете до срока повторения. */ +{ /* Снимок данных и запуск mailbox защищаются одним коротким запретом IRQ. */ + Uint32 hiword, loword, now; /* Ровно два 32-битных поля соответствуют восьми октетам существующего формата BALZAM. */ + unsigned int saved = __disable_interrupts(); /* Исключает разрыв трёхсловного снимка и гонку запуска с timeout ISR. */ + (void)Port; now = CAN_millis(); /* Как прежний передатчик, использует eCAN-A; время берётся из общего Timer1. */ + if (!CAN_tx_ready(now)) { __restore_interrupts(saved); return 0; } /* При занятости ничего не перезаписывает и не подтверждает данные программно. */ + if (Addr == CAN_HEARTBEAT_ADDR) { /* Отдельный пакет состояния узла не обращается к data=NULL или к R256. */ + hiword = 0xE0000000UL | ((Uint32)CAN_HEARTBEAT_ADDR << 16) | can_heartbeat_count; /* Сохраняет маркер BALZAM и передаёт циклический 16-битный счётчик секунд. */ + loword = ((Uint32)(unsigned int)Mode << 16) | (unsigned int)Desk; /* Два следующих слова сообщают аппаратный Mode и тип платы Desk. */ + can_pending_addr = -1; /* Успешный heartbeat не подтверждает ни один технологический пакет. */ + } else { /* Первый снимок, изменение любого слова либо истечение периода вызывают одинаковую отправку данных. */ + if (Addr < 0 || Addr >= ANSWER_LEN) { __restore_interrupts(saved); return 2; } /* Не допускает чтения за картой R0–R127 и позволяет планировщику пропустить неверный адрес. */ + hiword = 0xE0000000UL | ((Uint32)Addr << 16) | ((Uint32)data[Addr] & 0xFFFFUL); /* Первый регистр занимает младшие шестнадцать бит старшей половины CAN-данных. */ + loword = Addr + 1 < ANSWER_LEN ? ((Uint32)data[Addr + 1] & 0xFFFFUL) << 16 : 0UL; /* Отсутствующее второе слово у конца карты заменяется нулём. */ + if (Addr + 2 < ANSWER_LEN) loword |= (Uint32)data[Addr + 2] & 0xFFFFUL; /* Третье слово дополняется только в пределах реального массива регистров. */ + if ((can_valid[Addr >> 4] & (1U << (Addr & 15))) && can_last_hi[Addr] == hiword && can_last_lo[Addr] == loword && now - can_last_at[Addr] < CAN_REFRESH_TICKS) { /* Совпадение всех слов подавляет передачу только до индивидуального срока 1,3 с. */ + __restore_interrupts(saved); return 2; /* Не сдвигает время подтверждения: неизменные данные всё равно повторятся по таймеру. */ + } /* Изменение данных не ждёт окончания периода повторения. */ + can_pending_addr = Addr; /* Подготовленное состояние попадёт в кэш только после успешного аппаратного ACK. */ + } /* Для heartbeat и данных используется один и тот же безопасный жизненный цикл TX. */ + ECanaRegs.CANTA.all = 1UL; ECanaRegs.CANAA.all = 1UL; /* Очищает старые ACK и отмену mailbox 0 записью единиц в соответствующие биты eCAN. */ + ECanaMboxes.MBOX0.MDH.all = hiword; ECanaMboxes.MBOX0.MDL.all = loword; /* Записывает замороженный трёхсловный снимок без последующей подмены во время ожидания ACK. */ + EALLOW; ECanaRegs.CANTOC.all &= ~1UL; ECanaRegs.CANTSC = 0UL; ECanaRegs.CANTOC.all |= 1UL; EDIS; /* Перезапускает аппаратный timeout только для нового штатного запроса TX. */ + can_tx_at = now; can_tx_failed = 0U; wait = 1; /* Сохраняет момент запуска и принадлежность будущего ACK до разрешения прерываний. */ + ECanaRegs.CANTRS.all = 1UL; /* Однократно запускает аппаратную передачу в штатном mailbox 0. */ + led1_toggle(); __restore_interrupts(saved); /* Сохраняет индикацию передачи и возвращает исходную маску IRQ без ожидания шины. */ + return 1; /* Подтверждает только постановку кадра, оставляя окончательную доставку аппаратному ACK. */ +} /* Формат CAN ID, порядок слов и служебный mailbox логгера остаются прежними. */ +void CAN_heartbeat_service(void) /* Обслуживает независимый секундный heartbeat и завершение ранее запущенного TX. */ +{ /* Вызывается из main и не зависит от Cancount, масок данных или признака CanGO. */ + Uint32 now = CAN_millis(), elapsed, seconds; /* Время Timer1 учитывает задержки main без накопления дрейфа секундной сетки. */ + unsigned int saved = __disable_interrupts(); /* Короткая обработка ACK синхронизируется с timeout ISR. */ + CAN_tx_ready(now); __restore_interrupts(saved); /* Сохраняет успешное завершение данных даже тогда, когда новых пакетов пока нет. */ + if (!can_started) { can_started = 1U; can_heartbeat_at = can_scan_at = now; } /* Первый heartbeat появляется спустя полную секунду от начала обслуживания текущего запуска. */ + elapsed = now - can_heartbeat_at; /* Беззнаковое вычитание корректно при переполнении 32-битного счётчика. */ + if (elapsed >= CAN_HEARTBEAT_TICKS) { /* Формирует новую отметку на каждой секундной границе Timer1. */ + seconds = elapsed / CAN_HEARTBEAT_TICKS; can_heartbeat_at += seconds * CAN_HEARTBEAT_TICKS; /* После паузы main оставляет последнее значение без очереди устаревших кадров. */ + can_heartbeat_count = (unsigned int)((can_heartbeat_count + seconds) & 0xFFFFUL); can_heartbeat_due = 1U; /* Счётчик heartbeat циклически проходит диапазон 0..65535 секунд. */ + } /* Занятый передатчик не теряет ожидающий heartbeat. */ + if (can_heartbeat_due && CAN_send(0, 0, CAN_HEARTBEAT_ADDR) == 1) can_heartbeat_due = 0U; /* Приоритетно ставит один актуальный heartbeat после освобождения mailbox. */ +} /* Следующий повтор heartbeat планируется секундной сеткой, а не длительностью обхода масок. */ +void CAN_telemetry_service(void) /* Передаёт изменившиеся данные и повторяет каждый подтверждённый пакет через 1,3 с. */ +{ /* Фоновое обслуживание не ждёт CAN ACK и выдаёт готовую очередь по мере освобождения mailbox. */ + Uint32 now; unsigned int attempt, stream, pass, address, cursor; int result; /* Локальные счётчики ограничивают обход двумя потоками и конечной картой из 128 регистров. */ + CAN_heartbeat_service(); now = CAN_millis(); /* Секундный heartbeat получает приоритет перед обычными данными независимо от настройки потоков. */ + if (now - can_scan_at >= CAN_SCAN_TICKS) { can_scan_at += (now - can_scan_at) / CAN_SCAN_TICKS * CAN_SCAN_TICKS; can_scan_due = 1U; } /* Сохраняет сетку обнаружения изменений с интервалом 20 мс без накопления дрейфа после задержек main. */ + if (!can_scan_due) return; /* При неизменных данных main не повторяет полный обход масок на каждой итерации. */ + for (attempt = 0U; attempt < 2U; ++attempt) { /* После успешной постановки чередует потоки, исключая голодание медленной маски. */ + stream = (can_stream + attempt) & 1U; cursor = can_cursor[stream]; /* Начинает следующий поиск после последнего принятого кадра данного потока. */ + if (!Cancount[stream]) continue; /* Ноль сохраняет штатное отключение соответствующего потока; ненулевой период заменяется общим повтором 1,3 с. */ + for (pass = 0U; pass < 2U; ++pass) { /* Два конечных прохода реализуют циклический обход без зависания на пустой маске. */ + for (address = 0U; address < ANSWER_LEN;) { /* Формирует те же трёхсловные группы от первого разрешённого адреса маски. */ + if (!(Maska[stream][address >> 4] & (1U << (address & 15)))) { ++address; continue; } /* Пропускает запрещённые регистры и гарантированно продвигает поиск. */ + if ((pass == 0U && address >= cursor) || (pass == 1U && address < cursor)) { /* Сначала рассматривает хвост карты после курсора, затем оставшееся начало. */ + result = CAN_send(0, modbus, address); /* Сравнивает полный снимок с подтверждённым кэшем и индивидуальным временем повтора. */ + if (result != 2) { /* Только неизменный пакет до срока допускает переход к следующему кандидату в этом же вызове. */ + if (result == 1) { can_cursor[stream] = address + 3U < ANSWER_LEN ? address + 3U : 0U; can_stream = stream ^ 1U; } /* Продвигает курсор лишь после постановки кадра и отдаёт следующий свободный слот другому потоку. */ + return; /* При занятости сохраняет адрес; при постановке продолжит готовую очередь следующей итерацией main после ACK. */ + } /* Пропущенные неизменные пакеты не занимают физическую шину. */ + } /* Завершает проверку одного допустимого начального адреса. */ + address += 3U; /* Как прежний протокол, один кадр покрывает три последовательных регистра независимо от разреженности маски. */ + } /* Максимальный адрес ограничен размером реальной технологической карты. */ + } /* После обхода обеих частей все пакеты потока рассмотрены ровно один раз. */ + } /* Ни один разрешённый пакет не требует немедленной передачи. */ + can_scan_due = 0U; /* Следующее плановое сканирование произойдёт через 20 мс; выдача готовой очереди этим ожиданием не задерживается. */ +} /* Период 1,3 с относится к каждому пакету, а не к одному кадру всей маски за один период. */ +#endif /* Закрывает единственную реализацию фоновой телеметрии текущей прошивки 166. */ diff --git a/Source/Internal/Include/ecan.h b/Source/Internal/Include/ecan.h index aed1b83..254dcc9 100644 --- a/Source/Internal/Include/ecan.h +++ b/Source/Internal/Include/ecan.h @@ -1,8 +1,12 @@ /* Штатный eCAN и отдельный служебный обмен УМП/GAS. Русские пояснения сохранены в UTF-8. */ /* Настраивает eCAN, скорость, почтовые ящики и маршрутизацию прерываний для выбранного режима платы. */ void InitCan(int Port, int DevNum); -/* Упаковывает адрес и технологические слова в штатный CAN-кадр и запускает передачу mailbox. */ -void CAN_send(int Port, int data[], int Addr); +/* Возвращает 0 при занятости mailbox, 1 при запуске кадра, 2 для неизменного пакета до индивидуального повтора 1,3 с. */ +int CAN_send(int Port, int data[], int Addr); +/* Поддерживает секундный heartbeat независимо от масок и подтверждает доставку предыдущего технологического кадра. */ +void CAN_heartbeat_service(void); +/* Передаёт изменившиеся данные и повторяет подтверждённые пакеты через 1,3 с, не ожидая CAN ACK в главном цикле. */ +void CAN_telemetry_service(void); /* Объявляет CAN_input_data — CAN вход данные. Память определена в другом модуле. */ extern int CAN_input_data[]; diff --git a/Source/Internal/Include/eeprom_config.h b/Source/Internal/Include/eeprom_config.h index f44f213..9d1b2c1 100644 --- a/Source/Internal/Include/eeprom_config.h +++ b/Source/Internal/Include/eeprom_config.h @@ -12,8 +12,8 @@ */ /* Начинает условие: EEPROM_M95M02 ещё не определён; защищает повторное включение или задаёт значение сборки. */ #ifndef EEPROM_M95M02 -/* Задаёт EEPROM_M95M02 = 0; выбор аппаратной EEPROM при сборке: 0 — AT25256B 32 КиБ, 1 — M95M02 256 КиБ и GAS. */ -#define EEPROM_M95M02 0 +/* Основная плата использует M95M02-DR ёмкостью 256 КиБ с областью GAS; старую AT25256B выбирают явно define=0 при отдельной сборке. */ +#define EEPROM_M95M02 1 /* Закрывает условный блок препроцессора; дальнейшие строки относятся к внешнему уровню. */ #endif /* Условие компиляции EEPROM_M95M02 != 0 и EEPROM_M95M02 != 1: выбранный блок включается в текущий вариант прошивки. */ diff --git a/Source/Internal/Include/flash_rs.h b/Source/Internal/Include/flash_rs.h new file mode 100644 index 0000000..51920c6 --- /dev/null +++ b/Source/Internal/Include/flash_rs.h @@ -0,0 +1,5 @@ +#ifndef BALSAM_FLASH_RS_H /* Исключает повторное объявление интерфейса обновления внутренней Flash. */ +#define BALSAM_FLASH_RS_H /* Помечает уже подключённый интерфейс RS-загрузчика. */ +#include "RS485.h" /* Передаёт выбранный SCI-порт и сведения о проверенном блоке из действующего загрузчика. */ +void FlashRs_Command(RS_DATA *rs); /* Выполняет пробу возможностей или запись Flash; после стирания никогда не возвращается в прежний код. */ +#endif /* Завершает защиту заголовка обновления через RS. */ diff --git a/Source/Internal/Include/gas167.h b/Source/Internal/Include/gas167.h index 392dd8e..c19ae6d 100644 --- a/Source/Internal/Include/gas167.h +++ b/Source/Internal/Include/gas167.h @@ -13,7 +13,7 @@ * Дополнительные области GAS присутствуют только при установленной M95M02. */ /* Условие компиляции EEPROM_M95M02: выбранный блок включается в текущий вариант прошивки. */ -#if EEPROM_M95M02 +#if EEPROM_M95M02 || defined(FLASH_BOOT) /* Flash-вариант тоже публикует GAS; физический профиль EEPROM по-прежнему выбирается независимо. */ /* Задаёт GAS167_REG_BASE = 0x5000U; начало псевдонимов 128 технологических регистров GAS, адрес 0x5000. */ #define GAS167_REG_BASE 0x5000U /* Задаёт GAS167_REG_COUNT = 128U; число технологических слов GAS; скрытое слово modbus[128] в диапазон не входит. */ diff --git a/Source/Internal/Include/package.h b/Source/Internal/Include/package.h index b996deb..305fdc3 100644 --- a/Source/Internal/Include/package.h +++ b/Source/Internal/Include/package.h @@ -181,8 +181,8 @@ /* Сохраняет прежний заводской предел пяти пусков для сброса настроек и замены нуля из старой EEPROM. */ #define LOAD_DEFAULT_MAX_ATTEMPTS 5U -/* Задаёт Cancount = (modbus+0x60); данные Cancount. */ -#define Cancount (modbus+0x60) // пауза между I посылками CAN +/* Регистры R96/R97 включают быстрый и медленный потоки CAN: 0 отключает поток, любое ненулевое значение разрешает изменения и индивидуальный повтор каждого пакета через 1,3 с. */ +#define Cancount (modbus+0x60) /* Сохраняет адреса настроек EEPROM и Modbus; интервалы задаёт аппаратное время в can_telemetry_impl.h. */ /* Задаёт Bright = (modbus+0x62); данные Bright. */ #define Bright (modbus+0x62) // аркость сигнальных лампочек /* Задаёт Brightness = modbus[0x62]; данные Brightness. */ diff --git a/Source/Internal/bios.c b/Source/Internal/bios.c index 89eacd5..c218bdb 100644 --- a/Source/Internal/bios.c +++ b/Source/Internal/bios.c @@ -11,6 +11,9 @@ #include "spise2p.h" /* Подключает "log_to_mem.h": двухбанковый регистратор событий нагрузки во внешней RAM. */ #include "log_to_mem.h" +#if defined(FLASH_BOOT) /* Только отдельный Flash-образ содержит официальный алгоритм программирования, выполняемый из RAM. */ +#include "flash_rs.h" /* Передаёт выбранный SCI и сведения о CRC-подтверждённом LOAD в автономный Flash-загрузчик. */ +#endif /* RAM/EEPROM-вариант сохраняет прежний объём без подключения TI Flash API. */ //#include "flash_tools.h" //#include "spartan_tools.h" //#include "big_dsp_module.h" @@ -524,6 +527,20 @@ void xflash(RS_DATA *rs_arr) void tflash(RS_DATA *rs_arr) /* Открывает составной блок или описание полей; следующие строки принадлежат этому уровню вложенности. */ { +#if defined(FLASH_BOOT) /* Разрешает внутреннюю Flash только в образе с отдельными секциями LOAD/RUN и TI API. */ + FlashRs_Command(rs_arr); /* Нулевой запрос проверяет поддержку; ненулевой программирует только полностью проверенную таблицу Flash SCI8. */ +#else /* Обычная прошивка явно сообщает отсутствие Flash-загрузчика вместо молчаливого игнорирования команды 55. */ + unsigned int crc; /* Контрольная сумма защищает адрес, CMD_TFLASH и двухоктетный статус отказа. */ + rs_arr->buffer[0] = rs_arr->addr_recive; /* Ответ относится к фактическому адресу запроса, включая служебный адрес платы. */ + rs_arr->buffer[1] = CMD_TFLASH; /* Сохраняет номер 55 существующего BIOS-протокола и таблицы длин. */ + rs_arr->buffer[2] = 1U; /* Младшая октета статуса 0x1001 означает отсутствие Flash API в RAM/EEPROM-сборке. */ + rs_arr->buffer[3] = 0x10U; /* Старшая октета отличает отказ загрузчика от штатных кодов официального TI API. */ + crc = get_crc_16(0xFFFFU, rs_arr->buffer, 4); /* Использует Modbus CRC16 с начальным 0xFFFF для первых четырёх октет ответа. */ + rs_arr->buffer[4] = LOBYTE(crc); /* CRC передаётся младшей октетой вперёд, как в остальных BIOS-командах. */ + rs_arr->buffer[5] = HIBYTE(crc); /* Завершает защищённую часть кадра старшей октетой CRC. */ + rs_arr->buffer[6] = rs_arr->buffer[7] = 0; /* Сохраняет два исторических завершающих нуля короткого BIOS-ответа. */ + RS_Send(rs_arr, rs_arr->buffer, 8); /* Передаёт явный отказ без обращения к EEPROM, RAM-буферу загрузки или внутренней Flash. */ +#endif /* Завершает выбор обработчика по типу исполняемого образа. */ // volatile unsigned long Address1,Address2; // volatile unsigned long Length, LengthW; /* diff --git a/Source/Internal/ecan.c b/Source/Internal/ecan.c index 1afcb62..3fbf496 100644 --- a/Source/Internal/ecan.c +++ b/Source/Internal/ecan.c @@ -75,6 +75,8 @@ static unsigned int ump_can_waiting; /* Объявляет ump_can_sent_at — время последней передачи фрагмента ответа CAN. */ static unsigned long ump_can_sent_at; +/* Подключает планировщик после объявления wait: оба CAN-канала сохраняют независимое состояние и тайм-ауты. */ +#include "can_telemetry_impl.h" /* Сбрасывает служебный обмен до включения CAN: загрузка COFF не гарантирует обнуление неинициализированной RAM. */ static void ump_can_init(void) /* Начинает сброс состояния; InitCan вызывается при запрещённых глобальных прерываниях. */ @@ -118,6 +120,7 @@ void InitCan(int Port, int DevNum) long id = 0x80BA0000; /* Обнуляет состояние служебного обмена до настройки mailbox и разрешения прерываний CAN. */ + CAN_telemetry_init(); /* Сбрасывает ACK-кэш, курсоры, heartbeat и wait до разрешения прерываний, включая перезапуск с остатками RAM. */ ump_can_init(); /* Изменяет счётчик в выражении DevNum--; на один для следующего шага. */ @@ -295,77 +298,7 @@ void InitCan(int Port, int DevNum) /* Закрывает текущий блок или список полей; следующие операторы относятся к внешнему уровню. */ } -/* Упаковывает адрес и технологические слова в штатный CAN-кадр и запускает передачу mailbox. */ -void CAN_send(int Port, int data[], int Addr) -/* Открывает составной блок или описание полей; следующие строки принадлежат этому уровню вложенности. */ -{ - /* Объявляет hiword — данные hiword; loword — данные loword. */ - unsigned long hiword,loword; - /* Продолжает список аргументов, полей или выражение volatile struct ECAN_REGS * ECanRegs;; порядок элементов сохраняется. */ - volatile struct ECAN_REGS * ECanRegs; - /* Продолжает список аргументов, полей или выражение volatile struct ECAN_MBOXES * ECanMboxes;; порядок элементов сохраняется. */ - volatile struct ECAN_MBOXES * ECanMboxes; - - /* Записывает ECanRegs значением &ECanaRegs; E CAN Regs. */ - ECanRegs = &ECanaRegs; - /* Записывает ECanMboxes значением &ECanaMboxes; E CAN Mboxes. */ - ECanMboxes = &ECanaMboxes; - - /* Не меняет данные mailbox 0, пока передача либо её аппаратная отмена ещё выполняется; бит 0 соответствует штатному TX. */ - if ((ECanRegs->CANTRS.all | ECanRegs->CANTRR.all) & 1UL) - return; /* Сохраняет текущий кадр до завершения CANTRS/CANTRR, не ожидая аппаратного ответа в главном цикле. */ - - /* Проверяет if(wait); последующий оператор выполняется только при истинном условии. */ - if(wait) - /* Проверяет if(!(ECanRegs->CANTA.all & 1)); последующий оператор выполняется только при истинном условии. */ - if(!(ECanRegs->CANTA.all & 1)) - /* Проверяет if(!(ECanRegs->CANAA.all & 1)); последующий оператор выполняется только при истинном условии. */ - if(!(ECanRegs->CANAA.all & 1)) - /* Завершает CAN_send без возвращаемого значения. */ - return; - - /* Записывает ECanRegs->CANTA.all значением 1; представление регистра целым словом без разбиения на битовые поля. */ - ECanRegs->CANTA.all = 1; - /* Записывает ECanRegs->CANAA.all значением 1; представление регистра целым словом без разбиения на битовые поля. */ - ECanRegs->CANAA.all = 1; - - /* Записывает hiword значением ((((Uint32) Addr ) & 0xffff)<<16)| 0xE0000000 |; данные hiword. */ - hiword= ((((Uint32) Addr ) & 0xffff)<<16)| 0xE0000000 | - /* Продолжает список аргументов, полей или выражение ((((Uint32)data[Addr ]) & 0xffff) );; порядок элементов сохраняется. */ - ((((Uint32)data[Addr ]) & 0xffff) ); - /* Записывает loword значением ((((Uint32)data[Addr+1]) & 0xffff)<<16)|; данные loword. */ - loword= ((((Uint32)data[Addr+1]) & 0xffff)<<16)| - /* Продолжает список аргументов, полей или выражение ((((Uint32)data[Addr+2]) & 0xffff) );; порядок элементов сохраняется. */ - ((((Uint32)data[Addr+2]) & 0xffff) ); - - /* Записывает ECanMboxes->MBOX0.MDH.all значением hiword; представление регистра целым словом без разбиения на битовые поля. */ - ECanMboxes->MBOX0.MDH.all = hiword; - /* Записывает ECanMboxes->MBOX0.MDL.all значением loword; представление регистра целым словом без разбиения на битовые поля. */ - ECanMboxes->MBOX0.MDL.all = loword; - - /* Разрешение записи в защищённые регистры периферии. */ - EALLOW; - /* Отключает старый контроль mailbox 0 перед перезапуском счётчика; остальные mailbox используют собственный контроль. */ - ECanRegs->CANTOC.all = 0; - /* Записывает ECanRegs->CANTSC значением 0; E CAN Regs CANTSC. */ - ECanRegs->CANTSC = 0; // clear time-out counter - /* Заново разрешает тайм-аут штатного TX с порогом MOTO0, установленным в InitCan, только для следующего кадра. */ - ECanRegs->CANTOC.all = 1UL; - /* Закрытие доступа к защищённым регистрам периферии. */ - EDIS; - - /* Записывает ECanRegs->CANTRS.all значением 1; представление регистра целым словом без разбиения на битовые поля. */ - ECanRegs->CANTRS.all = 1; // запустить передачу - - /* Записывает wait значением 1; данные wait. */ - wait=1; - /* Вызывает led1_toggle с аргументами () в текущей последовательности операций. */ - led1_toggle(); -/* Закрывает текущий блок или список полей; следующие операторы относятся к внешнему уровню. */ -} - - -/* Начинает функцию Handlai; назначение модуля — штатный eCAN и отдельный служебный обмен УМП/GAS. */ +/* Разделяет служебные запросы УМП и технологические команды CAN, сохраняя защиту нагрузки и журналирование версии 167. */ void Handlai(volatile struct ECAN_MBOXES * ECanMboxes) /* Открывает составной блок или описание полей; следующие строки принадлежат этому уровню вложенности. */ { @@ -497,10 +430,11 @@ interrupt void CANa_reset_err(void) if (ECanaRegs.CANTOS.all & 1UL) { /* Обрабатывает только штатный передатчик; служебный mailbox 2 контролируется отдельно в UMP_CAN_service. */ EALLOW; /* Разрешает изменение управляющего регистра eCAN перед снятием источника прерывания. */ - ECanaRegs.CANTOC.all = 0; /* Сброс TOC снимает TOS и MTOF согласно SPRUI07, 13.8.18.3; одной отмены TX недостаточно. */ + ECanaRegs.CANTOC.all &= ~1UL; /* Снимает источник timeout только MBOX0 по SPRUI07 13.8.18.3; остальные каналы сохраняют свой контроль. */ EDIS; /* Восстанавливает защиту регистров до выхода из ветви обработки тайм-аута. */ if (ECanaRegs.CANTRS.all & 1UL) /* Передача могла завершиться между чтением TOS и снятием контроля. */ ECanaRegs.CANTRR.all = 1UL; /* Запрашивает отмену только ещё занятого mailbox 0, не задерживая прерывание ожиданием CANAA. */ + can_tx_failed = 1U; /* Не позволяет отложенному ACK после тайм-аута подтвердить недоставленный снимок и подавить повтор данных. */ CanTimeOutErrorTR++; /* Считает обнаруженный тайм-аут один раз, исключая фантомные повторные входы PIE. */ } /* Следующая штатная передача сама заново включит TOC после сброса временного счётчика. */ @@ -575,7 +509,7 @@ void UMP_CAN_service(void) /* Записывает ump_can_length значением Load_protection_prepare(tag & 0xFF, address, value, ump_can_reply, &error); ump CAN длина. */ ump_can_length = Load_protection_prepare(tag & 0xFF, address, value, ump_can_reply, &error); /* Условие компиляции EEPROM_M95M02: выбранный блок включается в текущий вариант прошивки. */ -#if EEPROM_M95M02 +#if EEPROM_M95M02 || defined(FLASH_BOOT) /* Отдельный Flash-вариант использует тот же GAS provider и согласованный снимок служебного CAN. */ /* Проверяет else if (gas167_contains(address)); последующий оператор выполняется только при истинном условии. */ else if (gas167_contains(address)) /* Записывает ump_can_length значением gas167_prepare(tag & 0xFF, address, value, 1, ump_can_reply, &error); ump CAN длина. */ diff --git a/Source/Internal/flash_codestart.asm b/Source/Internal/flash_codestart.asm new file mode 100644 index 0000000..b69fe2f --- /dev/null +++ b/Source/Internal/flash_codestart.asm @@ -0,0 +1,12 @@ + .ref _c_int00 ; Стандартный COFF startup применяет .cinit и вызывает main после отключения раннего watchdog. + .sect "codestart" ; Boot ROM передаёт управление двухсловному переходу по аппаратному адресу слова 0x33FFF6. + LB flash_boot_start ; Переходит к короткой подготовке C28x во Flash, сохраняя точный размер аппаратного входа два слова. + .text ; Последовательность взята из штатного TI DSP2833x_CodeStartBranch.asm и размещается в секторе основного кода. +flash_boot_start: ; Отключает watchdog до возможной длительной обработки таблиц инициализации C, а не только после входа в main. + SETC OBJMODE ; Выбирает C28x object mode, требуемый стандартным COFF startup процессора F28335. + EALLOW ; Разрешает запись защищённого регистра WDCR перед запуском C runtime. + MOVZ DP, #7029h>>6 ; Адрес слова WDCR=0x7029 из карты F28335; DP выбирает соответствующую страницу по 64 слова. + MOV @7029h, #0068h ; Значение 0x0068 содержит контрольные биты WDCHK=101 и WDDIS=1, как официальный TI startup. + EDIS ; Возвращает защиту системных регистров перед переходом в библиотечную инициализацию. + LB _c_int00 ; Применяет штатную ROM-модель инициализации и затем запускает текущий main 167. + .end ; Завершает модуль, не добавляя произвольных данных рядом с CSM-паролями сектора A. diff --git a/Source/Internal/flash_rs.c b/Source/Internal/flash_rs.c new file mode 100644 index 0000000..fd78ec9 --- /dev/null +++ b/Source/Internal/flash_rs.c @@ -0,0 +1,179 @@ +#if defined(FLASH_BOOT) /* Подключает Flash API только к образу, исполняемому из внутренней Flash. */ +#include "DSP2833x_Device.h" /* Предоставляет регистры SCI, watchdog, CSM и действующую частоту SYSCLKOUT в герцах. */ +#include "flash_rs.h" /* Связывает алгоритм с существующим загрузчиком RAM и выбранным RS-портом. */ +#include "bios_dsp.h" /* Использует неизменный номер CMD_TFLASH действующего BIOS-протокола. */ +#include "DAC.h" /* Проверяет завершённую штатную остановку нагрузки до отключения Timer1 на время стирания. */ +#include "Flash2833x_API_Library.h" /* Вызывает официальный алгоритм TI v2.10 для стирания, записи и проверки F28335. */ +#include /* Переносит код и константы Flash API во внутреннюю RAM до изменения Flash. */ +extern Uint16 FlashRsLoadStart, FlashRsLoadEnd, FlashRsRunStart; /* Границы отдельной секции задаёт F28335_flash.cmd в адресуемых 16-битных словах. */ +static Uint16 program_buffer[128]; /* Буфер на 128 слов хранится во внутренней RAM; он не зависит от стираемых секторов или порядка байтов RS. */ +static FLASH_ST api_status; /* Внутренняя RAM сохраняет адрес и данные первой ошибки официального Flash API. */ +static unsigned int flash_load_guard(unsigned long length) /* Проверяет разрешение записи до CRC/erase, сохраняя безопасную пробу поддержки с нулевой длиной. */ +{ /* Делегирует состояние нагрузки её единственному владельцу, не дублируя управление GPIO внутри загрузчика. */ + return length && !Load_flash_safe() ? 0x1005U : 0U; /* Статус 0x1005 требует штатного STOP и подтверждённых 4 мА; probe длины ноль никогда не останавливает работу нагрузки. */ +} /* После положительной проверки вызывающий код удерживает IRQ заблокированными до автономной записи. */ +#pragma CODE_SECTION(byte_at, "flash_rs") /* Размещает декодер упакованных RS-байтов в копируемой секции RAM. */ +static Uint16 byte_at(const Uint16 *data, Uint32 offset) /* Читает октет из старшего байта чётного слова и младшего байта нечётного, как RS_RX_Handler. */ +{ /* Начинает независимый от Flash декодер входного блока. */ + return (offset & 1UL) ? (data[offset >> 1] & 255U) : (data[offset >> 1] >> 8); /* Преобразует упаковку RS в октеты без обращения к таблицам или runtime. */ +} /* Завершает чтение одного октета. */ +#pragma CODE_SECTION(word_at, "flash_rs") /* Делает декодирование формата SCI8 доступным после стирания Flash. */ +static Uint16 word_at(const Uint16 *data, Uint32 offset) /* Читает слово TI boot table, в котором младший октет передаётся первым. */ +{ /* Начинает объединение двух октетов. */ + return byte_at(data, offset) | (byte_at(data, offset + 1UL) << 8); /* Восстанавливает 16-битное слово, сохраняя отличие от внутренней упаковки RS. */ +} /* Завершает чтение слова SCI8. */ +#pragma CODE_SECTION(long_at, "flash_rs") /* Декодирует адреса без обращения к коду стираемой программы. */ +static Uint32 long_at(const Uint16 *data, Uint32 offset) /* Формат TI передаёт старшее 16-битное слово адреса перед младшим. */ +{ /* Начинает чтение 32-битного адреса в словах C28x. */ + return ((Uint32)word_at(data, offset) << 16) | word_at(data, offset + 2UL); /* Объединяет два слова без зависимости от порядка байтов процессора ПК. */ +} /* Завершает декодирование адреса. */ +#pragma CODE_SECTION(crc_byte, "flash_rs") /* Вычисляет CRC без таблицы crc16, расположенной в стираемом секторе G. */ +static Uint16 crc_byte(Uint16 crc, Uint16 value) /* Продвигает стандартный CRC16 Modbus, используемый существующим RS-протоколом. */ +{ /* Начинает обработку одного октета сообщения. */ + Uint16 bit; /* Счётчик восьми битов октета не требует библиотечной арифметики. */ + crc ^= value; /* Добавляет младший октет к состоянию отражённого CRC. */ + for (bit = 0; bit < 8U; ++bit) /* Полином применяется ровно к восьми передаваемым битам. */ + crc = (crc & 1U) ? ((crc >> 1) ^ 0xA001U) : (crc >> 1); /* 0xA001 — отражённый полином CRC16 Modbus, совпадающий с проверкой GUI. */ + return crc; /* Возвращает состояние без вызовов функции из Flash. */ +} /* Завершает шаг CRC. */ +#pragma CODE_SECTION(reply_status, "flash_rs") /* Передаёт итог даже после удаления старой программы и её ISR. */ +static void reply_status(RS_DATA *rs, Uint16 status) /* Ответ содержит адрес, команду, 16-битный статус и CRC; SCI и RS485 обслуживаются опросом. */ +{ /* Начинает автономную передачу подтверждения из RAM. */ + Uint16 frame[8], crc = 0xFFFFU, i; /* Шесть октетов кадра и два завершающих нуля соответствуют традиционным ответам BIOS. */ + volatile Uint32 guard; /* Ограничивает ожидание SCI, чтобы отказ передатчика не блокировал завершение навсегда. */ + frame[0] = rs->addr_recive; /* Отвечает на фактический адрес запроса, поддерживая обычный и сервисный адрес платы. */ + frame[1] = CMD_TFLASH; /* Номер 55 сохраняет совместимость с таблицей длин существующего загрузчика. */ + frame[2] = status & 255U; /* Передаёт младший октет результата TI API либо ошибки проверки образа. */ + frame[3] = status >> 8; /* Передаёт старший октет статуса для различения ошибок загрузчика и API. */ + for (i = 0; i < 4U; ++i) crc = crc_byte(crc, frame[i]); /* Контрольная сумма защищает и адрес, и конкретный результат записи. */ + frame[4] = crc & 255U; /* CRC Modbus передаёт младший октет первым. */ + frame[5] = crc >> 8; /* Передаёт старший октет контрольной суммы. */ + frame[6] = frame[7] = 0; /* Сохраняет два завершающих нуля действующего RS-протокола. */ + rs->SciRegs->SCICTL2.bit.TXINTENA = 0; /* Передача не должна вызывать ISR из удаляемой Flash. */ + rs->SciRegs->SCICTL2.bit.RXBKINTENA = 0; /* Приём во время итогового ответа также не обращается к старому обработчику. */ + rs->SciRegs->SCIFFTX.bit.TXFFIENA = 0; /* Отключает FIFO-прерывание для автономной передачи опросом. */ + rs->SciRegs->SCIFFRX.bit.RXFFIENA = 0; /* Исключает FIFO RX ISR до восстановления обычного приёма или перезапуска. */ + rs->SciRegs->SCICCR.bit.PARITYENA = 0; /* Ответ передаётся в режиме 8N1, принятом в обычном подтверждении BIOS. */ + if (rs->commnumber == COM_2) GpioDataRegs.GPBDAT.bit.GPIO34 = 0; /* На этой плате низкий GPIO34 включает передатчик RS485, как RS_Line_to_send. */ + for (i = 0; i < 8U; ++i) /* Последовательно отправляет кадр и завершающие байты без прерываний. */ + { /* Начинает ожидание свободного места в передатчике. */ + guard = 10000000UL; /* Десять миллионов опросов дают запас на восемь байтов при штатных скоростях RS. */ + while (!rs->SciRegs->SCICTL2.bit.TXRDY && --guard) {} /* Не переписывает SCITXBUF, пока предыдущее слово не принято передатчиком. */ + if (!guard) break; /* Завершает попытку при неисправном или отключённом SCI. */ + rs->SciRegs->SCITXBUF = frame[i]; /* Передаёт один восьмибитный октет по выбранному SCI-A или SCI-B. */ + } /* Завершает отправку кадра. */ + guard = 10000000UL; /* Ограничивает ожидание ухода последнего стопового бита тем же запасом. */ + while ((rs->SciRegs->SCIFFTX.bit.TXFFST || !rs->SciRegs->SCICTL2.bit.TXEMPTY) && --guard) {} /* Для FIFO SCI-A ждёт также опустошения очереди, чтобы не потерять конец кадра при выключении RS485 или reset. */ + if (rs->commnumber == COM_2) GpioDataRegs.GPBDAT.bit.GPIO34 = 1; /* Возвращает направление RS485 к приёму после завершения кадра. */ +} /* Завершает автономный ответ. */ +#pragma CODE_SECTION(validate_image, "flash_rs") /* Проверка таблицы также находится в RAM и не зависит от Flash API. */ +static Uint16 validate_image(const Uint16 *data, Uint32 length) /* Проверяет весь образ до первого стирания, включая загрузочный переход и запрещённые адреса CSM. */ +{ /* Начинает проверку структуры таблицы TI SCI8. */ + Uint32 offset = 22UL, address, bytes, entry_address, prior, prior_address; /* Заголовок TI занимает 11 слов; дополнительные курсоры проверяют перекрытия и наличие данных по адресу входа. */ + Uint16 count, boot = 0, entry = 0, prior_count; /* Требует единственный boot-переход и хотя бы один блок, содержащий точку входа C runtime. */ + if (length < 24UL || (length & 1UL) || word_at(data, 0) != 0x08AAU) return 0x1002U; /* Ключ SCI8 и чётный размер исключают обычный плоский EEPROM BIN. */ + address = long_at(data, 18UL); /* Адрес входа следует после восьми резервных слов заголовка TI. */ + entry_address = address; /* Сохраняет адрес для проверки наличия соответствующих исполняемых слов в таблице. */ + if (address < 0x300000UL || address >= 0x33FF80UL) return 0x1002U; /* Точка входа C runtime обязана находиться в исполняемой Flash вне CSM. */ + while (offset + 2UL <= length) /* Читает блоки только в пределах подтверждённого загрузчиком массива. */ + { /* Начинает проверку очередного блока. */ + count = word_at(data, offset); /* Размер блока выражен в 16-битных словах C28x, а не октетах RS. */ + offset += 2UL; /* Продвигает курсор за поле длины. */ + if (!count) return (offset == length && boot == 1U && entry) ? 0U : 0x1002U; /* Нулевой блок завершает образ; хвост, отсутствие boot-перехода или данных C runtime запрещены. */ + if (offset + 4UL > length) return 0x1002U; /* Не читает обрезанный 32-битный адрес назначения. */ + address = long_at(data, offset); /* Читает адрес в формате старшее слово, затем младшее. */ + offset += 4UL; /* Переводит курсор к первому слову данных блока. */ + bytes = (Uint32)count << 1; /* Каждое слово программы соответствует двум октетам файла. */ + if (bytes > length - offset) return 0x1002U; /* Проверяет остаток файла без возможности выхода за границы массива. */ + if (address == 0x33FFF6UL && count == 2U) ++boot; /* Единственное исключение рядом с CSM — аппаратный переход из двух слов. */ + else if (address < 0x300000UL || address >= 0x33FF80UL || (Uint32)count > 0x33FF80UL - address) return 0x1002U; /* Запрещает OTP, RAM, пароли CSM и зарезервированный диапазон сектора A. */ + if (entry_address >= address && entry_address < address + count) entry = 1U; /* Подтверждает, что после boot-перехода процессор получит код из текущего образа. */ + prior = 22UL; /* Начинает сравнение диапазона с уже проверенными блоками таблицы. */ + while (prior < offset - 6UL) /* Повторно рассматривает только заголовки, прошедшие проверки границ выше. */ + { /* Начинает проверку одного предыдущего диапазона. */ + prior_count = word_at(data, prior); /* Предыдущая длина уже подтверждена и не может вывести курсор за массив. */ + prior_address = long_at(data, prior + 2UL); /* Читает ранее проверенный Flash-адрес. */ + if (address < prior_address + prior_count && prior_address < address + count) return 0x1002U; /* Не разрешает повторную запись слова Flash из разных блоков даже при корректном CRC. */ + prior += 6UL + ((Uint32)prior_count << 1); /* Пропускает предыдущий заголовок и все его октеты данных. */ + } /* Завершает сравнение с предыдущими секциями. */ + offset += bytes; /* Переходит к следующему полностью проверенному блоку. */ + } /* Завершает обход блоков. */ + return 0x1002U; /* Отсутствие нулевого терминатора делает образ неполным. */ +} /* Завершает проверку адресов и структуры. */ +#pragma CODE_SECTION(program_image, "flash_rs") /* Весь участок от стирания до завершения выполняется из внутренней RAM. */ +static void program_image(RS_DATA *rs, const Uint16 *data, Uint32 length) /* Никогда не возвращается в удалённую программу после первого вызова Flash_Erase. */ +{ /* Начинает автономный алгоритм записи и проверки. */ + Uint32 offset = 22UL, address, boot_offset = 0UL; /* Хранит курсоры данных и отложенного загрузочного перехода во внутренней RAM. */ + Uint16 count, chunk, i, status; /* Состояние API и размеры блоков не зависят от внешних таблиц. */ + status = Flash_Erase(0x00FFU, &api_status); /* Маска TI содержит биты A–H; обновление заменяет весь исполняемый образ, параметры внешней EEPROM сохраняются. */ + while (!status && offset < length) /* Программирует блоки только после успешного стирания всех секторов. */ + { /* Начинает обработку очередной секции изображения. */ + count = word_at(data, offset); /* Использует размер, уже проверенный до входа в критический участок. */ + offset += 2UL; /* Пропускает поле количества слов. */ + if (!count) break; /* Нулевой блок означает конец файла TI. */ + address = long_at(data, offset); /* Восстанавливает Flash-адрес текущей секции. */ + offset += 4UL; /* Переводит курсор к полезным словам секции. */ + if (address == 0x33FFF6UL) { boot_offset = offset; offset += 4UL; continue; } /* Откладывает boot-переход до успешной записи и проверки остальных секций. */ + while (count && !status) /* Делит большие секции на буферные порции без переполнения RAM. */ + { /* Начинает запись очередной порции. */ + chunk = count > 128U ? 128U : count; /* Ограничение совпадает с размером внутреннего программирующего буфера в словах. */ + for (i = 0; i < chunk; ++i) program_buffer[i] = word_at(data, offset + ((Uint32)i << 1)); /* Распаковывает байты RS в слова, ожидаемые Flash API. */ + status = Flash_Program((Uint16 *)address, program_buffer, chunk, &api_status); /* Использует официальный алгоритм записи F28335 вместо обычной записи указателем. */ + if (!status) status = Flash_Verify((Uint16 *)address, program_buffer, chunk, &api_status); /* Подтверждает записанные слова чтением из Flash до перехода к следующей порции. */ + offset += (Uint32)chunk << 1; /* Продвигает курсор файла на фактически обработанные октеты. */ + address += chunk; /* Продвигает Flash-адрес на количество адресуемых слов C28x. */ + count -= chunk; /* Уменьшает остаток текущей секции. */ + } /* Завершает запись секции. */ + } /* Завершает запись обычных блоков. */ + if (!status && boot_offset) /* Разрешает запуск нового образа только после проверки всех предыдущих секций. */ + { /* Начинает фиксацию аппаратной точки входа. */ + program_buffer[0] = word_at(data, boot_offset); /* Восстанавливает первое слово инструкции длинного перехода. */ + program_buffer[1] = word_at(data, boot_offset + 2UL); /* Восстанавливает второе слово адреса перехода. */ + status = Flash_Program((Uint16 *)0x33FFF6UL, program_buffer, 2UL, &api_status); /* Записывает boot-переход последним, исключая запуск неполного образа. */ + if (!status) status = Flash_Verify((Uint16 *)0x33FFF6UL, program_buffer, 2UL, &api_status); /* Проверяет обе части аппаратной точки входа. */ + } /* Завершает фиксацию загрузочного перехода. */ + reply_status(rs, status); /* Отправляет конкретный результат после записи и аппаратной проверки. */ + if (!status) { EALLOW; SysCtrlRegs.WDCR = 0x0028U; EDIS; } /* Успех включает watchdog для аппаратного перезапуска; режим загрузки задают GPIO84–87. */ + for (;;) {} /* Не возвращается в удалённый код; при успехе watchdog перезапустит устройство, при ошибке требуется восстановление. */ +} /* Завершает автономный алгоритм. */ +void FlashRs_Command(RS_DATA *rs) /* До стирания проверяет исходный блок и копирует все необходимые функции во внутреннюю RAM. */ +{ /* Начинает обработку команды действующего загрузчика. */ + Uint32 source, length, destination, i; /* Адреса и длина команды передаются младшим октетом вперёд в BIOS-протоколе. */ + Uint16 status = 0U, crc, saved; /* Сохраняет код API, CRC и прежнюю маску IRQ для безопасного возврата после пробы либо отказа до стирания. */ + source = (Uint32)rs->RS_Header[6] | ((Uint32)rs->RS_Header[7] << 8) | ((Uint32)rs->RS_Header[8] << 16) | ((Uint32)rs->RS_Header[9] << 24); /* Поле источника задаёт адрес ранее загруженного блока RAM. */ + length = (Uint32)rs->RS_Header[10] | ((Uint32)rs->RS_Header[11] << 8) | ((Uint32)rs->RS_Header[12] << 16) | ((Uint32)rs->RS_Header[13] << 24); /* Поле длины выражено в октетах файла, как команда INITLOAD. */ + destination = (Uint32)rs->RS_Header[2] | ((Uint32)rs->RS_Header[3] << 8) | ((Uint32)rs->RS_Header[4] << 16) | ((Uint32)rs->RS_Header[5] << 24); /* Адрес 0x300000 подтверждает назначение команды внутренней Flash. */ + memcpy(&FlashRsRunStart, &FlashRsLoadStart, &FlashRsLoadEnd - &FlashRsLoadStart); /* Копирует функции, константы и библиотеку TI до возможного стирания их загрузочного сектора. */ + saved = __disable_interrupts(); /* Не позволяет CAN/таймеру регистратора изменить внешний RAM-буфер между повторной проверкой CRC и передачей управления автономному загрузчику. */ + if (CsmRegs.CSMSCR.bit.SECURE) status = 0x1004U; /* Защищённое устройство не должно переходить к стиранию без корректного разблокирования CSM. */ + if (!status) status = flash_load_guard(length); /* Активная/неостановленная нагрузка получает отказ до доступа к Flash API; GPIO и уставку меняет только штатный автомат Timer1. */ + if (!status && (source || length || destination)) /* Нулевая тройка параметров — только проверка поддержки, без доступа к буферу и стирания. */ + { /* Начинает проверку команды программирования. */ + if (destination != 0x300000UL || source < 0x200000UL || source >= 0x20FC00UL || length < 24UL || length > ((0x20FC00UL - source) << 1) - 2UL || !rs->BS_LoadOK || (Uint32)rs->pRecvPtr != source || rs->RS_Length != length + 2UL) status = 0x1003U; /* Принимает лишь CRC-подтверждённый блок в штатной внешней RAM, включая место для принятого CRC. */ + if (!status) /* Перепроверяет байты буфера после LOAD, защищая от изменения RAM до стирания. */ + { /* Начинает повторное вычисление CRC исходного кадра LOAD. */ + crc = crc_byte(crc_byte(0xFFFFU, rs->addr_recive), CMD_LOAD); /* CRC включает тот же адрес и номер команды, что и первоначальная передача блока. */ + for (i = 0; i < length; ++i) crc = crc_byte(crc, byte_at((Uint16 *)source, i)); /* Проверяет каждую полезную октету без таблицы из Flash. */ + if (crc != word_at((Uint16 *)source, length)) status = 0x1003U; /* Запрещает стирание, если блок RAM отличается от подтверждённого загрузчиком. */ + else status = validate_image((Uint16 *)source, length); /* Проверяет все адреса, завершённость и аппаратный переход до изменения Flash. */ + } /* Завершает проверки буфера. */ + } /* Завершает предварительную проверку команды. */ + if (!status && length) /* Входит в автономный участок только для полностью проверенной ненулевой записи. */ + { /* Начинает остановку обычного приложения перед обновлением. */ + Flash_CPUScaleFactor = (Uint32)(1048576.0L * (200.0L * (long double)SYSCLKOUT / 1000000000.0L)); /* Формула TI IQ20 использует реальный SYSCLKOUT в герцах вместо ошибочного фиксированного значения 150 МГц. */ + Flash_CallbackPtr = 0; /* Исключает callback из стираемой программы во время операции Flash API. */ + DINT; /* Запрещает все обычные ISR, размещённые в обновляемой Flash. */ + EALLOW; /* Разрешает запись защищённых регистров watchdog и аварийного отключения PWM. */ + SysCtrlRegs.WDCR = 0x0068U; /* Отключает watchdog на время стирания, которое может длиться несколько секунд. */ + EPwm1Regs.TZCTL.all = EPwm2Regs.TZCTL.all = EPwm3Regs.TZCTL.all = EPwm4Regs.TZCTL.all = EPwm5Regs.TZCTL.all = EPwm6Regs.TZCTL.all = 0x000AU; /* Коды TZA=2 и TZB=2 переводят оба PWM-выхода в низкий уровень при аварийном отключении. */ + EPwm1Regs.TZFRC.bit.OST = EPwm2Regs.TZFRC.bit.OST = EPwm3Regs.TZFRC.bit.OST = EPwm4Regs.TZFRC.bit.OST = EPwm5Regs.TZFRC.bit.OST = EPwm6Regs.TZFRC.bit.OST = 1; /* Принудительно блокирует PWM до перезапуска новой прошивки. */ + EDIS; /* Завершает доступ к защищённым регистрам перед вызовом TI API. */ + program_image(rs, (Uint16 *)source, length); /* После первого стирания управление остаётся в RAM до ответа и перезапуска. */ + } /* Завершает путь программирования. */ + reply_status(rs, status); /* Проба поддержки и ошибки до стирания возвращают короткий проверяемый ответ. */ + RS_SetBitMode(rs, 9); /* Восстанавливает стандартный приём BIOS после безопасного отказа или пробы возможностей. */ + rs->SciRegs->SCICTL2.bit.RXBKINTENA = 1; /* Возвращает обычное обслуживание следующих запросов RS. */ + if (rs->commnumber == COM_1) rs->SciRegs->SCIFFRX.bit.RXFFIENA = 1; /* SCI-A в существующем драйвере принимает данные через FIFO-прерывание. */ + __restore_interrupts(saved); /* При пробе или отказе до стирания возвращает ровно прежнее состояние прерываний приложения. */ +} /* Завершает команду до стирания; после стирания эта функция не получает управление. */ +#endif /* Обычный EEPROM/RAM-образ не включает объёмную библиотеку программирования Flash. */ diff --git a/Source/Internal/gas167.c b/Source/Internal/gas167.c index 2e2050a..ce8bb77 100644 --- a/Source/Internal/gas167.c +++ b/Source/Internal/gas167.c @@ -2,7 +2,7 @@ /* Подключает границы GAS и общий выбор EEPROM; при профиле 0 интерфейс GAS исключён. */ #include "gas167.h" /* Весь дополнительный код включается только для физически установленной M95M02; вариант AT25256B не растёт. */ -#if EEPROM_M95M02 +#if EEPROM_M95M02 || defined(FLASH_BOOT) /* Отдельный Flash-вариант включает ту же карту GAS даже при совместимой сборке с AT25256B. */ /* Подключает тип платы dsk_LOAD и псевдонимы регистров, чтобы сохранять правила обычного Modbus. */ #include "package.h" @@ -18,7 +18,12 @@ static const uint16_t gas167_info[GAS167_INFO_WORDS] = { /* Слова 0..3: сигнатура GA/S1, версия профиля и номер проекта 167. */ 0x4741U, 0x5331U, GAS167_PROFILE_VERSION, 167U, /* Слова 4..7: база технологических регистров, число слов, маска RTU/CAN/FC06 и признак EEPROM M95M02 = 1; последний элемент описывает аппаратный профиль. */ - GAS167_REG_BASE, GAS167_REG_COUNT, GAS167_CAPABILITIES, 1U + GAS167_REG_BASE, GAS167_REG_COUNT, GAS167_CAPABILITIES, /* Первые три поля второй половины дескриптора сохраняют базу, длину и возможности общего GAS 167/v1. */ +#if defined(FLASH_BOOT) /* Код исполнения отделяет Flash от загрузки кода из внешней EEPROM без изменения адресов GAS. */ + 2U /* Значение 2 описывает исполнение из внутренней Flash; физическая EEPROM остаётся заданной EEPROM_M95M02. */ +#else /* EEPROM/RAM-вариант M95M02 сохраняет прежний аппаратный признак дескриптора. */ + 1U /* Значение 1 соответствует прежнему GAS-профилю M95M02 и совместимо с существующими клиентами. */ +#endif /* Завершает выбор только последнего слова дескриптора; технологические регистры остаются общими. */ /* Закрывает текущий блок или список полей; следующие операторы относятся к внешнему уровню. */ }; diff --git a/Source/Internal/main.c b/Source/Internal/main.c index 9ecc4c9..9dd1962 100644 --- a/Source/Internal/main.c +++ b/Source/Internal/main.c @@ -1,6 +1,8 @@ /* Порядок запуска периферии и обслуживание команд в главном цикле. Русские пояснения сохранены в UTF-8. */ /* Подключает "DSP2833x_Device.h": поставляемое TI описание Device для семейства C2833x. */ #include "DSP2833x_Device.h" // DSP2833x Headerfile Include File +#include /* Переносит только функции ramfuncs из загрузочного диапазона Flash в рабочую внутреннюю RAM. */ +extern unsigned int RamfuncsLoadStart, RamfuncsLoadEnd, RamfuncsRunStart; /* Оба linker-варианта экспортируют границы копирования в адресуемых словах C28x. */ /* Подключает "cntrl_adr.h": собственный и универсальный адрес устройства. */ #include "cntrl_adr.h" @@ -70,9 +72,9 @@ void main() /* Открывает составной блок или описание полей; следующие строки принадлежат этому уровню вложенности. */ { /* Объявляет i — данные i; j — данные j; mask — маска. */ - int i,j,mask; - /* Объявляет cancount — данные cancount; cancell — данные cancell; rscount — данные rscount. */ - static unsigned int cancount[2], cancell[2], rscount; + int i,j; /* Счётчики обслуживают RS-порты; курсоры CAN и маски теперь принадлежат независимому сервису телеметрии. */ + /* Делитель внешнего Modbus-опроса принадлежит main; состояние рассылки CAN хранится и сбрасывается в ecan.c. */ + static unsigned int rscount; /* Сохраняет только делитель опроса OWEN; частота CAN больше не зависит от этого счётчика. */ /* Объявляет rs — данные rs. Хранит указатель на данные. */ RS_DATA * rs; /* Объявляет presbyt — данные presbyt. Начальное значение задаётся справа от знака равенства. */ @@ -80,6 +82,9 @@ void main() /* Инициализирует модуль вызовом InitSysCtrl(); перед его дальнейшим использованием. */ InitSysCtrl(); + if (&RamfuncsLoadStart != &RamfuncsRunStart) /* RAM-образ не копирует одинаковые диапазоны; Flash-образ переносит InitFlash перед изменением wait states. */ + memcpy(&RamfuncsRunStart, &RamfuncsLoadStart, &RamfuncsLoadEnd - &RamfuncsLoadStart); /* Размер равен разности адресуемых 16-битных слов TI, а не физическим октетам ПК. */ + InitFlash(); /* Вызывает находящийся в RAM код настройки Flash-пайплайна и задержек для штатного SYSCLKOUT. */ // Disable CPU interrupts and clear all CPU interrupt flags: /* Запрет маскируемых прерываний ядра. */ @@ -148,7 +153,8 @@ void main() Load_control_init(); /* Explicit initialization before IRQ. */ /* Вызывает Load_control_init. Сбрасывает все переменные автомата нагрузки до разрешения прерываний; COFF не обнуляет всю .ebss. */ /* Вызывает set_cntrl_addr. Устанавливает собственный и универсальный адрес для проверки адресованных команд. */ set_cntrl_addr(Mode,16); - /* Вызывает timer_Init. Назначает обработчик Timer1 по типу платы и задаёт период основного такта. */ + rscount = 0U; /* Обнуляет делитель OWEN до Timer1, чтобы остатки RAM не задержали первый опрос внешнего прибора. */ + /* Назначает обработчик Timer1 по типу платы и задаёт период основного такта после сброса программных делителей. */ timer_Init(); /* Вызывает EnableInterrupts с аргументами () в текущей последовательности операций. */ EnableInterrupts(); @@ -196,72 +202,16 @@ void main() /* Чтение логгера по CAN не блокирует цикл управления. */ /* Вызывает UMP_CAN_service. В главном цикле готовит замороженный ответ служебного CAN и отправляет его двухсловными фрагментами. */ UMP_CAN_service(); - /* Проверяет if(CanGO); последующий оператор выполняется только при истинном условии. */ - if(CanGO) - /* Открывает составной блок или описание полей; следующие строки принадлежат этому уровню вложенности. */ - { - /* Записывает CanGO значением 0; CAN GO. */ - CanGO=0; + CAN_telemetry_service(); /* Обслуживает heartbeat 1 Гц, изменения и повторы 1,3 с независимо от делителя OWEN и состояния CanGO. */ + if (CanGO) { /* Миллисекундный Timer1 по-прежнему выдаёт один флаг каждые CANPOWSE=20 мс для внешнего опроса. */ + CanGO = 0; /* Поглощает текущий такт опроса, не ограничивая выдачу уже готовой очереди CAN. */ + if (Owncount && ++rscount >= Owncount) { /* Нулевой период отключает OWEN; ненулевой задаёт прежнее число шагов по 20 мс. */ + rscount = 0U; /* Начинает очередной интервал после отправки запроса внешнему регулятору. */ + SendCommandModbus4(&rs_b); /* Сохраняет добавленный в 167 опрос измерений OWEN по второму UART. */ + } /* Заканчивает обслуживание делителя внешнего прибора без блокирующих ожиданий CAN. */ + } /* Следующие фоновые операции выполняются при любом состоянии флага периодического опроса. */ - /* Проверяет if(Owncount); последующий оператор выполняется только при истинном условии. */ - if(Owncount) - /* Проверяет if(++rscount>=Owncount); последующий оператор выполняется только при истинном условии. */ - if(++rscount>=Owncount) - /* Открывает составной блок или описание полей; следующие строки принадлежат этому уровню вложенности. */ - { - /* Записывает rscount значением 0; данные rscount. */ - rscount=0; - /* Вызывает SendCommandModbus4 с аргументами (&rs_b) в текущей последовательности операций. */ - SendCommandModbus4(&rs_b); - /* Закрывает текущий блок или список полей; следующие операторы относятся к внешнему уровню. */ - } - - /* Цикл for(i=0;i<2;i++); задаёт начальное значение, условие продолжения и шаг перебора. */ - for(i=0;i<2;i++) - /* Проверяет if(++cancount[i] >= Cancount[i]); последующий оператор выполняется только при истинном условии. */ - if(++cancount[i] >= Cancount[i]) - /* Проверяет if(Cancount[i] и cancount[0]); последующий оператор выполняется только при истинном условии. */ - if(Cancount[i] && cancount[0]) - /* Открывает составной блок или описание полей; следующие строки принадлежат этому уровню вложенности. */ - { - /* Записывает cancount[i] значением 0; данные cancount. */ - cancount[i] = 0; - - /* Повторяет или ожидает, пока выполняется while(1). */ - while(1) - /* Открывает составной блок или описание полей; следующие строки принадлежат этому уровню вложенности. */ - { - /* Проверяет if(cancell[i] >= 0x80) cancell[i]=0; последующий оператор выполняется только при истинном условии. */ - if(cancell[i] >= 0x80) cancell[i]=0; - /* Записывает mask значением Maska[i][cancell[i]/16] >> (cancell[i]%16); маска. */ - mask = Maska[i][cancell[i]/16] >> (cancell[i]%16); - /* Проверяет if(!mask) cancell[i] = (cancell[i] + 0x10) & 0xFFF0; последующий оператор выполняется только при истинном условии. */ - if(!mask) cancell[i] = (cancell[i] + 0x10) & 0xFFF0; - /* Выполняет альтернативу, если предыдущее условие if оказалось ложным. */ - else - /* Открывает составной блок или описание полей; следующие строки принадлежат этому уровню вложенности. */ - { - /* Повторяет или ожидает, пока выполняется while(!(mask & 1)). */ - while(!(mask & 1)) - /* Открывает составной блок или описание полей; следующие строки принадлежат этому уровню вложенности. */ - { - /* Изменяет счётчик в выражении cancell[i]++; mask >>= 1; на один для следующего шага. */ - cancell[i]++; mask >>= 1; - /* Закрывает текущий блок или список полей; следующие операторы относятся к внешнему уровню. */ - } - /* Прерывает ближайший цикл или ветвь switch и продолжает после него. */ - break; - /* Закрывает текущий блок или список полей; следующие операторы относятся к внешнему уровню. */ - } } - /* Вызывает CAN_send. Упаковывает адрес и технологические слова в штатный CAN-кадр и запускает передачу mailbox. */ - CAN_send(0,modbus,cancell[i]); - /* Увеличивает cancell[i] значением 3; данные cancell. */ - cancell[i]+=3; - /* Закрывает текущий блок или список полей; следующие операторы относятся к внешнему уровню. */ - } } - - /* Проверяет if(cDefParam); последующий оператор выполняется только при истинном условии. */ - if(cDefParam) + if(cDefParam) /* Применяет заводские параметры по команде, сохраняя действующие биты управления нагрузкой в обработчике параметров. */ /* Открывает составной блок или описание полей; следующие строки принадлежат этому уровню вложенности. */ { /* Записывает cDefParam значением 0; данные cDefParam. */ diff --git a/Source/Internal/measure.c b/Source/Internal/measure.c index 5c3ee24..f716089 100644 --- a/Source/Internal/measure.c +++ b/Source/Internal/measure.c @@ -698,8 +698,8 @@ void Init_packMask() { /* Ветвь выбора для dsk_EPLT; выполняется при совпадении селектора switch. */ case dsk_EPLT: - /* Устанавливает биты Maska[m_SLOW][0] значением 0x000F; данные Maska. */ - Maska[m_SLOW][0]|= 0x000F; // Полученное + /* Маска 0x000F выбирает R0–R3 пульта для быстрой передачи аварийных и управляющих флагов. */ + Maska[m_FAST][0]|= 0x000F; /* Возвращает период передачи этих четырёх слов из ветки 166, не добавляя их в медленный поток. */ /* Устанавливает биты Maska[m_SLOW][6] значением 0x01FC; данные Maska. */ Maska[m_SLOW][6]|= 0x01FC; // Яркость ламп и одиночество /* Прерывает ближайший цикл или ветвь switch и продолжает после него. */ diff --git a/Source/Internal/message.c b/Source/Internal/message.c index be1bf8e..fe1f588 100644 --- a/Source/Internal/message.c +++ b/Source/Internal/message.c @@ -59,12 +59,21 @@ int modbus[ANSWER_LEN+1]; /* Объявляет param — данные param. */ unsigned int param[ANSWER_LEN+1]; +/* Сброс параметров не является командой пуска: у работающей нагрузки сохраняются только текущие START/RESET, полученные по интерфейсу. */ +static void reset_parameter_commands(void) /* Объединяет поведение Load/Default и исключает пуск из сохранённого слова R127. */ +{ /* Начинает короткую критическую секцию без EEPROM, ожиданий и переключения GPIO. */ + unsigned int saved = __disable_interrupts(); /* Сохраняет маску IRQ, чтобы команда CAN не затерялась между чтением и очисткой служебных битов. */ + if (Desk == dsk_LOAD && READY) Commands &= 0x00C0U; /* Биты 7/6 — START/RESET; сохранение действующих уровней не сбрасывает RUN, STOP и запрет повтора автомата167. */ + else Commands = 0; /* До готовности платы и у остальных типов очищает все команды: EEPROM и неинициализированная RAM не задают пуск. */ + __restore_interrupts(saved); /* Возвращает прежнюю маску, включая вызов внутри защищённого применения параметров. */ +} /* Завершает сброс служебных команд, не меняя счётчики, импульсы и состояние защиты нагрузки. */ + /* Заполняет регистры заводскими значениями; для LOAD задаёт тайм-аут свежести CAN 2000 мс. Значение 600 мс применяется отдельно при восстановлении недопустимого параметра. */ void Default_params() /* Открывает составной блок или описание полей; следующие строки принадлежат этому уровню вложенности. */ { - /* Объявляет i — данные i; def — данные def. */ - unsigned int i,def; + /* Объявляет индекс параметров, номер заводской калибровки и сохранённую маску IRQ. */ + unsigned int i,def,saved; /* Хранит индекс, номер заводской калибровки и прежнюю маску IRQ для согласованного применения параметров. */ /* Объявляет shkf_lo_edge — данные shkf_lo_edge. Начальное значение задаётся справа от знака равенства. */ int shkf_lo_edge[] = {300, // 380 @@ -88,12 +97,13 @@ void Default_params() /* Объявляет bright — данные bright. Начальное значение задаётся справа от знака равенства. */ int bright[] = {10,8,6,8,6,8}; + saved = __disable_interrupts(); /* Не позволяет Timer1 увидеть промежуточные нулевые лимит и тайм-аут защиты при заполнении заводских параметров. */ /* Цикл for(i=0;iRS_Header[1], address, value, 0, words, &error); число. */ diff --git a/Source/Internal/peripher.c b/Source/Internal/peripher.c index 28f534f..32d1c5c 100644 --- a/Source/Internal/peripher.c +++ b/Source/Internal/peripher.c @@ -405,8 +405,7 @@ void get_Buttons() if(Desk==dsk_EPLT) /* Открывает составной блок или описание полей; следующие строки принадлежат этому уровню вложенности. */ { - /* Сохраняет предыдущее состояние TESTLAMP, пока накопитель INcount0 не достигнет одного из порогов; 1000 — число опросов главного цикла, а не миллисекунды. Краткий провал GPIO26 больше не переключает передачу с тестовых масок на рабочие. */ - bTestLamp = er_anal(GpioDataRegs.GPADAT.bit.GPIO26, &INcount0, 1000, bTestLamp); + /* TESTLAMP обрабатывается Timer1 в pulto.c: опрос остальных кнопок не должен менять отсчитанную аппаратным таймером задержку. */ /* Проверяет if(GpioDataRegs.GPADAT.bit.GPIO27) butt +=0x0001; последующий оператор выполняется только при истинном условии. */ if(GpioDataRegs.GPADAT.bit.GPIO27) butt +=0x0001; diff --git a/Source/Internal/pulto.c b/Source/Internal/pulto.c index f2ee6a3..b476c73 100644 --- a/Source/Internal/pulto.c +++ b/Source/Internal/pulto.c @@ -70,6 +70,8 @@ interrupt void cpu_timer1_isr_PULT(void) unsigned int light=0, i; /* Объявляет Alone — данные Alone; READY_FREQ — частота основного такта Timer1 в герцах. Хранит указатель на данные. Начальное значение задаётся справа от знака равенства. */ unsigned int Alone = 20*READY_FREQ; + /* Считает непрерывное удержание TESTLAMP в тактах Timer1: READY_FREQ задаёт число тактов за секунду, независимо от занятости main. */ + static unsigned int test_lamp_hold_ticks = 0; /* Разрешение записи в защищённые регистры периферии. */ EALLOW; @@ -121,6 +123,14 @@ interrupt void cpu_timer1_isr_PULT(void) /* Проверяет if(++cownt>2) cownt=0; последующий оператор выполняется только при истинном условии. */ if(++cownt>2) cownt=0; + if(GpioDataRegs.GPADAT.bit.GPIO26) /* Высокий уровень входа означает нажатую физическую кнопку TESTLAMP на плате пульта. */ + { /* Накапливает только непрерывное удержание; насыщение защищает счётчик от переполнения при долгом нажатии. */ + if(test_lamp_hold_ticks < 2U * (unsigned int)READY_FREQ) /* Порог двух секунд перенесён из 166: короткое нажатие не включает общий тест индикации. */ + test_lamp_hold_ticks++; /* Добавляет один аппаратный такт, поэтому задержка не зависит от обмена и чтения EEPROM в главном цикле. */ + } /* Завершает накопление текущего удержания физической кнопки. */ + else test_lamp_hold_ticks = 0; /* Отпускание сразу отменяет тест и не позволяет суммировать отдельные короткие нажатия. */ + bTestLamp = (test_lamp_hold_ticks >= 2U * (unsigned int)READY_FREQ); /* Обновляет флаг до отправки очередного пакета; удалённая команда cTestLamp сохраняет отдельное управление. */ + /* Проверяет if(cTestLamp|bTestLamp); последующий оператор выполняется только при истинном условии. */ if(cTestLamp|bTestLamp) /* Открывает составной блок или описание полей; следующие строки принадлежат этому уровню вложенности. */ diff --git a/UKSSTMS320F28335/.cproject b/UKSSTMS320F28335/.cproject index 958b36a..0f178e6 100644 --- a/UKSSTMS320F28335/.cproject +++ b/UKSSTMS320F28335/.cproject @@ -28,8 +28,8 @@ - - + + @@ -424,8 +424,8 @@ - - + + diff --git a/UKSSTMS320F28335/.project b/UKSSTMS320F28335/.project index ee06cfe..7302695 100644 --- a/UKSSTMS320F28335/.project +++ b/UKSSTMS320F28335/.project @@ -421,6 +421,11 @@ SRC_ROOT1/Source/Internal/gas167.c + + Source/Internal/flash_rs.c + 1 + SRC_ROOT1/Source/Internal/flash_rs.c + diff --git a/UKSSTMS320F28335/README.md b/UKSSTMS320F28335/README.md index 184f89a..f2b5c80 100644 --- a/UKSSTMS320F28335/README.md +++ b/UKSSTMS320F28335/README.md @@ -21,31 +21,37 @@ Debug и Release вызывают prepare_identity.py. Он читает ном Для текущих конфигураций нужны Python, Git и TI C2000 22.6 LTS. Debug и Release используют оптимизацию уровня 3 и приоритет размера -`--opt_for_speed=0`: это необходимо для размещения образа с настраиваемым -лимитом попыток в AT25256B. Проверенная локальная сборка находится в -`Bin/AttemptLimit`; перед загрузкой применяется SCI8-проверка границы EEPROM. +`--opt_for_speed=0`. Основной аппаратный профиль — M95M02-DR, 256 КиБ. +Локальный проверенный выпуск после переноса доработок 166 находится +в `Bin/Final167/M95M02`: EEPROM/RAM SCI8 занимает 34 120 байт при пределе 261 884. +AT25256B остаётся явным альтернативным профилем, однако текущий полный образ +превышает её предел; защищённый выпуск не разрешает такой BIN. Общая реализация УМП и порт TMS320F28335 подключаются из ../newProject/SETGUI/third_party/templates/c/set-protocol относительно корня репозитория. shared_ump.c включает эту реализацию и модуль паспорта. -В .cproject обе конфигурации используют генератор и post-build из -K:/git_project/newProject/templates. При переносе проекта проверьте этот путь, -пути Include Options к общей реализации и каталог назначения BIN. Генератор -исходников и post-build сейчас находятся в соседнем репозитории; старые -локальные build_bin.bat/preBuildStep.bat больше не входят в этот проект. +В .cproject обе конфигурации используют локальные prepare_identity.py и +publish_identity.py. Генератор паспорта и Git-ревизия зависимости берутся из +той же копии SETGUI/third_party/templates, из которой компилируется общий C-код. +Инструмент конверсии postcompile.bat находится в +K:/git_project/newProject/templates/tools/firmware-build/ccs. При переносе +проекта проверьте оба пути, Include Options и каталог назначения BIN. -Post-build вызывает tools/firmware-build/ccs/postcompile.bat: формирует загрузочный -образ, проверяет границы, создаёт паспорт и SHA-256. Текущий каталог копирования +Защищённый post-build вызывает общий postcompile только во временный каталог. +До публикации проверяются аппаратный профиль по MAP, размер до параметров +EEPROM, структура SCI8, RAM-адреса, встроенный паспорт, manifest и SHA-256. +Поле eeprom различает AT25256B и M95M02. Текущий каталог копирования задан в .cproject: K:/git_project/newProject/SETGUI/..periph_28335/firmware/periph28335/balzam167. -COM_1 работает на 115200 с FIFO. COM_2 использует 57600 для OWEN либо 115200 +COM_1 работает на 115200 с FIFO. COM_2 использует 9600 для OWEN либо 115200 для другого режима. Эти скорости задаются в main.c. ## Профиль EEPROM и GAS В Source/Internal/Include/eeprom_config.h значение EEPROM_M95M02=0 выбирает -AT25256B 32 КиБ; значение 1 — M95M02-DR 256 КиБ. Выбор обязан соответствовать +AT25256B 32 КиБ; значение 1 — M95M02-DR 256 КиБ и выбрано по умолчанию. +Выбор обязан соответствовать микросхеме на плате. После изменения нужен Clean/Rebuild. При отдельной сборке определение должно совпадать у компилятора и у препроцессора линкера. @@ -61,3 +67,80 @@ tests/gas167/run_test.py, tests/eeprom_profiles/run_test.py, tests/load_control/run_test.py и проверки регистратора в tests/ump_logger. Проверка FIFO: tests/rs232_fifo/run_test.py --cc <путь к GCC>. Для MinGW каталог исполняемых файлов GCC должен присутствовать в PATH. + +## Полезные доработки из ветки 166 + +В 167 перенесены heartbeat CAN 1 Гц, отправка изменившихся данных, повтор +каждого подтверждённого пакета через 1,3 с, повтор после потери ACK и защита +границы R127. R96/R97 теперь включают потоки: 0 выключает соответствующую +маску, любое ненулевое значение разрешает её. Значение больше не задаёт +делитель периода. R0–R3 пульта находятся в быстрой маске; служебный канал +УМП/GAS и опрос OWEN сохраняются. Подробности: [PERIPH28335_CAN.md](../Doc/PERIPH28335_CAN.md). + +TESTLAMP включается после двух секунд непрерывного удержания физической +кнопки. Время считает Timer1, поэтому занятость главного цикла и EEPROM не +меняет задержку. Порядок индикаторов в профиле 167 сохранён. + +Загрузка и заводской сброс параметров сохраняют действующие START/RESET +готовой платы нагрузки. Сохранённое в EEPROM слово команд не подаётся +автомату даже на промежуточный такт. Новый автомат защиты нагрузки и +регистратор 167 сохранены. + +Проверки переноса: `tests/can_telemetry/run_test.py`, +`tests/can_vpu_rx/run_test.py`, `tests/can_timeout/run_test.py`, +`tests/testlamp/run_test.py --cc <путь к GCC>` и +`tests/load_control/run_test.py`. Проверки защищённого выпуска: +`tests/firmware_identity/test_release.py`. + +## Внутренняя Flash + +В 167 добавлен отдельный Flash-вариант на основе ветки `прошивка-через-flash` +(`437f250`) с сохранением текущих CAN, GAS, регистратора и защиты нагрузки. +Основной `F28335.cmd` размещает `.text` в RAM. Альтернативный +`F28335_flash.cmd` размещает код во FlashH, константы и инициализацию — во +FlashG, аппаратный переход `codestart` — по адресу слова `0x33FFF6`. +Предстартовый код TI отключает watchdog до входа в `_c_int00`, чтобы холодный +запуск не прерывал инициализацию C. +`ramfuncs` копируются в RAML4 до `InitFlash`; автономный RS-программатор и TI +Flash API копируются из FlashF в RAML0 до стирания. + +Штатный защищённый postbuild вызывает `build_flash.py` и выпускает оба варианта +с одним паспортом текущей компиляции. Требуется официальный TI Flash API v2.10 +для F28335; его каталог задаётся `TI_FLASH_API_ROOT` либо штатным путём controlSUITE. +Перед публикацией проверяются COFF официальным `ofd2000`, точные LOAD/RUN, +точка входа, содержимое SCI8 и побайтное совпадение загружаемых слов COFF/SCI8. + +Проверенный локальный комплект: `Bin/Final167/M95M02`. + +| Файл | Назначение | +|---|---| +| `firmware_9858046+.bin` | M95M02 → RAM, 34 120 байт SCI8 | +| `firmware_flash.out` | Первичная запись внутренней Flash через JTAG, COFF с символами | +| `firmware_flash_rs.bin` | Обновление внутренней Flash по RS, 39 494 байта SCI8 | + +Паспорта Flash дополнены `execution=FLASH` и `image_format=COFF` либо +`SCI8-FLASH`; поле `eeprom=M95M02` описывает отдельную память параметров. +Для запуска после сброса аппаратные boot-перемычки должны выбирать Flash. +При SPI boot загружается EEPROM/RAM-образ из M95M02. + +Команда записи Flash на плате нагрузки отклоняется кодом `0x1005`, пока +штатный STOP не завершён и SPI не подтвердил отправку текущего кода 4 мА. +Probe не останавливает нагрузку. После начала стирания IRQ запрещены, все +вызовы остаются в RAM, boot-переход записывается последним. При ошибке записи +может потребоваться восстановление через JTAG: резервного Flash-образа нет. +Сборка, хостовые проверки и дизассемблер не заменяют проверку записи и +самостоятельного запуска на стенде; физическая плата в этой работе не прошивалась. + +## Помощник настройки linker + +Общая утилита и инструкция находятся в соседнем репозитории +[`templates/tools/c2000-linker`](../../newProject/templates/tools/c2000-linker/README.md). +Она создаёт стартовые карты COFF для TMS320F28335 и TMS320F2812 в режимах +RAM/Flash, объясняет LOAD/RUN и проверяет физические пересечения памяти. +Профильные особенности платы и внешняя XINTF RAM задаются в проекте. + +EEPROM M95M02 хранит загрузочный поток SCI8. Её 256 КиБ не являются +CPU-адресуемым расширением RAM и не добавляются в MEMORY linker-файла. +В текущем RAM-варианте `.text` занимает внутренние RAML0–RAML3, а `.cinit` +и константы — RAML4. При Flash-варианте код и константы загружаются во Flash, +а секции `ramfuncs` и автономного программатора имеют отдельные LOAD и RUN. diff --git a/UKSSTMS320F28335/build_flash.py b/UKSSTMS320F28335/build_flash.py new file mode 100644 index 0000000..91f721b --- /dev/null +++ b/UKSSTMS320F28335/build_flash.py @@ -0,0 +1,101 @@ +"""Собирает независимый Flash COFF текущей 167 с тем же паспортом и аппаратной EEPROM.""" # Альтернативная сборка сохраняет основную RAM/EEPROM-прошивку. +import argparse # Поддерживает штатный вызов общего postbuild и явную проверочную сборку без Make. +import json # Читает подготовленный паспорт, включая выбранную физическую EEPROM и компилируемую зависимость. +import os # Позволяет указать установленный TI Flash API без изменения исходников проекта. +from pathlib import Path # Разрешает все файлы относительно проекта, исключая зависимость от текущего терминала. +import subprocess # Запускает только локальные компилятор и линкер TI, не программируя устройство. +import tempfile # Каждая компиляция получает новые объектные файлы, не зависящие от старых артефактов или блокировок CCS. +import xml.etree.ElementTree as ET # Читает фактические списки исходников и include paths CCS вместо устаревшего generated Makefile. +from prepare_identity import read_profile, read_version # Сохраняет ручную версию и физическую разметку текущей 167. + +PROJECT = Path(__file__).resolve().parent # Каталог CCS содержит проверяемые .project и .cproject. +ROOT = PROJECT.parent # Корень production-исходников и обоих linker-вариантов устройства. +COMPILER = Path("C:/ti/ccs1230/ccs/tools/compiler/ti-cgt-c2000_22.6.0.LTS") # Доступный установленный COFF/FPU32 toolchain проекта. +FLASH_API = Path(os.environ.get("TI_FLASH_API_ROOT", "C:/ti/controlSUITE/libs/utilities/flash_api/2833x/28335/v210")) # Официальный API v2.10 F28335 с COFF ABI; сама библиотека не копируется в репозиторий. + + +def configuration_paths(configuration, compiler, build_dir): # Получает include paths и оптимизацию именно выбранной конфигурации CCS. + configs = ET.parse(PROJECT / ".cproject").findall(".//configuration") # Читает актуальный XML, а не параметры старой ветки Flash. + config = next(item for item in configs if item.get("name") == configuration) # Ошибка неизвестного Debug/Release не подменяется произвольными настройками. + options = config.findall(".//tool/option") # Содержит общие и файловые настройки компилятора в штатном порядке CCS. + include_option = next(item for item in options if item.get("superClass", "").endswith("compilerID.INCLUDE_PATH")) # Использует действующие include paths, включая vendored templates. + macros = {"${SRC_ROOT}": str(ROOT), "${PROJECT_LOC}": str(PROJECT), "${PROJECT_ROOT}": str(PROJECT), "${CG_TOOL_ROOT}": str(compiler), "${BuildDirectory}": str(build_dir)} # Разрешает только известные файловые макросы CCS; PROJECT_ROOT совпадает с каталогом .project. + includes = [] # Поддерживает исходный порядок поиска заголовков компилятора. + for option in include_option.findall("listOptionValue"): # Обрабатывает каждый путь фактически выбранной конфигурации. + value = option.get("value", "") # Пустые некорректные настройки будут отвергнуты до компиляции. + if value == "${COM_TI_XDC_INCLUDE_PATH}": # Штатная конфигурация проекта без SYS/BIOS не задаёт каталогов XDC, а CCS оставляет служебный макрос пустым. + continue # Не передаёт неразрешённый встроенный placeholder отдельному CLI-компилятору. + for macro, replacement in macros.items(): # Подставляет реальные абсолютные пути данной проверки. + value = value.replace(macro, replacement) # Не использует shell expansion и не исполняет текст XML. + if not value or "${" in value: # Не передаёт компилятору неразрешённый путь вместо общей зависимости. + raise ValueError("Unresolved CCS include path: " + value) # Указывает настройку, которую требуется исправить в проекте. + includes.append(Path(value).resolve()) # Позволяет сборке запускаться из любого каталога postbuild. + level = next(item.get("value").rsplit(".", 1)[-1] for item in options if "compilerID.OPT_LEVEL" in item.get("superClass", "")) # Сохраняет нынешний O3 вместо старого O1 Flash-ветки. + speed = next(item.get("value").rsplit(".", 1)[-1] for item in options if item.get("superClass", "").endswith("compilerID.OPT_FOR_SPEED")) # Приоритет размера остаётся общим для RAM и Flash. + return includes, ["-O" + level, "--opt_for_speed=" + speed] # Прочие архитектурные настройки C28x/FPU32/COFF задаются явно ниже. + + +def project_sources(): # Перечисляет только связанные production C/ASM-файлы текущего CCS-проекта. + sources = [] # Каждая запись компилируется в независимый Flash-объект без использования старой RAM-сборки. + for link in ET.parse(PROJECT / ".project").findall(".//linkedResources/link"): # Сохраняет подключённые внешние TI-модули и все актуальные локальные изменения. + uri = link.findtext("locationURI", "") # Исходники должны находиться в текущем дереве SRC_ROOT1. + if uri.startswith("SRC_ROOT1/Source/") and Path(uri).suffix.lower() in (".c", ".asm"): # Заголовки и linker command файлы не компилируются как самостоятельный C-код. + sources.append(ROOT / uri.removeprefix("SRC_ROOT1/")) # Избегает ссылок на исходники другой ветки или случайный generated Source. + sources.append(ROOT / "Source/Internal/flash_codestart.asm") # Аппаратный boot-переход подключается только к Flash, сохраняя чистую RAM SCI8-таблицу. + return list(dict.fromkeys(sources)) # Исключает дубли после повторного импорта CCS, не меняя первоначальный порядок. + + +def build_flash(compiler, build_dir, output, configuration="Debug"): # Полностью пересобирает отдельный Flash-вариант без рекурсивного Make и без прошивки платы. + compiler, build_dir, output = compiler.resolve(), build_dir.resolve(), output.resolve() # Фиксирует реальные пути входов и единственного запрошенного результата. + api_library = FLASH_API / "lib/Flash28335_API_V210.lib" # Официальная библиотека TI содержит алгоритмы стирания, записи и проверки F28335. + if not api_library.is_file(): # Нельзя выпускать образ с обещанным RS-обновлением без Flash API. + raise FileNotFoundError(api_library) # Диагностирует отсутствующую локальную установку до компиляции проекта. + metadata = json.loads((build_dir / "generated/firmware_identity.json").read_text(encoding="utf-8")) # Использует паспорт, подготовленный до RAM-сборки той же конфигурации. + templates = Path(metadata["templates_root"]) # Не возвращается к старой другой копии templates из Flash-ветки. + profile = read_profile(PROJECT, templates, metadata["eeprom"]) # Поддерживает согласованный CLI override обеих физических EEPROM. + if any(metadata.get(key) != value for key, value in profile.items()) or metadata["version"] != read_version(PROJECT / "firmware_version.h"): # Отвергает смену версии/аппаратного профиля после prebuild. + raise ValueError("Flash configuration changed: run prebuild before compiling") # Не подписывает новый Flash старой таблицей версии. + includes, optimize = configuration_paths(configuration, compiler, build_dir) # Сохраняет актуальные include paths и настройки оптимизации RAM-проекта. + includes += [ROOT, FLASH_API / "include", templates / "c/set-protocol", templates / "c/set-protocol/ports/tms320f28335", templates / "c/firmware-info/include", build_dir / "generated"] # Изолированная проверочная сборка получает тот же generated-паспорт и локальные Flash-заголовки. + selected = int(profile["eeprom"] == "M95M02") # Только физический тип EEPROM управляет адресацией SPI и расположением сохраняемых параметров. + flags = ["-v28", "-ml", "-mt", "--float_support=fpu32", "--abi=coffabi", *optimize, "--gen_func_subsections=on", "--define=LARGE_MODEL", "--define=FLASH_BOOT", "--define=EEPROM_M95M02=" + str(selected)] # Flash boot отделён от аппаратной EEPROM; общий compiler guard проверяет выбранный профиль. + flags += ["--define=_DEBUG"] if configuration == "Debug" else [] # Сохраняет отладочный макрос без переноса других старых поведенческих изменений. + flags += ["--include_path=" + str(path) for path in dict.fromkeys(includes)] # Повторные пути не меняют поиск, а список остаётся пригодным для журнала. + object_root = build_dir / "flash" # Дополнительные объекты не подменяют RAM-компиляцию и её проверенный образ. + object_root.mkdir(parents=True, exist_ok=True) # Создаёт только выделенный корень артефактов Flash, сохраняя прежние диагностические сборки. + objects = Path(tempfile.mkdtemp(prefix="build-", dir=object_root)) # Новый каталог исключает подстановку stale OBJ и ошибки перезаписи заблокированного старого файла другой ветки. + object_files = [] # Линкер получит лишь объекты успешно завершившихся компиляций этой попытки. + log_path = objects / "build.log" # Сохраняет команды и диагностику для воспроизводимого review без засорения исходников. + with log_path.open("w", encoding="utf-8") as log: # Один журнал принадлежит одному последовательному Flash-build. + for source in project_sources(): # Пересобирает все production-модули, сохраняя текущие CAN, GAS, защиты и регистратор 167. + object_file = objects / (source.stem + ".obj") # Имена исходников текущего CCS-проекта уникальны и совпадают с его объектной моделью. + command = [str(compiler / "bin/cl2000.exe"), *flags, "-c", "--output_file=" + str(object_file), str(source)] # Не использует объект из старого Makefile либо другой ветки. + log.write(json.dumps(command, ensure_ascii=False) + "\n") # Фиксирует точные include paths, compiler define и исходник текущей попытки. + log.flush() # Команда остаётся доступной при аварийном завершении компилятора. + result = subprocess.run(command, cwd=ROOT, stdout=log, stderr=subprocess.STDOUT) # Все сообщения TI сохраняются рядом с альтернативными объектами. + if result.returncode: # Ненулевой код не допускает линковку прежнего объектного файла. + raise RuntimeError(f"Flash compilation failed: {source}; see {log_path}") # Даёт точный модуль и журнал ошибки вместо ложного успеха старого OUT. + object_files.append(str(object_file)) # Только успешный объект входит в итоговый список линкера. + output.parent.mkdir(parents=True, exist_ok=True) # Общий postcompile обычно передаёт свой временный каталог выпуска. + command = [str(compiler / "bin/cl2000.exe"), "-v28", "--float_support=fpu32", "--abi=coffabi", "-z", "--define=EEPROM_M95M02=" + str(selected), "--stack_size=0x3f0", "--entry_point=_c_int00", "--rom_model", "--warn_sections", "--reread_libs", "-m" + str(output.with_suffix(".map")), "-o", str(output)] # Стандартный C startup и Flash codestart образуют две нужные точки входа устройства. + command += ["--search_path=" + str(path) for path in (ROOT, compiler / "lib", FLASH_API / "lib", templates / "c/set-protocol")] # Разрешает общую разметку EEPROM и секции официального TI API в linker command file. + command += [str(ROOT / "F28335_flash.cmd"), str(ROOT / "Source/External/v120/DSP2833x_headers/cmd/DSP2833x_Headers_nonBIOS.cmd"), *object_files, str(api_library), "-lrts2800_fpu32.lib", "-llibc.a"] # Берёт актуальный runtime TI вместо старой локальной библиотеки, в которой ранее ломался memcpy startup. + log.write(json.dumps(command, ensure_ascii=False) + "\n") # Сохраняет полную карту входов Flash-линкера для проверки LOAD/RUN. + log.flush() # Записывает команду до запуска потенциально ошибочной линковки. + result = subprocess.run(command, cwd=ROOT, stdout=log, stderr=subprocess.STDOUT) # Итоговый OUT принадлежит только этой свежей Flash-компиляции. + if result.returncode: # Нельзя выпускать старый OUT после неудачной линковки. + raise RuntimeError(f"Flash link failed; see {log_path}") # Общий postcompile прекратит выпуск всех вариантов текущей попытки. + print(f"[firmware-build] Flash {profile['eeprom']} COFF: {output}") # Показывает конкретный физический профиль и отдельный файл исполнения из Flash. + return output # Защищённый publish проверит COFF/MAP/SCI8 перед публикацией и export. + + +if __name__ == "__main__": # Импорт проверяющим кодом не запускает сборку и не создаёт файлы. + parser = argparse.ArgumentParser(description=__doc__) # Описывает отдельный вариант вместо скрытого переключения обычного BIN. + parser.add_argument("--postbuild", action="store_true") # Сохраняет совместимость с автоматическим обнаружением общего postcompile. + parser.add_argument("--compiler", type=Path, default=COMPILER) # Общий postbuild передаёт фактический CG_TOOL_ROOT. + parser.add_argument("--build-dir", type=Path, default=PROJECT / "Debug") # Разрешает независимый временный каталог проверки с собственным generated. + parser.add_argument("--output", type=Path, default=ROOT / "Bin/UKSSTMS320F28335_flash.out") # Не заменяет основной RAM OUT. + parser.add_argument("--configuration", choices=("Debug", "Release")) # Позволяет явно воспроизвести настройки CCS при изолированной проверке. + arguments = parser.parse_args() # Проверяет CLI до любых операций компилятора. + configuration = arguments.configuration or ("Release" if arguments.build_dir.name.lower() == "release" else "Debug") # Штатные каталоги CCS выбирают конфигурацию без дополнительного параметра общего postcompile. + build_flash(arguments.compiler, arguments.build_dir, arguments.output, configuration) # Запускает только компиляцию/линковку; программирование устройства отсутствует. diff --git a/UKSSTMS320F28335/prepare_identity.py b/UKSSTMS320F28335/prepare_identity.py index 565b51e..7faaf7c 100644 --- a/UKSSTMS320F28335/prepare_identity.py +++ b/UKSSTMS320F28335/prepare_identity.py @@ -11,10 +11,52 @@ import json from pathlib import Path # Подключает модуль re для этой проверки или подготовки сборки. import re +# Запускает Git с доверием только к явно выбранному рабочему дереву, не меняя глобальные настройки пользователя. +import subprocess # Подключает модуль tempfile для этой проверки или подготовки сборки. import tempfile +# Читает единственное числовое определение; вычисляемые выражения отклоняются, чтобы Python не исполнял содержимое C-заголовков. +def numeric_define(header, name): + text = header.read_text(encoding="utf-8-sig") # Поддерживает UTF-8 с BOM для пользовательских заголовков конфигурации. + text = re.sub(r"/\*.*?\*/|//[^\n]*", "", text, flags=re.S) # Удаляет комментарии перед поиском настоящих директив препроцессора. + values = re.findall(r"^\s*#\s*define\s+" + re.escape(name) + r"\s+([^\n]+)", text, re.M) # Требует определение нужного макроса, а не упоминание в тексте. + if len(values) != 1 or not re.fullmatch(r"(?:0[xX][0-9a-fA-F]+|[0-9]+)[uUlL]*", values[0].strip()): # Исключает неоднозначность и выражения, которые могут отличаться от C-препроцессора. + raise ValueError(f"{header}: define {name} once as an integer") # Останавливает выпуск при неподдерживаемой структуре общей разметки. + return int(values[0].strip().rstrip("uUlL"), 0) # Преобразует десятичную либо шестнадцатеричную величину в число Python. + + +# Получает физический профиль из той же конфигурации и той же копии разметки, которые читает компилятор 167. +def read_profile(project, templates, eeprom=None): + selected = numeric_define(project.parent / "Source/Internal/Include/eeprom_config.h", "EEPROM_M95M02") # По умолчанию повторяет единственный аппаратный выбор платы из заголовка. + if selected not in (0, 1): # Не допускает произвольное ненулевое значение вместо двух поддерживаемых микросхем. + raise ValueError("EEPROM_M95M02 must be 0 or 1") # Диагностирует ошибку до генерации паспорта и конверсии OUT. + name = eeprom or ("M95M02" if selected else "AT25256B") # Явное CLI-переопределение должно также передаваться компилятору и линкеру. + if name not in ("AT25256B", "M95M02"): # Проверяет также программные вызовы, которые обходят argparse. + raise ValueError("EEPROM must be AT25256B or M95M02") # Не подставляет профиль по умолчанию при неизвестном названии. + layout = templates / "c/set-protocol/include/balsam167_eeprom_layout.h" # Выбирает реальный заголовок из компилируемой зависимости. + total = numeric_define(layout, "BALSAM167_M95M02_BYTES" if name == "M95M02" else "BALSAM167_AT25256_BYTES") # Ёмкость задаётся общим контрактом в физических октетах. + words = numeric_define(layout, "BALSAM167_EEPROM_PARAM_WORDS") # Число сохраняемых 16-битных слов включает контрольную сумму параметров. + limit = total - (words + 1) * 2 # Резервирует параметры и последнее неиспользуемое слово; слово C28x занимает два октета EEPROM. + if total <= 0 or total % 2 or words <= 0 or limit < 24: # Минимальная таблица SCI8 требует 22 октета заголовка и двухоктетный терминатор. + raise ValueError("Invalid EEPROM layout") # Отклоняет повреждённую разметку вместо отключения ограничения размера. + return {"eeprom": name, "eeprom_bytes": total, "eeprom_param_words": words, "max_image_bytes": limit} # Эти значения становятся частью паспорта конечного BIN. + + +# Снимает состояние только явно переданного репозитория и не требует записи safe.directory в пользовательский Git config. +def git_state(repository): + directory = repository.resolve() # Рабочий каталог CCS может быть вложен в репозиторий приложения. + trusted = next((path for path in (directory, *directory.parents) if (path / ".git").exists()), directory) # Git проверяет корень checkout; .git-файл также учитывает подключённый templates submodule. + command = ["git", "-c", "safe.directory=" + trusted.as_posix(), "-C", str(directory)] # Локальное доверие ограничено корнем явно выбранной зависимости или исходников проекта. + try: # Отсутствие Git сохраняет прежний явный NOGIT-контракт общей библиотеки. + commit = subprocess.check_output(command + ["rev-parse", "HEAD"], stderr=subprocess.DEVNULL, text=True, encoding="utf-8").strip() # Получает полный SHA именно компилируемого дерева. + dirty = bool(subprocess.check_output(command + ["status", "--porcelain", "--untracked-files=normal"], stderr=subprocess.DEVNULL, text=True, encoding="utf-8").strip()) # Учитывает новые и изменённые исходники при формировании знака плюс. + return {"commit": commit, "dirty": dirty} # Возвращает совместимую с общим генератором структуру происхождения. + except (OSError, subprocess.CalledProcessError): # Не выдаёт неизвестную ревизию за чистое дерево. + return {"commit": None, "dirty": None} # Генератор пометит такую сборку идентификатором NOGIT000. + + # Объявляет функцию read_version и её входные параметры. def read_version(header): # Читает исходный файл или образ для последующей проверки и разбора. @@ -52,17 +94,20 @@ def read_version(header): # Объявляет функцию prepare и её входные параметры. -def prepare(project, templates, output): +def prepare(project, templates, output, eeprom=None): # Допускает явный аппаратный профиль для CLI при согласованных define компилятора и линкера. # Читает исходный файл или образ для последующей проверки и разбора. settings = json.loads((project / "firmware_identity.json").read_text(encoding="utf-8-sig")) # Задаёт settings["version"] по выражению справа; значение используется последующими операциями. settings["version"] = read_version(project / "firmware_version.h") + settings["increment_version"] = False # Ручная версия из C-заголовка не увеличивается даже при случайном появлении старого JSON-переключателя. + profile = read_profile(project, templates, eeprom) # Вычисляет границу BIN до вызова генератора, чтобы ошибочный профиль не оставлял пригодный паспорт. # Задаёт spec по выражению справа; значение используется последующими операциями. spec = importlib.util.spec_from_file_location("shared_firmware_identity", templates / "c/firmware-info/tools/identity.py") # Задаёт identity по выражению справа; значение используется последующими операциями. identity = importlib.util.module_from_spec(spec) # Продолжает текущий вызов, список или выражение: spec.loader.exec_module(identity); порядок аргументов сохраняется. spec.loader.exec_module(identity) + identity.git_state = git_state # Использует штатную структуру паспорта с безопасным локальным чтением Git для выбранной копии templates. # The source header owns SemVer; retain the shared generator and manifest format. # Открывает управляемый ресурс tempfile.TemporaryDirectory(prefix="balsam-identity-") as directory с автоматическим освобождением. with tempfile.TemporaryDirectory(prefix="balsam-identity-") as directory: @@ -71,7 +116,16 @@ def prepare(project, templates, output): # Сохраняет подготовленный файл в указанном формате для следующего шага. config.write_text(json.dumps(settings), encoding="utf-8") # Возвращает identity.prepare(project, templates, config, output) вызывающему коду и завершает функцию. - return identity.prepare(project, templates, config, output) + metadata = identity.prepare(project, templates, config, output) # Генерирует прежние 12 встроенных слов версии, не меняя транспортный контракт. + metadata.update(profile) # Различает физические EEPROM в JSON без добавления сохраняемых MCU-регистров. + metadata["templates_root"] = str(templates.resolve()) # Позволяет postbuild сверить, что использована та же компилируемая копия общих исходников. + header_path = output / "firmware_identity.h" # Штатный firmware_info_static.c обязательно компилирует этот заголовок внутри shared_ump.c. + guard = '\n#include "eeprom_config.h" /* Читает тот же аппаратный выбор, что SPI-драйвер и обработчики 167. */\n' # Проверка выполняется препроцессором и не увеличивает BIN малой EEPROM. + guard += f'#if EEPROM_M95M02 != {int(profile["eeprom"] == "M95M02")} /* Сверяет фактический define компилятора с профилем подготовленного паспорта. */\n' # Явный CLI-профиль требует согласованного compiler define. + guard += '#error EEPROM_profile_differs_from_prebuild /* Останавливает сборку при compiler-only override или смене EEPROM после prebuild. */\n#endif /* Завершает проверку соответствия аппаратного профиля паспорту. */\n' # Не позволяет подписать M95M02-драйвер паспортом AT25256B даже при согласованной MAP. + identity.write_atomic(header_path, header_path.read_text(encoding="utf-8") + guard) # Сохраняет исходный общий контракт и добавляет только локальную проверку конфигурации. + identity.write_atomic(output / "firmware_identity.json", json.dumps(metadata, ensure_ascii=False, indent=2) + "\n") # Сохраняет обогащённый паспорт до компиляции и последующей подписи BIN. + return metadata # Возвращает фактическую ручную версию и аппаратный профиль текущей сборки. # Проверяет условие if __name__ == "__main__" перед выполнением следующего блока. @@ -84,7 +138,8 @@ if __name__ == "__main__": parser.add_argument("--templates", type=Path, required=True) # Задаёт parser.add_argument("--output", type по выражению справа; значение используется последующими операциями. parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--eeprom", choices=("AT25256B", "M95M02")) # CLI-переопределение требует того же аппаратного define у компилятора и линкера. # Задаёт args по выражению справа; значение используется последующими операциями. args = parser.parse_args() # Продолжает текущий вызов, список или выражение: print(json.dumps(prepare(args.project, args.templates, args.output))); порядок аргументов сохраняется. - print(json.dumps(prepare(args.project, args.templates, args.output))) + print(json.dumps(prepare(args.project, args.templates, args.output, args.eeprom))) # Показывает выбранный профиль вместе с прежними полями идентификации. diff --git a/UKSSTMS320F28335/publish_identity.py b/UKSSTMS320F28335/publish_identity.py new file mode 100644 index 0000000..198f9e0 --- /dev/null +++ b/UKSSTMS320F28335/publish_identity.py @@ -0,0 +1,202 @@ +"""Публикует EEPROM/RAM SCI8 только после проверки встроенного паспорта и разметки платы.""" # Локальная защита дополняет общий конвертор без изменения соседнего templates. +import argparse # Разбирает одинаковые аргументы защищённого postbuild для CCS Debug и Release. +import base64 # Декодирует точные байты секций COFF из официального XML-вывода TI ofd2000. +from datetime import datetime # Восстанавливает слова UTC из паспорта, созданного перед компиляцией. +import hashlib # Проверяет точные байты конечного BIN и сопроводительного SHA-256. +import importlib.util # Подключает штатный механизм экспорта только после проверки временного выпуска. +import json # Сверяет изделие, аппаратный профиль и идентификацию текущей сборки. +from pathlib import Path # Разрешает пути относительно выбранной конфигурации CCS, а не текущего терминала. +import re # Читает абсолютные EEPROM-символы из карты линкера TI. +import shutil # Копирует только полностью проверенные файлы из временного каталога. +import struct # Разбирает 16-битные слова SCI8 и адреса загрузочных блоков C28x. +import subprocess # Вызывает общую BAT-точку конверсии без публикации во внешнее приложение. +import tempfile # Изолирует непроверенные результаты общего postcompile от постоянных выпусков. +import xml.etree.ElementTree as ET # Проверяет фактические LOAD/RUN адреса Flash COFF без собственного двоичного парсера TI. +from prepare_identity import read_profile, read_version # Использует тот же ручной номер и ту же физическую разметку, что и prebuild. + + +def verify_embedded(image, metadata, flash=False): # RAM и Flash SCI8 проверяются отдельно по разным допустимым адресам и ограничениям загрузчика. + data = image.read_bytes() # Проверяет содержимое файла, которое будет передано в EEPROM-программатор. + limit = 0xFC00 * 2 - 2 if flash else metadata["max_image_bytes"] # RS Flash использует внешний буфер 0x200000..0x20FC00 и ещё два октета CRC; RAM BIN ограничен резервом физической EEPROM. + if not 24 <= len(data) <= limit: # Учитывает заголовки SCI8 вместе с кодом, не позволяя переписать резерв параметров либо выйти за буфер RS. + raise ValueError("SCI8 exceeds EEPROM firmware boundary or is too short") # Отвергает слишком большой BIN до создания постоянного каталога выпуска. + if data[:2] != b"\xaa\x08": # 0x08AA — обязательный ключ восьмибитной boot-таблицы TI. + raise ValueError("Invalid SCI8 header") # Не принимает HEX, COFF либо плоский дамп под видом загрузочного BIN. + stamp = datetime.fromisoformat(metadata["built_at_utc"].replace("Z", "+00:00")) # Берёт момент именно того prebuild, который подготовил C-заголовок. + build_id = metadata["build_id"] # Контракт firmware-info v1 хранит ровно восемь ASCII-символов идентификатора. + if not re.fullmatch(r"[0-9a-fA-F]{8}|[0-9a-fA-F]{7}\+|NOGIT000", build_id) or stamp.utcoffset() is None or stamp.utcoffset().total_seconds(): # Исключает неоднозначную дату и неподдерживаемый идентификатор. + raise ValueError("Invalid prebuild identity") # Требует заново выполнить prebuild вместо выпуска чужих метаданных. + words = [1, *metadata["version"], stamp.year, stamp.month * 256 + stamp.day, stamp.hour * 256 + stamp.minute, stamp.second] # Независимо восстанавливает первые восемь 16-битных регистров общего контракта. + words += [ord(build_id[index]) * 256 + ord(build_id[index + 1]) for index in range(0, 8, 2)] # Упаковывает две ASCII-октеты в каждое из четырёх слов идентификатора. + expected = struct.pack("<12H", *words) # TI SCI8 хранит каждое слово полезных данных младшей октетой вперёд. + high, low = struct.unpack_from(" len(data): # Требует полный 32-битный адрес и все октеты полезной нагрузки. + raise ValueError("Truncated SCI8 section") # Не допускает выпуск усечённой секции даже при паспорте в её начале. + high, low = struct.unpack_from(" /* Фиксирует ширину таймера и аппаратных полей eCAN независимо от разрядности host-компилятора. */ +#include /* Выводит точную строку нарушенного сценария вместо молчаливого неверного результата. */ +#include /* Изолирует независимые случаи очисткой модели аппаратуры и данных. */ +typedef uint16_t Uint16; typedef uint32_t Uint32; /* Повторяет 16- и 32-битные аппаратные типы TI C28x. */ +#define READY_FREQ 1000U /* Один такт Timer1 равен одной миллисекунде, как в действующей прошивке 166. */ +#define CANPOWSE 20U /* Прежний базовый интервал обнаружения обновлений данных составляет 20 мс. */ +#define ANSWER_LEN 128 /* Как в package.h, технологическая карта содержит ровно R0–R127 без дополнительного запаса для ошибочного чтения. */ +#define interrupt /* Host-модель вызывает настоящий ISR как обычную функцию без атрибутов процессора TI. */ +#define EALLOW ((void)0) /* Защищённый доступ аппаратных регистров отдельно проверяется настоящим компилятором TI. */ +#define EDIS ((void)0) /* Модель не подменяет логику изменения битов тайм-аута mailbox. */ +#define EINT ((void)0) /* Тесты явно выбирают момент аппаратного прерывания, исключая недетерминированные потоки. */ +#define DINT ((void)0) /* Сценарий проверяет восстановление маски, не симулируя планировщик ОС. */ +#define M_INT9 0x0100U /* Девятая группа PIE обслуживает eCAN в production-обработчике. */ +#define MINT9 0x0100U /* Сохраняет аппаратную маску CPU INT9 при обработке ошибки передачи. */ +#define MG96 0x00DFU /* Маска соответствует подавлению вложенного INTx6 в старом ISR проекта. */ +struct REG { Uint32 all; }; /* Даёт production-коду настоящие 32-битные поля регистров eCAN. */ +struct ECAN_REGS { struct REG CANTA, CANAA, CANTRR, CANTRS, CANTOC, CANTOS; Uint32 CANTSC; }; /* Моделирует только используемый жизненный цикл штатного mailbox. */ +static volatile struct ECAN_REGS ECanaRegs; /* Volatile сохраняет все наблюдаемые записи production-функций при оптимизации O3. */ +static struct { struct { struct REG MDH, MDL; } MBOX0; } ECanaMboxes; /* Восемь октетов передаваемого кадра наблюдаются независимо от кэша алгоритма. */ +static struct { Uint32 InterruptCount; } CpuTimer1; /* Счётчик переполняется в точности через 2^32 тактов. */ +static struct { struct { Uint16 all; } PIEIER9; union { unsigned all; struct { unsigned reserved:8, ACK9:1; } bit; } PIEACK; } PieCtrlRegs; /* Сохраняет реальное положение PIEACK9 в маске подтверждения IRQ. */ +static Uint16 IER; static unsigned int irq_mask; /* Позволяет проверить возврат исходного состояния прерываний после короткого снимка данных. */ +static int wait, CanTimeOutErrorTR, Mode, Desk, modbus[ANSWER_LEN]; /* Использует настоящий размер массива технологических слов и наблюдаемые параметры узла. */ +static unsigned int Maska[2][8]; /* Каждому потоку соответствует восемь 16-битных масок разрешённых регистров. */ +#define Cancount (modbus + 0x60) /* Как в package.h, R96 и R97 определяют включение двух потоков данных. */ +static unsigned int __disable_interrupts(void) { unsigned int previous = irq_mask; irq_mask = 1U; return previous; } /* Имитирует сохранение исходной маски IRQ даже при вложенном чтении Timer1. */ +static void __restore_interrupts(unsigned int previous) { irq_mask = previous; } /* Выявляет ошибочное безусловное разрешение прерываний отправителем. */ +static void led1_toggle(void) {} /* Индикация не влияет на подтверждение доставки или период таймера. */ +#include "can_telemetry_impl.h" /* Компилирует точный production-алгоритм, а не отдельную копию реализации в тесте. */ +/* TIMEOUT_ISR */ +#define CHECK(condition) do { if (!(condition)) { printf("FAIL line %d: %s\n", __LINE__, #condition); return 1; } } while (0) /* Проверка остаётся активной при оптимизации и показывает нарушенное ожидание. */ +static Uint32 frames_hi[2048], frames_lo[2048], frames_at[2048]; static unsigned int count; /* Журнал хранит только действительно запущенные аппаратные кадры и момент их передачи. */ +static void hardware_ack(void) { ECanaRegs.CANTRS.all &= ~1UL; ECanaRegs.CANTRR.all &= ~1UL; ECanaRegs.CANTA.all = 1UL; ECanaRegs.CANAA.all = 0UL; } /* Завершает именно mailbox 0 успешным аппаратным ACK. */ +static void prepare(unsigned int mode, unsigned int desk) { /* Начинает независимый сценарий, включая имитацию повторного запуска программы. */ + memset((void *)&ECanaRegs, 0, sizeof(ECanaRegs)); memset(&ECanaMboxes, 0, sizeof(ECanaMboxes)); memset(&PieCtrlRegs, 0, sizeof(PieCtrlRegs)); /* Сбрасывает только модель периферии между испытаниями. */ + memset(modbus, 0, sizeof(modbus)); memset(Maska, 0, sizeof(Maska)); CpuTimer1.InterruptCount = 0U; count = 0U; irq_mask = 0U; /* Устанавливает известное начальное содержимое карты и таймера. */ + Mode = mode; Desk = desk; Cancount[0] = 10; Cancount[1] = 101; IER = M_INT9; PieCtrlRegs.PIEIER9.all = 0x0030U; CanTimeOutErrorTR = 0; /* Периоды 10 и 101 проверяют независимость нового повтора от старых делителей. */ + CAN_telemetry_init(); /* Первый снимок после каждого запуска должен отправляться независимо от содержимого старых кэш-массивов. */ +} /* Аппаратный ACK в каждом сценарии задаётся явно. */ +static int pump(void) { /* Выдаёт всю уже готовую очередь с ACK между кадрами, не продвигая тестовое время. */ + unsigned int limit; /* Ограничивает итерации для обнаружения зависания на пустой маске или неизменном пакете. */ + for (limit = 0U; limit < 512U; ++limit) { /* Полная карта из двух потоков требует значительно меньше 512 аппаратных кадров. */ + CAN_telemetry_service(); /* Обслуживает реальную очередь, heartbeat и индивидуальные периоды подтверждённых данных. */ + CHECK(irq_mask == 0U); /* После любого пути отправки прерывания должны иметь первоначальное состояние. */ + if (!(ECanaRegs.CANTRS.all & 1UL)) return 0; /* Отсутствие нового TX означает завершённую выдачу готовых кадров. */ + CHECK(count < 2048U); /* Исключает скрытое переполнение тестового журнала. */ + frames_hi[count] = ECanaMboxes.MBOX0.MDH.all; frames_lo[count] = ECanaMboxes.MBOX0.MDL.all; frames_at[count++] = CpuTimer1.InterruptCount; /* Запоминает точные данные mailbox, не используя внутренний кэш production-кода как эталон. */ + hardware_ack(); /* Только успешное подтверждение разрешает алгоритму подавить следующий неизменный пакет. */ + } /* Бесконечное повторение выявляется как ошибка конечной модели. */ + CHECK(0); /* Даже при пустой маске или неподтверждённом адресе сервис обязан возвращаться без блокировки. */ + return 1; /* Сохраняет однозначный код ошибки для компилятора C. */ +} /* Все вызовы pump выполняются с фиксированным временем, поэтому сравнения периодов точны. */ +static unsigned int address_at(unsigned int index) { return (unsigned int)((frames_hi[index] >> 16) & 0x1FFFU); } /* Декодирует существующий маркер BALZAM независимо от внутренней реализации отправителя. */ +static unsigned int count_address(unsigned int address) { unsigned int index, found = 0U; for (index = 0U; index < count; ++index) if (address_at(index) == address) ++found; return found; } /* Считает реальные кадры выбранного технологического или heartbeat-адреса. */ +int main(void) { /* Проверяет требования пользователя, границы карты и ошибки аппаратного TX на настоящем коде прошивки. */ + unsigned int mode, before, index; Uint32 started; /* Сценарии используют независимые ожидаемые периоды в миллисекундах. */ + for (mode = 1U; mode <= 9U; ++mode) { /* Сравнение данных и повтор 1,3 с применяются ко всем аппаратным Mode, включая задатчик. */ + prepare(mode, mode == 8U ? 4U : 0U); Maska[0][0] = 1U; modbus[0] = 9; modbus[1] = 12; modbus[2] = -1; CHECK(pump() == 0); /* Начальный пакет должен передаться сразу в прежнем порядке трёх слов. */ + CHECK(count == 1U && frames_hi[0] == 0xE0000009UL && frames_lo[0] == 0x000CFFFFUL); /* Отрицательное 16-битное значение не портит упаковку и старший регистр. */ + CpuTimer1.InterruptCount = 999U; CHECK(pump() == 0 && count == 1U); /* Неизменившиеся данные не создают лишних кадров до секундного heartbeat. */ + CpuTimer1.InterruptCount = 1000U; CHECK(pump() == 0 && count == 2U && address_at(1U) == 0x100U); /* Heartbeat появляется ровно через одну секунду, независимо от обоих Cancount. */ + CHECK((frames_hi[1] & 0xFFFFUL) == 1U && frames_lo[1] == ((Uint32)Mode << 16 | (unsigned int)Desk)); /* Отдельный heartbeat содержит счётчик секунд, Mode и Desk. */ + CpuTimer1.InterruptCount = 1299U; CHECK(pump() == 0 && count_address(0U) == 1U); /* Технологический пакет не повторяется раньше индивидуального срока. */ + CpuTimer1.InterruptCount = 1300U; CHECK(pump() == 0 && count_address(0U) == 2U); /* Неизменные данные повторяются на границе 1,3 с без искусственного изменения состояния планировщика тестом. */ + CHECK(frames_at[count - 1U] == 1300U && frames_hi[count - 1U] == frames_hi[0]); /* Повторяет сами данные, а не только отдельный heartbeat. */ + } /* Завершает проверку одинакового поведения всех Mode. */ + prepare(5U, 0U); Maska[0][0] = 1U; Maska[1][0] = 8U; modbus[0] = 1; modbus[3] = 2; CHECK(pump() == 0 && count == 2U); /* Два потока получают отдельные пакеты и подтверждения без ожидания старых делителей. */ + CpuTimer1.InterruptCount = 40U; modbus[1] = 77; CHECK(pump() == 0 && count == 3U && address_at(2U) == 0U); /* Изменение второго слова пакета отправляется до истечения 1,3 с. */ + CpuTimer1.InterruptCount = 60U; modbus[2] = 88; CHECK(pump() == 0 && count == 4U && (frames_lo[3] & 0xFFFFUL) == 88U); /* Изменение третьего слова тоже обнаруживается и не подавляется кэшем первого регистра. */ + CpuTimer1.InterruptCount = 1300U; CHECK(pump() == 0 && count_address(3U) == 2U && count_address(0U) == 3U); /* Независимый пакет повторяется через 1,3 с, а обновлённый пакет R0 ещё не достиг своего нового срока. */ + CpuTimer1.InterruptCount = 1360U; CHECK(pump() == 0 && count_address(0U) == 4U); /* Период R0 отсчитывается заново от ACK последнего изменённого снимка. */ + prepare(8U, 4U); Maska[0][1] = 1U; Maska[1][0] = 15U; ECanaRegs.CANTRS.all = 1U; ECanaMboxes.MBOX0.MDH.all = 0x12345678UL; CAN_telemetry_service(); /* Занятый внешним запросом mailbox нельзя менять или считать доставленным. */ + CHECK(ECanaMboxes.MBOX0.MDH.all == 0x12345678UL && can_scan_due == 1U); /* Оба потока сохраняют готовность до физического освобождения передатчика. */ + ECanaRegs.CANTRS.all = 0U; CHECK(pump() == 0 && count_address(16U) == 1U && count_address(0U) == 1U && count_address(3U) == 1U); /* После занятости не теряются R0–R2 и R3–R5 медленного потока. */ + prepare(5U, 0U); Maska[0][0] = 1U; CAN_telemetry_service(); CHECK(wait && !(can_valid[0] & 1U)); /* Постановка первого пакета в TX ещё не означает успешную доставку. */ + ECanaRegs.CANTA.all = 0U; ECanaRegs.CANTOS.all = 1U; ECanaRegs.CANTOC.all = 5U; CANa_reset_err(); /* Воспроизводит аппаратный timeout, пока отдельный контроль служебного mailbox тоже включён. */ + CHECK(can_tx_failed && CanTimeOutErrorTR == 1 && ECanaRegs.CANTRR.all == 1U && ECanaRegs.CANTOC.all == 4U); /* ISR помечает отказ и отменяет только штатный TX без выключения служебного контроля. */ + ECanaRegs.CANTOS.all &= ECanaRegs.CANTOC.all; ECanaRegs.CANTRS.all = ECanaRegs.CANTRR.all = 0U; ECanaRegs.CANAA.all = 0U; CpuTimer1.InterruptCount = 10U; CAN_telemetry_service(); /* Отмена завершена без CANAA: отсутствие этого флага не должно навсегда заблокировать очередь. */ + CHECK(!wait && can_backoff && !(can_valid[0] & 1U)); /* Неудачный снимок не становится подтверждённым и остаётся кандидатом следующей попытки. */ + CpuTimer1.InterruptCount = 1009U; modbus[0] = 42; CAN_telemetry_service(); CHECK(!(ECanaRegs.CANTRS.all & 1U)); /* До окончания секундной паузы после ошибки повтор не засоряет CAN. */ + CpuTimer1.InterruptCount = 1010U; CHECK(pump() == 0 && count_address(0U) == 1U && (frames_hi[count - 1U] & 0xFFFFUL) == 42U); /* После паузы повторяет актуальное изменение и отдельно ожидающий heartbeat. */ + prepare(5U, 0U); Maska[0][0] = 1U; CAN_telemetry_service(); ECanaRegs.CANTA.all = 0U; CpuTimer1.InterruptCount = 100U; CAN_telemetry_service(); /* Потерянный timeout IRQ страхуется реальным временем Timer1 через 100 мс. */ + CHECK(can_tx_failed && ECanaRegs.CANTRR.all == 1U && CanTimeOutErrorTR == 1); /* Программная отмена не ждёт аппаратного CANAA в блокирующем цикле. */ + CAN_telemetry_service(); CHECK(CanTimeOutErrorTR == 1); /* Длительная аппаратная отмена не увеличивает счётчик ошибки на каждой итерации main. */ + prepare(5U, 0U); Maska[0][0] = 1U; CHECK(pump() == 0); CpuTimer1.InterruptCount = 20U; modbus[0] = 2; CAN_telemetry_service(); ECanaRegs.CANTA.all = 0U; ECanaRegs.CANTRS.all = 0U; ECanaRegs.CANAA.all = 1U; CAN_telemetry_service(); /* Изменение, отменённое после старого успешного снимка, не должно затереть подтверждённый кэш. */ + CHECK(can_last_hi[0] == 0xE0000000UL && can_backoff); /* Только реально доставленный прежний пакет остаётся подтверждённым. */ + prepare(5U, 0U); CHECK(pump() == 0 && count == 0U); CpuTimer1.InterruptCount = 3500U; CHECK(pump() == 0 && count == 1U && address_at(0U) == 0x100U && (frames_hi[0] & 0xFFFFU) == 3U); /* Пустые маски не зависают; после задержки main отправляется последний heartbeat без пачки пропущенных секунд. */ + CHECK(pump() == 0 && count == 1U); /* Повторная итерация main не дублирует уже поставленный heartbeat. */ + prepare(5U, 0U); Maska[0][0] = 1U; Cancount[0] = 0; Cancount[1] = 0; CHECK(pump() == 0 && count == 0U); CpuTimer1.InterruptCount = 1000U; CHECK(pump() == 0 && count == 1U && address_at(0U) == 0x100U); /* Отключённые потоки не отключают независимый heartbeat узла. */ + prepare(5U, 0U); Maska[0][7] = 0x8000U; modbus[127] = 0x55AA; CHECK(pump() == 0 && count == 1U && frames_hi[0] == 0xE07F55AAUL && frames_lo[0] == 0U); /* Конец карты R127 дополняется нулями без чтения data[128] и data[129]. */ + prepare(5U, 0U); for (index = 0U; index < 8U; ++index) Maska[0][index] = 0xFFFFU; CHECK(pump() == 0 && count == 43U); /* Плотная маска сохраняет прежние 43 трёхсловных пакета вместо лишних 128 перекрывающихся кадров. */ + for (index = 0U; index < count; ++index) CHECK(address_at(index) == index * 3U); /* Первый полный снимок покрывает ровно R0, R3, ... R126 в установленном формате. */ + CpuTimer1.InterruptCount = 1300U; CHECK(pump() == 0 && count_address(0U) == 2U && count_address(126U) == 2U && count == 87U); /* Готовая очередь выдаёт всю карту за период, а не один кадр маски каждые 1,3 с. */ + prepare(5U, 0U); CpuTimer1.InterruptCount = 0xFFFFFF00UL; started = CpuTimer1.InterruptCount; Maska[0][0] = 1U; CHECK(pump() == 0); CpuTimer1.InterruptCount = started + 1000U; CHECK(pump() == 0 && count_address(0x100U) == 1U); /* Секундный heartbeat переживает переполнение Timer1. */ + CpuTimer1.InterruptCount = started + 1300U; CHECK(pump() == 0 && count_address(0U) == 2U); /* Период повторения данных тоже корректен через 32-битное переполнение. */ + prepare(5U, 0U); Maska[0][0] = 1U; CHECK(pump() == 0); before = count; can_valid[7] = 0xFFFFU; can_backoff = can_heartbeat_due = 1U; wait = 1; CAN_telemetry_init(); CHECK(pump() == 0 && count == before + 1U); /* Повторная инициализация отбрасывает грязное состояние и заново отправляет первый снимок. */ + prepare(5U, 0U); Maska[0][1] = 1U; Maska[1][0] = 1U; for (index = 0U; index <= 250U; ++index) { CpuTimer1.InterruptCount = index * 20U; modbus[16] = index; CHECK(pump() == 0); } /* Непрерывное изменение быстрого потока не должно вытеснить heartbeat и неизменный пакет другого потока. */ + CHECK(count_address(16U) == 251U && count_address(0U) == 4U && count_address(0x100U) == 5U); /* За пять секунд доставляются все изменения, четыре состояния R0 и пять секундных heartbeat. */ + prepare(5U, 0U); Maska[0][0] = 1U; CAN_telemetry_service(); CpuTimer1.InterruptCount = 50U; hardware_ack(); CAN_telemetry_service(); /* Подтверждает первый пакет через 50 мс после запуска, а не в нулевой момент передачи. */ + CpuTimer1.InterruptCount = 1349U; CHECK(CAN_send(0, modbus, 0) == 2); CpuTimer1.InterruptCount = 1350U; CHECK(CAN_send(0, modbus, 0) == 1); /* Индивидуальные 1300 мс отсчитываются от ACK, поэтому ранний повтор через 1300 мс от запуска запрещён. */ + prepare(5U, 0U); ECanaRegs.CANTOC.all = 1U; ECanaRegs.CANTRS.all = 1U; CANa_reset_err(); /* Вызывает фантомный ISR без аппаратного CANTOS0 во время исправной передачи. */ + CHECK(!can_tx_failed && !CanTimeOutErrorTR && ECanaRegs.CANTRR.all == 0U && ECanaRegs.CANTOC.all == 1U && PieCtrlRegs.PIEIER9.all == 0x0030U); /* Не отменяет исправный кадр и восстанавливает прежние разрешения обоих CAN IRQ. */ + prepare(5U, 0U); Maska[0][0] = 1U; ECanaRegs.CANTRR.all = 1U; ECanaMboxes.MBOX0.MDH.all = 0x11223344UL; CAN_telemetry_service(); CHECK(ECanaMboxes.MBOX0.MDH.all == 0x11223344UL && !wait); /* Даже без CANTRS незавершённая аппаратная отмена запрещает перезапись mailbox. */ + prepare(5U, 0U); irq_mask = 1U; CHECK(CAN_send(0, modbus, 0) == 1 && irq_mask == 1U); /* Передатчик сохраняет уже запрещённые IRQ вызывающего кода вместо безусловного разрешения. */ + puts("PASS: heartbeat 1 Hz; changed data; per-packet refresh 1300 ms; all modes; masks; ACK, timeout and backoff; wraparound; dirty RAM; bounds"); /* Подтверждает проверку реального production-кода без утверждения о запуске на физической плате. */ + return 0; /* Все независимые поведенческие проверки завершились успешно. */ +} /* Модель не требует подключения CAN-адаптера и не отправляет команды реальному оборудованию. */ diff --git a/tests/can_telemetry/run_test.py b/tests/can_telemetry/run_test.py new file mode 100644 index 0000000..2522e36 --- /dev/null +++ b/tests/can_telemetry/run_test.py @@ -0,0 +1,22 @@ +from pathlib import Path # Находим production-исходники относительно установленного теста либо явно выбранного проверяемого снимка. +import os # Подключаем runtime GCC только дочернему процессу теста. +import re # Извлекаем настоящий ISR с сохранением вложенных условий и без отдельной копии алгоритма ошибки. +import subprocess # Компилируем и выполняем поведенческую модель передатчика. +import sys # Разрешаем проверить изолированный снимок коммита вместо текущего рабочего дерева. +import tempfile # Создаём исполняемый файл проверки только во временном каталоге теста. +test = Path(__file__).resolve().parent # Производственная установка хранит этот файл в tests/can_telemetry. +root = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else test.parents[1] # При обычном запуске проверяет текущий проект прошивки. +source = (root / 'Source/Internal/ecan.c').read_text(encoding='utf-8') # Изменённый production-файл и русские комментарии обязаны быть корректным UTF-8. +clean = re.sub(r'/\*.*?\*/|//[^\n]*', '', source, flags=re.S) # Скобки комментариев не влияют на выделение ISR. +match = re.search(r'interrupt\s+void\s+CANa_reset_err\(void\)\s*\{', clean) # Отличаем определение обработчика от прототипа и регистрации IRQ. +assert match # Отсутствующий обработчик не считается успешной проверкой передачи. +end, depth = match.end(), 1 # Начинаем поиск после внешней открывающей скобки функции. +while depth: depth += (clean[end] == '{') - (clean[end] == '}'); end += 1 # Сохраняем все вложенные аппаратные проверки оригинала. +model = (test / 'model.c').read_text(encoding='utf-8').replace('/* TIMEOUT_ISR */', clean[match.start():end]) # Сама реализация телеметрии подключается напрямую через production-header. +compiler = os.environ.get('CC', 'C:/msys64/mingw64/bin/gcc.exe') # Поддерживаем установленный GCC и явный выбор другого совместимого компилятора. +environment = dict(os.environ); environment['PATH'] = str(Path(compiler).parent) + os.pathsep + environment.get('PATH', '') # Не меняем системный PATH пользователя или настройки CCS. +with tempfile.TemporaryDirectory(prefix='can-telemetry-166-') as directory: # Временные файлы не попадают в источник прошивки или Git-коммит. + cfile, exe = Path(directory) / 'test.c', Path(directory) / 'test.exe' # Отделяем исходник модели и её исполняемый результат. + cfile.write_text(model, encoding='utf-8') # Тестовые комментарии сохраняются в той же кодировке UTF-8. + subprocess.run([compiler, '-std=c99', '-O3', '-Wall', '-Wextra', '-Werror', '-I' + str(root / 'Source/Internal/Include'), str(cfile), '-o', str(exe)], env=environment, check=True) # Все предупреждения нового алгоритма считаются ошибками до создания коммита. + subprocess.run([str(exe)], env=environment, check=True, timeout=10) # Зависание очереди или нарушенный тайминг завершают проверку ошибкой. diff --git a/tests/can_timeout/run_test.py b/tests/can_timeout/run_test.py index 1ccc70a..b00a86e 100644 --- a/tests/can_timeout/run_test.py +++ b/tests/can_timeout/run_test.py @@ -1,31 +1,4 @@ -"""Проверяет тайм-аут штатного CAN на настоящих функциях проекта 167.""" # Модель не заменяет испытание аппаратного eCAN. -import os # Передаёт пути библиотек GCC только дочерним процессам. -from pathlib import Path # Находит исходники независимо от рабочего каталога запуска. -import re # Удаляет комментарии и выделяет тела проверяемых функций. -import subprocess # Запускает компилятор и проверяет поведенческий результат модели. -import tempfile # Изолирует сгенерированные файлы от образов прошивки пользователя. -import sys # Позволяет проверить отдельно сохранённую старую версию ecan.c. - -root = Path(__file__).resolve().parents[2] # Корень проекта содержит рабочую реализацию eCAN. -source_path = Path(sys.argv[1]) if len(sys.argv) > 1 else root / "Source/Internal/ecan.c" # Дополнительный путь нужен для проверки чувствительности к старой ошибке. -source = re.sub(r"/\*.*?\*/|//[^\n]*", "", source_path.read_text(encoding="utf-8"), flags=re.S) # Комментарии со скобками не участвуют в выделении функций. - -def function(name): # Извлекает текущий алгоритм вместо поддержания отдельной копии в тесте. - match = re.search(r"(?:interrupt\s+)?void\s+" + name + r"\([^;{}]*\)\s*\{", source) # Отличает определение от вызова и объявления. - assert match is not None, name # Исчезновение функции должно останавливать проверку. - end, depth = match.end(), 1 # Начинает поиск после открывающей скобки тела. - while depth: # Учитывает вложенные условия до закрытия внешнего блока. - depth += (source[end] == "{") - (source[end] == "}") # Считает только скобки кода без комментариев. - end += 1 # Включает найденную закрывающую скобку в результат. - return source[match.start():end] # Сохраняет все реальные обращения к регистрам и условия. - -model = Path(__file__).with_name("model.inc").read_text(encoding="utf-8") # Загружает минимальное аппаратное окружение и сценарии потери ACK. -model = model.replace("/* PRODUCTION */", function("CAN_send") + "\n" + function("CANa_reset_err")) # Проверяет передачу и прерывание в одном жизненном цикле. -compiler = os.environ.get("CC", "C:/msys64/mingw64/bin/gcc.exe") # Использует установленный GCC, сохраняя возможность явно выбрать другой. -environment = dict(os.environ) # Не меняет окружение приложения и проекта CCS. -environment["PATH"] = str(Path(compiler).parent) + os.pathsep + environment.get("PATH", "") # Делает библиотеки MinGW доступными тестовой программе. -with tempfile.TemporaryDirectory(prefix="can-timeout-") as directory: # Удаляет только собственные временные файлы после завершения проверки. - c_path, exe_path = Path(directory) / "test.c", Path(directory) / "test.exe" # Разделяет исходник модели и исполняемый результат. - c_path.write_text(model, encoding="utf-8") # Сохраняет русские пояснения модели в требуемой кодировке. - subprocess.run([compiler, "-std=c99", "-O3", "-Wall", "-Wextra", "-Wno-unused-parameter", "-Werror", str(c_path), "-o", str(exe_path)], env=environment, check=True) # Проверяет реальные функции под оптимизацией и без скрытых предупреждений. - subprocess.run([str(exe_path)], env=environment, check=True, timeout=10) # Зависание и нарушенное условие делают сценарий неуспешным. +"""Проверяет аппаратный timeout и его программную страховку общими сценариями передатчика.""" # После переноса ACK-кэша отказ и секундная пауза проверяются вместе с повтором данных. +from pathlib import Path # Находит соседний тест без зависимости от текущего рабочего каталога. +import runpy # Выполняет тот же набор проверок реального production-header вместо устаревшей копии void CAN_send. +runpy.run_path(str(Path(__file__).resolve().parents[1] / "can_telemetry" / "run_test.py"), run_name="__main__") # Сохраняет прежнюю команду проверки timeout и проверяет также CANAA, фантомный IRQ и backoff. diff --git a/tests/can_vpu_rx/run_test.py b/tests/can_vpu_rx/run_test.py new file mode 100644 index 0000000..215fcfc --- /dev/null +++ b/tests/can_vpu_rx/run_test.py @@ -0,0 +1,68 @@ +"""Проверяет частоту подтверждения R3 на настоящих масках и планировщике 167 после переноса рассылки.""" # Проверка выполняется на ПК без отправки команд плате. +from pathlib import Path # Находит исходники проекта и отдельно подготовленный вариант маски. +import os # Передаёт библиотеки GCC только дочерним процессам проверки. +import re # Извлекает определения и блоки C без комментариев. +import subprocess # Компилирует и исполняет реальные алгоритмы прошивки в модели времени. +import sys # Позволяет проверить старый и исправленный файлы одинаковым сценарием. +import tempfile # Ограничивает результаты компиляции временным каталогом. + +root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).resolve().parents[2] # Установленная проверка по умолчанию использует свой проект прошивки. +measure = Path(sys.argv[2]) if len(sys.argv) > 2 else root / 'Source/Internal/measure.c' # Подмена только этого файла позволяет воспроизвести ошибку до применения исправления. +def clean(path): # Удаляет комментарии перед поиском парных скобок C. + raw = path.read_bytes() # Сохраняет возможность чтения старых исходников Windows-1251. + source = raw.decode('utf-8') if _utf8(raw) else raw.decode('cp1251') # Не теряет русские комментарии исходника при выборе кодировки. + return re.sub(r'/\*.*?\*/|//[^\n]*', '', source, flags=re.S) # Скобки в пояснениях не меняют границы блока алгоритма. +def _utf8(raw): # Отличает старую Windows-кодировку от требуемого UTF-8 без замены символов. + try: # Корректный UTF-8 имеет приоритет перед совместимой однобайтовой кодировкой. + raw.decode('utf-8'); return True # Успешное декодирование разрешает чтение современного исходника. + except UnicodeDecodeError: # Старый файл будет прочитан в его исходной кодировке. + return False # Не выдаёт повреждённые комментарии за исходный текст. +def block(source, start): # Выделяет настоящий блок с вложенными условиями. + cursor, depth = source.index('{', start) + 1, 1 # Начинает после открывающей скобки внешнего блока. + while depth: # Ищет соответствующую закрывающую скобку. + depth += (source[cursor] == '{') - (source[cursor] == '}') # Учитывает только скобки очищенного C-кода. + cursor += 1 # Включает завершающую скобку в извлечённый текст. + return source[start:cursor] # Проверка не поддерживает отдельную копию алгоритма прошивки. +package = clean(root / 'Source/Internal/Include/package.h') # Значения режимов и индексов потоков берутся из текущего проекта 167. +names = ('dsk_COMM', 'dsk_BKSD', 'dsk_BKST', 'dsk_EPLT', 'dsk_SHKF', 'dsk_LOAD', 'm_FAST', 'm_SLOW') # Модели нужны только константы, используемые масками. +defines = '\n'.join(re.search(r'^#define\s+' + name + r'\s+\d+', package, re.M).group() for name in names) # Не дублирует номера плат в тестовой карте. +measure_source = clean(measure) # Выбирает проверяемую версию Init_packMask. +mask_function = block(measure_source, measure_source.index('void Init_packMask(')) # Состав быстрой и медленной рассылки полностью берётся из прошивки. +ecan = clean(root / 'Source/Internal/ecan.c') # Реальный ISR завершает ту же передачу, что и новый сервис телеметрии. +match = re.search(r'interrupt\s+void\s+CANa_reset_err\(void\)\s*\{', ecan) # Находит определение обработчика ошибок вместо его прототипа. +assert match is not None # Исчезновение обработчика должно проваливать проверку. +model = (root / 'tests/can_telemetry/model.c').read_text(encoding='utf-8').split('int main(void)')[0] # Повторно использует аппаратуру, ACK и журнал кадров общей модели нового передатчика. +model = model.replace('/* TIMEOUT_ISR */', block(ecan, match.start())) # Вставляет реальное тело обработчика без отдельной копии его алгоритма. +model += '\n' + defines + '\nstatic int Kurrent, pow_addr, Owen; /* Повторяет настройки каналов и OWEN, используемые настоящими масками версии 167. */\n' + mask_function # Берёт реальные номера плат, потоков и состав масок из текущей прошивки. +model += r''' +int main(void) { /* Измеряет свежесть R3 при реальной карте пульта и разных настройках потоков EEPROM. */ + const unsigned periods[][2] = {{2,101},{10,101},{12,50},{2,0},{1,101},{1,1}}; /* Проверяет прежние настройки, включая отключённый медленный поток. */ + unsigned scenario, tick, cursor, received, last, gap, worst; /* Наблюдает переданные кадры и разрыв между обновлениями R3. */ + for (scenario = 0; scenario < sizeof(periods)/sizeof(periods[0]); ++scenario) { /* Проверяет независимые запуски платы. */ + prepare(8U, dsk_EPLT); Init_packMask(); /* Строит настоящую карту задатчика поверх сброшенной модели CAN. */ + CHECK((Maska[m_FAST][0] & 15U) == 15U && !(Maska[m_SLOW][0] & 15U)); /* R0–R3 остаются в быстром потоке при отключённом медленном. */ + CHECK(Maska[m_FAST][1] == 1U && Maska[m_SLOW][6] == 0x1FFU && Maska[m_SLOW][7] == 0xE000U); /* Кнопки и параметры сохраняют состав исходной карты. */ + Cancount[0] = periods[scenario][0]; Cancount[1] = periods[scenario][1]; /* Ненулевое значение теперь включает обновления по изменению и таймеру. */ + cursor = received = last = worst = 0U; /* Учитывает также начальное ожидание первого подтверждения. */ + for (tick = 1U; tick <= 5000U; ++tick) { /* Пять секунд включают heartbeat и обязательные повторы неизменных данных. */ + CpuTimer1.InterruptCount = tick; modbus[3] = tick; CHECK(pump() == 0); /* Меняет R3 каждую миллисекунду и выдаёт готовую очередь через настоящие CAN_send и ACK. */ + for (; cursor < count; ++cursor) { /* Анализирует только новые кадры mailbox. */ + if (address_at(cursor) != 3U) continue; /* R3 начинает второй трёхсловный фрагмент действительной маски. */ + CHECK((frames_hi[cursor] & 0xFFFFU) == tick); /* Требует свежее значение вместо старого снимка. */ + gap = tick - last; if (gap > worst) worst = gap; last = tick; ++received; /* Измеряет худший интервал получения актуального R3. */ + } /* Завершает анализ кадров текущего момента. */ + } /* Приоритет heartbeat обрабатывается общей моделью передатчика. */ + CHECK(received > 0U && worst <= CANPOWSE && tick - 1U - last <= CANPOWSE); /* При немедленном ACK интервал ограничен шагом сканирования 20 мс независимо от slow. */ + CHECK(count_address(CAN_HEARTBEAT_ADDR) >= 4U); /* Обновление R3 не должно вытеснять секундный heartbeat. */ + printf("PASS VPU fast=%u slow=%u: R3 max=%u ms, updates=%u\n", periods[scenario][0], periods[scenario][1], worst, received); /* Выводит измеренный интервал и число свежих кадров. */ + } /* Завершает матрицу настроек EEPROM. */ + return 0; /* Подтверждает программную модель без заявления об аппаратном испытании. */ +} /* Завершает проверку обратной связи R3. */ +''' # Использует production-маски и production-сервис вместо удалённого блока if(CanGO). +compiler = Path(os.environ.get('CC', 'C:/msys64/mingw64/bin/gcc.exe')) # Использует установленный GCC без изменения системных настроек. +environment = dict(os.environ, PATH=str(compiler.parent) + os.pathsep + os.environ.get('PATH', '')) # Добавляет runtime только дочерним процессам. +with tempfile.TemporaryDirectory(prefix='vpu-r3-rx-') as directory: # Отделяет временные результаты от образов пользователя. + source, executable = Path(directory) / 'test.c', Path(directory) / 'test.exe' # Разделяет исходник и исполняемый результат. + source.write_text(model, encoding='utf-8') # Сохраняет русские пояснения в UTF-8. + subprocess.run([str(compiler), '-std=c99', '-O3', '-Wall', '-Wextra', '-Werror', '-I' + str(root / 'Source/Internal/Include'), str(source), '-o', str(executable)], env=environment, check=True) # Компилирует настоящий заголовок при строгих предупреждениях. + subprocess.run([str(executable)], env=environment, check=True, timeout=10) # Нарушение задержки или зависание проваливает проверку. diff --git a/tests/firmware_identity/test_release.py b/tests/firmware_identity/test_release.py new file mode 100644 index 0000000..fe065a1 --- /dev/null +++ b/tests/firmware_identity/test_release.py @@ -0,0 +1,181 @@ +"""Проверяет выпуск 167: старый OUT, EEPROM, обрезанный SCI8 и отсутствие публикации при отказе.""" # Использует производственные проверки и настоящий TI-конвертор при доступном toolchain. +from datetime import datetime # Независимо восстанавливает регистры даты для контрольного образа. +import hashlib # Сравнивает постоянные каталоги до и после отклонённого выпуска. +import importlib.util # Загружает существующий общий seal/archive без изменения templates. +import json # Создаёт минимальный проект и проверяет поля конечного manifest. +import os # Позволяет указать установленный TI toolchain переменной TI_CGT_ROOT. +from pathlib import Path # Изолирует все временные файлы от настоящих generated и Bin проекта. +import struct # Формирует независимые контрольные SCI8-блоки согласно формату загрузчика TI. +import subprocess # Компилирует минимальный настоящий COFF для проверки интеграции с hex2000. +import sys # Подключает локальные prebuild/postbuild из каталога CCS-проекта. +import tempfile # Удаляет только временные каталоги этой проверки после завершения тестов. +import unittest # Группирует успешные и отказные сценарии с отдельными результатами. +from unittest.mock import patch # Подменяет только конверсию для испытания повреждённых результатов инструментов. + +ROOT = Path(__file__).resolve().parents[2] # Находит исходники проверяемой 167 независимо от рабочего каталога запуска. +sys.path.insert(0, str(ROOT / "UKSSTMS320F28335")) # Импортирует производственные функции без запуска их CLI. +import prepare_identity as prebuild # Проверяет фактическую генерацию ручной версии и физического профиля. +import publish_identity as release # Вызывает именно обязательный защищённый postbuild проекта. +VENDOR = ROOT.parent / "newProject/SETGUI/third_party/templates" # Совпадает с include paths обеих конфигураций CCS. +TOOLS = ROOT.parent / "newProject/templates" # Полная рабочая библиотека содержит общий BAT-конвертор и архивирование. +COMPILER = Path(os.environ.get("TI_CGT_ROOT", "C:/ti/ccs1230/ccs/tools/compiler/ti-cgt-c2000_22.6.0.LTS")) # Проверяет установленный TI без загрузки нового toolchain. +SPEC = importlib.util.spec_from_file_location("test_common_identity", TOOLS / "c/firmware-info/tools/identity.py") # Подключает тот же seal/archive, который вызывает postcompile. +IDENTITY = importlib.util.module_from_spec(SPEC) # Отделяет общие инструменты тестов от производственного prebuild. +SPEC.loader.exec_module(IDENTITY) # Неполная зависимость явно останавливает тест вместо пропуска проверок выпуска. + + +def identity_words(metadata): # Независимый контрольный вектор проверяет общий 12-словный контракт, а не копирует поиск production-кода. + stamp = datetime.fromisoformat(metadata["built_at_utc"]) # Использует фиксированное время того же тестового prebuild. + result = [1, *metadata["version"], stamp.year, stamp.month * 256 + stamp.day, stamp.hour * 256 + stamp.minute, stamp.second] # Первые восемь слов содержат формат, ручную версию и UTC. + result += [ord(metadata["build_id"][index]) * 256 + ord(metadata["build_id"][index + 1]) for index in range(0, 8, 2)] # Последние четыре слова хранят ASCII Build ID в сетевом порядке. + return result # Возвращает числа для независимой SCI8-таблицы и настоящего COFF-фикстура. + + +def block(address, words): # Строит минимальный загрузочный блок с адресом в словах C28x. + return struct.pack("> 16, address & 65535) + struct.pack("<" + "H" * len(words), *words) # Поля длины, старшего/младшего адресов и данных соответствуют документации SCI8. + + +def image_for(metadata): # Формирует два непересекающихся блока RAM с загруженной точкой входа и неизменяемым паспортом. + return b"\xaa\x08" + bytes(16) + struct.pack(" CODE PAGE=0 .econst: > DATA PAGE=0 } /* Создаёт два полных непересекающихся блока SCI8. */\n' + f'_EEPROM_TOTAL_BYTES={self.metadata["eeprom_bytes"]}; /* Физическая ёмкость выбранной EEPROM в октетах. */\n_EEPROM_FIRMWARE_LIMIT_BYTES={self.metadata["max_image_bytes"]}; /* Конец области прошивки до параметров, в октетах. */\n_EEPROM_PARAMS_START_WORD={self.metadata["max_image_bytes"] // 2}; /* Адрес первого слова сохранённых параметров. */\n', encoding="utf-8") # Абсолютные символы проходят ту же проверку, что F28335.cmd настоящего проекта. + subprocess.run([str(COMPILER / "bin/cl2000.exe"), "-v28", "--abi=coffabi", "-c", "--output_file=" + str(self.build / "fixture.obj"), str(assembly)], check=True) # Получает настоящий объект TI без аппаратного программирования. + subprocess.run([str(COMPILER / "bin/cl2000.exe"), "-v28", "--abi=coffabi", "-z", "--entry_point=_fixture_entry", "-m" + str(self.map_path), "-o", str(self.build / "application.out"), str(self.build / "fixture.obj"), str(linker)], check=True) # Линкует настоящий COFF и абсолютную карту выбранного EEPROM. + with self.assertRaises(ValueError): # Настоящий RAM COFF нельзя выдать за пригодный JTAG Flash OUT только из-за одинакового расширения. + release.verify_flash_coff(self.build / "application.out", COMPILER) # Проверяет отказ на фактических секциях TI вместо поддельной тестовой MAP. + image = self.invoke() # Запускает реальный общий BAT и hex2000, сохраняя обязательные производственные проверки и публикацию. + release.verify_embedded(image, self.metadata) # Дополнительно проверяет фактические байты, опубликованные после конверсии TI. + release.verify_package(image, self.metadata) # Подтверждает совпадение аппаратного manifest и контрольной суммы конечного файла. + + +if __name__ == "__main__": # Позволяет запускать всю проверку обычной командой Python из корня проекта. + unittest.main(verbosity=2) # Каждый отказный сценарий явно виден в отчёте без зависимости от pytest. diff --git a/tests/flash_rs/run_test.py b/tests/flash_rs/run_test.py new file mode 100644 index 0000000..306d97f --- /dev/null +++ b/tests/flash_rs/run_test.py @@ -0,0 +1,47 @@ +"""Компилирует реальные декодеры Flash RS на host и проверяет формат готового образа.""" # Проверяет производственную проверку границ независимо от реализации GUI. +import ctypes # Вызывает декодер C с точными 16- и 32-битными типами, соответствующими ABI C28x. +import os # Даёт GCC доступ к DLL установленного runtime Windows. +from pathlib import Path # Находит производственный источник и результаты сборки относительно теста. +import struct # Создаёт таблицы TI с заведомо известными допустимыми и запрещёнными адресами. +import subprocess # Компилирует настоящие функции производственного загрузчика. +import tempfile # Изолирует временный C-модуль и библиотеку от исходников прошивки. +ROOT = Path(__file__).resolve().parents[2] # Корень содержит проверяемый flash_rs.c и транспортный BIN текущей сборки. +GCC = Path('C:/msys64/mingw64/bin/gcc.exe') # Использует тот же host-компилятор, что существующие проверки прошивки. +os.environ['PATH'] = str(GCC.parent) + os.pathsep + os.environ['PATH'] # Подключает runtime DLL GCC только к процессам проверки. +source = (ROOT / 'Source/Internal/flash_rs.c').read_text(encoding='utf-8') # Читает реальные функции, а не переписывает алгоритм в тестовой модели. +helpers = source[source.index('#pragma CODE_SECTION(byte_at'):source.index('#pragma CODE_SECTION(reply_status')] # Берёт производственное преобразование упаковки RS и расчёт CRC. +validator = source[source.index('#pragma CODE_SECTION(validate_image'):source.index('#pragma CODE_SECTION(program_image')] # Берёт производственную проверку всех блоков и запрещённых диапазонов. +prefix = '#include /* Задаёт точные типы host-модели ABI C28x. */\ntypedef uint16_t Uint16; /* Слово C28x содержит 16 бит. */\ntypedef uint32_t Uint32; /* Адрес и длина C28x занимают 32 бита. */\n' # Сохраняет арифметические размеры оригинального кода. +wrapper = '__declspec(dllexport) Uint16 check_image(const Uint16 *octets, Uint32 length) /* Открывает реальный валидатор для Python. */\n{ /* Начинает упаковку RS-октетов host-модели. */\nstatic Uint16 packed[64512]; /* Внешняя RAM ZONE7A содержит 0xFC00 слов. */\nUint32 i; /* Счётчик октетов проверяемого массива. */\nfor(i=0;i<(length+1)/2;++i) packed[i]=0; /* Исключает остатки предыдущего сценария. */\nfor(i=0;i> 16, address & 65535) + struct.pack('<' + 'H' * len(words), *words) # Длина выражена словами, адрес содержит старшую половину перед младшей. + + +header = b'\xaa\x08' + bytes(16) + struct.pack(' /* Подключает : настройки и интерфейс string. */ #include +#define FLASH_BOOT 1 /* Проверяет дополнительный read-only запрос безопасности, который отсутствует в обычном RAM-образе. */ /* Подключает "../../Source/Internal/DAC.c": выход ЦАП и автомат защиты нагрузки с проверкой напряжения и свежести CAN. */ #include "../../Source/Internal/DAC.c" +#include "production_flash_guard.inc" /* Вызывает настоящий запрет FlashRs_Command перед CRC/erase на том же production-автомате нагрузки. */ /* Объявляет READY — данные READY; Desk — аппаратно определённый тип платы; Mode — рабочий адрес или режим платы, выбранный перемычками. Начальное значение задаётся справа от знака равенства. */ int READY = 1, Desk = 6, Mode = 5; /* Объявляет modbus — массив технологических регистров; слово int на C28x занимает 16 бит. */ @@ -198,6 +200,16 @@ static int attempt_limit_test(void) { /* Запускает новые стартовые сценарии и прежние регрессии автомата на программных подменах периферии. */ int main(void) { + setup(5); CHECK(flash_load_guard(0) == 0 && flash_load_guard(24) == 0x1005U); /* Probe разрешён при холодном старте, запись запрещена до штатного STOP и подтверждённого ЦАП. */ + Commands = 0x80; tick(10, 1, 200); /* Поднимает реальную нагрузку через Timer1-модель с START, разрешением и ненулевым заданием. */ + CHECK(load_state == LOAD_RUN && flash_load_guard(24) == 0x1005U && flash_load_guard(0) == 0); /* Активный LOAD_RUN отклоняется до Flash API; проверка возможностей не меняет рабочие выходы. */ + CHECK(test_start && test_permission && !test_stop); /* Отказ загрузчика не выполняет кустарную аварийную остановку GPIO вместо штатного автомата. */ + Commands = 0; tick(1, 1, 200); CHECK(flash_load_guard(24) == 0x1005U); /* Только начало STOP ещё не разрешает erase: нужно дождаться базового тока и полной выдержки. */ + tick(1000, 1, 200); CHECK(Load_flash_safe() && flash_load_guard(24) == 0); /* После штатного idle, STOP длительностью не менее секунды и SPI-отправки 4 мА запись разрешается. */ + CHECK(!test_start && !test_permission && test_stop && modbus[123] == DAC_min()); /* Проверяет программно подтверждённые безопасные выходы до будущего отключения Timer1. */ + Commands = 0x200; CHECK(flash_load_guard(24) == 0x1005U); /* Отложенная калибровка не должна изменить аналоговое задание во время автономного программирования. */ + Commands = 0; DAC_min()++; CHECK(flash_load_guard(24) == 0x1005U); /* Новая калибровка минимума требует фактической повторной SPI-отправки, а не доверия старому флагу нулевого задания. */ + modbus[123] = DAC_min(); CHECK(flash_load_guard(24) == 0x1005U); /* Подмена открытого диагностического регистра не заменяет приватное подтверждение последнего отправленного кода. */ /* Объявляет out — данные out; error — ошибка; count — число; ramp — данные ramp; code — данные code. */ unsigned int out[8], error, count, ramp, code, stopped_at; /* Сначала проверяет исправляемый штатный пуск с 6 мА, затем выполняет остальные регрессии защиты. */ diff --git a/tests/load_control/parameter_commands_test.c b/tests/load_control/parameter_commands_test.c new file mode 100644 index 0000000..a54d48d --- /dev/null +++ b/tests/load_control/parameter_commands_test.c @@ -0,0 +1,62 @@ +/* Проверяет реальные Default_params/Load_params и защиту живого слова команд от сохранённого START в EEPROM. */ +#include /* Делает нарушение сохранности команд, атомарности или состояния IRQ ошибкой теста. */ +#include /* Печатает результат самостоятельной регрессии загрузки параметров. */ +#include /* Копирует модель EEPROM и обнуляет регистры между независимыми сценариями. */ +#define interrupt /* Убирает аппаратный атрибут ISR при подключении настоящих типов и констант датчиков на ПК. */ +#include "../../Source/Internal/Include/filter_bat2.h" /* Предоставляет настоящий тип FILTERBAT, необходимый объявлениям measure.h. */ +#include "../../Source/Internal/Include/measure.h" /* Подключает настоящие ERROR, частоты и объявления глобальных измерительных данных. */ +typedef union { int all; struct { unsigned int reserved:4, bit4:1; } bit; } WORDE; /* Сохраняет адресацию слова R127 и бита калибровки4 в модели GCC. */ +#include "../../Source/Internal/Include/package.h" /* Использует настоящие адреса R127, калибровок, тайм-аута и лимита пусков профиля167. */ +#include "../../Source/Internal/Include/caliber.h" /* Даёт Default_params реальные заводские таблицы167 для всех режимов платы. */ +#define ANSWER_LEN 128U /* Повторяет длину технологической карты без аппаратных зависимостей message.h. */ +#define BALSAM167_EEPROM_PARAMS_START_WORD 0x3F7EU /* Адрес не влияет на управление: модель проверяет чтение полного блока параметров без запрета IRQ. */ +int modbus[129], READY, Desk, Mode, Kalibro, Owen, Kurrent, TermoSW; /* Предоставляет рабочим функциям регистры и признаки режима платы. */ +unsigned int param[129], Caliber_time; /* Предоставляет считанный EEPROM-блок и таймер калибровки из рабочего интерфейса. */ +int sens_type[24]; /* Позволяет заводской инициализации обращаться к таблице типов SHKF без изменения рабочего алгоритма. */ +static unsigned int irq_mask, saved_count, concurrent_command; /* Хранит исходную маску IRQ, число записей EEPROM и команду, принятую во время чтения. */ +static unsigned int image[129]; /* Моделирует отдельную сохранённую EEPROM, чтобы загрузка не читала текущие команды обратно из RAM. */ +static unsigned int __disable_interrupts(void) { unsigned int old = irq_mask; irq_mask = 1; return old; } /* Проверяет вложенное сохранение маски при применении RAM-настроек. */ +static void __restore_interrupts(unsigned int old) { irq_mask = old; } /* Возвращает переданную вызывающим кодом маску, включая заранее запрещённые IRQ. */ +static void Seeprom_read(unsigned long address, unsigned int *data, unsigned int bytes) /* Моделирует только длительное чтение EEPROM, не подменяя Default/Load. */ +{ /* Начинает передачу проверенного набора параметров в рабочий буфер param. */ + assert(!irq_mask && address == BALSAM167_EEPROM_PARAMS_START_WORD && bytes == 258U); /* EEPROM-драйвер нуждается в прерываниях; 129 слов занимают258 байт C28x. */ + memcpy(data, image, sizeof(image)); /* Считывает в том числе сохранённый START, чтобы проверить его исключение из применения параметров. */ + if (concurrent_command) Commands = concurrent_command; /* Имитирует принятие нового START/RESET по CAN во время длительного чтения EEPROM. */ +} /* Заканчивает чтение при исходно разрешённых прерываниях. */ +static unsigned int get_crc16(const unsigned int *data, unsigned int words) { assert(data == param && words == ANSWER_LEN); return 0x1234U; } /* Выбирает валидную ветку по контрольному слову image без повторения алгоритма загрузки. */ +void Save_params(void) { assert(!irq_mask); saved_count++; } /* Проверяет, что восстановление заводских настроек не удерживает IRQ во время записи EEPROM. */ +void Init_sensors_more(void) { assert(!irq_mask); } /* Проверяет освобождение короткой RAM-секции перед расчётом измерительных коэффициентов. */ +#include "production_parameters.inc" /* Подключает неизменённые reset_parameter_commands, Default_params и Load_params из текущего рабочего message.c. */ +static void setup(unsigned int live, unsigned int stored, unsigned int crc) /* Готовит независимую плату нагрузки и EEPROM с намеренно отличающимся словом команд. */ +{ /* Сбрасывает модель, сохраняя реальное расположение регистров профиля167. */ + memset(modbus, 0, sizeof(modbus)); memset(image, 0, sizeof(image)); /* Удаляет состояние предыдущего сценария только в тестовой модели. */ + READY = 1; Desk = dsk_LOAD; Mode = adr_LOA1; Kalibro = Kurrent = 1; Owen = TermoSW = 0; /* Выбирает готовую плату нагрузки с заводской калибровкой167. */ + Commands = live; image[127] = stored; image[126] = Mode; image[ANSWER_LEN] = crc; /* Разделяет текущую команду интерфейса и потенциально опасный сохранённый START. */ + image[0x52] = 2000; image[0x7D] = 7; irq_mask = saved_count = concurrent_command = 0; /* Задаёт допустимые новые защиты и независимые счётчики модели. */ +} /* Завершает подготовку до вызова настоящего рабочего кода. */ +int main(void) /* Проверяет режимы RUN/STOP/RESET/FAULT через сохранение уровней, на которых работает неизменённый автомат DAC167. */ +{ /* Начинает все комбинации текущих и сохранённых команд. */ + unsigned int live, stored; /* Перебирает четыре сочетания START/RESET независимо от содержимого EEPROM. */ + for (live = 0; live <= 0xC0U; live += 0x40U) { /* Проверяет ожидание, RESET, START и совместное удержание двух управляющих битов. */ + for (stored = 0; stored <= 0xC0U; stored += 0x40U) { /* Не допускает ни отмены текущего START, ни появления START из EEPROM. */ + setup(live | 0x0305U, stored, 0x1234U); Load_params(); /* Загружает корректный блок с дополнительными одноразовыми служебными командами. */ + assert((unsigned int)Commands == live && LOAD_MAX_ATTEMPTS == 7 && LOAD_CAN_TIMEOUT == 2000 && !irq_mask && !saved_count); /* Сохраняет только живые START/RESET и действительно применяет новые настройки защиты. */ + setup(live | 0x0305U, stored, 0); Load_params(); /* Проверяет ту же гарантию при неверной CRC и автоматическом восстановлении заводских параметров. */ + assert((unsigned int)Commands == live && saved_count == 1 && LOAD_MAX_ATTEMPTS == 5 && LOAD_CAN_TIMEOUT == 2000 && !irq_mask); /* Сохранение исправленного блока EEPROM не очищает управляющие уровни нагрузки. */ + } /* Завершает все варианты сохранённых команд для текущего живого уровня. */ + setup(live | 0x0305U, 0, 0x1234U); Default_params(); /* Проверяет прямую команду Default без предварительного чтения EEPROM. */ + assert((unsigned int)Commands == live && !irq_mask && LOAD_MAX_ATTEMPTS == 5); /* Заводской сброс сохраняет RUN/STOP-семантику, удаляя одноразовые служебные флаги. */ + } /* Завершает проверку всех четырёх живых сочетаний START/RESET. */ + setup(0, 0x80U, 0x1234U); concurrent_command = 0xC0U; Load_params(); /* Передаёт новый RESET/START во время чтения вместо снимка, снятого до EEPROM. */ + assert(Commands == 0xC0U && !irq_mask); /* Не теряет последнюю принятую по интерфейсу команду при завершении Load. */ + setup(0xC0U, 0x80U, 0x1234U); READY = 0; Load_params(); /* На холодном старте имитирует и мусор RAM, и сохранённый START EEPROM. */ + assert(!Commands && !irq_mask); /* До READY ни один источник сохранённого состояния не разрешает автоматический пуск. */ + setup(0xC0U, 0, 0x1234U); READY = 0; Default_params(); /* Проверяет ту же стартовую очистку в заводской ветке без EEPROM. */ + assert(!Commands && !irq_mask); /* Начальные START/RESET обязательно очищены до разрешения рабочего такта платы. */ + setup(0xC0U, 0x80U, 0x1234U); Desk = dsk_COMM; Kalibro = Kurrent = 0; Load_params(); /* У остальных плат сохраняет прежнее поведение очистки всех команд после загрузки. */ + assert(!Commands && !irq_mask); /* Не переносит специальную семантику УМП на несвязанные типы плат. */ + setup(0x2C0U, 0, 0x1234U); irq_mask = 1; reset_parameter_commands(); /* Проверяет вызов helper из уже защищённого блока применения параметров. */ + assert(Commands == 0xC0U && irq_mask == 1); /* Вложенный helper сохраняет внешнюю маску IRQ и не выдаёт EINT без разрешения вызывающего кода. */ + puts("PASS: production parameter loading/defaults preserve live START/RESET and reject EEPROM commands"); /* Сообщает о прохождении рабочего кода, а не копии алгоритма в модели. */ + return 0; /* Возвращает успех только после всех сценариев сохранности команд и состояния IRQ. */ +} /* Завершает регрессию без обращения к аппаратной плате. */ diff --git a/tests/load_control/run_test.py b/tests/load_control/run_test.py index ab98efa..63d6efd 100644 --- a/tests/load_control/run_test.py +++ b/tests/load_control/run_test.py @@ -3,6 +3,8 @@ """Compile the production DAC.c against GPIO/IRQ mocks; no hardware writes.""" # Подключает модуль os для этой проверки или подготовки сборки. import os +# Разбирает только границы настоящих функций message.c, сохраняя их неизменённое тело в регрессии параметров. +import re # Импортирует необходимые имена: from pathlib import Path. from pathlib import Path # Подключает модуль shutil для этой проверки или подготовки сборки. @@ -22,11 +24,51 @@ os.environ['PATH'] = str(Path(compiler).resolve().parent) + os.pathsep + os.envi with tempfile.TemporaryDirectory(prefix='load-control-') as tmp: # Задаёт exe по выражению справа; значение используется последующими операциями. exe = Path(tmp) / 'test.exe' + flash = (root / 'Source/Internal/flash_rs.c').read_text(encoding='utf-8') # Проверяет настоящий запрет активной нагрузки перед переходом к Flash API. + guard = re.search(r'^static unsigned int flash_load_guard\([^\n]*\n\{[^\n]*\n[^\n]*\n\}', flash, re.M) # Извлекает короткий production-helper без переписывания его условия в тестовой модели. + assert guard, 'Production Flash load guard missing' # Переименование или удаление защиты не должно незаметно отключить тест перед erase. + (Path(tmp) / 'production_flash_guard.inc').write_text(guard.group() + '\n', encoding='utf-8') # Включает реальный helper в тот же C-модуль, который исполняет production DAC.c. # Запускает внешний компилятор, генератор или тест; ненулевой результат проверяется параметрами вызова. subprocess.run([compiler, '-Wall', '-Wextra', '-std=c99', # Продолжает текущий вызов, список или выражение: '-Itests/load_control/stubs', '-ISource/Internal/Include',; порядок аргументов сохраняется. - '-Itests/load_control/stubs', '-ISource/Internal/Include', + '-Itests/load_control/stubs', '-ISource/Internal/Include', '-I' + tmp, # Добавляет только каталог извлечённого рабочего Flash-helper этой проверки. # Задаёт 'tests/load_control/load_control_test.c', '-o', str(exe)], cwd по выражению справа; значение используется последующими операциями. 'tests/load_control/load_control_test.c', '-o', str(exe)], cwd=root, check=True) # Запускает внешний компилятор, генератор или тест; ненулевой результат проверяется параметрами вызова. subprocess.run([str(exe)], check=True, timeout=10) + # Читает рабочие загрузку и заводской сброс параметров для отдельной проверки живых START/RESET. + message = (root / 'Source/Internal/message.c').read_text(encoding='utf-8') + # Маскирует комментарии пробелами той же длины, чтобы фигурные скобки пояснений не влияли на поиск границ функций. + masked = re.sub(r'/\*.*?\*/|//[^\n]*', lambda match: ' ' * len(match.group()), message, flags=re.S) + # Собирает только проверяемые рабочие функции: EEPROM и GPIO заменяются моделью, сам алгоритм загрузки остаётся исходным. + functions = [] + # Извлекает helper и обе ветки применения параметров независимо от форматирования вложенных блоков в исходнике. + for name in ('reset_parameter_commands', 'Default_params', 'Load_params'): + # Находит объявление функции с настоящим именем и необязательным static. + match = re.search(r'^(?:static )?void ' + name + r'\(', masked, re.M) + # Не допускает молчаливого запуска модели после удаления или переименования рабочего обработчика. + assert match, name + # Начинает подсчёт вложенных блоков с первой открывающей скобки тела найденной функции. + end, depth = masked.index('{', match.start()), 0 + # Идёт до парной закрывающей скобки, сохраняя все вложенные условия и циклы рабочего кода. + while end < len(masked): + # Учитывает открывающую скобку только после удаления комментариев. + depth += masked[end] == '{' + # Учитывает соответствующую закрывающую скобку в том же уровне вложенности. + depth -= masked[end] == '}' + # Переходит к следующему символу, включая последнюю закрывающую скобку в извлекаемый текст. + end += 1 + # Нулевой уровень означает завершение всей функции, а не первого вложенного if. + if depth == 0: + # Завершает поиск, чтобы не включить соседний обработчик Modbus. + break + # Копирует исходный текст функции вместе с русскими комментариями без изменения условий и присваиваний. + functions.append(message[match.start():end]) + # Передаёт извлечённый production-код компилятору из временного каталога, не записывая generated-файлы в репозиторий. + (Path(tmp) / 'production_parameters.inc').write_text('\n'.join(functions), encoding='utf-8') + # Выбирает отдельный исполняемый файл, чтобы проверка параметров не подменяла прежние регрессии автомата DAC. + parameters_exe = Path(tmp) / 'parameters.exe' + # Компилирует реальную загрузку параметров с контролем IRQ, модели EEPROM и содержимого R127. + subprocess.run([compiler, '-Wall', '-Wextra', '-std=c99', '-I' + tmp, 'tests/load_control/parameter_commands_test.c', '-o', str(parameters_exe)], cwd=root, check=True) + # Выполняет комбинации живых START/RESET, сохранённых команд, неверной CRC, холодного старта и CAN во время чтения. + subprocess.run([str(parameters_exe)], check=True, timeout=10) diff --git a/tests/testlamp/model.inc b/tests/testlamp/model.inc index 5297137..3711570 100644 --- a/tests/testlamp/model.inc +++ b/tests/testlamp/model.inc @@ -23,7 +23,8 @@ static struct { struct gpio_reg GPADAT, GPBDAT; } GpioDataRegs; /* Хранит static struct { unsigned long all; } DigErr; /* Принимает диагностическую маску посторонней ветви SHKF. */ static struct { unsigned long InterruptCount; } CpuTimer1; /* Учитывает вызовы настоящего обработчика пульта. */ int modbus[129], Desk; /* Предоставляет технологические регистры и выбирает ветвь пульта. */ -unsigned int INcount0, CanPowse, CanGO; /* Хранит production-накопитель кнопки и таймер выдачи CAN. */ +unsigned int INcount0; /* Сохраняет 16-битный тип накопителя кнопок из 167, соответствующий указателю er_anal. */ +unsigned int CanPowse, CanGO; /* Хранит счётчик тактов выдачи CAN и признак готовности очередной передачи. */ unsigned long isMask, Lonely; /* Хранит тестовую маску и возраст последней связи. */ int isNumb, quaLamp = 6; /* Сохраняет тестовые цифры и число ламп из pulto.c. */ static unsigned int IER, mask_at_eint, entry_mask, failures, sends, nested_entries, timer_pending; /* Отслеживает разрешения прерываний, пакетные вызовы и возможность вложенного Timer1. */ @@ -71,52 +72,37 @@ static void tick(void) /* Моделирует сохранение и восс check(mask_at_eint == (entry_mask & MINT13 & ~M_INT13), "interrupt mask"); /* Проверяет отсутствие собственного Timer1 и запрет включения ранее выключенных источников. */ check(sends >= 1 && sends <= 2, "packet count"); /* Требует штатный объём одного такта: пакет ламп и не более одного пакета очередного индикатора. */ } /* Завершает один такт индикации. */ -int main(void) /* Запускает на настоящих функциях сценарий рабочих ламп, теста с помехами и возврата в работу. */ -{ /* Начинает общий поведенческий сценарий. */ - int i, pwm_on = 0; /* Перебирает опросы и такты; считает включённые такты рабочего PWM за один полный период. */ - memset(modbus, 0, sizeof(modbus)); /* Убирает старые команды и состояния перед сценарием. */ - check(sizeof(WORDE) == sizeof(modbus[0]), "register layout"); /* Подтверждает корректность production-псевдонимов в хостовой модели. */ - Desk = dsk_EPLT; /* Выбирает реальную ветвь чтения TESTLAMP пульта. */ - modbus[2] = 1; /* Включает рабочую лампу до нажатия TESTLAMP, как в сообщении пользователя. */ - Bright[0] = maximum_bright; /* Задаёт уже включённой лампе постоянную яркость для обнаружения случайных провалов. */ - entry_mask = M_INT13 | (1U << 0) | (1U << 13); /* Разрешает Timer1, группу ADC и Timer2, проверяя также маскирование равного приоритета. */ - tick(); /* Получает первый рабочий пакет ламп. */ - check(lamp_word == 1, "normal lamp before test"); /* Подтверждает исходное постоянное свечение до нажатия. */ - GpioDataRegs.GPADAT.bit.GPIO26 = 1; /* Нажимает физическую кнопку активным высоким уровнем. */ - for (i = 0; i < 1001; i++) get_Buttons(); /* Проходит реальные 1000 накопительных опросов и следующий опрос фиксации порога. */ - check(bTestLamp == 1, "test activation"); /* Требует включения теста после полной выдержки. */ - for (i = 0; i < 600; i++) { /* Проверяет удержание более BLINK_TIME=500 тактов без подавления штатного медленного мигания. */ - unsigned long expected_mask; /* Сохраняет фазу теста перед очередным возмущением кнопки. */ - GpioDataRegs.GPADAT.bit.GPIO26 = 0; /* Вводит короткий провал входа на один опрос главного цикла. */ - get_Buttons(); /* Выполняет настоящий накопительный фильтр и публикацию кнопки. */ - check(bTestLamp == 1, "held test"); /* Проверяет сохранение TESTLAMP вместо мгновенного возврата рабочих масок. */ - timer_pending = 1; /* Допускает новый аппаратный такт во время передачи для проверки запрета повторного входа. */ - tick(); /* Формирует тестовый пакет при кратком провале физического входа. */ - expected_mask = isMask; /* Получает ожидаемую медленную фазу из настоящей what_is. */ - check(lamp_word == expected_mask, "test packet during input glitch"); /* Исключает рабочую маску уже горящих ламп из тестового потока. */ - check(!digit_sends || (digit_word == isNumb && digit_dot == 4), "test digit packet during input glitch"); /* Требует сохранения тестового значения и формата цифр во время помехи кнопки. */ - check(nested_entries == 0, "nested transmission"); /* Требует единственного владельца DATA/CLK на протяжении передачи. */ - timer_pending = 0; /* Убирает смоделированное событие после завершения текущего такта. */ - GpioDataRegs.GPADAT.bit.GPIO26 = 1; /* Возвращает уровень удерживаемой кнопки. */ - get_Buttons(); /* Восстанавливает насыщенный накопитель без смены тестового режима. */ - } /* Завершает серию коротких помех при удержании. */ - check(isNumb == 1111 && isMask == 0xFFFFFFFFUL, "slow test sequence"); /* После 600 миллисекунд требует единственного штатного шага what_is с периодом 500 тактов, а не ускоренного строба. */ - GpioDataRegs.GPADAT.bit.GPIO26 = 0; /* Отпускает физическую кнопку на длительное время. */ - for (i = 0; i < 1001; i++) get_Buttons(); /* Даёт накопителю полностью разрядиться и зафиксировать отпускание. */ - check(bTestLamp == 0, "test release"); /* Проверяет, что сохранение состояния не залипает в TESTLAMP. */ - tick(); /* Возвращает передачу рабочей маски после отпускания. */ - check(lamp_word == 1, "normal lamp after test"); /* Восстанавливает уже включённую до теста лампу. */ - cTestLamp = 1; /* Включает независимую удалённую команду теста через production-регистр. */ - tick(); /* Проверяет приоритет удалённого TESTLAMP при отпущенной физической кнопке. */ - check(lamp_word == isMask, "remote test"); /* Требует тестовую маску для обоих источников команды. */ - cTestLamp = 0; /* Завершает удалённый тест перед проверкой рабочего PWM. */ - Bright[0] = 6; /* Устанавливает шесть включённых тактов из десяти в production-периоде яркости. */ - for (i = 0; i < 10; i++) { /* Проверяет один полный период PWM после выхода из теста. */ - tick(); /* Даёт настоящей функции вычислить текущую фазу яркости. */ - check(lamp_word == 0 || lamp_word == 1, "normal PWM packet"); /* Исключает оставшиеся тестовые биты из рабочей маски. */ - pwm_on += lamp_word == 1; /* Измеряет реальное число включённых тактов production-PWM. */ - } /* Завершает период обычной яркости. */ - check(pwm_on == 6, "normal PWM duty"); /* Подтверждает сохранение заданной яркости 6/10 после выхода из теста. */ - printf("%s TESTLAMP: held input glitches, single ISR, normal lamps, release, remote command\n", failures ? "FAIL" : "PASS"); /* Выводит проверенные свойства без заявления о физическом испытании платы. */ - return failures ? 1 : 0; /* Возвращает результат всех поведенческих требований. */ -} /* Завершает хостовую проверку. */ +int main(void) /* Проверяет двухсекундное удержание по Timer1 и защиту стробов DATA/CLOCK от повторного входа. */ +{ /* Начинает независимые сценарии физической кнопки, удалённой команды и яркости. */ + int i, pwm_on = 0; /* Считает такты удержания и включённые фазы десятишагового PWM. */ + memset(modbus, 0, sizeof(modbus)); /* Очищает команды и входы перед запуском production-функций. */ + check(sizeof(WORDE) == sizeof(modbus[0]), "register layout"); /* Подтверждает совместимость битовых псевдонимов с массивом хостовой модели. */ + Desk = dsk_EPLT; modbus[2] = 1; Bright[0] = maximum_bright; /* Выбирает пульт и заранее включает рабочую лампу с полной яркостью. */ + entry_mask = M_INT13 | (1U << 0) | (1U << 13); /* Проверяет Timer1 вместе с ранее разрешёнными источниками разных приоритетов. */ + tick(); check(lamp_word == 1, "normal lamp before test"); /* Проверяет рабочую индикацию до начала удержания. */ + GpioDataRegs.GPADAT.bit.GPIO26 = 1; /* Нажатие TESTLAMP представлено высоким уровнем входа платы. */ + for (i = 0; i < 4000; i++) get_Buttons(); /* Частый main не должен ускорять выдержку, которую теперь считает только Timer1. */ + check(!bTestLamp, "polling independent"); /* Доказывает независимость порога от числа опросов главного цикла. */ + for (i = 0; i < 2 * READY_FREQ - 1; i++) { tick(); check(!bTestLamp, "short test"); } /* Даже 1999 тактов при 1000 Гц ещё не разрешают местный тест. */ + tick(); check(bTestLamp, "test activation"); /* Двухтысячный такт включает тест точно после двух секунд удержания. */ + for (i = 0; i < 600; i++) { /* Проверяет непрерывный тест дольше периода BLINK_TIME=500 без потери защиты стробов. */ + timer_pending = 1; tick(); /* Имитирует новый такт во время передачи, когда прежний ISR ещё не завершился. */ + check(bTestLamp && lamp_word == isMask, "held test"); /* Требует передачу тестовой фазы вместо исходной рабочей маски. */ + check(!digit_sends || (digit_word == isNumb && digit_dot == 4), "test digit packet"); /* Проверяет тестовый формат числового индикатора. */ + check(nested_entries == 0, "nested transmission"); timer_pending = 0; /* Единственный владелец DATA/CLOCK исключает перемешивание пакетов. */ + } /* Завершает длительное удержание физической кнопки. */ + GpioDataRegs.GPADAT.bit.GPIO26 = 0; tick(); /* Отпускание сбрасывает местный тест в следующем такте Timer1. */ + check(!bTestLamp && lamp_word == 1, "release"); /* Возвращает заранее включённую рабочую лампу без дополнительной задержки. */ + for (i = 0; i < 3; i++) { /* Отдельные короткие нажатия суммарно превышают две секунды, но не должны включать тест. */ + int held; GpioDataRegs.GPADAT.bit.GPIO26 = 1; /* Начинает очередное независимое нажатие. */ + for (held = 0; held < READY_FREQ; held++) tick(); /* Удерживает вход одну секунду, то есть меньше порога. */ + check(!bTestLamp, "separate presses"); /* Не позволяет сложить длительности разных нажатий. */ + GpioDataRegs.GPADAT.bit.GPIO26 = 0; tick(); /* Сбрасывает накопленную выдержку отпусканием. */ + } /* Завершает сценарий повторных коротких нажатий. */ + cTestLamp = 1; tick(); check(lamp_word == isMask, "remote test"); /* Команда Modbus сохраняет немедленный независимый запуск теста. */ + cTestLamp = 0; Bright[0] = 6; /* Возвращает рабочий режим с яркостью шесть тактов из десяти. */ + for (i = 0; i < 10; i++) { tick(); check(lamp_word <= 1, "normal PWM packet"); pwm_on += lamp_word == 1; } /* Измеряет полный период реального алгоритма яркости после теста. */ + check(pwm_on == 6, "normal PWM duty"); /* Подтверждает сохранение настроенной скважности рабочего сигнала. */ + printf("%s TESTLAMP: 2-second hold, main independent, single ISR, release, remote command, PWM\n", failures ? "FAIL" : "PASS"); /* Указывает проверенные программные свойства без заявления об аппаратном испытании. */ + return failures ? 1 : 0; /* Любое нарушенное условие завершает процесс проверки с ошибкой. */ +} /* Завершает проверку кнопки и защищённой передачи индикации. */ diff --git a/tests/testlamp/run_test.py b/tests/testlamp/run_test.py index 6670c6d..7ad699c 100644 --- a/tests/testlamp/run_test.py +++ b/tests/testlamp/run_test.py @@ -27,13 +27,13 @@ button = function(source("peripher.c"), "get_Buttons") # Проверяет и pult = source("pulto.c") # Берёт текущую обработку индикации, включая реальные ветви теста и яркости. body = "\n".join((function(source("measure.c"), "er_anal"), button, function(pult, "what_is"), function(pult, "cpu_timer1_isr_PULT"))) # Собирает функции, участвующие в переключении режима и передаче масок. before, after = Path(__file__).with_name("model.inc").read_text(encoding="utf-8").split("/* HANDLERS */") # Помещает production-функции между аппаратными заглушками и поведенческими сценариями. -old_button, count_button = re.subn(r"(er_anal\(GpioDataRegs\.GPADAT\.bit\.GPIO26,\s*&INcount0,\s*1000,\s*)bTestLamp(\))", r"\g<1>0\2", body) # Возвращает сброс фильтра при одном провале только во временную мутацию. +old_button, count_button = re.subn(r"test_lamp_hold_ticks >= 2U \* \(unsigned int\)READY_FREQ", "test_lamp_hold_ticks >= 1U", body) # Убирает задержку только во временной модели, чтобы доказать обнаружение преждевременного включения теста. old_mask, count_mask = re.subn(r"IER\s*&=\s*\(MINT13\s*&\s*~M_INT13\);", "IER |= MINT13;", body) # Восстанавливает ошибочное разрешение прерываний в отдельном временном варианте. assert count_button == count_mask == 1, "Не найдены единственные места исправлений TESTLAMP" # Не позволяет мутации незаметно проверять неизменённый код. environment = dict(os.environ) # Сохраняет окружение основного процесса. environment["PATH"] = str(Path(args.cc).resolve().parent) + os.pathsep + environment.get("PATH", "") # Делает DLL и вспомогательные программы GCC доступными дочерним процессам. with tempfile.TemporaryDirectory(prefix="testlamp-") as directory: # Изолирует артефакты каждой проверки от дерева прошивки. - for name, functions, failure in (("production", body, None), ("old_button", old_button, "FAIL held test"), ("old_mask", old_mask, "FAIL interrupt mask")): # Проверяет исправление и чувствительность сценариев к каждой из двух старых ошибок. + for name, functions, failure in (("production", body, None), ("old_button", old_button, "FAIL short test"), ("old_mask", old_mask, "FAIL interrupt mask")): # Проверяет задержку удержания и запрет вложенного Timer1 вместе с чувствительностью сценариев к каждой ошибке. c_path, exe_path = Path(directory) / (name + ".c"), Path(directory) / (name + ".exe") # Разделяет временные исходники и программы трёх вариантов. c_path.write_text(before + functions + after, encoding="utf-8") # Сохраняет модель и русские пояснения в UTF-8. subprocess.run([args.cc, "-std=c99", "-O3", "-fno-strict-aliasing", "-Werror=implicit-function-declaration", "-I", str(ROOT / "Source/Internal/Include"), "-I", str(ROOT / "Source/External/v120/DSP2833x_common/include"), str(c_path), "-o", str(exe_path)], env=environment, check=True) # Сохраняет адреса production-псевдонимов регистров и использует реальную таблицу приоритетов TI.