diff --git a/c/gas-logger/README.md b/c/gas-logger/README.md index f17c4db..0e10a25 100644 --- a/c/gas-logger/README.md +++ b/c/gas-logger/README.md @@ -1,5 +1,7 @@ # GAS Logger — экспериментальная версия 1 +> GAS `0x3` использует адреса отдельных 16-битных регистров. GAS SLOT `0x4` — отдельный сервис с маской в Word0 и тремя значениями Word1…Word3. Спецификация: `doc/setcan/protocan/OAP.md`; адреса этого документа остаются регистровыми. + Непрерывный регистратор с двумя кольцевыми банками, неизменяемым снимком и общей картой GAS / ProtoCAN GAS / Modbus FC03/FC06. Весь код и проектные/MCU адаптеры находятся в `templates`; приложения подключают ревизию сабмодулем. diff --git a/c/global-time/CMakeLists.txt b/c/global-time/CMakeLists.txt new file mode 100644 index 0000000..88db168 --- /dev/null +++ b/c/global-time/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.13) # Задаёт минимальную версию, совместимую с соседними C-библиотеками templates. +project(global_time C) # Создаёт независимую цель для программных глобальных часов. +add_library(global_time STATIC src/global_time.c) # Собирает переносимое ядро без обязательных MCU заголовков. +target_include_directories(global_time PUBLIC include) # Экспортирует потребителю только публичный контракт часов. +target_compile_features(global_time PUBLIC c_std_99) # Требует C99 для точных целочисленных типов и uint64_t меток. +option(GLOBAL_TIME_BUILD_F28335 "Собирать аппаратный порт TI F28335" OFF) # Позволяет MCU проекту явно включить адаптер со своими TI include-путями. +if(GLOBAL_TIME_BUILD_F28335) # Не навязывает host-приложению аппаратные регистры TI. + target_sources(global_time PRIVATE ports/tms320f28335/global_time_f28335.c) # Добавляет eCAN и необязательный CPU Timer2 порт к той же библиотеке. + target_include_directories(global_time PUBLIC ports/tms320f28335) # Делает аппаратные hooks доступными прошивке потребителя. +endif() # Завершает условное подключение аппаратного адаптера. +option(GLOBAL_TIME_BUILD_TESTS "Собирать host-проверки глобального времени" ON) # Разрешает выключить host-исполняемые файлы в MCU сборке. +if(GLOBAL_TIME_BUILD_TESTS AND NOT GLOBAL_TIME_BUILD_F28335) # Не подмешивает host-модель регистров к настоящему TI порту. + enable_testing() # Регистрирует проверяемые сценарии в CTest. + add_executable(test_global_time tests/test_global_time.c) # Собирает календарные, CAN и монотонные сценарии. + target_link_libraries(test_global_time PRIVATE global_time) # Использует то же ядро, которое подключает прошивка. + add_test(NAME global_time COMMAND test_global_time) # Делает host-проверку доступной через ctest. + add_executable(test_global_time_f28335 tests/test_f28335.c ports/tms320f28335/global_time_f28335.c) # Собирает настоящий аппаратный адаптер с минимальной моделью регистров. + target_include_directories(test_global_time_f28335 PRIVATE tests/f28335_stubs ports/tms320f28335) # Ограничивает подмену заголовков только этой host-целью. + target_link_libraries(test_global_time_f28335 PRIVATE global_time) # Связывает аппаратные hooks с реальным ядром часов. + add_test(NAME global_time_f28335 COMMAND test_global_time_f28335) # Регистрирует eCAN/Timer2 проверку отдельно от календарной. +endif() # Завершает host-проверки без влияния на MCU исходники. diff --git a/c/global-time/README.md b/c/global-time/README.md new file mode 100644 index 0000000..b4e0bf4 --- /dev/null +++ b/c/global-time/README.md @@ -0,0 +1,167 @@ +# Глобальное время + +Программные часы с тактом 1 мс, синхронизацией даты и времени через ProtoCAN +и атомарным получением метки для логгера. Ядро C99 не требует аппаратного RTC, +операционной системы или динамической памяти. Порт TMS320F28335 включает +eCAN-A и необязательный CPU Timer2. Все текстовые файлы — UTF-8. + +## Обмен при запуске + +1. Приложение инициализирует часы до разрешения IRQ и запускает источник 1 мс. +2. После завершения инициализации **всей платы** вызывает `gt_ready()` / + `gt_f28335_ready()`. +3. Из main библиотека отправляет `DISCRETE/FLAGS`, `MsgBody=0x3000`, + `Route=1`, адрес самой платы, `DLC=1`, `DATA=01`. +4. Верхний уровень распознаёт пакет через `gt_parse_ready()`, берёт свою + текущую дату и посылает **один broadcast всем платам**, используя `gt_make_rtc()`. +5. Каждый узел принимает штатный `RTCSETUP`, сравнивает время со своим, + сохраняет величину коррекции и немедленно обновляет календарные часы. + При отсутствии ответа узел повторяет готовность не чаще раза в секунду. + +`RTCSETUP` полностью сохраняет действующий формат ProtoCAN. +Универсальное сообщение готовности здесь определено как новый профиль +`DISCRETE/FLAGS`; ранее такого общего пакета в базовом протоколе не было. +Подробный [контракт обмена](../../doc/setcan/protocan/GLOBAL_TIME.md). +Код верхнего уровня для автоматической отправки времени подключается в его +приложении; библиотека предоставляет кодировщик и распознавание события старта. + +## Метка логгера + +`gt_get()` одним атомарным чтением возвращает: + +| Поле | Значение | +|---|---| +| `seconds`, `millisecond` | секунды от 2000-01-01 и доля 0..999 мс | +| `synchronized` | 0 до первого корректного RTCSETUP, 1 после | +| `uptime_ms` | время работы modulo 2^32, независимое от переводов часов | +| `sync_uptime_ms` | момент приёма последнего опорного пакета | +| `generation` | номер принятой коррекции | +| `correction_ms` | последнее смещение, знаковое 64-битное значение в мс | +| `weekday` | фаза недели 0..6, продвигаемая при смене суток | + +`gt_timestamp_ms()` формирует 64-битную метку от 2000 года. +`gt_calendar()` преобразует снимок в календарь и возвращает 0, пока дата неизвестна. +Для записей, которые должны различать переводы часов, сохраняйте также +`generation`, `synchronized` и/или `uptime_ms`. Календарное время может идти назад +после коррекции. Тайм-ауты следует считать по разности `uptime_ms`. +Без новых broadcast часы продолжают ход по локальному генератору; +возраст синхронизации равен `(uint32_t)(uptime_ms - sync_uptime_ms)`. + +```c +#include "global_time_f28335.h" /* Подключает общую временную базу F28335. */ +gt_stamp stamp; /* Содержит согласованную дату, миллисекунды и качество времени. */ +gt_get(gt_f28335_clock(), &stamp); /* Допускается из main и ISR, включая ISR логгера. */ +uint64_t time_ms = gt_timestamp_ms(&stamp); /* Перед сохранением календарной метки проверяется stamp.synchronized. */ +``` + +Это фрагмент для функции потребителя. Формат хранения существующего логгера +библиотека не меняет. В частности, старое 32-битное поле `time_ms` в GAS logger +и v4 УМП не вмещает календарные миллисекунды: потребителю нужен собственный +формат абсолютной метки либо отдельная временная база и номер поколения. + +## Подключение TMS320F28335 + +Добавьте в сборку `src/global_time.c` и +`ports/tms320f28335/global_time_f28335.c`. Include-пути: `include`, +`ports/tms320f28335`, штатные TI `DSP2833x_headers/include`, +`DSP2833x_common/include` и include компилятора. + +Ресурсы порта: + +| Ресурс | Назначение | +|---|---| +| eCAN-A MBOX7 | RX точного RTCSETUP, любой приоритет, IRQ line0 | +| eCAN-A MBOX8 | TX готовности, ACK проверяется в main | +| CPU Timer2 / INT14 | необязательный собственный такт 1 мс | + +Mailbox 0..6, скорость CAN, GPIO и глобальные маршруты PIE/CANGIM настраивает +приложение. `DBO=0` обязателен; порт отвергает другую раскладку октетов. +MBOX7 использует CANOPC: кадр защищён от аппаратной перезаписи до копирования ISR. +В программной очереди хранится последний broadcast, поскольку более новый +опорный пакет заменяет предыдущий. + +При запрещённых IRQ, **после** `InitECan` и `InitPieVectTable`: + +```c +int ok = gt_f28335_init_periph(5U); /* PERIPH5=Type1/ID5, то есть УМП1; номер берётся из конфигурации платы. */ +if (ok) ok = gt_f28335_timer2_start(150000000UL); /* Частота CPU задаётся проектом в Гц; пример соответствует 150 МГц. */ +``` + +`gt_f28335_init()` позволяет передать Type и ID непосредственно. +`init_periph()` поддерживает физические PERIPH 0..31: Type1/2 и ID0..15; +5 — УМП1, 6 — УМП2, 8 — задатчик. Порт инициализируется до подключения hooks. +Повторный init разрешён только при остановленном собственном Timer2, +отсутствующем TX и запрещённых глобальных IRQ. + +Если Timer2 занят RTOS или приложением, **не вызывайте** `timer2_start()`; +в существующем ISR с периодом ровно 1 мс вызывайте `gt_f28335_tick_1ms()`. +Используйте один источник такта. Отдельный Timer2 продолжает отсчёт и при +медленном main, но длительная блокировка IRQ на несколько миллисекунд приводит +к потере тиков: аппаратный флаг прерывания не считает пропущенные периоды. + +В существующем `ECAN0INTA` ISR перед очисткой общих CAN-флагов и PIEACK: + +```c +gt_f28335_rx_isr(); /* Копирует только MBOX7 и запоминает uptime при приёме, календарь проверяется позднее. */ +``` + +Приложение должно разрешить соответствующий CAN IRQ: `CANGIM.I0EN`, +PIE group9 `INTx5`, CPU `M_INT9`; оставить свой обработчик остальных mailbox +и штатные подтверждения CANGIF/PIEACK. Порт не заменяет общий CAN ISR. +Глобальный `EINT` выполняет приложение после полной настройки. + +После прикладной инициализации и в каждом проходе главного цикла соответственно: + +```c +gt_f28335_ready(); /* Выполняется один раз после загрузки параметров и запуска прикладных подсистем. */ +gt_f28335_service(); /* Выполняется регулярно в main; обработка календаря и CAN не ждёт аппаратного ACK. */ +``` + +Все атомарные чтения и обновления используют TI +`__disable_interrupts()` / `__restore_interrupts()`, сохраняя прежний INTM. +Транспорт ждёт подтверждение CANTA асинхронно и не перезаписывает занятый MBOX8. +При отсутствии других CAN-узлов передача остаётся аппаратному контроллеру +до восстановления шины. CAN ACK не подтверждает обработку пакета мастером; +приход корректного RTCSETUP прекращает повторные объявления готовности. + +## Точность и календарь + +Разрешение локальных часов — 1 мс. Действующий RTCSETUP передаёт только целые +секунды, поэтому сам формат **не гарантирует синхронизацию плат с точностью 1 мс**. +Мастер должен посылать актуальное значение около секундной границы. +Арбитраж CAN и задержка до RX ISR остаются погрешностью; задержка после RX ISR +до обработки в main компенсируется по монотонному счётчику. Для точности между +платами до 1 мс понадобится согласованное расширение проводного протокола. + +Одна шкала времени выбирается верхним уровнем (рекомендуется UTC); кадр не +содержит часовой пояс. Входные годы 2000..2099, дата проверяется по григорианскому +календарю. Формат дня недели 0..6 соответствует проверке существующего +STM32-порта; профиль обозначает 0 как воскресенье. Для совместимости переданная +фаза недели сохраняется без дополнительной проверки соответствия дате. +Некоторые старые клиенты отображают неделю как 1..7; перед отправкой им нужен +перевод воскресенья 7 → 0. +Секундная шкала uint32_t от 2000 года переполнится в феврале 2136 года. +Период между захватом RX uptime и `gt_receive()` должен быть меньше 2^32 мс. + +## Проверки + +Из корня templates: + +```text +python c/global-time/tests/run_tests.py +``` + +`--cc` задаёт GCC/Clang. Для отдельной TI C28x проверки передайте +`--ti-cgt` (каталог компилятора), `--ti-headers` (DSP2833x_headers/include) +и при необходимости `--ti-common` (DSP2833x_common/include). +Объекты и EXE создаются во временном каталоге. CMake также предоставляет +`global_time`, два CTest и опции `GLOBAL_TIME_BUILD_F28335` / +`GLOBAL_TIME_BUILD_TESTS`; MCU проект задаёт свои TI include-пути. + +Проверяются ошибки даты и кадра, високосный февраль, смена года, коррекции +в обе стороны, компенсация очереди через переполнение uptime, ранний broadcast, +повторы готовности, сохранение IRQ, изоляция eCAN mailbox, аппаратный ACK, +значения периода Timer2 и его реальный ISR через назначенный вектор. +TI-проверка компилирует исходники с large/COFF ABI и настоящими заголовками +проекта 28335_periph. Полная линковка конкретной прошивки и проверка на плате +в этот набор не входят. diff --git a/c/global-time/include/global_time.h b/c/global-time/include/global_time.h new file mode 100644 index 0000000..46f406c --- /dev/null +++ b/c/global-time/include/global_time.h @@ -0,0 +1,65 @@ +#ifndef GLOBAL_TIME_H /* Защищает единый контракт часов от повторного включения. */ +#define GLOBAL_TIME_H /* Отмечает подключённый интерфейс программных глобальных часов. */ +#include /* Фиксирует разрядность счётчиков и поддерживает 16-битный char TI C28x. */ +#ifdef __cplusplus /* Сохраняет совместимость C-библиотеки с приложениями C++. */ +extern "C" { /* Экспортирует функции без изменения имён компилятором C++. */ +#endif /* Завершает условное объявление C ABI. */ +#define GT_RTC_ID 0x00000030UL /* ProtoCAN: Route=0, Type/ID=0, BROADCAST=0, RTCSETUP=3 в Body[15:4]. */ +#define GT_READY_BODY 0x3000U /* Контракт GLOBAL_TIME.md: DISCRETE/FLAGS=3, страница общих флагов=0. */ +#define GT_READY_RETRY_MS 1000UL /* Повторяет сообщение готовности раз в секунду до первой синхронизации. */ +typedef struct gt_frame { /* Описывает classic CAN независимо от регистров и размера char процессора. */ + uint32_t id; /* Хранит только 29 бит идентификатора, без служебных битов драйвера. */ + uint16_t extended; /* Единица означает IDE=1; стандартные кадры часы игнорируют. */ + uint16_t remote; /* Единица означает RTR; удалённый запрос не содержит время. */ + uint16_t dlc; /* Считает октеты на линии, а не sizeof элементов массива C28x. */ + uint16_t data[8]; /* Каждый элемент содержит один октет 0..255 даже при CHAR_BIT=16. */ +} gt_frame; /* Завершает переносимый контейнер кадра. */ +typedef struct gt_datetime { /* Представляет календарь без зависимости от time_t и часового пояса ОС. */ + uint16_t year; /* Полный год; входной ProtoCAN поддерживает 2000..2099. */ + uint16_t month; /* Номер месяца 1..12, без BCD-кодирования. */ + uint16_t day; /* Число месяца с проверкой високосного февраля. */ + uint16_t weekday; /* День недели 0..6; профиль использует 0=воскресенье, 1=понедельник. */ + uint16_t hour; /* Часы 0..23 в 24-часовом формате. */ + uint16_t minute; /* Минуты 0..59; leap seconds протокол не передаёт. */ + uint16_t second; /* Секунды 0..59 согласно существующему RTCSETUP. */ + uint16_t millisecond; /* Доля секунды 0..999; RTCSETUP задаёт нулевую долю. */ +} gt_datetime; /* Завершает публичный календарный тип. */ +typedef struct gt_stamp { /* Даёт логгеру согласованную метку и её качество одним чтением. */ + uint32_t seconds; /* Секунды от 2000-01-01 00:00:00 в шкале времени мастера. */ + uint16_t millisecond; /* Миллисекундная доля текущей секунды. */ + uint16_t synchronized; /* Ноль до первого корректного RTCSETUP, единица после него. */ + uint32_t uptime_ms; /* Независимый счётчик работы modulo 2^32; коррекции его не меняют. */ + uint32_t sync_uptime_ms; /* Момент приёма последнего RTCSETUP для оценки давности синхронизации. */ + uint32_t generation; /* Число коррекций; логгер отличает участки до и после перевода часов. */ + int64_t correction_ms; /* Последнее смещение новых часов относительно старых, в миллисекундах. */ + uint16_t weekday; /* Принятый день недели, продвигаемый при каждом переходе через полночь. */ +} gt_stamp; /* Завершает атомарно читаемый снимок. */ +typedef struct gt_port { /* Передаёт ядру короткие критические секции и неблокирующую отправку CAN. */ + void *user; /* Контекст платформы, передаваемый каждому callback без преобразования. */ + uint16_t (*enter)(void *user); /* Запрещает IRQ и возвращает прежнее состояние для вложенных вызовов. */ + void (*leave)(void *user, uint16_t state); /* Восстанавливает прежнее состояние IRQ, включая вызовы из ISR. */ + int (*send)(void *user, const gt_frame *frame); /* Копирует кадр до возврата; 1=принят, 0=занято/ошибка. */ +} gt_port; /* Завершает платформенный контракт; heap и аппаратный RTC не нужны. */ +typedef struct gt_clock { /* Хранит состояние одного узла; инициализация обязательна до разрешения ISR. */ + gt_port port; /* Содержит проверенные callbacks и контекст платформы. */ + volatile gt_stamp stamp; /* Меняется только при удержании блокировки IRQ. */ + uint16_t device_type; /* Три адресных бита ProtoCAN, диапазон 0..7. */ + uint16_t device_id; /* Четыре адресных бита экземпляра, диапазон 0..15. */ + uint16_t ready; /* Устанавливается приложением после завершения всей инициализации платы. */ + uint16_t announced; /* Помнит принятую транспортом отправку готовности, даже если часы уже синхронизированы. */ + uint32_t sent_uptime_ms; /* Ограничивает частоту повторов готовности монотонной шкалой. */ +} gt_clock; /* Состояние допускает статическое выделение памяти на микроконтроллере. */ +int gt_init(gt_clock *clock, const gt_port *port, uint16_t device_type, uint16_t device_id); /* Проверяет адрес/callbacks и явно очищает RAM, включая TI COFF. */ +void gt_tick_1ms(gt_clock *clock); /* Вызывается ровно один раз за аппаратную миллисекунду и выполняет только короткую арифметику. */ +void gt_get(gt_clock *clock, gt_stamp *out); /* Атомарно копирует всю метку; допустим вызов из main, логгера и ISR. */ +void gt_ready(gt_clock *clock); /* Идемпотентно разрешает сообщение «плата инициализирована»; CAN отправляется из poll. */ +void gt_poll(gt_clock *clock); /* Вызывается единственным потребителем в main; не ждёт освобождения CAN. */ +int gt_receive(gt_clock *clock, const gt_frame *frame, uint32_t received_uptime_ms); /* Проверяет RTCSETUP и компенсирует задержку от приёма до обработки, менее 2^32 мс. */ +int gt_make_rtc(const gt_datetime *date, gt_frame *out); /* Даёт верхнему уровню кодировщик штатного 7-октетного broadcast. */ +int gt_parse_ready(const gt_frame *frame, uint16_t *device_type, uint16_t *device_id); /* Позволяет мастеру распознать готовность и выбрать ответ RTCSETUP. */ +int gt_calendar(const gt_stamp *stamp, gt_datetime *out); /* Преобразует согласованный снимок в календарь; до синхронизации возвращает 0. */ +uint64_t gt_timestamp_ms(const gt_stamp *stamp); /* Возвращает широкую метку от 2000 года без переполнения через 49,7 суток. */ +#ifdef __cplusplus /* Закрывает C ABI только для компилятора C++. */ +} /* Завершает блок функций с C-связыванием. */ +#endif /* Завершает проверку языка потребителя. */ +#endif /* Завершает защиту публичного заголовка. */ diff --git a/c/global-time/ports/tms320f28335/global_time_f28335.c b/c/global-time/ports/tms320f28335/global_time_f28335.c new file mode 100644 index 0000000..9a07f78 --- /dev/null +++ b/c/global-time/ports/tms320f28335/global_time_f28335.c @@ -0,0 +1,168 @@ +#include "DSP2833x_Device.h" /* Использует штатные описания 32-битных регистров eCAN, Timer2 и векторов TI. */ +#include "global_time_f28335.h" /* Подключает переносимые часы и контракт интеграции порта. */ +#include /* Явно очищает отложенный RX до первого прохода main, включая TI COFF. */ +#define GT_RX 0x00000080UL /* Бит 7 регистров eCAN соответствует выделенному приёмному MBOX7. */ +#define GT_TX 0x00000100UL /* Бит 8 регистров eCAN соответствует отдельному передающему MBOX8. */ +static gt_clock board_clock; /* Один узел F28335 использует одну глобальную временную шкалу. */ +static gt_frame pending_frame; /* Последний RX хранится до обработки в main; новый broadcast заменяет устаревший. */ +static volatile uint16_t pending_rx; /* Публикует полностью скопированный RX и его монотонную привязку. */ +static uint32_t received_at; /* Запоминает миллисекундный uptime в момент обработки CAN ISR. */ +static uint16_t tx_busy, timer_owned; /* Явно сбрасывается init: TI COFF не гарантирует нули в .ebss. */ +static uint16_t initialized = 0U; /* Запись в .cinit гарантирует безопасный отказ ранних ISR hooks до init даже при TI COFF. */ +static uint16_t enter(void *user) /* Реализует вложенную критическую секцию TI C28x. */ +{ /* Не разрешает прерывания при выходе из уже выполняющегося ISR. */ + (void)user; /* Порт использует один статический экземпляр, поэтому контекст не требуется. */ + return __disable_interrupts(); /* Возвращает предыдущее состояние ST1, необходимое для восстановления INTM. */ +} /* Завершает платформенный вход в критическую секцию. */ +static void leave(void *user, uint16_t state) /* Восстанавливает состояние IRQ до входа в библиотеку. */ +{ /* Сохраняет корректность вызовов и из main, и из ISR. */ + (void)user; /* Отдельный аппаратный экземпляр выбирается статическими регистрами eCAN-A. */ + __restore_interrupts(state); /* Восстанавливает исходный INTM вместо безусловного разрешения IRQ. */ +} /* Завершает платформенный выход из критической секции. */ +static int send(void *user, const gt_frame *frame) /* Копирует пакет готовности в выделенный аппаратный mailbox без ожидания. */ +{ /* Транспорт не перезаписывает кадр, пока контроллер не подтвердит передачу. */ + uint16_t state = enter(0); /* Защищает RMW общих регистров CANME от чужих CAN ISR, сохраняя прежний INTM. */ + (void)user; /* Контекст платформы не нужен для фиксированного eCAN-A MBOX8. */ + if (tx_busy || (ECanaRegs.CANTRS.all & GT_TX)) { /* При занятости сохраняет исходный пакет и предлагает повтор из main. */ + leave(0, state); /* Восстанавливает исходные IRQ и на раннем выходе занятого транспорта. */ + return 0; /* Не теряет готовность и не ждёт окончания аппаратной передачи. */ + } /* Продолжает только при владении свободным MBOX8. */ + EALLOW; /* Открывает запись защищённой конфигурации mailbox контроллера. */ + ECanaRegs.CANME.all &= ~GT_TX; /* Отключает только MBOX8 перед изменением ID и payload. */ + ECanaMboxes.MBOX8.MSGID.all = 0x80000000UL | frame->id; /* IDE в бите 31 eCAN разрешает расширенный ProtoCAN ID. */ + ECanaMboxes.MBOX8.MSGCTRL.all = 1UL; /* DLC=1 и RTR=0 соответствуют единственному октету готовности. */ + ECanaMboxes.MBOX8.MDL.all = frame->data[0]; /* Первый проводной октет располагается в младших восьми битах MDL при DBO=0. */ + ECanaMboxes.MBOX8.MDH.all = 0UL; /* Убирает остатки прежнего payload, хотя DLC передаёт только один октет. */ + ECanaRegs.CANTA.all = GT_TX; /* Сбрасывает только старый ACK MBOX8 записью единицы. */ + ECanaRegs.CANAA.all = GT_TX; /* Сбрасывает только старое подтверждение отмены MBOX8. */ + ECanaRegs.CANME.all |= GT_TX; /* Возвращает MBOX8 в работу, сохраняя чужие mailbox. */ + ECanaRegs.CANTRS.all = GT_TX; /* Запрашивает передачу без блокирующего ожидания ACK шины. */ + EDIS; /* Закрывает запись защищённых регистров после полной публикации кадра. */ + tx_busy = 1U; /* Отмечает владение кадром до CANTA или CANAA, даже при отсутствии других узлов на шине. */ + leave(0, state); /* Освобождает короткую аппаратную секцию сразу после публикации запроса передачи. */ + return 1; /* Подтверждает приём транспортом, но не отдельное прикладное подтверждение мастера. */ +} /* Завершает неблокирующий callback передачи. */ +int gt_f28335_init(uint16_t device_type, uint16_t device_id) /* Настраивает часы и только два выделенных mailbox при запрещённых IRQ. */ +{ /* Приложение заранее задаёт скорость eCAN, GPIO, DBO=0 и общие векторы прерываний. */ + gt_port port; /* Локальная таблица callbacks копируется ядром в постоянное состояние. */ + if (device_type > 7U || device_id > 15U || (ECanaRegs.CANTRS.all & GT_TX) || ECanaRegs.CANMC.bit.DBO) return 0; /* Проверяет адрес и порядок октетов; активную TX-передачу не прерывает повторным init. */ + if (initialized && timer_owned) return 0; /* Требует остановить захваченный Timer2 перед повторным init, сохраняя его владение при отказе. */ + port.user = 0; /* Единственный экземпляр порта не требует внешнего контекста. */ + port.enter = enter; /* Подключает защиту снимка штатным intrinsic TI. */ + port.leave = leave; /* Подключает восстановление предыдущей маски прерываний. */ + port.send = send; /* Подключает отправку в MBOX8 без ожидания свободной шины. */ + if (!gt_init(&board_clock, &port, device_type, device_id)) return 0; /* Сбрасывает RAM до открытия источников IRQ. */ + pending_rx = tx_busy = timer_owned = 0U; /* Явно очищает программные флаги после холодного и тёплого старта. */ + memset(&pending_frame, 0, sizeof(pending_frame)); /* Не читает неопределённую RAM при первом service без полученного пакета. */ + received_at = 0UL; /* Начальная привязка не читается до получения первого кадра. */ + EALLOW; /* Открывает изменение аппаратной конфигурации eCAN-A. */ + ECanaRegs.CANME.all &= ~(GT_RX | GT_TX); /* Временно отключает только выделенные mailbox 7/8. */ + ECanaMboxes.MBOX7.MSGID.all = 0xC0000000UL | GT_RTC_ID; /* IDE=1 и AME=1 включают расширенный ID и локальную маску точного RTCSETUP. */ + ECanaLAMRegs.LAM7.all = 0x10000000UL; /* LAM игнорирует только бит Priority[28]; LAMI=0 сохраняет проверку IDE. */ + ECanaMboxes.MBOX7.MSGCTRL.all = 7UL; /* Задаёт ожидаемые семь октетов; фактический DLC дополнительно проверяется ядром. */ + ECanaMboxes.MBOX8.MSGID.all = 0x80000000UL; /* Подготавливает расширенный TX без AME; адрес будет установлен callback отправки. */ + ECanaMboxes.MBOX8.MSGCTRL.all = 1UL; /* Пакет готовности занимает один октет CAN payload. */ + ECanaRegs.CANMD.all = (ECanaRegs.CANMD.all | GT_RX) & ~GT_TX; /* MBOX7 становится RX, MBOX8 — TX, направления остальных mailbox сохраняются. */ + ECanaRegs.CANOPC.all |= GT_RX; /* Запрещает перезапись RX до чтения ISR; при потере используется следующий периодический broadcast. */ + ECanaRegs.CANMIL.all &= ~GT_RX; /* Направляет MBOX7 в общую линию ECAN0INTA приложения. */ + ECanaRegs.CANMIM.all = (ECanaRegs.CANMIM.all | GT_RX) & ~GT_TX; /* Разрешает IRQ только на RX, а завершение TX проверяет main. */ + ECanaRegs.CANRMP.all = GT_RX; /* Удаляет старый pending только MBOX7 записью единицы. */ + ECanaRegs.CANRML.all = GT_RX; /* Очищает старую потерю приёма только выделенного mailbox. */ + ECanaRegs.CANTA.all = GT_TX; /* Удаляет старый ACK выделенного передатчика, не затрагивая CAN Boot. */ + ECanaRegs.CANAA.all = GT_TX; /* Удаляет старый флаг отмены перед настройкой нового обмена. */ + ECanaRegs.CANME.all |= GT_RX | GT_TX; /* Включает приём синхронизации и передачу готовности. */ + EDIS; /* Закрывает защищённую конфигурацию контроллера. */ + initialized = 1U; /* Публикует готовность самого порта после сброса всех RAM- и аппаратных флагов. */ + return 1; /* Приложение может запускать такт и разрешать общие CAN-прерывания. */ +} /* Завершает настройку без сброса скорости, GPIO и других CAN-сервисов. */ +int gt_f28335_init_periph(uint16_t periph) /* Привязывает часы к физическому адресу периферийной платы. */ +{ /* Использует тот же реестр, что существующий порт CAN Boot F28335. */ + if (periph >= 32U) return 0; /* Адреса PERIPH определены только для 32 устройств, без циклического обрезания. */ + return gt_f28335_init((uint16_t)(1U + periph / 16U), periph % 16U); /* Номера 0..15 используют Type=1, 16..31 — Type=2, младшие четыре бита задают ID. */ +} /* Завершает преобразование физического адреса. */ +gt_clock *gt_f28335_clock(void) /* Предоставляет логгеру общую временную базу платы. */ +{ /* Действует после успешной инициализации порта. */ + return &board_clock; /* Один и тот же объект обслуживают CAN, таймер и все потребители времени. */ +} /* Завершает доступ к экземпляру переносимого ядра. */ +void gt_f28335_ready(void) /* Публикует готовность прикладного оборудования после загрузки параметров. */ +{ /* Не объявляет успех при отказе конфигурации порта. */ + if (initialized) gt_ready(&board_clock); /* Первый пакет будет послан из main даже при уже полученном времени. */ +} /* Завершает hook завершения инициализации платы. */ +void gt_f28335_tick_1ms(void) /* Позволяет использовать существующий периодический ISR вместо Timer2. */ +{ /* Приложение выбирает один источник миллисекунд, исключая двойной отсчёт. */ + if (initialized) gt_tick_1ms(&board_clock); /* Продвигает локальные и календарные часы только после явного init. */ +} /* Завершает универсальный миллисекундный hook. */ +void gt_f28335_rx_isr(void) /* Захватывает CAN-время и момент получения без календарной обработки в ISR. */ +{ /* Вызывается существующим обработчиком ECAN0INTA до очистки CANGIF и PIEACK. */ + uint32_t low, high, uptime; /* Однократные 32-битные чтения не рвут слова аппаратного mailbox. */ + uint16_t i, state; /* Индекс восьми октетов и прежняя маска прерываний. */ + if (!initialized || !(ECanaRegs.CANRMP.all & GT_RX)) return; /* Не читает MBOX7 по прерыванию другого CAN-сервиса. */ + state = enter(0); /* Исключает вложенный таймер между чтением uptime и публикацией полученного кадра. */ + uptime = board_clock.stamp.uptime_ms; /* Фиксирует момент RX в монотонных миллисекундах, независимых от синхронизации. */ + pending_frame.id = ECanaMboxes.MBOX7.MSGID.all & 0x1FFFFFFFUL; /* Удаляет служебные IDE/AME биты регистра из 29-битного ID. */ + pending_frame.extended = (uint16_t)(ECanaMboxes.MBOX7.MSGID.all >> 31U); /* Ядро дополнительно проверит IDE даже при аппаратной фильтрации. */ + pending_frame.remote = ECanaMboxes.MBOX7.MSGCTRL.bit.RTR; /* Сохраняет RTR, чтобы удалённый запрос никогда не устанавливал часы. */ + pending_frame.dlc = ECanaMboxes.MBOX7.MSGCTRL.bit.DLC; /* Сохраняет фактическую длину, включая ошибочный или пустой payload. */ + low = ECanaMboxes.MBOX7.MDL.all; /* Читает первые четыре октета одним 32-битным доступом eCAN. */ + high = ECanaMboxes.MBOX7.MDH.all; /* Читает оставшиеся четыре октета без указателей на байты C28x. */ + for (i = 0U; i < 8U; ++i) pending_frame.data[i] = (uint16_t)(((i < 4U ? low : high) >> (8U * (i % 4U))) & 255UL); /* Извлекает каждый проводной октет при DBO=0 в отдельное 16-битное слово. */ + received_at = uptime; /* Публикует временную привязку того же аппаратного кадра. */ + pending_rx = 1U; /* Отмечает полностью готовый снимок; main читает его под той же блокировкой. */ + ECanaRegs.CANRMP.all = GT_RX; /* Освобождает MBOX7 только после полного копирования защищённого CANOPC кадра. */ + ECanaRegs.CANRML.all = GT_RX; /* Сбрасывает индикатор пропущенного кадра, сохраняя уже скопированное опорное время. */ + leave(0, state); /* Возвращает исходный INTM обработчика CAN. */ +} /* Завершает короткий ISR без отправки, ожидания CAN и расчёта календаря. */ +void gt_f28335_service(void) /* Обрабатывает часы и готовность из единственного основного цикла. */ +{ /* Отложенная календарная работа не увеличивает длительность CAN ISR. */ + gt_frame frame; /* Локальная копия допускает замену следующего pending кадра новым CAN ISR. */ + uint32_t uptime; /* Сохраняет привязку прочитанного кадра до выхода из критической секции. */ + uint16_t state, have_rx; /* Разделяет короткий захват очереди и длительную проверку календаря. */ + if (!initialized) return; /* Не обращается к неинициализированным callbacks после отказа init. */ + state = enter(0); /* Блокирует ISR только на время копирования одного небольшого кадра. */ + have_rx = pending_rx; /* Запоминает наличие кадра перед освобождением слота. */ + frame = pending_frame; /* Копирует payload и заголовок согласованно с моментом RX. */ + uptime = received_at; /* Забирает момент получения того же кадра. */ + pending_rx = 0U; /* Разрешает приём следующего broadcast, пока main рассчитывает дату текущего. */ + leave(0, state); /* Возобновляет IRQ до календарных циклов. */ + if (have_rx) (void)gt_receive(&board_clock, &frame, uptime); /* Проверяет время и добавляет тики, прошедшие с RX ISR. */ + if (tx_busy && ((ECanaRegs.CANTA.all | ECanaRegs.CANAA.all) & GT_TX)) { /* Освобождает TX только после подтверждения передачи либо аппаратной отмены. */ + ECanaRegs.CANTA.all = GT_TX; /* Сбрасывает только ACK собственного mailbox. */ + ECanaRegs.CANAA.all = GT_TX; /* Сбрасывает только флаг отмены собственного mailbox. */ + tx_busy = 0U; /* Следующий gt_poll сможет повторить готовность, если время ещё не получено. */ + } /* При отсутствии ACK запрос остаётся аппаратному CAN для автоматической передачи после восстановления шины. */ + gt_poll(&board_clock); /* Объявляет готовность и ограничивает повторы одной секундой локального времени. */ +} /* Завершает неблокирующий проход обслуживания. */ +__interrupt void gt_f28335_timer2_isr(void) /* Обслуживает прямой CPU INT14 без использования группы PIE. */ +{ /* Этот вектор принадлежит библиотеке только после явного timer2_start. */ + CpuTimer2Regs.TCR.all = 0xC000U; /* TIF[15]=1 очищает флаг, TIE[14]=1 сохраняет IRQ, TSS=0 продолжает счёт. */ + gt_f28335_tick_1ms(); /* Один период Timer2 соответствует одной миллисекунде программных часов. */ +} /* Для прямого INT14 подтверждение PIEACK не требуется. */ +int gt_f28335_timer2_start(uint32_t cpu_hz) /* Опционально запускает выделенный таймер с периодом ровно 1 мс. */ +{ /* Частота поступает от проекта; порт не предполагает жёстко заданные 150 МГц. */ + uint16_t state; /* Сохраняет прежнюю глобальную маску IRQ на время смены вектора. */ + if (!initialized || cpu_hz < 1000UL || cpu_hz % 1000UL != 0UL) return 0; /* Отвергает период с дробным числом тактов вместо накопления скрытого дрейфа. */ + state = enter(0); /* Исключает запуск INT14 со старым вектором и новым периодом. */ + CpuTimer2Regs.TCR.all = 0x0010U; /* TSS[4]=1 останавливает счёт, TIE=0 временно запрещает IRQ Timer2. */ + CpuTimer2Regs.PRD.all = cpu_hz / 1000UL - 1UL; /* Период включает значения PRD..0; при 150 МГц это 149999, то есть 150000 тактов за 1 мс. */ + CpuTimer2Regs.TPR.all = 0U; /* Нулевой младший предделитель означает один такт CPU на шаг счётчика. */ + CpuTimer2Regs.TPRH.all = 0U; /* Нулевой старший предделитель не добавляет деления частоты. */ + EALLOW; /* Разрешает запись защищённой таблицы векторов прерываний TI. */ + PieVectTable.TINT2 = >_f28335_timer2_isr; /* Подключает собственный обработчик прямого CPU Timer2/INT14. */ + EDIS; /* Закрывает доступ к векторной таблице после установки обработчика. */ + IFR &= (uint16_t)~M_INT14; /* Снимает старый pending CPU INT14 до запуска нового периодического такта. */ + IER |= M_INT14; /* Разрешает только INT14, сохраняя маски остальных CPU-прерываний. */ + timer_owned = 1U; /* Позволяет stop изменять Timer2 только после явного захвата этим портом. */ + CpuTimer2Regs.TCR.all = 0xC020U; /* TIF=1 очищает флаг, TIE=1 разрешает IRQ, TRB=1 загружает PRD, TSS=0 запускает таймер. */ + leave(0, state); /* Сохраняет выбор приложения, когда разрешить глобальные прерывания. */ + return 1; /* Приложение должно исключить параллельный вызов tick из своего Timer1. */ +} /* Завершает настройку автономного миллисекундного такта. */ +void gt_f28335_timer2_stop(void) /* Освобождает аппаратный такт, ранее явно запущенный библиотекой. */ +{ /* Не трогает чужой Timer2, если выбран hook существующего таймера. */ + uint16_t state = enter(0); /* Не допускает INT14 между остановкой счётчика и снятием разрешения CPU. */ + if (timer_owned) { /* Ограничивает изменения ресурсом, захваченным этим портом. */ + CpuTimer2Regs.TCR.all = 0x8010U; /* TIF=1 очищает флаг, TSS=1 останавливает таймер, TIE=0 запрещает источник IRQ. */ + IER &= (uint16_t)~M_INT14; /* Убирает только разрешение прямого Timer2 IRQ, сохраняя остальные CPU-сервисы. */ + timer_owned = 0U; /* Фиксирует освобождение источника аппаратного такта. */ + } /* При отсутствии владения никакие регистры таймера не меняются. */ + leave(0, state); /* Возвращает прежнее состояние глобальной маски IRQ. */ +} /* Завершает необязательную остановку аппаратных часов. */ diff --git a/c/global-time/ports/tms320f28335/global_time_f28335.h b/c/global-time/ports/tms320f28335/global_time_f28335.h new file mode 100644 index 0000000..92ccbce --- /dev/null +++ b/c/global-time/ports/tms320f28335/global_time_f28335.h @@ -0,0 +1,19 @@ +#ifndef GLOBAL_TIME_F28335_H /* Защищает порт F28335 от повторного включения. */ +#define GLOBAL_TIME_F28335_H /* Отмечает единственный экземпляр аппаратного порта. */ +#include "global_time.h" /* Экспортирует тот же снимок метки и календарный API, что переносимое ядро. */ +#ifdef __cplusplus /* Сохраняет возможность подключения из приложения C++. */ +extern "C" { /* Фиксирует C ABI аппаратных точек входа. */ +#endif /* Завершает условное открытие C ABI. */ +int gt_f28335_init(uint16_t device_type, uint16_t device_id); /* После InitECan и InitPieVectTable, до IRQ: занимает только eCAN-A MBOX7 RX и MBOX8 TX. */ +int gt_f28335_init_periph(uint16_t periph); /* Преобразует PERIPH 0..31 в Type=1/2, ID=0..15 по реестру ProtoCAN. */ +gt_clock *gt_f28335_clock(void); /* Даёт логгеру ядро для gt_get; указатель действителен после успешного init. */ +void gt_f28335_ready(void); /* Вызывается после успешной инициализации всей платы, затем отправка происходит из service. */ +void gt_f28335_tick_1ms(void); /* Подключается к существующему ISR 1 мс, если отдельный Timer2 не используется. */ +void gt_f28335_rx_isr(void); /* Вызывается общим ECAN0INTA ISR до очистки общих флагов; не выполняет календарных циклов. */ +void gt_f28335_service(void); /* Вызывается в main: применяет последний RX, обслуживает ACK и повторы готовности без ожиданий. */ +int gt_f28335_timer2_start(uint32_t cpu_hz); /* Необязательный такт: занимает CPU Timer2/INT14, принимает частоту CPU в Гц, кратную 1000. */ +void gt_f28335_timer2_stop(void); /* Останавливает только ранее запущенный портом Timer2, сохраняя остальные разрешения CPU IRQ. */ +#ifdef __cplusplus /* Закрывает C ABI при использовании C++. */ +} /* Завершает блок аппаратных функций с C-связыванием. */ +#endif /* Завершает языковую проверку. */ +#endif /* Завершает защиту заголовка порта. */ diff --git a/c/global-time/src/global_time.c b/c/global-time/src/global_time.c new file mode 100644 index 0000000..5ef014c --- /dev/null +++ b/c/global-time/src/global_time.c @@ -0,0 +1,156 @@ +#include "global_time.h" /* Подключает переносимые типы, API часов и фиксированные поля ProtoCAN. */ +#include /* Использует явный сброс состояния вместо предположений о старте TI COFF. */ +static uint16_t month_days(uint16_t year, uint16_t month) /* Вычисляет длину уже проверенного месяца по григорианскому календарю. */ +{ /* Ограничивает область таблицы календаря и условия високосного года. */ + static const uint16_t days[12] = {31,28,31,30,31,30,31,31,30,31,30,31}; /* Число суток каждого месяца обычного года. */ + return (uint16_t)(days[month - 1U] + (month == 2U && year % 4U == 0U && (year % 100U != 0U || year % 400U == 0U))); /* Добавляет 29 февраля, включая исключение для 2100 года. */ +} /* Завершает календарный расчёт без обращения к платформе. */ +static int date_seconds(const gt_datetime *date, uint32_t *seconds) /* Проверяет все входные поля до индексации календарных таблиц. */ +{ /* Собирает секунды эпохи отдельно от критической секции часов. */ + uint16_t year, month; /* Счётчики лет и месяцев нужны только при редкой синхронизации. */ + uint32_t days = 0; /* Накопленные сутки от начала 2000 года; диапазон CAN не превышает 36525 суток. */ + if (!date || date->year < 2000U || date->year > 2099U || date->month < 1U || date->month > 12U) return 0; /* Отвергает неподдерживаемую эпоху и неверный индекс месяца. */ + if (date->day < 1U || date->day > month_days(date->year, date->month) || date->weekday > 6U) return 0; /* Принимает только существующую дату и формат дня недели действующего STM32 RTCSETUP. */ + if (date->hour > 23U || date->minute > 59U || date->second > 59U || date->millisecond > 999U) return 0; /* Исключает перенос некорректных полей в соседние сутки. */ + for (year = 2000U; year < date->year; ++year) days += 365UL + (month_days(year, 2U) == 29U); /* Учитывает каждый полный год относительно фиксированной эпохи. */ + for (month = 1U; month < date->month; ++month) days += month_days(date->year, month); /* Прибавляет полные месяцы текущего года. */ + days += date->day - 1U; /* Переводит календарное число 1..31 в число завершённых суток. */ + *seconds = days * 86400UL + (uint32_t)date->hour * 3600UL + (uint32_t)date->minute * 60UL + date->second; /* Константы задают секунд в сутках, часе и минуте; UL исключает 16-битное умножение C28x. */ + return 1; /* Подтверждает полностью проверенную календарную величину. */ +} /* Завершает преобразование календаря в секундную шкалу. */ +uint64_t gt_timestamp_ms(const gt_stamp *stamp) /* Позволяет логгеру сохранить абсолютное время без потери миллисекунд. */ +{ /* Выполняет расчёт по уже снятому атомарному снимку. */ + return (uint64_t)stamp->seconds * 1000ULL + stamp->millisecond; /* Расширяет до умножения, чтобы календарная метка не переполняла uint32_t. */ +} /* Завершает получение широкой метки времени. */ +int gt_init(gt_clock *clock, const gt_port *port, uint16_t device_type, uint16_t device_id) /* Создаёт часы до запуска прерываний устройства. */ +{ /* Отказывается от конфигурации без реальной защиты общего состояния. */ + if (!clock || !port || !port->enter || !port->leave || !port->send || device_type > 7U || device_id > 15U) return 0; /* Не обрезает ошибочный CAN-адрес до адреса другой платы. */ + memset(clock, 0, sizeof(*clock)); /* Сбрасывает в том числе неинициализированную .ebss TI COFF и старый признак синхронизации. */ + clock->port = *port; /* Сохраняет callbacks без выделения динамической памяти. */ + clock->device_type = device_type; /* Запоминает тип источника пакета готовности. */ + clock->device_id = device_id; /* Запоминает экземпляр источника пакета готовности. */ + clock->stamp.weekday = 6U; /* Начальная локальная эпоха 2000-01-01 приходится на субботу при 0=воскресенье. */ + return 1; /* Разрешает приложению подключать такт и CAN только после успеха. */ +} /* Завершает обязательную явную инициализацию. */ +void gt_tick_1ms(gt_clock *clock) /* Выполняет короткий шаг часов из миллисекундного ISR. */ +{ /* Защищает разрядные переносы и от вложенных читателей. */ + uint16_t state = clock->port.enter(clock->port.user); /* Сохраняет прежний INTM, чтобы выход из ISR не разрешил вложенные IRQ. */ + ++clock->stamp.uptime_ms; /* Продвигает независимое время работы modulo 2^32 миллисекунд. */ + if (++clock->stamp.millisecond == 1000U) { /* Переносит ровно 1000 аппаратных миллисекунд в одну секунду. */ + clock->stamp.millisecond = 0U; /* Начинает следующую секунду с нулевой дробной части. */ + if (++clock->stamp.seconds % 86400UL == 0UL) clock->stamp.weekday = (clock->stamp.weekday + 1U) % 7U; /* При переходе суток продвигает принятый день недели по циклу из семи дней. */ + } /* Завершает редкую секундную ветвь без календарных циклов в ISR. */ + clock->port.leave(clock->port.user, state); /* Восстанавливает исходную маску IRQ вызывающего кода. */ +} /* Завершает единственный источник продвижения часов. */ +void gt_get(gt_clock *clock, gt_stamp *out) /* Снимает согласованные дату, долю секунды и качество синхронизации. */ +{ /* Не допускает разрыва 32/64-битных полей на 16-битной шине C28x. */ + uint16_t state = clock->port.enter(clock->port.user); /* Блокирует изменение снимка таймером и CAN до завершения копирования. */ + *out = clock->stamp; /* Копирует все поля в пределах одной критической секции. */ + clock->port.leave(clock->port.user, state); /* Восстанавливает IRQ без безусловного EINT. */ +} /* Завершает чтение метки для логгера и тайм-аутов. */ +void gt_ready(gt_clock *clock) /* Отмечает фактическое завершение инициализации прикладных подсистем. */ +{ /* Разделяет настройку часов и объявление готовности всей платы. */ + clock->ready = 1U; /* Повторные вызовы не сбрасывают историю отправок или синхронизаций. */ +} /* Передачу сообщения выполнит основной цикл. */ +void gt_poll(gt_clock *clock) /* Обслуживает объявление готовности без задержек и ожидания CAN. */ +{ /* Разрешён один вызывающий основной цикл; callback send копирует временный кадр. */ + gt_stamp now; /* Согласованное время определяет частоту повторов независимо от перевода часов. */ + gt_frame frame; /* Локальный пакет не занимает место в постоянной очереди ядра. */ + if (!clock->ready) return; /* Плата не сообщает об успехе раньше завершения своей инициализации. */ + gt_get(clock, &now); /* Снимает монотонную базу и признак получения RTCSETUP. */ + if (clock->announced && (now.synchronized || (uint32_t)(now.uptime_ms - clock->sent_uptime_ms) < GT_READY_RETRY_MS)) return; /* После синхронизации прекращает повторы, сохраняя обязательную первую отправку. */ + memset(&frame, 0, sizeof(frame)); /* Явно обнуляет RTR и неиспользуемые октеты сообщения. */ + frame.id = 0x18010000UL | ((uint32_t)clock->device_type << 24U) | ((uint32_t)clock->device_id << 20U) | GT_READY_BODY; /* Задаёт стандартный приоритет, Route=1, DISCRETE=1 и FLAGS=3. */ + frame.extended = 1U; /* Использует только штатный расширенный идентификатор ProtoCAN. */ + frame.dlc = 1U; /* Одного октета хватает для общего флага готовности. */ + frame.data[0] = 1U; /* Бит 0 означает завершение инициализации; прочие биты зарезервированы нулями. */ + if (clock->port.send(clock->port.user, &frame)) { /* Не считает занятый mailbox успешной отправкой. */ + clock->announced = 1U; /* Фиксирует приём транспортом, а не отдельный прикладной ACK мастера. */ + clock->sent_uptime_ms = now.uptime_ms; /* Начинает секундный интервал до повтора при отсутствии RTCSETUP. */ + } /* При отказе транспорта следующая итерация main повторит попытку без ожидания. */ +} /* Завершает неблокирующее обслуживание готовности. */ +static int data_frame(const gt_frame *frame) /* Выполняет общую проверку контейнера classic CAN. */ +{ /* Не позволяет служебным битам драйвера участвовать в разборе ID. */ + return frame && frame->extended == 1U && !frame->remote && frame->id <= 0x1FFFFFFFUL; /* Требует IDE=1, RTR=0 и ровно 29 адресных битов. */ +} /* Завершает общую проверку адресного формата. */ +int gt_parse_ready(const gt_frame *frame, uint16_t *device_type, uint16_t *device_id) /* Распознаёт на верхнем уровне только определённый пакет готовности. */ +{ /* Не путает событие старта с пульсом или произвольными дискретными флагами. */ + if (!data_frame(frame) || (frame->id & 0x080FFFFFUL) != (0x08010000UL | GT_READY_BODY) || frame->dlc != 1U || frame->data[0] != 1U) return 0; /* Проверяет Route=1, MsgType=1, Body=0x3000 и единственный разрешённый флаг. */ + if (device_type) *device_type = (uint16_t)((frame->id >> 24U) & 7UL); /* Возвращает трёхбитный тип источника при наличии выходного указателя. */ + if (device_id) *device_id = (uint16_t)((frame->id >> 20U) & 15UL); /* Возвращает четырёхбитный номер экземпляра источника. */ + return 1; /* Верхний уровень может ответить одним общим RTCSETUP всем устройствам. */ +} /* Завершает распознавание стартового события мастером. */ +int gt_make_rtc(const gt_datetime *date, gt_frame *out) /* Кодирует существующую команду синхронизации для верхнего уровня. */ +{ /* Передаёт ровно те семь полей, которые принимает STM32-порт ProtoCAN. */ + uint32_t seconds; /* При проверке календаря получает допустимую секундную величину. */ + if (!out || !date_seconds(date, &seconds) || date->millisecond != 0U) return 0; /* Не отбрасывает ненулевые миллисекунды молча: проводной формат их не содержит. */ + memset(out, 0, sizeof(*out)); /* Нули задают RTR=0, критический приоритет и резерв payload. */ + out->id = GT_RTC_ID; /* Передаёт BROADCAST RTCSETUP от верхнего уровня с нулевым параметром команды. */ + out->extended = 1U; /* Выбирает расширенный 29-битный CAN-кадр. */ + out->dlc = 7U; /* Сохраняет существующий контракт DLC=7 без добавления миллисекундного расширения. */ + out->data[0] = date->hour; /* Первый октет содержит часы в двоичном представлении. */ + out->data[1] = date->minute; /* Второй октет содержит минуты. */ + out->data[2] = date->second; /* Третий октет содержит целые секунды. */ + out->data[3] = date->year - 2000U; /* Четвёртый октет хранит смещение года 0..99 от эпохи 2000. */ + out->data[4] = date->month; /* Пятый октет содержит месяц 1..12. */ + out->data[5] = date->day; /* Шестой октет содержит проверенное число месяца. */ + out->data[6] = date->weekday; /* Седьмой октет сохраняет принятый формат дня недели 0..6. */ + return 1; /* Кадр готов к неблокирующей отправке транспортом мастера. */ +} /* Завершает кодирование общего broadcast. */ +int gt_receive(gt_clock *clock, const gt_frame *frame, uint32_t received_uptime_ms) /* Проверяет и применяет RTCSETUP, привязанный к моменту приёма. */ +{ /* Отвергает некорректные кадры до изменения состояния устройства. */ + gt_datetime date; /* Временный календарь строится из отдельных октетов без struct overlay. */ + uint32_t seconds, elapsed, corrected_seconds, whole_days; /* Содержит секунды мастера, задержку в мс, актуальные секунды и перенос дня недели. */ + uint16_t i, state; /* Индекс проверки payload и сохранённое состояние IRQ. */ + uint16_t corrected_ms; /* Хранит дробную часть отдельно, исключая дорогие 64-битные деления под маской IRQ C28x. */ + if (!data_frame(frame) || (frame->id & 0x0FFFFFFFUL) != GT_RTC_ID || frame->dlc != 7U) return 0; /* Принимает оба приоритета, но только Route=0, Type/ID=0, BROADCAST RTCSETUP с параметром 0. */ + for (i = 0U; i < 7U; ++i) if (frame->data[i] > 255U) return 0; /* C28x хранит октет в 16-битном элементе; старшие биты означают ошибку адаптера. */ + date.hour = frame->data[0]; /* Восстанавливает часы по штатному порядку RTCSETUP. */ + date.minute = frame->data[1]; /* Восстанавливает минуты без BCD-преобразований. */ + date.second = frame->data[2]; /* Восстанавливает целые секунды мастера. */ + date.year = 2000U + frame->data[3]; /* Восстанавливает полный год из двухзначного поля протокола. */ + date.month = frame->data[4]; /* Читает номер месяца до проверки таблицы длительности. */ + date.day = frame->data[5]; /* Читает число месяца для строгой календарной проверки. */ + date.weekday = frame->data[6]; /* Сохраняет присланную мастером фазу недели. */ + date.millisecond = 0U; /* Семь октетов RTCSETUP задают точность источника только до секунды. */ + if (!date_seconds(&date, &seconds)) return 0; /* Неверные даты не сбрасывают часы и признак предыдущей синхронизации. */ + state = clock->port.enter(clock->port.user); /* Защищает сравнение, перенос и публикацию новой временной базы. */ + elapsed = clock->stamp.uptime_ms - received_uptime_ms; /* Учитывает время очереди main, в том числе через переполнение uint32_t uptime. */ + corrected_seconds = seconds + elapsed / 1000UL; /* Прибавляет полные секунды задержки RX; диапазон входных годов и задержка до 2^32 мс помещаются в uint32_t. */ + corrected_ms = (uint16_t)(elapsed % 1000UL); /* Восстанавливает фазу миллисекунд без 64-битного деления в критической секции. */ + clock->stamp.correction_ms = ((int64_t)corrected_seconds - clock->stamp.seconds) * 1000LL + corrected_ms - clock->stamp.millisecond; /* Сохраняет знак и полную величину смещения, расширяя разность секунд до умножения. */ + whole_days = corrected_seconds / 86400UL - seconds / 86400UL; /* Определяет переходы через полночь за время отложенной обработки. */ + clock->stamp.seconds = corrected_seconds; /* Публикует новые секунды независимо от монотонного uptime. */ + clock->stamp.millisecond = corrected_ms; /* Публикует долю секунды, соответствующую текущему локальному тику. */ + clock->stamp.weekday = (uint16_t)((date.weekday + whole_days) % 7UL); /* Сохраняет правильный день недели даже при обработке после полуночи. */ + clock->stamp.sync_uptime_ms = received_uptime_ms; /* Позволяет потребителю вычислить возраст последнего опорного пакета. */ + ++clock->stamp.generation; /* Отмечает любую принятую коррекцию, включая перевод часов назад. */ + clock->stamp.synchronized = 1U; /* Последней записью подтверждает валидность полностью опубликованной даты. */ + clock->port.leave(clock->port.user, state); /* Возвращает прежнюю маску прерываний после атомарного обновления. */ + return 1; /* Сообщает транспорту об успешной обработке синхронизации. */ +} /* Завершает сравнение и немедленную коррекцию часов. */ +int gt_calendar(const gt_stamp *stamp, gt_datetime *out) /* Преобразует снимок для человека без блокировки аппаратного таймера. */ +{ /* Вызывает календарные циклы только по требованию логгера или интерфейса. */ + uint32_t days, remainder; /* Разделяет полные сутки и секунды внутри суток. */ + uint16_t year = 2000U, month = 1U, length; /* Начинает разбор от эпохи библиотеки. */ + if (!stamp || !out || !stamp->synchronized) return 0; /* Не выдаёт локальный отсчёт до синхронизации за известную календарную дату. */ + days = stamp->seconds / 86400UL; /* Выделяет количество завершённых календарных суток. */ + remainder = stamp->seconds % 86400UL; /* Сохраняет секунды текущих суток отдельно от календарных вычислений. */ + while (days >= (uint32_t)(length = (uint16_t)(365U + (month_days(year, 2U) == 29U)))) { /* Вычитает полные годы с учётом григорианских исключений. */ + days -= length; /* Оставляет только сутки после текущего полного года. */ + ++year; /* Переходит к следующему году, включая работу после 2099 без нового CAN-пакета. */ + } /* Завершает поиск календарного года. */ + while (days >= (length = month_days(year, month))) { /* Вычитает полные месяцы найденного года. */ + days -= length; /* Сохраняет остаток суток внутри следующего месяца. */ + ++month; /* Продвигает месяц до того, которому принадлежит остаток. */ + } /* Завершает поиск календарного месяца. */ + out->year = year; /* Возвращает полный календарный год. */ + out->month = month; /* Возвращает месяц в привычном диапазоне 1..12. */ + out->day = (uint16_t)(days + 1UL); /* Переводит нулевой индекс остатка суток в календарное число. */ + out->weekday = stamp->weekday; /* Возвращает фазу недели из согласованного снимка, а не из текущих часов. */ + out->hour = (uint16_t)(remainder / 3600UL); /* Выделяет целые часы внутри суток. */ + out->minute = (uint16_t)((remainder % 3600UL) / 60UL); /* Выделяет минуты после последнего полного часа. */ + out->second = (uint16_t)(remainder % 60UL); /* Возвращает остаток секунд текущей минуты. */ + out->millisecond = stamp->millisecond; /* Сохраняет миллисекундную долю исходного снимка. */ + return 1; /* Подтверждает наличие календарной синхронизации в исходной метке. */ +} /* Завершает календарное представление метки логгера. */ diff --git a/c/global-time/tests/f28335_stubs/DSP2833x_Device.h b/c/global-time/tests/f28335_stubs/DSP2833x_Device.h new file mode 100644 index 0000000..a4659db --- /dev/null +++ b/c/global-time/tests/f28335_stubs/DSP2833x_Device.h @@ -0,0 +1,26 @@ +#ifndef GT_TEST_DSP2833X_DEVICE_H /* Изолирует модель регистров от настоящих заголовков TI. */ +#define GT_TEST_DSP2833X_DEVICE_H /* Предотвращает повторное определение тестовых типов. */ +#include /* Использует одинаковые разрядности полей на host-платформе. */ +#define EALLOW ((void)0) /* Модель не требует аппаратного разрешения защищённой записи. */ +#define EDIS ((void)0) /* Закрытие защищённой записи не меняет модельные регистры. */ +#define __interrupt /* Host-компилятор проверяет обычное тело ISR без ABI TI. */ +#define M_INT14 0x2000U /* CPU INT14 соответствует биту 13 в IER/IFR TMS320F28335. */ +union gt_test_reg { /* Даёт модели 32-битный доступ all и поля, которые использует настоящий порт. */ + uint32_t all; /* Имитирует единственный аппаратный доступ к регистру eCAN. */ + struct { unsigned DLC:4; unsigned RTR:1; unsigned DBO:1; } bit; /* Для модели важны доступные имена; фактическую раскладку проверяет отдельная TI-компиляция. */ +}; /* Завершает минимальную модель регистра. */ +struct gt_test_mbox { union gt_test_reg MSGID, MSGCTRL, MDL, MDH; }; /* Выделяет поля mailbox, реально используемые драйвером. */ +struct gt_test_mboxes { struct gt_test_mbox MBOX7, MBOX8; }; /* Не предоставляет портам доступ к невыделенным mailbox. */ +struct gt_test_ecan { union gt_test_reg CANME, CANMD, CANOPC, CANMIL, CANMIM, CANRMP, CANRML, CANTA, CANAA, CANTRS, CANMC; }; /* Моделирует только задействованные управляющие регистры CAN. */ +struct gt_test_lam { union gt_test_reg LAM7; }; /* Предоставляет локальную маску только своего RX. */ +struct gt_test_timer { union gt_test_reg TCR, PRD, TPR, TPRH; }; /* Хранит настройку периода и предделителей CPU Timer2. */ +struct gt_test_vectors { void (*TINT2)(void); }; /* Позволяет вызвать реальный обработчик порта через установленный им вектор. */ +extern struct gt_test_mboxes ECanaMboxes; /* Делит модельные mailbox между портом и тестом. */ +extern struct gt_test_ecan ECanaRegs; /* Делит управляющие регистры между кодом порта и проверками. */ +extern struct gt_test_lam ECanaLAMRegs; /* Делит состояние аппаратного фильтра. */ +extern struct gt_test_timer CpuTimer2Regs; /* Делит конфигурацию автономного миллисекундного таймера. */ +extern struct gt_test_vectors PieVectTable; /* Делит назначенный Timer2 вектор. */ +extern uint16_t IER, IFR; /* Имитирует CPU-регистры разрешения и ожидания прерываний. */ +uint16_t __disable_interrupts(void); /* Предоставляет модель intrinsic с реальным сохранением прежнего состояния. */ +void __restore_interrupts(uint16_t state); /* Предоставляет модель точного восстановления IRQ. */ +#endif /* Завершает защиту тестовой модели аппаратуры. */ diff --git a/c/global-time/tests/run_tests.py b/c/global-time/tests/run_tests.py new file mode 100644 index 0000000..3410ad2 --- /dev/null +++ b/c/global-time/tests/run_tests.py @@ -0,0 +1,36 @@ +"""Проверяет ядро и настоящий порт на host; опционально компилирует их TI C28x.""" # Описывает границу проверки без обещания аппаратного испытания. +import argparse # Принимает явные пути TI SDK вместо привязки библиотеки к рабочему компьютеру. +import os # Настраивает PATH компилятора, чтобы Windows находила его DLL. +from pathlib import Path # Строит пути относительно самого тестового набора. +import shutil # Находит GCC или Clang в окружении разработчика. +import subprocess # Запускает компиляторы аргументами без shell-интерполяции. +import tempfile # Изолирует все исполняемые и объектные файлы во временном каталоге. +root = Path(__file__).resolve().parents[1] # Определяет корень c/global-time независимо от текущего каталога. +parser = argparse.ArgumentParser(description=__doc__) # Показывает назначение host- и TI-проверок при --help. +parser.add_argument("--cc", default=os.environ.get("CC")) # Разрешает явно выбрать совместимый host-компилятор. +parser.add_argument("--ti-cgt", type=Path) # Принимает каталог TI C2000 compiler с bin/cl2000 и include. +parser.add_argument("--ti-headers", type=Path) # Принимает каталог с настоящим DSP2833x_Device.h проекта или TI SDK. +parser.add_argument("--ti-common", type=Path) # Принимает дополнительный каталог DSP2833x_GlobalPrototypes.h из DSP2833x_common/include. +args = parser.parse_args() # Проверяет параметры до создания любых файлов сборки. +candidates = [args.cc, shutil.which("gcc"), shutil.which("clang"), "C:/msys64/ucrt64/bin/gcc.exe"] # Последний путь обеспечивает привычное Windows-окружение без изменения глобального PATH. +compiler = next((str(Path(p).resolve()) for p in candidates if p and Path(p).is_file()), None) # Выбирает первый существующий компилятор, сохраняя явный --cc в приоритете. +if not compiler: # Не заменяет отсутствие компилятора успешным пустым запуском. + parser.error("Укажите --cc с путём к GCC/Clang") # Даёт воспроизводимое требование для запуска регрессий. +if bool(args.ti_cgt) != bool(args.ti_headers): # TI проверка требует одновременно компилятор и аппаратные заголовки. + parser.error("--ti-cgt и --ti-headers нужно указать вместе") # Предотвращает случайную проверку порта с host-моделью вместо SDK. +env = dict(os.environ) # Сохраняет исходное окружение остальных инструментов. +env["PATH"] = str(Path(compiler).parent) + os.pathsep + env.get("PATH", "") # Добавляет соседние DLL GCC только в дочерние процессы тестов. +with tempfile.TemporaryDirectory(prefix="global-time-") as output: # Удаляет только собственный временный каталог после завершения проверок. + for name, sources, includes in [("core", ["src/global_time.c", "tests/test_global_time.c"], ["include"]), ("f28335", ["src/global_time.c", "ports/tms320f28335/global_time_f28335.c", "tests/test_f28335.c"], ["include", "ports/tms320f28335", "tests/f28335_stubs"])]: # Проверяет ядро отдельно и реальный порт с модельными регистрами. + exe = Path(output) / (name + (".exe" if os.name == "nt" else "")) # Сохраняет host-программы за пределами исходного дерева. + command = [compiler, "-std=c99", "-Wall", "-Wextra", "-Werror", "-pedantic", *["-I" + str(root / p) for p in includes], *[str(root / p) for p in sources], "-o", str(exe)] # Считает все предупреждения ошибками и использует реальные исходники библиотеки. + subprocess.run(command, check=True, env=env, timeout=60) # Не маскирует ошибку компиляции успешным запуском старого EXE. + subprocess.run([str(exe)], check=True, env=env, timeout=30) # Выполняет календарные и аппаратные сценарии с ограниченным временем. + if args.ti_cgt: # Дополнительная проверка нужна для ABI с 16-битным char и настоящих регистров TI. + ti = args.ti_cgt.resolve() # Фиксирует абсолютный корень выбранного SDK компилятора. + for source in ["src/global_time.c", "ports/tms320f28335/global_time_f28335.c"]: # Компилирует оба поставляемых MCU исходника без изменения прошивки потребителя. + obj = Path(output) / (Path(source).stem + ".obj") # Разделяет объектные файлы ядра и аппаратного адаптера. + include_paths = [root / "include", root / "ports/tms320f28335", args.ti_headers.resolve(), ti / "include", *([args.ti_common.resolve()] if args.ti_common else [])] # Отдельно подключает common-заголовки старого TI SDK, не смешивая их с host-моделью. + command = [str(ti / "bin/cl2000.exe"), "--compile_only", "-v28", "-ml", "-mt", "--float_support=fpu32", "--abi=coffabi", "--c99", "-O2", "--opt_for_speed=0", "--display_error_number", *["--include_path=" + str(p) for p in include_paths], "--output_file=" + str(obj), str(root / source)] # Использует large/COFF ABI действующего проекта F28335 и штатные заголовки. + subprocess.run(command, check=True, timeout=60) # Ошибка TI C28x останавливает проверку вместо объявления host-модели достаточной. + print("global-time: TI C28x large/COFF compilation OK") # Подтверждает именно компиляцию объектов, не аппаратные испытания или полную линковку прошивки. diff --git a/c/global-time/tests/test_f28335.c b/c/global-time/tests/test_f28335.c new file mode 100644 index 0000000..76c3cb2 --- /dev/null +++ b/c/global-time/tests/test_f28335.c @@ -0,0 +1,88 @@ +#include "DSP2833x_Device.h" /* Использует host-модель регистров, а не альтернативную реализацию драйвера. */ +#include "global_time_f28335.h" /* Тестирует публичные hooks настоящего аппаратного порта. */ +#ifdef NDEBUG /* Не позволяет Release-сборке превратить аппаратные проверки в пустую программу. */ +#undef NDEBUG /* Включает assert независимо от флагов конфигурации CMake. */ +#endif /* Завершает настройку обязательных проверяющих утверждений. */ +#include /* Проверяет сохранность соседних ресурсов, обработку CAN и период Timer2. */ +#include /* Выводит краткое подтверждение проверенных сценариев. */ +struct gt_test_mboxes ECanaMboxes; /* Выделяет модельные MBOX7 и MBOX8. */ +struct gt_test_ecan ECanaRegs; /* Выделяет модель управляющих регистров eCAN-A. */ +struct gt_test_lam ECanaLAMRegs; /* Выделяет локальную маску RX. */ +struct gt_test_timer CpuTimer2Regs; /* Выделяет модель CPU Timer2. */ +struct gt_test_vectors PieVectTable; /* Хранит реальный адрес ISR, назначенный портом. */ +uint16_t IER, IFR; /* Выделяет модель CPU-масок INT14 и соседних прерываний. */ +static uint16_t masked; /* Сохраняет модельное состояние глобальной маски INTM. */ +uint16_t __disable_interrupts(void) /* Моделирует атомарный запрет IRQ и возвращаемый предыдущий INTM. */ +{ /* Повторный запрет из ISR обязан вернуть уже установленную маску. */ + uint16_t old = masked; /* Сохраняет прежнюю маску до её изменения. */ + masked = 1U; /* Имитирует запрет маскируемых IRQ. */ + return old; /* Позволяет реальному порту точно восстановить предыдущий режим. */ +} /* Завершает модель intrinsic запрета. */ +void __restore_interrupts(uint16_t state) /* Восстанавливает маску, полученную соответствующим входом. */ +{ /* Проверяет, что порт действительно вызвал вход в критическую секцию. */ + assert(masked == 1U); /* Восстановление без предшествующего запрета нарушило бы вложенность. */ + masked = state; /* Сохраняет исходный ISR-контекст без безусловного разрешения IRQ. */ +} /* Завершает модель intrinsic восстановления. */ +static void inject(const gt_frame *frame) /* Доставляет wire-данные в аппаратный RX и вызывает настоящий ISR hook. */ +{ /* Не вызывает gt_receive напрямую, проверяя весь путь распаковки MDL/MDH. */ + uint16_t i; /* Перебирает восемь проводных октетов фиксированного classic CAN. */ + ECanaMboxes.MBOX7.MSGID.all = 0xC0000000UL | frame->id; /* Имитирует расширенный ID и включённую локальную фильтрацию eCAN. */ + ECanaMboxes.MBOX7.MSGCTRL.all = frame->dlc | ((uint32_t)frame->remote << 4U); /* Укладывает DLC и RTR в модельный регистр сообщения. */ + ECanaMboxes.MBOX7.MDL.all = ECanaMboxes.MBOX7.MDH.all = 0UL; /* Сбрасывает старый payload перед имитацией нового приёма. */ + for (i = 0U; i < 8U; ++i) { /* Укладывает данные младшим октетом вперёд, как eCAN при DBO=0. */ + if (i < 4U) ECanaMboxes.MBOX7.MDL.all |= (uint32_t)frame->data[i] << (8U * i); /* Первые четыре октета занимают MDL. */ + else ECanaMboxes.MBOX7.MDH.all |= (uint32_t)frame->data[i] << (8U * (i - 4U)); /* Следующие четыре октета занимают MDH. */ + } /* Завершает аппаратное размещение кадра. */ + ECanaRegs.CANRMP.all = 0x82UL; /* Выставляет RX7 и независимый pending legacy mailbox1. */ + gt_f28335_rx_isr(); /* Захватывает временную привязку и кадр в очередь настоящего порта. */ + assert(ECanaRegs.CANRMP.all == 0x80UL); /* Запись W1C должна содержать только бит собственного mailbox, сохраняя pending соседа на аппаратуре. */ + ECanaRegs.CANRMP.all = 2UL; /* Вручную применяет W1C к обычной RAM модели: RX7 снят, RX1 сохранён. */ +} /* Завершает модель получения кадра без подмены алгоритма порта. */ +int main(void) /* Проверяет аппаратную настройку и общий путь ISR → main → метка логгера. */ +{ /* Модель не утверждает электрические тайминги или CAN ACK настоящего трансивера. */ + gt_stamp stamp; /* Читает время через тот же атомарный API, которым будет пользоваться логгер. */ + gt_datetime date = {2026U, 10U, 19U, 1U, 12U, 34U, 56U, 0U}; /* Независимый календарный эталон: понедельник 19 октября 2026 года. */ + gt_frame frame; /* Содержит broadcast мастера для доставки через модель eCAN. */ + uint16_t i; /* Управляет известным числом тиков до обработки кадра в main. */ + gt_f28335_tick_1ms(); gt_f28335_rx_isr(); gt_f28335_service(); /* Ранние hooks до init обязаны безопасно завершаться. */ + ECanaRegs.CANME.all = 0x7FUL; /* Резервирует существующие mailbox0..6 для legacy CAN, Boot и pulse. */ + ECanaRegs.CANMD.all = 0xFFFFFFFAUL; /* Воспроизводит исходные направления legacy mailbox проекта 28335_periph. */ + ECanaRegs.CANMIM.all = 0x4AUL; /* Имитирует разрешённые RX1/RX3/RX6 других CAN-сервисов. */ + ECanaRegs.CANMIL.all = 0x40UL; /* Фиксирует чужую маршрутизацию IRQ, которую порт обязан сохранить. */ + assert(!gt_f28335_init_periph(32U)); /* Некорректный физический адрес не должен открыть новый CAN-сервис. */ + assert(gt_f28335_init_periph(5U)); /* Настраивает УМП1 как PERIPH5, Type1/ID5. */ + assert(ECanaRegs.CANME.all == 0x1FFUL && (ECanaRegs.CANMD.all & 0x7FUL) == 0x7AUL); /* Проверяет добавление только mailbox7/8 и сохранность чужих направлений. */ + assert(ECanaRegs.CANMIM.all == 0xCAUL && ECanaRegs.CANMIL.all == 0x40UL); /* Проверяет RX7 IRQ line0 без изменения RX1/RX3/RX6. */ + assert(ECanaMboxes.MBOX7.MSGID.all == 0xC0000030UL && ECanaLAMRegs.LAM7.all == 0x10000000UL); /* Фильтр принимает только время мастера с любым приоритетом. */ + ECanaRegs.CANTA.all = ECanaRegs.CANAA.all = ECanaRegs.CANRMP.all = 0UL; /* Применяет к RAM модели W1C, выполненные init на реальной аппаратуре. */ + gt_f28335_service(); /* Проверяет первый проход main при отсутствии входящего кадра. */ + assert(ECanaRegs.CANTRS.all == 0UL); /* Сам init не должен объявить готовность прикладной платы. */ + gt_f28335_ready(); gt_f28335_service(); /* Публикует прикладной успех и запускает передачу из main. */ + assert(ECanaRegs.CANTRS.all == 0x100UL && ECanaMboxes.MBOX8.MSGID.all == 0x99513000UL && ECanaMboxes.MBOX8.MDL.all == 1UL); /* Проверяет точный аппаратный запрос READY с правильным адресом. */ + assert(gt_make_rtc(&date, &frame)); /* Строит штатный RTCSETUP без аппаратного RTC мастера в тесте. */ + inject(&frame); /* Проверяет разбор всех календарных полей через настоящий RX ISR. */ + for (i = 0U; i < 37U; ++i) gt_f28335_tick_1ms(); /* Имитирует 37 мс задержки main при продолжающемся такте. */ + gt_f28335_service(); /* Применяет отложенный RX и сохраняет привязку к исходному моменту приёма. */ + gt_get(gt_f28335_clock(), &stamp); /* Снимает тот же экземпляр часов, что обслуживает порт. */ + assert(gt_calendar(&stamp, &date) && date.year == 2026U && date.month == 10U && date.day == 19U && date.hour == 12U && date.minute == 34U && date.second == 56U && date.millisecond == 37U); /* Проверяет календарь и компенсацию задержки всего пути приёма. */ + assert(stamp.sync_uptime_ms == 0UL && stamp.uptime_ms == 37UL); /* Очередь не должна подменить время RX временем обработки main. */ + ECanaRegs.CANTA.all = 0x100UL; ECanaRegs.CANTRS.all = 0UL; /* Имитирует аппаратный ACK ранее отправленной готовности. */ + gt_f28335_service(); /* Освобождает собственный TX без повторной отправки после синхронизации. */ + ECanaRegs.CANTA.all = ECanaRegs.CANAA.all = 0UL; /* Применяет аппаратное W1C к RAM модели ACK. */ + assert(ECanaRegs.CANTRS.all == 0UL); /* Синхронизация прекращает повторную стартовую рассылку. */ + IER = 0x1001U; IFR = 0x3001U; /* Подготавливает чужие CPU IRQ и старый pending INT14. */ + assert(!gt_f28335_timer2_start(150000001UL)); /* Некратная 1000 частота не должна давать скрытый дробный период. */ + assert(gt_f28335_timer2_start(150000000UL)); /* Настраивает аппаратные 1 мс для типичной частоты F28335 150 МГц. */ + assert(CpuTimer2Regs.PRD.all == 149999UL && CpuTimer2Regs.TPR.all == 0UL && CpuTimer2Regs.TPRH.all == 0UL); /* Проверяет включительный период PRD..0 и отсутствие предделителей. */ + assert(IER == 0x3001U && IFR == 0x1001U && CpuTimer2Regs.TCR.all == 0xC020UL); /* Сохраняет чужие CPU маски и запускает только Timer2. */ + PieVectTable.TINT2(); /* Вызывает реальное тело ISR через вектор, назначенный портом. */ + gt_get(gt_f28335_clock(), &stamp); /* Проверяет единственный миллисекундный шаг собственного ISR. */ + assert(stamp.uptime_ms == 38UL && stamp.millisecond == 38U); /* Timer2 должен добавлять ровно один тик на прерывание. */ + assert(!gt_f28335_init_periph(6U)); /* Повторный init не должен потерять владение работающим Timer2. */ + gt_f28335_timer2_stop(); /* Освобождает ранее захваченный аппаратный такт. */ + assert(IER == 0x1001U && CpuTimer2Regs.TCR.all == 0x8010UL && masked == 0U); /* Остановка сохраняет чужие IRQ и исходный глобальный INTM. */ + ECanaRegs.CANMC.bit.DBO = 1U; /* Имитирует несовместимый порядок октетов контроллера. */ + assert(!gt_f28335_init_periph(6U)); /* Не позволяет молча декодировать другую аппаратную раскладку payload. */ + puts("global-time F28335: eCAN isolation, RX delay, ACK and Timer2 OK"); /* Выводит компактный результат аппаратных сценариев. */ + return 0; /* Завершает проверку после всех обязательных утверждений. */ +} /* Завершает host-регрессию порта TI F28335. */ diff --git a/c/global-time/tests/test_global_time.c b/c/global-time/tests/test_global_time.c new file mode 100644 index 0000000..ecfa67a --- /dev/null +++ b/c/global-time/tests/test_global_time.c @@ -0,0 +1,145 @@ +#include "global_time.h" /* Проверяет настоящий переносимый код часов без подмены его реализации. */ +#ifdef NDEBUG /* Проверки должны выполняться и при Release-сборке CMake. */ +#undef NDEBUG /* Сохраняет assert активным независимо от выбранного уровня оптимизации. */ +#endif /* Завершает принудительное включение проверяющих утверждений. */ +#include /* Останавливает тест при нарушении временного или проводного контракта. */ +#include /* Выводит компактное подтверждение выполнения набора проверок. */ +#include /* Позволяет воспроизвести ненулевую RAM после тёплого старта. */ +static uint16_t irq_mask, depth; /* Моделирует исходную маску IRQ и вложенность критических секций. */ +static int accepted, sends; /* Управляет занятостью транспорта и считает попытки объявления готовности. */ +static gt_frame sent; /* Сохраняет последний кадр для независимой проверки ID и payload. */ +static uint16_t enter(void *user) /* Моделирует TI intrinsic с сохранением предыдущего INTM. */ +{ /* Проверяет, что ядро пользуется единым механизмом защиты состояния. */ + uint16_t previous = irq_mask; /* Сохраняет маску до входа в текущий уровень блокировки. */ + assert(user == &sends); /* Подтверждает неизменную передачу пользовательского контекста. */ + irq_mask = 1U; /* Запрещает модельным прерываниям менять данные до leave. */ + ++depth; /* Отмечает вложенный уровень для контроля парности входа и выхода. */ + return previous; /* Возвращает исходное состояние, включая вызов из уже закрытого ISR. */ +} /* Завершает модель входа в критическую секцию. */ +static void leave(void *user, uint16_t previous) /* Проверяет восстановление предыдущей маски без безусловного разрешения. */ +{ /* Делает нарушения вложенности видимыми в host-тесте. */ + assert(user == &sends && depth > 0U && irq_mask == 1U); /* Выход допустим только из реально захваченной секции. */ + --depth; /* Освобождает один уровень модельной блокировки. */ + irq_mask = previous; /* Возвращает именно исходную маску вызывающего кода. */ +} /* Завершает проверяемую модель выхода. */ +static int send(void *user, const gt_frame *frame) /* Моделирует неблокирующую передачу и копирование временного payload. */ +{ /* Callback не должен выполняться с длинной критической секцией ядра. */ + assert(user == &sends && depth == 0U); /* Проверяет передачу контекста и отсутствие блокировки IRQ на время отправки. */ + ++sends; /* Считает и принятые, и отклонённые транспортом попытки. */ + sent = *frame; /* Сохраняет независимую копию до возврата callback. */ + return accepted; /* Имитирует занятость mailbox без ожидания. */ +} /* Завершает модель транспорта. */ +static void advance(gt_clock *clock, uint32_t milliseconds) /* Продвигает реальный алгоритм через границы календарных полей. */ +{ /* Пошаговый вызов также проверяет миллисекундные переносы в ISR. */ + while (milliseconds--) gt_tick_1ms(clock); /* Не подменяет поведение таймера прямой правкой абсолютного времени. */ +} /* Завершает управляемое продвижение времени. */ +static gt_frame rtc(uint16_t year, uint16_t month, uint16_t day, uint16_t hour, uint16_t minute, uint16_t second) /* Строит календарный эталон кодировщиком верхнего уровня. */ +{ /* Ожидаемые значения времени в тестах проверяются отдельно от кодировщика. */ + gt_datetime date = {year, month, day, 6U, hour, minute, second, 0U}; /* День недели сохраняется как допустимая фаза цикла, исходная суббота=6. */ + gt_frame frame; /* Получает полноценный расширенный CAN кадр. */ + assert(gt_make_rtc(&date, &frame)); /* Корректный календарь обязан кодироваться в существующие семь октетов. */ + return frame; /* Возвращает независимый контейнер для изменения ошибочных полей. */ +} /* Завершает построение тестовых опорных кадров. */ +int main(void) /* Проверяет календарь, коррекции, задержки, переполнения и стартовый CAN-обмен. */ +{ /* Использует только публичный API, кроме целевой установки границы uptime. */ + gt_clock clock; /* Состояние заполняется мусором, имитируя startup без очистки .ebss. */ + gt_port port = {&sends, enter, leave, send}; /* Подключает проверяющие блокировки и управляемый транспорт. */ + gt_stamp stamp, before; /* Два атомарных снимка позволяют проверять отсутствие побочных изменений при ошибке. */ + gt_datetime date; /* Календарный результат проверяется по известным границам дат. */ + gt_frame frame, bad; /* Исходный корректный кадр и копия для отрицательных проверок. */ + uint16_t type, id, index; /* Полученные адресные поля и индекс проверки каждого октета. */ + const uint16_t invalid[7] = {24U, 60U, 60U, 100U, 0U, 0U, 7U}; /* Независимые граничные ошибки каждого календарного поля на линии. */ + memset(&clock, 0xA5, sizeof(clock)); /* Воспроизводит остатки RAM вместо неявной инициализации нулями хоста. */ + assert(!gt_init(&clock, &port, 8U, 0U)); /* Запрещает трёхбитному типу переполниться в направление Route. */ + assert(!gt_init(&clock, &port, 1U, 16U)); /* Запрещает молчаливое обрезание экземпляра до адреса другой платы. */ + assert(gt_init(&clock, &port, 1U, 5U)); /* Создаёт УМП1 по действующему реестру PERIPH. */ + gt_get(&clock, &stamp); /* Проверяет результат явной инициализации всей временной базы. */ + assert(stamp.seconds == 0UL && stamp.uptime_ms == 0UL && !stamp.synchronized && stamp.generation == 0UL); /* До синхронизации календарь не должен выглядеть достоверным. */ + assert(!gt_calendar(&stamp, &date)); /* Не позволяет приложению выдавать локальную эпоху за полученную дату. */ + advance(&clock, 1001UL); /* Пересекает первую секундную границу программного таймера. */ + gt_get(&clock, &stamp); /* Снимает дробную часть после переноса секунды. */ + assert(stamp.seconds == 1UL && stamp.millisecond == 1U && stamp.uptime_ms == 1001UL); /* Проверяет ровно 1000 тиков на секунду без ошибки на единицу. */ + gt_poll(&clock); /* Пытается обслужить отправку до завершения инициализации платы. */ + assert(sends == 0); /* Пакет готовности запрещён до явного ready. */ + gt_ready(&clock); /* Разрешает стартовый пакет только после прикладной готовности. */ + gt_poll(&clock); /* Имитирует занятую CAN передачу при accepted=0. */ + assert(sends == 1 && !clock.announced); /* Занятость транспорта не теряет обязательный пакет инициализации. */ + accepted = 1; /* Освобождает транспорт на следующем проходе main. */ + gt_poll(&clock); /* Повторяет отклонённую попытку без задержки и блокировки IRQ. */ + assert(sends == 2 && sent.id == 0x19513000UL && sent.dlc == 1U && sent.data[0] == 1U); /* Фиксирует проводной эталон READY узла Type=1, ID=5. */ + assert(gt_parse_ready(&sent, &type, &id) && type == 1U && id == 5U); /* Мастер должен узнать физический адрес стартовавшей платы. */ + bad = sent; /* Готовит отрицательный вариант стартового сообщения. */ + bad.id ^= 0x08000000UL; /* Меняет направление на запрос мастера, сохранив payload. */ + assert(!gt_parse_ready(&bad, 0, 0)); /* Исключает распознавание собственного запроса как готовности платы. */ + advance(&clock, 999UL); /* Проверяет ограничение частоты повторов перед секундной границей. */ + gt_poll(&clock); /* Обслуживает CAN на последней миллисекунде запрета повтора. */ + assert(sends == 2); /* Готовность не должна засорять шину на каждом проходе main. */ + advance(&clock, 1UL); /* Достигает точно секундного интервала повторной отправки. */ + gt_poll(&clock); /* Повторяет событие старта при отсутствии ответа временем. */ + assert(sends == 3); /* Потерянный ответ мастера должен приводить к повтору объявления готовности. */ + frame = rtc(2024U, 2U, 28U, 23U, 59U, 59U); /* Задаёт известную границу високосного февраля. */ + assert(frame.id == 0x30UL && frame.dlc == 7U && frame.data[3] == 24U && frame.data[4] == 2U); /* Проверяет совместимый ID и календарный wire format. */ + assert(gt_receive(&clock, &frame, clock.stamp.uptime_ms)); /* Устанавливает первую реальную календарную дату. */ + advance(&clock, 1001UL); /* Пересекает полночь на один миллисекундный тик. */ + gt_get(&clock, &stamp); /* Снимает календарную метку после перехода даты. */ + assert(gt_calendar(&stamp, &date) && date.year == 2024U && date.month == 2U && date.day == 29U && date.hour == 0U && date.millisecond == 1U); /* Проверяет 29 февраля и сброс часов/минут/секунд на полночь. */ + assert(stamp.weekday == 0U && stamp.generation == 1UL); /* День недели циклически продвигается, а тики не создают новые поколения коррекции. */ + assert(gt_timestamp_ms(&stamp) > UINT32_MAX); /* Календарные миллисекунды остаются широкими и не обрезаются до uptime. */ + advance(&clock, 2000UL); /* Даёт мастеру время ответить; уже синхронизированный узел не повторяет READY. */ + gt_poll(&clock); /* Проверяет прекращение стартовых повторов после RTCSETUP. */ + assert(sends == 3); /* Получение календаря завершает повторный запрос времени. */ + before = stamp; /* Сохраняет известную метку для проверки отрицательной коррекции. */ + frame = rtc(2000U, 1U, 1U, 0U, 0U, 0U); /* Задаёт независимый эталон нулевой секунды эпохи библиотеки. */ + assert(gt_receive(&clock, &frame, clock.stamp.uptime_ms)); /* Применяет разрешённый перевод глобальных часов назад. */ + gt_get(&clock, &stamp); /* Снимает результат атомарной коррекции. */ + assert(stamp.seconds == 0UL && stamp.millisecond == 0U && stamp.correction_ms < 0 && stamp.uptime_ms > before.uptime_ms && stamp.generation == 2UL); /* Проверяет независимость тайм-аутов от отрицательной календарной коррекции. */ + assert(gt_timestamp_ms(&stamp) == 0ULL); /* Фиксирует точное начало принятой эпохи. */ + before = stamp; /* Сохраняет состояние до серии ошибочных CAN кадров. */ + for (index = 0U; index < 7U; ++index) { /* Проверяет наличие лишних старших битов в каждом слове, содержащем октет C28x. */ + bad = frame; /* Восстанавливает исходный корректный RTCSETUP перед каждой ошибкой. */ + bad.data[index] = 256U; /* Значение вне одного проводного октета не должно обрезаться до нуля. */ + assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Отвергает ошибку порта до изменения даты. */ + bad = frame; /* Возвращает корректный исходный кадр для проверки допустимого диапазона поля. */ + bad.data[index] = invalid[index]; /* Подставляет двоичное, но календарно недопустимое значение. */ + assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Исключает нормализацию ошибочного времени и выход за границы таблицы месяцев. */ + } /* Завершает проверку всей широкой wire-границы. */ + bad = frame; bad.extended = 0U; /* Подготавливает стандартный 11-битный кадр с похожим ID. */ + assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* ProtoCAN требует IDE=1 независимо от численного ID. */ + bad = frame; bad.remote = 1U; /* Подготавливает RTR без реальных календарных данных. */ + assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Удалённый запрос не устанавливает глобальное время. */ + bad = frame; bad.dlc = 8U; /* Добавляет неописанный восьмой октет без согласованного расширения протокола. */ + assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Длина существующего RTCSETUP должна быть ровно семь октетов. */ + bad = frame; bad.id |= 0x01000000UL; /* Имитирует broadcast от чужого типа устройства вместо верхнего уровня. */ + assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Исключает коррекцию по произвольным сообщениям другой платы. */ + bad = frame; bad.data[4] = 2U; bad.data[5] = 30U; /* Подготавливает несуществующее 30 февраля високосного года. */ + assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Календарные ошибки не нормализуются в другую дату. */ + bad = frame; bad.data[3] = 23U; bad.data[4] = 2U; bad.data[5] = 29U; /* Подготавливает 29 февраля невисокосного года. */ + assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Отвергает некорректную дату даже при допустимых диапазонах отдельных полей. */ + gt_get(&clock, &stamp); /* Проверяет итоговое состояние после всех отклонённых кадров. */ + assert(stamp.seconds == before.seconds && stamp.generation == before.generation && stamp.synchronized); /* Неверные пакеты не теряют последнюю достоверную синхронизацию. */ + before.seconds = 3160857600UL; /* Независимый календарный эталон: ровно 36584 суток от 2000-01-01 до 2100-03-01. */ + assert(gt_calendar(&before, &date) && date.year == 2100U && date.month == 3U && date.day == 1U); /* Проверяет исключение невисокосного 2100 года при продолжении локального хода часов. */ + clock.stamp.uptime_ms = UINT32_MAX - 4UL; /* Целевым образом размещает монотонный счётчик около переполнения 49,7 суток. */ + advance(&clock, 10UL); /* Пересекает modulo-границу до обработки уже полученного кадра. */ + frame.id |= 0x10000000UL; /* Проверяет допустимость стандартного приоритета RTCSETUP наряду с критическим. */ + assert(gt_receive(&clock, &frame, UINT32_MAX - 4UL)); /* Передаёт момент RX до переполнения, обработку — после. */ + gt_get(&clock, &stamp); /* Снимает скорректированную фазу доли секунды. */ + assert(stamp.seconds == 0UL && stamp.millisecond == 10U && stamp.uptime_ms == 5UL); /* Компенсация очереди должна быть 10 мс, независимо от переполнения uptime. */ + frame = rtc(2023U, 12U, 31U, 23U, 59U, 59U); /* Проверяет смену календарного года при отложенной обработке. */ + before = stamp; /* Запоминает момент RX непосредственно перед задержкой основного цикла. */ + advance(&clock, 1250UL); /* Имитирует долгую прикладную операцию при продолжающемся аппаратном таймере. */ + assert(gt_receive(&clock, &frame, before.uptime_ms)); /* Применяет время по моменту RX, а не по моменту выхода из очереди. */ + gt_get(&clock, &stamp); /* Снимает дату после учёта задержки обработки. */ + assert(gt_calendar(&stamp, &date) && date.year == 2024U && date.month == 1U && date.day == 1U && date.second == 0U && date.millisecond == 250U); /* Проверяет перенос секунды, суток и года с сохранением 250 мс. */ + irq_mask = 1U; /* Имитирует вызов логгера из ISR с уже запрещёнными прерываниями. */ + gt_get(&clock, &stamp); /* Проверяет чтение метки при вложенной критической секции. */ + gt_tick_1ms(&clock); /* Проверяет продвижение часов при исходном INTM=1. */ + assert(irq_mask == 1U && depth == 0U); /* Выходы не должны случайно разрешить IRQ вызывающего обработчика. */ + irq_mask = 0U; /* Восстанавливает модельное окружение main перед новой инициализацией. */ + assert(gt_init(&clock, &port, 2U, 15U)); /* Проверяет холодный reset качества и верхний адрес PERIPH31. */ + assert(gt_receive(&clock, &frame, 0UL)); /* Имитирует broadcast ещё до прикладного ready. */ + gt_ready(&clock); gt_ready(&clock); gt_poll(&clock); /* Повторный ready должен быть идемпотентным, но первая отправка всё равно обязательна. */ + assert(sends == 4 && gt_parse_ready(&sent, &type, &id) && type == 2U && id == 15U); /* Ранняя синхронизация не подавляет сообщение об успешной инициализации платы. */ + puts("global-time: calendar, CAN, correction, IRQ and rollover OK"); /* Возвращает краткое свидетельство пройденных смысловых сценариев. */ + return 0; /* Завершает host-тест только после всех обязательных проверок. */ +} /* Завершает независимую проверку переносимого ядра. */ diff --git a/c/parallel-nand/docs/MEMORY_GAS.md b/c/parallel-nand/docs/MEMORY_GAS.md index a91be51..4792dd2 100644 --- a/c/parallel-nand/docs/MEMORY_GAS.md +++ b/c/parallel-nand/docs/MEMORY_GAS.md @@ -1,5 +1,7 @@ # Работа с parallel NAND через GAS +> GAS `0x3` использует адреса отдельных 16-битных регистров. GAS SLOT `0x4` — отдельный сервис с маской в Word0 и тремя значениями Word1…Word3. Спецификация: `doc/setcan/protocan/OAP.md`; адреса этого документа остаются регистровыми. + ## Архитектура Внешняя NAND значительно больше общего адресного пространства GAS. Например, diff --git a/doc/setcan/protocan/GLOBAL_TIME.md b/doc/setcan/protocan/GLOBAL_TIME.md new file mode 100644 index 0000000..ae25d39 --- /dev/null +++ b/doc/setcan/protocan/GLOBAL_TIME.md @@ -0,0 +1,97 @@ +# ProtoCAN: глобальное время и завершение инициализации платы + +Профиль библиотеки [`c/global-time`](../../../c/global-time/README.md). +Время использует штатную команду BROADCAST RTCSETUP. +Общий флаг готовности — новое соглашение этого профиля поверх DISCRETE/FLAGS; +его нельзя считать уже реализованным всеми историческими приборами ProtoCAN. + +## Плата → верхний уровень: готовность + +После успешной инициализации оборудования и параметров плата отправляет: + +| Поле | Значение | +|---|---| +| IDE / RTR | 1 / 0 | +| Priority | 1, standard | +| Route | 1, от устройства | +| DeviceType / DeviceID | собственный адрес платы | +| MsgType | 0x1, DISCRETE | +| MsgBody | 0x3000: подтип FLAGS=3, страница 0 | +| DLC | 1 | +| DATA[0] | 0x01: бит 0 означает «инициализация завершена» | + +Биты DATA[0] 1..7 зарезервированы нулями. Пакет не меняет локальные +технологические флаги «Статус инициализации» в ОАП конкретных приборов. +В этом профиле Body `0x3000`, DLC1, DATA01 закреплены за общим событием готовности; +приборы с другим историческим назначением этой комбинации требуют адаптера. + +Для PERIPH5 / УМП1, Type1/ID5: + +```text +CAN ID 0x19513000, DLC 1, DATA 01 +``` + +Плата посылает готовность хотя бы один раз, даже если успела получить broadcast +ещё до завершения собственной инициализации. Пока синхронизация не получена, +событие повторяется не чаще одного раза в 1000 мс. Повтор не означает новый reset; +мастер может объединять несколько близких запросов в один broadcast. + +## Верхний уровень → все платы: дата и время + +При получении готовности мастер читает свою текущую дату, формирует штатный +RTCSETUP и отправляет один кадр всем узлам. Может также периодически передавать +RTCSETUP для коррекции дрейфа; библиотека принимает каждый корректный пакет. + +| Поле | Значение | +|---|---| +| IDE / RTR | 1 / 0 | +| Priority | 0 или 1 | +| Route | 0, от ПМ | +| DeviceType / DeviceID | 0 / 0, верхний уровень | +| MsgType | 0, BROADCAST | +| MsgBody | 0x0030: команда RTCSETUP=3, параметр=0 | +| DLC | ровно 7 | +| DATA | hour, minute, second, year−2000, month, day, weekday | + +Все поля двоичные, без BCD. Диапазоны: часы 0..23, минуты/секунды 0..59, +год 0..99, месяц 1..12, число согласно календарю, день недели 0..6. +Профиль использует 0=воскресенье, 1=понедельник; библиотека сохраняет переданную +фазу недели и затем продвигает её вместе с календарём. Дата имеет приоритет +для абсолютной метки; несоответствие фазы недели календарю не меняет дату. + +Пример: 2026-10-19 12:34:56, понедельник: + +```text +CAN ID 0x00000030, DLC 7, DATA 0C 22 38 1A 0A 13 01 +``` + +Каждая плата независимо проверяет кадр и календарь, рассчитывает смещение +от своей временной базы и атомарно корректирует её. Повреждённый или чужой +кадр не меняет время и качество предыдущей синхронизации. При отсутствии +новых пакетов часы продолжают локальный отсчёт. Тайм-ауты основаны на независимом +uptime; перевод календаря назад не удлиняет и не сокращает их. + +## Миллисекунды и потребители + +Нулевой миллисекундной долей опорной даты считается момент RX ISR. +Тики, прошедшие до обработки в main, прибавляются к присланному времени. +Исходный кадр содержит только целые секунды: локальное разрешение 1 мс +не означает погрешность синхронизации между платами 1 мс. +Мастер выбирает общую шкалу, например UTC, и момент отправки около секундной +границы. Часовой пояс, leap seconds и компенсация арбитража CAN не передаются. + +Логгер снимает `gt_stamp` с секундами от 2000 года, миллисекундной долей, +признаком синхронизации, номером коррекции и независимым uptime. +64-битная календарная метка не помещается в старое 32-битное поле времени +логгеров; изменение формата архива выполняет конкретное приложение. + +## Верхний уровень: точки подключения + +`gt_parse_ready()` извлекает Type/ID из сообщения платы. +`gt_make_rtc()` проверяет дату и формирует кадр с DLC7; ненулевые миллисекунды +на входе отклоняются, чтобы не терять точность молча. Источник календаря, +CAN-транспорт и обработчик входящего события предоставляет приложение мастера. + +Для TMS320F28335 готовый порт занимает eCAN-A mailbox7/8 и по выбору +CPU Timer2/INT14 либо hook существующего ISR 1 мс. Подробные порядок вызовов, +требования к общему CAN ISR и проверки — в README библиотеки.