Исправить TESTLAMP, запуск CAN и расчёт фазных токов

Сохранить состояние TESTLAMP при кратких провалах входа и исключить повторный вход Timer1 во время передачи индикации.
Явно сбрасывать служебный обмен CAN при запуске и выбирать соседнюю фазу по локальному индексу канала АЦП.
Сохранить текущую последовательность DATA/CLK и проверочные варианты таймингов, добавить поведенческие тесты и обновить образ прошивки с картой памяти.
This commit is contained in:
2026-10-13 14:13:43 +03:00
parent fa20ed085b
commit 04a9d989fb
18 changed files with 1983 additions and 655 deletions

View File

@@ -0,0 +1,56 @@
# Проверяет запуск служебного CAN на грязной RAM: использует рабочие обработчики и канонический упаковщик протокола.
import os # Читает пути общей библиотеки и компилятора, не меняя настройки проекта.
from pathlib import Path # Строит абсолютные пути независимо от каталога запуска проверки.
import re # Извлекает рабочие функции, не заменяя их алгоритмы копиями в модели.
import shutil # Находит доступный компилятор GCC по пользовательскому PATH.
import subprocess # Запускает компиляцию и отдельный процесс проверки с контролем кода завершения.
import sys # Повторно использует текущий Python для загрузки тестовой библиотеки.
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" # Выбирает канонические проверку запросов и формат фрагментов CAN.
compiler = os.environ.get("CC") or shutil.which("gcc") or "C:/msys64/mingw64/bin/gcc.exe" # Поддерживает заданный CC и установленный в рабочем окружении MinGW.
environment = os.environ.copy() # Ограничивает изменение PATH процессами компилятора и модели.
environment["PATH"] = str(Path(compiler).resolve().parent) + os.pathsep + environment.get("PATH", "") # Делает доступными вспомогательные DLL выбранного компилятора в Windows.
def function(text, name): # Возвращает полное тело указанной функции вместе с её рабочими комментариями.
match = re.search(r"^(?:static\s+)?(?:void|int)\s+" + re.escape(name) + r"\([^;{}]*\)", text, re.M) # Отличает определение функции от вызова и прототипа.
if not match: # Не позволяет тесту незаметно проверять устаревшую копию после переименования функции.
raise AssertionError(f"Не найдена рабочая функция {name}") # Завершает проверку с названием отсутствующей точки интеграции.
end = re.search(r"^}", text[match.end():], re.M) # Использует принятое в модуле размещение закрывающей скобки функции в первом столбце.
if not end: # Отвергает неполный фрагмент вместо генерации частично работающей модели.
raise AssertionError(f"Не найден конец функции {name}") # Указывает причину невозможности извлечения рабочего алгоритма.
return text[match.start():match.end() + end.end()] + "\n" # Сохраняет исходный порядок операторов для компиляции под O3.
source = (root / "Source/Internal/ecan.c").read_text(encoding="utf-8") # Читает именно текущие исходники транспорта, включая новый сброс состояния.
startup = function(source, "InitCan") # Проверяет реальную связь сброса с аппаратной инициализацией, а не только отдельный helper.
startup_code = re.sub(r"/\*.*?\*/|//[^\n]*", "", startup, flags=re.S) # Удаляет комментарии, чтобы упоминания helper или регистров не выдавались за вызов.
assert len(re.findall(r"\bump_can_init\s*\(\s*\)\s*;", startup_code)) == 1, "InitCan должен один раз вызывать ump_can_init" # Требует выполнения нового сброса при каждом аппаратном запуске.
hardware_start = re.search(r"\b(?:EALLOW|GpioCtrlRegs|ECanaRegs|PieCtrlRegs|IER)\b", startup_code) # Находит первую настройку периферии или разрешение CAN-прерываний.
assert hardware_start and startup_code.index("ump_can_init()") < hardware_start.start(), "Сброс CAN должен предшествовать настройке периферии" # Не допускает окна прерывания с неочищенными полями обмена.
state_start = source.index("static volatile unsigned int ump_can_pending;") # Начинает извлечение общих между ISR и главным циклом переменных.
state_end = source.index("static unsigned long ump_can_sent_at;") + len("static unsigned long ump_can_sent_at;") # Ограничивает фрагмент управляющими полями и штатным буфером ответа.
state = source[state_start:state_end] # Использует объявления из рабочего модуля, чтобы тест замечал изменение состава состояния.
state = state.replace("static unsigned int ump_can_reply[", "static uint16_t ump_can_reply[") # Воспроизводит на ПК 16-битный unsigned int C28x именно в буфере слов CAN.
model = (Path(__file__).with_name("model.inc")).read_text(encoding="utf-8") # Читает аппаратную модель и поведенческие сценарии, сохраняемые рядом с запускателем.
body = state + "\n" + "".join(function(source, name) for name in ("ump_can_init", "Handlai", "UMP_CAN_service")) # Подставляет сброс, настоящий приём запроса и сервис отправки без изменений их логики.
assert model.count("/* PRODUCTION_CAN */") == 1, "Нужна единственная точка подключения рабочего кода" # Исключает пропуск или двойную компиляцию проверяемых функций.
model = model.replace("/* PRODUCTION_CAN */", body) # Встраивает текущие обработчики между аппаратной моделью и сценариями проверки.
with tempfile.TemporaryDirectory(prefix="ump-can-init-") as directory: # Автоматически удаляет только временные результаты этой проверки.
build = Path(directory) # Фиксирует каталог файлов, не затрагивающих бинарные артефакты репозитория.
source_file = build / "can_init_test.c" # Выделяет общий сгенерированный исходник для обоих профилей EEPROM.
source_file.write_text(model, encoding="utf-8") # Сохраняет русские комментарии и извлечённые исходники без потери кодировки.
for profile in (0, 1): # Проверяет обе ветви маршрутизации CAN: AT25256B и M95M02.
library = build / (f"can_init_{profile}.dll" if os.name == "nt" else f"can_init_{profile}.so") # Разделяет библиотеки, чтобы профиль не подменял результат соседней сборки.
command = [compiler, "-O3", "-shared", "-Wall", "-Wextra", "-Werror", f"-DEEPROM_M95M02={profile}"] # Проверяет рабочий код с оптимизацией O3 и запрещает скрывать предупреждения модели.
command.append("-Wno-misleading-indentation") # Допускает существующие смешанные отступы Handlai без изменения проверяемого исходника TI.
if os.name != "nt": # Учитывает требования загрузки разделяемых библиотек ELF на Linux.
command.append("-fPIC") # Генерирует переносимые адреса только там, где они нужны формату библиотеки.
command += ["-I" + str(protocol / "ports/tms320f28335"), str(source_file)] # Передаёт модели официальные объявления порта C28x и текущий извлечённый код.
command += [str(protocol / "src/ump_protocol.c"), str(protocol / "ports/tms320f28335/ump_tms28335.c"), "-o", str(library)] # Линкует настоящие валидатор и упаковщик фрагментов вместо их заглушек.
subprocess.run(command, cwd=root, env=environment, check=True) # Прерывает проверку при ошибке любого профиля до объявления успешного результата.
runner = "import ctypes,sys; result=ctypes.CDLL(sys.argv[1]).run_tests(); print('PASS' if result==0 else f'FAIL: C line {result}'); sys.exit(bool(result))" # Возвращает строку неуспешной проверки C без зависимости от assert и режима NDEBUG.
subprocess.run([sys.executable, "-c", runner, str(library)], cwd=root, env=environment, check=True) # Освобождает DLL завершением дочернего процесса перед очисткой временного каталога Windows.
print(f"EEPROM_M95M02={profile}: холодный старт и первый запрос CAN проверены при -O3") # Показывает независимо подтверждённый результат каждого аппаратного профиля.