Перенести улучшения прошивки в ветку 166
Добавить CAN heartbeat и передачу по изменению с повтором через 1,3 с, компактный логгер v4, паспорт прошивки и блочное чтение EEPROM. Подключить автоматическую версию и проверяемую сборку BIN. Сохранить локальные исправления стробов и защиты Timer1. Обновить тесты, документацию, UTF-8 и исключения временных файлов. Проверки: CAN, R3, EEPROM, паспорт, TESTLAMP, логгер, START/STOP; 9 сценариев сборки, включая реальную конверсию TI. Все 17 внутренних модулей скомпилированы TI C2000 без ошибок.
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
#include <stdint.h> /* Фиксирует ширину таймера и аппаратных полей eCAN независимо от разрядности host-компилятора. */
|
||||
#include <stdio.h> /* Выводит точную строку нарушенного сценария вместо молчаливого неверного результата. */
|
||||
#include <string.h> /* Изолирует независимые случаи очисткой модели аппаратуры и данных. */
|
||||
typedef uint16_t Uint16; typedef uint32_t Uint32; /* Повторяет 16- и 32-битные аппаратные типы TI C28x. */
|
||||
#define READY_FREQ 1000U /* Один такт Timer1 равен одной миллисекунде, как в действующей прошивке 166. */
|
||||
#define CANPOWSE 20U /* Прежний базовый интервал обнаружения обновлений данных составляет 20 мс. */
|
||||
#define ANSWER_LEN 128 /* Как в package.h, технологическая карта содержит ровно R0–R127 без дополнительного запаса для ошибочного чтения. */
|
||||
#define interrupt /* Host-модель вызывает настоящий ISR как обычную функцию без атрибутов процессора TI. */
|
||||
#define EALLOW ((void)0) /* Защищённый доступ аппаратных регистров отдельно проверяется настоящим компилятором TI. */
|
||||
#define EDIS ((void)0) /* Модель не подменяет логику изменения битов тайм-аута mailbox. */
|
||||
#define EINT ((void)0) /* Тесты явно выбирают момент аппаратного прерывания, исключая недетерминированные потоки. */
|
||||
#define DINT ((void)0) /* Сценарий проверяет восстановление маски, не симулируя планировщик ОС. */
|
||||
#define M_INT9 0x0100U /* Девятая группа PIE обслуживает eCAN в production-обработчике. */
|
||||
#define MINT9 0x0100U /* Сохраняет аппаратную маску CPU INT9 при обработке ошибки передачи. */
|
||||
#define MG96 0x00DFU /* Маска соответствует подавлению вложенного INTx6 в старом ISR проекта. */
|
||||
struct REG { Uint32 all; }; /* Даёт production-коду настоящие 32-битные поля регистров eCAN. */
|
||||
struct ECAN_REGS { struct REG CANTA, CANAA, CANTRR, CANTRS, CANTOC, CANTOS; Uint32 CANTSC; }; /* Моделирует только используемый жизненный цикл штатного mailbox. */
|
||||
static volatile struct ECAN_REGS ECanaRegs; /* Volatile сохраняет все наблюдаемые записи production-функций при оптимизации O3. */
|
||||
static struct { struct { struct REG MDH, MDL; } MBOX0; } ECanaMboxes; /* Восемь октетов передаваемого кадра наблюдаются независимо от кэша алгоритма. */
|
||||
static struct { Uint32 InterruptCount; } CpuTimer1; /* Счётчик переполняется в точности через 2^32 тактов. */
|
||||
static struct { struct { Uint16 all; } PIEIER9; union { unsigned all; struct { unsigned reserved:8, ACK9:1; } bit; } PIEACK; } PieCtrlRegs; /* Сохраняет реальное положение PIEACK9 в маске подтверждения IRQ. */
|
||||
static Uint16 IER; static unsigned int irq_mask; /* Позволяет проверить возврат исходного состояния прерываний после короткого снимка данных. */
|
||||
static int wait, CanTimeOutErrorTR, Mode, Desk, modbus[ANSWER_LEN]; /* Использует настоящий размер массива технологических слов и наблюдаемые параметры узла. */
|
||||
static unsigned int Maska[2][8]; /* Каждому потоку соответствует восемь 16-битных масок разрешённых регистров. */
|
||||
#define Cancount (modbus + 0x60) /* Как в package.h, R96 и R97 определяют включение двух потоков данных. */
|
||||
static unsigned int __disable_interrupts(void) { unsigned int previous = irq_mask; irq_mask = 1U; return previous; } /* Имитирует сохранение исходной маски IRQ даже при вложенном чтении Timer1. */
|
||||
static void __restore_interrupts(unsigned int previous) { irq_mask = previous; } /* Выявляет ошибочное безусловное разрешение прерываний отправителем. */
|
||||
static void led1_toggle(void) {} /* Индикация не влияет на подтверждение доставки или период таймера. */
|
||||
#include "can_telemetry_impl.h" /* Компилирует точный production-алгоритм, а не отдельную копию реализации в тесте. */
|
||||
/* TIMEOUT_ISR */
|
||||
#define CHECK(condition) do { if (!(condition)) { printf("FAIL line %d: %s\n", __LINE__, #condition); return 1; } } while (0) /* Проверка остаётся активной при оптимизации и показывает нарушенное ожидание. */
|
||||
static Uint32 frames_hi[2048], frames_lo[2048], frames_at[2048]; static unsigned int count; /* Журнал хранит только действительно запущенные аппаратные кадры и момент их передачи. */
|
||||
static void hardware_ack(void) { ECanaRegs.CANTRS.all &= ~1UL; ECanaRegs.CANTRR.all &= ~1UL; ECanaRegs.CANTA.all = 1UL; ECanaRegs.CANAA.all = 0UL; } /* Завершает именно mailbox 0 успешным аппаратным ACK. */
|
||||
static void prepare(unsigned int mode, unsigned int desk) { /* Начинает независимый сценарий, включая имитацию повторного запуска программы. */
|
||||
memset((void *)&ECanaRegs, 0, sizeof(ECanaRegs)); memset(&ECanaMboxes, 0, sizeof(ECanaMboxes)); memset(&PieCtrlRegs, 0, sizeof(PieCtrlRegs)); /* Сбрасывает только модель периферии между испытаниями. */
|
||||
memset(modbus, 0, sizeof(modbus)); memset(Maska, 0, sizeof(Maska)); CpuTimer1.InterruptCount = 0U; count = 0U; irq_mask = 0U; /* Устанавливает известное начальное содержимое карты и таймера. */
|
||||
Mode = mode; Desk = desk; Cancount[0] = 10; Cancount[1] = 101; IER = M_INT9; PieCtrlRegs.PIEIER9.all = 0x0030U; CanTimeOutErrorTR = 0; /* Периоды 10 и 101 проверяют независимость нового повтора от старых делителей. */
|
||||
CAN_telemetry_init(); /* Первый снимок после каждого запуска должен отправляться независимо от содержимого старых кэш-массивов. */
|
||||
} /* Аппаратный ACK в каждом сценарии задаётся явно. */
|
||||
static int pump(void) { /* Выдаёт всю уже готовую очередь с ACK между кадрами, не продвигая тестовое время. */
|
||||
unsigned int limit; /* Ограничивает итерации для обнаружения зависания на пустой маске или неизменном пакете. */
|
||||
for (limit = 0U; limit < 512U; ++limit) { /* Полная карта из двух потоков требует значительно меньше 512 аппаратных кадров. */
|
||||
CAN_telemetry_service(); /* Обслуживает реальную очередь, heartbeat и индивидуальные периоды подтверждённых данных. */
|
||||
CHECK(irq_mask == 0U); /* После любого пути отправки прерывания должны иметь первоначальное состояние. */
|
||||
if (!(ECanaRegs.CANTRS.all & 1UL)) return 0; /* Отсутствие нового TX означает завершённую выдачу готовых кадров. */
|
||||
CHECK(count < 2048U); /* Исключает скрытое переполнение тестового журнала. */
|
||||
frames_hi[count] = ECanaMboxes.MBOX0.MDH.all; frames_lo[count] = ECanaMboxes.MBOX0.MDL.all; frames_at[count++] = CpuTimer1.InterruptCount; /* Запоминает точные данные mailbox, не используя внутренний кэш production-кода как эталон. */
|
||||
hardware_ack(); /* Только успешное подтверждение разрешает алгоритму подавить следующий неизменный пакет. */
|
||||
} /* Бесконечное повторение выявляется как ошибка конечной модели. */
|
||||
CHECK(0); /* Даже при пустой маске или неподтверждённом адресе сервис обязан возвращаться без блокировки. */
|
||||
return 1; /* Сохраняет однозначный код ошибки для компилятора C. */
|
||||
} /* Все вызовы pump выполняются с фиксированным временем, поэтому сравнения периодов точны. */
|
||||
static unsigned int address_at(unsigned int index) { return (unsigned int)((frames_hi[index] >> 16) & 0x1FFFU); } /* Декодирует существующий маркер BALZAM независимо от внутренней реализации отправителя. */
|
||||
static unsigned int count_address(unsigned int address) { unsigned int index, found = 0U; for (index = 0U; index < count; ++index) if (address_at(index) == address) ++found; return found; } /* Считает реальные кадры выбранного технологического или heartbeat-адреса. */
|
||||
int main(void) { /* Проверяет требования пользователя, границы карты и ошибки аппаратного TX на настоящем коде прошивки. */
|
||||
unsigned int mode, before, index; Uint32 started; /* Сценарии используют независимые ожидаемые периоды в миллисекундах. */
|
||||
for (mode = 1U; mode <= 9U; ++mode) { /* Сравнение данных и повтор 1,3 с применяются ко всем аппаратным Mode, включая задатчик. */
|
||||
prepare(mode, mode == 8U ? 4U : 0U); Maska[0][0] = 1U; modbus[0] = 9; modbus[1] = 12; modbus[2] = -1; CHECK(pump() == 0); /* Начальный пакет должен передаться сразу в прежнем порядке трёх слов. */
|
||||
CHECK(count == 1U && frames_hi[0] == 0xE0000009UL && frames_lo[0] == 0x000CFFFFUL); /* Отрицательное 16-битное значение не портит упаковку и старший регистр. */
|
||||
CpuTimer1.InterruptCount = 999U; CHECK(pump() == 0 && count == 1U); /* Неизменившиеся данные не создают лишних кадров до секундного heartbeat. */
|
||||
CpuTimer1.InterruptCount = 1000U; CHECK(pump() == 0 && count == 2U && address_at(1U) == 0x100U); /* Heartbeat появляется ровно через одну секунду, независимо от обоих Cancount. */
|
||||
CHECK((frames_hi[1] & 0xFFFFUL) == 1U && frames_lo[1] == ((Uint32)Mode << 16 | (unsigned int)Desk)); /* Отдельный heartbeat содержит счётчик секунд, Mode и Desk. */
|
||||
CpuTimer1.InterruptCount = 1299U; CHECK(pump() == 0 && count_address(0U) == 1U); /* Технологический пакет не повторяется раньше индивидуального срока. */
|
||||
CpuTimer1.InterruptCount = 1300U; CHECK(pump() == 0 && count_address(0U) == 2U); /* Неизменные данные повторяются на границе 1,3 с без искусственного изменения состояния планировщика тестом. */
|
||||
CHECK(frames_at[count - 1U] == 1300U && frames_hi[count - 1U] == frames_hi[0]); /* Повторяет сами данные, а не только отдельный heartbeat. */
|
||||
} /* Завершает проверку одинакового поведения всех Mode. */
|
||||
prepare(5U, 0U); Maska[0][0] = 1U; Maska[1][0] = 8U; modbus[0] = 1; modbus[3] = 2; CHECK(pump() == 0 && count == 2U); /* Два потока получают отдельные пакеты и подтверждения без ожидания старых делителей. */
|
||||
CpuTimer1.InterruptCount = 40U; modbus[1] = 77; CHECK(pump() == 0 && count == 3U && address_at(2U) == 0U); /* Изменение второго слова пакета отправляется до истечения 1,3 с. */
|
||||
CpuTimer1.InterruptCount = 60U; modbus[2] = 88; CHECK(pump() == 0 && count == 4U && (frames_lo[3] & 0xFFFFUL) == 88U); /* Изменение третьего слова тоже обнаруживается и не подавляется кэшем первого регистра. */
|
||||
CpuTimer1.InterruptCount = 1300U; CHECK(pump() == 0 && count_address(3U) == 2U && count_address(0U) == 3U); /* Независимый пакет повторяется через 1,3 с, а обновлённый пакет R0 ещё не достиг своего нового срока. */
|
||||
CpuTimer1.InterruptCount = 1360U; CHECK(pump() == 0 && count_address(0U) == 4U); /* Период R0 отсчитывается заново от ACK последнего изменённого снимка. */
|
||||
prepare(8U, 4U); Maska[0][1] = 1U; Maska[1][0] = 15U; ECanaRegs.CANTRS.all = 1U; ECanaMboxes.MBOX0.MDH.all = 0x12345678UL; CAN_telemetry_service(); /* Занятый внешним запросом mailbox нельзя менять или считать доставленным. */
|
||||
CHECK(ECanaMboxes.MBOX0.MDH.all == 0x12345678UL && can_scan_due == 1U); /* Оба потока сохраняют готовность до физического освобождения передатчика. */
|
||||
ECanaRegs.CANTRS.all = 0U; CHECK(pump() == 0 && count_address(16U) == 1U && count_address(0U) == 1U && count_address(3U) == 1U); /* После занятости не теряются R0–R2 и R3–R5 медленного потока. */
|
||||
prepare(5U, 0U); Maska[0][0] = 1U; CAN_telemetry_service(); CHECK(wait && !(can_valid[0] & 1U)); /* Постановка первого пакета в TX ещё не означает успешную доставку. */
|
||||
ECanaRegs.CANTA.all = 0U; ECanaRegs.CANTOS.all = 1U; ECanaRegs.CANTOC.all = 5U; CANa_reset_err(); /* Воспроизводит аппаратный timeout, пока отдельный контроль служебного mailbox тоже включён. */
|
||||
CHECK(can_tx_failed && CanTimeOutErrorTR == 1 && ECanaRegs.CANTRR.all == 1U && ECanaRegs.CANTOC.all == 4U); /* ISR помечает отказ и отменяет только штатный TX без выключения служебного контроля. */
|
||||
ECanaRegs.CANTOS.all &= ECanaRegs.CANTOC.all; ECanaRegs.CANTRS.all = ECanaRegs.CANTRR.all = 0U; ECanaRegs.CANAA.all = 0U; CpuTimer1.InterruptCount = 10U; CAN_telemetry_service(); /* Отмена завершена без CANAA: отсутствие этого флага не должно навсегда заблокировать очередь. */
|
||||
CHECK(!wait && can_backoff && !(can_valid[0] & 1U)); /* Неудачный снимок не становится подтверждённым и остаётся кандидатом следующей попытки. */
|
||||
CpuTimer1.InterruptCount = 1009U; modbus[0] = 42; CAN_telemetry_service(); CHECK(!(ECanaRegs.CANTRS.all & 1U)); /* До окончания секундной паузы после ошибки повтор не засоряет CAN. */
|
||||
CpuTimer1.InterruptCount = 1010U; CHECK(pump() == 0 && count_address(0U) == 1U && (frames_hi[count - 1U] & 0xFFFFUL) == 42U); /* После паузы повторяет актуальное изменение и отдельно ожидающий heartbeat. */
|
||||
prepare(5U, 0U); Maska[0][0] = 1U; CAN_telemetry_service(); ECanaRegs.CANTA.all = 0U; CpuTimer1.InterruptCount = 100U; CAN_telemetry_service(); /* Потерянный timeout IRQ страхуется реальным временем Timer1 через 100 мс. */
|
||||
CHECK(can_tx_failed && ECanaRegs.CANTRR.all == 1U && CanTimeOutErrorTR == 1); /* Программная отмена не ждёт аппаратного CANAA в блокирующем цикле. */
|
||||
CAN_telemetry_service(); CHECK(CanTimeOutErrorTR == 1); /* Длительная аппаратная отмена не увеличивает счётчик ошибки на каждой итерации main. */
|
||||
prepare(5U, 0U); Maska[0][0] = 1U; CHECK(pump() == 0); CpuTimer1.InterruptCount = 20U; modbus[0] = 2; CAN_telemetry_service(); ECanaRegs.CANTA.all = 0U; ECanaRegs.CANTRS.all = 0U; ECanaRegs.CANAA.all = 1U; CAN_telemetry_service(); /* Изменение, отменённое после старого успешного снимка, не должно затереть подтверждённый кэш. */
|
||||
CHECK(can_last_hi[0] == 0xE0000000UL && can_backoff); /* Только реально доставленный прежний пакет остаётся подтверждённым. */
|
||||
prepare(5U, 0U); CHECK(pump() == 0 && count == 0U); CpuTimer1.InterruptCount = 3500U; CHECK(pump() == 0 && count == 1U && address_at(0U) == 0x100U && (frames_hi[0] & 0xFFFFU) == 3U); /* Пустые маски не зависают; после задержки main отправляется последний heartbeat без пачки пропущенных секунд. */
|
||||
CHECK(pump() == 0 && count == 1U); /* Повторная итерация main не дублирует уже поставленный heartbeat. */
|
||||
prepare(5U, 0U); Maska[0][0] = 1U; Cancount[0] = 0; Cancount[1] = 0; CHECK(pump() == 0 && count == 0U); CpuTimer1.InterruptCount = 1000U; CHECK(pump() == 0 && count == 1U && address_at(0U) == 0x100U); /* Отключённые потоки не отключают независимый heartbeat узла. */
|
||||
prepare(5U, 0U); Maska[0][7] = 0x8000U; modbus[127] = 0x55AA; CHECK(pump() == 0 && count == 1U && frames_hi[0] == 0xE07F55AAUL && frames_lo[0] == 0U); /* Конец карты R127 дополняется нулями без чтения data[128] и data[129]. */
|
||||
prepare(5U, 0U); for (index = 0U; index < 8U; ++index) Maska[0][index] = 0xFFFFU; CHECK(pump() == 0 && count == 43U); /* Плотная маска сохраняет прежние 43 трёхсловных пакета вместо лишних 128 перекрывающихся кадров. */
|
||||
for (index = 0U; index < count; ++index) CHECK(address_at(index) == index * 3U); /* Первый полный снимок покрывает ровно R0, R3, ... R126 в установленном формате. */
|
||||
CpuTimer1.InterruptCount = 1300U; CHECK(pump() == 0 && count_address(0U) == 2U && count_address(126U) == 2U && count == 87U); /* Готовая очередь выдаёт всю карту за период, а не один кадр маски каждые 1,3 с. */
|
||||
prepare(5U, 0U); CpuTimer1.InterruptCount = 0xFFFFFF00UL; started = CpuTimer1.InterruptCount; Maska[0][0] = 1U; CHECK(pump() == 0); CpuTimer1.InterruptCount = started + 1000U; CHECK(pump() == 0 && count_address(0x100U) == 1U); /* Секундный heartbeat переживает переполнение Timer1. */
|
||||
CpuTimer1.InterruptCount = started + 1300U; CHECK(pump() == 0 && count_address(0U) == 2U); /* Период повторения данных тоже корректен через 32-битное переполнение. */
|
||||
prepare(5U, 0U); Maska[0][0] = 1U; CHECK(pump() == 0); before = count; can_valid[7] = 0xFFFFU; can_backoff = can_heartbeat_due = 1U; wait = 1; CAN_telemetry_init(); CHECK(pump() == 0 && count == before + 1U); /* Повторная инициализация отбрасывает грязное состояние и заново отправляет первый снимок. */
|
||||
prepare(5U, 0U); Maska[0][1] = 1U; Maska[1][0] = 1U; for (index = 0U; index <= 250U; ++index) { CpuTimer1.InterruptCount = index * 20U; modbus[16] = index; CHECK(pump() == 0); } /* Непрерывное изменение быстрого потока не должно вытеснить heartbeat и неизменный пакет другого потока. */
|
||||
CHECK(count_address(16U) == 251U && count_address(0U) == 4U && count_address(0x100U) == 5U); /* За пять секунд доставляются все изменения, четыре состояния R0 и пять секундных heartbeat. */
|
||||
prepare(5U, 0U); Maska[0][0] = 1U; CAN_telemetry_service(); CpuTimer1.InterruptCount = 50U; hardware_ack(); CAN_telemetry_service(); /* Подтверждает первый пакет через 50 мс после запуска, а не в нулевой момент передачи. */
|
||||
CpuTimer1.InterruptCount = 1349U; CHECK(CAN_send(0, modbus, 0) == 2); CpuTimer1.InterruptCount = 1350U; CHECK(CAN_send(0, modbus, 0) == 1); /* Индивидуальные 1300 мс отсчитываются от ACK, поэтому ранний повтор через 1300 мс от запуска запрещён. */
|
||||
prepare(5U, 0U); ECanaRegs.CANTOC.all = 1U; ECanaRegs.CANTRS.all = 1U; CANa_reset_err(); /* Вызывает фантомный ISR без аппаратного CANTOS0 во время исправной передачи. */
|
||||
CHECK(!can_tx_failed && !CanTimeOutErrorTR && ECanaRegs.CANTRR.all == 0U && ECanaRegs.CANTOC.all == 1U && PieCtrlRegs.PIEIER9.all == 0x0030U); /* Не отменяет исправный кадр и восстанавливает прежние разрешения обоих CAN IRQ. */
|
||||
prepare(5U, 0U); Maska[0][0] = 1U; ECanaRegs.CANTRR.all = 1U; ECanaMboxes.MBOX0.MDH.all = 0x11223344UL; CAN_telemetry_service(); CHECK(ECanaMboxes.MBOX0.MDH.all == 0x11223344UL && !wait); /* Даже без CANTRS незавершённая аппаратная отмена запрещает перезапись mailbox. */
|
||||
prepare(5U, 0U); irq_mask = 1U; CHECK(CAN_send(0, modbus, 0) == 1 && irq_mask == 1U); /* Передатчик сохраняет уже запрещённые IRQ вызывающего кода вместо безусловного разрешения. */
|
||||
puts("PASS: heartbeat 1 Hz; changed data; per-packet refresh 1300 ms; all modes; masks; ACK, timeout and backoff; wraparound; dirty RAM; bounds"); /* Подтверждает проверку реального production-кода без утверждения о запуске на физической плате. */
|
||||
return 0; /* Все независимые поведенческие проверки завершились успешно. */
|
||||
} /* Модель не требует подключения CAN-адаптера и не отправляет команды реальному оборудованию. */
|
||||
@@ -0,0 +1,22 @@
|
||||
from pathlib import Path # Находим production-исходники относительно установленного теста либо явно выбранного проверяемого снимка.
|
||||
import os # Подключаем runtime GCC только дочернему процессу теста.
|
||||
import re # Извлекаем настоящий ISR с сохранением вложенных условий и без отдельной копии алгоритма ошибки.
|
||||
import subprocess # Компилируем и выполняем поведенческую модель передатчика.
|
||||
import sys # Разрешаем проверить изолированный снимок коммита вместо текущего рабочего дерева.
|
||||
import tempfile # Создаём исполняемый файл проверки только во временном каталоге теста.
|
||||
test = Path(__file__).resolve().parent # Производственная установка хранит этот файл в tests/can_telemetry.
|
||||
root = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else test.parents[1] # При обычном запуске проверяет текущий проект прошивки.
|
||||
source = (root / 'Source/Internal/ecan.c').read_text(encoding='utf-8') # Изменённый production-файл и русские комментарии обязаны быть корректным UTF-8.
|
||||
clean = re.sub(r'/\*.*?\*/|//[^\n]*', '', source, flags=re.S) # Скобки комментариев не влияют на выделение ISR.
|
||||
match = re.search(r'interrupt\s+void\s+CANa_reset_err\(void\)\s*\{', clean) # Отличаем определение обработчика от прототипа и регистрации IRQ.
|
||||
assert match # Отсутствующий обработчик не считается успешной проверкой передачи.
|
||||
end, depth = match.end(), 1 # Начинаем поиск после внешней открывающей скобки функции.
|
||||
while depth: depth += (clean[end] == '{') - (clean[end] == '}'); end += 1 # Сохраняем все вложенные аппаратные проверки оригинала.
|
||||
model = (test / 'model.c').read_text(encoding='utf-8').replace('/* TIMEOUT_ISR */', clean[match.start():end]) # Сама реализация телеметрии подключается напрямую через production-header.
|
||||
compiler = os.environ.get('CC', 'C:/msys64/mingw64/bin/gcc.exe') # Поддерживаем установленный GCC и явный выбор другого совместимого компилятора.
|
||||
environment = dict(os.environ); environment['PATH'] = str(Path(compiler).parent) + os.pathsep + environment.get('PATH', '') # Не меняем системный PATH пользователя или настройки CCS.
|
||||
with tempfile.TemporaryDirectory(prefix='can-telemetry-166-') as directory: # Временные файлы не попадают в источник прошивки или Git-коммит.
|
||||
cfile, exe = Path(directory) / 'test.c', Path(directory) / 'test.exe' # Отделяем исходник модели и её исполняемый результат.
|
||||
cfile.write_text(model, encoding='utf-8') # Тестовые комментарии сохраняются в той же кодировке UTF-8.
|
||||
subprocess.run([compiler, '-std=c99', '-O3', '-Wall', '-Wextra', '-Werror', '-I' + str(root / 'Source/Internal/Include'), str(cfile), '-o', str(exe)], env=environment, check=True) # Все предупреждения нового алгоритма считаются ошибками до создания коммита.
|
||||
subprocess.run([str(exe)], env=environment, check=True, timeout=10) # Зависание очереди или нарушенный тайминг завершают проверку ошибкой.
|
||||
@@ -9,6 +9,11 @@ import sys # Позволяет проверить отдельно сохра
|
||||
root = Path(__file__).resolve().parents[2] # Корень проекта содержит рабочую реализацию eCAN.
|
||||
source_path = Path(sys.argv[1]) if len(sys.argv) > 1 else root / "Source/Internal/ecan.c" # Дополнительный путь нужен для проверки чувствительности к старой ошибке.
|
||||
source = re.sub(r"/\*.*?\*/|//[^\n]*", "", source_path.read_text(encoding="utf-8"), flags=re.S) # Комментарии со скобками не участвуют в выделении функций.
|
||||
if '#include "can_telemetry_impl.h"' in source: # Новая реализация отправителя вынесена в заголовок и требует модели с подтверждённым кэшем и временем Timer1.
|
||||
import runpy # Запускает поведенческую проверку действующего передатчика вместо извлечения отсутствующей функции из ecan.c.
|
||||
sys.argv = [str(root / "tests/can_telemetry/run_test.py")] # Не передаёт путь отдельного ecan.c как ошибочный корень проекта новому тесту.
|
||||
runpy.run_path(sys.argv[0], run_name="__main__") # Проверяет аппаратный и программный тайм-ауты, отмену TX, ACK и секундную паузу на production-коде.
|
||||
raise SystemExit(0) # Старую модель ниже сохраняет только для явно выбранных снимков прежнего передатчика.
|
||||
|
||||
def function(name): # Извлекает текущий алгоритм вместо поддержания отдельной копии в тесте.
|
||||
match = re.search(r"(?:interrupt\s+)?void\s+" + name + r"\([^;{}]*\)\s*\{", source) # Отличает определение от вызова и объявления.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Проверяет частоту подтверждения 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(')) # Состав быстрой и медленной рассылки полностью берётся из прошивки.
|
||||
ecan = clean(root / 'Source/Internal/ecan.c') # Реальный ISR завершает ту же передачу, что и новый сервис телеметрии.
|
||||
match = re.search(r'interrupt\s+void\s+CANa_reset_err\(void\)\s*\{', ecan) # Находит определение обработчика ошибок вместо его прототипа.
|
||||
assert match is not None # Исчезновение обработчика должно проваливать проверку.
|
||||
model = (root / 'tests/can_telemetry/model.c').read_text(encoding='utf-8').split('int main(void)')[0] # Повторно использует аппаратуру, ACK и журнал кадров общей модели нового передатчика.
|
||||
model = model.replace('/* TIMEOUT_ISR */', block(ecan, match.start())) # Вставляет реальное тело обработчика без отдельной копии его алгоритма.
|
||||
model += '\n' + defines + '\nstatic int Kurrent, tpl_cans; /* Настройки требуются ветвям настоящей Init_packMask. */\n' + mask_function # Берёт реальные номера плат, потоков и состав масок из текущей прошивки.
|
||||
model += r'''
|
||||
int main(void) { /* Измеряет свежесть R3 при реальной карте пульта и разных настройках потоков EEPROM. */
|
||||
const unsigned periods[][2] = {{2,101},{10,101},{12,50},{2,0}}; /* Проверяет прежние настройки, включая отключённый медленный поток. */
|
||||
unsigned scenario, tick, cursor, received, last, gap, worst; /* Наблюдает переданные кадры и разрыв между обновлениями R3. */
|
||||
for (scenario = 0; scenario < sizeof(periods)/sizeof(periods[0]); ++scenario) { /* Проверяет независимые запуски платы. */
|
||||
prepare(8U, dsk_EPLT); Init_packMask(); /* Строит настоящую карту задатчика поверх сброшенной модели CAN. */
|
||||
CHECK((Maska[m_FAST][0] & 15U) == 15U && !(Maska[m_SLOW][0] & 15U)); /* R0–R3 остаются в быстром потоке при отключённом медленном. */
|
||||
CHECK(Maska[m_FAST][1] == 1U && Maska[m_SLOW][6] == 0x1FFU && Maska[m_SLOW][7] == 0xE000U); /* Кнопки и параметры сохраняют состав исходной карты. */
|
||||
Cancount[0] = periods[scenario][0]; Cancount[1] = periods[scenario][1]; /* Ненулевое значение теперь включает обновления по изменению и таймеру. */
|
||||
cursor = received = last = worst = 0U; /* Учитывает также начальное ожидание первого подтверждения. */
|
||||
for (tick = 1U; tick <= 5000U; ++tick) { /* Пять секунд включают heartbeat и обязательные повторы неизменных данных. */
|
||||
CpuTimer1.InterruptCount = tick; modbus[3] = tick; CHECK(pump() == 0); /* Меняет R3 каждую миллисекунду и выдаёт готовую очередь через настоящие CAN_send и ACK. */
|
||||
for (; cursor < count; ++cursor) { /* Анализирует только новые кадры mailbox. */
|
||||
if (address_at(cursor) != 3U) continue; /* R3 начинает второй трёхсловный фрагмент действительной маски. */
|
||||
CHECK((frames_hi[cursor] & 0xFFFFU) == tick); /* Требует свежее значение вместо старого снимка. */
|
||||
gap = tick - last; if (gap > worst) worst = gap; last = tick; ++received; /* Измеряет худший интервал получения актуального R3. */
|
||||
} /* Завершает анализ кадров текущего момента. */
|
||||
} /* Приоритет heartbeat обрабатывается общей моделью передатчика. */
|
||||
CHECK(received > 0U && worst <= CANPOWSE && tick - 1U - last <= CANPOWSE); /* При немедленном ACK интервал ограничен шагом сканирования 20 мс независимо от slow. */
|
||||
CHECK(count_address(CAN_HEARTBEAT_ADDR) >= 4U); /* Обновление R3 не должно вытеснять секундный heartbeat. */
|
||||
printf("PASS VPU fast=%u slow=%u: R3 max=%u ms, updates=%u\n", periods[scenario][0], periods[scenario][1], worst, received); /* Выводит измеренный интервал и число свежих кадров. */
|
||||
} /* Завершает матрицу настроек EEPROM. */
|
||||
return 0; /* Подтверждает программную модель без заявления об аппаратном испытании. */
|
||||
} /* Завершает проверку обратной связи R3. */
|
||||
''' # Использует production-маски и production-сервис вместо удалённого блока if(CanGO).
|
||||
compiler = Path(os.environ.get('CC', 'C:/msys64/mingw64/bin/gcc.exe')) # Использует установленный GCC без изменения системных настроек.
|
||||
environment = dict(os.environ, PATH=str(compiler.parent) + os.pathsep + os.environ.get('PATH', '')) # Добавляет runtime только дочерним процессам.
|
||||
with tempfile.TemporaryDirectory(prefix='vpu-r3-rx-') as directory: # Отделяет временные результаты от образов пользователя.
|
||||
source, executable = Path(directory) / 'test.c', Path(directory) / 'test.exe' # Разделяет исходник и исполняемый результат.
|
||||
source.write_text(model, encoding='utf-8') # Сохраняет русские пояснения в UTF-8.
|
||||
subprocess.run([str(compiler), '-std=c99', '-O3', '-Wall', '-Wextra', '-Werror', '-I' + str(root / 'Source/Internal/Include'), str(source), '-o', str(executable)], env=environment, check=True) # Компилирует настоящий заголовок при строгих предупреждениях.
|
||||
subprocess.run([str(executable)], env=environment, check=True, timeout=10) # Нарушение задержки или зависание проваливает проверку.
|
||||
@@ -0,0 +1,83 @@
|
||||
#include <stdint.h> /* Задаёт независимые от host ABI размеры слов паспорта. */
|
||||
#include <stdio.h> /* Сообщает точную строку нарушенной регрессии. */
|
||||
#include <string.h> /* Сбрасывает только модель RS-запроса между сценариями. */
|
||||
#include "crc16.h" /* Использует настоящую CRC прошивки вместо имитации проверки ответа. */
|
||||
#include "firmware_info.h" /* Подключает действительный адаптер границ 0x1200..0x120B. */
|
||||
#include "../src/firmware_info_static.c" /* Проверяет тот же общий упаковщик, который компилируется в message.c. */
|
||||
#define CHECK(value) do { if (!(value)) { printf("FAIL line %d: %s\n", __LINE__, #value); return 1; } } while (0) /* Сохраняет проверки при оптимизации O3. */
|
||||
#define CMD_MODBUS_3 3U /* Код чтения holding-регистров остаётся прежним. */
|
||||
#define CMD_MODBUS_6 6U /* Код одиночной записи используется для проверки защиты паспорта. */
|
||||
#define CNTRL_ADDR 1U /* Тестирует конкретный корректный адрес узла без изменения производственной адресации. */
|
||||
#define ANSWER_LEN 128 /* Повторяет размер прежней технологической карты. */
|
||||
#define true 1 /* Повторяет 16-битное представление флага готовности ответа C28x. */
|
||||
#define LOBYTE(value) ((value) & 255U) /* Сохраняет порядок младшего октета CRC в RS-ответе. */
|
||||
#define HIBYTE(value) (((value) >> 8) & 255U) /* Сохраняет старший октет контрольной суммы Modbus. */
|
||||
typedef struct { unsigned int RS_Header[400], buffer[400], flag_TIMEOUT_to_Send; } RS_DATA; /* Даёт настоящим обработчикам только используемые поля UART-окружения. */
|
||||
static unsigned int sent, irq_mask, irq_calls, reads, writes, command, command_events; /* Наблюдает длину кадра, защиту IRQ и возможное повреждение старого backend. */
|
||||
static int modbus[ANSWER_LEN + 1]; /* Проверяет чтение технологических регистров прежним маршрутом. */
|
||||
static struct { unsigned int all; } Modbus[ANSWER_LEN + 1]; /* Поддерживает прежний интерфейс записи в таблицу параметров. */
|
||||
static unsigned int __disable_interrupts(void) { unsigned int previous = irq_mask; irq_mask = 1U; ++irq_calls; return previous; } /* Выявляет лишнюю блокировку IRQ при чтении неизменяемого паспорта. */
|
||||
static void __restore_interrupts(unsigned int previous) { irq_mask = previous; } /* Проверяет сохранение исходной маски для снимков логгера. */
|
||||
static void RS_Send(RS_DATA *rs, unsigned int *data, unsigned long length) { (void)rs; (void)data; sent = (unsigned int)length; } /* Перехватывает готовый кадр без реального UART. */
|
||||
unsigned int ump_log_read(unsigned int address) { ++reads; return address == 0x1000U ? 0x554DU : address; } /* Делает обращения к старому логгеру видимыми и узнаваемыми. */
|
||||
int ump_log_write(unsigned int address, unsigned int value) { (void)address; (void)value; ++writes; return 1; } /* Любая ошибочная запись паспорта в backend станет видна тесту. */
|
||||
static void UMP_commands_write(unsigned int value) { command = value; } /* Сохраняет наблюдение штатной команды регистра 127. */
|
||||
static void ump_log_rs(void) { ++command_events; } /* Проверяет сохранение прежней регистрации команды RS. */
|
||||
#include "ump_can_protocol.h" /* Тестирует реальный адаптер CAN 166, сохранивший двенадцатисловный логгер v4. */
|
||||
#include "include/balsam167_eeprom_block.h" /* Использует общий с 167 контракт FC03: окно 0x8000..0xBFFF, до 120 слов AT25256B за запрос. */
|
||||
static unsigned int spi_reads, spi_address, spi_bytes, spi_blocked; /* Наблюдает число SPI-транзакций, словный адрес, длину в октетах и маску IRQ во время чтения. */
|
||||
static void Seeprom_read(unsigned int address, unsigned int *data, unsigned int length) /* Имитирует только SPI-периферию с независимым узнаваемым содержимым всех 32 КиБ. */
|
||||
{ /* Производственный обработчик, упаковка и CRC остаются настоящими. */
|
||||
unsigned int index; /* Перебирает аппаратные 16-битные слова запрошенного блока. */
|
||||
++spi_reads; spi_address = address; spi_bytes = length; spi_blocked |= irq_mask; /* Выявляет лишние SPI-чтения и запрет Timer2 IRQ внутри нового маршрута. */
|
||||
for (index = 0U; index < length / 2U; ++index) data[index] = (address + index) ^ 0xA55AU; /* Эталон зависит от полного адреса, поэтому обнаруживает повтор блока и ошибку смещения. */
|
||||
} /* Модель не записывает EEPROM и не подменяет поведение UART. */
|
||||
/* PRODUCTION_MODBUS */
|
||||
static void request(RS_DATA *rs, unsigned int address, unsigned int count) /* Формирует уже проверенный транспортом заголовок для настоящих обработчиков. */
|
||||
{ /* Здесь не подменяется алгоритм чтения или упаковки ответа. */
|
||||
memset(rs, 0, sizeof(*rs)); sent = 0; /* Не переносит данные предыдущего ответа в новый запрос. */
|
||||
rs->RS_Header[2] = address >> 8; rs->RS_Header[3] = address & 255U; /* Упаковывает исходный адрес старшим октетом вперёд. */
|
||||
rs->RS_Header[4] = count >> 8; rs->RS_Header[5] = count & 255U; /* Упаковывает число слов либо значение одиночной записи. */
|
||||
} /* Приём полного CRC-запроса остаётся вне этой локальной проверки обработчиков. */
|
||||
int main(void) /* Проверяет перенос паспорта и сохранность старых границ протокола. */
|
||||
{ /* Эталон задан независимо от макроса общего C-инициализатора. */
|
||||
const unsigned int expected[12] = {1, 1, 0, 0, 2026, 0x0A10, 0x0834, 22, 0x3132, 0x3334, 0x6162, 0x6364}; /* Формат v1, версия 1.0.0, UTC 2026-10-16 08:52:22, Build ID 1234abcd. */
|
||||
RS_DATA rs; unsigned int words[UMP_CAN_MAX_WORDS], error, i, before; /* Использует те же ёмкости ответа, что действующий служебный CAN. */
|
||||
for (before = 0U; before < 16384U; before += 120U) { /* Полный дамп 32 КиБ должен занять 137 запросов вместо 16384 запросов PEEK. */
|
||||
unsigned int count = 16384U - before < 120U ? 16384U - before : 120U; /* Последний блок содержит только оставшиеся 64 слова, не выходя за физическую границу. */
|
||||
request(&rs, 0x8000U + before, count); ReceiveCommandModbus3(&rs); /* Вызывает настоящий новый маршрут FC03 по всему окну, включая последний блок. */
|
||||
CHECK(sent == count * 2U + 5U && rs.buffer[1] == 3U && rs.buffer[2] == count * 2U); /* Ответ имеет точную длину и не содержит нулей после CRC. */
|
||||
CHECK(spi_address == before && spi_bytes == count * 2U && !spi_blocked && !irq_calls); /* SPI получает правильные единицы адреса и длины при разрешённых прерываниях Timer2. */
|
||||
CHECK(get_crc_16(0xFFFFU, rs.buffer, sent) == 0U); /* Проверяет настоящую CRC каждого полного кадра независимым проходом по всем октетам. */
|
||||
for (i = 0U; i < count; ++i) CHECK(((rs.buffer[3U + i * 2U] << 8) | rs.buffer[4U + i * 2U]) == ((before + i) ^ 0xA55AU)); /* Сверяет каждое слово и порядок октетов после расширения UART-буфера на месте. */
|
||||
} /* Все 16384 слова должны быть доступны без чтения соседней памяти. */
|
||||
CHECK(spi_reads == 137U && reads == 0U && writes == 0U); /* Блочный маршрут не обращается к backend логгера и не выполняет записи. */
|
||||
request(&rs, 0xBFFFU, 1U); ReceiveCommandModbus3(&rs); CHECK(sent == 7U && spi_address == 0x3FFFU && spi_bytes == 2U); /* Последнее слово EEPROM остаётся доступно отдельным запросом. */
|
||||
before = spi_reads; /* Запоминает число разрешённых чтений до проверки всех ошибочных границ. */
|
||||
request(&rs, 0xBFFFU, 2U); ReceiveCommandModbus3(&rs); CHECK(sent == 5U && rs.buffer[1] == 0x83U && rs.buffer[2] == 2U); /* Пересечение конца EEPROM возвращает исключение до обращения к SPI. */
|
||||
request(&rs, 0x8000U, 0U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Нулевой блок не начинает зависающее аппаратное ожидание. */
|
||||
request(&rs, 0x8000U, 121U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Максимум 120 слов соответствует контракту SETGUI и безопасной длине ответа 245 октетов. */
|
||||
request(&rs, 0xC000U, 1U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Адрес сразу после окна не считается первым словом другой EEPROM. */
|
||||
request(&rs, 0xFFFFU, 120U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Переполнение 16-битного адреса не обходит ограничение диапазона. */
|
||||
request(&rs, 0x8000U, 0x8001U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Маркер M95M02 не принимается прошивкой с историческим 16-битным SPI-драйвером. */
|
||||
request(&rs, 0x7FFFU, 1U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Адрес перед окном остаётся запрещённым в старой карте регистров. */
|
||||
CHECK(spi_reads == before && get_crc_16(0xFFFFU, rs.buffer, sent) == 0U); /* Неверные запросы не читают SPI и получают корректное исключение с CRC. */
|
||||
request(&rs, 0x1200U, 12U); ReceiveCommandModbus3(&rs); CHECK(sent == 29U && rs.buffer[1] == 3U && rs.buffer[2] == 24U); /* Полный паспорт отвечает стандартным FC03 без хвостовых нулей. */
|
||||
for (i = 0; i < 12U; ++i) CHECK(((rs.buffer[3 + i * 2] << 8) | rs.buffer[4 + i * 2]) == expected[i]); /* Проверяет все поля, порядок символов и октетов независимо от производственной упаковки. */
|
||||
CHECK(get_crc_16(0xFFFFU, rs.buffer, sent) == 0U && reads == 0U && irq_calls == 0U); /* Паспорт защищён настоящей CRC и не обращается к логгеру или маске IRQ. */
|
||||
request(&rs, 0x120BU, 1U); ReceiveCommandModbus3(&rs); CHECK(sent == 7U && rs.buffer[3] == 0x63U && rs.buffer[4] == 0x64U); /* Последнее слово поддерживает частичное чтение в пределах контракта 167. */
|
||||
request(&rs, 0x120BU, 2U); ReceiveCommandModbus3(&rs); CHECK(sent == 5U && rs.buffer[1] == 0x83U && rs.buffer[2] == 2U); /* Пересечение верхней границы не раскрывает соседнюю память. */
|
||||
request(&rs, 0x11FFU, 2U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Запрос из несуществующего регистра перед паспортом не считается допустимым. */
|
||||
request(&rs, 0x1200U, 0U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Пустое чтение не принимает произвольный контекст за паспорт. */
|
||||
request(&rs, 0xFFFFU, 12U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Проверка диапазона выдерживает переполнение 16-битного адреса. */
|
||||
request(&rs, 0x1200U, 99U); ReceiveCommandModbus6(&rs); CHECK(sent == 5U && rs.buffer[1] == 0x86U && rs.buffer[2] == 2U && writes == 0U); /* Запись паспорта явно отвергается, не попадая в сохранённые параметры или логгер. */
|
||||
CHECK(ump_can_prepare(3U, 0x1200U, 12U, words, &error) == 12U && error == 0U); /* Служебный CAN читает тот же паспорт без переноса остального драйвера 167. */
|
||||
for (i = 0; i < 12U; ++i) CHECK(words[i] == expected[i]); /* Исключает расхождение паспорта между RS и CAN. */
|
||||
ump_can_prepare(3U, 0x120BU, 2U, words, &error); CHECK(error == 2U); ump_can_prepare(6U, 0x1200U, 0U, words, &error); CHECK(error == 2U && writes == 0U); /* CAN также запрещает выход за паспорт и запись в него. */
|
||||
modbus[5] = 0x1234; request(&rs, 5U, 1U); ReceiveCommandModbus3(&rs); CHECK(sent == 10U && rs.buffer[3] == 0x12U && rs.buffer[4] == 0x34U); /* Исторический ответ технологического регистра и его хвост сохранены. */
|
||||
request(&rs, 0x1000U, 16U); ReceiveCommandModbus3(&rs); CHECK(sent == 37U && rs.buffer[3] == 0x55U && rs.buffer[4] == 0x4DU && reads == 16U && irq_calls == 1U && irq_mask == 0U); /* Снимок статуса логгера сохраняет прежний маршрут и защиту IRQ. */
|
||||
before = reads; CHECK(ump_can_prepare(3U, UMP_LOG_DATA, 12U, words, &error) == 12U && error == 0U && reads == before + 12U && UMP_LOG_WORDS == 12U); /* Текущая запись v4 остаётся двенадцатисловной и не заменяется реализацией 167. */
|
||||
request(&rs, 127U, 0x55U); ReceiveCommandModbus6(&rs); CHECK(command == 0x55U && command_events == 1U && sent == 10U); /* Штатная команда управления проходит прежний обработчик без изменения её смысла. */
|
||||
puts("PASS: firmware-info RS/CAN, all 12 words, CRC, bounds, read-only, legacy registers and logger v4"); /* Подтверждает проверенный перенос без заявления об испытании аппаратной платы. */
|
||||
return 0; /* Завершает все поведенческие сценарии реального кода обработчиков. */
|
||||
} /* Остальные аппаратные алгоритмы не участвуют в тесте и не изменяются доработкой. */
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Проверяет точечный перенос паспорта 167 в действующие обработчики 166.""" # Использует реальные функции, общий статический порт и независимый эталон регистра.
|
||||
import importlib.util # Загружает проверку встроенного паспорта без запуска конверсии или сборки.
|
||||
import json # Формирует независимый тестовый manifest.
|
||||
import os # Подключает runtime установленного GCC только дочерним процессам.
|
||||
from pathlib import Path # Находит production-исходники и общую библиотеку относительно теста.
|
||||
import re # Извлекает функции Modbus без копирования их реализации в тест.
|
||||
import struct # Создаёт минимальный SCI8 с известными загрузочными словами.
|
||||
import subprocess # Компилирует настоящие обработчики под оптимизацией O3.
|
||||
import sys # Подключает общий анализатор паспорта, который использует SetGUI.
|
||||
import tempfile # Изолирует временные заголовки и исполняемые файлы от выпуска прошивки.
|
||||
ROOT = Path(__file__).resolve().parents[2] # Корень 166 содержит проверяемый message.c.
|
||||
TEMPLATES = ROOT.parent / "newProject/templates" # Использует ту же общую зависимость, что проект 167 и текущая сборка.
|
||||
def load(name, path): # Импортирует проектный скрипт без его CLI и побочных эффектов postbuild.
|
||||
spec = importlib.util.spec_from_file_location(name, path); module = importlib.util.module_from_spec(spec) # Создаёт независимый модуль с точным путём источника.
|
||||
spec.loader.exec_module(module); return module # Возвращает реальные функции проверки версии и SCI8.
|
||||
builder = load("balsam166_build", ROOT / "UKSSTMS320F28335/build_identity.py") # Проверяет именно выпускной валидатор проекта.
|
||||
source = re.sub(r"/\*.*?\*/|//[^\n]*", "", (ROOT / "Source/Internal/message.c").read_text(encoding="utf-8"), flags=re.S) # Исключает скобки из комментариев при выделении определений.
|
||||
def function(name): # Извлекает рабочую функцию с произвольной вложенностью условий.
|
||||
match = re.search(r"(?:static\s+)?void\s+" + name + r"\([^;{}]*\)\s*\{", source); assert match, name # Не принимает вызов или прототип за определение.
|
||||
end, depth = match.end(), 1 # Начинает подсчёт после открывающей скобки функции.
|
||||
while depth: depth += (source[end] == "{") - (source[end] == "}"); end += 1 # Находит внешнюю закрывающую скобку настоящего алгоритма.
|
||||
return source[match.start():end] # Сохраняет все операторы производственного обработчика.
|
||||
model = Path(__file__).with_name("port_test.c").read_text(encoding="utf-8") # Окружение содержит только UART/IRQ и наблюдаемый backend логгера.
|
||||
model = model.replace("/* PRODUCTION_MODBUS */", "\n".join(function(name) for name in ("modbus_exception", "ReceiveCommandModbus3", "ReceiveCommandModbus6"))) # Подставляет реальный код, который попадёт в прошивку.
|
||||
compiler = os.environ.get("CC", "C:/msys64/mingw64/bin/gcc.exe") # Сохраняет возможность запускать тест доступным C99-компилятором.
|
||||
environment = dict(os.environ); environment["PATH"] = str(Path(compiler).parent) + os.pathsep + environment.get("PATH", "") # Не меняет окружение CCS или приложения.
|
||||
with tempfile.TemporaryDirectory(prefix="firmware166-test-") as directory: # Удаляет только собственные временные данные теста.
|
||||
temporary = Path(directory); cfile = temporary / "port.c"; exe = temporary / "port.exe" # Разделяет C-модель и результат исполнения.
|
||||
values = dict(zip(("FIRMWARE_VERSION_MAJOR", "FIRMWARE_VERSION_MINOR", "FIRMWARE_VERSION_PATCH", "FI_YEAR", "FI_MONTH", "FI_DAY", "FI_HOUR", "FI_MINUTE", "FI_SECOND"), (1, 0, 0, 2026, 10, 16, 8, 52, 22))) # Детерминированный паспорт независим от времени запуска теста.
|
||||
values.update({f"FI_ID_{i}": ord(char) for i, char in enumerate("1234abcd")}) # Даёт известную последовательность ASCII-слов для проверки порядка октетов.
|
||||
(temporary / "firmware_identity.h").write_text("\n".join(f"#define {name} {value}U /* Проверочная константа паспорта v1. */" for name, value in values.items()), encoding="utf-8") # Сохраняет fixture заголовка в требуемой кодировке.
|
||||
cfile.write_text(model, encoding="utf-8") # Сохраняет извлечённые русские исходники без изменения production-файла.
|
||||
command = [compiler, "-std=c99", "-O3", "-Wall", "-Wextra", "-Werror", "-Wno-unused-function", "-I" + str(TEMPLATES / "c/set-protocol"), "-I" + str(temporary), "-I" + str(ROOT / "Source/Internal/Include"), "-I" + str(TEMPLATES / "c/firmware-info/include"), str(cfile), str(ROOT / "Source/Internal/crc16.c"), "-o", str(exe)] # Проверяет типы и реальные CRC вместе с обработчиками RS/CAN.
|
||||
subprocess.run(command, env=environment, check=True); subprocess.run([str(exe)], env=environment, check=True, timeout=10) # Ошибка типов, поведения или зависание делают тест неуспешным.
|
||||
metadata = {"schema": 1, "product": "Balsam-166-PM35", "target": "TMS320F28335", "version": [1, 0, 0], "build_id": "1234abcd", "built_at_utc": "2026-10-16T08:52:22+00:00", "identity_contract": 1, "identity_register_count": 12} # Фиксирует весь контракт manifest, который должен принять существующий GUI.
|
||||
words = [1, 1, 0, 0, 2026, 0x0A10, 0x0834, 22, 0x3132, 0x3334, 0x6162, 0x6364] # Независимый эталон совпадает с показанным протоколом, а не берётся из C-инициализатора.
|
||||
data = b"\xaa\x08" + bytes(20) + struct.pack("<HHH12HH", 12, 0, 0x8000, *words, 0) # Создаёт корректный SCI8 с одним 12-словным блоком и терминатором.
|
||||
image = temporary / "firmware.bin"; image.write_bytes(data); builder.verify_embedded(image, metadata, 32508) # Принимает совпавший паспорт с пределом 32508 байт до области параметров EEPROM платы 166.
|
||||
for broken in (data[:-2], data + b"\0\0", data[:28] + bytes(24) + data[52:], bytes(32509)): # Проверяет усечённый файл, хвост, чужие слова паспорта и пересечение параметров EEPROM.
|
||||
image.write_bytes(broken) # Подменяет только временный проверяемый образ.
|
||||
try: builder.verify_embedded(image, metadata, 32508) # Настоящий валидатор отклоняет дефекты SCI8 и превышение границы параметров 166 в 32508 байт.
|
||||
except (ValueError, struct.error): pass # Ожидаемый отказ подтверждает защиту выпуска.
|
||||
else: raise AssertionError("Invalid or stale SCI8 was accepted") # Не допускает фиктивного manifest поверх старой прошивки.
|
||||
image.write_bytes(data); settings = temporary / "metadata.json"; settings.write_text(json.dumps(metadata), encoding="utf-8") # Восстанавливает корректный временный выпуск для проверки GUI.
|
||||
sys.path.insert(0, str(TEMPLATES / "c/firmware-info/tools")); from identity import seal # Использует общий формат манифеста, применяемый в 167.
|
||||
seal(image, settings) # Подписывает точные байты тестового BIN общим алгоритмом SHA-256.
|
||||
native = ROOT.parent / "newProject/SETGUI/src/gui_desktop/native/setprotocol.dll" # Существующий GUI содержит ту же native-библиотеку CRC, которую импортирует анализатор паспорта.
|
||||
if native.is_file(): os.environ.setdefault("SETPROTOCOL_LIBRARY", str(native)) # Сохраняет явно заданный путь пользователя и подключает установленный GUI без изменения файлов зависимости.
|
||||
gui_python = ROOT.parent / "newProject/SETGUI/third_party/templates/python" # Проверяет фактическую vendored-копию анализатора текущего SetGUI, которая может отличаться от основной ветки templates.
|
||||
sys.path.insert(0, str(gui_python if (gui_python / "set_devices/firmware_identity.py").is_file() else TEMPLATES / "python")); from set_devices.firmware_identity import inspect_image, decode_identity # Без установленного GUI использует тот же общий контракт библиотеки.
|
||||
identity = inspect_image(image, expected_product="Balsam-166-PM35"); assert identity == decode_identity(words) # Версия из BIN и версия из ответа платы должны сравниваться без адаптации GUI.
|
||||
try: inspect_image(image, expected_product="Balsam-167-PM35") # Перенос проверки не должен разрешать выдавать прошивку 166 за 167.
|
||||
except ValueError: pass # Существующая защита GUI отличает product двух плат.
|
||||
else: raise AssertionError("166 firmware accepted as 167") # Ошибка product делает перенос небезопасным для выбора модели.
|
||||
print("PASS: 137-block EEPROM dump, SPI IRQ, bounds, embedded identity, stale/truncated/oversize rejection, exact manifest and unchanged SetGUI decoder") # Подтверждает проверку выпуска и совместимость без физического обмена с платой.
|
||||
File diff suppressed because one or more lines are too long
+34
-49
@@ -72,52 +72,37 @@ static void tick(void) /* Моделирует сохранение и восс
|
||||
check(mask_at_eint == (entry_mask & MINT13 & ~M_INT13), "interrupt mask"); /* Проверяет отсутствие собственного Timer1 и запрет включения ранее выключенных источников. */
|
||||
check(sends >= 1 && sends <= 2, "packet count"); /* Требует штатный объём одного такта: пакет ламп и не более одного пакета очередного индикатора. */
|
||||
} /* Завершает один такт индикации. */
|
||||
int main(void) /* Запускает на настоящих функциях сценарий рабочих ламп, теста с помехами и возврата в работу. */
|
||||
{ /* Начинает общий поведенческий сценарий. */
|
||||
int i, pwm_on = 0; /* Перебирает опросы и такты; считает включённые такты рабочего PWM за один полный период. */
|
||||
memset(modbus, 0, sizeof(modbus)); /* Убирает старые команды и состояния перед сценарием. */
|
||||
check(sizeof(WORDE) == sizeof(modbus[0]), "register layout"); /* Подтверждает корректность production-псевдонимов в хостовой модели. */
|
||||
Desk = dsk_EPLT; /* Выбирает реальную ветвь чтения TESTLAMP пульта. */
|
||||
modbus[2] = 1; /* Включает рабочую лампу до нажатия TESTLAMP, как в сообщении пользователя. */
|
||||
Bright[0] = maximum_bright; /* Задаёт уже включённой лампе постоянную яркость для обнаружения случайных провалов. */
|
||||
entry_mask = M_INT13 | (1U << 0) | (1U << 13); /* Разрешает Timer1, группу ADC и Timer2, проверяя также маскирование равного приоритета. */
|
||||
tick(); /* Получает первый рабочий пакет ламп. */
|
||||
check(lamp_word == 1, "normal lamp before test"); /* Подтверждает исходное постоянное свечение до нажатия. */
|
||||
GpioDataRegs.GPADAT.bit.GPIO26 = 1; /* Нажимает физическую кнопку активным высоким уровнем. */
|
||||
for (i = 0; i < 1001; i++) get_Buttons(); /* Проходит реальные 1000 накопительных опросов и следующий опрос фиксации порога. */
|
||||
check(bTestLamp == 1, "test activation"); /* Требует включения теста после полной выдержки. */
|
||||
for (i = 0; i < 600; i++) { /* Проверяет удержание более BLINK_TIME=500 тактов без подавления штатного медленного мигания. */
|
||||
unsigned long expected_mask; /* Сохраняет фазу теста перед очередным возмущением кнопки. */
|
||||
GpioDataRegs.GPADAT.bit.GPIO26 = 0; /* Вводит короткий провал входа на один опрос главного цикла. */
|
||||
get_Buttons(); /* Выполняет настоящий накопительный фильтр и публикацию кнопки. */
|
||||
check(bTestLamp == 1, "held test"); /* Проверяет сохранение TESTLAMP вместо мгновенного возврата рабочих масок. */
|
||||
timer_pending = 1; /* Допускает новый аппаратный такт во время передачи для проверки запрета повторного входа. */
|
||||
tick(); /* Формирует тестовый пакет при кратком провале физического входа. */
|
||||
expected_mask = isMask; /* Получает ожидаемую медленную фазу из настоящей what_is. */
|
||||
check(lamp_word == expected_mask, "test packet during input glitch"); /* Исключает рабочую маску уже горящих ламп из тестового потока. */
|
||||
check(!digit_sends || (digit_word == isNumb && digit_dot == 4), "test digit packet during input glitch"); /* Требует сохранения тестового значения и формата цифр во время помехи кнопки. */
|
||||
check(nested_entries == 0, "nested transmission"); /* Требует единственного владельца DATA/CLK на протяжении передачи. */
|
||||
timer_pending = 0; /* Убирает смоделированное событие после завершения текущего такта. */
|
||||
GpioDataRegs.GPADAT.bit.GPIO26 = 1; /* Возвращает уровень удерживаемой кнопки. */
|
||||
get_Buttons(); /* Восстанавливает насыщенный накопитель без смены тестового режима. */
|
||||
} /* Завершает серию коротких помех при удержании. */
|
||||
check(isNumb == 1111 && isMask == 0xFFFFFFFFUL, "slow test sequence"); /* После 600 миллисекунд требует единственного штатного шага what_is с периодом 500 тактов, а не ускоренного строба. */
|
||||
GpioDataRegs.GPADAT.bit.GPIO26 = 0; /* Отпускает физическую кнопку на длительное время. */
|
||||
for (i = 0; i < 1001; i++) get_Buttons(); /* Даёт накопителю полностью разрядиться и зафиксировать отпускание. */
|
||||
check(bTestLamp == 0, "test release"); /* Проверяет, что сохранение состояния не залипает в TESTLAMP. */
|
||||
tick(); /* Возвращает передачу рабочей маски после отпускания. */
|
||||
check(lamp_word == 1, "normal lamp after test"); /* Восстанавливает уже включённую до теста лампу. */
|
||||
cTestLamp = 1; /* Включает независимую удалённую команду теста через production-регистр. */
|
||||
tick(); /* Проверяет приоритет удалённого TESTLAMP при отпущенной физической кнопке. */
|
||||
check(lamp_word == isMask, "remote test"); /* Требует тестовую маску для обоих источников команды. */
|
||||
cTestLamp = 0; /* Завершает удалённый тест перед проверкой рабочего PWM. */
|
||||
Bright[0] = 6; /* Устанавливает шесть включённых тактов из десяти в production-периоде яркости. */
|
||||
for (i = 0; i < 10; i++) { /* Проверяет один полный период PWM после выхода из теста. */
|
||||
tick(); /* Даёт настоящей функции вычислить текущую фазу яркости. */
|
||||
check(lamp_word == 0 || lamp_word == 1, "normal PWM packet"); /* Исключает оставшиеся тестовые биты из рабочей маски. */
|
||||
pwm_on += lamp_word == 1; /* Измеряет реальное число включённых тактов production-PWM. */
|
||||
} /* Завершает период обычной яркости. */
|
||||
check(pwm_on == 6, "normal PWM duty"); /* Подтверждает сохранение заданной яркости 6/10 после выхода из теста. */
|
||||
printf("%s TESTLAMP: held input glitches, single ISR, normal lamps, release, remote command\n", failures ? "FAIL" : "PASS"); /* Выводит проверенные свойства без заявления о физическом испытании платы. */
|
||||
return failures ? 1 : 0; /* Возвращает результат всех поведенческих требований. */
|
||||
} /* Завершает хостовую проверку. */
|
||||
int main(void) /* Проверяет двухсекундное удержание по Timer1 и защиту стробов DATA/CLOCK от повторного входа. */
|
||||
{ /* Начинает независимые сценарии физической кнопки, удалённой команды и яркости. */
|
||||
int i, pwm_on = 0; /* Считает такты удержания и включённые фазы десятишагового PWM. */
|
||||
memset(modbus, 0, sizeof(modbus)); /* Очищает команды и входы перед запуском production-функций. */
|
||||
check(sizeof(WORDE) == sizeof(modbus[0]), "register layout"); /* Подтверждает совместимость битовых псевдонимов с массивом хостовой модели. */
|
||||
Desk = dsk_EPLT; modbus[2] = 1; Bright[0] = maximum_bright; /* Выбирает пульт и заранее включает рабочую лампу с полной яркостью. */
|
||||
entry_mask = M_INT13 | (1U << 0) | (1U << 13); /* Проверяет Timer1 вместе с ранее разрешёнными источниками разных приоритетов. */
|
||||
tick(); check(lamp_word == 1, "normal lamp before test"); /* Проверяет рабочую индикацию до начала удержания. */
|
||||
GpioDataRegs.GPADAT.bit.GPIO26 = 1; /* Нажатие TESTLAMP представлено высоким уровнем входа платы. */
|
||||
for (i = 0; i < 4000; i++) get_Buttons(); /* Частый main не должен ускорять выдержку, которую теперь считает только Timer1. */
|
||||
check(!bTestLamp, "polling independent"); /* Доказывает независимость порога от числа опросов главного цикла. */
|
||||
for (i = 0; i < 2 * READY_FREQ - 1; i++) { tick(); check(!bTestLamp, "short test"); } /* Даже 1999 тактов при 1000 Гц ещё не разрешают местный тест. */
|
||||
tick(); check(bTestLamp, "test activation"); /* Двухтысячный такт включает тест точно после двух секунд удержания. */
|
||||
for (i = 0; i < 600; i++) { /* Проверяет непрерывный тест дольше периода BLINK_TIME=500 без потери защиты стробов. */
|
||||
timer_pending = 1; tick(); /* Имитирует новый такт во время передачи, когда прежний ISR ещё не завершился. */
|
||||
check(bTestLamp && lamp_word == isMask, "held test"); /* Требует передачу тестовой фазы вместо исходной рабочей маски. */
|
||||
check(!digit_sends || (digit_word == isNumb && digit_dot == 4), "test digit packet"); /* Проверяет тестовый формат числового индикатора. */
|
||||
check(nested_entries == 0, "nested transmission"); timer_pending = 0; /* Единственный владелец DATA/CLOCK исключает перемешивание пакетов. */
|
||||
} /* Завершает длительное удержание физической кнопки. */
|
||||
GpioDataRegs.GPADAT.bit.GPIO26 = 0; tick(); /* Отпускание сбрасывает местный тест в следующем такте Timer1. */
|
||||
check(!bTestLamp && lamp_word == 1, "release"); /* Возвращает заранее включённую рабочую лампу без дополнительной задержки. */
|
||||
for (i = 0; i < 3; i++) { /* Отдельные короткие нажатия суммарно превышают две секунды, но не должны включать тест. */
|
||||
int held; GpioDataRegs.GPADAT.bit.GPIO26 = 1; /* Начинает очередное независимое нажатие. */
|
||||
for (held = 0; held < READY_FREQ; held++) tick(); /* Удерживает вход одну секунду, то есть меньше порога. */
|
||||
check(!bTestLamp, "separate presses"); /* Не позволяет сложить длительности разных нажатий. */
|
||||
GpioDataRegs.GPADAT.bit.GPIO26 = 0; tick(); /* Сбрасывает накопленную выдержку отпусканием. */
|
||||
} /* Завершает сценарий повторных коротких нажатий. */
|
||||
cTestLamp = 1; tick(); check(lamp_word == isMask, "remote test"); /* Команда Modbus сохраняет немедленный независимый запуск теста. */
|
||||
cTestLamp = 0; Bright[0] = 6; /* Возвращает рабочий режим с яркостью шесть тактов из десяти. */
|
||||
for (i = 0; i < 10; i++) { tick(); check(lamp_word <= 1, "normal PWM packet"); pwm_on += lamp_word == 1; } /* Измеряет полный период реального алгоритма яркости после теста. */
|
||||
check(pwm_on == 6, "normal PWM duty"); /* Подтверждает сохранение настроенной скважности рабочего сигнала. */
|
||||
printf("%s TESTLAMP: 2-second hold, main independent, single ISR, release, remote command, PWM\n", failures ? "FAIL" : "PASS"); /* Указывает проверенные программные свойства без заявления об аппаратном испытании. */
|
||||
return failures ? 1 : 0; /* Любое нарушенное условие завершает процесс проверки с ошибкой. */
|
||||
} /* Завершает проверку кнопки и защищённой передачи индикации. */
|
||||
|
||||
@@ -32,13 +32,13 @@ button = function(source("peripher.c"), "get_Buttons") # Проверяет и
|
||||
pult = source("pulto.c") # Берёт текущую обработку индикации, включая реальные ветви теста и яркости.
|
||||
body = "\n".join((function(source("measure.c"), "er_anal"), button, function(pult, "what_is"), function(pult, "cpu_timer1_isr_PULT"))) # Собирает функции, участвующие в переключении режима и передаче масок.
|
||||
before, after = Path(__file__).with_name("model.inc").read_text(encoding="utf-8").split("/* HANDLERS */") # Помещает production-функции между аппаратными заглушками и поведенческими сценариями.
|
||||
old_button, count_button = re.subn(r"(er_anal\(GpioDataRegs\.GPADAT\.bit\.GPIO26,\s*&INcount0,\s*1000,\s*)bTestLamp(\))", r"\g<1>0\2", body) # Возвращает сброс фильтра при одном провале только во временную мутацию.
|
||||
old_button, count_button = re.subn(r"test_lamp_hold_ticks >= 2U \* \(unsigned int\)READY_FREQ", "test_lamp_hold_ticks >= 1U", body) # Мутация включает TESTLAMP после одного такта вместо двух секунд и должна нарушить сценарий короткого нажатия.
|
||||
old_mask, count_mask = re.subn(r"IER\s*&=\s*\(MINT13\s*&\s*~M_INT13\);", "IER |= MINT13;", body) # Восстанавливает ошибочное разрешение прерываний в отдельном временном варианте.
|
||||
assert count_button == count_mask == 1, "Не найдены единственные места исправлений TESTLAMP" # Не позволяет мутации незаметно проверять неизменённый код.
|
||||
environment = dict(os.environ) # Сохраняет окружение основного процесса.
|
||||
environment["PATH"] = str(Path(args.cc).resolve().parent) + os.pathsep + environment.get("PATH", "") # Делает DLL и вспомогательные программы GCC доступными дочерним процессам.
|
||||
with tempfile.TemporaryDirectory(prefix="testlamp-") as directory: # Изолирует артефакты каждой проверки от дерева прошивки.
|
||||
for name, functions, failure in (("production", body, None), ("old_button", old_button, "FAIL held test"), ("old_mask", old_mask, "FAIL interrupt mask")): # Проверяет исправление и чувствительность сценариев к каждой из двух старых ошибок.
|
||||
for name, functions, failure in (("production", body, None), ("old_button", old_button, "FAIL short test"), ("old_mask", old_mask, "FAIL interrupt mask")): # Проверяет выдержку кнопки и чувствительность к повторному разрешению Timer1 во время передачи индикации.
|
||||
c_path, exe_path = Path(directory) / (name + ".c"), Path(directory) / (name + ".exe") # Разделяет временные исходники и программы трёх вариантов.
|
||||
c_path.write_text(before + functions + after, encoding="utf-8") # Сохраняет модель и русские пояснения в UTF-8.
|
||||
subprocess.run([args.cc, "-std=c99", "-O3", "-fno-strict-aliasing", "-Werror=implicit-function-declaration", "-I", str(ROOT / "Source/Internal/Include"), "-I", str(ROOT / "Source/External/v120/DSP2833x_common/include"), str(c_path), "-o", str(exe_path)], env=environment, check=True) # Сохраняет адреса production-псевдонимов регистров и использует реальную таблицу приоритетов TI.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "../../Source/Internal/log_to_mem.c"
|
||||
#include "../../Source/Internal/Include/ump_can_protocol.h"
|
||||
uint16_t firmware_info_static_read(unsigned index) { (void)index; return 0U; } /* Изолирует проверку старого логгера от данных сборки; реальные слова паспорта проверяются отдельным port_test.c. */
|
||||
struct mock_gpio GpioDataRegs;
|
||||
int READY = 0, Desk = 6, Mode = 5;
|
||||
int modbus[128];
|
||||
@@ -13,37 +14,76 @@ unsigned int adc_table_lem[4];
|
||||
__declspec(dllexport) int run_tests(void)
|
||||
{
|
||||
unsigned int i, generation, archived, reply[UMP_CAN_MAX_WORDS], error, length;
|
||||
/* Индексы перебирают все поля десяти записей онлайн-окна, проверяя отсутствие сдвига на старые 40 слов. */
|
||||
unsigned int record_index, word_index, block_words;
|
||||
unsigned long sequence;
|
||||
READY = 100;
|
||||
CHECK(!ump_log_write(UMP_LOG_BASE, 1));
|
||||
READY = 0;
|
||||
/* Явная инициализация кэша гарантирует нулевые ЗПТ до первого принятого измерения. */
|
||||
CHECK(ump_pm67_voltage[0] == 0 && ump_pm67_voltage[1] == 0);
|
||||
/* Регистр 306 ПМ67 передаётся нулевым CAN-индексом 305 в первой позиции кадра. */
|
||||
ump_log_can(0x8000U | 305U, 321, 0, 0);
|
||||
/* Регистр 9 ПМ67 находится во второй позиции кадра с начальным индексом 7 и маской 0x4000. */
|
||||
ump_log_can(0x4000U | 7U, 0, 456, 0);
|
||||
/* Напряжения должны обновляться ещё до включения регистратора, чтобы первый снимок уже содержал измерения. */
|
||||
CHECK(ump_pm67_voltage[0] == 321 && ump_pm67_voltage[1] == 456 && ump_count == 0);
|
||||
/* Нулевая маска не разрешает первое слово и не должна подменять сохранённое напряжение. */
|
||||
ump_log_can(305U, 999, 0, 0);
|
||||
/* Индекс 306 не является индексом регистра 306: прошивка принимает именно нулевую нумерацию CAN. */
|
||||
ump_log_can(0x8000U | 306U, 999, 0, 0);
|
||||
/* Оба неверных заголовка обязаны оставить последнее корректное ЗПТ1 без изменения. */
|
||||
CHECK(ump_pm67_voltage[0] == 321);
|
||||
/* Третья позиция соответствует индексу 305 даже при пропущенных первых двух словах кадра. */
|
||||
ump_log_can(0x2000U | 303U, 0, 0, 123);
|
||||
/* Аналогично проверяет третью позицию для нулевого индекса 8 второго напряжения. */
|
||||
ump_log_can(0x2000U | 6U, 0, 0, 789);
|
||||
/* Последний принятый набор должен совпадать с ожидаемыми ЗПТ1/ЗПТ2 в вольтах. */
|
||||
CHECK(ump_pm67_voltage[0] == 123 && ump_pm67_voltage[1] == 789);
|
||||
adc_table_lem[0] = 2048; adc_table_lem[3] = 4095;
|
||||
CHECK(!ump_log_write(UMP_LOG_BASE+1, 0));
|
||||
CHECK(!ump_log_write(UMP_LOG_BASE+1, 1001));
|
||||
CHECK(ump_log_write(UMP_LOG_BASE+1, 1));
|
||||
modbus[127] = 0x80;
|
||||
/* Дополнительные биты полного слова команд не должны попадать в компактное поле пуска/сброса v4. */
|
||||
modbus[127] = 0xAB80;
|
||||
/* Unrelated registers must not determine the fixed 166 ramp. */
|
||||
modbus[0x50] = 99; modbus[0x51] = 123;
|
||||
modbus[0x64] = 88; modbus[0x65] = 111;
|
||||
modbus[0x7B] = 1234; modbus[0x1C] = 160;
|
||||
/* Сторожевое значение во втором слоте обнаружит случайную запись старых полей за 12-словную границу. */
|
||||
logs_block[UMP_LOG_WORDS] = 0xA55A;
|
||||
/* Второй банк не должен затрагиваться заполнением и оборотом кольца первого банка. */
|
||||
logs_block[UMP_LOG_CAPACITY * UMP_LOG_WORDS] = 0x5AA5;
|
||||
CHECK(ump_log_write(UMP_LOG_BASE, 1));
|
||||
/* Число слов и чётная длина совпадают с компактным форматом исполнения 167. */
|
||||
CHECK(UMP_LOG_WORDS == 12 && 2UL * UMP_LOG_CAPACITY * UMP_LOG_WORDS <= LOG_PAGE_LEN);
|
||||
/* Первый снимок обязан содержать напряжения, принятые до запуска записи. */
|
||||
CHECK(ump_word(0, 0, 9) == 123 && ump_word(0, 0, 10) == 789);
|
||||
/* Нулевой резерв необходим для парной упаковки CAN и не должен содержать старый код ЦАП. */
|
||||
CHECK(ump_word(0, 0, 11) == 0 && logs_block[UMP_LOG_WORDS] == 0xA55A);
|
||||
/* В начале доступен один снимок, а оставшаяся часть 124-словного онлайн-окна заполнена нулями. */
|
||||
CHECK(ump_log_read(UMP_LOG_LIVE+2) == 1 && ump_log_read(UMP_LOG_LIVE+4+UMP_LOG_WORDS) == 0);
|
||||
CHECK(!ump_log_write(UMP_LOG_BASE, 1));
|
||||
CHECK(!ump_log_write(UMP_LOG_BASE+1, 20));
|
||||
for (i=0; i<900; i++) ump_log_tick();
|
||||
CHECK(ump_count == UMP_LOG_CAPACITY);
|
||||
CHECK(ump_word(0, 0, 0) == 101);
|
||||
CHECK(ump_word(0, UMP_LOG_CAPACITY-1, 0) == 900);
|
||||
CHECK(ump_word(0, 0, 8) == 1234);
|
||||
CHECK(ump_word(0, 0, 24) == 6);
|
||||
CHECK(ump_word(0, 0, 25) == 16);
|
||||
CHECK(ump_word(0, 0, 36) == 2048);
|
||||
CHECK(ump_word(0, 0, 39) == 4095);
|
||||
/* После оборота кольца поле команд содержит только биты пуска и сброса, несмотря на посторонние биты 0xAB80. */
|
||||
CHECK(ump_word(0, 0, 3) == 0x80);
|
||||
/* Расчётное задание тока переместилось в слово 6 и сохраняет масштаб 0,1 мА. */
|
||||
CHECK(ump_word(0, 0, 6) == 160);
|
||||
/* Уставки исполнения 166 остаются фиксированными 6/16 мА независимо от технологических регистров 167. */
|
||||
CHECK(ump_word(0, 0, 7) == 6 && ump_word(0, 0, 8) == 16);
|
||||
/* При обороте кольца не теряются оба напряжения и не изменяется сторожевое значение следующего банка. */
|
||||
CHECK(ump_word(0, 0, 9) == 123 && ump_word(0, 0, 10) == 789 && logs_block[UMP_LOG_CAPACITY * UMP_LOG_WORDS] == 0x5AA5);
|
||||
/* Короткая 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_word(0, ump_count-1, 18) == 0x807F);
|
||||
/* Событие CAN хранит выбранные команды в слове 3 вместо удалённого сырого заголовка в прежнем слове 18. */
|
||||
CHECK(ump_word(0, ump_count-1, 3) == 0x40);
|
||||
for (i=0; i<999; i++) ump_log_tick();
|
||||
CHECK(ump_generation == 0);
|
||||
ump_log_tick();
|
||||
@@ -59,8 +99,16 @@ __declspec(dllexport) int run_tests(void)
|
||||
CHECK(ump_word(1, UMP_LOG_CAPACITY-1, 0) == archived);
|
||||
CHECK(ump_generation == generation);
|
||||
CHECK(ump_sequence == sequence + 200);
|
||||
CHECK(ump_log_read(UMP_LOG_LIVE+2) == 3);
|
||||
CHECK(ump_log_read(UMP_LOG_LIVE+4) == ump_time-2);
|
||||
/* Уменьшение записи увеличивает глубину онлайн-окна с трёх до десяти полных снимков. */
|
||||
CHECK(ump_log_read(UMP_LOG_LIVE+2) == UMP_LOG_LIVE_RECORDS && UMP_LOG_LIVE_RECORDS == 10);
|
||||
/* Первое онлайн-время относится к самому старому из десяти переданных снимков. */
|
||||
CHECK(ump_log_read(UMP_LOG_LIVE+4) == ump_time-UMP_LOG_LIVE_RECORDS+1);
|
||||
/* Сопоставляет каждую запись онлайн-окна с хронологическим чтением кольцевого банка. */
|
||||
for (record_index = 0; record_index < UMP_LOG_LIVE_RECORDS; record_index++)
|
||||
/* Проверяет все 12 слов, включая время, выбранные сигналы и нулевой резерв. */
|
||||
for (word_index = 0; word_index < UMP_LOG_WORDS; word_index++)
|
||||
/* Смещения должны заполнять ровно 120 полезных слов после четырёх служебных слов. */
|
||||
CHECK(ump_log_read(UMP_LOG_LIVE+4+record_index*UMP_LOG_WORDS+word_index) == ump_word(0, ump_count-UMP_LOG_LIVE_RECORDS+record_index, word_index));
|
||||
CHECK(ump_log_write(UMP_LOG_BASE+3, 1));
|
||||
CHECK(ump_log_write(UMP_LOG_BASE+2, UMP_LOG_CAPACITY-1));
|
||||
CHECK(ump_log_read(UMP_LOG_DATA) == archived);
|
||||
@@ -81,7 +129,27 @@ __declspec(dllexport) int run_tests(void)
|
||||
/* CAN читает те же регистры и формирует цельный снимок из 124 слов.
|
||||
* Запись по старым технологическим адресам через сервис запрещена. */
|
||||
length = ump_can_prepare(3, UMP_LOG_BASE, 16, reply, &error);
|
||||
CHECK(!error && length == 16 && reply[0] == 0x554D && reply[1] == 2);
|
||||
/* Служебный ответ должен объявлять версию v4, длину 12 слов и сохранённую ёмкость банка исполнения 166. */
|
||||
CHECK(!error && length == 16 && reply[0] == 0x554D && reply[1] == 4 && reply[4] == 12 && reply[5] == UMP_LOG_CAPACITY);
|
||||
/* Проверяет каждое разрешённое число целых компактных записей в архивном CAN-окне. */
|
||||
for (block_words = UMP_LOG_WORDS; block_words <= 120; block_words += UMP_LOG_WORDS) {
|
||||
/* Запрашивает от одной до десяти записей, не превышая полезную длину 120 слов. */
|
||||
length = ump_can_prepare(3, UMP_LOG_DATA, block_words, reply, &error);
|
||||
/* Все кратные длине записи запросы должны проходить без исключения Modbus. */
|
||||
CHECK(!error && length == block_words);
|
||||
} /* Завершает проверку допустимых размеров окна компактного архива. */
|
||||
/* Прежний блок v2 из 40 слов теперь обрывает запись v4 и должен отклоняться CAN-протоколом. */
|
||||
ump_can_prepare(3, UMP_LOG_DATA, 40, reply, &error);
|
||||
/* Исключение 2 сообщает о недопустимом диапазоне регистров. */
|
||||
CHECK(error == 2);
|
||||
/* Отклоняет пустой архивный запрос вместо его трактовки как успешного чтения. */
|
||||
ump_can_prepare(3, UMP_LOG_DATA, 0, reply, &error);
|
||||
/* Нулевая длина не содержит ни одной целой записи v4. */
|
||||
CHECK(error == 2);
|
||||
/* Блок из 11 записей уже превышает разрешённые 120 слов полезного окна. */
|
||||
ump_can_prepare(3, UMP_LOG_DATA, 132, reply, &error);
|
||||
/* Ограничение защищает буфер ответа от записи за его границами. */
|
||||
CHECK(error == 2);
|
||||
length = ump_can_prepare(3, UMP_LOG_LIVE, 124, reply, &error);
|
||||
CHECK(!error && length == 124);
|
||||
ump_can_prepare(3, UMP_LOG_LIVE, 126, reply, &error);
|
||||
|
||||
@@ -11,21 +11,24 @@ import subprocess
|
||||
import tempfile
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--compiler', default=shutil.which('clang') or shutil.which('cl'))
|
||||
parser.add_argument('--compiler', default=shutil.which('clang') or shutil.which('cl') or shutil.which('gcc') or ('C:/msys64/mingw64/bin/gcc.exe' if Path('C:/msys64/mingw64/bin/gcc.exe').is_file() else None)) # Поддерживает имеющийся GCC для проверки нового include-пути без обязательной установки другого host-компилятора.
|
||||
args = parser.parse_args()
|
||||
if not args.compiler:
|
||||
parser.error('Provide --compiler pointing to clang.exe or cl.exe')
|
||||
parser.error('Provide --compiler pointing to clang.exe, cl.exe or gcc.exe') # Явно перечисляет поддерживаемые инструменты проверки, не меняя компилятор самой прошивки.
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
with tempfile.TemporaryDirectory(prefix='ump-recorder-') as directory:
|
||||
output = Path(directory)
|
||||
library = output / 'recorder_test.dll'
|
||||
includes = [root / 'tests/ump_logger/stubs', root / 'Source/Internal/Include']
|
||||
includes = [root / 'tests/ump_logger/stubs', root / 'Source/Internal/Include', root.parent / 'newProject/templates/c/firmware-info/include'] # Адаптер CAN теперь использует общий статический интерфейс паспорта, как production-сборка 166.
|
||||
source = root / 'tests/ump_logger/recorder_test.c'
|
||||
if Path(args.compiler).stem.lower() == 'cl':
|
||||
command = [args.compiler, '/nologo', '/LD', '/GS-', '/wd4068', '/wd4312',
|
||||
*['/I' + str(p) for p in includes], str(source),
|
||||
'/Fo' + str(output / 'recorder.obj'), '/link', '/NOENTRY',
|
||||
'/NODEFAULTLIB', '/OUT:' + str(library)]
|
||||
elif Path(args.compiler).stem.lower() in ('gcc', 'cc'): # MinGW использует обычную DLL с CRT и не принимает специальные параметры компоновщика Clang/lld.
|
||||
command = [args.compiler, '-shared', '-O3', '-Wno-unknown-pragmas', '-Wno-int-to-pointer-cast', *['-I' + str(p) for p in includes], str(source), '-o', str(library)] # Компилирует тот же существующий recorder_test.c и production-логгер без копирования его алгоритма.
|
||||
os.environ['PATH'] = str(Path(args.compiler).resolve().parent) + os.pathsep + os.environ.get('PATH', '') # Даёт дочернему Python доступ к runtime DLL выбранного MinGW только в этом тестовом процессе.
|
||||
else:
|
||||
command = [args.compiler, '-shared', '-nostdlib', '-fuse-ld=lld',
|
||||
'-Wl,/noentry,/nodefaultlib', '-Wno-unknown-pragmas',
|
||||
|
||||
Reference in New Issue
Block a user