feat(global-time): Добавь общие часы и порт TMS320F28335

Вынеси программные часы, коррекцию по ProtoCAN RTCSETUP и атомарную метку логгера в переносимое ядро без глобального состояния.

Подключи eCAN-A и CPU Timer2 отдельным портом F28335; зафиксируй календарные границы, переполнение, задержку RX и защиту IRQ в хостовых тестах.
This commit is contained in:
2026-10-23 14:16:21 +03:00
parent 62c0492bfd
commit 2da7e7b525
13 changed files with 992 additions and 0 deletions
+2
View File
@@ -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`; приложения подключают ревизию сабмодулем.
+21
View File
@@ -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 исходники.
+167
View File
@@ -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. Полная линковка конкретной прошивки и проверка на плате
в этот набор не входят.
+65
View File
@@ -0,0 +1,65 @@
#ifndef GLOBAL_TIME_H /* Защищает единый контракт часов от повторного включения. */
#define GLOBAL_TIME_H /* Отмечает подключённый интерфейс программных глобальных часов. */
#include <stdint.h> /* Фиксирует разрядность счётчиков и поддерживает 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 /* Завершает защиту публичного заголовка. */
@@ -0,0 +1,168 @@
#include "DSP2833x_Device.h" /* Использует штатные описания 32-битных регистров eCAN, Timer2 и векторов TI. */
#include "global_time_f28335.h" /* Подключает переносимые часы и контракт интеграции порта. */
#include <string.h> /* Явно очищает отложенный 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 = &gt_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. */
} /* Завершает необязательную остановку аппаратных часов. */
@@ -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 /* Завершает защиту заголовка порта. */
+156
View File
@@ -0,0 +1,156 @@
#include "global_time.h" /* Подключает переносимые типы, API часов и фиксированные поля ProtoCAN. */
#include <string.h> /* Использует явный сброс состояния вместо предположений о старте 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; /* Подтверждает наличие календарной синхронизации в исходной метке. */
} /* Завершает календарное представление метки логгера. */
@@ -0,0 +1,26 @@
#ifndef GT_TEST_DSP2833X_DEVICE_H /* Изолирует модель регистров от настоящих заголовков TI. */
#define GT_TEST_DSP2833X_DEVICE_H /* Предотвращает повторное определение тестовых типов. */
#include <stdint.h> /* Использует одинаковые разрядности полей на 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 /* Завершает защиту тестовой модели аппаратуры. */
+36
View File
@@ -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") # Подтверждает именно компиляцию объектов, не аппаратные испытания или полную линковку прошивки.
+88
View File
@@ -0,0 +1,88 @@
#include "DSP2833x_Device.h" /* Использует host-модель регистров, а не альтернативную реализацию драйвера. */
#include "global_time_f28335.h" /* Тестирует публичные hooks настоящего аппаратного порта. */
#ifdef NDEBUG /* Не позволяет Release-сборке превратить аппаратные проверки в пустую программу. */
#undef NDEBUG /* Включает assert независимо от флагов конфигурации CMake. */
#endif /* Завершает настройку обязательных проверяющих утверждений. */
#include <assert.h> /* Проверяет сохранность соседних ресурсов, обработку CAN и период Timer2. */
#include <stdio.h> /* Выводит краткое подтверждение проверенных сценариев. */
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. */
+145
View File
@@ -0,0 +1,145 @@
#include "global_time.h" /* Проверяет настоящий переносимый код часов без подмены его реализации. */
#ifdef NDEBUG /* Проверки должны выполняться и при Release-сборке CMake. */
#undef NDEBUG /* Сохраняет assert активным независимо от выбранного уровня оптимизации. */
#endif /* Завершает принудительное включение проверяющих утверждений. */
#include <assert.h> /* Останавливает тест при нарушении временного или проводного контракта. */
#include <stdio.h> /* Выводит компактное подтверждение выполнения набора проверок. */
#include <string.h> /* Позволяет воспроизвести ненулевую 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-тест только после всех обязательных проверок. */
} /* Завершает независимую проверку переносимого ядра. */
+2
View File
@@ -1,5 +1,7 @@
# Работа с parallel NAND через GAS
> GAS `0x3` использует адреса отдельных 16-битных регистров. GAS SLOT `0x4` — отдельный сервис с маской в Word0 и тремя значениями Word1…Word3. Спецификация: `doc/setcan/protocan/OAP.md`; адреса этого документа остаются регистровыми.
## Архитектура
Внешняя NAND значительно больше общего адресного пространства GAS. Например,
+97
View File
@@ -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 библиотеки.