From 5d9818291b55dfa641069180d7971afef3cdb55b Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 9 Oct 2026 16:17:37 +0300 Subject: [PATCH] feat: add M95M02-only GAS and EEPROM profiles Expose the existing registers through Modbus and service CAN using one GAS provider only in M95M02 builds. Include the EEPROM layout/driver, LOAD protection and calibration, firmware identity and shared CCS build workflow. Add transport/profile and SCI8 validation tests, documentation and the checked M95M02 firmware image. --- .gitignore | 4 + .../UKSSTMS320F28335_GAS_M95M02.bin | Bin 0 -> 33174 bytes ...STMS320F28335_GAS_M95M02.bin.manifest.json | 41 +++ .../UKSSTMS320F28335_GAS_M95M02.bin.sha256 | 1 + Bin/GAS_M95M02/profile-comparison.json | 16 + Doc/EEPROM_LAYOUT.md | 136 ++++++++ Doc/FIRMWARE_IDENTITY.md | 167 ++++++++++ Doc/GAS_167_M95M02.md | 97 ++++++ Doc/GAS_MODBUS_PROJECT_COMPARISON.md | 241 ++++++++++++++ Doc/UMP_VOLTAGE_PROTECTION.md | 115 +++++++ F28335.cmd | 12 + Source/Internal/ADC.c | 2 +- Source/Internal/DAC.c | 302 ++++++++++++++---- Source/Internal/Include/DAC.h | 10 + Source/Internal/Include/caliber.h | 4 +- Source/Internal/Include/eeprom_config.h | 15 + Source/Internal/Include/gas167.h | 28 ++ Source/Internal/Include/package.h | 5 + Source/Internal/Include/spise2p.h | 31 +- Source/Internal/bios.c | 11 +- Source/Internal/ecan.c | 23 +- Source/Internal/gas167.c | 63 ++++ Source/Internal/log_to_mem.c | 59 ++-- Source/Internal/main.c | 26 +- Source/Internal/measure.c | 2 +- Source/Internal/message.c | 50 ++- Source/Internal/shared_ump.c | 4 + Source/Internal/spise2p.c | 40 ++- UKSSTMS320F28335/.cproject | 151 ++++++--- UKSSTMS320F28335/.gitignore | 1 + UKSSTMS320F28335/.project | 20 ++ .../com.ti.ccstudio.project.core.prefs | 4 - UKSSTMS320F28335/README.md | 39 +++ UKSSTMS320F28335/firmware_identity.json | 4 + UKSSTMS320F28335/firmware_version.h | 9 + UKSSTMS320F28335/postBuildStep_Debug.bat | 33 -- UKSSTMS320F28335/prepare_identity.py | 48 +++ build_bin.bat | 20 -- build_logger.ps1 | 53 --- check_firmware.ps1 | 36 --- tests/eeprom_profiles/run_test.py | 21 ++ tests/eeprom_profiles/spi_test.c | 47 +++ tests/eeprom_profiles/stubs/DSP2833x_Device.h | 22 ++ tests/firmware_identity/check_build.py | 42 +++ tests/firmware_identity/port_test.c | 33 ++ tests/gas167/check_image.py | 48 +++ tests/gas167/gas167_test.c | 196 ++++++++++++ tests/gas167/run_test.py | 51 +++ tests/load_control/load_control_test.c | 103 ++++++ tests/load_control/run_test.py | 16 + tests/load_control/stubs/DSP2833x_Device.h | 9 + .../stubs/DSP2833x_SWPrioritizedIsrLevels.h | 1 + tests/load_control/stubs/RS485.h | 1 + tests/load_control/stubs/filter_bat2.h | 1 + tests/load_control/stubs/measure.h | 4 + tests/load_control/stubs/message.h | 1 + tests/load_control/stubs/package.h | 17 + tests/load_control/stubs/peripher.h | 14 + tests/load_control/stubs/tools.h | 1 + tests/ump_logger/init_test.c | 4 +- tests/ump_logger/recorder_test.c | 66 +++- tests/ump_logger/run_test.py | 7 + tests/ump_logger/test_firmware_image.py | 44 ++- 63 files changed, 2307 insertions(+), 365 deletions(-) create mode 100644 Bin/GAS_M95M02/UKSSTMS320F28335_GAS_M95M02.bin create mode 100644 Bin/GAS_M95M02/UKSSTMS320F28335_GAS_M95M02.bin.manifest.json create mode 100644 Bin/GAS_M95M02/UKSSTMS320F28335_GAS_M95M02.bin.sha256 create mode 100644 Bin/GAS_M95M02/profile-comparison.json create mode 100644 Doc/EEPROM_LAYOUT.md create mode 100644 Doc/FIRMWARE_IDENTITY.md create mode 100644 Doc/GAS_167_M95M02.md create mode 100644 Doc/GAS_MODBUS_PROJECT_COMPARISON.md create mode 100644 Doc/UMP_VOLTAGE_PROTECTION.md create mode 100644 Source/Internal/Include/eeprom_config.h create mode 100644 Source/Internal/Include/gas167.h create mode 100644 Source/Internal/gas167.c delete mode 100644 UKSSTMS320F28335/.settings/com.ti.ccstudio.project.core.prefs create mode 100644 UKSSTMS320F28335/firmware_identity.json create mode 100644 UKSSTMS320F28335/firmware_version.h delete mode 100644 UKSSTMS320F28335/postBuildStep_Debug.bat create mode 100644 UKSSTMS320F28335/prepare_identity.py delete mode 100644 build_bin.bat delete mode 100644 build_logger.ps1 delete mode 100644 check_firmware.ps1 create mode 100644 tests/eeprom_profiles/run_test.py create mode 100644 tests/eeprom_profiles/spi_test.c create mode 100644 tests/eeprom_profiles/stubs/DSP2833x_Device.h create mode 100644 tests/firmware_identity/check_build.py create mode 100644 tests/firmware_identity/port_test.c create mode 100644 tests/gas167/check_image.py create mode 100644 tests/gas167/gas167_test.c create mode 100644 tests/gas167/run_test.py create mode 100644 tests/load_control/load_control_test.c create mode 100644 tests/load_control/run_test.py create mode 100644 tests/load_control/stubs/DSP2833x_Device.h create mode 100644 tests/load_control/stubs/DSP2833x_SWPrioritizedIsrLevels.h create mode 100644 tests/load_control/stubs/RS485.h create mode 100644 tests/load_control/stubs/filter_bat2.h create mode 100644 tests/load_control/stubs/measure.h create mode 100644 tests/load_control/stubs/message.h create mode 100644 tests/load_control/stubs/package.h create mode 100644 tests/load_control/stubs/peripher.h create mode 100644 tests/load_control/stubs/tools.h diff --git a/.gitignore b/.gitignore index 6252007..5d7cad2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ /Bin/Rs232NoFifoTest/ /Bin/UartFifoTest/ /Bin/*.pending +/UKSSTMS320F28335/Generated/ +/Bin/FirmwareIdentity/ +/Bin/*.bin.manifest.json +/Bin/*.bin.sha256 diff --git a/Bin/GAS_M95M02/UKSSTMS320F28335_GAS_M95M02.bin b/Bin/GAS_M95M02/UKSSTMS320F28335_GAS_M95M02.bin new file mode 100644 index 0000000000000000000000000000000000000000..b1299f69438446f383cab88513198264c16a2292 GIT binary patch literal 33174 zcmeFadwf*Yxi`Mn-jkilB$=6n5JG?uF+yOtgqt8B?1X@LsnUT)L^x+gx%z`MYJJB` zEtwFZX(%Sy2}2uDbDf!;2?c3utA%6|615m~P^dQoDJt-uW2^O8?M2zY@3Z#|1nZ%m zzVGM!{`uweS+nlfy)Mu5eV(<_W@dl)w^9ABfWQdTBJWUSSEQ^<=KM%R^a+LC{Kbgm zODueeC3N+c^~fgP7fJD@79L}^uGh+5m95Oub+qgiIhp?^lH@ZNzRcf>Wch{{zRgB- zy<7HiT`zhw?=Jek9L# zUEv38T-W)sbMja=rt56kd-ApXcadv+xrGC~A>#763me(JUCm`p@;z)_SD-8?ujj3i zwZ1ilEo@U)d)YpDGv6Qa`2MZ%r@Sq4pYQ)E46}`0YFS9$z#i%PRoV0Mqx{#A@A)1p ze1UgFw)jd5JK6WU4woI0ALl=hJmmXdg}-1Acm1;LIoZn|>^fNXGx@vx*~kOF`wI`S zrCrr!HS)K4ZDg6xS*UP-q{Me;;coVwu3cqy@^Zd6vdXu*@G17MUC)#~E&mH!*;QY* zN4}f?ByyK;Md6QmMP!NZ=E5JcTf26YRm!)p+q<4DtCH{FJ0sun-B$P`R^0VO**19* z|3Ty?-{QjUd~2l0x3I9B6?T=C`D8hw$!9K4k(c=Md{5Gfq7k^TrLa&UT3OtGTzXbm zC8jZ+@Mja1`ESCT`i!(BBqWgk%fv4eTP>{?O%wVV%V3%&!e+2dF*_k^JMbSP;7H(M~G$>;5$DpuP$;?PoFj=P~;iG5fig{mYpB ztC;WWYTY*K4*Zm{(@!$?lS7Qv!Dt#ApcqZ#x_wcz>NaB+ zHTxx3`XyIJ%=HH?3$9yIg!yC`^DQ8=Dd{hB2OA30G|QU|f0~vYvr}Sr8f+Tz4A`7~ ztbP0zV+)wX?q+web!;>HK1*b!%)?~Lj=x7?G|e%m5Wx42HNSmakj#bs)xT(xvSZRjbxC8t>7AMyj$$S7V9(he({0j^&gC*n zBkx=?P^^e0Dj#yThw+j|CS0hQk$iyy6?FR!^bbEU%qy(fSE7+WE_nR_4l=N32dHZ zbKNHKxj#CW�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.bin +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`. + +Если CCS пишет `Error ... (ignored)`, сборка не считается успешной: убедитесь, +что последняя проверка и `seal` завершились без ошибок. Старый BIN с таким же +именем может оставаться на диске после неудачной сборки. + +## Как прочитать версию + +По прямому RS232/RS485: Modbus **03**, адрес начала **4608 (`0x1200`)**, **12 слов**. +Это zero-based адрес в запросе, а не обозначение `4xxxx` в интерфейсе терминала. +Адрес ведомого остаётся текущим `CNTRL_ADDR`. Возвращается стандартный RTU-ответ +с 24 байтами данных и CRC; дополнительного legacy padding в этом окне нет. + +| Адрес | Содержимое | +|---|---| +| `0x1200` | Версия формата = 1 | +| `0x1201..0x1203` | Major, minor, patch | +| `0x1204` | Год UTC | +| `0x1205` | Месяц:день, старший:младший октет | +| `0x1206` | Час:минута UTC | +| `0x1207` | Секунда | +| `0x1208..0x120B` | 8 ASCII-символов Build ID, по два на слово | + +По служебному CAN ПМ35: запрос UMP функции 03, `0x1200`, 12 слов; ответ состоит +из шести фрагментов. Используется выбранный Mode и существующий token запроса. +CAN ПМ67 не переназначается. Запись функции 06 в окно версии запрещена. + +Старый образ может вернуть исключение или неизвестный контракт. Это не следует +заменять «ожидаемой» версией выбранного BIN. После прошивки переподключитесь или +явно обновите чтение: ответ должен поступить с устройства, а не из кэша GUI. + +## Что сравнивать после заливки + +Сравните SemVer, все поля UTC и Build ID с паспортом именно загруженного BIN. +`+` означает локальные изменения приложения **или templates**; полный Git SHA +и состояние обоих деревьев записаны в JSON. Проверка SHA на ПК подтверждает +целостность файла, но не заменяет проверку программатором записанной памяти. + +В контракте нет аппаратного product ID и хеша всей памяти. Подключённый профиль +должен оставаться ПМ35/167, а файл — `product=Balsam-167-PM35`, +`target=TMS320F28335`. Не переносите файл на ПМ67 только по похожему имени. + +## Влияние на память и параметры + +С проверенными настройками текущий образ занимает **32 286 байт**, предел — +**32 508**, запас **222 байта**. Механизм добавил 116 байт относительно сборки +без него. Это результат конкретной сборки, не обещание для следующих изменений. + +Таблица версии — часть `.econst` прошивки. Её не сохраняют в область параметров; +старые настройки не могут подменить версию после обновления. Размер буфера лога +и размещение EEPROM-параметров не менялись. Дата не разбирается на MCU, SHA-256 +не вычисляется на MCU. Проверка размера `check_firmware.ps1` остаётся обязательной. + +## Проверки и ограничения + +Проверка соответствия паспорта **реально встроенной** таблице в собранном SCI8: + +```powershell +python tests/firmware_identity/check_build.py Bin/FirmwareIdentity/UKSSTMS320F28335.bin +``` + +Этот независимый тест также сверяет SHA и размер, проходит boot-блоки и ищет +точное 12-словное представление версии. Он обнаружит старый объект версии или +паспорт другой сборки. Это тест для 167, не транспортный код для GUI. + +Host-тест реального порта (GCC должен быть в PATH): + +```powershell +$templates = '../newProject/SETGUI/third_party/templates' +gcc -std=c99 -Wall -Wextra -Werror ` + "-I$templates/c/firmware-info/include" -IBin/FirmwareIdentity/generated ` + "-I$templates/c/set-protocol" "-I$templates/c/set-protocol/ports/tms320f28335" ` + tests/firmware_identity/port_test.c Source/Internal/shared_ump.c ` + -o Bin/FirmwareIdentity/port_test.exe +./Bin/FirmwareIdentity/port_test.exe +python tests/ump_logger/test_firmware_image.py +python tests/ump_logger/run_test.py +python tests/ump_logger/run_init_test.py +``` + +Проверены полный TI build 167, формат SCI8, host-тесты протокола и регистратора, +общие тесты identity/SHA, компиляция общего C-кода TI C28x без FPU и ARM Cortex-M4. +Это не аппаратная проверка TMS2812/STM32 и не полноценная сборка другого проекта. +Физическое чтение нового окна версии и проверку после загрузки нужно выполнить +на стенде. В SETGUI на вкладке «Загрузка / запуск / прошивка» доступны +«Прочитать версию с платы» и «Проверить файл»: показаны SemVer, дата UTC, +Build ID и сравнение с выбранным BIN. Для проверки файла держите рядом его +`.bin.manifest.json`; GUI проверяет product/target, размер и SHA-256 и повторяет +проверку перед передачей. BIN без паспорта отмечается как непроверенный старый +образ; неверный паспорт блокирует загрузку. После RESET прочитайте версию снова. diff --git a/Doc/GAS_167_M95M02.md b/Doc/GAS_167_M95M02.md new file mode 100644 index 0000000..6395f40 --- /dev/null +++ b/Doc/GAS_167_M95M02.md @@ -0,0 +1,97 @@ +# GAS 167: только M95M02 + +Реализован профиль GAS **167/v1** для установленной EEPROM **M95M02-DR, 256 КиБ**. Он предоставляет общий доступ к существующим технологическим регистрам через Modbus RTU и служебный CAN УМП. Оба транспорта используют один provider [gas167.c](../Source/Internal/gas167.c). Состав регистров, алгоритм управления, защита, сохранение параметров и регистратор УМП сохранены. + +## Включение и сборка + +В [eeprom_config.h](../Source/Internal/Include/eeprom_config.h) сейчас выбран **EEPROM_M95M02=0** (AT25256B). Для установленной M95M02 выбрать **1**. GAS включается только этим аппаратным выбором; отдельного runtime-переключателя нет. Для AT25256B установить **0**, затем выполнить **Clean/Rebuild**. В этой сборке отсутствуют GAS-код и дополнительные регионы. + +Новый файл gas167.c подключён к CCS через .project. После обновления проекта выполнить Clean/Rebuild, чтобы CCS добавил его в сборку. Конфигурацию памяти менять не требуется: код продолжает загружаться в прежнюю RAM. + +CLI define EEPROM_M95M02 должен передаваться и компилятору, и линкеру. Значение в заголовке теперь находится внутри ifndef, поэтому override не переопределяется обратно на 1. Выбор должен соответствовать установленной микросхеме. + +## Карта адресов + +Адреса ниже относятся к профилю конкретного устройства **167/v1**. Они не назначают новые универсальные значения нормативному XLSX ОАП и не совместимы по смыслу с экспериментальным профилем GAS-логгера ПМ35. + +| GAS | Штатный адрес / источник | Назначение | Доступ | +|---|---|---|---| +| 0x5000…0x507F | 0x0000…0x007F, modbus[128] | Те же 128 технологических регистров; GAS = 0x5000 + индекс | Как штатный Modbus; ограничения LOAD ниже | +| 0x5050 | 0x0050 | Задание ЦАП | RW, штатный raw | +| 0x5051 | 0x0051 | Штатный регистр останова | RW | +| 0x5052 | 0x0052 | CAN timeout LOAD, мс | RW; для LOAD 1…60000 | +| 0x507C | 0x007C | LOAD_STATUS | Только чтение для LOAD | +| 0x507D | 0x007D | LOAD_ATTEMPTS | Только чтение для LOAD | +| 0x507E | 0x007E | LastMode | Как штатный регистр | +| 0x507F | 0x007F | Commands, битовая маска | RW; штатная фиксация команды | +| 0x5F00…0x5F07 | Константы GAS-профиля | Сигнатура, версия, возможности | Только чтение | + +Один регистр — **16 бит raw**. Битовые маски, знаковость и масштаб отдельных параметров сохраняют смысл существующего проекта; преобразование в физические величины выполняет клиент. Отдельной копии значений, нового массива логгера и новых сохранённых параметров нет. + +Прежние адреса 0x0000…0x007F, УМП 0x1000…0x117B, паспорта 0x1200…0x120B, защиты 0x1300…0x1307 и EEPROM 0x8000…0xFFFF продолжают обслуживаться своими обработчиками. Дополнительные окна GAS не пересекают их. + +## Обнаружение профиля + +Прочитать **8 слов с 0x5F00**. Перед записью клиент проверяет сигнатуру, версию, номер профиля и аппаратный вариант. + +| Адрес | Значение | Смысл | +|---|---:|---| +| 0x5F00 | 0x4741 | Сигнатура GA | +| 0x5F01 | 0x5331 | Сигнатура S1 | +| 0x5F02 | 1 | Версия контракта профиля | +| 0x5F03 | 167 | Профиль устройства | +| 0x5F04 | 0x5000 | Начало технологических регистров | +| 0x5F05 | 128 | Количество технологических регистров | +| 0x5F06 | 7 | Бит 0: RTU; бит 1: служебный CAN; бит 2: запись FC06 | +| 0x5F07 | 1 | EEPROM M95M02 | + +В сборке AT25256B эти адреса отклоняются как недопустимые; сигнатуру GAS она не возвращает. + +## Modbus RTU и служебный CAN + +**Modbus:** FC03 читает 1…124 слова, FC06 записывает одно слово. Блок целиком находится в одном регионе. Ответ FC06 возвращает исходный GAS-адрес. GAS использует обычный ответ расширения без исторических хвостовых нулей; штатные низкие адреса сохраняют прежний формат. + +Примеры: FC03(0x5F00, 8) — профиль; FC03(0x5000, 124) и FC03(0x507C, 4) — все технологические слова; FC06(0x5052, 1000) — штатный timeout LOAD. + +**CAN:** используется существующий служебный запрос УМП с маркером 0x1F00, ненулевым token и функцией 3/6. Прикладной provider тот же, что у RTU. Для MBOX1 поля запроса: MDL = (GAS_address << 16) | quantity_or_value; MDH = 0x1F000000 | (token << 8) | function. Extended CAN ID запроса/ответа, порядок упаковки C28x и планировщик MBOX2 остаются штатными. + +Чтение CAN допускает **2…124 слова, чётное количество**: один ответный фрагмент содержит два слова. Запись возвращает пару GAS_address/value и код результата, token сохраняется. Ответы передаются из снимка; изменение RAM между фрагментами не меняет уже захваченный блок. Для одного слова читать допустимую пару и выбрать нужное слово. Если пара вышла бы за границу региона, начать её на один адрес раньше. + +Нативный ProtoCAN MsgType=0x3 не добавлялся. Для подключения SETGUI/другого клиента нужно использовать этот профиль и существующий служебный транспорт УМП, а не кодировку экспериментального pcan_gas. + +## Проверки доступа + +| Условие | Результат | +|---|---| +| Функция отличается от 3/6 | Код 1, illegal function | +| Адрес вне региона; блок пересекает границу; запись паспорта GAS | Код 2, illegal address | +| FC03 quantity = 0 или >124; нечётное количество CAN | Код 3, illegal value | +| LOAD: запись 0x507C или 0x507D | Код 2; значения не меняются | +| LOAD: запись timeout 0 или >60000 в 0x5052 | Код 3; значение не меняется | +| Запись 0x507F | Штатная запись Commands и вызов ump_log_rs, как при FC06(0x007F) | + +Валидация блока выполняется до чтения. Параметр modbus[128] вне опубликованных 128 слов не раскрывается. Чтение и запись GAS выполняются под существующей короткой IRQ-блокировкой main loop; EEPROM SPI остаётся вне этой блокировки. + +## Проверенная сборка + +TI C2000 22.6.0.LTS, -O2, speed=0, gen_func_subsections=on, FPU32/COFF. Версия из пользовательского firmware_version.h: **1.1.2**. Загрузочный образ проверен как SCI8: заголовок, полные блоки, завершающее слово, адреса RAM и entry point. + +| Сборка | Без GAS, байт | С GAS, байт | Добавлено | Лимит прошивки | Свободно | +|---|---:|---:|---:|---:|---:| +| AT25256B | 32472 | **32472** | **0** | 32508 | 36 | +| M95M02 | 32764 | **33174** | **410** | 261884 | **228710** | + +AT25256B сравнен с исходниками до добавления GAS при одинаковых параметрах и generated identity: BIN совпадает **побайтно и по SHA-256**. GAS-символов в его MAP нет. У M95M02 остаётся 0x033F слов (1662 физических байта) свободной RAML0; секции и данные прежнего логгера не увеличены. + +Готовый образ: [GAS M95M02 BIN](../Bin/GAS_M95M02/UKSSTMS320F28335_GAS_M95M02.bin), рядом manifest с eeprom=M95M02, GAS-профилем и SHA-256. Сборка включает незакоммиченные изменения проекта; build_id содержит знак +. + +Проверки на ПК: + +~~~powershell +python tests/gas167/run_test.py +python tests/gas167/check_image.py Bin/GAS_M95M02/UKSSTMS320F28335_GAS_M95M02.bin --eeprom M95M02 +~~~ + +Тест использует production GAS provider, фактические тела обработчиков Modbus и UMP_CAN_service, общую сериализацию CRC/CAN и аппаратные mocks. Проверены обе конфигурации, адреса/границы, запреты записи, 124-словный RTU-ответ и полный CAN-снимок в 62 фрагментах. Числовая ABI GCC адаптирована к 16-битным словам C28x; целевая компиляция дополнительно проверена TI. + +Загрузка на плату, холодный SPI boot и аппаратные тайминги в этой задаче не проверялись. diff --git a/Doc/GAS_MODBUS_PROJECT_COMPARISON.md b/Doc/GAS_MODBUS_PROJECT_COMPARISON.md new file mode 100644 index 0000000..1d6c1b7 --- /dev/null +++ b/Doc/GAS_MODBUS_PROJECT_COMPARISON.md @@ -0,0 +1,241 @@ +# GAS для всех проектов: сопоставление с Modbus, CAN и SETProtocol + +Дата анализа: **9 октября 2026 года**. + +**Вывод:** GAS можно сделать общим способом описания и доступа к параметрам всех проектов. Практический вариант — единый реестр в `templates`, профили конкретных устройств и адаптеры существующих протоколов. Modbus, ProtoCAN, SET и старые пакеты TMS обращаются к одним источникам данных. Подключение GAS и замена регистратора — отдельные задачи. + +Здесь GAS означает общее адресное пространство; в библиотеке используется название **General Address Space**. Это логические адреса данных, а не адреса RAM процессора. GAS сам по себе не задаёт UART, CAN ID, CRC, скорость линии или подтверждение команды. + +Охват: проекты в `K:/git_project`, включая `newProject` и `home_proj`. Подробно проверены общая библиотека, экспериментальный GAS, ПМ35/167 и описания связанных протоколов. Для остальных проектов указана глубина проверки: наличие каталога или отдельного исходника не означает проверку всей прошивки. Пустые каталоги и непроверенные реализации отмечены явно. Для 167 после анализа реализован GAS только в сборке M95M02 (раздел 5.2); остальные проекты этим анализом не изменены. + +## 1. GAS, протоколы и интерфейсы + +| Вариант | Что задаёт | Адресация | Чтение / запись | Каталог параметров | Подтверждение | Применение | +|---|---|---|---|---|---|---| +| **GAS / ОАП** | Логическую карту и правила доступа | `0x0000…0xFFFF` внутри профиля устройства | Внутренний API/callbacks; обмен зависит от оболочки | Имена, типы, единицы — отдельный реестр | Определяет протокол доступа | Общая основа настроек, графиков и логгеров | +| **Modbus RTU** | Запрос/ответ, функции, serial framing и CRC16 | Устройство + таблица + нулевой адрес PDU | FC03/04 чтение, FC06/16 запись; конкретная плата реализует подмножество | Вне стандартного кадра | Ответ/exception; broadcast-запись без ответа | Существующие приборы; адаптер к GAS сохраняет старые адреса | +| **Modbus TCP** | Modbus PDU в TCP с MBAP | Соединение + Unit ID + таблица + адрес | Аналогично RTU на уровне PDU | Внешний каталог | Ответ с Transaction ID | Дополнительный адаптер; сервер на конкретных платах здесь не установлен | +| **ProtoCAN GAS** | GAS-доступ в формате ProtoCAN | Поля устройства/маршрута ID; `MsgType=0x3`, адрес в `MsgBody` | До 4 слов в classic CAN; `DLC=0` — расширение для чтения | Отдельный каталог | В текущем `pcan_gas` чтение с ответом, запись без прикладного ACK | Устройства ProtoCAN и мост; команды требуют проверки результата | +| **Legacy CAN Бальзам / ПМ35** | Штатные технологические кадры | Extended ID; адрес/маски в payload | Технологический кадр ПМ35 — до 3 слов | Таблицы проекта | CAN ACK не подтверждает применение команды | Сохранить формат, добавить адаптер к GAS | +| **Служебный CAN УМП** | Расширенные регистровые запросы/ответы | ID платы + `0x1F00` + token/function/адрес | Функции 3/6; ответ по 2 слова в кадре | Сигнатура/версия сервиса | token и код результата | Готовый путь к расширениям 167; отличается от ProtoCAN GAS | +| **SETGUI v1** | Потоковые кадры `A5 5A`, sequence, CRC32 | Регистр/объект согласно сообщению | READ/WRITE, каталог/watch при наличии обработчиков | GAS_CATALOG: тип, масштаб, единица, имя | sequence/ACK/NACK по контракту | Мост, К1921ВК028, приборы DS18B20 с разными наборами сообщений | +| **SETProtocol v2** | Общий кадр, READ/WRITE, CATALOG, SUBSCRIBE/PUBLISH, firmware service | source/destination + адрес/ID в payload сервиса | По capabilities устройства | Каталог и версия словаря | sequence, response/status; политика задаётся сервисом | Новые устройства/клиенты; GAS подключается к provider | +| **Legacy TMS / ПУИ** | Исторические команды/телеметрия | Номер параметра, индекс или адрес памяти по своему контракту | Только операции конкретной прошивки | Вне кадра | По собственному протоколу | Адаптер существующих полей к GAS | +| **RS232 / RS485 / USB / Ethernet** | Физический интерфейс или транспорт | Порт/соединение | Передача кадров | Не задают | Зависит от стека | RS485 не означает Modbus; CAN не означает ProtoCAN GAS | + +Нулевую адресацию PDU, порядок байтов внутри Modbus-регистра и предел FC03 в 125 регистров задаёт [спецификация Modbus V1.1b3](https://www.modbus.org/file/secure/modbusprotocolspecification.pdf). Фактические лимиты: GAS-логгер — 125 слов, `set_regmap` — 124, архив УМП/EEPROM 167 — 120. Порядок слов многорегистрового числа задаётся профилем отдельно. + +## 2. Существующие реализации и общая архитектура + +| Реализация | Уже реализовано | Ограничение | Предлагаемая роль | +|---|---|---|---| +| `templates/c/set-protocol/pcan_gas` | Регионы, storage/callbacks, права, проверка карты, ProtoCAN GAS | `uint8_t`; блоки могут выполняться частично; запись без ответа | CAN-адаптер к словарю; C28x требует портирования | +| `templates/c/set-protocol/set_regmap` | Регионы/каталог, dispatcher 3/6, RTU и служебный CAN | Заголовок связан с byte ABI; C28x-компиляция не проходит | Основа общего dispatcher после отделения word API | +| `templates/c/gas-logger` | Два банка, capture, snapshot, generation/release, JSON-профиль | Сервис регистрации; не весь словарь и не драйвер связи | Опциональное расширение GAS | +| `newProject/home/gas` | Стабильные ID, `float` + time/valid, `parameters.def`, HR→GAS, график | Host-пример; нет UART/parser/CRC, записи HR и межпоточной защиты | Пример связи измерения, меню и Modbus | +| SETGUI / Android + `templates` | Клиенты, модели, каталоги, графики и порты | Возможности зависят от устройства и ревизии | Единое описание данных для всех клиентов | + +Объединять нужно **контракт и реестр**, затем общие части реализации: сейчас модули с названием GAS имеют разные API и модель хранения. Предлагаемый поток: `источники данных → профиль GAS → адаптер RTU/ProtoCAN/SET/TMS → клиент`; меню и логгеры читают тот же профиль. Простые параметры могут читаться callbacks из существующей RAM: массив на 65 536 регистров не требуется. + +| Поле общего профиля | Назначение / правило | +|---|---| +| `semantic_key` | Стабильный смысл, например `ump.commands`; имя на экране меняется отдельно | +| `device/profile/version` | Область применимости адресов: 167, эмулятор и прибор датчиков имеют разные профили | +| `gas_address`, `registers` | Логический адрес и ширина; `u32` занимает два слова | +| `type`, `unit`, `scale` | Знаковость и raw→физическая величина; неизвестный масштаб не выдумывать | +| `access`, `range`, `command_policy` | RO/RW, диапазоны и проверка условий выполнения записи | +| `modbus` | Таблица, FC и старый адрес PDU; GAS ID и Modbus-адрес могут различаться | +| `can/set/legacy` | MsgBody, поле payload, индекс параметра и формат; это не один универсальный CAN ID | +| `source`, `validity`, `time` | RAM/удалённый кэш, достоверность и давность выборки | +| `word_order`, `limits`, `schema_id` | Порядок слов, максимальный блок, версия/CRC профиля | + +Для существующего ОАП нормативным источником остаётся `doc/setcan/Протокол CAN и ОАП.xlsx`. JSON GAS-логгера — отдельный экспериментальный профиль. При внедрении согласовать их и генерировать описания C/Python/клиентов из утверждённого источника. CRC карты обнаруживает несовместимость, но не подтверждает подлинность устройства. + +## 3. Пригодность GAS по всем обнаруженным проектам + +Сложность — предварительная инженерная оценка без сроков в днях. «Обзор» означает, что полная карта и сборка проекта не проверены. + +### Прошивки, стенды и библиотеки + +| Проект / каталог | Что подтверждено | Применение GAS | Требования / оценка | +|---|---|---|---| +| `newProject/templates`, `SETGUI/third_party/templates` | Общие протоколы, карты и порты; разные checkout | Реестр + независимое от MCU word-ядро | Согласовать ревизию, отделить byte API C28x; средняя | +| `Balsam_167_periph` / ПМ35 / F28335 | FC03/06, legacy/служебный CAN, УМП, паспорт, EEPROM; GAS 167/v1 для M95M02 | GAS-регионы 0x5000…0x507F и 0x5F00…0x5F07 через RTU и служебный CAN | Реализовано и проверено сборкой/host-тестами; AT25256B без GAS, его BIN совпадает побайтно | +| `Balsam_166_periph` | Локальный каталог пуст | Собственный профиль 166 | Нужны исходники; текущая поддержка не установлена | +| `BALZAM_167` / ПМ67 / F2812 | Legacy CAN, Modbus-таблицы, CMD_STD в коде | Фасад над таблицами/состояниями ПМ67 | C28x-порт и политика записей; высокая | +| `22220/ural`, `sibir`, `leningrad` | В `ural/TMS_new_bus` найдены исходники обмена; другие варианты не сверены | Профиль каждого варианта; общие semantic keys | Не переносить карту 167 по названию; аудит вариантов | +| `K1921BK028/1921vk028` | GUI transport и ProtoCAN в `lib` | Object dictionary/RTC/diagnostics в карте | Providers, синхронизация и карта; средняя | +| `newProject/KONOR_ds18b20` | GUI v1 для датчиков; ProtoCAN в `src/can.c` | Температуры, качество, опрос, состояние | Сохранить ROM-ID и команды датчиков; средняя | +| `GUI_18b20` | Каталог пуст; проект упоминается в документах SETGUI | Профиль датчиков после восстановления кода | Локальная реализация не проверена | +| `newProject/home/gas` | Host-пример GAS→график/Modbus | Прототип модели параметров | Общее ядро, порт и проверка платы; средняя | +| Эмуляторы F407/G474 в `templates/ports` | `set_regmap`, УМП/TMS/IGBT/DAC и MCU-порты | Первый пилот на готовой карте/каталоге | Порт не доказывает проверку конкретной платы; небольшая/средняя | +| `newProject/g474/params_AD`, `winding_analyzer` | STM32-проекты/документы; GAS в обзорном поиске не найден | Измерения, настройки, диагностика | Сверить обмен и назначить профиль; предварительно средняя | +| `newProject/online_logger`, `home_proj/stm32h755_logger/online_logger` | Архитектура H755; во второй копии есть OLGR | GAS-настройки/статус, блоковый поток выборок | Согласовать копии, DMA и владение данными; средняя/высокая | +| `UKSI` | CAN/UART, FreeRTOS, переменные/модули Modbus | Фасад над таблицами | Согласованный доступ задач/ISR; средняя | +| `can_rs485_do_di_test/tester_CAN_RS_DIO` | Собственные конфигурация/данные Modbus | DO/DI и диагностика обмена | Сохранить протокол испытаний; средняя | +| `DC_DC_boost_103/DC_DC_DIMA_S`, вариант G474 | Собственные Modbus-модули, разные варианты | Текущие значения, уставки, аварии | Отдельные профили и проверка записей; средняя | +| `diode_test/Diod_Test` | Собственные interfaces/`rs_message`, настройки тестера | Состояние теста и результаты | Wire-формат/FC не полностью проверены; средняя | +| `ds128b20/new rev`, `john_proj`, `DS18B20_Library` | STM32 Modbus в `new rev`, отдельная библиотека датчиков | Каналы/качество и конфигурация | Сохранить 1-Wire/ROM-ID; средняя | +| `UPP-master/upp` | Параметры и ссылки на Modbus в собственном коде | Callback-карта существующих параметров | Сверить обработчики и условия пуска; средняя | +| `newProject/prolitarski_ups` | ПУИ↔МПСУ: команды `0x54/0x5D`, CRC16/Modbus | Номер параметра→semantic key | Этот формат не Modbus RTU, несмотря на CRC; средняя | +| `newProject/Optical_tester`, `test_optical` | STM32-проект/описание измерений первого; обзор второго | Результаты и настройки | Сначала профиль и аудит протокола | +| `newProject/home/climate`, `remote_ds18b20_f103` | Найдены прошивки, библиотеки и документы связи | Температуры, клапаны, состояние | Wire-адаптеры/адреса требуют отдельной сверки | +| `Lamps-master`, `UKP` | Обзор каталогов; назначение UKP в README | Параметры при наличии внешнего интерфейса | Реализация GAS/конкретного протокола не установлена | +| `stm32g474`, моторные проекты `home_proj` | Найдены проекты управления приводом | Уставки и диагностика | Аудит протокола/времени доступа; быстрый контур остаётся в приложении | + +### Мосты, адаптеры и клиенты + +| Проект / каталог | Текущая роль / глубина проверки | Использование GAS | +|---|---|---| +| `newProject/can_to_rs` / F407 | Мост, собственные GAS-регистры, каталог/watch GUI v1 | GAS уже есть для своих параметров; удалённые кадры маршрутизируются отдельно | +| `newProject/esp_parser` / ESP32 CAN485 | CAN/RS485, текст USB/Web, CSV, консоль | Передача GAS-кадров через адаптер кадров; собственный GAS-каталог не подтверждён | +| `newProject/CANdle_stm32`, `CANdle_stm32g474` | USB-CAN/gs_usb, CAN/FDCAN-порты | Транспорт кадров; словарь живёт на целевом узле/в клиенте | +| `newProject/SETGUI` | Каталоги/графики, Modbus/CAN/SET, экспериментальный GAS-клиент | Основной клиент; общая логика в `templates`, UI выбирает параметр | +| `newProject/GUI_Android` | TMS/CAN/SET-клиент, общие порты/ядро | Тот же словарь; полнота нового GAS-логгера здесь не проверена | +| `newProject/Set_Terminal_28335` | Старый Delphi-клиент ПМ35 | Сохранить legacy адреса; новые окна требуют обновления клиента | +| `newProject/CAN_terminal`, `CANgaroo-0.16.0` | CAN-клиенты/мониторы; обзор файлов/описаний | Декодирование по профилю; просмотр кадра не доказывает GAS-команды | +| `newProject/PySetTerminal-master` | Терминал; обзор каталога | Клиент общего профиля после сверки parser | +| `LogsView`, `newProject/LogsView` | Просмотр журналов; обзор каталогов | Имена/единицы/масштаб из профиля, без собственного GAS-сервера | +| `PANE_ROSMORPORT` | Прикладное ПО; обзор каталога | Применимость зависит от данных; протокол не проверен | + +### Остальные каталоги + +Строки ниже включены для полноты инвентаризации. Применимость GAS здесь — архитектурная оценка, а не утверждение о найденной реализации. + +| Каталоги | Рассмотрение / применимость | +|---|---| +| `home_proj/altera`, `altera_loggerTK`, `altera_loggerTK_legacy_mixed_20260918`, `фдеукф`; `xilinx/cpld`, `omdazz_sp6`; `home_proj/zynq` | FPGA/CPLD: GAS через host/MCU-шлюз; быстрый поток со своим форматом. GAS HDL не проверен | +| `home_proj/DCDC_KONOR`, `focIhm08M01`, `g474_nucl_3phase`, `stm32g474_IHM08`, `john103C6T6NewVer` | Дополнительные платы: собственные карты не сверены, нужны отдельные profiles | +| `home_proj/GRBLSTM32F103`, `zigbeeWB55`, `HomeAssistant`, `Flipper`, `lamp_clock` | Собственные экосистемы; GAS возможен как дополнительная модель, необходимость не установлена | +| `newProject/home/ChameleonUltra`, `puty`, `DSView` | Устройства/анализаторы: общий словарь на стороне клиента при необходимости | +| `builds_labview`, `can_rs485_do_di_test/labview`, `newProject/matlab` | Клиенты/сборки: экспорт карты и клиентский адаптер; исходники доступны не для всех | +| `newProject/SET_Publisher`, `TroubleshootingDB`, `embedded-catalog` | Публикация/документация: хранить версии и описания профилей | +| `newProject/DOC`, `home_proj/site_Sveta` | Документы/сайт: публикация реестра; firmware-интеграция не требуется | +| `newProject/Новая папка`, `newProject/home/Новая папка` | Организационные/пустые каталоги; поддерживаемый проект не установлен | + +## 4. Конкретные адреса GAS ↔ Modbus ↔ CAN: ПМ35 + +Это **экспериментальный профиль** `gas_logger_maps/pm35.json`, а не карта уже работающей прошивки 167. Семь новых alias — RO: ядро GAS-логгера не реализует запись в них. Управление УМП остаётся отдельной операцией старого протокола. + +| Параметр | GAS | Modbus FC03 alias | ProtoCAN GAS MsgBody | Источник / legacy индекс | Формат | +|---|---|---|---|---|---| +| Команды УМП | `0x5000` | `0x5000` = 20480 | `0x5000` | `modbus[0x007F]` = 127 | Маска 16 бит, чтение команд | +| Расчётное задание тока | `0x5001` | `0x5001` = 20481 | `0x5001` | `modbus[0x001C]` = 28 | 0.1 мА; не измеренный ток выхода | +| Код ЦАП | `0x5002` | `0x5002` = 20482 | `0x5002` | `modbus[0x007B]` = 123 | Исходный код | +| Начальная уставка | `0x5003` | `0x5003` = 20483 | `0x5003` | `modbus[0x0050]` = 80 | мА по текущему контракту | +| Конечная уставка | `0x5004` | `0x5004` = 20484 | `0x5004` | `modbus[0x0051]` = 81 | мА по текущему контракту | +| ЗПТ1 | `0x5005` | `0x5005` = 20485 | `0x5005` | Кэш ПМ67; legacy CAN индекс 305 | Raw `u16`, масштаб отдельно | +| ЗПТ2 | `0x5006` | `0x5006` = 20486 | `0x5006` | Кэш ПМ67; legacy CAN индекс 8 | Raw `u16`, свежесть отдельно | + +Равенство трёх новых адресов — выбор профиля. Старые Modbus-адреса другие. `MsgBody` — часть ProtoCAN ID, а 305/8 — индексы старого payload; подставлять одно вместо другого нельзя. + +В `home/gas` сопоставление отличается: напряжение GAS ID `0x0123` → Modbus PDU-адрес `1000`; ток `0x0124` → `1001`, 1.25 A кодируется как 125. Это демонстрационные адреса, не назначение общих регистров всех приборов. + +### Занятые и экспериментальные окна + +| Профиль | Диапазон | Назначение / следствие | +|---|---|---| +| 167 | `0x0000…0x007F` | Технологические регистры: сохранить адреса/проверки записи | +| 167 | `0x1000…0x100F`, `0x1010…0x1087`, `0x1100…0x117B` | Статус, курсорное окно, онлайн УМП | +| 167 | `0x1200…0x120B` | Паспорт прошивки, 12 слов | +| 167 | `0x1300…0x1307` | Защита нагрузки, действующий проверяющий handler | +| 167 / AT25256B | `0x8000…0xBFFF` | RO-окно EEPROM, пересекает тестовый GAS-снимок | +| 167 / M95M02 | `0x8000…0xFFFF` + частное quantity/bank | Банковое EEPROM-чтение: не обычный плоский массив Modbus/GAS | +| Тест GAS ПМ35 | `0x5000…0x5006` | Семь alias; в firmware 167 не подключены | +| Тест GAS ПМ35 | Статус `0x6000…0x6011`, команды `0x6012…0x6014`, снимок `0x6100…0x849F` | **Снимок конфликтует с EEPROM 167** | +| Эмулятор `set_emu_server` | `0x1300` DAC, `0x1400` IGBT, `0x1500` TMS, `0x2000` архив, `0xA000` таблица DAC | Другой профиль: одинаковое число адреса может означать другой сервис | + +Расчёт полного тестового окна: `0x6000 + 0x0100 + 760 × (7 + 5) = 0x84A0` — конец, не включая. Проверять нужно весь сервис, а не только регистры команд. + +Для 167 возможен, например, **кандидат** base `0x2000`: данные такого снимка заняли бы `0x2100…0x449F`. Это предложение только для нового профиля 167, не общий назначенный адрес: нужны сверка с ОАП/полной картой, новый JSON/C-профиль и CRC. В эмуляторе `0x2000` уже занят. + +## 5. Ограничения и результаты проверки + +| Ограничение | Проверенный факт | Решение | +|---|---|---| +| C28x ядро | `gas_logger.c`, PM35 source-port, C28x IRQ-port скомпилированы TI 22.6 без ошибок | Ядро использовать можно; требуется полная firmware-компоновка | +| C28x byte API | `pcan_gas.c`, `set_regmap.c`: `identifier "uint8_t" is undefined` | Отделить word-map; octet-порт хранит 8-битные значения подходящим типом и явно сериализует | +| Размер C-byte | C28x `CHAR_BIT=16`; обычный ARM/PC — 8 | Простого typedef `uint8_t=uint16_t` недостаточно: проверить ABI/размеры/packing | +| Адреса | Тестовый снимок до `0x849F` пересекает EEPROM | Отдельный профиль и проверка пересечений | +| BIN 167 | Текущее дерево пересобрано: 32472 байта при лимите AT25256B 32508; запас **36 байт**. Проба GAS-алиасов: 32524 байта, превышение 16 | При сохранении всех функций проверенный GAS-вариант не помещается; подробности ниже | +| RAM УМП | `2 × 760 × 42 = 63840` слов внутри `logs_block[64000]` / `.logg` | Новые банки нельзя одновременно размещать в той же памяти | +| RAM GAS-логгера | `2 × 760 × 12 = 18240` слов = 36480 физических байт, без контекста/стека | Другая подтверждённая RAM, меньшая ёмкость или взаимно исключающие варианты | +| Состав записи | УМП 42 слова, тестовый GAS 12 | Сокращение трафика также связано с удалением полей; это не измерение ускорения одинакового журнала | +| Snapshot | Закреплён до RELEASE; нет STOP/автоосвобождения | Согласовать отмену/возобновление, BUSY и одного управляющего клиента | +| CAN-запись | Текущий ProtoCAN GAS без прикладного ACK | Проверять статус/поколение выполнения | +| Источники | PM35 source-port читает локальный кэш; не отправляет CAN-запрос | Только быстрые RAM-чтения под блокировкой; отдельные time/validity | +| Dispatcher 167 | `receive_ump()` перехватывает адреса от `0x1000` | Явная маршрутизация GAS-окон без попадания в чужой provider | +| Две copies templates | HEAD: third_party `86ece3ea907c`, соседний `eab8dc5e0a7f` | Зафиксировать ревизию; старые документы/пути не доказывают поддержку текущей сборкой | + +### 5.1. Предварительная проба EEPROM 167 до внедрения GAS + +Условие: сохранить функции проекта, текущую EEPROM AT25256B, настройки сборки и штатные протоколы. Замена регистратора, сокращение полей и переход на другую EEPROM в эту проверку не входят. + +| Показатель | Физические байты | Результат | +|---|---:|---| +| Полная EEPROM AT25256B | 32768 | 32 KiB | +| Параметры и исторически неиспользуемое последнее слово | 260 | 129 слов × 2 + 2 | +| Лимит загрузочного образа SCI8 | **32508** | 0x7EFC | +| Полная пересборка текущих исходников | **32472** | Свободно **36** байт | +| Проба GAS-алиасов к существующим 128 регистрам | **32524** | Добавлено **52** байта; превышение лимита **16** байт | +| Только общий словарь и преобразование адресов на ПК/шлюзе | +0 в прошивке | Существующий образ помещается; новая GAS-служба на устройстве не появляется | + +Проба сделана во временном каталоге: 28 исходных файлов, TI C2000 22.6.0.LTS, текущие -O2 --opt_for_speed=0 --gen_func_subsections=on, FPU32/COFF, существующий generated firmware identity. Использованы действующие linker-файлы и hex2000 -boot -sci8 -b. Контроль SHA-256 подтвердил, что все 28 исходных файлов остались без изменений. Конфигурация и рабочие BIN не изменялись. + +Временная копия **message.c** добавляет чтение/запись тех же регистров через Modbus FC03/FC06 по демонстрационному окну **0x5000…0x507F**, сохраняя старые адреса, проверки и обработчики. Это окно используется только для оценки размера и не назначает адреса общего профиля. Каталог GAS, новый CAN-handler и GAS-логгер не добавлялись. + +**Вывод:** проверенная компактная реализация GAS-алиасов при сохранении проекта не помещается в AT25256B. Расход полноценного общего provider и ProtoCAN-доступа пока не измерен. 52 байта — результат конкретной реализации, а не теоретический минимум любого GAS. Поэтому гарантировать размещение GAS в текущем запасе 36 байт нельзя. Общий словарь на ПК/шлюзе можно добавить без увеличения прошивки. + +Испытание на плате и полная интеграция библиотек GAS не выполнялись. Другие старые BIN имеют другие размеры; вывод выше относится к пересборке текущего дерева. + +Изолированная проверка 09.10.2026: TI `cl2000` 22.6.0.LTS, `--compile_only -v28 -ml -mt --float_support=fpu32 --abi=coffabi --c99 -O2 --opt_for_speed=0`, исходники из `SETGUI/third_party/templates`. Получены три успешных объекта и две неудачи byte API. Результаты создавались во временном каталоге. Это не проверка размера BIN или аппаратных таймингов. + +Особенности типов C28x подтверждены также [руководством TI, разделы 6.3 и J.3.4](https://www.ti.com/lit/ug/spru514y/spru514y.pdf). + +### 5.2. Реализовано: GAS только для M95M02 + +По последующему запросу GAS добавлен в проект под **EEPROM_M95M02=1**. Оба обработчика — RTU и служебный CAN УМП — используют один word-provider. Новые адреса 0x5000…0x507F отображают существующие 128 регистров, 0x5F00…0x5F07 публикуют сигнатуру и версию профиля 167/v1. Штатные адреса, защита, параметры и логгер сохранены. + +| Сборка | До GAS | После GAS | Добавочный размер | Лимит EEPROM для образа | Запас | +|---|---:|---:|---:|---:|---:| +| AT25256B | 32472 | **32472** | **0** | 32508 | 36 | +| M95M02 | 32764 | **33174** | **410** | 261884 | **228710** | + +Единицы — физические байты SCI8. AT25256B проверен сравнением с кодом до внедрения при одинаковом generated identity: BIN идентичен по SHA-256, GAS-символов в MAP нет. RAML0 M95M02 имеет запас 831 слов / 1662 байта. Настройки компилятора и размещение секций не менялись. + +Проверены обе целевые сборки TI, валидность boot-таблиц, host-тесты реальных RTU/CAN обработчиков, границы/RO/timeout, CRC и полный 124-словный CAN-снимок. Аппаратное испытание не выполнялось. + +Это профиль конкретного устройства поверх его действующих транспортов. Нативный ProtoCAN MsgType=0x3 и GAS-логгер не интегрировались. Полная карта, ограничения и готовый BIN: [GAS_167_M95M02.md](GAS_167_M95M02.md). + +## 6. Порядок внедрения + +| Этап | Результат | Критерий готовности | +|---|---|---| +| 1. Реестр/профили | Общие semantic keys, типы/масштабы/права, старые transport alias | Нет пересечений; источники/направления определены; ОАП согласован | +| 2. Общее ядро | Word-map/callbacks отдельно от byte ABI; C28x и ARM/PC-порты | Совпадают значения, ошибки, границы и операции | +| 3. STM32/host пилот | Чтение одной карты через RTU и существующий CAN/SET | Один источник даёт одинаковые данные через оба транспорта | +| 4. ПМ35/167 | GAS-фасад над штатными регистрами/сервисами | Старый терминал совместим, BIN/MAP проверены, управление/защита работают | +| 5. Остальные MCU | Отдельные профили 166/ПМ67/стендов/приборов | Для каждого проверены адреса, типы, права, память, тайминги | +| 6. Все клиенты | SETGUI/Android/CLI/просмотр журналов используют экспорт профиля | Имена/единицы/версия совпадают, неподдерживаемые сервисы обнаруживаются | +| 7. Логгеры/потоки | GAS-логгер для snapshot; GAS-настройки для OLGR/FPGA | Поля/триггеры сохранены; измерены RAM, IRQ и скорость | + +Начать с **общего словаря поверх существующих данных**, затем подключать транспорты, после этого решать замену логгеров. Высокочастотные выборки передавать блоками со своей временной базой и sequence; GAS описывает каналы, настройки и состояние. + +## Источники + +| Источник | Что проверялось | +|---|---| +| [main.c](../Source/Internal/main.c), [message.c](../Source/Internal/message.c), [ecan.c](../Source/Internal/ecan.c) | Реальные dispatcher/handlers 167 | +| [UMP_LOGGER.md](UMP_LOGGER.md), [EEPROM_LAYOUT.md](EEPROM_LAYOUT.md), [FIRMWARE_IDENTITY.md](FIRMWARE_IDENTITY.md), [UMP_VOLTAGE_PROTECTION.md](UMP_VOLTAGE_PROTECTION.md) | Карта, состав записи, паспорт и защита | +| [F28335.cmd](../F28335.cmd), [log_to_mem.c](../Source/Internal/log_to_mem.c), [manifest](../Bin/VoltageProtection/UKSSTMS320F28335_voltage_protection.bin.manifest.json) | Память и размер сохранённого образа | +| [GAS Logger](../../newProject/SETGUI/third_party/templates/c/gas-logger/README.md), [gas_logger.c](../../newProject/SETGUI/third_party/templates/c/gas-logger/src/gas_logger.c), [pm35_map.h](../../newProject/SETGUI/third_party/templates/c/gas-logger/ports/pm35/pm35_map.h), [pm35.json](../../newProject/SETGUI/third_party/templates/python/set_devices/gas_logger_maps/pm35.json) | Точный экспериментальный профиль/реализация | +| [GAS.md](../../newProject/SETGUI/third_party/templates/c/set-protocol/docs/legacy/GAS.md), [pcan_gas.h](../../newProject/SETGUI/third_party/templates/c/set-protocol/include/pcan_gas.h) | ProtoCAN GAS и ограничения | +| [EMULATOR_SERVICES](../../newProject/SETGUI/third_party/templates/c/set-protocol/docs/EMULATOR_SERVICES.md), [set_regmap.h](../../newProject/SETGUI/third_party/templates/c/set-protocol/include/set_regmap.h) | Каталог, dispatcher, byte ABI | +| [ОАП](../../newProject/SETGUI/third_party/templates/doc/setcan/protocan/OAP.md), [устройства](../../newProject/SETGUI/third_party/templates/doc/setcan/DEVICE_REGISTRY.md) | Нормативный источник и идентичность узла | +| [Host GAS](../../newProject/home/gas/README.md), [gas.h](../../newProject/home/gas/firmware/gas.h) | Пример ID/значений/HR | +| [PROJECT_RELATIONS](../../newProject/SETGUI/docs/PROJECT_RELATIONS.md), [FIRMWARE_PROTOCOL_AUDIT](../../newProject/SETGUI/docs/FIRMWARE_PROTOCOL_AUDIT.md), [GAS_LOGGER_EXPERIMENT](../../newProject/SETGUI/docs/GAS_LOGGER_EXPERIMENT.md) | Клиенты, связи и статус эксперимента | +| [CAN_to_RS485](../../newProject/can_to_rs/README.md), [ESP32](../../newProject/esp_parser/README.md), [KONOR](../../newProject/KONOR_ds18b20/PROTOCOL.md) | Мосты/каталоги/датчики | +| [CanBalzam7.c](../../BALZAM_167/Src/balzam_7/CanBalzam7.c), [UKSI](../../UKSI/Core/Src/freertos.c), [ПУИ](../../newProject/prolitarski_ups/doc/MPSU_EXCHANGE_PARAMETERS.md) | Существующие CAN/Modbus/legacy реализации | +| [Online Logger](../../home_proj/stm32h755_logger/online_logger/protocol/online_logger_header/README.md) | Параметры отдельно от потока OLGR | + +Ссылки отражают локальную структуру на дату анализа. HEAD не описывает незакоммиченные изменения. Для строк «обзор» использованы инвентаризация и поиск в собственных исходниках; отсутствие совпадения не доказывает отсутствие протокола. diff --git a/Doc/UMP_VOLTAGE_PROTECTION.md b/Doc/UMP_VOLTAGE_PROTECTION.md new file mode 100644 index 0000000..327c992 --- /dev/null +++ b/Doc/UMP_VOLTAGE_PROTECTION.md @@ -0,0 +1,115 @@ +# Контроль роста ЗПТ при пуске УМП + +УМП с адресом 5 (`Mode=5`) контролирует ЗПТ1 — CAN-индекс 305 +(регистр ПМ67 306). УМП 6 контролирует ЗПТ2 — CAN-индекс 8 +(регистр ПМ67 9). Масштаб исходного слова: 1 единица = 1 В. + +## Условия и последовательность + +Начальный безопасный код задания соответствует 4 мА по `DAC_min()`. +Рост напряжения считается достаточным при ЗПТ **не меньше 30 В**. +Проверяется расчётное задание ЦАП, а не измеренный выходной ток. +Ручная калибровка не подменяет задание при активной команде пуска +или удерживаемом STOP. + +Если очередной шаг рампы требует **строго больше 6 мА**, а свежее +CAN-напряжение меньше 30 В, шаг не передаётся в ЦАП. Вместо него: + +1. Снимается START, включается STOP, снимается разрешение RES_OUT_2. +2. Положение рампы обнуляется, передаётся код 4 мА без подмены калибровкой. +3. STOP остаётся включённым **1000 мс от момента его установки**. +4. Когда код 4 мА отправлен, прошла секунда и есть свежее ЗПТ, + снимается STOP, сразу выставляется START, затем разрешение. +5. Новая рампа начинается с нулевого положения с исходными уставками + `DAC_go`/`DAC_stop` (штатные значения могут начинать рост с 6 мА). + +Первый START входит в лимит: **всего 5 попыток**, не 5 дополнительных. +После пятой неудачной попытки удерживаются STOP и 4 мА, разрешение снято, +шестой START не выдаётся. Ошибка снимается командой `cUMPstart=0`; +затем можно подать новый пуск. Команда RESET имеет приоритет перед START. +Отпускание RESET с оставшимся START=1 само по себе не перезапускает УМП. + +Выходами и рампой управляет один обработчик Timer1 (1 мс). Основной цикл +больше не формирует независимые фронты START/STOP. Расчёт/отправка ЦАП +остались с периодом 5 мс. Импульс START длится до 1000 мс, но защита +или RESET прекращают его раньше. + +## Отсутствие CAN + +Таймаут по умолчанию **600 мс**, диапазон настройки **1…60000 мс**. +Свежесть подтверждается только получением нужного ЗПТ в полном +технологическом CAN-кадре с соответствующим битом маски. Другое +напряжение, сервисные запросы, команды и произвольные CAN-пакеты +таймаут не сбрасывают. До первого значения напряжение считается неизвестным. + +При таймауте работающая рампа удерживает последнее положение и отправленный +код. Попытка не считается неудачной, STOP по одному таймауту не включается. +После свежего ЗПТ рост продолжается; если оно меньше 30 В при задании +больше 6 мА, выполняется защитный STOP. Если обмен исчез во время +сброса, STOP держится и после секунды — до свежего напряжения. +Первый пуск также ждёт свежего значения. + +Настройка хранится в технологическом регистре `0x52` платы нагрузки. +Значение 0 в старом EEPROM заменяется на 600 мс при запуске. +Для сохранения изменения используйте штатную команду сохранения параметров. + +## Диагностика и эмуляция в GUI + +Во вкладке **Логгер УМП** добавлены: + +- **Защита ЗПТ: прочитать** — состояние защиты, число попыток, напряжение. +- **Эмуляция: нет CAN** — получение своего ЗПТ игнорируется платой; + после заданного таймаута рампа удерживается. +- **Эмуляция: ЗПТ не растёт** — для проверки защиты используется 0 В, + хотя настоящие кадры продолжают поступать. Реальные напряжения в + журнале не подменяются. Этот режим действительно вызывает STOP/пуски. +- **Задать таймаут** — запись значения в плату с обязательным чтением обратно. + +Кнопки эмуляции включаются после проверки сигнатуры прошивки. Их положение +меняется только после подтверждённого чтения обратно. Два режима можно +включать независимо; при отсутствии свежести проверка роста приостановлена. +Эмуляция живёт только в RAM, не сохраняется в EEPROM и выключена после +перезагрузки. GUI показывает активную эмуляцию отдельно от реального ЗПТ. + +При онлайн-чтении GUI опрашивает защиту каждые 500 мс и показывает +**«НЕИСПРАВНОСТЬ: ЗПТ не достигло 30 В за 5 попыток»**. +Старые прошивки без блока защиты продолжают обычное онлайн-чтение; +эмуляция для них недоступна. + +Технологические регистры только для чтения: `0x7C` — статус, +`0x7D` — число попыток. Оба включены в быстрый CAN-цикл. +Статус: бит 0 — ошибка пяти попыток, бит 1 — нет свежего ЗПТ, +бит 2 — сброс перед повтором, бит 3 — работа рампы, +биты 4/5 — активная эмуляция отсутствия CAN/роста ЗПТ. + +Локальный блок RTU и служебного CAN: FC03 `0x1300`, ровно 8 слов: + +| Смещение | Значение | +|---|---| +| 0 | Сигнатура `0x5550` | +| 1 | Версия 1 | +| 2 | Эмуляция: бит 0 — нет CAN, бит 1 — нет роста | +| 3 | Таймаут, мс | +| 4 | Статус защиты | +| 5 | Число попыток | +| 6 | Последнее реальное CAN-напряжение, В | +| 7 | Возраст значения, мс; 65535 до первого значения | + +FC06 допускает запись только `0x1302` (0…3) и `0x1303` (1…60000). +Диагностический обмен не обновляет возраст технологического напряжения. + +## Проверки + +`python tests/load_control/run_test.py` компилирует непосредственно +`DAC.c` с подменой GPIO/IRQ. Проверяются выбор ЗПТ, маски CAN, таймаут, +удержание кода, граница 30 В, порядок STOP/START, точная пауза 1000 мс, +лимит пяти пусков, RESET, эмуляция и невозможность подмены 4 мА калибровкой. + +GUI: `tests/test_ump_voltage_protection.py` в SETGUI плюс регрессии логгера. +Сборка: `build_logger.ps1 -OutputDirectory Bin/VoltageProtection +-ImageName UKSSTMS320F28335_voltage_protection`. Размер проверяется до +публикации BIN; для AT25256B предел — 32508 байт. В режиме оптимизации +функции разнесены по секциям, чтобы линкер удалял неиспользуемые функции. + +Это проверки на компьютере. Физический ток 4 мА, уровни разрешения, +STOP и интервал 1 с нужно проверить на стенде с установленной прошивкой. diff --git a/F28335.cmd b/F28335.cmd index 4912802..a472cd4 100644 --- a/F28335.cmd +++ b/F28335.cmd @@ -69,6 +69,18 @@ */ +/* 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. */ +#include "Source/Internal/Include/eeprom_config.h" +#include "include/balsam167_eeprom_layout.h" +_EEPROM_TOTAL_BYTES = BALSAM167_EEPROM_BYTES; +_EEPROM_FIRMWARE_LIMIT_BYTES = BALSAM167_EEPROM_FIRMWARE_BYTES; +_EEPROM_PARAMS_START_WORD = BALSAM167_EEPROM_PARAMS_START_WORD; +_EEPROM_PARAMS_START_BYTE = BALSAM167_EEPROM_FIRMWARE_BYTES; +_EEPROM_PARAMS_END_BYTE = BALSAM167_EEPROM_PARAMS_END_WORD * 2; + MEMORY { PAGE 0: /* Program Memory */ diff --git a/Source/Internal/ADC.c b/Source/Internal/ADC.c index 119d8a4..57ac07b 100644 --- a/Source/Internal/ADC.c +++ b/Source/Internal/ADC.c @@ -1,5 +1,5 @@ #include "DSP2833x_Device.h" // DSP281x Headerfile Include File -#include "DSP2833x_Examples.h" // DSP281x Examples Include File +#include "DSP2833x_Examples.h" // DSP281x Examples Include File #include "DSP2833x_SWPrioritizedIsrLevels.h" #include "ADC.h" diff --git a/Source/Internal/DAC.c b/Source/Internal/DAC.c index ba02453..e09cc7b 100644 --- a/Source/Internal/DAC.c +++ b/Source/Internal/DAC.c @@ -21,12 +21,110 @@ unsigned long WAKEpowse; unsigned long IMPowse; unsigned int period_dac, time_dac; +/* Âñå âûõîäû ÓÌÏ è ðàìïà ïðèíàäëåæàò îäíîìó îáðàáîò÷èêó Timer1. + * Îñíîâíîé öèêë áîëüøå íå âûäà¸ò START: èíà÷å îí ìîæåò óâèäåòü êîðîòêèé + * ñïàä êîìàíäû, êîòîðûé ïðîïóñòèò ðàñ÷¸ò ÖÀÏ ñ ïåðèîäîì 5 ìñ. */ +#define LOAD_IDLE 0U +#define LOAD_RUN 1U +#define LOAD_RELOAD 2U +#define LOAD_FAULT 3U +#define LOAD_MAX_ATTEMPTS 5U +#define LOAD_VOLTAGE_MIN 30U +#define LOAD_PULSE_MS 1000U +static unsigned int load_state, load_attempts, load_inhibit; +static unsigned int load_count_dac, load_count_ramp, load_init_count; +static unsigned int load_zero_sent, load_stop_asserted, load_initialized; +static float load_go, load_end; +/* 16-áèòíûå ïîëÿ îáùèå ñ CAN ISR. Ñíèìîê è óâåëè÷åíèå âîçðàñòà âûïîëíÿþòñÿ + * ïîä êîðîòêîé áëîêèðîâêîé IRQ; îòïðàâêà SPI ïîä áëîêèðîâêîé íå âûïîëíÿåòñÿ. */ +static volatile unsigned int load_voltage, load_voltage_age, load_voltage_seen; +static volatile unsigned int load_simulation; + +void Load_control_init(void) +{ + load_state = LOAD_IDLE; load_attempts = 0; load_inhibit = 0; + load_count_dac = 0; load_count_ramp = 0; load_init_count = 1; + load_zero_sent = 0; load_stop_asserted = 0; load_initialized = 0; + load_go = 0; load_end = 0; + load_voltage = 0; load_voltage_age = 0; load_voltage_seen = 0; + load_simulation = 0; + IMPowse = 0; WAKEpowse = 0; period_dac = 0; time_dac = 0; +} + +/* Âîçðàñò ñáðàñûâàåò òîëüêî íóæíîå íàïðÿæåíèå, à íå ëþáîé CAN-ïàêåò. + * ÓÌÏ 5: ÇÏÒ1, èíäåêñ 305; ÓÌÏ 6: ÇÏÒ2, èíäåêñ 8. Ìàñøòàá 1 Â/åä. + * Ìàñêè ñëîòîâ ñîõðàíÿþò àäðåñà äàæå äëÿ ïðîïóùåííûõ ñëîâ êàäðà. */ +void Load_voltage_can(unsigned int header, unsigned int a, + unsigned int b, unsigned int c) +{ + unsigned int address = header & 0x1FFFU; + unsigned int selected, i, mask = 0x8000U, data[3]; + if (Desk != dsk_LOAD || (load_simulation & 1U)) return; + if (Mode == adr_LOA1) selected = 305U; + else if (Mode == adr_LOA2) selected = 8U; + else return; + data[0] = a; data[1] = b; data[2] = c; + for (i = 0; i < 3; i++, address++, mask >>= 1) { + if ((header & mask) && address == selected) { + load_voltage = data[i]; + load_voltage_age = 0; + load_voltage_seen = 1; + } + } +} + +/* STOP óäåðæèâàåòñÿ âåñü ñáðîñ, âêëþ÷àÿ îòïðàâêó êîäà 4 ìÀ â ÖÀÏ. + * Ïîâòîðíûé ïóñê ðàçðåø¸í òîëüêî ïîñëå ýòîé îòïðàâêè è 1000 ìñ STOP. + * Ýòî ïîäòâåðæäåíèå ïðîãðàììíîé îòïðàâêè, íå èçìåðåíèå ôèçè÷åñêîãî òîêà. */ +static void load_stop(unsigned int state) +{ + clear_START_DPTB(); + set_STOP_DPTB(); + clear_RES_OUT_2(); + load_stop_asserted = 1; + load_state = state; + load_count_ramp = 0; + load_zero_sent = 0; + IMPowse = LOAD_PULSE_MS; +} + +static void load_start(void) +{ + /* Óñòàâêè ôèêñèðóþòñÿ äëÿ êàæäîé ïîïûòêè. Âíóòðåííèé íîëü = 4 ìÀ. + * Èñïðàâëåíà îïå÷àòêà ñòàðîãî îãðàíè÷åíèÿ: îãðàíè÷èâàåì êîíåö ðàìïû, + * à íå çàìåíÿåì íà÷àëüíóþ óñòàâêó íà 16 (÷òî äàâàëî ñêà÷îê äî 20 ìÀ). */ + load_go = (float)DAC_go - 4.0; + load_end = (float)DAC_stop - 4.0; + if (load_go < 0) load_go = 0; + if (load_go > 5) load_go = 5; + if (load_end > 16) load_end = 16; + if (load_end <= load_go) load_end = load_go + 1; + ump_dac_go10 = (unsigned int)((load_go + 4.0) * 10.0); + ump_dac_stop10 = (unsigned int)((load_end + 4.0) * 10.0); + load_count_ramp = 0; + load_attempts++; + /* STOP îòïóñêàåì íåïîñðåäñòâåííî ïåðåä START, ðàçðåøåíèå — ïîñëå íåãî. + * Çàäàíèå îñòà¸òñÿ 4 ìÀ äî ïåðâîãî øàãà íîâîé ðàìïû. */ + clear_STOP_DPTB(); + load_stop_asserted = 0; + set_START_DPTB(); + set_RES_OUT_2(); + IMPowse = LOAD_PULSE_MS; + load_state = LOAD_RUN; +} + void Setup_DAC_time(void) { WAKEpowse = 3L * DAC_FREQ +1; period_dac = READY_FREQ / DAC_FREQ; time_dac = LOAD_TIME * DAC_FREQ; + /* Ñòàðûå EEPROM èìåþò íîëü â íîâîì ïàðàìåòðå: èñïîëüçóåì 600 ìñ. + * 1..60000 ìñ ìîæíî ìåíÿòü ÷åðåç ðåãèñòð 0x52 è ñîõðàíÿòü øòàòíî. */ + if ((unsigned int)LOAD_CAN_TIMEOUT == 0U || + (unsigned int)LOAD_CAN_TIMEOUT > 60000U) LOAD_CAN_TIMEOUT = 600; + LOAD_STATUS = 0; LOAD_ATTEMPTS = 0; + load_initialized = 1; } void dOUT(int x) @@ -63,7 +161,7 @@ void Init_DAC(void) toggle_RES_OUT_1(); } -void Anal_output(long vrot, long maxx) +static void anal_output(long vrot, long maxx, int calibration) { long out; static int x; @@ -72,7 +170,7 @@ void Anal_output(long vrot, long maxx) x=0;// !x; òùåòíî áûòèå - if(cCalibrDac) out=DAC_cal(); + if(calibration && cCalibrDac) out=DAC_cal(); out = out & 0xFFF; @@ -99,79 +197,145 @@ Log_to_mem(modbus[0x1D]); } -void Load_runner() +/* Ñîõðàí¸í ïóáëè÷íûé èíòåðôåéñ; çàùèòíûé ñáðîñ âûçûâàåò âíóòðåííþþ + * ôóíêöèþ ñ calibration=0, ÷òîáû ðó÷íàÿ êàëèáðîâêà íå ïîäìåíèëà 4 ìÀ. */ +void Anal_output(long vrot, long maxx) { - static unsigned int count_dac=0, count_load=0, x=1; - static unsigned int preStart=0; - static float dac_go,dac_stop; - float sinus=0; - int kod; + if (maxx > 0) anal_output(vrot, maxx, 1); +} - if(IMPowse) IMPowse--; - if(++count_dac >= period_dac) - { - count_dac=0; +void Load_runner(void) +{ + unsigned int saved, commands, voltage, fresh, timeout; + unsigned int start, reset, active = 0; + float sinus = 0; - if(WAKEpowse) - { - --WAKEpowse; + /* ISR ðàáîòàåò 1 êÃö, íî GPIO/ÖÀÏ íåëüçÿ îáñëóæèâàòü äî çàãðóçêè + * ïàðàìåòðîâ. Ýòî òàêæå èñêëþ÷àåò ïðåæíåå äåëåíèå íà time_dac=0. */ + if (!load_initialized || !READY || !period_dac || !time_dac) return; + saved = __disable_interrupts(); + commands = (unsigned int)Commands; + voltage = (load_simulation & 2U) ? 0U : load_voltage; + timeout = (unsigned int)LOAD_CAN_TIMEOUT; + if (!timeout || timeout > 60000U) timeout = 600U; + /* Äî ïåðâîãî ïàêåòà çíà÷åíèå 0 íå ñ÷èòàåòñÿ ñâåæèì èçìåðåíèåì. + * Íàñûùåíèå âîçðàñòà èñêëþ÷àåò ëîæíîå âîññòàíîâëåíèå ïîñëå ïåðåïîëíåíèÿ. */ + fresh = load_voltage_seen && load_voltage_age < timeout; + if (load_voltage_age < 65535U) load_voltage_age++; + __restore_interrupts(saved); + start = commands & 0x80U; reset = commands & 0x40U; + if (IMPowse) IMPowse--; - if( (WAKEpowse == DAC_FREQ/2*6)|| - (WAKEpowse == DAC_FREQ/2*5)|| - (WAKEpowse == DAC_FREQ/2*4)|| - (WAKEpowse == DAC_FREQ/2*3)|| - (WAKEpowse == DAC_FREQ/2*2)|| - (WAKEpowse == DAC_FREQ/2*1) ) - { - Init_DAC(); x=1; - } } + /* Îòñóòñòâèå êîìàíäû ïóñêà — íà÷àëî íîâîãî ñåàíñà, ñáðàñûâàåò îøèáêó. + * RESET âñåãäà ñòàðøå START; îòïóñêàíèå RESET ïðè îñòàâøåìñÿ START + * íå äà¸ò íåîæèäàííîãî ïîâòîðíîãî ïóñêà: ñíà÷àëà òðåáóåòñÿ START=0. */ + if (!start) { + if (load_state != LOAD_IDLE) load_stop(LOAD_IDLE); + load_attempts = 0; load_inhibit = 0; + } else if (reset) { + if (load_state != LOAD_FAULT && load_state != LOAD_IDLE) + load_stop(LOAD_IDLE); + load_inhibit = 1; + } + if (reset && !load_stop_asserted) load_stop(LOAD_IDLE); - else - { - if(cUMPstart) - { - if(!preStart) - { - dac_go=DAC_go-4; - dac_stop=DAC_stop-4; - if(dac_go<0) dac_go=0; - if(dac_go>5) dac_go=5; - if(dac_stop>16) dac_go=16; - if(dac_stop<=dac_go) dac_stop=dac_go+1; - /* Äëÿ ëîãà ñîõðàíÿåì ðåàëüíûå îãðàíè÷åííûå óñòàâêè: - * âíóòðåííèå dac_go/stop îòñ÷èòûâàþòñÿ îò 4 ìÀ. */ - ump_dac_go10 = (unsigned int)((dac_go + 4.0) * 10.0); - ump_dac_stop10 = (unsigned int)((dac_stop + 4.0) * 10.0); - } + /* Ïîâòîðíûé START äîïóñêàåòñÿ ïîñëå ñáðîñà ÖÀÏ, îêîí÷àíèÿ STOP è + * âîññòàíîâëåíèÿ íóæíîãî CAN-íàïðÿæåíèÿ. Òàéìàóò ñàì íå òðàòèò ïîïûòêó. */ + if (start && !reset && !load_inhibit && !WAKEpowse && fresh && + load_zero_sent && !IMPowse && + (load_state == LOAD_IDLE || load_state == LOAD_RELOAD)) load_start(); + if (load_state == LOAD_RUN && !IMPowse) clear_START_DPTB(); - if(count_load > (time_dac/2))count_load++; - - if(++count_load > time_dac) count_load = time_dac; - - sinus = ((dac_stop - dac_go) / 16.0 * count_load) + (dac_go / 16.0 * time_dac); - -// sinus = (10.0 / 16.0 * count_load) + (2.0 / 16.0 * time_dac); - } - else - { count_load = 0; - sinus=0; - x = (x+1)&0x3FF; - } - - /* Ñíèìîê ñòàäèè ðàçãîíà äëÿ ñîïîñòàâëåíèÿ êîìàíäû CAN, - * îæèäàåìîãî òîêà è êîäà, äåéñòâèòåëüíî îòïðàâëåííîãî ÖÀÏ. */ - ump_dac_ramp = count_load; + if (++load_count_dac >= period_dac) { + load_count_dac = 0; + if (WAKEpowse) { + --WAKEpowse; + if (WAKEpowse && !(WAKEpowse % (DAC_FREQ / 2))) { + Init_DAC(); load_init_count = 1; + } + } else { + active = load_state == LOAD_RUN; + if (active) { + /* Ïðè òàéìàóòå ñîõðàíÿåì ïîëîæåíèå È îòïðàâëåííîå çàäàíèå, + * íå äåëàåì STOP è íå ïðîäîëæàåì ðàçãîí áåç îáðàòíîé ñâÿçè. */ + if (fresh) { + if (load_count_ramp > time_dac / 2) load_count_ramp++; + if (++load_count_ramp > time_dac) load_count_ramp = time_dac; + } + sinus = ((load_end - load_go) / 16.0 * load_count_ramp) + + (load_go / 16.0 * time_dac); + /* Ïðîâåðÿåì ñëåäóþùèé øàã ÄÎ ïåðåäà÷è ÖÀÏ. 6 ìÀ äîïóñòèìû, + * ñòðîãî >6 ìÀ è <30  òðåáóþò íåìåäëåííîãî STOP. + * Ïåðâàÿ ïîïûòêà âõîäèò â ëèìèò 5; øåñòîãî START íå áóäåò. */ + if (fresh && voltage < LOAD_VOLTAGE_MIN && + sinus * 16.0 > 2.0 * time_dac) { + load_stop(load_attempts >= LOAD_MAX_ATTEMPTS ? + LOAD_FAULT : LOAD_RELOAD); + active = 0; sinus = 0; + } + } else { + load_count_ramp = 0; + load_init_count = (load_init_count + 1) & 0x3FFU; + } + /* Âî âðåìÿ çàùèòû è ïóñêà ðó÷íîé êîä êàëèáðîâêè çàïðåù¸í. + * Êàëèáðîâêà ñîõðàíåíà òîëüêî ïðè ñíÿòîì START è ñâîáîäíîì STOP. + * Íóëåâîé êîä çàäàíèÿ çäåñü îçíà÷àåò òîê 4 ìÀ ïî DAC_min(). */ + ump_dac_ramp = load_count_ramp; ump_dac_duration = time_dac; ump_dac_position = (unsigned int)sinus; - preStart = cUMPstart; + if (active && !fresh) { + /* Óäåðæèâàåì ÖÀÏ. Äàæå cInitDac íå ìåíÿåò òîê â ïàóçå CAN. */ + } else if ((!load_init_count || cInitDac) && !load_stop_asserted) { + Init_DAC(); load_init_count = 1; cInitDac = 0; + } else { + anal_output((int)sinus, time_dac, + !start && !load_stop_asserted); + /* A manual calibration frame is not a confirmed 4 mA reload. + * START must wait for a forced baseline frame after it. */ + if (!active) load_zero_sent = !(!start && !load_stop_asserted && cCalibrDac); + } + } + } + /* Äèàãíîñòèêà äîñòóïíà îáû÷íûì Modbus/CAN, íåçàâèñèìî îò ëîããåðà: + * áèò 0 — îøèáêà ïÿòè ïóñêîâ; áèò 1 — íåò ñâåæåãî íàïðÿæåíèÿ; + * áèò 2 — ñáðîñ ïåðåä ïîâòîðîì; áèò 3 — ðàáîòà ðàìïû. + * Îøèáêà óäåðæèâàåòñÿ ñî STOP, ïîêà îïåðàòîð íå ñíèìåò START. */ + LOAD_STATUS = (load_state == LOAD_FAULT ? 1U : 0U) | + (!fresh ? 2U : 0U) | + (load_state == LOAD_RELOAD ? 4U : 0U) | + (load_state == LOAD_RUN ? 8U : 0U) | + (load_simulation << 4); + LOAD_ATTEMPTS = load_attempts; +} - if(!x || cInitDac) - { - Init_DAC(); x=1; cInitDac=0; - } - else - { - kod = (int)sinus; - - Anal_output(kod, time_dac); -} } } } +/* Local versioned block, shared by RTU and service CAN. Probe its signature + * before writes: old firmware returns exception 2 instead of starting tests. + * Simulation is RAM-only, zeroed at boot, and never copied into EEPROM. + * bit 0 ignores ZPT reception (age reaches the configured timeout); + * bit 1 substitutes 0 V only for protection, leaving real logged CAN intact. */ +unsigned int Load_protection_prepare(unsigned int function, unsigned int address, + unsigned int value, unsigned int *out, unsigned int *error) +{ + unsigned int saved = __disable_interrupts(); + *error = 2; + if (Desk == dsk_LOAD && address == 0x1300U && function == 3 && value == 8) { + out[0] = 0x5550U; out[1] = 1; + out[2] = load_simulation; out[3] = (unsigned int)LOAD_CAN_TIMEOUT; + out[4] = (unsigned int)LOAD_STATUS; out[5] = load_attempts; + out[6] = load_voltage; out[7] = load_voltage_seen ? load_voltage_age : 65535U; + *error = 0; + __restore_interrupts(saved); + return 8; + } + if (Desk == dsk_LOAD && function == 6) { + if (address == 0x1302U && value <= 3U) { + load_simulation = value; + *error = 0; + } else if (address == 0x1303U && value >= 1U && value <= 60000U) { + LOAD_CAN_TIMEOUT = value; *error = 0; + } else if (address == 0x1302U || address == 0x1303U) *error = 3; + } + out[0] = address; out[1] = value; + __restore_interrupts(saved); + return 2; +} diff --git a/Source/Internal/Include/DAC.h b/Source/Internal/Include/DAC.h index 257b94f..8139737 100644 --- a/Source/Internal/Include/DAC.h +++ b/Source/Internal/Include/DAC.h @@ -6,3 +6,13 @@ void Setup_DAC_time(void); /* Ñ÷¸ò÷èê ïóñêîâîãî èìïóëüñà è âûäåðæêè èíèöèàëèçàöèè äîñòóïíû ëîããåðó. */ extern unsigned long IMPowse, WAKEpowse; extern unsigned int period_dac, time_dac; + +/* Call once before IRQ; complete technological CAN packets update voltage. */ +void Load_control_init(void); +void Load_voltage_can(unsigned int header, unsigned int a, + unsigned int b, unsigned int c); + +/* 0x1300/8 words: signature, version, simulation, timeout, status, attempts, + * voltage, age. Only 0x1302 (simulation) and 0x1303 (timeout) are writable. */ +unsigned int Load_protection_prepare(unsigned int function, unsigned int address, + unsigned int value, unsigned int *out, unsigned int *error); diff --git a/Source/Internal/Include/caliber.h b/Source/Internal/Include/caliber.h index 5da8d7b..dcaf6af 100644 --- a/Source/Internal/Include/caliber.h +++ b/Source/Internal/Include/caliber.h @@ -10,8 +10,8 @@ 4876, 5005, 4876, 4847, 1898, 1873, 2535, 2502, // Áîðò 1 // ÓÌÏ UA UC IA IC 20mA O 4mA O 20mA I 4mA I - 3820, 3790, 3505, 3380, 1736, 683, 854, 14, // Áîðò 0 1062 1980 - 3835, 3785, 3522, 3332, 1745, 689, 860, 17, // Áîðò 1 1720 2405 + 7580, 7540, 2310, 2420, 1736, 683, 854, 14, // Áîðò 0 1062 1980 + 7580, 7540, 2310, 2420, 1745, 689, 860, 17, // Áîðò 1 1720 2405 // ÁÊÑÑÄ 100 Ohm 150 Ohm 1281, 1272, 0, 0, 622, 2263, 0, 0, diff --git a/Source/Internal/Include/eeprom_config.h b/Source/Internal/Include/eeprom_config.h new file mode 100644 index 0000000..f3c3eaa --- /dev/null +++ b/Source/Internal/Include/eeprom_config.h @@ -0,0 +1,15 @@ +#ifndef BALSAM167_EEPROM_CONFIG_H +#define BALSAM167_EEPROM_CONFIG_H +/* Build-time checkbox: 0 = AT25256B (32 KiB), 1 = M95M02-DR (256 KiB). + * This must match the soldered part BEFORE startup loads saved parameters. + * CCS: change the value below, then Project > Clean and Rebuild. + * Command-line builds may override EEPROM_M95M02 for that build only. + * A GUI checkbox cannot change SPI ROM boot or startup initialization. + */ +#ifndef EEPROM_M95M02 +#define EEPROM_M95M02 0 +#endif +#if EEPROM_M95M02 != 0 && EEPROM_M95M02 != 1 +#error EEPROM_M95M02_must_be_0_or_1 +#endif +#endif diff --git a/Source/Internal/Include/gas167.h b/Source/Internal/Include/gas167.h new file mode 100644 index 0000000..444a5f2 --- /dev/null +++ b/Source/Internal/Include/gas167.h @@ -0,0 +1,28 @@ +#ifndef BALSAM167_GAS_H +#define BALSAM167_GAS_H +#include "eeprom_config.h" +#include + +/* Board profile 167/v1, word addresses, independent of the wire transport. + * The original register and UMP addresses remain valid in both builds. + * Only the physically fitted M95M02 version exposes the additional regions. */ +#if EEPROM_M95M02 +#define GAS167_REG_BASE 0x5000U +#define GAS167_REG_COUNT 128U +#define GAS167_INFO_BASE 0x5F00U +#define GAS167_INFO_WORDS 8U +#define GAS167_MAX_WORDS 124U +#define GAS167_PROFILE_VERSION 1U +#define GAS167_CAPABILITIES 7U /* bit 0 RTU, bit 1 service CAN, bit 2 FC06 */ + +/* Called by the main-loop transports under their snapshot IRQ lock. + * Capacity: at least GAS167_MAX_WORDS uint16_t words. + * RTU reads: 1..124 words; paired service CAN reads: 2..124, even. + * Successful reads return their count. Writes/errors return address/value. + * error uses Modbus codes 0 (success), 1 (function), 2 (address), 3 (value). + * No EEPROM I/O, allocation, or changes to the existing logger. */ +int gas167_contains(uint16_t address); +unsigned gas167_prepare(unsigned function, uint16_t address, uint16_t value, + int can, uint16_t *reply, unsigned *error); +#endif +#endif diff --git a/Source/Internal/Include/package.h b/Source/Internal/Include/package.h index 0b6672d..e90cf33 100644 --- a/Source/Internal/Include/package.h +++ b/Source/Internal/Include/package.h @@ -95,6 +95,11 @@ #define DAC_go modbus[0x50] // íà÷àëî çàðàäà #define DAC_stop modbus[0x51] // êîíåö çàðàäà +/* LOAD-only persistent CAN timeout (ms), read-only status and attempts. */ +#define LOAD_CAN_TIMEOUT modbus[0x52] +#define LOAD_STATUS modbus[0x7C] +#define LOAD_ATTEMPTS modbus[0x7D] + #define Cancount (modbus+0x60) // ïàóçà ìåæäó I ïîñûëêàìè CAN #define Bright (modbus+0x62) // àðêîñòü ñèãíàëüíûõ ëàìïî÷åê #define Brightness modbus[0x62] // àðêîñòü ñèãíàëüíûõ ëàìïî÷åê diff --git a/Source/Internal/Include/spise2p.h b/Source/Internal/Include/spise2p.h index 286c879..c94bba8 100644 --- a/Source/Internal/Include/spise2p.h +++ b/Source/Internal/Include/spise2p.h @@ -2,8 +2,19 @@ #define __SPISE2P_H__ +/* Physical EEPROM layout shared with the linker and SETGUI. */ +#include "eeprom_config.h" +#include "include/balsam167_eeprom_layout.h" + // ¨ìêîñòü ïàìàòè â áàéòàõ -#define SEEPROM_LEN 0x10000 +/* Preserve compact 16-bit arithmetic for the original 32-KiB build. + * Only the large part needs 32-bit word addresses and transfer lengths. */ +#if EEPROM_M95M02 +typedef unsigned long SE2P_SIZE; +#else +typedef unsigned int SE2P_SIZE; +#endif +#define SEEPROM_LEN BALSAM167_EEPROM_BYTES #define NULL 0 @@ -58,8 +69,10 @@ typedef struct { /* Object declaration */ typedef struct { - SE2P_DATA *msgPtr; - unsigned int csr; /* control/status register */ +/* Main loop publishes the descriptor before setting csr; Timer2 consumes it. + * Both accesses are volatile so optimization cannot publish the request first. */ + SE2P_DATA * volatile msgPtr; + volatile unsigned int csr; /* control/status register */ void (*init)(void *); void (*tick)(void *); void (*csset)(void); @@ -85,12 +98,12 @@ void spiSe2pWrite(SPISE2P_DRV *se2p, SE2P_DATA *data); void spiSe2pRead(SPISE2P_DRV *se2p, SE2P_DATA *data); #if(SPISE2P_DATA_WIDTH==SIXTEEN_BIT) - #define PROM_LEN 0x8000 - #define PAGE_LEN 0x20 + #define PROM_LEN BALSAM167_EEPROM_WORDS + #define PAGE_LEN (BALSAM167_EEPROM_PAGE_BYTES / 2) #define WORD_LEN 2 #else - #define PROM_LEN 0x4000 - #define PAGE_LEN 0x40 + #define PROM_LEN BALSAM167_EEPROM_BYTES + #define PAGE_LEN BALSAM167_EEPROM_PAGE_BYTES #define WORD_LEN 1 #endif @@ -105,7 +118,7 @@ void InitSeeprom(void); ** adres = 0..0x4000, åñëè äëèíà ñëîâà 16 áèò ** ** buf - óêàçàòåëü íà ïàìàòü, îòêóäà ïèñàòü. ** ** size - äëèíà áëîêà â áàéòàõ. Ïî-ëþáîìó â áàéòàõ! */ -void Seeprom_write(unsigned int adres, unsigned int buf[], unsigned int size); +void Seeprom_write(SE2P_SIZE adres, unsigned int buf[], SE2P_SIZE size); /* ×òåíèå áëîêà èç SEEPROM. Ïàðàìåòðû òàêîâû: ** ** adres - àäðåñ â åïðîìêå, îòêóäà ÷èòàòü. ** @@ -113,7 +126,7 @@ void Seeprom_write(unsigned int adres, unsigned int buf[], unsigned int size); ** adres = 0..0x4000, åñëè äëèíà ñëîâà 16 áèò ** ** buf - óêàçàòåëü íà ïàìàòü, êóäà ÷èòàòü. ** ** size - äëèíà áëîêà â áàéòàõ. Ïî-ëþáîìó â áàéòàõ! */ -void Seeprom_read(unsigned int adres, unsigned int buf[], unsigned int size); +void Seeprom_read(SE2P_SIZE adres, unsigned int buf[], SE2P_SIZE size); #endif diff --git a/Source/Internal/bios.c b/Source/Internal/bios.c index 02eb806..eb632d3 100644 --- a/Source/Internal/bios.c +++ b/Source/Internal/bios.c @@ -398,7 +398,16 @@ void extendbios(RS_DATA *rs_arr) switch ( code ) { // Ïðîøèâàåì EPROM Èç RAM - case 4: Seeprom_write(Address1,(unsigned int*)Address2,Length); + #if EEPROM_M95M02 + /* 0x44 is intentionally rejected by old firmware. Conversely this build + * rejects legacy code 4, preventing writes with the wrong GUI profile. */ + case 0x44: +#else + case 4: +#endif + if (!Length || Address1 >= BALSAM167_EEPROM_FIRMWARE_BYTES/2UL || + Length > BALSAM167_EEPROM_FIRMWARE_BYTES-Address1*2UL) return; + Seeprom_write(Address1,(unsigned int*)Address2,Length); break; // ×èòàåì èç EPROM â RAM case 5: Seeprom_read(Address1,(unsigned int*)Address2,Length); diff --git a/Source/Internal/ecan.c b/Source/Internal/ecan.c index e3ac7ac..8fe4a4e 100644 --- a/Source/Internal/ecan.c +++ b/Source/Internal/ecan.c @@ -12,7 +12,9 @@ #include "message.h" #include "peripher.h" #include "log_to_mem.h" +#include "DAC.h" #include "ump_can_protocol.h" +#include "gas167.h" // Prototype statements for functions found within this file. @@ -234,11 +236,18 @@ void Handlai(volatile struct ECAN_MBOXES * ECanMboxes) Data[1] = (loword>>16) & 0xffff; Data[2] = (loword ) & 0xffff; - if(bit[0]) if(adr < ANSWER_LEN) modbus[adr] = Data[0]; adr++; - if(bit[1]) if(adr < ANSWER_LEN) modbus[adr] = Data[1]; adr++; - if(bit[2]) if(adr < ANSWER_LEN) modbus[adr] = Data[2]; + if(bit[0]) if(adr < ANSWER_LEN && + !(Desk == dsk_LOAD && (adr == 0x7CU || adr == 0x7DU))) modbus[adr] = Data[0]; adr++; + if(bit[1]) if(adr < ANSWER_LEN && + !(Desk == dsk_LOAD && (adr == 0x7CU || adr == 0x7DU))) modbus[adr] = Data[1]; adr++; + if(bit[2]) if(adr < ANSWER_LEN && + !(Desk == dsk_LOAD && (adr == 0x7CU || adr == 0x7DU))) modbus[adr] = Data[2]; /* Ôèêñèðóåì ïðèìåí¸ííóþ êîìàíäó äî âûõîäà èç îáðàáîò÷èêà CAN. */ + /* A short frame cannot confirm voltage freshness. Service frames + * UMP_MARKER return above and do not refresh this timeout. */ + if (ECanMboxes->MBOX1.MSGCTRL.bit.DLC == 8) + Load_voltage_can((unsigned int)(hiword >> 16), Data[0], Data[1], Data[2]); ump_log_can((unsigned int)(hiword >> 16), Data[0], Data[1], Data[2]); led2_toggle(); } @@ -316,7 +325,13 @@ void UMP_CAN_service(void) saved = __disable_interrupts(); tag = ump_can_tag; address = ump_can_address; value = ump_can_value; ump_can_pending = 0; - ump_can_length = ump_can_prepare(tag & 0xFF, address, value, ump_can_reply, &error); + if (address >= 0x1300U && address < 0x1308U) + ump_can_length = Load_protection_prepare(tag & 0xFF, address, value, ump_can_reply, &error); +#if EEPROM_M95M02 + else if (gas167_contains(address)) + ump_can_length = gas167_prepare(tag & 0xFF, address, value, 1, ump_can_reply, &error); +#endif + else ump_can_length = ump_can_prepare(tag & 0xFF, address, value, ump_can_reply, &error); ump_can_reply_tag = (tag & 0xFF00) | error; ump_can_index = 0; __restore_interrupts(saved); diff --git a/Source/Internal/gas167.c b/Source/Internal/gas167.c new file mode 100644 index 0000000..0404dfc --- /dev/null +++ b/Source/Internal/gas167.c @@ -0,0 +1,63 @@ +#include "gas167.h" +#if EEPROM_M95M02 +#include "package.h" + +extern int modbus[]; +extern int Desk; +extern void ump_log_rs(void); + +static const uint16_t gas167_info[GAS167_INFO_WORDS] = { + 0x4741U, 0x5331U, GAS167_PROFILE_VERSION, 167U, + GAS167_REG_BASE, GAS167_REG_COUNT, GAS167_CAPABILITIES, 1U +}; + +int gas167_contains(uint16_t address) +{ + return (uint16_t)(address - GAS167_REG_BASE) < GAS167_REG_COUNT || + (uint16_t)(address - GAS167_INFO_BASE) < GAS167_INFO_WORDS; +} + +static unsigned gas167_write(uint16_t offset, uint16_t value) +{ + /* Match the existing native Modbus write policy. These status words + * cannot be overwritten and an invalid timeout cannot bypass protection. */ + if (Desk == dsk_LOAD && (offset == 0x7CU || offset == 0x7DU)) return 2; + if (Desk == dsk_LOAD && offset == 0x52U && + (value == 0U || value > 60000U)) return 3; + /* C28x int is one register word; reads always expose the raw uint16_t. */ + modbus[offset] = (int)value; + if (offset == 127U) ump_log_rs(); + return 0; +} + +unsigned gas167_prepare(unsigned function, uint16_t address, uint16_t value, + int can, uint16_t *reply, unsigned *error) +{ + uint16_t offset; + unsigned i, available; + int info; + if (!reply || !error) return 0; + *error = 0; + info = (uint16_t)(address - GAS167_INFO_BASE) < GAS167_INFO_WORDS; + offset = (uint16_t)(address - (info ? GAS167_INFO_BASE : GAS167_REG_BASE)); + available = info ? GAS167_INFO_WORDS : GAS167_REG_COUNT; + if (function != 3U && function != 6U) *error = 1; + else if (offset >= available) *error = 2; + else if (function == 6U) { + *error = info ? 2U : gas167_write(offset, value); + } else if (!value || value > GAS167_MAX_WORDS || (can && (value & 1U))) { + *error = 3; + } else if (value > available - offset) { + *error = 2; + } else { + /* Validate the entire block before reading. A block cannot cross a + * region boundary, wrap at 0xFFFF, or leak the hidden parameter word. */ + for (i = 0; i < value; ++i) + reply[i] = info ? gas167_info[offset+i] : (uint16_t)modbus[offset+i]; + return value; + } + reply[0] = address; + reply[1] = value; + return 2; +} +#endif diff --git a/Source/Internal/log_to_mem.c b/Source/Internal/log_to_mem.c index ff30687..b491ddd 100644 --- a/Source/Internal/log_to_mem.c +++ b/Source/Internal/log_to_mem.c @@ -31,17 +31,15 @@ void clear_mem() for (i=Log.Start; i= UMP_LOG_CAPACITY/2) ump_count = UMP_LOG_CAPACITY/2-1; - ump_post_dropped = 0; - ump_post = 1000; + inputs = ump_inputs(); outputs = ump_outputs(); + /* Freeze only an actual running-to-stopped output transition. + * STOP is set before RES_OUT_2 is cleared in main; an interrupt can + * observe these writes separately, so require its level, not its edge. + * Repeated reset/STOP pulses while already stopped leave the archive. */ + if (!ump_post && (ump_prev_outputs & 4U) && !(outputs & 4U) && + (outputs & 2U)) { + /* Keep 90% including the trigger; reserve 10% for later records. */ + if (ump_count >= UMP_LOG_CAPACITY - UMP_POST_POINTS) + ump_count = UMP_LOG_CAPACITY - UMP_POST_POINTS - 1; + ump_post = UMP_POST_POINTS; reason |= 0x100; } - /* Ïîñëåäíåå ìåñòî îñòàâëÿåì äëÿ ñíèìêà êîíöà ïîñòèíòåðâàëà. Ïðè - * ïåðåïîëíåíèè ÷åñòíî îòìå÷àåì óñå÷åíèå áèòîì 0x200 â êîíå÷íîé çàïèñè. */ - if (ump_post && ump_count >= UMP_LOG_CAPACITY-1) { - ump_post_dropped = 1; - return; - } - if (reason == 5 && ump_post_dropped) reason |= 0x200; + ump_prev_commands = commands; ump_prev_inputs = inputs; ump_prev_outputs = outputs; + /* Every stored point counts, including CAN and I/O, except the trigger. */ + if (ump_post && !(reason & 0x100) && --ump_post == 0) reason = 5; p = logs_block + ((unsigned long)ump_bank * UMP_LOG_CAPACITY + ump_next) * UMP_LOG_WORDS; p[0] = (unsigned int)ump_time; p[1] = (unsigned int)(ump_time >> 16); p[2] = reason; p[3] = commands; - p[4] = ump_inputs(); p[5] = ump_outputs(); + p[4] = inputs; p[5] = outputs; p[6] = (unsigned int)IMPowse; p[7] = (unsigned int)WAKEpowse; p[8] = modbus[0x7B]; p[9] = modbus[0x1C]; for (i = 0; i < 4; i++) { p[10+i] = sens_data[i]; p[14+i] = modbus[i]; } @@ -136,11 +132,11 @@ static void ump_capture(unsigned int reason) p[33] = ump_dac_go10; p[34] = ump_dac_stop10; p[35] = ump_dac_position; for (i = 0; i < 4; i++) p[36+i] = ADC_table[i]; p[40] = ump_pm67_voltage[0]; p[41] = ump_pm67_voltage[1]; - ump_prev_commands = commands; ump_prev_inputs = p[4]; ump_prev_outputs = p[5]; ump_next++; if (ump_next == UMP_LOG_CAPACITY) ump_next = 0; if (ump_count < UMP_LOG_CAPACITY) ump_count++; ump_sequence++; + if (reason == 5) ump_freeze(); } /* Çàâåðø¸ííûé áàíê áîëüøå íå èçìåíÿåòñÿ äî ñëåäóþùåãî îòêëþ÷åíèÿ. @@ -160,10 +156,6 @@ void ump_log_tick(void) if (ump_state == 1) { ump_time++; if (++ump_divider >= ump_period) { ump_divider = 0; ump_capture(1); } - if (ump_post && --ump_post == 0) { - ump_capture(5); - ump_freeze(); - } } __restore_interrupts(saved); } @@ -224,6 +216,7 @@ int ump_log_write(unsigned int address, unsigned int value) ump_count = 0; ump_next = 0; ump_cursor = 0; ump_divider = 0; ump_post = 0; ump_source = 0; ump_prev_commands = Commands; + ump_prev_inputs = ump_inputs(); ump_prev_outputs = ump_outputs(); no_write = 0; never_write = 0; ump_state = 1; ump_capture(1); @@ -274,7 +267,11 @@ unsigned int ump_log_read(unsigned int address) case UMP_LOG_BASE+11: return (unsigned int)READY_FREQ; case UMP_LOG_BASE+12: return ump_generation; case UMP_LOG_BASE+13: return ump_archive_count; - case UMP_LOG_BASE+14: return ump_post; + /* Preserve the legacy millisecond status field as an upper estimate. + * Additional event records may finish the post interval sooner. */ + case UMP_LOG_BASE+14: + first = (unsigned long)ump_post * ump_period; + return first > 65535UL ? 65535U : (unsigned int)first; case UMP_LOG_BASE+15: return ump_state; } if (address >= UMP_LOG_DATA && address < UMP_LOG_DATA+120 && (ump_source || ump_state != 1)) { diff --git a/Source/Internal/main.c b/Source/Internal/main.c index d51ee0d..e731770 100644 --- a/Source/Internal/main.c +++ b/Source/Internal/main.c @@ -39,8 +39,6 @@ void main() { int i,j,mask; static unsigned int cancount[2], cancell[2], rscount; - static int preCstart=1,preCstop=1; - int umpstart,umpstop; RS_DATA * rs; int presbyt=0; @@ -83,6 +81,7 @@ void main() clear_mem(); ump_log_init(); + Load_control_init(); /* Explicit initialization before IRQ. */ set_cntrl_addr(Mode,16); timer_Init(); EnableInterrupts(); @@ -178,27 +177,8 @@ void main() presbyt = bTermoCal; } - if(Desk==dsk_LOAD) - { - umpstart = cUMPstart; - umpstop = cUMPreset; - - if(umpstart & !preCstart) - { - set_START_DPTB(); set_RES_OUT_2(); - IMPowse = READY_FREQ; - } - preCstart = umpstart; - - if(umpstop & !preCstop) - { - set_STOP_DPTB(); clear_RES_OUT_2(); - IMPowse = READY_FREQ; - } - preCstop = umpstop; - - if(!IMPowse) { clear_START_DPTB(); clear_STOP_DPTB(); } - } + /* Timer1 owns START/STOP/permission and the DAC ramp together. + * Main must not re-enable the output during a protected reload. */ /* Ñíèìîê ïîñëå îáíîâëåíèÿ âõîäîâ è ôîðìèðîâàíèÿ ïóñêîâûõ âûõîäîâ. */ ump_log_io(); diff --git a/Source/Internal/measure.c b/Source/Internal/measure.c index b684925..3dfa276 100644 --- a/Source/Internal/measure.c +++ b/Source/Internal/measure.c @@ -417,7 +417,7 @@ void Init_packMask() if(Desk==dsk_LOAD) { Maska[m_SLOW][7]|= 0x0030; // Êàëèáðîâêà ÖÀÏ Maska[m_SLOW][5]|= 0x0003; // Íà÷àëî è êîíåö çàðàäà - Maska[m_FAST][7]|= 0xE000; // Äóáëèðîâàòü êîìàíäû + Maska[m_FAST][7]|= 0xF000; /* Protection status 0x7C + attempts 0x7D. */ // Äóáëèðîâàòü êîìàíäû Maska[m_FAST][1]|= 0x3000; // Óïðàâëàþùèé òîê ÑÈÔÓ } } diff --git a/Source/Internal/message.c b/Source/Internal/message.c index 8abaef2..86d76f7 100644 --- a/Source/Internal/message.c +++ b/Source/Internal/message.c @@ -1,3 +1,5 @@ +#include "eeprom_config.h" +#include "include/balsam167_eeprom_block.h" #include "DSP2833x_Device.h" // DSP2833x Headerfile Include File #include "package.h" #include "RS485.h" @@ -18,7 +20,13 @@ #include "caliber.h" #include "log_to_mem.h" +#include "DAC.h" #include "ump_tms28335.h" +#include "gas167.h" +#include "include/balsam167_eeprom_layout.h" +#if (ANSWER_LEN + 1) != BALSAM167_EEPROM_PARAM_WORDS +#error EEPROM_parameter_reservation_must_match_ANSWER_LEN +#endif int modbus[ANSWER_LEN+1]; unsigned int param[ANSWER_LEN+1]; @@ -126,6 +134,7 @@ void Default_params() // Ïðèøëî âðåìà! DAC_go = 6; // íà÷àëî çàðàäà + LOAD_CAN_TIMEOUT = 2000; /* ms without fresh ZPT: freeze ramp. */ DAC_stop= 16; // êîíåö çàðàäà } @@ -141,7 +150,7 @@ void Load_params() { unsigned int i,crc; - Seeprom_read(0x3FFF-(ANSWER_LEN+1), param, (ANSWER_LEN+1)*2); + Seeprom_read(BALSAM167_EEPROM_PARAMS_START_WORD, param, (ANSWER_LEN+1)*2); crc = get_crc16(param,ANSWER_LEN); if( (crc==param[ANSWER_LEN]) && (crc !=0xFFFF) && @@ -174,7 +183,7 @@ void Save_params() if(dif) { param[ANSWER_LEN] = get_crc16(param,ANSWER_LEN); - Seeprom_write(0x3FFF-(ANSWER_LEN+1),param,(ANSWER_LEN+1)*2); + Seeprom_write(BALSAM167_EEPROM_PARAMS_START_WORD,param,(ANSWER_LEN+1)*2); } } /***************************************************************/ @@ -206,9 +215,39 @@ static int receive_ump(RS_DATA *rs_arr) address = (rs_arr->RS_Header[2] << 8) | rs_arr->RS_Header[3]; if (address < UMP_BASE) return 0; value = (rs_arr->RS_Header[4] << 8) | rs_arr->RS_Header[5]; + /* SPI driver advances in Timer2 ISR: NEVER mask interrupts here. + * Reuse the existing snapshot buffer; the main loop owns this transaction. + * The shared validator rejects writes, wraparound and out-of-device reads. */ + if (address >= 0x1300U && address < 0x1308U) { + count = Load_protection_prepare(rs_arr->RS_Header[1], address, value, words, &error); + } else if (address >= BALSAM167_EEPROM_RTU_BASE) { + error = 2; +#if EEPROM_M95M02 + /* Exception 3 prevents old AT-profile clients from falling back to + * legacy PEEK after a profile mismatch. No SPI transaction is issued. */ + if (!(value & BALSAM167_EEPROM_M95_MARKER)) error = 3; +#endif + count = 0; + if (balsam167_eeprom_block_valid(rs_arr->RS_Header[1], address, value)) { +#if EEPROM_M95M02 + count = value & 255U; + Seeprom_read(balsam167_eeprom_block_word(address, value), words, count * 2); +#else + count = value; + Seeprom_read(address - BALSAM167_EEPROM_RTU_BASE, words, count * 2); +#endif + error = 0; + } + } else { saved = __disable_interrupts(); +#if EEPROM_M95M02 + if (gas167_contains(address)) + count = gas167_prepare(rs_arr->RS_Header[1], address, value, 0, words, &error); + else +#endif count = ump_tms28335_capture(rs_arr->RS_Header[1], address, value, words, &error); __restore_interrupts(saved); + } length = ump_rtu_encode(CNTRL_ADDR, rs_arr->RS_Header[1], error, words, count, rs_arr->buffer, MAX_SEND_LENGTH); if (length) { @@ -289,6 +328,13 @@ void ReceiveCommandModbus6(RS_DATA *rs_arr) // ïîëó÷èëè ñëîâî äàííûõ Data_MB = (rs_arr->RS_Header[4] << 8) | rs_arr->RS_Header[5]; + if (Desk == dsk_LOAD && (Address_MB == 0x7CU || Address_MB == 0x7DU)) { + modbus_exception(rs_arr, CMD_MODBUS_6, 2); return; + } + if (Desk == dsk_LOAD && Address_MB == 0x52U && + (Data_MB == 0U || Data_MB > 60000U)) { + modbus_exception(rs_arr, CMD_MODBUS_6, 3); return; + } if (Address_MB < ANSWER_LEN) { Modbus[Address_MB].all = Data_MB; if (Address_MB == 127) ump_log_rs(); diff --git a/Source/Internal/shared_ump.c b/Source/Internal/shared_ump.c index 3c01a81..2c67289 100644 --- a/Source/Internal/shared_ump.c +++ b/Source/Internal/shared_ump.c @@ -1,3 +1,7 @@ /* Build the canonical sources directly; do not copy their implementations. */ +#include "../firmware-info/src/firmware_info_static.c" +/* Bind only this PM35 application's identity. The shared transport still owns + * range checks, read-only access, CRC and CAN fragment packing. */ +#define UMP_FIRMWARE_INFO_READ firmware_info_static_read #include "src/ump_protocol.c" #include "ports/tms320f28335/ump_tms28335.c" diff --git a/Source/Internal/spise2p.c b/Source/Internal/spise2p.c index fa07c32..57784fe 100644 --- a/Source/Internal/spise2p.c +++ b/Source/Internal/spise2p.c @@ -19,12 +19,18 @@ int ccc=0; /******* SPI bus Serial EEPROM driver Initialization routine ********/ /********************************************************************/ -void Seeprom_write( unsigned int adres, +void Seeprom_write( SE2P_SIZE adres, unsigned int buf[], - unsigned int size) + SE2P_SIZE size) { unsigned int len; +#if EEPROM_M95M02 + /* Bound physical accesses before byte-to-word rounding. Legacy 32-KiB + * callers retain their compact driver; EXTEND is checked independently. */ + if (adres >= PROM_LEN || !size || size > (PROM_LEN-adres)*2) return; +#endif + CpuTimer2.InterruptCount=0; CpuTimer2Regs.TCR.all = 0x4020; // Use write-only instruction to set TSS bit = 0 @@ -50,12 +56,18 @@ void Seeprom_write( unsigned int adres, CpuTimer2Regs.TCR.all = 0x4010; // Use write-only instruction to set TSS bit = 1 } -void Seeprom_read( unsigned int adres, +void Seeprom_read( SE2P_SIZE adres, unsigned int buf[], - unsigned int size) + SE2P_SIZE size) { unsigned int len; +#if EEPROM_M95M02 + /* Bound physical accesses before byte-to-word rounding. Legacy 32-KiB + * callers retain their compact driver; EXTEND is checked independently. */ + if (adres >= PROM_LEN || !size || size > (PROM_LEN-adres)*2) return; +#endif + CpuTimer2.InterruptCount=0; CpuTimer2Regs.TCR.all = 0x4020; // Use write-only instruction to set TSS bit = 0 @@ -244,6 +256,15 @@ void SPISE2P_DRV_tick(SPISE2P_DRV *eeprom) case 5: /* Send Address */ +#if EEPROM_M95M02 + SpiaRegs.SPICCR.all=SPISE2P_TFR8BIT; + SpiaRegs.SPITXBUF=(unsigned int)(eeprom->msgPtr->se2pAddr >> 16) << 8; + step=20; + break; + case 20: + if (!SpiaRegs.SPISTS.bit.INT_FLAG) break; + dummy=SpiaRegs.SPIRXBUF; +#endif #if(SPISE2P_ADDR_WIDTH==SIXTEEN_BIT) SpiaRegs.SPICCR.all= SPISE2P_TFR16BIT; SpiaRegs.SPITXBUF=eeprom->msgPtr->se2pAddr; @@ -374,7 +395,16 @@ void SPISE2P_DRV_tick(SPISE2P_DRV *eeprom) case 15: /* Send Address */ - #if(SPISE2P_ADDR_WIDTH==SIXTEEN_BIT) + #if EEPROM_M95M02 + SpiaRegs.SPICCR.all=SPISE2P_TFR8BIT; + SpiaRegs.SPITXBUF=(unsigned int)(eeprom->msgPtr->se2pAddr >> 16) << 8; + step=21; + break; + case 21: + if (!SpiaRegs.SPISTS.bit.INT_FLAG) break; + dummy=SpiaRegs.SPIRXBUF; +#endif + #if(SPISE2P_ADDR_WIDTH==SIXTEEN_BIT) SpiaRegs.SPICCR.all= SPISE2P_TFR16BIT; SpiaRegs.SPITXBUF=eeprom->msgPtr->se2pAddr; #endif diff --git a/UKSSTMS320F28335/.cproject b/UKSSTMS320F28335/.cproject index adeca19..162d211 100644 --- a/UKSSTMS320F28335/.cproject +++ b/UKSSTMS320F28335/.cproject @@ -1,5 +1,6 @@ - + + @@ -13,7 +14,7 @@ - + - - + + + + + - + + + + + + - + + + + + + - + + + + + + - + + + + + + - + + + + + + - + + + + + + - + + + + + + - + + + + + + - + + + + + + - + + + + + + @@ -153,7 +212,7 @@ - + @@ -186,6 +247,7 @@ diff --git a/UKSSTMS320F28335/.gitignore b/UKSSTMS320F28335/.gitignore index 6d4a3d8..c744cf6 100644 --- a/UKSSTMS320F28335/.gitignore +++ b/UKSSTMS320F28335/.gitignore @@ -3,3 +3,4 @@ /.launches/ /targetConfigs/ /project.log +/Release/ diff --git a/UKSSTMS320F28335/.project b/UKSSTMS320F28335/.project index a0e67ed..2414266 100644 --- a/UKSSTMS320F28335/.project +++ b/UKSSTMS320F28335/.project @@ -190,6 +190,26 @@ 1 SRC_ROOT1/Source/External/v120/DSP2833x_headers/source/DSP2833x_GlobalVariableDefs.c + + Source/Internal/Include + 2 + SRC_ROOT1/Source/Internal/Include + + + Source/External/v120/DSP2833x_headers/include + 2 + SRC_ROOT1/Source/External/v120/DSP2833x_headers/include + + + Source/External/v120/DSP2833x_common/include + 2 + SRC_ROOT1/Source/External/v120/DSP2833x_common/include + + + Source/Internal/gas167.c + 1 + SRC_ROOT1/Source/Internal/gas167.c + diff --git a/UKSSTMS320F28335/.settings/com.ti.ccstudio.project.core.prefs b/UKSSTMS320F28335/.settings/com.ti.ccstudio.project.core.prefs deleted file mode 100644 index 76e251e..0000000 --- a/UKSSTMS320F28335/.settings/com.ti.ccstudio.project.core.prefs +++ /dev/null @@ -1,4 +0,0 @@ -ccsVersionValidationPolicy=warning -compilerVersionValidationPolicy=flexible -eclipse.preferences.version=1 -productVersionsValidationPolicy=flexible diff --git a/UKSSTMS320F28335/README.md b/UKSSTMS320F28335/README.md index 3720dc9..4dd7c97 100644 --- a/UKSSTMS320F28335/README.md +++ b/UKSSTMS320F28335/README.md @@ -1,5 +1,40 @@ +## Номер выпуска + +Меняйте `FIRMWARE_VERSION_MAJOR`, `FIRMWARE_VERSION_MINOR` и +`FIRMWARE_VERSION_PATCH` в `firmware_version.h` в корне проекта CCS. +Например, `0U`, `1U`, `2U` задают версию 0.1.2. Сохраните файл и +выполните Rebuild. После обновления настроек закройте/откройте проект и +выполните Clean/Rebuild. + +Debug и Release запускают `prepare_identity.py`: он читает номер из заголовка +и передаёт его общему генератору. `firmware_identity.json` содержит только +product/target; номер выпуска в нём редактировать не нужно. +`${BuildDirectory}/generated/firmware_identity.h` и JSON-паспорт создаются +автоматически; вручную их не меняйте. Диапазоны: major/minor 0..255, +patch 0..999. Неверный номер останавливает генерацию. # Результаты сборки +Версия 0.1.1 возвращает паспорт прошивки и блочное чтение EEPROM поверх +подтверждённой на плате базы `c6c9128`. COM_1 остаётся на 115200; исходники +`main.c`, `RS485.c`, `RS485.h` и настройки PLL не менялись относительно этой базы. +Не используйте 250000/256000 для проверки этой сборки. FIFO остаётся включённым, +как в рабочей базе; диагностический вариант без FIFO здесь не используется. +Блочное чтение, проверка размера и host-тесты проверяются программно; +после загрузки сначала проверьте чтение версии 0.1.1, затем 256 байт EEPROM. + +Debug post-build CCS12 также копирует проверенный BIN и сопутствующие +`.manifest.json`/`.sha256` в +`../newProject/SETGUI/..periph_28335/firmware/periph28335/bal`. +Папка создаётся автоматически; имя BIN с Git commit сохраняется. +Копирование выполняет `copy_bin_to_setgui.ps1` после успешной проверки образа +и создания паспорта. Пути определяются относительно репозитория, не cwd CCS. +Для ручного копирования: `./copy_bin_to_setgui.ps1 -Path ./Bin/<имя>.bin`. + +Версия работающей прошивки, SHA-256 BIN и паспорт сборки описаны в +[инструкции для 167](../Doc/FIRMWARE_IDENTITY.md). Перед компиляцией запускается +`preBuildStep.bat`; требуются Python 3.10+ и Git в PATH CCS. Debug post-build +создаёт рядом с BIN файлы `.manifest.json` и `.sha256`. + Для сборки требуется общий UMP-протокол из соседнего репозитория: `../newProject/SETGUI/third_party/templates/c/set-protocol` (от корня этого репозитория). Пути к заголовкам заданы в `.cproject` для Debug и Release; @@ -14,3 +49,7 @@ BIN создаётся скриптом `build_bin.bat`; Git должен быть доступен в `PATH` процесса CCS. Если определить коммит не удалось, сборка завершается ошибкой. При наличии незакоммиченных правок хеш обозначает HEAD и не описывает эти правки. + +## GAS для новой EEPROM + +При EEPROM_M95M02=1 доступен профиль GAS 167/v1 через Modbus и служебный CAN УМП. Карта, проверки и измеренные размеры: [GAS_167_M95M02.md](../Doc/GAS_167_M95M02.md). В сборке AT25256B GAS исключён; её загрузочный BIN не увеличивается. После обновления проекта выполнить Clean/Rebuild: новый gas167.c зарегистрирован в .project. diff --git a/UKSSTMS320F28335/firmware_identity.json b/UKSSTMS320F28335/firmware_identity.json new file mode 100644 index 0000000..f4dd0da --- /dev/null +++ b/UKSSTMS320F28335/firmware_identity.json @@ -0,0 +1,4 @@ +{ + "product": "Balsam-167-PM35", + "target": "TMS320F28335" +} diff --git a/UKSSTMS320F28335/firmware_version.h b/UKSSTMS320F28335/firmware_version.h new file mode 100644 index 0000000..c6a5b44 --- /dev/null +++ b/UKSSTMS320F28335/firmware_version.h @@ -0,0 +1,9 @@ +#ifndef BALSAM_FIRMWARE_VERSION_H +#define BALSAM_FIRMWARE_VERSION_H + +/* Edit these numbers before releasing firmware, then rebuild. */ +#define FIRMWARE_VERSION_MAJOR 1U +#define FIRMWARE_VERSION_MINOR 1U +#define FIRMWARE_VERSION_PATCH 2U + +#endif diff --git a/UKSSTMS320F28335/postBuildStep_Debug.bat b/UKSSTMS320F28335/postBuildStep_Debug.bat deleted file mode 100644 index 2baef7c..0000000 --- a/UKSSTMS320F28335/postBuildStep_Debug.bat +++ /dev/null @@ -1,33 +0,0 @@ -@echo off -setlocal - -for %%I in ("%~dp0..") do set "SRC_ROOT=%%~fI" - -:process_arg -if "%1"=="" goto end_process_arg -set name=%1 -set value= - -:process_arg_value -if NOT "%value%"=="" set value=%value% %2 -if "%value%"=="" set value=%2 -shift -if "%2"=="!" goto set_arg -if "%2"=="" goto set_arg -goto process_arg_value - -:set_arg -set %name%=%value% -shift -shift -goto process_arg -:end_process_arg - -pushd "%SRC_ROOT%\Bin" || exit /b 1 -hex2000.exe UKSSTMS320F28335.out -boot -sci8 -map UKSSTMS320F28335.map -o UKSSTMS320F28335.hex -i -set "BUILD_RESULT=%errorlevel%" -popd -if not "%BUILD_RESULT%"=="0" exit /b %BUILD_RESULT% - -call "%SRC_ROOT%\build_bin.bat" -exit /b %errorlevel% diff --git a/UKSSTMS320F28335/prepare_identity.py b/UKSSTMS320F28335/prepare_identity.py new file mode 100644 index 0000000..cff4b2d --- /dev/null +++ b/UKSSTMS320F28335/prepare_identity.py @@ -0,0 +1,48 @@ +"""Generate build identity using the release version from firmware_version.h.""" +import argparse +import importlib.util +import json +from pathlib import Path +import re +import tempfile + + +def read_version(header): + text = header.read_text(encoding="utf-8-sig") + text = re.sub(r"/\*.*?\*/|//[^\n]*", "", text, flags=re.S) + version = [] + for part, limit in (("MAJOR", 255), ("MINOR", 255), ("PATCH", 999)): + name = "FIRMWARE_VERSION_" + part + definitions = re.findall(r"^\s*#\s*define\s+" + name + r"\b([^\n]*)", text, re.M) + if len(definitions) != 1: + raise ValueError(f"{header}: define {name} exactly once") + value = definitions[0].strip() + if not re.fullmatch(r"(?:0|[1-9][0-9]*)[uU]?", value): + raise ValueError(f"{header}: {name} must be a decimal integer") + number = int(value.rstrip("uU")) + if not 0 <= number <= limit: + raise ValueError(f"{header}: {name} must be in 0..{limit}") + version.append(number) + return version + + +def prepare(project, templates, output): + settings = json.loads((project / "firmware_identity.json").read_text(encoding="utf-8-sig")) + settings["version"] = read_version(project / "firmware_version.h") + spec = importlib.util.spec_from_file_location("shared_firmware_identity", templates / "c/firmware-info/tools/identity.py") + identity = importlib.util.module_from_spec(spec) + spec.loader.exec_module(identity) + # The source header owns SemVer; retain the shared generator and manifest format. + with tempfile.TemporaryDirectory(prefix="balsam-identity-") as directory: + config = Path(directory) / "config.json" + config.write_text(json.dumps(settings), encoding="utf-8") + return identity.prepare(project, templates, config, output) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--project", type=Path, required=True) + parser.add_argument("--templates", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + print(json.dumps(prepare(args.project, args.templates, args.output))) diff --git a/build_bin.bat b/build_bin.bat deleted file mode 100644 index ecbed48..0000000 --- a/build_bin.bat +++ /dev/null @@ -1,20 +0,0 @@ -@echo off -setlocal -set "SRC_ROOT=%~dp0" -set "GIT_COMMIT=" - -for /f "delims=" %%H in ('git -C "%SRC_ROOT%." rev-parse --short^=8 HEAD') do set "GIT_COMMIT=%%H" -if not defined GIT_COMMIT ( - echo ERROR: Cannot determine the Git commit. Make Git available in PATH. 1>&2 - exit /b 1 -) - -pushd "%SRC_ROOT%Bin" || exit /b 1 -hex2000.exe UKSSTMS320F28335.out -boot -sci8 -map UKSSTMS320F28335.map -o UKSSTMS320F28335_%GIT_COMMIT%.bin.pending -b -set "BUILD_RESULT=%errorlevel%" -if not "%BUILD_RESULT%"=="0" goto finish -powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%SRC_ROOT%check_firmware.ps1" -Path "UKSSTMS320F28335_%GIT_COMMIT%.bin.pending" -PublishPath "UKSSTMS320F28335_%GIT_COMMIT%.bin" -set "BUILD_RESULT=%errorlevel%" -:finish -popd -exit /b %BUILD_RESULT% diff --git a/build_logger.ps1 b/build_logger.ps1 deleted file mode 100644 index 112c437..0000000 --- a/build_logger.ps1 +++ /dev/null @@ -1,53 +0,0 @@ -param( - [string]$TemplatesRoot = "$PSScriptRoot/../newProject/SETGUI/third_party/templates", - [string]$CompilerRoot = 'C:/ti/ccs1230/ccs/tools/compiler/ti-cgt-c2000_22.6.0.LTS', - [string]$OutputDirectory = "$PSScriptRoot/Bin/LoggerEepromFix", - [switch]$OptimizeForSize, - [ValidateSet(0,1)][int]$Rs232Fifo = 1, - [string]$ImageName = "UKSSTMS320F28335_logger_eeprom_fix" -) -$ErrorActionPreference = 'Stop' -$protocol = Join-Path (Resolve-Path -LiteralPath $TemplatesRoot).Path 'c/set-protocol' -foreach ($required in @('src/ump_protocol.c', 'ports/tms320f28335/ump_tms28335.c')) { - if (!(Test-Path -LiteralPath (Join-Path $protocol $required))) { - throw "Missing $required in $protocol. Use templates with the UMP v3 port." - } -} -$output = $OutputDirectory -$objects = Join-Path $output 'obj' -New-Item -ItemType Directory -Force -Path $objects | Out-Null -$includes = @('Source/Internal/Include', 'Source/External/v120/DSP2833x_headers/include', - 'Source/External/v120/DSP2833x_common/include') | ForEach-Object { "--include_path=$PSScriptRoot/$_" } -$includes += @("--include_path=$CompilerRoot/include", "--include_path=$protocol", - "--include_path=$protocol/ports/tms320f28335") -# Match the CCS Debug configuration used for 34a95bc5. -$flags = @('-v28', '-ml', '-mt', '--float_support=fpu32', '-O1', '--define=LARGE_MODEL', - '--define=_DEBUG', '--symdebug:dwarf', '--abi=coffabi', '--opt_for_speed=3') -$flags += "--define=RS232_USE_FIFO=$Rs232Fifo" -if ($OptimizeForSize) { - $flags = @($flags | Where-Object { $_ -ne '-O1' -and $_ -ne '--opt_for_speed=3' }) - $flags += @('-O2', '--opt_for_speed=0') -} -$external = @('DSP2833x_ADC_cal.asm', 'DSP2833x_Adc.c', 'DSP2833x_CpuTimers.c', - 'DSP2833x_PieCtrl.c', 'DSP2833x_SWPrioritizedDefaultIsr.c', 'DSP2833x_SWPrioritizedPieVect.c', - 'DSP2833x_SysCtrl.c', 'DSP2833x_Xintf.c', 'DSP2833x_usDelay.asm') -$sources = @(Get-ChildItem -LiteralPath "$PSScriptRoot/Source/Internal" -Filter '*.c' | Sort-Object Name | ForEach-Object FullName) -$sources += $external | ForEach-Object { "$PSScriptRoot/Source/External/v120/DSP2833x_common/source/$_" } -$sources += "$PSScriptRoot/Source/External/v120/DSP2833x_headers/source/DSP2833x_GlobalVariableDefs.c" -$objectFiles = @() -foreach ($source in $sources) { - & "$CompilerRoot/bin/cl2000.exe" @flags @includes --compile_only "--obj_directory=$objects" $source - if ($LASTEXITCODE -ne 0) { throw "Compilation failed: $source" } - $objectFiles += Join-Path $objects ([IO.Path]::GetFileNameWithoutExtension($source) + '.obj') -} -$name = $ImageName -& "$CompilerRoot/bin/cl2000.exe" @flags -z "--map_file=$output/$name.map" --stack_size=0x3f0 --warn_sections ` - "--search_path=$CompilerRoot/lib" --reread_libs --entry_point=_c_int00 --rom_model ` - "--output_file=$output/$name.out" @objectFiles "$PSScriptRoot/F28335.cmd" ` - "$PSScriptRoot/Source/External/v120/DSP2833x_headers/cmd/DSP2833x_Headers_nonBIOS.cmd" ` - "$PSScriptRoot/Libraries/rts2800_fpu32.lib" -if ($LASTEXITCODE -ne 0) { throw 'Link failed' } -& "$CompilerRoot/bin/hex2000.exe" "$output/$name.out" -boot -sci8 -map "$output/${name}_hex.map" -o "$output/$name.bin.pending" -b -if ($LASTEXITCODE -ne 0) { throw 'Binary conversion failed' } -& "$PSScriptRoot/check_firmware.ps1" -Path "$output/$name.bin.pending" -PublishPath "$output/$name.bin" -Write-Output "Firmware: $output/$name.bin" diff --git a/check_firmware.ps1 b/check_firmware.ps1 deleted file mode 100644 index 335c8be..0000000 --- a/check_firmware.ps1 +++ /dev/null @@ -1,36 +0,0 @@ -param( - [Parameter(Mandatory=$true)][string]$Path, - [string]$PublishPath -) -$ErrorActionPreference = 'Stop' -# Load_params/Save_params use word address 0x3FFF-(ANSWER_LEN+1). -# Seeprom_write converts that address to wire bytes (WORD_LEN=2). -$header = Get-Content -Raw -LiteralPath "$PSScriptRoot/Source/Internal/Include/message.h" -if ($header -notmatch '(?m)^#define\s+ANSWER_LEN\s+(0x[0-9a-fA-F]+|[0-9]+)') { - throw 'Cannot determine the EEPROM parameter reservation from ANSWER_LEN' -} -$registerCount = if ($Matches[1].StartsWith('0x')) { - [Convert]::ToInt32($Matches[1].Substring(2), 16) -} else { [int]$Matches[1] } -$limit = (0x3FFF - ($registerCount + 1)) * 2 -$bytes = [IO.File]::ReadAllBytes((Resolve-Path -LiteralPath $Path).Path) -if ($bytes.Length -gt $limit) { - throw "Firmware size $($bytes.Length) exceeds EEPROM firmware area $limit bytes; overlaps saved parameters by $($bytes.Length - $limit) bytes." -} -if ($bytes.Length -lt 30 -or [BitConverter]::ToUInt16($bytes, 0) -ne 0x08AA) { - throw 'Expected a C2000 SCI8 boot image' -} -$offset = 22 -$blocks = 0 -while ($true) { - if ($offset + 2 -gt $bytes.Length) { throw 'Missing boot table terminator' } - $words = [BitConverter]::ToUInt16($bytes, $offset) - $offset += 2 - if (!$words) { break } - $offset += 4 + 2 * $words - if ($offset -gt $bytes.Length) { throw 'Truncated boot table block' } - $blocks++ -} -if (!$blocks -or $offset -ne $bytes.Length) { throw 'Invalid boot table length' } -if ($PublishPath) { Move-Item -LiteralPath $Path -Destination $PublishPath -Force } -Write-Output "Verified SCI8 image: $($bytes.Length) bytes, $blocks blocks, $($limit - $bytes.Length) bytes before saved parameters." diff --git a/tests/eeprom_profiles/run_test.py b/tests/eeprom_profiles/run_test.py new file mode 100644 index 0000000..8759d98 --- /dev/null +++ b/tests/eeprom_profiles/run_test.py @@ -0,0 +1,21 @@ +"""Check SPI wire address widths for both production EEPROM configurations.""" +from pathlib import Path +import subprocess +import os +import shutil +import tempfile + +root = Path(__file__).resolve().parents[2] +protocol = root.parent / 'newProject/SETGUI/third_party/templates/c/set-protocol' +compiler = os.environ.get('CC') or shutil.which('gcc') or 'C:/msys64/mingw64/bin/gcc.exe' +os.environ['PATH'] = str(Path(compiler).resolve().parent) + os.pathsep + os.environ['PATH'] +with tempfile.TemporaryDirectory() as folder: + for profile in (0, 1): + exe = Path(folder) / f'spi{profile}.exe' + subprocess.run([compiler, '-std=c99', + f'-DEEPROM_M95M02={profile}', '-Itests/eeprom_profiles/stubs', + '-ISource/Internal/Include', '-I' + str(protocol), + 'tests/eeprom_profiles/spi_test.c', '-o', str(exe)], + cwd=root, check=True) + subprocess.run([str(exe)], check=True, timeout=10) + print(f'PASS SPI profile {profile}') diff --git a/tests/eeprom_profiles/spi_test.c b/tests/eeprom_profiles/spi_test.c new file mode 100644 index 0000000..c0c876e --- /dev/null +++ b/tests/eeprom_profiles/spi_test.c @@ -0,0 +1,47 @@ +/* Exercise the production Timer2 SPI state machine; no hardware writes. */ +#include +#include "../../Source/Internal/spise2p.c" +static unsigned trace[300], widths[300], used; +static void transfer(unsigned long address, int write) +{ + unsigned buf[2]={0x1234,0x5678}, i; + SE2P_DATA message={buf,2,address}; + used=0; + if(write) spiSe2pWrite(&se2p,&message); else spiSe2pRead(&se2p,&message); + for(i=0;i<100 && !spiSe2pFree(&se2p);i++) { + SpiaRegs.SPITXBUF=0xdead; + SpiaRegs.SPIRXBUF=0; /* WIP clear for write, deterministic read data. */ + SpiaRegs.SPISTS.bit.INT_FLAG=1; + SPISE2P_DRV_tick(&se2p); + if(SpiaRegs.SPITXBUF!=0xdead) { + assert(GpioDataRegs.GPADAT.bit.GPIO19==0); + trace[used]=SpiaRegs.SPITXBUF; + widths[used++]=(SpiaRegs.SPICCR.all&15)+1; + } + } + assert(spiSe2pFree(&se2p) && GpioDataRegs.GPADAT.bit.GPIO19==1); + i=0; + if(write) { assert(trace[i]==0x0600 && widths[i++]==8); } + assert(trace[i]==(write ? 0x0200 : 0x0300) && widths[i++]==8); +#if EEPROM_M95M02 + assert(trace[i]==((address>>16)<<8) && widths[i++]==8); +#endif + assert(trace[i]==(address&65535) && widths[i++]==16); + assert(trace[i]==(write ? 0x1234 : 0) && widths[i++]==16); + assert(trace[i]==(write ? 0x5678 : 0) && widths[i++]==16); + if(write) { assert(trace[i]==0x0500 && widths[i++]==8); assert(trace[i++]==0); } + assert(i==used); +} +int main(void) +{ + SPISE2P_DRV_init(&se2p); + transfer(0x7ffc,0); transfer(0x7ffc,1); +#if EEPROM_M95M02 + assert(BALSAM167_EEPROM_FIRMWARE_BYTES==261884UL); + transfer(0xfffe,0); transfer(0x10000,0); + transfer(0x20000,1); transfer(0x3fffc,0); transfer(0x3fffc,1); +#else + assert(BALSAM167_EEPROM_FIRMWARE_BYTES==32508); +#endif + return 0; +} diff --git a/tests/eeprom_profiles/stubs/DSP2833x_Device.h b/tests/eeprom_profiles/stubs/DSP2833x_Device.h new file mode 100644 index 0000000..f0d64db --- /dev/null +++ b/tests/eeprom_profiles/stubs/DSP2833x_Device.h @@ -0,0 +1,22 @@ +#ifndef EEPROM_TEST_DEVICE_H +#define EEPROM_TEST_DEVICE_H +#include +#define interrupt +#define EALLOW +#define EDIS +#define CLKMULT 2 +#define SYSCLKOUT 60000000UL +#define M_INT14 0x2000 +static unsigned IER; +struct bits { unsigned GPIO16,GPIO17,GPIO18,GPIO19,SPISWRESET,BUFFULL_FLAG,INT_FLAG; }; +struct reg { unsigned all; struct bits bit; }; +static struct { struct reg GPAMUX2,GPADIR; } GpioCtrlRegs; +static struct { struct reg GPADAT; } GpioDataRegs; +static struct { void (*TINT2)(void); } PieVectTable; +static struct { unsigned InterruptCount; } CpuTimer2; +static struct { struct reg TCR; } CpuTimer2Regs; +static struct { struct reg SPICCR,SPICTL,SPISTS,SPIFFTX,SPIFFRX,SPIFFCT,SPIPRI; + unsigned SPIBRR; uint16_t SPITXBUF,SPIRXBUF; } SpiaRegs; +static void InitCpuTimers(void) {} +static void ConfigCpuTimer(void *p, unsigned long f, unsigned t) {(void)p;(void)f;(void)t;} +#endif diff --git a/tests/firmware_identity/check_build.py b/tests/firmware_identity/check_build.py new file mode 100644 index 0000000..ac160c7 --- /dev/null +++ b/tests/firmware_identity/check_build.py @@ -0,0 +1,42 @@ +"""Independent test oracle: does this built SCI8 file contain its manifest identity? + +This is a build test, not a second production protocol codec. The 12-word vector +is deliberately constructed independently of the C implementation to detect a +stale compilation unit, incorrect byte order or a mismatched pre-build JSON. +Usage: python tests/firmware_identity/check_build.py path/to/firmware.bin +""" +from datetime import datetime +import hashlib +import json +from pathlib import Path +import struct +import sys + +image = Path(sys.argv[1]) +data = image.read_bytes() +meta = json.loads(image.with_suffix(image.suffix + '.manifest.json').read_text()) +assert hashlib.sha256(data).hexdigest() == meta['sha256'], 'Wrong file hash' +assert len(data) == meta['size_bytes'], 'Wrong image size' +stamp = datetime.fromisoformat(meta['built_at_utc']) +build_id = meta['build_id'] +words = [1, *meta['version'], stamp.year, stamp.month * 256 + stamp.day, + stamp.hour * 256 + stamp.minute, stamp.second] +words += [ord(build_id[i]) * 256 + ord(build_id[i+1]) for i in range(0, 8, 2)] +expected = struct.pack('<12H', *words) +assert data[:2] == b'\xaa\x08', 'Not SCI8' +offset = 22 +found = 0 +while True: + count, = struct.unpack_from(' +static unsigned logger_reads, logger_writes; +unsigned int ump_log_read(unsigned int address) +{ (void)address; ++logger_reads; return 0x554dU; } +int ump_log_write(unsigned int address, unsigned int value) +{ (void)address; (void)value; ++logger_writes; return 1; } + +int main(void) +{ + uint16_t words[UMP_MAX_WORDS]; + ump_octet wire[UMP_RTU_CAPACITY]; + unsigned error, i; + assert(ump_tms28335_prepare(3, UMP_FIRMWARE_INFO, 12, words, &error) == 12 && !error); + assert(words[0] == 1 && words[1] == FIRMWARE_VERSION_MAJOR); + assert(words[2] == FIRMWARE_VERSION_MINOR && words[3] == FIRMWARE_VERSION_PATCH); + assert(words[4] == FI_YEAR && words[5] == FI_PAIR(FI_MONTH, FI_DAY)); + assert(words[8] == FI_PAIR(FI_ID_0, FI_ID_1)); + assert(logger_reads == 0); + assert(ump_tms28335_rtu(1,3,UMP_FIRMWARE_INFO,12,wire,UMP_RTU_CAPACITY) == 29); + for (i=0; i<12; ++i) assert(((wire[3+i*2]<<8) | wire[4+i*2]) == words[i]); + assert(ump_crc(wire,29) == 0); + ump_tms28335_prepare(6,UMP_FIRMWARE_INFO,123,words,&error); + assert(error == 2 && logger_writes == 0); + /* Firmware reads must not steal the existing logger window. */ + assert(ump_tms28335_prepare(3,UMP_BASE,16,words,&error) == 16 && !error); + assert(logger_reads == 16 && words[0] == 0x554dU); + return 0; +} diff --git a/tests/gas167/check_image.py b/tests/gas167/check_image.py new file mode 100644 index 0000000..fb03cb2 --- /dev/null +++ b/tests/gas167/check_image.py @@ -0,0 +1,48 @@ +"""Validate the PM35 RAM-load SCI8 image against the selected EEPROM profile.""" +import argparse +from pathlib import Path +import struct + + +def check_image(path, eeprom): + data = path.read_bytes() + limit = {"AT25256B": 32508, "M95M02": 261884}[eeprom] + if not 24 <= len(data) <= limit: + raise ValueError(f"Image size {len(data)} outside 24..{limit}") + if struct.unpack_from(" len(data): + raise ValueError("Missing boot terminator") + words = struct.unpack_from(" len(data): + raise ValueError("Truncated boot block") + high, low = struct.unpack_from(" +#include +#include +#include +#include +#include "gas167.h" +#include "ump_protocol.h" +#include "ump_tms28335.h" +#include "include/balsam167_eeprom_block.h" + +#define ANSWER_LEN 128U +#define MAX_SEND_LENGTH 400U +#define CNTRL_ADDR 16U +#define CMD_MODBUS_3 3U +#define CMD_MODBUS_6 6U +#define dsk_LOAD 6 +#define LOBYTE(v) ((v) & 255U) +#define HIBYTE(v) (((v) >> 8) & 255U) +#define UMP_CAN_MAX_WORDS UMP_MAX_WORDS +#define ump_can_prepare ump_tms28335_prepare +typedef struct { int all; } WORDE; +#define Modbus ((WORDE *)modbus) +typedef struct { + uint16_t RS_Header[8], buffer[MAX_SEND_LENGTH]; + unsigned flag_TIMEOUT_to_Send; +} RS_DATA; + +int modbus[129], Desk = dsk_LOAD; +static unsigned captures, spi_reads, sent, irq_depth, legacy_reads; +static unsigned __disable_interrupts(void) { + unsigned saved = irq_depth; + ++irq_depth; + return saved; +} +static void __restore_interrupts(unsigned saved) { irq_depth = saved; } +void ump_log_rs(void) { ++captures; } +unsigned int ump_log_read(unsigned int address) { ++legacy_reads; return address; } +int ump_log_write(unsigned int address, unsigned int value) { return 1; } +static unsigned get_crc_16(unsigned seed, const uint16_t *data, unsigned size) { + assert(seed == 0xFFFFU); + return ump_crc(data, size); +} +static void RS_Send(RS_DATA *rs, uint16_t *data, unsigned size) { + assert(irq_depth == 0); + sent = size; +} +static void Seeprom_read(unsigned long word, uint16_t *data, unsigned bytes) { + unsigned i; + assert(irq_depth == 0); /* The Timer2 SPI driver must retain interrupts. */ + ++spi_reads; + for (i = 0; i < bytes/2; ++i) data[i] = (uint16_t)(0xBEEFU+i); +} +static unsigned Load_protection_prepare(unsigned fn, unsigned a, unsigned n, + uint16_t *out, unsigned *error) { + out[0] = (uint16_t)a; out[1] = (uint16_t)n; *error = 0; + return 2; +} +struct mock_reg { uint32_t all; }; +static struct { + struct mock_reg CANTA, CANAA, CANTRR, CANTRS; +} ECanaRegs; +static struct { + struct { struct mock_reg MDH, MDL; } MBOX2; +} ECanaMboxes; +static struct { unsigned long InterruptCount; } CpuTimer1; +#include "production_routes.inc" + +static void request(RS_DATA *rs, unsigned fn, unsigned a, unsigned n) { + unsigned crc; + memset(rs, 0, sizeof(*rs)); + rs->RS_Header[0] = CNTRL_ADDR; rs->RS_Header[1] = fn; + rs->RS_Header[2] = a >> 8; rs->RS_Header[3] = a & 255U; + rs->RS_Header[4] = n >> 8; rs->RS_Header[5] = n & 255U; + crc = ump_crc(rs->RS_Header, 6); + rs->RS_Header[6] = crc & 255U; rs->RS_Header[7] = crc >> 8; + sent = 0; + if (fn == 3) ReceiveCommandModbus3(rs); + else ReceiveCommandModbus6(rs); + assert(irq_depth == 0 && rs->flag_TIMEOUT_to_Send); + assert(ump_crc(rs->buffer, rs->buffer[1] & 128U ? 5 : + (fn == 6 ? 8 : 5+rs->buffer[2])) == 0); +} +static void can_request(unsigned fn, unsigned a, unsigned n) { + memset(&ECanaRegs, 0, sizeof(ECanaRegs)); + memset(&ECanaMboxes, 0, sizeof(ECanaMboxes)); + ump_can_waiting = 0; ump_can_length = 0; ump_can_index = 0; + ump_can_pending = 1; ump_can_tag = 0x0100U | fn; + ump_can_address = a; ump_can_value = n; + UMP_CAN_service(); + assert(irq_depth == 0 && !ump_can_pending); + assert(ECanaRegs.CANTRS.all == 4U); +} +int main(void) { + RS_DATA rs; + unsigned i; + for (i = 0; i < 129; ++i) modbus[i] = (int)i; + modbus[128] = 0xD00D; + /* Exercise the real native handlers in both builds. */ + request(&rs, 6, 1, 0xFEDC); + assert(modbus[1] == 0xFEDC && sent == 10); + request(&rs, 3, 0, 2); + assert(rs.buffer[2] == 4 && rs.buffer[5] == 0xFE && rs.buffer[6] == 0xDC); + request(&rs, 6, 0x52, 0); + assert(rs.buffer[1] == 0x86 && rs.buffer[2] == 3 && modbus[0x52] == 0x52); + request(&rs, 6, 0x7C, 9); + assert(rs.buffer[2] == 2 && modbus[0x7C] == 0x7C); + /* Existing UMP and protection routing must still work. */ + request(&rs, 3, 0x1000, 2); + assert(rs.buffer[1] == 3 && rs.buffer[3] == 0x10 && legacy_reads == 2); + request(&rs, 3, 0x1300, 2); + assert(rs.buffer[1] == 3 && rs.buffer[3] == 0x13); +#if EEPROM_M95M02 + { + uint16_t out[124]; + unsigned error, count; + for (i = 0; i < 65536U; ++i) + assert(gas167_contains((uint16_t)i) == + ((i >= 0x5000U && i <= 0x507FU) || (i >= 0x5F00U && i <= 0x5F07U))); + assert(gas167_prepare(3, 0x5000, 124, 0, out, &error) == 124 && !error); + assert(out[1] == 0xFEDC && out[123] == 123); + count = gas167_prepare(3, 0x507F, 2, 0, out, &error); + assert(count == 2 && error == 2 && modbus[128] == 0xD00D); + assert(gas167_prepare(3, 0x5000, 0, 0, out, &error) == 2 && error == 3); + assert(gas167_prepare(3, 0x5000, 125, 0, out, &error) == 2 && error == 3); + assert(gas167_prepare(3, 0x5000, 1, 1, out, &error) == 2 && error == 3); + assert(gas167_prepare(3, 0xFFFF, 2, 0, out, &error) == 2 && error == 2); + assert(gas167_prepare(16, 0x5000, 1, 0, out, &error) == 2 && error == 1); + assert(gas167_prepare(6, 0x5080, 7, 0, out, &error) == 2 && error == 2); + assert(gas167_prepare(6, 0x5052, 60001, 0, out, &error) == 2 && error == 3); + assert(gas167_prepare(6, 0x5052, 60000, 0, out, &error) == 2 && !error); + assert(modbus[0x52] == 60000); + assert(gas167_prepare(3, 0x5000, 1, 0, 0, &error) == 0); + assert(gas167_prepare(6, 0x5000, 1, 0, out, 0) == 0 && modbus[0] == 0); + } + request(&rs, 3, 0x5000, 2); + assert(sent == 9 && rs.buffer[5] == 0xFE && rs.buffer[6] == 0xDC); + request(&rs, 3, 0x5000, 124); + assert(sent == 253 && rs.buffer[2] == 248); + request(&rs, 3, 0x5F00, 8); + assert(sent == 21 && rs.buffer[3] == 0x47 && rs.buffer[4] == 0x41); + assert(rs.buffer[5] == 0x53 && rs.buffer[6] == 0x31); + assert(rs.buffer[10] == 167 && rs.buffer[18] == 1); + request(&rs, 6, 0x5F00, 0); + assert(sent == 5 && rs.buffer[2] == 2); + request(&rs, 3, 0x5F07, 2); + assert(rs.buffer[2] == 2); + request(&rs, 6, 0x5052, 0); + assert(rs.buffer[2] == 3 && modbus[0x52] == 60000); + request(&rs, 6, 0x507D, 9); + assert(rs.buffer[2] == 2 && modbus[0x7D] == 0x7D); + request(&rs, 6, 0x507F, 0x80); + assert(sent == 8 && rs.buffer[2] == 0x50 && rs.buffer[3] == 0x7F); + assert(captures == 1 && modbus[127] == 0x80); + can_request(6, 0x5001, 0xFEDC); + assert(modbus[1] == 0xFEDC && ECanaMboxes.MBOX2.MDL.all == 0x5001FEDCUL); + assert(ECanaMboxes.MBOX2.MDH.all == 0x1F000100UL); + can_request(3, 0x5001, 2); + assert(ECanaMboxes.MBOX2.MDL.all == 0xFEDC0002UL); + can_request(3, 0x5000, 124); + assert(ump_can_length == 124 && ump_can_index == 2); + modbus[2] = 777; /* All fragments must retain the original snapshot. */ + for (i = 1; i < 62; ++i) { + ECanaRegs.CANTA.all = 4; + UMP_CAN_service(); + assert(ump_can_index == 2*(i+1)); + if (i == 1) assert(ECanaMboxes.MBOX2.MDL.all == 0x00020003UL); + } + modbus[2] = 2; + can_request(3, 0x5000, 3); + assert(ECanaMboxes.MBOX2.MDH.all == 0x1F000103UL); + can_request(6, 0x507C, 9); + assert((ECanaMboxes.MBOX2.MDH.all & 255U) == 2 && modbus[0x7C] == 0x7C); + can_request(6, 0x507F, 0x40); + assert(captures == 2 && modbus[127] == 0x40); + can_request(6, 0x5F00, 9); + assert((ECanaMboxes.MBOX2.MDH.all & 255U) == 2); + request(&rs, 3, 0x8000, 0x8001); +#else + request(&rs, 3, 0x5000, 2); + assert(sent == 5 && rs.buffer[2] == 2); + request(&rs, 6, 0x5001, 9); + assert(rs.buffer[2] == 2 && modbus[1] == 0xFEDC); + request(&rs, 3, 0x5000, 124); + assert(sent == 5 && rs.buffer[2] == 2); + request(&rs, 3, 0x5F00, 8); + assert(rs.buffer[2] == 2); + can_request(6, 0x5001, 9); + assert((ECanaMboxes.MBOX2.MDH.all & 255U) == 2 && modbus[1] == 0xFEDC); + assert(captures == 0); + request(&rs, 3, 0x8000, 1); +#endif + assert(spi_reads == 1 && rs.buffer[3] == 0xBE && rs.buffer[4] == 0xEF); + assert(modbus[128] == 0xD00D); + printf("PASS GAS167 profile %d: production RTU/CAN routes, guards, CRC and legacy services\n", EEPROM_M95M02); + return 0; +} diff --git a/tests/gas167/run_test.py b/tests/gas167/run_test.py new file mode 100644 index 0000000..9b569a9 --- /dev/null +++ b/tests/gas167/run_test.py @@ -0,0 +1,51 @@ +"""Exercise the production GAS provider and extracted production RTU/CAN routes.""" +import os +from pathlib import Path +import re +import shutil +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +templates = Path(os.environ.get("TEMPLATES_ROOT", root.parent / "newProject/SETGUI/third_party/templates")) +protocol = templates / "c/set-protocol" +compiler = os.environ.get("CC") or shutil.which("gcc") or "C:/msys64/mingw64/bin/gcc.exe" +os.environ["PATH"] = str(Path(compiler).resolve().parent) + os.pathsep + os.environ["PATH"] + + +def function(text, name): + match = re.search(r"^(?:static )?(?:void|int) " + name + r"\(", text, re.M) + if not match: + raise ValueError(f"Production function {name} missing") + end = re.search(r"^}", text[match.start():], re.M) + if not end: + raise ValueError(f"Production function {name} incomplete") + return text[match.start():match.start() + end.end()] + "\n" + + +message = (root / "Source/Internal/message.c").read_text(encoding="cp1251") +ecan = (root / "Source/Internal/ecan.c").read_text(encoding="cp1251") +can_globals = ecan[ecan.index("static volatile unsigned int ump_can_pending;"): + ecan.index("static unsigned long ump_can_sent_at;") + len("static unsigned long ump_can_sent_at;")] +# On C28x unsigned int is uint16_t. Preserve that target word layout on GCC. +can_globals = can_globals.replace("static unsigned int ump_can_reply[", + "static uint16_t ump_can_reply[") +routes = can_globals + "\n" + "".join(function(message, name) for name in + ("modbus_exception", "receive_ump", "ReceiveCommandModbus3", "ReceiveCommandModbus6")) +routes += function(ecan, "UMP_CAN_service") +with tempfile.TemporaryDirectory(prefix="gas167-test-") as tmp: + directory = Path(tmp) + (directory / "production_routes.inc").write_text(routes, encoding="utf-8") + for profile in (0, 1): + exe = directory / f"gas167_{profile}.exe" + subprocess.run([compiler, "-std=c99", "-Wall", "-Wextra", + "-Wno-unused-function", "-Wno-unused-parameter", + f"-DEEPROM_M95M02={profile}", "-I" + str(directory), + "-ISource/Internal/Include", "-I" + str(protocol), + "-I" + str(protocol / "include"), + "-I" + str(protocol / "ports/tms320f28335"), + "tests/gas167/gas167_test.c", "Source/Internal/gas167.c", + str(protocol / "src/ump_protocol.c"), + str(protocol / "ports/tms320f28335/ump_tms28335.c"), + "-o", str(exe)], cwd=root, check=True) + subprocess.run([str(exe)], check=True, timeout=10) diff --git a/tests/load_control/load_control_test.c b/tests/load_control/load_control_test.c new file mode 100644 index 0000000..222d0a2 --- /dev/null +++ b/tests/load_control/load_control_test.c @@ -0,0 +1,103 @@ +#include +#include +#include "../../Source/Internal/DAC.c" +int READY = 1, Desk = 6, Mode = 5; +int modbus[128]; +unsigned int test_start, test_stop, test_permission, test_tick; +unsigned int test_starts, test_last_stop_tick, test_last_start_tick, test_init_dac; +int test_bad_order; +unsigned int ump_dac_ramp, ump_dac_duration, ump_dac_go10, ump_dac_stop10, ump_dac_position; +#define CHECK(x) do { if (!(x)) { printf("FAIL line %d: %s\n", __LINE__, #x); return 1; } } while (0) +static void setup(int mode) { + memset(modbus, 0, sizeof(modbus)); Mode = mode; READY = 1; + test_start = test_stop = test_permission = test_tick = test_starts = 0; + test_bad_order = test_init_dac = 0; + Load_control_init(); Setup_DAC_time(); WAKEpowse = 0; + DAC_go = 4; DAC_stop = 16; DAC_min() = 100; DAC_max() = 3000; +} +static void tick(unsigned int n, int exchange, unsigned int volts) { + while (n--) { + if (exchange) Load_voltage_can(0x8000U | (Mode == 5 ? 305U : 8U), volts, 0, 0); + test_tick++; Load_runner(); + } +} +int main(void) { + unsigned int out[8], error, count, ramp, code, stopped_at; + setup(5); + /* Boot ISR before parameters: no divide-by-zero and no GPIO start. */ + Load_control_init(); Commands = 0x80; tick(10, 1, 0); CHECK(!test_starts); + Setup_DAC_time(); WAKEpowse = 0; + tick(10, 0, 0); CHECK(test_starts == 1 && test_permission); + CHECK(LOAD_CAN_TIMEOUT == 600); + /* Wrong ZPT and masked slots never confirm freshness. */ + setup(5); Commands = 0x80; + Load_voltage_can(0x8000U | 8U, 200, 0, 0); + Load_voltage_can(305U, 200, 0, 0); tick(10, 0, 0); + CHECK(!test_starts && (LOAD_STATUS & 2)); + Load_voltage_can(0x2000U | 303U, 0, 0, 30); + tick(1, 0, 0); CHECK(test_starts == 1); + /* Freeze current and code at timeout; unrelated exchange cannot unfreeze. */ + tick(1000, 1, 30); tick(600, 0, 0); + ramp = ump_dac_ramp; code = modbus[123]; + tick(2000, 0, 0); CHECK(ump_dac_ramp == ramp && (unsigned int)modbus[123] == code); + CHECK(test_permission && !test_stop && (LOAD_STATUS & 2)); + Load_voltage_can(0x8000U | 8U, 200, 0, 0); tick(10, 0, 0); + CHECK(ump_dac_ramp == ramp); + tick(10, 1, 30); CHECK(ump_dac_ramp > ramp); + /* Exactly 30 V permits growth; 29 V fails strictly above 6 mA. */ + setup(6); Commands = 0x80; tick(10, 1, 30); + load_count_ramp = time_dac / 6; tick(5, 1, 30); + CHECK(load_state == LOAD_RUN && modbus[0x1C] >= 60); + tick(5, 1, 29); CHECK(load_state == LOAD_RELOAD && test_stop && !test_permission); + CHECK(!test_start && ump_dac_ramp == 0 && modbus[0x1C] == 40); + CHECK(modbus[123] == DAC_min()); stopped_at = test_last_stop_tick; + tick(999, 1, 0); CHECK(test_starts == 1 && test_stop); + tick(1, 1, 0); CHECK(test_starts == 2 && test_last_start_tick-stopped_at == 1000); + CHECK(!test_stop && test_permission && !test_bad_order); + /* Five total attempts, then a persistent fault; no sixth start. */ + while (load_state != LOAD_FAULT) { + load_count_ramp = time_dac / 6; tick(5, 1, 0); + if (load_state == LOAD_RELOAD) tick(1000, 1, 0); + } + CHECK(test_starts == 5 && LOAD_ATTEMPTS == 5 && (LOAD_STATUS & 1)); + tick(3000, 1, 200); CHECK(test_starts == 5 && test_stop && !test_permission); + Commands = 0xC0; tick(10, 1, 200); CHECK(load_state == LOAD_FAULT); + Commands = 0; tick(1000, 1, 200); CHECK(!(LOAD_STATUS & 1)); + Commands = 0x80; test_starts = 0; tick(10, 1, 200); CHECK(LOAD_ATTEMPTS == 1); + /* Manual reset dominates START; releasing it alone cannot start again. */ + Commands = 0xC0; tick(1, 1, 200); CHECK(test_stop && !test_permission); + Commands = 0x80; tick(1500, 1, 200); CHECK(test_starts == 1); + /* Versioned simulation writes are bounded, read-only fields reject writes. */ + count = Load_protection_prepare(3, 0x1300, 8, out, &error); + CHECK(count == 8 && !error && out[0] == 0x5550 && out[1] == 1); + Load_protection_prepare(6, 0x1303, 0, out, &error); CHECK(error == 3); + Load_protection_prepare(6, 0x1304, 0, out, &error); CHECK(error == 2); + Load_protection_prepare(6, 0x1302, 4, out, &error); CHECK(error == 3); + setup(5); Commands = 0x80; tick(10, 1, 200); + Load_protection_prepare(6, 0x1303, 20, out, &error); CHECK(!error); + Load_protection_prepare(6, 0x1302, 1, out, &error); CHECK(!error); + tick(30, 1, 200); CHECK(LOAD_STATUS & 2); + ramp = ump_dac_ramp; tick(100, 1, 200); CHECK(ump_dac_ramp == ramp); + Load_protection_prepare(6, 0x1302, 0, out, &error); + tick(10, 1, 200); CHECK(!(LOAD_STATUS & 2) && ump_dac_ramp > ramp); + Load_protection_prepare(6, 0x1302, 2, out, &error); + load_count_ramp = time_dac / 6; tick(5, 1, 200); CHECK(load_state == LOAD_RELOAD); + /* Calibration must not replace the forced 4 mA output during reload. */ + Commands |= 0x200; DAC_cal() = 4095; tick(5, 1, 200); + CHECK(modbus[123] == DAC_min() && modbus[0x1C] == 40); + /* A calibration code preceding START must be replaced by 4 mA first. */ + setup(5); Commands = 0x200; DAC_cal() = 4095; + tick(10, 1, 200); CHECK(modbus[123] == 4095 && !load_zero_sent); + Commands = 0x280; tick(1, 1, 200); CHECK(!test_starts); + tick(4, 1, 200); CHECK(!test_starts && modbus[123] == DAC_min()); + tick(1, 1, 200); CHECK(test_starts == 1); + /* Timeout while STOP is held cannot initiate the pending retry. */ + setup(5); Commands = 0x80; tick(10, 1, 0); + load_count_ramp = time_dac / 6; tick(5, 1, 0); + CHECK(load_state == LOAD_RELOAD); + tick(1500, 0, 0); CHECK(test_starts == 1 && test_stop); + tick(1, 1, 0); CHECK(test_starts == 2 && !test_stop); + Load_control_init(); CHECK(!load_simulation); + puts("PASS: production DAC control, timeout, CAN slots, STOP ordering, five attempts, simulation"); + return 0; +} diff --git a/tests/load_control/run_test.py b/tests/load_control/run_test.py new file mode 100644 index 0000000..41fa105 --- /dev/null +++ b/tests/load_control/run_test.py @@ -0,0 +1,16 @@ +"""Compile the production DAC.c against GPIO/IRQ mocks; no hardware writes.""" +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[2] +compiler = os.environ.get('CC') or shutil.which('gcc') or 'C:/msys64/mingw64/bin/gcc.exe' +os.environ['PATH'] = str(Path(compiler).resolve().parent) + os.pathsep + os.environ['PATH'] +with tempfile.TemporaryDirectory(prefix='load-control-') as tmp: + exe = Path(tmp) / 'test.exe' + subprocess.run([compiler, '-Wall', '-Wextra', '-std=c99', + '-Itests/load_control/stubs', '-ISource/Internal/Include', + 'tests/load_control/load_control_test.c', '-o', str(exe)], cwd=root, check=True) + subprocess.run([str(exe)], check=True, timeout=10) diff --git a/tests/load_control/stubs/DSP2833x_Device.h b/tests/load_control/stubs/DSP2833x_Device.h new file mode 100644 index 0000000..2807eba --- /dev/null +++ b/tests/load_control/stubs/DSP2833x_Device.h @@ -0,0 +1,9 @@ +#ifndef TEST_DEVICE_H +#define TEST_DEVICE_H +struct bits { unsigned int GPIO59, GPIO60, GPIO63; }; +struct reg { struct bits bit; }; +static struct { struct reg GPBSET, GPBCLEAR; } GpioDataRegs; +static unsigned int __disable_interrupts(void) { return 0; } +static void __restore_interrupts(unsigned int saved) { (void)saved; } +static void DSP28x_usDelay(unsigned long n) { (void)n; } +#endif diff --git a/tests/load_control/stubs/DSP2833x_SWPrioritizedIsrLevels.h b/tests/load_control/stubs/DSP2833x_SWPrioritizedIsrLevels.h new file mode 100644 index 0000000..1fd1a88 --- /dev/null +++ b/tests/load_control/stubs/DSP2833x_SWPrioritizedIsrLevels.h @@ -0,0 +1 @@ +/* No IRQ registers needed in the host test. */ diff --git a/tests/load_control/stubs/RS485.h b/tests/load_control/stubs/RS485.h new file mode 100644 index 0000000..570fb8d --- /dev/null +++ b/tests/load_control/stubs/RS485.h @@ -0,0 +1 @@ +/* No serial dependencies in the host test. */ diff --git a/tests/load_control/stubs/filter_bat2.h b/tests/load_control/stubs/filter_bat2.h new file mode 100644 index 0000000..7cbd46b --- /dev/null +++ b/tests/load_control/stubs/filter_bat2.h @@ -0,0 +1 @@ +/* No filter dependencies in the host test. */ diff --git a/tests/load_control/stubs/measure.h b/tests/load_control/stubs/measure.h new file mode 100644 index 0000000..f76a6e2 --- /dev/null +++ b/tests/load_control/stubs/measure.h @@ -0,0 +1,4 @@ +#define READY_FREQ 1000 +#define DAC_FREQ 200 +#define LOAD_TIME 30 +extern int READY; diff --git a/tests/load_control/stubs/message.h b/tests/load_control/stubs/message.h new file mode 100644 index 0000000..8a4d817 --- /dev/null +++ b/tests/load_control/stubs/message.h @@ -0,0 +1 @@ +extern int modbus[128]; diff --git a/tests/load_control/stubs/package.h b/tests/load_control/stubs/package.h new file mode 100644 index 0000000..f35b080 --- /dev/null +++ b/tests/load_control/stubs/package.h @@ -0,0 +1,17 @@ +#define dsk_LOAD 6 +#define adr_LOA1 5 +#define adr_LOA2 6 +#define Commands modbus[127] +#define cUMPstart (Commands & 0x80) +#define cUMPreset (Commands & 0x40) +extern unsigned int test_init_dac; +#define cInitDac test_init_dac +#define cCalibrDac (Commands & 0x200) +#define DAC_go modbus[0x50] +#define DAC_stop modbus[0x51] +#define LOAD_CAN_TIMEOUT modbus[0x52] +#define LOAD_STATUS modbus[0x7C] +#define LOAD_ATTEMPTS modbus[0x7D] +#define DAC_min() modbus[0x79] +#define DAC_max() modbus[0x78] +#define DAC_cal() modbus[0x7A] diff --git a/tests/load_control/stubs/peripher.h b/tests/load_control/stubs/peripher.h new file mode 100644 index 0000000..d9d0e3f --- /dev/null +++ b/tests/load_control/stubs/peripher.h @@ -0,0 +1,14 @@ +extern int Desk, Mode; +extern unsigned int test_start, test_stop, test_permission, test_tick; +extern unsigned int test_starts, test_last_stop_tick, test_last_start_tick; +extern int test_bad_order; +static void clear_START_DPTB(void) { test_start = 0; } +static void set_START_DPTB(void) { + if (test_stop || (test_starts && test_tick-test_last_stop_tick < 1000)) test_bad_order = 1; + test_start = 1; test_last_start_tick = test_tick; test_starts++; +} +static void set_STOP_DPTB(void) { test_stop = 1; test_last_stop_tick = test_tick; } +static void clear_STOP_DPTB(void) { test_stop = 0; } +static void clear_RES_OUT_2(void) { test_permission = 0; } +static void set_RES_OUT_2(void) { if (!test_start || test_stop) test_bad_order = 1; test_permission = 1; } +static void toggle_RES_OUT_1(void) {} diff --git a/tests/load_control/stubs/tools.h b/tests/load_control/stubs/tools.h new file mode 100644 index 0000000..0309add --- /dev/null +++ b/tests/load_control/stubs/tools.h @@ -0,0 +1 @@ +/* Delay provided by the device stub. */ diff --git a/tests/ump_logger/init_test.c b/tests/ump_logger/init_test.c index b3a604b..21d47c9 100644 --- a/tests/ump_logger/init_test.c +++ b/tests/ump_logger/init_test.c @@ -16,7 +16,7 @@ int main(void) ump_time = ump_sequence = 0xA5A5A5A5UL; ump_bank = ump_archive_bank = 0xA5A5; ump_archive_count = ump_archive_next = ump_generation = 0xA5A5; - ump_source = ump_post = ump_post_dropped = 0xA5A5; + ump_source = ump_post = 0xA5A5; ump_can_header = ump_can_count = 0xA5A5; for (i = 0; i < 3; i++) ump_can_data[i] = 0xA5A5; for (i = 0; i < 2; i++) ump_pm67_voltage[i] = 0xA5A5; @@ -34,7 +34,7 @@ int main(void) for (i = 12; i <= 15; i++) CHECK(ump_log_read(UMP_LOG_BASE+i) == 0); CHECK(ump_bank == 0 && ump_archive_bank == 1); CHECK(ump_cursor == 0 && ump_next == 0 && ump_archive_next == 0); - CHECK(ump_divider == 0 && ump_sequence == 0 && ump_post_dropped == 0); + CHECK(ump_divider == 0 && ump_sequence == 0 && ump_post == 0); CHECK(ump_can_header == 0 && ump_can_count == 0); for (i = 0; i < 3; i++) CHECK(ump_can_data[i] == 0); for (i = 0; i < 2; i++) CHECK(ump_pm67_voltage[i] == 0); diff --git a/tests/ump_logger/recorder_test.c b/tests/ump_logger/recorder_test.c index 8e1a8b2..03efb2c 100644 --- a/tests/ump_logger/recorder_test.c +++ b/tests/ump_logger/recorder_test.c @@ -44,16 +44,22 @@ __declspec(dllexport) int run_tests(void) /* Короткая CAN-команда должна остаться отдельным событием. */ modbus[127] = 0x40; ump_log_can(0x807F, 0x40, 0, 0); - CHECK(ump_post == 1000); - CHECK(ump_word(0, ump_count-1, 2) == 0x102); + CHECK(ump_post == 0); /* A command alone is not an output transition. */ + GpioDataRegs.GPBDAT.bit.GPIO58 = 1; + ump_log_io(); /* STOP may be sampled before RES_OUT_2 is cleared. */ + CHECK(ump_post == 0); + GpioDataRegs.GPADAT.bit.GPIO3 = 1; + ump_log_io(); + CHECK(ump_post == UMP_POST_POINTS); + CHECK(ump_word(0, ump_count-1, 2) == 0x103); CHECK(ump_word(0, ump_count-1, 18) == 0x807F); - for (i=0; i<999; i++) ump_log_tick(); + for (i=0; i