Добавить heartbeat и передачу CAN по изменению и таймеру

Для всех Mode передавать пакеты при обновлении данных и повторять каждый через 1300 мс от последнего CAN ACK. Отдельно отправлять heartbeat раз в секунду. Сохранить маски рассылки, отключение потоков нулевым периодом и неблокирующее восстановление после ошибок TX.

Проверены границы периодов, все Mode, занятый mailbox, отмена без CANAA, отсутствие ACK, переполнение Timer1, первый снимок и справедливость потоков. Изменённые модули ecan.c и main.c скомпилированы компилятором TI C2000.
This commit is contained in:
2026-10-16 23:17:00 +03:00
parent 345c81166e
commit 13f1a2c5fd
7 changed files with 351 additions and 120 deletions

View File

@@ -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. */

View File

@@ -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);

View File

@@ -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];

View File

@@ -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;

View File

@@ -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`.

107
tests/can_telemetry/model.c Normal file
View File

@@ -0,0 +1,107 @@
#include <stdint.h> /* Фиксирует ширину таймера и аппаратных полей eCAN независимо от разрядности host-компилятора. */
#include <stdio.h> /* Выводит точную строку нарушенного сценария вместо молчаливого неверного результата. */
#include <string.h> /* Изолирует независимые случаи очисткой модели аппаратуры и данных. */
typedef uint16_t Uint16; typedef uint32_t Uint32; /* Повторяет 16- и 32-битные аппаратные типы TI C28x. */
#define READY_FREQ 1000U /* Один такт Timer1 равен одной миллисекунде, как в действующей прошивке 166. */
#define CANPOWSE 20U /* Прежний базовый интервал обнаружения обновлений данных составляет 20 мс. */
#define ANSWER_LEN 128 /* Как в package.h, технологическая карта содержит ровно R0–R127 без дополнительного запаса для ошибочного чтения. */
#define interrupt /* Host-модель вызывает настоящий ISR как обычную функцию без атрибутов процессора TI. */
#define EALLOW ((void)0) /* Защищённый доступ аппаратных регистров отдельно проверяется настоящим компилятором TI. */
#define EDIS ((void)0) /* Модель не подменяет логику изменения битов тайм-аута mailbox. */
#define EINT ((void)0) /* Тесты явно выбирают момент аппаратного прерывания, исключая недетерминированные потоки. */
#define DINT ((void)0) /* Сценарий проверяет восстановление маски, не симулируя планировщик ОС. */
#define M_INT9 0x0100U /* Девятая группа PIE обслуживает eCAN в production-обработчике. */
#define MINT9 0x0100U /* Сохраняет аппаратную маску CPU INT9 при обработке ошибки передачи. */
#define MG96 0x00DFU /* Маска соответствует подавлению вложенного INTx6 в старом ISR проекта. */
struct REG { Uint32 all; }; /* Даёт production-коду настоящие 32-битные поля регистров eCAN. */
struct ECAN_REGS { struct REG CANTA, CANAA, CANTRR, CANTRS, CANTOC, CANTOS; Uint32 CANTSC; }; /* Моделирует только используемый жизненный цикл штатного mailbox. */
static volatile struct ECAN_REGS ECanaRegs; /* Volatile сохраняет все наблюдаемые записи production-функций при оптимизации O3. */
static struct { struct { struct REG MDH, MDL; } MBOX0; } ECanaMboxes; /* Восемь октетов передаваемого кадра наблюдаются независимо от кэша алгоритма. */
static struct { Uint32 InterruptCount; } CpuTimer1; /* Счётчик переполняется в точности через 2^32 тактов. */
static struct { struct { Uint16 all; } PIEIER9; union { unsigned all; struct { unsigned reserved:8, ACK9:1; } bit; } PIEACK; } PieCtrlRegs; /* Сохраняет реальное положение PIEACK9 в маске подтверждения IRQ. */
static Uint16 IER; static unsigned int irq_mask; /* Позволяет проверить возврат исходного состояния прерываний после короткого снимка данных. */
static int wait, CanTimeOutErrorTR, Mode, Desk, modbus[ANSWER_LEN]; /* Использует настоящий размер массива технологических слов и наблюдаемые параметры узла. */
static unsigned int Maska[2][8]; /* Каждому потоку соответствует восемь 16-битных масок разрешённых регистров. */
#define Cancount (modbus + 0x60) /* Как в package.h, R96 и R97 определяют включение двух потоков данных. */
static unsigned int __disable_interrupts(void) { unsigned int previous = irq_mask; irq_mask = 1U; return previous; } /* Имитирует сохранение исходной маски IRQ даже при вложенном чтении Timer1. */
static void __restore_interrupts(unsigned int previous) { irq_mask = previous; } /* Выявляет ошибочное безусловное разрешение прерываний отправителем. */
static void led1_toggle(void) {} /* Индикация не влияет на подтверждение доставки или период таймера. */
#include "can_telemetry_impl.h" /* Компилирует точный production-алгоритм, а не отдельную копию реализации в тесте. */
/* TIMEOUT_ISR */
#define CHECK(condition) do { if (!(condition)) { printf("FAIL line %d: %s\n", __LINE__, #condition); return 1; } } while (0) /* Проверка остаётся активной при оптимизации и показывает нарушенное ожидание. */
static Uint32 frames_hi[2048], frames_lo[2048], frames_at[2048]; static unsigned int count; /* Журнал хранит только действительно запущенные аппаратные кадры и момент их передачи. */
static void hardware_ack(void) { ECanaRegs.CANTRS.all &= ~1UL; ECanaRegs.CANTRR.all &= ~1UL; ECanaRegs.CANTA.all = 1UL; ECanaRegs.CANAA.all = 0UL; } /* Завершает именно mailbox 0 успешным аппаратным ACK. */
static void prepare(unsigned int mode, unsigned int desk) { /* Начинает независимый сценарий, включая имитацию повторного запуска программы. */
memset((void *)&ECanaRegs, 0, sizeof(ECanaRegs)); memset(&ECanaMboxes, 0, sizeof(ECanaMboxes)); memset(&PieCtrlRegs, 0, sizeof(PieCtrlRegs)); /* Сбрасывает только модель периферии между испытаниями. */
memset(modbus, 0, sizeof(modbus)); memset(Maska, 0, sizeof(Maska)); CpuTimer1.InterruptCount = 0U; count = 0U; irq_mask = 0U; /* Устанавливает известное начальное содержимое карты и таймера. */
Mode = mode; Desk = desk; Cancount[0] = 10; Cancount[1] = 101; IER = M_INT9; PieCtrlRegs.PIEIER9.all = 0x0030U; CanTimeOutErrorTR = 0; /* Периоды 10 и 101 проверяют независимость нового повтора от старых делителей. */
CAN_telemetry_init(); /* Первый снимок после каждого запуска должен отправляться независимо от содержимого старых кэш-массивов. */
} /* Аппаратный ACK в каждом сценарии задаётся явно. */
static int pump(void) { /* Выдаёт всю уже готовую очередь с ACK между кадрами, не продвигая тестовое время. */
unsigned int limit; /* Ограничивает итерации для обнаружения зависания на пустой маске или неизменном пакете. */
for (limit = 0U; limit < 512U; ++limit) { /* Полная карта из двух потоков требует значительно меньше 512 аппаратных кадров. */
CAN_telemetry_service(); /* Обслуживает реальную очередь, heartbeat и индивидуальные периоды подтверждённых данных. */
CHECK(irq_mask == 0U); /* После любого пути отправки прерывания должны иметь первоначальное состояние. */
if (!(ECanaRegs.CANTRS.all & 1UL)) return 0; /* Отсутствие нового TX означает завершённую выдачу готовых кадров. */
CHECK(count < 2048U); /* Исключает скрытое переполнение тестового журнала. */
frames_hi[count] = ECanaMboxes.MBOX0.MDH.all; frames_lo[count] = ECanaMboxes.MBOX0.MDL.all; frames_at[count++] = CpuTimer1.InterruptCount; /* Запоминает точные данные mailbox, не используя внутренний кэш production-кода как эталон. */
hardware_ack(); /* Только успешное подтверждение разрешает алгоритму подавить следующий неизменный пакет. */
} /* Бесконечное повторение выявляется как ошибка конечной модели. */
CHECK(0); /* Даже при пустой маске или неподтверждённом адресе сервис обязан возвращаться без блокировки. */
return 1; /* Сохраняет однозначный код ошибки для компилятора C. */
} /* Все вызовы pump выполняются с фиксированным временем, поэтому сравнения периодов точны. */
static unsigned int address_at(unsigned int index) { return (unsigned int)((frames_hi[index] >> 16) & 0x1FFFU); } /* Декодирует существующий маркер BALZAM независимо от внутренней реализации отправителя. */
static unsigned int count_address(unsigned int address) { unsigned int index, found = 0U; for (index = 0U; index < count; ++index) if (address_at(index) == address) ++found; return found; } /* Считает реальные кадры выбранного технологического или heartbeat-адреса. */
int main(void) { /* Проверяет требования пользователя, границы карты и ошибки аппаратного TX на настоящем коде прошивки. */
unsigned int mode, before, index; Uint32 started; /* Сценарии используют независимые ожидаемые периоды в миллисекундах. */
for (mode = 1U; mode <= 9U; ++mode) { /* Сравнение данных и повтор 1,3 с применяются ко всем аппаратным Mode, включая задатчик. */
prepare(mode, mode == 8U ? 4U : 0U); Maska[0][0] = 1U; modbus[0] = 9; modbus[1] = 12; modbus[2] = -1; CHECK(pump() == 0); /* Начальный пакет должен передаться сразу в прежнем порядке трёх слов. */
CHECK(count == 1U && frames_hi[0] == 0xE0000009UL && frames_lo[0] == 0x000CFFFFUL); /* Отрицательное 16-битное значение не портит упаковку и старший регистр. */
CpuTimer1.InterruptCount = 999U; CHECK(pump() == 0 && count == 1U); /* Неизменившиеся данные не создают лишних кадров до секундного heartbeat. */
CpuTimer1.InterruptCount = 1000U; CHECK(pump() == 0 && count == 2U && address_at(1U) == 0x100U); /* Heartbeat появляется ровно через одну секунду, независимо от обоих Cancount. */
CHECK((frames_hi[1] & 0xFFFFUL) == 1U && frames_lo[1] == ((Uint32)Mode << 16 | (unsigned int)Desk)); /* Отдельный heartbeat содержит счётчик секунд, Mode и Desk. */
CpuTimer1.InterruptCount = 1299U; CHECK(pump() == 0 && count_address(0U) == 1U); /* Технологический пакет не повторяется раньше индивидуального срока. */
CpuTimer1.InterruptCount = 1300U; CHECK(pump() == 0 && count_address(0U) == 2U); /* Неизменные данные повторяются на границе 1,3 с без искусственного изменения состояния планировщика тестом. */
CHECK(frames_at[count - 1U] == 1300U && frames_hi[count - 1U] == frames_hi[0]); /* Повторяет сами данные, а не только отдельный heartbeat. */
} /* Завершает проверку одинакового поведения всех Mode. */
prepare(5U, 0U); Maska[0][0] = 1U; Maska[1][0] = 8U; modbus[0] = 1; modbus[3] = 2; CHECK(pump() == 0 && count == 2U); /* Два потока получают отдельные пакеты и подтверждения без ожидания старых делителей. */
CpuTimer1.InterruptCount = 40U; modbus[1] = 77; CHECK(pump() == 0 && count == 3U && address_at(2U) == 0U); /* Изменение второго слова пакета отправляется до истечения 1,3 с. */
CpuTimer1.InterruptCount = 60U; modbus[2] = 88; CHECK(pump() == 0 && count == 4U && (frames_lo[3] & 0xFFFFUL) == 88U); /* Изменение третьего слова тоже обнаруживается и не подавляется кэшем первого регистра. */
CpuTimer1.InterruptCount = 1300U; CHECK(pump() == 0 && count_address(3U) == 2U && count_address(0U) == 3U); /* Независимый пакет повторяется через 1,3 с, а обновлённый пакет R0 ещё не достиг своего нового срока. */
CpuTimer1.InterruptCount = 1360U; CHECK(pump() == 0 && count_address(0U) == 4U); /* Период R0 отсчитывается заново от ACK последнего изменённого снимка. */
prepare(8U, 4U); Maska[0][1] = 1U; Maska[1][0] = 15U; ECanaRegs.CANTRS.all = 1U; ECanaMboxes.MBOX0.MDH.all = 0x12345678UL; CAN_telemetry_service(); /* Занятый внешним запросом mailbox нельзя менять или считать доставленным. */
CHECK(ECanaMboxes.MBOX0.MDH.all == 0x12345678UL && can_scan_due == 1U); /* Оба потока сохраняют готовность до физического освобождения передатчика. */
ECanaRegs.CANTRS.all = 0U; CHECK(pump() == 0 && count_address(16U) == 1U && count_address(0U) == 1U && count_address(3U) == 1U); /* После занятости не теряются R0–R2 и R3–R5 медленного потока. */
prepare(5U, 0U); Maska[0][0] = 1U; CAN_telemetry_service(); CHECK(wait && !(can_valid[0] & 1U)); /* Постановка первого пакета в TX ещё не означает успешную доставку. */
ECanaRegs.CANTA.all = 0U; ECanaRegs.CANTOS.all = 1U; ECanaRegs.CANTOC.all = 5U; CANa_reset_err(); /* Воспроизводит аппаратный timeout, пока отдельный контроль служебного mailbox тоже включён. */
CHECK(can_tx_failed && CanTimeOutErrorTR == 1 && ECanaRegs.CANTRR.all == 1U && ECanaRegs.CANTOC.all == 4U); /* ISR помечает отказ и отменяет только штатный TX без выключения служебного контроля. */
ECanaRegs.CANTOS.all &= ECanaRegs.CANTOC.all; ECanaRegs.CANTRS.all = ECanaRegs.CANTRR.all = 0U; ECanaRegs.CANAA.all = 0U; CpuTimer1.InterruptCount = 10U; CAN_telemetry_service(); /* Отмена завершена без CANAA: отсутствие этого флага не должно навсегда заблокировать очередь. */
CHECK(!wait && can_backoff && !(can_valid[0] & 1U)); /* Неудачный снимок не становится подтверждённым и остаётся кандидатом следующей попытки. */
CpuTimer1.InterruptCount = 1009U; modbus[0] = 42; CAN_telemetry_service(); CHECK(!(ECanaRegs.CANTRS.all & 1U)); /* До окончания секундной паузы после ошибки повтор не засоряет CAN. */
CpuTimer1.InterruptCount = 1010U; CHECK(pump() == 0 && count_address(0U) == 1U && (frames_hi[count - 1U] & 0xFFFFUL) == 42U); /* После паузы повторяет актуальное изменение и отдельно ожидающий heartbeat. */
prepare(5U, 0U); Maska[0][0] = 1U; CAN_telemetry_service(); ECanaRegs.CANTA.all = 0U; CpuTimer1.InterruptCount = 100U; CAN_telemetry_service(); /* Потерянный timeout IRQ страхуется реальным временем Timer1 через 100 мс. */
CHECK(can_tx_failed && ECanaRegs.CANTRR.all == 1U && CanTimeOutErrorTR == 1); /* Программная отмена не ждёт аппаратного CANAA в блокирующем цикле. */
CAN_telemetry_service(); CHECK(CanTimeOutErrorTR == 1); /* Длительная аппаратная отмена не увеличивает счётчик ошибки на каждой итерации main. */
prepare(5U, 0U); Maska[0][0] = 1U; CHECK(pump() == 0); CpuTimer1.InterruptCount = 20U; modbus[0] = 2; CAN_telemetry_service(); ECanaRegs.CANTA.all = 0U; ECanaRegs.CANTRS.all = 0U; ECanaRegs.CANAA.all = 1U; CAN_telemetry_service(); /* Изменение, отменённое после старого успешного снимка, не должно затереть подтверждённый кэш. */
CHECK(can_last_hi[0] == 0xE0000000UL && can_backoff); /* Только реально доставленный прежний пакет остаётся подтверждённым. */
prepare(5U, 0U); CHECK(pump() == 0 && count == 0U); CpuTimer1.InterruptCount = 3500U; CHECK(pump() == 0 && count == 1U && address_at(0U) == 0x100U && (frames_hi[0] & 0xFFFFU) == 3U); /* Пустые маски не зависают; после задержки main отправляется последний heartbeat без пачки пропущенных секунд. */
CHECK(pump() == 0 && count == 1U); /* Повторная итерация main не дублирует уже поставленный heartbeat. */
prepare(5U, 0U); Maska[0][0] = 1U; Cancount[0] = 0; Cancount[1] = 0; CHECK(pump() == 0 && count == 0U); CpuTimer1.InterruptCount = 1000U; CHECK(pump() == 0 && count == 1U && address_at(0U) == 0x100U); /* Отключённые потоки не отключают независимый heartbeat узла. */
prepare(5U, 0U); Maska[0][7] = 0x8000U; modbus[127] = 0x55AA; CHECK(pump() == 0 && count == 1U && frames_hi[0] == 0xE07F55AAUL && frames_lo[0] == 0U); /* Конец карты R127 дополняется нулями без чтения data[128] и data[129]. */
prepare(5U, 0U); for (index = 0U; index < 8U; ++index) Maska[0][index] = 0xFFFFU; CHECK(pump() == 0 && count == 43U); /* Плотная маска сохраняет прежние 43 трёхсловных пакета вместо лишних 128 перекрывающихся кадров. */
for (index = 0U; index < count; ++index) CHECK(address_at(index) == index * 3U); /* Первый полный снимок покрывает ровно R0, R3, ... R126 в установленном формате. */
CpuTimer1.InterruptCount = 1300U; CHECK(pump() == 0 && count_address(0U) == 2U && count_address(126U) == 2U && count == 87U); /* Готовая очередь выдаёт всю карту за период, а не один кадр маски каждые 1,3 с. */
prepare(5U, 0U); CpuTimer1.InterruptCount = 0xFFFFFF00UL; started = CpuTimer1.InterruptCount; Maska[0][0] = 1U; CHECK(pump() == 0); CpuTimer1.InterruptCount = started + 1000U; CHECK(pump() == 0 && count_address(0x100U) == 1U); /* Секундный heartbeat переживает переполнение Timer1. */
CpuTimer1.InterruptCount = started + 1300U; CHECK(pump() == 0 && count_address(0U) == 2U); /* Период повторения данных тоже корректен через 32-битное переполнение. */
prepare(5U, 0U); Maska[0][0] = 1U; CHECK(pump() == 0); before = count; can_valid[7] = 0xFFFFU; can_backoff = can_heartbeat_due = 1U; wait = 1; CAN_telemetry_init(); CHECK(pump() == 0 && count == before + 1U); /* Повторная инициализация отбрасывает грязное состояние и заново отправляет первый снимок. */
prepare(5U, 0U); Maska[0][1] = 1U; Maska[1][0] = 1U; for (index = 0U; index <= 250U; ++index) { CpuTimer1.InterruptCount = index * 20U; modbus[16] = index; CHECK(pump() == 0); } /* Непрерывное изменение быстрого потока не должно вытеснить heartbeat и неизменный пакет другого потока. */
CHECK(count_address(16U) == 251U && count_address(0U) == 4U && count_address(0x100U) == 5U); /* За пять секунд доставляются все изменения, четыре состояния R0 и пять секундных heartbeat. */
prepare(5U, 0U); Maska[0][0] = 1U; CAN_telemetry_service(); CpuTimer1.InterruptCount = 50U; hardware_ack(); CAN_telemetry_service(); /* Подтверждает первый пакет через 50 мс после запуска, а не в нулевой момент передачи. */
CpuTimer1.InterruptCount = 1349U; CHECK(CAN_send(0, modbus, 0) == 2); CpuTimer1.InterruptCount = 1350U; CHECK(CAN_send(0, modbus, 0) == 1); /* Индивидуальные 1300 мс отсчитываются от ACK, поэтому ранний повтор через 1300 мс от запуска запрещён. */
prepare(5U, 0U); ECanaRegs.CANTOC.all = 1U; ECanaRegs.CANTRS.all = 1U; CANa_reset_err(); /* Вызывает фантомный ISR без аппаратного CANTOS0 во время исправной передачи. */
CHECK(!can_tx_failed && !CanTimeOutErrorTR && ECanaRegs.CANTRR.all == 0U && ECanaRegs.CANTOC.all == 1U && PieCtrlRegs.PIEIER9.all == 0x0030U); /* Не отменяет исправный кадр и восстанавливает прежние разрешения обоих CAN IRQ. */
prepare(5U, 0U); Maska[0][0] = 1U; ECanaRegs.CANTRR.all = 1U; ECanaMboxes.MBOX0.MDH.all = 0x11223344UL; CAN_telemetry_service(); CHECK(ECanaMboxes.MBOX0.MDH.all == 0x11223344UL && !wait); /* Даже без CANTRS незавершённая аппаратная отмена запрещает перезапись mailbox. */
prepare(5U, 0U); irq_mask = 1U; CHECK(CAN_send(0, modbus, 0) == 1 && irq_mask == 1U); /* Передатчик сохраняет уже запрещённые IRQ вызывающего кода вместо безусловного разрешения. */
puts("PASS: heartbeat 1 Hz; changed data; per-packet refresh 1300 ms; all modes; masks; ACK, timeout and backoff; wraparound; dirty RAM; bounds"); /* Подтверждает проверку реального production-кода без утверждения о запуске на физической плате. */
return 0; /* Все независимые поведенческие проверки завершились успешно. */
} /* Модель не требует подключения CAN-адаптера и не отправляет команды реальному оборудованию. */

View File

@@ -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) # Зависание очереди или нарушенный тайминг завершают проверку ошибкой.