diff --git a/Source/Internal/ecan.c b/Source/Internal/ecan.c index e755fbf..1afcb62 100644 --- a/Source/Internal/ecan.c +++ b/Source/Internal/ecan.c @@ -244,10 +244,8 @@ void InitCan(int Port, int DevNum) /* Записывает ECanMOTORegs->MOTO1 значением 550000; E CAN MOTO Regs 1. */ ECanMOTORegs->MOTO1 = 550000; - /* Записывает ECanRegs->CANTOC.all значением 1; представление регистра целым словом без разбиения на битовые поля. */ - ECanRegs->CANTOC.all = 1; - /* Записывает ECanRegs->CANTOS.all значением 0; представление регистра целым словом без разбиения на битовые поля. */ - 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; E CAN Regs CANTSC. */ ECanRegs->CANTSC = 0; // clear time-out counter @@ -313,6 +311,10 @@ void CAN_send(int Port, int data[], int Addr) /* Записывает ECanMboxes значением &ECanaMboxes; E CAN Mboxes. */ ECanMboxes = &ECanaMboxes; + /* Не меняет данные mailbox 0, пока передача либо её аппаратная отмена ещё выполняется; бит 0 соответствует штатному TX. */ + if ((ECanRegs->CANTRS.all | ECanRegs->CANTRR.all) & 1UL) + return; /* Сохраняет текущий кадр до завершения CANTRS/CANTRR, не ожидая аппаратного ответа в главном цикле. */ + /* Проверяет if(wait); последующий оператор выполняется только при истинном условии. */ if(wait) /* Проверяет if(!(ECanRegs->CANTA.all & 1)); последующий оператор выполняется только при истинном условии. */ @@ -343,8 +345,12 @@ void CAN_send(int Port, int data[], int Addr) /* Разрешение записи в защищённые регистры периферии. */ 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; @@ -487,10 +493,16 @@ interrupt void CANa_reset_err(void) /* Разрешение маскируемых прерываний ядра. */ EINT; - /* Записывает ECanaRegs.CANTRR.all значением 1; представление регистра целым словом без разбиения на битовые поля. */ - ECanaRegs.CANTRR.all = 1; - /* Изменяет счётчик в выражении CanTimeOutErrorTR++; на один для следующего шага. */ - 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; данные ACK9. */ PieCtrlRegs.PIEACK.bit.ACK9 |= 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..1ccc70a --- /dev/null +++ b/tests/can_timeout/run_test.py @@ -0,0 +1,31 @@ +"""Проверяет тайм-аут штатного CAN на настоящих функциях проекта 167.""" # Модель не заменяет испытание аппаратного 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) # Зависание и нарушенное условие делают сценарий неуспешным.