diff --git a/Source/Internal/Include/can_telemetry_impl.h b/Source/Internal/Include/can_telemetry_impl.h new file mode 100644 index 0000000..69b187e --- /dev/null +++ b/Source/Internal/Include/can_telemetry_impl.h @@ -0,0 +1,123 @@ +#ifndef CAN_TELEMETRY_IMPL_H /* Реализация включается только в ecan.c после аппаратных типов и переменных штатного передатчика. */ +#define CAN_TELEMETRY_IMPL_H /* Не допускает повторного определения состояния и функций в одной единице трансляции. */ +#define CAN_HEARTBEAT_ADDR 0x0100U /* Как в варианте 167, отдельный адрес heartbeat находится вне R0–R127 и служебной области логгера 0x1000. */ +#define CAN_HEARTBEAT_TICKS ((Uint32)READY_FREQ) /* READY_FREQ задаёт частоту Timer1 в герцах; это ровно одна секунда его тактов. */ +#define CAN_REFRESH_TICKS ((Uint32)READY_FREQ * 13UL / 10UL) /* Тринадцать десятых секунды дают период повторения данных 1,3 с независимо от числа циклов main. */ +#define CAN_SCAN_TICKS ((Uint32)READY_FREQ * CANPOWSE / 1000UL) /* Существующие CANPOWSE=20 мс ограничивают задержку обнаружения обновлений, но не скорость выдачи уже готовой очереди. */ +#define CAN_ACK_TICKS ((Uint32)READY_FREQ / 10UL) /* Программная страховка через 100 мс отменяет зависший TX, если аппаратный timeout IRQ не был обработан. */ +static Uint32 can_last_hi[ANSWER_LEN], can_last_lo[ANSWER_LEN], can_last_at[ANSWER_LEN]; /* Последний подтверждённый пакет и время ACK хранятся отдельно для каждого начального адреса R0–R127. */ +static unsigned int can_valid[8], can_cursor[2], can_stream; /* Восемь 16-битных масок покрывают 128 адресов; два курсора и очередность обеспечивают справедливость потоков. */ +static Uint32 can_tx_at, can_failed_at, can_heartbeat_at, can_scan_at; /* Беззнаковые отметки Timer1 поддерживают вычитание через переполнение 32-битного счётчика. */ +static unsigned int can_backoff, can_started, can_heartbeat_count, can_heartbeat_due, can_scan_due; /* Отделяет секундную паузу после ошибки, старт часов, heartbeat и незавершённое сканирование масок. */ +static int can_pending_addr; /* Значение -1 обозначает heartbeat, который не должен индексировать кэш технологических регистров. */ +static volatile unsigned int can_tx_failed; /* Timeout ISR отмечает ошибку текущего mailbox до обработки ACK в основном цикле. */ +static Uint32 CAN_millis(void) /* Возвращает согласованный счётчик Timer1; при READY_FREQ=1000 один такт соответствует миллисекунде. */ +{ /* Чтение 32 бит защищается отдельно на 16-битном C28x без ожидания аппаратуры. */ + unsigned int saved = __disable_interrupts(); /* Сохраняет исходное состояние IRQ, включая вызовы из уже защищённой отправки. */ + Uint32 now = CpuTimer1.InterruptCount; /* Счётчик обновляют оба существующих обработчика Timer1 для пульта и измерительной платы. */ + __restore_interrupts(saved); /* Немедленно возвращает прежнюю маску прерываний. */ + return now; /* Наружу передаётся один целостный снимок времени. */ +} /* Длительные расчёты масок выполняются с разрешёнными прерываниями. */ +static void CAN_telemetry_init(void) /* Сбрасывает программное состояние при каждом InitCan, в том числе после запуска с грязной RAM. */ +{ /* Крупные массивы не очищаются, потому что их элементы недоступны без valid-бита. */ + unsigned int i; /* Перебирает восемь масок достоверности кэша. */ + for (i = 0U; i < 8U; ++i) can_valid[i] = 0U; /* Первый пакет каждого разрешённого адреса обязательно передаётся после запуска. */ + can_cursor[0] = can_cursor[1] = can_stream = 0U; /* Обе маски начинают обход с R0; далее потоки чередуются после реально поставленного кадра. */ + can_backoff = can_started = can_heartbeat_count = can_heartbeat_due = 0U; /* Новые часы и секундный heartbeat начинают отсчёт при первом обслуживании main. */ + can_scan_due = 1U; /* Первый снимок данных выдаётся сразу, без ожидания периода 1,3 с. */ + can_pending_addr = -1; can_tx_failed = 0U; wait = 0; /* Исключает подтверждение кадра и ошибку из предыдущего запуска программы. */ +} /* Сброс не меняет аппаратные CAN ID, маски регистров и настройки EEPROM. */ +static int CAN_tx_ready(Uint32 now) /* Обслуживает завершение штатного mailbox 0; вызывающий код уже запретил IRQ на короткое время. */ +{ /* Возвращает готовность без ожидания ACK, CANAA или освобождения шины в цикле. */ + if ((ECanaRegs.CANTRS.all | ECanaRegs.CANTRR.all) & 1UL) { /* Бит 0 относится только к штатному TX; служебный mailbox 2 не перезаписывается. */ + if (wait && !can_tx_failed && (ECanaRegs.CANTRS.all & 1UL) && now - can_tx_at >= CAN_ACK_TICKS) { /* Программный timeout страхует потерянное аппаратное прерывание спустя 100 мс. */ + can_tx_failed = 1U; ++CanTimeOutErrorTR; /* Отмечает ошибку ровно один раз, пока этот запрос ещё ожидает завершения отмены. */ + EALLOW; ECanaRegs.CANTOC.all &= ~1UL; EDIS; /* Снимает источник аппаратного timeout только для mailbox 0, сохраняя остальные биты контроля. */ + ECanaRegs.CANTRR.all = 1UL; /* Запрашивает аппаратную отмену без блокирующего ожидания CANAA. */ + } /* Данные занятого mailbox остаются неизменными до аппаратного освобождения. */ + return 0; /* Текущий адрес и признак готовности сканирования сохраняются для повторного обслуживания. */ + } /* Только свободный mailbox позволяет обработать завершение предыдущей передачи. */ + if (wait) { /* ACK или ошибка относятся ровно к одному ранее запущенному кадру. */ + if ((ECanaRegs.CANTA.all & 1UL) && !can_tx_failed) { /* Только успешный ACK позволяет подавлять повтор неизменившихся данных. */ + if (can_pending_addr >= 0) { /* Heartbeat не занимает ограниченный технологический кэш. */ + can_last_hi[can_pending_addr] = ECanaMboxes.MBOX0.MDH.all; /* Запоминает действительно доставленные адрес и первое слово из неизменявшегося mailbox. */ + can_last_lo[can_pending_addr] = ECanaMboxes.MBOX0.MDL.all; /* Сохраняет второе и третье слова того же подтверждённого кадра. */ + can_last_at[can_pending_addr] = now; /* Отсчитывает повтор через 1,3 с от последнего подтверждения, а не от неудачной попытки. */ + can_valid[can_pending_addr >> 4] |= 1U << (can_pending_addr & 15); /* Устанавливает один бит достоверности начального адреса пакета. */ + } /* Перекрывающиеся пакеты разных масок имеют независимые начальные адреса. */ + } else { /* Отмена, ошибка или свободный TX без CANAA не подтверждают доставку текущих данных. */ + can_failed_at = now; can_backoff = 1U; /* Ограничивает новые попытки одной секундой при отсутствии получателя и CAN ACK. */ + } /* Старый подтверждённый кэш сохраняется, поэтому неудачная передача не теряет обновление. */ + wait = 0; can_tx_failed = 0U; can_pending_addr = -1; /* Завершение обрабатывается один раз даже без отдельного CANAA после отмены. */ + } /* Проверяет паузу после ошибки до подготовки следующего кадра. */ + if (can_backoff && now - can_failed_at < CAN_HEARTBEAT_TICKS) return 0; /* Во время секундной паузы данные и heartbeat остаются ожидающими. */ + can_backoff = 0U; /* Снятие паузы не требует изменения таймеров технологических данных. */ + return 1; /* Mailbox свободен и может принять один новый актуальный кадр. */ +} /* Чужие mailbox, аппаратный CAN ID и получаемые команды не меняются. */ +int CAN_send(int Port, int data[], int Addr) /* Возвращает 0 при занятости, 1 после запуска TX и 2 при неизменном пакете до срока повторения. */ +{ /* Снимок данных и запуск mailbox защищаются одним коротким запретом IRQ. */ + Uint32 hiword, loword, now; /* Ровно два 32-битных поля соответствуют восьми октетам существующего формата BALZAM. */ + unsigned int saved = __disable_interrupts(); /* Исключает разрыв трёхсловного снимка и гонку запуска с timeout ISR. */ + (void)Port; now = CAN_millis(); /* Как прежний передатчик, использует eCAN-A; время берётся из общего Timer1. */ + if (!CAN_tx_ready(now)) { __restore_interrupts(saved); return 0; } /* При занятости ничего не перезаписывает и не подтверждает данные программно. */ + if (Addr == CAN_HEARTBEAT_ADDR) { /* Отдельный пакет состояния узла не обращается к data=NULL или к R256. */ + hiword = 0xE0000000UL | ((Uint32)CAN_HEARTBEAT_ADDR << 16) | can_heartbeat_count; /* Сохраняет маркер BALZAM и передаёт циклический 16-битный счётчик секунд. */ + loword = ((Uint32)(unsigned int)Mode << 16) | (unsigned int)Desk; /* Два следующих слова сообщают аппаратный Mode и тип платы Desk. */ + can_pending_addr = -1; /* Успешный heartbeat не подтверждает ни один технологический пакет. */ + } else { /* Первый снимок, изменение любого слова либо истечение периода вызывают одинаковую отправку данных. */ + if (Addr < 0 || Addr >= ANSWER_LEN) { __restore_interrupts(saved); return 2; } /* Не допускает чтения за картой R0–R127 и позволяет планировщику пропустить неверный адрес. */ + hiword = 0xE0000000UL | ((Uint32)Addr << 16) | ((Uint32)data[Addr] & 0xFFFFUL); /* Первый регистр занимает младшие шестнадцать бит старшей половины CAN-данных. */ + loword = Addr + 1 < ANSWER_LEN ? ((Uint32)data[Addr + 1] & 0xFFFFUL) << 16 : 0UL; /* Отсутствующее второе слово у конца карты заменяется нулём. */ + if (Addr + 2 < ANSWER_LEN) loword |= (Uint32)data[Addr + 2] & 0xFFFFUL; /* Третье слово дополняется только в пределах реального массива регистров. */ + if ((can_valid[Addr >> 4] & (1U << (Addr & 15))) && can_last_hi[Addr] == hiword && can_last_lo[Addr] == loword && now - can_last_at[Addr] < CAN_REFRESH_TICKS) { /* Совпадение всех слов подавляет передачу только до индивидуального срока 1,3 с. */ + __restore_interrupts(saved); return 2; /* Не сдвигает время подтверждения: неизменные данные всё равно повторятся по таймеру. */ + } /* Изменение данных не ждёт окончания периода повторения. */ + can_pending_addr = Addr; /* Подготовленное состояние попадёт в кэш только после успешного аппаратного ACK. */ + } /* Для heartbeat и данных используется один и тот же безопасный жизненный цикл TX. */ + ECanaRegs.CANTA.all = 1UL; ECanaRegs.CANAA.all = 1UL; /* Очищает старые ACK и отмену mailbox 0 записью единиц в соответствующие биты eCAN. */ + ECanaMboxes.MBOX0.MDH.all = hiword; ECanaMboxes.MBOX0.MDL.all = loword; /* Записывает замороженный трёхсловный снимок без последующей подмены во время ожидания ACK. */ + EALLOW; ECanaRegs.CANTOC.all &= ~1UL; ECanaRegs.CANTSC = 0UL; ECanaRegs.CANTOC.all |= 1UL; EDIS; /* Перезапускает аппаратный timeout только для нового штатного запроса TX. */ + can_tx_at = now; can_tx_failed = 0U; wait = 1; /* Сохраняет момент запуска и принадлежность будущего ACK до разрешения прерываний. */ + ECanaRegs.CANTRS.all = 1UL; /* Однократно запускает аппаратную передачу в штатном mailbox 0. */ + led1_toggle(); __restore_interrupts(saved); /* Сохраняет индикацию передачи и возвращает исходную маску IRQ без ожидания шины. */ + return 1; /* Подтверждает только постановку кадра, оставляя окончательную доставку аппаратному ACK. */ +} /* Формат CAN ID, порядок слов и служебный mailbox логгера остаются прежними. */ +void CAN_heartbeat_service(void) /* Обслуживает независимый секундный heartbeat и завершение ранее запущенного TX. */ +{ /* Вызывается из main и не зависит от Cancount, масок данных или признака CanGO. */ + Uint32 now = CAN_millis(), elapsed, seconds; /* Время Timer1 учитывает задержки main без накопления дрейфа секундной сетки. */ + unsigned int saved = __disable_interrupts(); /* Короткая обработка ACK синхронизируется с timeout ISR. */ + CAN_tx_ready(now); __restore_interrupts(saved); /* Сохраняет успешное завершение данных даже тогда, когда новых пакетов пока нет. */ + if (!can_started) { can_started = 1U; can_heartbeat_at = can_scan_at = now; } /* Первый heartbeat появляется спустя полную секунду от начала обслуживания текущего запуска. */ + elapsed = now - can_heartbeat_at; /* Беззнаковое вычитание корректно при переполнении 32-битного счётчика. */ + if (elapsed >= CAN_HEARTBEAT_TICKS) { /* Формирует новую отметку на каждой секундной границе Timer1. */ + seconds = elapsed / CAN_HEARTBEAT_TICKS; can_heartbeat_at += seconds * CAN_HEARTBEAT_TICKS; /* После паузы main оставляет последнее значение без очереди устаревших кадров. */ + can_heartbeat_count = (unsigned int)((can_heartbeat_count + seconds) & 0xFFFFUL); can_heartbeat_due = 1U; /* Счётчик heartbeat циклически проходит диапазон 0..65535 секунд. */ + } /* Занятый передатчик не теряет ожидающий heartbeat. */ + if (can_heartbeat_due && CAN_send(0, 0, CAN_HEARTBEAT_ADDR) == 1) can_heartbeat_due = 0U; /* Приоритетно ставит один актуальный heartbeat после освобождения mailbox. */ +} /* Следующий повтор heartbeat планируется секундной сеткой, а не длительностью обхода масок. */ +void CAN_telemetry_service(void) /* Передаёт изменившиеся данные и повторяет каждый подтверждённый пакет через 1,3 с. */ +{ /* Фоновое обслуживание не ждёт CAN ACK и выдаёт готовую очередь по мере освобождения mailbox. */ + Uint32 now; unsigned int attempt, stream, pass, address, cursor; int result; /* Локальные счётчики ограничивают обход двумя потоками и конечной картой из 128 регистров. */ + CAN_heartbeat_service(); now = CAN_millis(); /* Секундный heartbeat получает приоритет перед обычными данными независимо от настройки потоков. */ + if (now - can_scan_at >= CAN_SCAN_TICKS) { can_scan_at += (now - can_scan_at) / CAN_SCAN_TICKS * CAN_SCAN_TICKS; can_scan_due = 1U; } /* Сохраняет сетку обнаружения изменений с интервалом 20 мс без накопления дрейфа после задержек main. */ + if (!can_scan_due) return; /* При неизменных данных main не повторяет полный обход масок на каждой итерации. */ + for (attempt = 0U; attempt < 2U; ++attempt) { /* После успешной постановки чередует потоки, исключая голодание медленной маски. */ + stream = (can_stream + attempt) & 1U; cursor = can_cursor[stream]; /* Начинает следующий поиск после последнего принятого кадра данного потока. */ + if (!Cancount[stream]) continue; /* Ноль сохраняет штатное отключение соответствующего потока; ненулевой период заменяется общим повтором 1,3 с. */ + for (pass = 0U; pass < 2U; ++pass) { /* Два конечных прохода реализуют циклический обход без зависания на пустой маске. */ + for (address = 0U; address < ANSWER_LEN;) { /* Формирует те же трёхсловные группы от первого разрешённого адреса маски. */ + if (!(Maska[stream][address >> 4] & (1U << (address & 15)))) { ++address; continue; } /* Пропускает запрещённые регистры и гарантированно продвигает поиск. */ + if ((pass == 0U && address >= cursor) || (pass == 1U && address < cursor)) { /* Сначала рассматривает хвост карты после курсора, затем оставшееся начало. */ + result = CAN_send(0, modbus, address); /* Сравнивает полный снимок с подтверждённым кэшем и индивидуальным временем повтора. */ + if (result != 2) { /* Только неизменный пакет до срока допускает переход к следующему кандидату в этом же вызове. */ + if (result == 1) { can_cursor[stream] = address + 3U < ANSWER_LEN ? address + 3U : 0U; can_stream = stream ^ 1U; } /* Продвигает курсор лишь после постановки кадра и отдаёт следующий свободный слот другому потоку. */ + return; /* При занятости сохраняет адрес; при постановке продолжит готовую очередь следующей итерацией main после ACK. */ + } /* Пропущенные неизменные пакеты не занимают физическую шину. */ + } /* Завершает проверку одного допустимого начального адреса. */ + address += 3U; /* Как прежний протокол, один кадр покрывает три последовательных регистра независимо от разреженности маски. */ + } /* Максимальный адрес ограничен размером реальной технологической карты. */ + } /* После обхода обеих частей все пакеты потока рассмотрены ровно один раз. */ + } /* Ни один разрешённый пакет не требует немедленной передачи. */ + can_scan_due = 0U; /* Следующее плановое сканирование произойдёт через 20 мс; выдача готовой очереди этим ожиданием не задерживается. */ +} /* Период 1,3 с относится к каждому пакету, а не к одному кадру всей маски за один период. */ +#endif /* Закрывает единственную реализацию фоновой телеметрии текущей прошивки 166. */ diff --git a/Source/Internal/Include/ecan.h b/Source/Internal/Include/ecan.h index bf17d0a..ab0a61d 100644 --- a/Source/Internal/Include/ecan.h +++ b/Source/Internal/Include/ecan.h @@ -1,9 +1,11 @@ -/* Òåëåìåòðèÿ eCAN è ôîíîâàÿ âûäà÷à îòâåòîâ ëîããåðà. - * UMP_CAN_service âûçûâàåòñÿ èç main, íå èç ïðè¸ìíîãî IRQ. +/* Телеметрия eCAN и фоновая выдача ответов логгера. + * UMP_CAN_service вызывается из main, не из приёмного IRQ. */ void InitCan(int Port, int DevNum); -void CAN_send(int Port, int data[], int Addr); +int CAN_send(int Port, int data[], int Addr); /* 0: занят, 1: кадр поставлен, 2: подтверждённые данные не изменились и 1,3 с ещё не прошли. */ +void CAN_heartbeat_service(void); /* Планирует отдельный heartbeat по Timer1 с периодом одна секунда. */ +void CAN_telemetry_service(void); /* В main обслуживает heartbeat, изменения данных и повтор каждого разрешённого пакета через 1,3 с. */ extern int CAN_input_data[]; -/* Íåáëîêèðóþùàÿ ïåðåäà÷à îòâåòîâ ëîããåðà îòäåëüíûì CAN mailbox. */ +/* Неблокирующая передача ответов логгера отдельным CAN mailbox. */ void UMP_CAN_service(void); diff --git a/Source/Internal/ecan.c b/Source/Internal/ecan.c index f3aa798..f2ab448 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]; @@ -49,6 +49,8 @@ static unsigned int ump_can_length, ump_can_index, ump_can_reply_tag; static unsigned int ump_can_waiting; static unsigned long ump_can_sent_at; +#include "can_telemetry_impl.h" /* Определяет heartbeat, подтверждённый кэш и повтор данных через 1,3 с после объявления состояния штатного TX. */ + void InitCan(int Port, int DevNum) { struct ECAN_REGS ECanShadow; @@ -59,6 +61,7 @@ void InitCan(int Port, int DevNum) long id = 0x80BA0000; + CAN_telemetry_init(); /* Сбрасывает кэш, очередь и секундный heartbeat до разрешения аппаратных прерываний. */ DevNum--; if(DevNum<0)DevNum=0; if(DevNum>15)DevNum=15; @@ -86,17 +89,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 +124,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,7 +137,7 @@ 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; @@ -161,7 +164,7 @@ void InitCan(int Port, int DevNum) IER |= M_INT9; // Enable CPU INT EDIS; -// çàâåðøèëè íàñòðîéêó CAN àùèêîâ +// завершили настройку CAN ащиков MessageReceivedCount = 0; ErrorCount = 0; @@ -169,43 +172,10 @@ 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) -{ - unsigned long hiword,loword; - volatile struct ECAN_REGS * ECanRegs; - volatile struct ECAN_MBOXES * ECanMboxes; - - ECanRegs = &ECanaRegs; - ECanMboxes = &ECanaMboxes; - - if(wait) - if(!(ECanRegs->CANTA.all & 1)) - if(!(ECanRegs->CANAA.all & 1)) - return; - - ECanRegs->CANTA.all = 1; - ECanRegs->CANAA.all = 1; - - hiword= ((((Uint32) Addr ) & 0xffff)<<16)| 0xE0000000 | - ((((Uint32)data[Addr ]) & 0xffff) ); - loword= ((((Uint32)data[Addr+1]) & 0xffff)<<16)| - ((((Uint32)data[Addr+2]) & 0xffff) ); - - ECanMboxes->MBOX0.MDH.all = hiword; - ECanMboxes->MBOX0.MDL.all = loword; - - EALLOW; - ECanRegs->CANTSC = 0; // clear time-out counter - EDIS; - - ECanRegs->CANTRS.all = 1; // çàïóñòèòü ïåðåäà÷ó - - wait=1; - led1_toggle(); -} +/* CAN_send определён общим для этой единицы трансляции блоком can_telemetry_impl.h выше. */ void Handlai(volatile struct ECAN_MBOXES * ECanMboxes) @@ -220,9 +190,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 +214,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 +228,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 +262,17 @@ 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 &= ~1UL; /* Снимает timeout только mailbox 0, сохраняя контроль служебных передач. */ /* Сброс TOC снимает TOS и MTOF согласно SPRUI07, 13.8.18.3; одной отмены TX недостаточно. */ + EDIS; /* Восстанавливает защиту регистров до выхода из ветви обработки тайм-аута. */ + if (ECanaRegs.CANTRS.all & 1UL) /* Передача могла завершиться между чтением TOS и снятием контроля. */ + ECanaRegs.CANTRR.all = 1UL; /* Запрашивает отмену только ещё занятого mailbox 0, не задерживая прерывание ожиданием CANAA. */ + can_tx_failed = 1U; /* Не допускает записи повреждённого или отменённого кадра в подтверждённый кэш телеметрии. */ + CanTimeOutErrorTR++; /* Считает обнаруженный тайм-аут один раз, исключая фантомные повторные входы PIE. */ + } /* Следующая штатная передача сама заново включит TOC после сброса временного счётчика. */ PieCtrlRegs.PIEACK.bit.ACK9 |= 1; @@ -307,10 +286,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 +300,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 +321,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/Source/Internal/main.c b/Source/Internal/main.c index 1e9c576..4c5ad16 100644 --- a/Source/Internal/main.c +++ b/Source/Internal/main.c @@ -1,8 +1,8 @@ -/* Èíèöèàëèçàöèÿ è ôîíîâûé öèêë ïëàòû. Ïîñëå íàñòðîéêè GPIO, îáìåíà, - * ïàðàìåòðîâ è ÖÀÏ UMP_pulse_init ðàçðåøàåò ñâåæèå êîìàíäû ÑÈÔÓ. - *  main îñòàþòñÿ òåëåìåòðèÿ, EEPROM è ðàçáîð ãîòîâûõ RS-ïàêåòîâ. - * CAN-ïóñê è îêîí÷àíèå èìïóëüñà ðàáîòàþò â IRQ, íå îæèäàÿ main. - * RS-êîìàíäà åù¸ äîëæíà ïðîéòè ïðîâåðêó CRC â get_command èç main. +/* Инициализация и фоновый цикл платы. После настройки GPIO, обмена, + * параметров и ЦАП UMP_pulse_init разрешает свежие команды СИФУ. + * В main остаются телеметрия, EEPROM и разбор готовых RS-пакетов. + * CAN-пуск и окончание импульса работают в IRQ, не ожидая main. + * RS-команда ещё должна пройти проверку CRC в get_command из main. */ #include "DSP2833x_Device.h" // DSP2833x Headerfile Include File @@ -43,8 +43,7 @@ void timer_Init() void main() { - int i,j,mask; - static unsigned int cancount[2], cancell[2]; + int i,j; /* Счётчики остались для прежних операций main; выбор CAN-пакетов перенесён в независимый сервис. */ RS_DATA * rs; int presbyt=0; @@ -100,7 +99,7 @@ void main() if(Desk==dsk_LOAD) Setup_DAC_time(); - /* Çàâåðøåíà íà÷àëüíàÿ íàñòðîéêà: òîëüêî òåïåðü ðàçðåøàåì äèñêðåòû. */ + /* Завершена начальная настройка: только теперь разрешаем дискреты. */ UMP_pulse_init(); LastMode = Mode; @@ -110,32 +109,9 @@ void main() while(1) { - /* ×òåíèå ëîããåðà ïî CAN íå áëîêèðóåò öèêë óïðàâëåíèÿ. */ + /* Чтение логгера по CAN не блокирует цикл управления. */ UMP_CAN_service(); - if(CanGO) - { - CanGO=0; - for(i=0;i<2;i++) - if(Cancount[i]) - if(++cancount[i] >= Cancount[i]) - { - cancount[i] = 0; - while(1) - { - if(cancell[i] >= 0x80) cancell[i]=0; - mask = Maska[i][cancell[i]/16] >> (cancell[i]%16); - if(!mask) cancell[i] = (cancell[i] + 0x10) & 0xFFF0; - else - { - while(!(mask & 1)) - { - cancell[i]++; mask >>= 1; - } - break; - } } - CAN_send(0,modbus,cancell[i]); - cancell[i]+=3; - } } + CAN_telemetry_service(); /* Обслуживает heartbeat 1 Гц, изменения масок данных и индивидуальные повторы 1,3 с без ожидания CAN ACK. */ if(cDefParam) { @@ -169,10 +145,10 @@ void main() presbyt = bTermoCal; } - /* START/STOP èñïîëíÿþòñÿ ïðè ïðè¸ìå CAN/RS, ñíèìàþòñÿ òàéìåðîì. - * Çäåñü áîëüøå íåò îïðîñà óðîâíÿ: main ìîæåò îæèäàòü EEPROM. */ + /* START/STOP исполняются при приёме CAN/RS, снимаются таймером. + * Здесь больше нет опроса уровня: main может ожидать EEPROM. */ - /* Ñíèìîê ïîñëå îáíîâëåíèÿ âõîäîâ è ôîðìèðîâàíèÿ ïóñêîâûõ âûõîäîâ. */ + /* Снимок после обновления входов и формирования пусковых выходов. */ ump_log_io(); for(i=0;i<2;i++) @@ -188,16 +164,16 @@ void main() led2_toggle(); switch(j) { - case CMD_INIT: init(rs); break; // íà÷àëüíûå óñòàíîâêè - case CMD_INITLOAD: initload(rs); break; // íàñòðîéêà çàãðóçêè - case CMD_RUN: run(rs); break; // çàãðóçèòü áëîê - case CMD_LOAD: load(rs); break; // çàãðóçèòü áëîê - case CMD_PEEK: peek(rs); break; // ïðî÷èòàòü à÷åéêó ïàìàòè - case CMD_POKE: poke(rs); break; // çàïèñàòü â à÷åéêó ïàìàòè - case CMD_UPLOAD: upload(rs); break; // ïåðåäàòü áëîê ïàìàòè - case CMD_EXTEND: extendbios(rs); break; // ðàñøèðåííûå êîìàíäû äëà áèîñà + case CMD_INIT: init(rs); break; // начальные установки + case CMD_INITLOAD: initload(rs); break; // настройка загрузки + case CMD_RUN: run(rs); break; // загрузить блок + case CMD_LOAD: load(rs); break; // загрузить блок + case CMD_PEEK: peek(rs); break; // прочитать ачейку памати + case CMD_POKE: poke(rs); break; // записать в ачейку памати + case CMD_UPLOAD: upload(rs); break; // передать блок памати + case CMD_EXTEND: extendbios(rs); break; // расширенные команды дла биоса - case CMD_TFLASH: tflash(rs); break; // ïðîøèòü TMS + case CMD_TFLASH: tflash(rs); break; // прошить TMS case CMD_MODBUS_3: ReceiveCommandModbus3(rs); break; case CMD_MODBUS_6: ReceiveCommandModbus6(rs); break; diff --git a/UKSSTMS320F28335/README.md b/UKSSTMS320F28335/README.md index c3ffea9..5be4b48 100644 --- a/UKSSTMS320F28335/README.md +++ b/UKSSTMS320F28335/README.md @@ -73,3 +73,25 @@ SetGUI проверено программно; обмен новой проши Конфигурация **Release** перенесена из 167 без изменений и не проверялась; для сборки прошивки используйте **Debug**. + +## Телеметрия CAN: heartbeat, изменения и повтор 1,3 с + +Для всех Mode технологические пакеты выбранных масками регистров R0–R127 +передаются при первом снимке, изменении любого из трёх слов или через 1300 мс +после последнего успешного CAN ACK этого пакета. Неудачная попытка не обновляет +подтверждённый снимок. Конечные пакеты карты дополняются нулями за R127. +Сканирование изменений идёт по Timer1 с шагом 20 мс; готовая очередь выдаётся +по мере освобождения mailbox, без ожидания нового периода между её кадрами. +Фактическую отправку могут задержать занятая шина, ошибки и работа main. + +Heartbeat отправляется независимо раз в секунду: адрес `0x0100`, затем три +16-битных слова — счётчик секунд, Mode, Desk. После задержки main отправляется +последнее значение, без очереди пропущенных секундных кадров. + +Ноль в `Cancount[0]` или `Cancount[1]` по-прежнему отключает данные соответствующей +маски. Любое ненулевое значение включает новый режим изменения/1300 мс; +прежний числовой делитель периода больше не используется. Heartbeat остаётся +включённым при отключённых потоках данных. Параметры EEPROM не переносятся. +После ошибки TX новая попытка разрешается через секунду без ожидания в цикле. + +Проверка без CAN-адаптера: `python tests/can_telemetry/run_test.py`. diff --git a/tests/can_telemetry/model.c b/tests/can_telemetry/model.c new file mode 100644 index 0000000..2c82498 --- /dev/null +++ b/tests/can_telemetry/model.c @@ -0,0 +1,107 @@ +#include /* Фиксирует ширину таймера и аппаратных полей eCAN независимо от разрядности host-компилятора. */ +#include /* Выводит точную строку нарушенного сценария вместо молчаливого неверного результата. */ +#include /* Изолирует независимые случаи очисткой модели аппаратуры и данных. */ +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-адаптера и не отправляет команды реальному оборудованию. */ diff --git a/tests/can_telemetry/run_test.py b/tests/can_telemetry/run_test.py new file mode 100644 index 0000000..2522e36 --- /dev/null +++ b/tests/can_telemetry/run_test.py @@ -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) # Зависание очереди или нарушенный тайминг завершают проверку ошибкой.