"""Проверяет частоту подтверждения R3 на настоящих масках и планировщике 166.""" # Проверка выполняется на ПК без отправки команд плате. from pathlib import Path # Находит исходники проекта и отдельно подготовленный вариант маски. import os # Передаёт библиотеки GCC только дочерним процессам проверки. import re # Извлекает определения и блоки C без комментариев. import subprocess # Компилирует и исполняет реальные алгоритмы прошивки в модели времени. import sys # Позволяет проверить старый и исправленный файлы одинаковым сценарием. import tempfile # Ограничивает результаты компиляции временным каталогом. root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).resolve().parents[2] # Установленная проверка по умолчанию использует свой проект прошивки. measure = Path(sys.argv[2]) if len(sys.argv) > 2 else root / 'Source/Internal/measure.c' # Подмена только этого файла позволяет воспроизвести ошибку до применения исправления. def clean(path): # Удаляет комментарии перед поиском парных скобок C. raw = path.read_bytes() # Сохраняет возможность чтения старых исходников Windows-1251. source = raw.decode('utf-8') if _utf8(raw) else raw.decode('cp1251') # Не теряет русские комментарии исходника при выборе кодировки. return re.sub(r'/\*.*?\*/|//[^\n]*', '', source, flags=re.S) # Скобки в пояснениях не меняют границы блока алгоритма. def _utf8(raw): # Отличает старую Windows-кодировку от требуемого UTF-8 без замены символов. try: # Корректный UTF-8 имеет приоритет перед совместимой однобайтовой кодировкой. raw.decode('utf-8'); return True # Успешное декодирование разрешает чтение современного исходника. except UnicodeDecodeError: # Старый файл будет прочитан в его исходной кодировке. return False # Не выдаёт повреждённые комментарии за исходный текст. def block(source, start): # Выделяет настоящий блок с вложенными условиями. cursor, depth = source.index('{', start) + 1, 1 # Начинает после открывающей скобки внешнего блока. while depth: # Ищет соответствующую закрывающую скобку. depth += (source[cursor] == '{') - (source[cursor] == '}') # Учитывает только скобки очищенного C-кода. cursor += 1 # Включает завершающую скобку в извлечённый текст. return source[start:cursor] # Проверка не поддерживает отдельную копию алгоритма прошивки. package = clean(root / 'Source/Internal/Include/package.h') # Значения режимов и индексов потоков берутся из проекта 166. names = ('dsk_COMM', 'dsk_BKSD', 'dsk_BKST', 'dsk_EPLT', 'dsk_SHKF', 'dsk_LOAD', 'm_FAST', 'm_SLOW') # Модели нужны только константы, используемые масками. defines = '\n'.join(re.search(r'^#define\s+' + name + r'\s+\d+', package, re.M).group() for name in names) # Не дублирует номера плат в тестовой карте. measure_source = clean(measure) # Выбирает проверяемую версию Init_packMask. mask_function = block(measure_source, measure_source.index('void Init_packMask(')) # Состав быстрой и медленной рассылки полностью берётся из прошивки. main = clean(root / 'Source/Internal/main.c') # Последовательность передачи берётся из текущего рабочего main.c. scheduler = block(main, main.index('if(CanGO)')) # Проверяет настоящий учёт занятого mailbox и продвижение адресов. base_tick = int(re.search(r'^#define\s+CANPOWSE\s+(\d+)', clean(root / 'Source/Internal/Include/measure.h'), re.M).group(1)) # При READY_FREQ=1000 Гц делитель CANPOWSE равен числу миллисекунд базового шага. model = r''' #include /* Выводит измеренные моделью интервалы подтверждения. */ #include /* Сбрасывает состояние перед каждым сочетанием периодов EEPROM. */ /* DEFINES */ static int Desk, Kurrent, tpl_cans, CanGO, modbus[131], busy; /* Запас слов повторяет трёхсловное чтение у границы R127. */ static unsigned int Maska[2][8], cancount[2], cancell[2], now, received_at, max_gap, received_count; /* Каждое состояние соответствует маскам, очередям и статистике RX. */ #define Cancount (modbus+0x60) /* R96/R97 задают периоды fast/slow в базовых интервалах CANPOWSE. */ #define CHECK(condition) do { if (!(condition)) { printf("FAIL line %d: %s\n", __LINE__, #condition); return 1; } } while (0) /* Ошибка остаётся видимой и при включённой оптимизации. */ static int CAN_send(int port, int data[], int address) { /* Моделирует только принятие кадра свободным mailbox, не аппаратный ACK. */ (void)port; /* Номер порта не влияет на порядок технологических кадров. */ if(busy) return 0; /* Второй поток в том же тике должен повторить попытку позднее. */ busy=1; /* Один передатчик одновременно принимает только один кадр. */ if(address<=3 && address+2>=3) { /* R3 передаётся и отдельным кадром, и внутри трёхсловного фрагмента. */ if(data[3] != (int)now) return 0; /* Проверяет передачу свежего слова, изменённого перед текущим шагом. */ if(now-received_at>max_gap) max_gap=now-received_at; /* Наблюдает худшее ожидание следующего реального кадра с R3. */ received_at=now; received_count++; /* Фиксирует только принятый передатчиком регистр. */ } /* Завершает наблюдение очередного фрагмента. */ return 1; /* Успешная постановка позволяет настоящему main продвинуть адрес. */ } /* Модель намеренно не заменяет проверку физического CAN на плате. */ /* MASK */ static void step(void) { int i, mask; /* Локальные переменные повторяют контекст фонового цикла main.c. */ /* SCHEDULER */ } /* Выполняет один базовый интервал настоящего планировщика. */ int main(void) { /* Покрывает штатные периоды 166 и варианты из EEPROM, включая отключённый медленный поток. */ const unsigned periods[][2]={{2,101},{10,101},{12,50},{2,0}}; /* Периоды заданы в тиках CANPOWSE: 2 — штатный быстрый делитель VPU-166, 101 — медленный. */ unsigned scenario, tick; /* Сценарии независимы, каждый охватывает сто секунд модельного времени. */ Desk=dsk_EPLT; Init_packMask(); /* Строит реальную карту задатчика выбранной версии прошивки. */ CHECK((Maska[m_FAST][0]&15)==15 && !(Maska[m_SLOW][0]&15)); /* R0–R3 должны подтверждаться быстрым потоком независимо от периода параметров. */ CHECK(Maska[m_FAST][1]==1 && Maska[m_SLOW][6]==0x1FF && Maska[m_SLOW][7]==0xE000); /* Кнопки R16 и медленные параметры R96–R104/R125–R127 сохраняют состав. */ for(scenario=0;scenario0 && max_gap<=3*(periods[scenario][0]+1)); /* Три быстрых кадра R0–R2/R3–R5/R16–R18 ограничивают ожидание независимо от slow. */ printf("PASS fast=%u slow=%u: R3 max=%u ms, updates=%u\n",periods[scenario][0],periods[scenario][1],max_gap*BASE_TICK,received_count); /* Переводит тики в миллисекунды базового таймера платы. */ } /* Все сочетания периодов обязаны давать частое подтверждение R3. */ return 0; /* Успех означает прохождение модели, а не испытание физического устройства. */ } /* Завершает регрессию задержки обратной связи VPU. */ ''' # Вставки ниже исполняют текущий код прошивки, а не его переписанную модель. model = model.replace('/* DEFINES */', defines + f'\n#define BASE_TICK {base_tick} /* Длительность базового шага в миллисекундах при таймере 1000 Гц. */').replace('/* MASK */', mask_function).replace('/* SCHEDULER */', scheduler) # Связывает реальные маски и планировщик с моделью одного занятого передатчика. compiler = Path(os.environ.get('CC', 'C:/msys64/mingw64/bin/gcc.exe')) # Использует установленный GCC без загрузки инструментов. environment = dict(os.environ, PATH=str(compiler.parent) + os.pathsep + os.environ.get('PATH', '')) # Делает библиотеки компилятора доступными только дочерним программам. with tempfile.TemporaryDirectory(prefix='vpu-r3-rx-') as directory: # Удаляет только собственные временные результаты после проверки. source, executable = Path(directory) / 'test.c', Path(directory) / 'test.exe' # Разделяет C-код проверки и исполняемую программу. source.write_text(model, encoding='utf-8') # Все создаваемые исходники и русские комментарии сохраняются в UTF-8. subprocess.run([str(compiler), '-std=c99', '-O3', '-Wall', '-Wextra', '-Werror', str(source), '-o', str(executable)], env=environment, check=True) # Требует чистой компиляции настоящих алгоритмов при оптимизации. subprocess.run([str(executable)], env=environment, check=True, timeout=10) # Зависание либо нарушение ограничения задержки проваливает регрессию.