From b25e026dec6cc429abf3048933a6e67ed842d8cb Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 13 Oct 2026 19:18:10 +0300 Subject: [PATCH 1/3] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BD=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D1=81=D1=82=D1=80=D0=BE=D0=B1=D0=B0=20?= =?UTF-8?q?=D0=B8=D0=BD=D0=B4=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B8?= =?UTF-8?q?=D0=B7=20167?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Перенести настройки таймингов из 13e5bfe373904298e21483c3a1c9b71a1446b39c и относящуюся к индикации часть 04a9d989fb382cddd2e8de8ec2d25f154bce7937. Сохранить текущую последовательность DATA/CLOCK ветки 167, удерживать TESTLAMP при кратких провалах входа и исключить повторный вход Timer1. Адаптировать тесты TESTLAMP к типу накопителя и кодировкам проекта 166. Сохранить изменённые исходники и русские комментарии в UTF-8. Проверка: поведенческие и мутационные тесты TESTLAMP, полная сборка 27 модулей TI C2000 21.12.0.STS и линковка, образ SCI8 30174 байта, компиляция канала индикации с общей оптимизацией -O3. --- Source/Internal/kanal.c | 35 ++++++++--- Source/Internal/peripher.c | 27 ++++---- Source/Internal/pulto.c | 15 ++--- tests/testlamp/model.inc | 123 +++++++++++++++++++++++++++++++++++++ tests/testlamp/run_test.py | 51 +++++++++++++++ 5 files changed, 224 insertions(+), 27 deletions(-) create mode 100644 tests/testlamp/model.inc create mode 100644 tests/testlamp/run_test.py diff --git a/Source/Internal/kanal.c b/Source/Internal/kanal.c index b45382c..454de2a 100644 --- a/Source/Internal/kanal.c +++ b/Source/Internal/kanal.c @@ -1,7 +1,7 @@ -/* Ïðîãðàììíûé êàíàë èíäèêàöèè. digits/readr çàäàþò ñèìâîëû è âûáîð - * ýëåìåíòîâ, DCLK/DOUT è kanal_Send ôîðìèðóþò ïîñëåäîâàòåëüíîñòü GPIO. - * Ýòî äðóãîé èíòåðôåéñ, ÷åì dCLK/dCS â DAC.c. Ïîõîæèå èìåíà íå - * îçíà÷àþò îáùèé ÖÀÏ èëè îáùèé âûõîä çàïóñêà íàãðóçêè. +/* Программный канал индикации. digits/readr задают символы и выбор + * элементов, DCLK/DOUT и kanal_Send формируют последовательность GPIO. + * Это другой интерфейс, чем dCLK/dCS в DAC.c. Похожие имена не + * означают общий ЦАП или общий выход запуска нагрузки. */ #include "DSP2833x_Device.h" // DSP281x Headerfile Include File #include "DSP2833x_SWPrioritizedIsrLevels.h" @@ -20,6 +20,27 @@ int digits[16] = {63,6,91,79,102,109,125,7,127,111,64,0,0,0,121,0}; int readr[5] = {0x08,0x0C,0x10,0x00,0x04}; +/* Ограничивает настройки таймингов компилятором TI C2000; модель на ПК не использует эти директивы. */ +#if defined(__TI_COMPILER_VERSION__) +/* Сохраняет отдельный вызов переключения CLOCK: встраивание при -O3 сокращало удержание DATA после фронта. */ +#pragma FUNC_CANNOT_INLINE(DCLK) +/* Возвращает генерацию DCLK как в образе от 06.10.2026: уровень 1 и приоритет скорости 3 сохраняют прежнюю последовательность инструкций GPIO. */ +#pragma FUNCTION_OPTIONS(DCLK, "--opt_level=1 --opt_for_speed=3") +/* Сохраняет отдельный вызов переключения DATA, чтобы длительность импульсов не зависела от встраивания в передатчик. */ +#pragma FUNC_CANNOT_INLINE(DOUT) +/* Возвращает генерацию DOUT с прежними параметрами -O1 и --opt_for_speed=3; значения взяты из старой конфигурации CCS. */ +#pragma FUNCTION_OPTIONS(DOUT, "--opt_level=1 --opt_for_speed=3") +/* Не позволяет встроить разделитель пакетов в цикл передачи и изменить интервалы между переключениями линий. */ +#pragma FUNC_CANNOT_INLINE(RESET) +/* Сохраняет прежнюю последовательность вызовов GPIO и задержек разделителя, используя настройки старого образа. */ +#pragma FUNCTION_OPTIONS(RESET, "--opt_level=1 --opt_for_speed=3") +/* Сохраняет отдельный вызов передачи бита; протокол использует порядок и интервалы фронтов CLOCK и DATA. */ +#pragma FUNC_CANNOT_INLINE(SENDBIT) +/* Закрепляет для передачи бита настройки из 167: -O1 и --opt_for_speed=3 сохраняют интервалы GPIO независимо от общего уровня оптимизации проекта. */ +#pragma FUNCTION_OPTIONS(SENDBIT, "--opt_level=1 --opt_for_speed=3") +/* Завершает блок настроек TI, не изменяя формат пакетов и существующие счётчики задержек. */ +#endif + void DCLK(int x) { if(x) GpioDataRegs.GPASET.bit.GPIO6=1; @@ -88,7 +109,7 @@ void kanal_Send(int adr, long dat, int dot) long Word,data,aliq_part,dg[4]; int i,j,bit,byt,addr,sgn=0,punkt=0,aliq_len=0,full_len, isdot=0; - if(adr>1) // Ëàìïî÷êè + if(adr>1) // Адреса каналов выше 1 передают готовую битовую маску ламп; каналы 0 и 1 требуют преобразования числа в сегменты. { Word =dat; } @@ -139,7 +160,7 @@ void kanal_Send(int adr, long dat, int dot) for(i=3;i>0;i--) { if((dg[i]==0)&&(i!=dot)) - dg[i]=0xF; // Ýòî çíà÷èò ïóñòî + dg[i]=0xF; // Индекс 0xF таблицы digits задаёт пустой символ, скрывая ведущий ноль вне позиции десятичной точки. else break; } @@ -148,7 +169,7 @@ void kanal_Send(int adr, long dat, int dot) { if( (dg[i]==0xF)||(i==3)) { - dg[i]=0xA; // Ýòî çíà÷èò ìèíóñ + dg[i]=0xA; // Индекс 0xA таблицы digits задаёт знак минуса в свободном разряде отрицательного числа. break; } } diff --git a/Source/Internal/peripher.c b/Source/Internal/peripher.c index 1dcf311..885ffb2 100644 --- a/Source/Internal/peripher.c +++ b/Source/Internal/peripher.c @@ -1,8 +1,8 @@ -/* Âûáîð òèïà ïëàòû, íàïðàâëåíèÿ GPIO è ÷òåíèå âõîäîâ. get_Mode - * ÷èòàåò àïïàðàòíûé àäðåñ è âûáèðàåò Desk. Îäèí âûâîä â ðàçíûõ - * ðåæèìàõ ìîæåò èìåòü ðàçíîå íàçíà÷åíèå. Äëÿ íàãðóçêè START=GPIO62, - * STOP=GPIO58, RES_OUT_2=GPIO3 (àêòèâíûé íîëü). Íàïðàâëåíèÿ çàäàíû - * â GPIO_table.h; èçìåíåíèå ðàñïèíîâêè òðåáóåò ñâåðêè ñî ñõåìîé. +/* Выбор типа платы, направления GPIO и чтение входов. get_Mode + * читает аппаратный адрес и выбирает Desk. Один вывод в разных + * режимах может иметь разное назначение. Для нагрузки START=GPIO62, + * STOP=GPIO58, RES_OUT_2=GPIO3 (активный ноль). Направления заданы + * в GPIO_table.h; изменение распиновки требует сверки со схемой. */ #include "DSP2833x_Device.h" // DSP281x Headerfile Include File #include "filter_bat2.h" @@ -51,10 +51,10 @@ void get_Mode() { EALLOW; - GpioCtrlRegs.GPAMUX1.all &= 0xFF000000; // 00—11 - GpioCtrlRegs.GPAMUX2.all &= 0xFF00003F; // 19—27 - GpioCtrlRegs.GPBMUX1.all &= 0xFFFFFFC0; // 32—34 - GpioCtrlRegs.GPBMUX2.all &= 0x000FF000; // 48—53, 58—63 + GpioCtrlRegs.GPAMUX1.all &= 0xFF000000; // Обнуляет двухбитовые поля MUX для GPIO0–11 по карте регистров TMS320F28335, выбирая обычный GPIO и сохраняя назначение GPIO12–15. + GpioCtrlRegs.GPAMUX2.all &= 0xFF00003F; // Выбирает обычный GPIO для выводов 19–27: маска сохраняет поля GPIO16–18 и GPIO28–31 в двухбитовой таблице MUX. + GpioCtrlRegs.GPBMUX1.all &= 0xFFFFFFC0; // Обнуляет младшие шесть битов MUX, переводя GPIO32–34 в режим обычных входов или выходов без изменения остальных выводов банка. + GpioCtrlRegs.GPBMUX2.all &= 0x000FF000; // Сохраняет поля GPIO54–57 и переводит GPIO48–53 и GPIO58–63 в обычный GPIO согласно двухбитовой раскладке GPBMUX2. GpioCtrlRegs.GPADIR.bit.GPIO20 = 0; GpioCtrlRegs.GPADIR.bit.GPIO21 = 0; @@ -187,7 +187,7 @@ void get_Buttons() Inputs.all = butt; } -/* Íè÷åãî òàêîãî â äåéñòâèòåëüíîñòè íåò +/* Ничего такого в действительности нет if(Desk==dsk_BKST) { bSecretBt = !GpioDataRegs.GPBDAT.bit.GPIO63; InputRep1 = bSecretBt; @@ -205,7 +205,8 @@ void get_Buttons() if(Desk==dsk_EPLT) { - bTestLamp = er_anal(GpioDataRegs.GPADAT.bit.GPIO26, &INcount0, 1000, 0); + /* Сохраняет TESTLAMP до достижения порога фильтра: 1000 означает число опросов главного цикла, а bTestLamp удерживает прежний режим при кратком провале GPIO26, чтобы рабочие маски не прерывали тест индикации. */ + bTestLamp = er_anal(GpioDataRegs.GPADAT.bit.GPIO26, &INcount0, 1000, bTestLamp); if(GpioDataRegs.GPADAT.bit.GPIO27) butt +=0x0001; if(GpioDataRegs.GPADAT.bit.GPIO24) butt +=0x0002; @@ -223,7 +224,7 @@ void get_Buttons() if(GpioDataRegs.GPADAT.bit.GPIO5 ) butt +=0x0800; if(GpioDataRegs.GPBDAT.bit.GPIO48) butt +=0x1000; - if(GpioDataRegs.GPADAT.bit.GPIO10) butt +=0x2000; // Ñåêðåòíûé âõîä + if(GpioDataRegs.GPADAT.bit.GPIO10) butt +=0x2000; // Публикует активный секретный вход GPIO10 в бите 13 слова Inputs; маска 0x2000 задаёт назначенную ему позицию протокола. Inputs.all = butt; } @@ -267,4 +268,4 @@ void get_Buttons() butt += 8 * !GpioDataRegs.GPBDAT.bit.GPIO51; Jumpers = butt; -} \ No newline at end of file +} diff --git a/Source/Internal/pulto.c b/Source/Internal/pulto.c index 2f2ee91..8790b7f 100644 --- a/Source/Internal/pulto.c +++ b/Source/Internal/pulto.c @@ -1,7 +1,7 @@ -/* Ëîãèêà ïóëüòà è èíäèêàöèè. cpu_timer1_isr_PULT âûáèðàåòñÿ - * òîëüêî äëÿ dsk_EPLT; äëÿ íàãðóçêè ðàáîòàåò îáðàáîò÷èê èç measure.c. - * Lonely îòíîñèòñÿ ê àêòèâíîñòè îáìåíà, íå ê çàäåðæêå ïóñêà ÑÈÔÓ. - * Íå ñìåøèâàòü ôóíêöèè äâóõ ðåæèìîâ ïðè èçìåíåíèè îáùåãî òàéìåðà. +/* Логика пульта и индикации. cpu_timer1_isr_PULT выбирается + * только для dsk_EPLT; для нагрузки работает обработчик из measure.c. + * Lonely относится к активности обмена, не к задержке пуска СИФУ. + * Не смешивать функции двух режимов при изменении общего таймера. */ #include "DSP2833x_Device.h" // DSP2833x Headerfile Include File #include "DSP2833x_SWPrioritizedIsrLevels.h" @@ -46,7 +46,8 @@ interrupt void cpu_timer1_isr_PULT(void) EALLOW; CpuTimer1.InterruptCount++; - IER |= MINT13; // Set "global" priority + /* Оставляет только ранее разрешённые источники по таблице приоритетов TI MINT13 и сбрасывает M_INT13 — бит 12 регистра IER, запрещая повторный Timer1 и перемешивание стробов DATA/CLOCK; исходную маску восстановит IRET. */ + IER &= (MINT13 & ~M_INT13); EINT; EDIS; // This is needed to disable write to EALLOW protected registers @@ -60,8 +61,8 @@ interrupt void cpu_timer1_isr_PULT(void) toggle_ONLINE(); - toggle_RES_OUT_1(); // òèïà ýòî ãîòîâíîñòü - toggle_RES_OUT_2(); // íà âñàêèé + toggle_RES_OUT_1(); // Переключает резервный выход RES_OUT_1 в каждом такте пульта, сохраняя существующую выдачу признака активности обработчика. + toggle_RES_OUT_2(); // Переключает второй резервный выход RES_OUT_2 синхронно с тактом пульта, сохраняя существующее поведение этого режима платы. if(++Lonely > Alone) Lonely = Alone; diff --git a/tests/testlamp/model.inc b/tests/testlamp/model.inc new file mode 100644 index 0000000..4d38045 --- /dev/null +++ b/tests/testlamp/model.inc @@ -0,0 +1,123 @@ +/* Проверяет удержание TESTLAMP и выбор пакетов при уже включённых рабочих лампах. */ +#include /* Выводит конкретные нарушенные условия и итог сценария. */ +#include /* Обнуляет регистровое пространство перед началом проверки. */ +#define interrupt /* Убирает аппаратный атрибут ISR при компиляции модели на ПК. */ +typedef float FILTERBAT; /* Позволяет подключить production-заголовок с интерфейсами фильтров. */ +#include "measure.h" /* Получает настоящие READY_FREQ, BLINK_TIME, CANPOWSE и maximum_bright. */ +#include "package.h" /* Получает настоящие адреса TESTLAMP, яркости, ламп и типов плат. */ +#include "DSP2833x_SWPrioritizedIsrLevels.h" /* Использует реальную вычисляемую маску приоритетов TI MINT13. */ +typedef union { /* Моделирует слово Modbus размером хостового int, сохраняя номера битов production-псевдонимов. */ + unsigned int all; /* Обеспечивает запись целого слова по тому же смещению массива. */ + struct { /* Размещает шестнадцать технологических битов в младшей части слова. */ + unsigned int bit0:1, bit1:1, bit2:1, bit3:1, bit4:1, bit5:1, bit6:1, bit7:1; /* Сохраняет позиции кнопок и команд 0..7. */ + unsigned int bit8:1, bit9:1, bitA:1, bitB:1, bitC:1, bitD:1, bitE:1, bitF:1; /* Сохраняет позиции повторителей входов и старших команд 8..15. */ + } bit; /* Предоставляет production-псевдонимам доступ к полям регистра. */ +} WORDE; /* Завершает хостовое представление одного регистра Modbus. */ +struct gpio_bits { /* Предоставляет входы всех ветвей настоящей get_Buttons, без моделирования электрических свойств. */ + unsigned GPIO0, GPIO1, GPIO2, GPIO3, GPIO4, GPIO5, GPIO6, GPIO7, GPIO8, GPIO9, GPIO10, GPIO11; /* Представляет дискретные входы нескольких типов плат. */ + unsigned GPIO20, GPIO21, GPIO22, GPIO23, GPIO24, GPIO25, GPIO26, GPIO27; /* Представляет перемычки и вход TESTLAMP GPIO26 пульта. */ + unsigned GPIO48, GPIO49, GPIO50, GPIO51, GPIO52, GPIO53; /* Представляет входы банка B, читаемые get_Buttons. */ +}; /* Завершает перечень моделируемых входов. */ +struct gpio_reg { struct gpio_bits bit; }; /* Даёт регистрам GPADAT и GPBDAT production-форму обращения. */ +static struct { struct gpio_reg GPADAT, GPBDAT; } GpioDataRegs; /* Хранит управляемые сценарием уровни входов. */ +static struct { unsigned long all; } DigErr; /* Принимает диагностическую маску посторонней ветви SHKF. */ +static struct { unsigned long InterruptCount; } CpuTimer1; /* Учитывает вызовы настоящего обработчика пульта. */ +int modbus[129], Desk; /* Предоставляет технологические регистры и выбирает ветвь пульта. */ +long INcount0; /* Повторяет тип накопителя в peripher.c проекта 166 и параметр long * функции er_anal, чтобы модель проверяла настоящий фильтр без несовместимых указателей. */ +unsigned int CanPowse, CanGO; /* Хранит счётчик тактов выдачи CAN и признак готовности очередной передачи. */ +unsigned long isMask, Lonely; /* Хранит тестовую маску и возраст последней связи. */ +int isNumb, quaLamp = 6; /* Сохраняет тестовые цифры и число ламп из pulto.c. */ +static unsigned int IER, mask_at_eint, entry_mask, failures, sends, nested_entries, timer_pending; /* Отслеживает разрешения прерываний, пакетные вызовы и возможность вложенного Timer1. */ +static unsigned long lamp_word; /* Запоминает последний пакет шести сигнальных ламп канала 2. */ +static long digit_word; /* Сохраняет последнее значение одного из двух семисегментных индикаторов в текущем такте. */ +static int digit_dot, digit_sends; /* Запоминает режим форматирования цифр и наличие числового пакета в текущем такте. */ +#define M_INT13 0x1000U /* Повторяет бит 12 INT13 из DSP2833x_Device.h для модели регистра IER. */ +#define EALLOW ((void)0) /* Не моделирует защиту аппаратных регистров, поскольку она не влияет на рассматриваемые ветви. */ +#define EDIS ((void)0) /* Завершает заглушку защищённой записи. */ +#define EINT (mask_at_eint = IER) /* Снимает реально применённую маску в момент разрешения вложенных прерываний. */ +#define ServiceDog() ((void)0) /* Исключает аппаратный сторожевой таймер из проверки индикации. */ +#define toggle_ONLINE() ((void)0) /* Не моделирует электрический сигнал готовности ПЛИС. */ +#define toggle_RES_OUT_1() ((void)0) /* Исключает резервный GPIO из логики выбора TESTLAMP. */ +#define toggle_RES_OUT_2() ((void)0) /* Исключает второй резервный GPIO из пакетной проверки. */ +void cpu_timer1_isr_PULT(void); /* Позволяет заглушке передачи смоделировать повторное аппаратное событие Timer1. */ +static void kanal_Send(int adr, long dat, int dot) /* Записывает передаваемые данные, не подменяя проверяемый выбор режима. */ +{ /* Начинает обработку одного production-вызова передачи. */ + sends++; /* Позволяет обнаружить дополнительные вызовы передачи при повторном входе в ISR. */ + if (timer_pending && (IER & M_INT13)) { /* Моделирует новый такт Timer1 во время незаконченной передачи, только когда собственное прерывание ошибочно разрешено. */ + timer_pending = 0; /* Ограничивает искусственное событие одним вложенным вызовом. */ + nested_entries++; /* Регистрирует перемешивание обработки двух тактов. */ + cpu_timer1_isr_PULT(); /* Запускает настоящую production-функцию повторно при неверной маске. */ + } /* Завершает моделирование потенциального повторного входа. */ + if (adr == 2) lamp_word = (unsigned long)dat; /* Сохраняет именно канал сигнальных ламп, обновляемый в каждом такте. */ + if (adr < 2) { /* Записывает также передачу цифр, чтобы обнаружить переход семисегментного индикатора в рабочий режим при провале кнопки. */ + digit_word = dat; /* Сохраняет переданное production-функцией числовое значение. */ + digit_dot = dot; /* Различает тест четырёх одинаковых цифр и обычное форматирование измерений. */ + digit_sends++; /* Отмечает присутствие числового пакета в текущем такте. */ + } /* Завершает фиксацию передачи на семисегментный индикатор. */ +} /* Завершает заглушку аппаратной передачи. */ +/* HANDLERS */ +static void check(int condition, const char *name) /* Накапливает ошибки, позволяя проверить все последствия одного дефекта. */ +{ /* Начинает регистрацию одного требования. */ + if (!condition) { /* Выводит только нарушенные условия. */ + printf("FAIL %s\n", name); /* Даёт мутационному тесту устойчивое имя обнаруженного дефекта. */ + failures++; /* Делает код завершения ненулевым при любой регрессии. */ + } /* Завершает обработку ошибки. */ +} /* Завершает проверку одного требования. */ +static void tick(void) /* Моделирует сохранение и восстановление IER на границе аппаратного ISR. */ +{ /* Начинает один такт индикации. */ + IER = entry_mask; /* Возвращает исходные разрешения источников перед новым обработчиком. */ + sends = 0; /* Отделяет вызовы передачи текущего такта от предыдущих. */ + digit_sends = 0; /* Не принимает пакет предыдущего такта за обновление цифр в текущем. */ + cpu_timer1_isr_PULT(); /* Выполняет настоящий алгоритм выбора режима, PWM и пакетов. */ + 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; /* Возвращает результат всех поведенческих требований. */ +} /* Завершает хостовую проверку. */ diff --git a/tests/testlamp/run_test.py b/tests/testlamp/run_test.py new file mode 100644 index 0000000..927d724 --- /dev/null +++ b/tests/testlamp/run_test.py @@ -0,0 +1,51 @@ +"""Проверяет TESTLAMP на настоящих функциях прошивки без подключения платы.""" +import argparse # Позволяет использовать доступный GCC для модели аппаратного окружения. +import os # Передаёт компилятору PATH с его вспомогательными программами и библиотеками. +from pathlib import Path # Разрешает исходники и модель относительно каталога этого теста. +import re # Извлекает функции, убирая комментарии, которые могут содержать примеры скобок. +import subprocess # Компилирует и запускает независимые проверки с контролем результата. +import tempfile # Удаляет только временные исходники и исполняемые файлы данного запуска. + +ROOT = Path(__file__).resolve().parents[2] # Получает корень репозитория из расположения tests/testlamp. + +def source(name): # Читает production-модуль без изменения исходного файла. + data = (ROOT / "Source/Internal" / name).read_bytes() # Читает исходник без изменения его кодировки: в 166 ещё есть модули Windows-1251. + try: # Сначала использует UTF-8 для перенесённых модулей индикации. + text = data.decode("utf-8") # Строго декодирует обновлённые исходники без потери русских комментариев. + except UnicodeDecodeError: # Поддерживает старый measure.c, не изменяя его ради хостового теста. + text = data.decode("cp1251") # Читает исходную Windows-1251 только при невозможности декодирования UTF-8. + return re.sub(r"/\*.*?\*/|//[^\n]*", "", text, flags=re.S) # Убирает комментарии перед разбором вложенных блоков. + +def function(text, name): # Выбирает определение функции вместо копии её алгоритма в тесте. + match = re.search(r"(?:interrupt\s+)?(?:void|int)\s+" + name + r"\([^;{}]*\)\s*\{", text) # Отличает тело функции от прототипов и вызовов. + assert match is not None, name # Завершает проверку, если проверяемая production-функция исчезла. + end, depth = match.end(), 1 # Учитывает найденную открывающую скобку внешнего блока. + while depth: # Читает все вложенные ветви до конца определения. + depth += (text[end] == "{") - (text[end] == "}") # Находит парную закрывающую скобку. + end += 1 # Включает текущий символ в извлекаемый фрагмент. + return text[match.start():end] # Сохраняет порядок операторов настоящей функции. + +parser = argparse.ArgumentParser(description=__doc__) # Объясняет назначение отдельного запуска проверки. +parser.add_argument("--cc", default="C:/msys64/mingw64/bin/gcc.exe", help="Путь к GCC") # Повторяет установленный в этом проекте хостовый компилятор. +args = parser.parse_args() # Разбирает параметры до формирования временных файлов. +button = function(source("peripher.c"), "get_Buttons") # Проверяет именно публикацию отфильтрованной кнопки в Modbus. +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_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")): # Проверяет исправление и чувствительность сценариев к каждой из двух старых ошибок. + 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. + result = subprocess.run([str(exe_path)], env=environment, capture_output=True, text=True, timeout=20) # Ограничивает зависание и сохраняет конкретные нарушенные условия. + if failure is None: # Рабочая версия обязана пройти все сценарии. + print(result.stdout, end="") # Показывает результат поведенческих проверок текущей прошивки. + assert result.returncode == 0, result.stdout + result.stderr # Возвращает ошибку при любой регрессии TESTLAMP или рабочего режима. + else: # Старая ошибка должна воспроизводимо нарушить соответствующее требование. + assert result.returncode == 1 and failure in result.stdout, result.stdout + result.stderr # Доказывает, что тест действительно обнаруживает возвращённый дефект. + print("PASS mutation:", name, "rejected") # Сообщает об ожидаемом провале временного варианта со старой ошибкой. From 9ddea2d99a0dcaf0f8ff58697dd4ea8f6ba01242 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 13 Oct 2026 19:17:43 +0300 Subject: [PATCH 2/3] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B2=D0=BE=D1=81=D1=81=D1=82=D0=B0=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20CAN=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=20=D1=82=D0=B0=D0=B9=D0=BC-=D0=B0=D1=83=D1=82?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B0=D1=87=D0=B8=20?= =?UTF-8?q?=D0=B2=20166?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Перенесено из коммита 985804607d40b84f985eb04fc1f1b0a0dcfbe795 ветки 167. Конфликты разрешены с сохранением обработчиков проекта 166. --- Source/Internal/ecan.c | 97 ++++++++++++++++++++--------------- tests/can_timeout/model.inc | 58 +++++++++++++++++++++ tests/can_timeout/run_test.py | 31 +++++++++++ 3 files changed, 146 insertions(+), 40 deletions(-) create mode 100644 tests/can_timeout/model.inc create mode 100644 tests/can_timeout/run_test.py diff --git a/Source/Internal/ecan.c b/Source/Internal/ecan.c index f3aa798..bae99d6 100644 --- a/Source/Internal/ecan.c +++ b/Source/Internal/ecan.c @@ -1,8 +1,8 @@ -/* Êîìàíäû è òåëåìåòðèÿ eCAN. MBOX1 ïðèíèìàåò êàäðû, MBOX0 ïåðåäà¸ò - * òåõíîëîãè÷åñêèå ðåãèñòðû, MBOX2 âûäåëåí ïîä îòâåòû ëîããåðà. - * Ñëîâî 127 ïðîõîäèò UMP_commands_write íåïîñðåäñòâåííî â CAN ISR. - * Çàïðîñ àðõèâà ISR ëèøü ñòàâèò íà îáñëóæèâàíèå; ôðàãìåíòàöèÿ - * îòâåòîâ âûïîëíÿåòñÿ UMP_CAN_service èç ôîíîâîãî öèêëà. +/* Команды и телеметрия eCAN. MBOX1 принимает кадры, MBOX0 передаёт + * технологические регистры, MBOX2 выделен под ответы логгера. + * Слово 127 проходит UMP_commands_write непосредственно в CAN ISR. + * Запрос архива ISR лишь ставит на обслуживание; фрагментация + * ответов выполняется UMP_CAN_service из фонового цикла. */ #include "DSP2833x_Device.h" // DSP2833x Headerfile Include File #include "DSP2833x_SWPrioritizedIsrLevels.h" @@ -39,9 +39,9 @@ int CanTimeOutErrorTR = 0; int wait=0; -/* MBOX2 âûäåëåí ïîä îòâåòû ðåãèñòðàòîðà; øòàòíûé MBOX0 ïðîäîëæàåò - * ïåðåäàâàòü òåõíîëîãè÷åñêèå äàííûå íåçàâèñèìî. ISR òîëüêî ñòàâèò çàïðîñ, - * à ñíèìîê è îòïðàâêà êîðîòêèìè øàãàìè âûïîëíÿþòñÿ â îñíîâíîì öèêëå. */ +/* MBOX2 выделен под ответы регистратора; штатный MBOX0 продолжает + * передавать технологические данные независимо. ISR только ставит запрос, + * а снимок и отправка короткими шагами выполняются в основном цикле. */ static volatile unsigned int ump_can_pending; static volatile unsigned int ump_can_tag, ump_can_address, ump_can_value; static unsigned int ump_can_reply[UMP_CAN_MAX_WORDS]; @@ -86,17 +86,17 @@ void InitCan(int Port, int DevNum) // Required before writing the MSGIDs ECanRegs->CANME.all = 0; -// çàäàåì àäðåñ 0 àùèêa íà ïåðåäà÷ó +// задаем адрес 0 ащикa на передачу ECanMboxes->MBOX0.MSGID.all = id + 0x10 + DevNum; -// çàäàåì àäðåñ 1 àùèêa íà ïðèåì - ECanMboxes->MBOX1.MSGID.all = id + DevNum; //ïîìåíàòü!!!! 1 è 0!!! +// задаем адрес 1 ащикa на прием + ECanMboxes->MBOX1.MSGID.all = id + DevNum; //поменать!!!! 1 и 0!!! -// MBOX0 è MBOX2 ïåðåäàþò äàííûå; MBOX1 ïðèíèìàåò êîìàíäû. +// MBOX0 и MBOX2 передают данные; MBOX1 принимает команды. ECanMboxes->MBOX2.MSGID.all = id + 0x20 + DevNum; ECanRegs->CANMD.all = 0xFFFFFFFA; -// Âêëþ÷àåì òðè mailbox: òåëåìåòðèÿ, êîìàíäû è îòâåòû ëîããåðà. +// Включаем три mailbox: телеметрия, команды и ответы логгера. ECanRegs->CANME.all = 0x00000007; // Clear all TAn bits @@ -121,7 +121,7 @@ void InitCan(int Port, int DevNum) ECanRegs->CANMC.all = ECanShadow.CANMC.all; while(!ECanRegs->CANES.bit.CCE); // Wait for CCE bit to be set.. -// íàñòðèâàåì ñêîðîñòü CAN +// настриваем скорость CAN ECanShadow.CANBTC.all = ECanRegs->CANBTC.all; ECanShadow.CANBTC.bit.SJWREG=1; @@ -134,12 +134,12 @@ void InitCan(int Port, int DevNum) ECanRegs->CANMC.all = ECanShadow.CANMC.all; while(ECanRegs->CANES.bit.CCE); // Wait for CCE bit to be cleared.. -// çàäàåì òàéìàóòû äëà îæèäàíèà îòïðàâêè ïîëó÷åíèà ïîñûëêè +// задаем таймауты дла ожиданиа отправки получениа посылки ECanMOTORegs->MOTO0 = 550000; ECanMOTORegs->MOTO1 = 550000; - ECanRegs->CANTOC.all = 1; - ECanRegs->CANTOS.all = 0; // clear all time-out flags + /* До первой передачи отключает тайм-ауты всех mailbox; по SPRUI07, 13.8.18.3, сброс TOC снимает TOS и MTOF. */ + ECanRegs->CANTOC.all = 0; ECanRegs->CANTSC = 0; // clear time-out counter ECanShadow.CANGIM.all = 0; @@ -161,7 +161,7 @@ void InitCan(int Port, int DevNum) IER |= M_INT9; // Enable CPU INT EDIS; -// çàâåðøèëè íàñòðîéêó CAN àùèêîâ +// завершили настройку CAN ащиков MessageReceivedCount = 0; ErrorCount = 0; @@ -169,8 +169,8 @@ void InitCan(int Port, int DevNum) MessageTransivedCount=0; } -/* Ïðè çàíÿòîì MBOX0 âîçâðàùàåòñÿ áåç îæèäàíèÿ ACK â öèêëå. - * Ýòî îòëè÷àåòñÿ îò áëîêèðóþùèõ UART/EEPROM îïåðàöèé main. +/* При занятом MBOX0 возвращается без ожидания ACK в цикле. + * Это отличается от блокирующих UART/EEPROM операций main. */ void CAN_send(int Port, int data[], int Addr) { @@ -181,6 +181,10 @@ void CAN_send(int Port, int data[], int Addr) ECanRegs = &ECanaRegs; ECanMboxes = &ECanaMboxes; + /* Не меняет данные mailbox 0, пока передача либо её аппаратная отмена ещё выполняется; бит 0 соответствует штатному TX. */ + if ((ECanRegs->CANTRS.all | ECanRegs->CANTRR.all) & 1UL) + return; /* Сохраняет текущий кадр до завершения CANTRS/CANTRR, не ожидая аппаратного ответа в главном цикле. */ + if(wait) if(!(ECanRegs->CANTA.all & 1)) if(!(ECanRegs->CANAA.all & 1)) @@ -198,10 +202,15 @@ void CAN_send(int Port, int data[], int Addr) ECanMboxes->MBOX0.MDL.all = loword; EALLOW; + /* Отключает старый контроль mailbox 0 перед перезапуском счётчика; остальные mailbox используют собственный контроль. */ + ECanRegs->CANTOC.all = 0; + /* Записывает ECanRegs->CANTSC значением 0; E CAN Regs CANTSC. */ ECanRegs->CANTSC = 0; // clear time-out counter + /* Заново разрешает тайм-аут штатного TX с порогом MOTO0, установленным в InitCan, только для следующего кадра. */ + ECanRegs->CANTOC.all = 1UL; EDIS; - ECanRegs->CANTRS.all = 1; // çàïóñòèòü ïåðåäà÷ó + ECanRegs->CANTRS.all = 1; // запустить передачу wait=1; led1_toggle(); @@ -220,9 +229,9 @@ void Handlai(volatile struct ECAN_MBOXES * ECanMboxes) hiword = ECanMboxes->MBOX1.MDH.all; loword = ECanMboxes->MBOX1.MDL.all; - /* Ñëóæåáíûé çàïðîñ ëîããåðà íå ÿâëÿåòñÿ êîìàíäîé ÓÌÏ è ñàì íå äîëæåí - * çàïîëíÿòü æóðíàë CAN-ñîáûòèÿìè ïðè îíëàéí-÷òåíèè. Êîðîòêèå êàäðû - * îòáðàñûâàþòñÿ: íåçàïîëíåííûå áàéòû mailbox ìîãëè îñòàòüñÿ îò ïðîøëîãî. */ + /* Служебный запрос логгера не является командой УМП и сам не должен + * заполнять журнал CAN-событиями при онлайн-чтении. Короткие кадры + * отбрасываются: незаполненные байты mailbox могли остаться от прошлого. */ if ((unsigned int)(hiword >> 16) == UMP_CAN_MARKER) { if (ECanMboxes->MBOX1.MSGCTRL.bit.DLC == 8) { ump_can_tag = (unsigned int)hiword; @@ -244,10 +253,10 @@ void Handlai(volatile struct ECAN_MBOXES * ECanMboxes) Data[1] = (loword>>16) & 0xffff; Data[2] = (loword ) & 0xffff; - /* Êàæäîå ðàçðåø¸ííîå ñëîâî èìååò ñîáñòâåííûé àäðåñ. Íå îïðàøèâàåì - * Commands ïîñëå êàäðà áåçóñëîâíî: ïàêåò òåëåìåòðèè íå äîëæåí - * ïðåâðàùàòü âíóòðåííèé ñáðîñ ïàðàìåòðîâ â êîìàíäíûé ôðîíò. - * Àäðåñ 127 ïðîõîäèò ÷åðåç åäèíûé îáðàáîò÷èê ïîëíîãî ñëîâà. */ + /* Каждое разрешённое слово имеет собственный адрес. Не опрашиваем + * Commands после кадра безусловно: пакет телеметрии не должен + * превращать внутренний сброс параметров в командный фронт. + * Адрес 127 проходит через единый обработчик полного слова. */ { unsigned int i; for (i = 0; i < 3; i++, adr++) { @@ -258,7 +267,7 @@ void Handlai(volatile struct ECAN_MBOXES * ECanMboxes) } } - /* Ôèêñèðóåì ïðèìåí¸ííóþ êîìàíäó äî âûõîäà èç îáðàáîò÷èêà CAN. */ + /* Фиксируем применённую команду до выхода из обработчика CAN. */ ump_log_can((unsigned int)(hiword >> 16), Data[0], Data[1], Data[2]); led2_toggle(); } @@ -292,8 +301,16 @@ interrupt void CANa_reset_err(void) PieCtrlRegs.PIEACK.all = 0xFFFF; // Enable PIE interrupts EINT; - ECanaRegs.CANTRR.all = 1; - CanTimeOutErrorTR++; + /* Проверяет реальный тайм-аут mailbox 0: отложенное PIE-прерывание после успешного TX уже может не иметь причины. */ + if (ECanaRegs.CANTOS.all & 1UL) + { /* Обрабатывает только штатный передатчик; служебный mailbox 2 контролируется отдельно в UMP_CAN_service. */ + EALLOW; /* Разрешает изменение управляющего регистра eCAN перед снятием источника прерывания. */ + ECanaRegs.CANTOC.all = 0; /* Сброс TOC снимает TOS и MTOF согласно SPRUI07, 13.8.18.3; одной отмены TX недостаточно. */ + EDIS; /* Восстанавливает защиту регистров до выхода из ветви обработки тайм-аута. */ + if (ECanaRegs.CANTRS.all & 1UL) /* Передача могла завершиться между чтением TOS и снятием контроля. */ + ECanaRegs.CANTRR.all = 1UL; /* Запрашивает отмену только ещё занятого mailbox 0, не задерживая прерывание ожиданием CANAA. */ + CanTimeOutErrorTR++; /* Считает обнаруженный тайм-аут один раз, исключая фантомные повторные входы PIE. */ + } /* Следующая штатная передача сама заново включит TOC после сброса временного счётчика. */ PieCtrlRegs.PIEACK.bit.ACK9 |= 1; @@ -307,10 +324,10 @@ interrupt void CANa_reset_err(void) //=========================================================================== -/* Îäèí âûçîâ îáñëóæèâàåò ìàêñèìóì îäèí êàäð è íèêîãäà íå æä¸ò ACK â öèêëå. - * Ñíèìîê ôèêñèðóåòñÿ îäèí ðàç íà çàïðîñ: CAN-ôðàãìåíòû íå ñìåøèâàþò - * çíà÷åíèÿ äî/ïîñëå ïåðåêëþ÷åíèÿ êîëüöåâîãî áàíêà. Íîâûå çàïðîñû çàáèðàåì - * ïîñëå çàâåðøåíèÿ ïðåäûäóùåãî îòâåòà. GUI äåðæèò òîëüêî îäèí çàïðîñ. */ +/* Один вызов обслуживает максимум один кадр и никогда не ждёт ACK в цикле. + * Снимок фиксируется один раз на запрос: CAN-фрагменты не смешивают + * значения до/после переключения кольцевого банка. Новые запросы забираем + * после завершения предыдущего ответа. GUI держит только один запрос. */ void UMP_CAN_service(void) { unsigned int saved, tag, address, value, error; @@ -321,11 +338,11 @@ void UMP_CAN_service(void) ump_can_waiting = 0; } else if ((ECanaRegs.CANAA.all & 4UL) || ((unsigned long)CpuTimer1.InterruptCount - ump_can_sent_at > 250UL)) { - /* Íåò ïîäòâåðæäåíèÿ øèíû: îòìåíÿåì òîëüêî ñëóæåáíûé mailbox. - * GUI îáíàðóæèò íåïîëíûé îòâåò ïî òàéì-àóòó; óïðàâëåíèå MBOX0 - * è ïðè¸ì êîìàíä MBOX1 ïðîäîëæàþòñÿ. */ + /* Нет подтверждения шины: отменяем только служебный mailbox. + * GUI обнаружит неполный ответ по тайм-ауту; управление MBOX0 + * и приём команд MBOX1 продолжаются. */ ECanaRegs.CANTRR.all = 4UL; - /* Äî ôàêòè÷åñêîãî çàâåðøåíèÿ îòìåíû mailbox íå ïåðåèñïîëüçóåì. */ + /* До фактического завершения отмены mailbox не переиспользуем. */ if (!(ECanaRegs.CANAA.all & 4UL)) return; ECanaRegs.CANAA.all = 4UL; ump_can_waiting = 0; @@ -342,7 +359,7 @@ void UMP_CAN_service(void) __restore_interrupts(saved); } if (ump_can_index >= ump_can_length) return; - /* Ïåðåä ñëåäóþùèì ôðàãìåíòîì óñòóïàåì øòàòíîé òåëåìåòðèè MBOX0. */ + /* Перед следующим фрагментом уступаем штатной телеметрии MBOX0. */ if (ECanaRegs.CANTRS.all & 1UL) return; hiword = ((unsigned long)(UMP_CAN_MARKER + ump_can_index/2) << 16) | ump_can_reply_tag; loword = ((unsigned long)ump_can_reply[ump_can_index] << 16) | ump_can_reply[ump_can_index+1]; diff --git a/tests/can_timeout/model.inc b/tests/can_timeout/model.inc new file mode 100644 index 0000000..1291628 --- /dev/null +++ b/tests/can_timeout/model.inc @@ -0,0 +1,58 @@ +#include /* Задаёт размеры 16- и 32-битных типов C28x в хостовой модели. */ +#include /* Печатает конкретное нарушенное условие и итог сценариев. */ +#include /* Обнуляет модель между независимыми сценариями. */ +typedef uint16_t Uint16; /* Воспроизводит тип масок PIE, используемый обработчиком TI. */ +typedef uint32_t Uint32; /* Воспроизводит упаковку двух технологических слов в MDH/MDL. */ +#define interrupt /* Убирает только аппаратный атрибут функции при компиляции на ПК. */ +#define EALLOW ((void)0) /* Модель не воспроизводит механизм защиты регистров ядра. */ +#define EDIS ((void)0) /* Защита EALLOW проверяется сборкой настоящего модуля под TI. */ +#define EINT ((void)0) /* Входы ISR вызывает сценарий, не поток операционной системы. */ +#define DINT ((void)0) /* Отключение прерываний не требует имитации планировщика на ПК. */ +#define M_INT9 0x0100U /* Маска CPU INT9 соответствует девятой группе PIE. */ +#define MINT9 0x0100U /* Для проверки жизненного цикла сохраняет разрешение группы CAN. */ +#define MG96 0x00DFU /* Исключает повторную вложенную обработку INTx6 той же группы. */ +struct REG { uint32_t all; }; /* Представляет 32-битные регистры eCAN без зависимости от размеров long на хосте. */ +struct ECAN_REGS { struct REG CANTA, CANAA, CANTRR, CANTRS, CANTOC, CANTOS; uint32_t CANTSC; }; /* Даёт рабочим функциям реальные имена используемых регистров. */ +static volatile struct ECAN_REGS ECanaRegs; /* Позволяет наблюдать все записи production-функций при оптимизации O3. */ +struct ECAN_MBOXES { struct { struct REG MDH, MDL; } MBOX0; }; /* Моделирует только штатный передатчик, который проверяет этот тест. */ +static volatile struct ECAN_MBOXES ECanaMboxes; /* Содержимое mailbox остаётся доступным для проверок неизменности занятого кадра. */ +static struct { struct { Uint16 all; } PIEIER9; union { unsigned all; struct { unsigned reserved:8, ACK9:1; } bit; } PIEACK; } PieCtrlRegs; /* Связывает запись всей маски PIEACK с битом подтверждения девятой группы в позиции 8. */ +static Uint16 IER; /* Представляет маску разрешённых прерываний ядра. */ +static int wait, CanTimeOutErrorTR; /* Повторяет состояние штатного передатчика и счётчик обнаруженных тайм-аутов. */ +static void led1_toggle(void) {} /* Светодиод передачи не влияет на завершение аппаратной транзакции. */ +/* PRODUCTION */ +#define CHECK(condition) do { if (!(condition)) { printf("FAIL line %d: %s\n", __LINE__, #condition); return 1; } } while (0) /* Не отключает проверку условий при оптимизации или NDEBUG. */ +static void reset_model(void) { /* Начинает сценарий без остаточных запросов и подтверждений предыдущего теста. */ + memset((void *)&ECanaRegs, 0, sizeof(ECanaRegs)); /* Сбрасывает моделируемые аппаратные регистры CAN. */ + memset((void *)&ECanaMboxes, 0, sizeof(ECanaMboxes)); /* Убирает ранее переданное содержимое mailbox. */ + memset(&PieCtrlRegs, 0, sizeof(PieCtrlRegs)); /* Исключает ложное подтверждение PIE из предыдущего сценария. */ + PieCtrlRegs.PIEIER9.all = 0x0030U; IER = M_INT9; /* Разрешает оба CAN-прерывания для проверки восстановления маски ISR. */ + wait = 0; CanTimeOutErrorTR = 0; /* Повторяет начальные значения production-переменных. */ +} /* Завершает начальную установку независимого сценария. */ +int main(void) { /* Проверяет потерю ACK, фантомное прерывание, занятый mailbox и восстановление отправки. */ + int words[128] = {0}; unsigned calls; /* Хранит данные кнопок и ограничивает имитацию повторных входов ISR. */ + reset_model(); ECanaRegs.CANTOC.all = 1; ECanaRegs.CANTOS.all = 1; ECanaRegs.CANTRS.all = 5; wait = 1; /* Тайм-аут относится к mailbox 0, пока служебный mailbox 2 тоже занят. */ + for (calls = 0; calls < 4 && ECanaRegs.CANTOS.all; calls++) { /* Повторяет IRQ, пока источник не снят, с явной защитой от зависания старого кода. */ + CANa_reset_err(); /* Выполняет настоящий обработчик проекта, включая отмену TX и подтверждение PIE. */ + ECanaRegs.CANTOS.all &= ECanaRegs.CANTOC.all; /* По SPRUI07 13.8.18.3 сброс TOC аппаратно снимает TOS и MTOF; одна запись CANTRR этого не гарантирует. */ + } /* Проверяет, что обработчик освобождает процессор после первого тайм-аута. */ + CHECK(calls == 1 && ECanaRegs.CANTOS.all == 0 && CanTimeOutErrorTR == 1); /* Повторные входы без снятия источника означают блокирование основного цикла. */ + CHECK(ECanaRegs.CANTRR.all == 1 && ECanaRegs.CANTRS.all == 5); /* Отмена запрошена только для штатного TX; модель намеренно ещё не завершила аппаратную отмену. */ + CHECK(PieCtrlRegs.PIEIER9.all == 0x0030U && PieCtrlRegs.PIEACK.bit.ACK9 == 1); /* ISR сохраняет прежние разрешения обоих каналов CAN и подтверждает свою группу. */ + words[16] = 8; ECanaMboxes.MBOX0.MDH.all = 0x12345678UL; /* Подготавливает новое состояние кнопки при ещё занятом предыдущем кадре. */ + CAN_send(0, words, 16); CHECK(ECanaMboxes.MBOX0.MDH.all == 0x12345678UL); /* Не допускает перезаписи данных до завершения отмены. */ + ECanaRegs.CANTRS.all = 0; ECanaRegs.CANTRR.all = 0; ECanaRegs.CANAA.all = 1; ECanaRegs.CANTSC = 999; /* Имитирует аппаратное завершение отмены и подтверждение CANAA0. */ + CAN_send(0, words, 16); /* Проверяет повторный запуск тем же реальным передатчиком после исчезновения тайм-аута. */ + CHECK(ECanaRegs.CANTOC.all == 1 && ECanaRegs.CANTSC == 0 && ECanaRegs.CANTRS.all == 1); /* Новая передача вновь получает собственный отсчёт тайм-аута. */ + CHECK(ECanaMboxes.MBOX0.MDH.all == 0xE0100008UL && ECanaMboxes.MBOX0.MDL.all == 0); /* После восстановления передаются свежие данные R16, а не старый нулевой кадр. */ + reset_model(); ECanaRegs.CANTOC.all = 1; ECanaRegs.CANTRS.all = 1; /* Фантомное PIE-прерывание может прийти, когда начат следующий кадр без тайм-аута. */ + CANa_reset_err(); CHECK(ECanaRegs.CANTRR.all == 0 && CanTimeOutErrorTR == 0 && ECanaRegs.CANTOC.all == 1); /* Не отменяет исправную передачу при уже снятом TOS0. */ + reset_model(); ECanaRegs.CANTOC.all = 1; ECanaRegs.CANTOS.all = 1; /* Передача успела завершиться, но сценарий сохраняет зафиксированный флаг тайм-аута. */ + CANa_reset_err(); CHECK(ECanaRegs.CANTRR.all == 0 && ECanaRegs.CANTOC.all == 0); /* Снимает контроль, не отменяя уже свободный mailbox. */ + reset_model(); ECanaRegs.CANTRR.all = 1; ECanaMboxes.MBOX0.MDH.all = 0x11223344UL; /* Аппаратная отмена ещё активна, даже если CANTRS уже снят. */ + CAN_send(0, words, 16); CHECK(ECanaMboxes.MBOX0.MDH.all == 0x11223344UL && ECanaRegs.CANTRS.all == 0); /* Не запускает повторный кадр до снятия CANTRR0. */ + reset_model(); wait = 1; ECanaRegs.CANTA.all = 1; /* Проверяет обычную передачу после успешного подтверждения предыдущего кадра. */ + CAN_send(0, words, 16); CHECK(ECanaRegs.CANTRS.all == 1 && ECanaRegs.CANTOC.all == 1); /* Исправление не блокирует штатный обмен при наличии CAN ACK. */ + puts("PASS CAN timeout: IRQ released, phantom ignored, busy mailbox preserved, TX recovered"); /* Сообщает только о проверенных сценариях программной модели. */ + return 0; /* Подтверждает прохождение всех проверок оптимизированного рабочего кода. */ +} /* Завершает тест без доступа к реальному USB-CAN или питанию платы. */ diff --git a/tests/can_timeout/run_test.py b/tests/can_timeout/run_test.py new file mode 100644 index 0000000..b8e34d3 --- /dev/null +++ b/tests/can_timeout/run_test.py @@ -0,0 +1,31 @@ +"""Проверяет тайм-аут штатного CAN на настоящих функциях проекта 166.""" # Модель не заменяет испытание аппаратного eCAN. +import os # Передаёт пути библиотек GCC только дочерним процессам. +from pathlib import Path # Находит исходники независимо от рабочего каталога запуска. +import re # Удаляет комментарии и выделяет тела проверяемых функций. +import subprocess # Запускает компилятор и проверяет поведенческий результат модели. +import tempfile # Изолирует сгенерированные файлы от образов прошивки пользователя. +import sys # Позволяет проверить отдельно сохранённую старую версию ecan.c. + +root = Path(__file__).resolve().parents[2] # Корень проекта содержит рабочую реализацию eCAN. +source_path = Path(sys.argv[1]) if len(sys.argv) > 1 else root / "Source/Internal/ecan.c" # Дополнительный путь нужен для проверки чувствительности к старой ошибке. +source = re.sub(r"/\*.*?\*/|//[^\n]*", "", source_path.read_text(encoding="utf-8"), flags=re.S) # Комментарии со скобками не участвуют в выделении функций. + +def function(name): # Извлекает текущий алгоритм вместо поддержания отдельной копии в тесте. + match = re.search(r"(?:interrupt\s+)?void\s+" + name + r"\([^;{}]*\)\s*\{", source) # Отличает определение от вызова и объявления. + assert match is not None, name # Исчезновение функции должно останавливать проверку. + end, depth = match.end(), 1 # Начинает поиск после открывающей скобки тела. + while depth: # Учитывает вложенные условия до закрытия внешнего блока. + depth += (source[end] == "{") - (source[end] == "}") # Считает только скобки кода без комментариев. + end += 1 # Включает найденную закрывающую скобку в результат. + return source[match.start():end] # Сохраняет все реальные обращения к регистрам и условия. + +model = Path(__file__).with_name("model.inc").read_text(encoding="utf-8") # Загружает минимальное аппаратное окружение и сценарии потери ACK. +model = model.replace("/* PRODUCTION */", function("CAN_send") + "\n" + function("CANa_reset_err")) # Проверяет передачу и прерывание в одном жизненном цикле. +compiler = os.environ.get("CC", "C:/msys64/mingw64/bin/gcc.exe") # Использует установленный GCC, сохраняя возможность явно выбрать другой. +environment = dict(os.environ) # Не меняет окружение приложения и проекта CCS. +environment["PATH"] = str(Path(compiler).parent) + os.pathsep + environment.get("PATH", "") # Делает библиотеки MinGW доступными тестовой программе. +with tempfile.TemporaryDirectory(prefix="can-timeout-") as directory: # Удаляет только собственные временные файлы после завершения проверки. + c_path, exe_path = Path(directory) / "test.c", Path(directory) / "test.exe" # Разделяет исходник модели и исполняемый результат. + c_path.write_text(model, encoding="utf-8") # Сохраняет русские пояснения модели в требуемой кодировке. + subprocess.run([compiler, "-std=c99", "-O3", "-Wall", "-Wextra", "-Wno-unused-parameter", "-Werror", str(c_path), "-o", str(exe_path)], env=environment, check=True) # Проверяет реальные функции под оптимизацией и без скрытых предупреждений. + subprocess.run([str(exe_path)], env=environment, check=True, timeout=10) # Зависание и нарушенное условие делают сценарий неуспешным. From ee79c7a37681e3ef16524bffdebd5170ca7bc49f Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 16 Oct 2026 16:46:15 +0300 Subject: [PATCH 3/3] =?UTF-8?q?=D0=A3=D1=81=D0=BA=D0=BE=D1=80=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BE=D0=B1=D1=80=D0=B0=D1=82=D0=BD=D1=83=D1=8E=20?= =?UTF-8?q?=D1=81=D0=B2=D1=8F=D0=B7=D1=8C=20R0=E2=80=93R3=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D1=82=D1=87=D0=B8=D0=BA=D0=B0=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=9F=D0=9C67?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Перенести регистры индикации VPU в быстрый поток CAN, чтобы R3 не ожидал медленного обхода параметров. Сохранить управление светодиодами и настройки периодов EEPROM. Добавить проверку настоящих масок и планировщика: интервал R3 составляет 120 мс при быстром делителе 2 и 600 мс при делителе 10. Сохранить изменённый исходник в UTF-8. --- Source/Internal/measure.c | 112 +++++++++++++++++------------------ tests/can_vpu_rx/run_test.py | 83 ++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 56 deletions(-) create mode 100644 tests/can_vpu_rx/run_test.py diff --git a/Source/Internal/measure.c b/Source/Internal/measure.c index 4909173..187a5f3 100644 --- a/Source/Internal/measure.c +++ b/Source/Internal/measure.c @@ -1,8 +1,8 @@ -/* Èçìåðåíèÿ, äèàãíîñòèêà è ïåðèîäè÷åñêîå îáñëóæèâàíèå. ADC ISR - * ïîñòàâëÿåò âûáîðêè; òàéìåð 1 îáñëóæèâàåò èíäèêàöèþ è òåëåìåòðèþ. - * Äëÿ dsk_LOAD îí ñíà÷àëà çàâåðøàåò èìïóëüñ ÑÈÔÓ, çàòåì âûçûâàåò - * ðàñ÷¸ò ÖÀÏ è ñíèìîê ëîããåðà. ×àñòîòû íàõîäÿòñÿ â measure.h; - * ïðè èõ èçìåíåíèè òðåáóåòñÿ ïåðåñ÷èòàòü çàâèñèìûå èíòåðâàëû. +/* Измерения, диагностика и периодическое обслуживание. ADC ISR + * поставляет выборки; таймер 1 обслуживает индикацию и телеметрию. + * Для dsk_LOAD он сначала завершает импульс СИФУ, затем вызывает + * расчёт ЦАП и снимок логгера. Частоты находятся в measure.h; + * при их изменении требуется пересчитать зависимые интервалы. */ #include "DSP2833x_Device.h" // DSP2833x Headerfile Include File #include "DSP2833x_SWPrioritizedIsrLevels.h" @@ -20,13 +20,13 @@ #include "message.h" #include "log_to_mem.h" -#include // Ýòî ÷òîáû ìåðèòü àìïëèòóäó! sqrt áåç ýòîãî áóäåò êðèâ!!! +#include // Это чтобы мерить амплитуду! sqrt без этого будет крив!!! unsigned int CanPowse=CANPOWSE,CanGO=0; unsigned int Maska[2][8]; -int TPL_CANS=0; // Êîëè÷åñòâî òåìïåðàòóðíûõ êàíàëîâ +int TPL_CANS=0; // Количество температурных каналов int tpl_cans=0; int cal_addr=0; @@ -53,23 +53,23 @@ int Curr_Edge; float powK[] = { - 0.127, // ïèòàíèå 380 - 0.127, // ïèòàíèå 380 + 0.127, // питание 380В + 0.127, // питание 380В - 1.000, // òîê - 1.000, // íàïðàæåíèå + 1.000, // ток + 1.000, // напражение - 0.0076, // ïèòàíèå 31 - 0.0076, // ïèòàíèå 24 - 0.0076, // ïèòàíèå 24 - 0.0076, // ïèòàíèå 15 - 1.0000, // òåðìîäàò÷èê ìåëêîñõåìà + 0.0076, // питание 31В + 0.0076, // питание 24В + 0.0076, // питание 24В + 0.0076, // питание 15В + 1.0000, // термодатчик мелкосхема }; FILTERBAT def_FILTERBAT = DEF_FILTERBAT; -FILTERBAT adc_filter[20+4]; // ìàêñ.TPL*2 + 4 êàëèáð -FILTERBAT out_filter[20+4]; // ìàêñ.TPL*2 + 4 íàïðàæ -FILTERBAT zer_filter[4]; // 4 íàïðàæ +FILTERBAT adc_filter[20+4]; // макс.TPL*2 + 4 калибр +FILTERBAT out_filter[20+4]; // макс.TPL*2 + 4 напраж +FILTERBAT zer_filter[4]; // 4 напраж long sens_count[24+4]; @@ -79,7 +79,7 @@ float K_T1,K_T2; interrupt void cpu_timer1_isr_SENS(void); /********************************************************************/ -/* Ðàñ÷åò ìîäóëà òîêà èç ïîêàçàíèé äâóõ ôàç */ +/* Расчет модула тока из показаний двух фаз */ /********************************************************************/ float im_calc(float ia,float ib) { @@ -115,8 +115,8 @@ interrupt void cpu_timer1_isr_SENS(void) int TST; -#define err_lamp work_lamp // ýòî äëà øêàïà, -#define alm_lamp heat_lamp // ÷òîá áûëî ïîíàòíåå +#define err_lamp work_lamp // это дла шкапа, +#define alm_lamp heat_lamp // чтоб было понатнее EALLOW; CpuTimer1.InterruptCount++; @@ -140,10 +140,10 @@ interrupt void cpu_timer1_isr_SENS(void) if(Desk==dsk_LOAD) { - /* Ñíÿòü èñò¸êøèé èìïóëüñ äî ðàñ÷¸òà ÖÀÏ, íåçàâèñèìî îò main. */ + /* Снять истёкший импульс до расчёта ЦАП, независимо от main. */ UMP_pulse_tick(); Load_runner(); - /* Áàçà âðåìåíè 1 ìñ; ñíèìîê ñîäåðæèò ðåçóëüòàò òåêóùåãî øàãà ÖÀÏ. */ + /* База времени 1 мс; снимок содержит результат текущего шага ЦАП. */ ump_log_tick(); return; } @@ -156,9 +156,9 @@ interrupt void cpu_timer1_isr_SENS(void) ((Desk==dsk_COMM) && heat_lamp) ) set_RES_OUT_1(); else clear_RES_OUT_1(); - if(work_lamp) set_LED_OUT_1(); // Îíà æå err_lamp äëà øêàïà + if(work_lamp) set_LED_OUT_1(); // Она же err_lamp дла шкапа else clear_LED_OUT_1(); - if(heat_lamp) set_LED_OUT_2(); // Îíà æå alm_lamp äëà øêàïà + if(heat_lamp) set_LED_OUT_2(); // Она же alm_lamp дла шкапа else clear_LED_OUT_2(); } @@ -360,37 +360,37 @@ void Init_packMask() switch(Desk) { case dsk_EPLT: - Maska[m_SLOW][0]|= 0x000F; // Ïîëó÷åííîå - Maska[m_SLOW][6]|= 0x01FC; // ßðêîñòü ëàìï è îäèíî÷åñòâî + Maska[m_FAST][0]|= 0x000F; // Возвращает R0–R3 задатчика в быстром потоке: маска 0x000F выбирает четыре младших регистра, чтобы подтверждение R3 для ПМ67 не ожидало медленного обхода параметров. + Maska[m_SLOW][6]|= 0x01FC; // Яркость ламп и одиночество break; default: for(i=0;itpl_cans) return; -// Kun èìåííî ÷òîá ñêîìïåíñèðîâàòü íå÷åòíîñòü +// Kun именно чтоб скомпенсировать нечетность if(kun&1) Numb = (Numb-K300_2)*K_T2+ZERO; else Numb = (Numb-K300_1)*K_T1+ZERO; } @@ -706,7 +706,7 @@ sens_data[chan+8] = adc_table_tpl[chan]; error.all = 0; no_ignor = !sens_error[chan].bit.Ignor; -// Îáðûâ èëè íåèñïðàâíîñòü +// Обрыв или неисправность if(Temper==-20 || Temper==200) { error.bit.Tear = 1; @@ -714,7 +714,7 @@ sens_data[chan+8] = adc_table_tpl[chan]; } else -// Ïåðåãðåâ +// Перегрев if(((Temper>sens_hi_edge[chan]-Cooling) && (sens_error[chan].bit.Hyper)) || (Temper>sens_hi_edge[chan]) ) { @@ -724,7 +724,7 @@ sens_data[chan+8] = adc_table_tpl[chan]; } else -// Ïðåäóïðåæäåíèå ïî òåìïåðàòóðå +// Предупреждение по температуре if(Temper>sens_lo_edge[chan]) { error.bit.Over = 1; @@ -796,7 +796,7 @@ void Power_count(int chan) error.bit.Stop = 1; } } -/* Ïîâûøåííîå íàïðàæåíèå +/* Повышенное напражение if(Power > sens_hi_edge[chan]) { error.bit.Hyper = 1; diff --git a/tests/can_vpu_rx/run_test.py b/tests/can_vpu_rx/run_test.py new file mode 100644 index 0000000..dd69ac5 --- /dev/null +++ b/tests/can_vpu_rx/run_test.py @@ -0,0 +1,83 @@ +"""Проверяет частоту подтверждения 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) # Зависание либо нарушение ограничения задержки проваливает регрессию.