diff --git a/c/protocan-transport/CMakeLists.txt b/c/protocan-transport/CMakeLists.txt new file mode 100644 index 0000000..dff8aa2 --- /dev/null +++ b/c/protocan-transport/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.13) +project(protocan_transport C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# Ядро библиотеки: платформенных зависимостей нет, собирается под что угодно. +add_library(protocan_transport STATIC + src/pcan_crc.c + src/pcan_frame.c + src/pcan_id.c + src/pcan_link.c + src/pcan_ring.c + src/pcan_gas.c +) + +target_include_directories(protocan_transport PUBLIC include) + +if(MSVC) + target_compile_options(protocan_transport PRIVATE /W4) +else() + target_compile_options(protocan_transport PRIVATE -Wall -Wextra -Wpedantic) +endif() + +# Порт ports/stm32f4 сюда не входит: ему нужен CMSIS-заголовок stm32f4xx.h, +# он подключается напрямую в проект прошивки. + +option(PCAN_BUILD_TESTS "Собирать тесты" ON) + +if(PCAN_BUILD_TESTS) + enable_testing() + add_executable(test_transport tests/test_transport.c) + target_link_libraries(test_transport PRIVATE protocan_transport) + add_test(NAME transport COMMAND test_transport) +endif() diff --git a/c/protocan-transport/README.md b/c/protocan-transport/README.md new file mode 100644 index 0000000..9dc66b1 --- /dev/null +++ b/c/protocan-transport/README.md @@ -0,0 +1,126 @@ +# protocan-transport + +Переносимая библиотека транспортного уровня для протокола **ProtoCAN**: +кадрирование поверх любого байтового потока (RS485, RS232, USB CDC), +разбор идентификатора и общее адресное пространство регистров. + +Написана на C99, без динамической памяти, без ОС, без зависимостей от HAL +и от конкретного микроконтроллера. Состояние живёт в структурах вызывающего, +поэтому в одной прошивке поднимается сколько угодно независимых каналов. + +``` + ваш код protocan-transport платформа + ┌────────┐ ┌────────────────────┐ ┌──────────────┐ + │ кадры │─────►│ pcan_link_send() │─────►│ io.write() │──► UART/DMA + │ │◄─────│ on_frame() │◄─────│ pcan_link_feed() + └────────┘ └────────────────────┘ └──────────────┘ + pcan_gas_* pcan_id_* +``` + +## Состав + +| Модуль | Назначение | +|---|---| +| `pcan_frame` | кадр `AA 55 … CRC16` и потоковый разборщик с ресинхронизацией | +| `pcan_crc` | CRC-16/CCITT-FALSE, побитовый или табличный | +| `pcan_link` | экземпляр канала: приём, передача, SEQ, счётчики | +| `pcan_ring` | кольцевой буфер, отдаёт непрерывный участок для DMA | +| `pcan_id` | упаковка и разбор 29-битного идентификатора ProtoCAN | +| `pcan_gas` | общее адресное пространство: карта регионов, чтение/запись, мост к кадрам | +| `ports/stm32f4` | готовый порт USART + DMA (пакетная передача, кольцевой приём) | + +## Кадр + +``` +AA 55 | LEN | SEQ | FLAGS | ID0 ID1 ID2 ID3 | DATA[0..8] | CRC_L CRC_H +``` + +`LEN = 6 + DLC` (6..14), CRC-16/CCITT-FALSE по байтам `LEN..DATA`, +little-endian. Подробности — [docs/FRAME.md](docs/FRAME.md). + +## Общее адресное пространство + +Плоское пространство 16-битных регистров `0x0000..0xFFFF`, собранное из +регионов. Регион ссылается либо на массив в памяти, либо на пару колбэков — +так в карту попадают и переменные, и вычисляемые значения, и регистры +периферии. Подробности — [docs/GAS.md](docs/GAS.md). + +```c +static uint16_t holding[8]; + +static const pcan_gas_region_t regions[] = { + { 0x0000, 8, holding, NULL, NULL, 0, NULL, "holding" }, + { 0xFF00, 4, NULL, diag_read, NULL, PCAN_GAS_RDONLY, NULL, "diag" }, +}; +static const pcan_gas_map_t map = { regions, 2 }; +``` + +## Использование + +```c +#include "protocan_transport.h" + +static void on_frame(const pcan_frame_t *f, void *user) +{ + /* ... */ +} + +pcan_io_t io; +pcan_uart_io(&uart, &io); /* или свой io */ + +pcan_link_t link; +pcan_link_init(&link, &io, on_frame, NULL); + +pcan_frame_t f = { .flags = PCAN_FLAG_IDE, .id = 0x1234567, .dlc = 2 }; +f.data[0] = 0xAA; f.data[1] = 0xBB; +pcan_link_send(&link, &f); /* SEQ проставится сам */ +``` + +Для своей платформы достаточно реализовать две функции: + +```c +size_t my_write(void *ctx, const uint8_t *data, size_t len); /* всё-или-ничего */ +size_t my_space(void *ctx); +``` + +## Сборка + +Библиотека — шесть файлов в `src/` и заголовки в `include/`. Добавьте их +в проект и укажите `include/` в путях поиска. CMake для тестов и хостовых +сборок: + +```bash +cmake -B build && cmake --build build && ctest --test-dir build +``` + +Либо напрямую: + +```bash +clang -std=c99 -Wall -Wextra -Iinclude tests/test_transport.c src/pcan_*.c -o test && ./test +``` + +Порт `ports/stm32f4` в тесты не входит: ему нужен CMSIS-заголовок +`stm32f4xx.h`, подключайте его в проект прошивки отдельно. + +## Настройки + +Переопределяются через `-D` либо через свой `pcan_config_user.h` +(с `-DPCAN_USE_USER_CONFIG`): + +| Макрос | По умолчанию | Смысл | +|---|---|---| +| `PCAN_DATA_MAX` | 8 | длина поля данных CAN | +| `PCAN_CRC_TABLE` | 0 | 1 — таблица на 512 байт вместо побитового расчёта | +| `PCAN_GAS_MAX_REGIONS` | 16 | предел числа регионов в карте | +| `PCAN_BARRIER()` | барьер компилятора | для очередей, разделяемых с прерыванием | + +## Ограничения + +- Очереди рассчитаны на схему «один писатель + один читатель». Если + писателей несколько, оборачивайте вызовы своей блокировкой. +- Размер кольцевого буфера обязан быть степенью двойки; `pcan_ring_init()` + вернёт `false`, а не станет молча портить индексы. +- На STM32F407 DMA не видит CCM RAM (`0x10000000`) — буферы держите + в основном SRAM. +- Порядок байт на линии фиксирован (little-endian) и не зависит от порядка + байт хоста. diff --git a/c/protocan-transport/docs/FRAME.md b/c/protocan-transport/docs/FRAME.md new file mode 100644 index 0000000..0400e1f --- /dev/null +++ b/c/protocan-transport/docs/FRAME.md @@ -0,0 +1,78 @@ +# Транспортный кадр + +## Формат + +``` ++------+------+-----+-----+-------+----------------+-------------+-------+-------+ +| 0xAA | 0x55 | LEN | SEQ | FLAGS | ID0 ID1 ID2 ID3| DATA[0..8] | CRC_L | CRC_H | ++------+------+-----+-----+-------+----------------+-------------+-------+-------+ +``` + +| Поле | Байт | Описание | +|---|---:|---| +| SOF | 2 | сигнатура `0xAA 0x55` | +| `LEN` | 1 | длина участка `SEQ..DATA` = `6 + DLC`, диапазон 6..14 | +| `SEQ` | 1 | счётчик кадров 0..255, инкремент на каждый успешно отданный кадр | +| `FLAGS` | 1 | см. ниже | +| `ID` | 4 | 29-битный CAN-идентификатор, little-endian (старшие 3 бита = 0) | +| `DATA` | 0..8 | `DLC = LEN - 6` байт данных CAN | +| `CRC` | 2 | CRC-16/CCITT-FALSE, little-endian | + +CRC считается по байтам от `LEN` до последнего байта `DATA` включительно; +сигнатура SOF в расчёт не входит. Полином `0x1021`, начальное значение +`0xFFFF`, без рефлексии и без финального XOR — контрольное значение для +строки `123456789` равно `0x29B1`. + +Максимальный размер кадра — 19 байт (`PCAN_FRAME_MAX`). + +## FLAGS + +| Бит | Имя | Значение | +|---:|---|---| +| 0 | `IDE` | 1 = расширенный ID (29 бит), 0 = стандартный (11 бит) | +| 1 | `RTR` | 1 = remote frame | +| 2 | `DIR` | 0 = кадр пришёл из CAN, 1 = кадр надо передать в CAN | +| 3 | `ERR` | 1 = служебный кадр моста (диагностика), не трафик шины | +| 7..4 | — | резерв, передавать нулями | + +## Синхронизация + +Приёмник ищет `0xAA 0x55`, читает `LEN`, проверяет диапазон `6..14`, +набирает `LEN + 2` байт и сверяет CRC. При неверном `LEN` или несовпадении +CRC разборщик возвращается к поиску сигнатуры, причём байт, оборвавший +разбор, сам проверяется на `0xAA` — последовательность `AA AA 55` тоже +распознаётся. Потеря синхронизации стоит не больше одного кадра. + +Счётчики разбора (`pcan_parse_stats_t`) отдельно считают кадры, ошибки CRC, +неверные `LEN` и байты вне кадров — по ним видно, шумит линия или сбоит +источник. + +## SEQ + +`SEQ` инкрементируется только на успешно отданном кадре: если кадр не влез +в очередь передачи, счётчик не двигается, и приёмник не засчитает потерю +там, где кадра просто не было. Разрыв в `SEQ` на приёме означает реальную +потерю в линии. + +## Почему так + +- **Длина плюс CRC, без байт-стаффинга.** Стаффинг раздувает кадр + непредсказуемо и усложняет расчёт таймингов на полудуплексной линии. + Фиксированный заголовок даёт заранее известный максимум 19 байт. +- **Сигнатура из двух байт.** Один байт слишком часто встречается в + случайных данных; два дают приемлемую вероятность ложного старта, + который всё равно отсеет CRC. +- **`LEN` в начале.** Приёмник сразу знает, сколько байт набирать, и не + зависит от содержимого данных. +- **Little-endian везде.** Совпадает с порядком регистров в + `PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()` и с обоими целевыми МК. + +## Пример + +CAN-кадр: ID `0x1234567`, DLC 2, данные `AA BB`, `SEQ = 1`, флаг `IDE`. + +``` +AA 55 08 01 01 67 45 23 01 AA BB FE 14 +``` + +`LEN = 6 + 2 = 8`, `FLAGS = 0x01`, CRC = `0x14FE`. diff --git a/c/protocan-transport/docs/GAS.md b/c/protocan-transport/docs/GAS.md new file mode 100644 index 0000000..b0f11bd --- /dev/null +++ b/c/protocan-transport/docs/GAS.md @@ -0,0 +1,87 @@ +# Общее адресное пространство (GAS) + +Плоское пространство 16-битных регистров с адресом `0x0000..0xFFFF`. +Пространство собирается из **регионов**; регионы не перекрываются и +хранятся отсортированными по адресу, поиск — двоичный. + +## Регион + +```c +typedef struct pcan_gas_region { + uint16_t base; /* адрес первого регистра */ + uint16_t count; /* число регистров */ + uint16_t *storage; /* массив либо NULL */ + pcan_gas_read_fn read; + pcan_gas_write_fn write; + uint8_t flags; /* PCAN_GAS_RDONLY / PCAN_GAS_WRONLY */ + void *user; + const char *name; +} pcan_gas_region_t; +``` + +Если `storage != NULL`, чтение и запись идут прямо в массив — это самый +дешёвый вариант для обычных уставок. Если нужен вычисляемый регистр +(счётчик, состояние периферии, время работы), задайте `read`/`write`: +колбэк получает смещение внутри региона и указатель `user`. + +`pcan_gas_map_validate()` проверяет карту на этапе старта: нулевые регионы, +выход за `0xFFFF`, перекрытие, нарушение порядка и регион без источника +данных. Вызывайте её один раз при инициализации — ошибка в таблице ловится +сразу, а не через месяц в поле. + +## Доступ + +```c +uint16_t v; +pcan_gas_read(&map, 0x0002, &v); +pcan_gas_write(&map, 0x0002, 0x1234); + +uint16_t block[4]; +uint16_t n = pcan_gas_read_block(&map, 0x0000, block, 4); +``` + +Блочное чтение обрывается на первом адресе, которого нет в карте, поэтому +вызывающий всегда получает непрерывный кусок и знает его длину. + +## Отображение на кадры ProtoCAN + +Тип сообщения `PCAN_MSG_GAS` (`0b0011`). Адрес первого регистра лежит +в `MsgBody`, данные — до 4 регистров подряд, младшим байтом вперёд. +Это ровно то, что делает `PROTOCAN_SEND_GENERAL_ADDRESS_SPACE()` +в `SETCAN/Src/protocan.c`, поэтому обмен совместим с существующими +устройствами. + +| Кадр | DLC | Смысл | +|---|---:|---| +| GAS, `MsgBody = addr` | 0 | **запрос на чтение** | +| GAS, `MsgBody = addr` | 2..8 | значения регистров начиная с `addr` | + +Запрос на чтение с `DLC = 0` — **расширение**: в исходном коде SETCAN +такой кодировки нет, там ответы на GAS формировала заглушка `ProtoCanMsgToGeneralAddressSpace()`, +возвращавшая строку `GAS-XXXX`. Кодировка выбрана так, чтобы не занимать +новых типов сообщений и не конфликтовать с существующим форматом ответа. + +```c +pcan_frame_t rsp; +if (pcan_gas_handle(&map, &incoming, &rsp)) { + pcan_link_send(&link, &rsp); /* был запрос на чтение */ +} +``` + +`pcan_gas_handle()`: + +- на запрос чтения кладёт в `rsp` до 4 регистров и переключает `Route` + на `FROM_DEVICE`, возвращает `true`; +- на запись пишет регистры в карту и возвращает `false` — ответа нет; +- если адреса нет в карте, возвращает `false`: отвечать нечем, а молчание + честнее, чем ответ с нулями. + +## Ограничения + +- В один кадр помещается не больше 4 регистров (`PCAN_GAS_REGS_PER_FRAME`). + Длинные блоки разбивайте на несколько кадров. +- Частичная запись: если в середине блока попался адрес вне карты или + регион только для чтения, запись обрывается на нём. `pcan_gas_write_block()` + возвращает число фактически записанных регистров. +- Атомарности между регистрами нет. Если два регистра обязаны меняться + вместе, заведите колбэк, который применяет их по записи второго. diff --git a/c/protocan-transport/docs/GUI_CATALOG.md b/c/protocan-transport/docs/GUI_CATALOG.md new file mode 100644 index 0000000..cee8e6b --- /dev/null +++ b/c/protocan-transport/docs/GUI_CATALOG.md @@ -0,0 +1,158 @@ +# Каталог общего адресного пространства и поток значений + +Расширение GUI-протокола SETGUI (`A5 5A`, см. `gui_desktop/core/protocol.py`) +для работы с общим адресным пространством: прибор сам объявляет, какие +регистры у него есть и как они называются, а оператор выбирает, что +показывать. Схема повторяет то, как устроен реестр регистров в +ST Motor Control Workbench / Motor Pilot. + +Транспорт не меняется: `A5 5A | ver | type | seq(BE) | size(BE) | payload | CRC32(LE)`, +payload до 512 байт. Добавлены только три типа сообщений. + +``` + прибор GUI + │ ── GAS_CATALOG (seq = 0) ──────────► │ каталог приходит сам + │ ── GAS_CATALOG (seq = 0) ──────────► │ при инициализации + │ ── GAS_CATALOG (seq = 0) ──────────► │ + │ │ оператор отмечает нужное + │ ◄──────────────── GAS_WATCH_SET ──── │ + │ ── GAS_WATCH_SET (эхо) ────────────► │ + │ ── GAS_WATCH_DATA (seq = 0) ───────► │ поток значений + │ ── GAS_WATCH_DATA (seq = 0) ───────► │ +``` + +## Типы сообщений + +Заняты из свободного диапазона `0x11..0x1F` (между `READ_LOGS = 0x10` +и `SENSOR_SCAN = 0x20`). Существующие значения не перенумерованы. + +| Код | Имя | Направление | +|---:|---|---| +| `0x11` | `GAS_CATALOG` | запрос GUI → прибор; записи прибор → GUI | +| `0x12` | `GAS_WATCH_SET` | GUI → прибор, прибор отвечает эхом | +| `0x13` | `GAS_WATCH_DATA` | прибор → GUI, без запроса, `sequence = 0` | + +Незапрошенные кадры прибор публикует с `sequence = 0` — так же, как уже +устроены `SENSOR_DATA` и `UI_STATE`. + +## `GAS_CATALOG` (0x11) + +### Запрос (GUI → прибор), 4 байта + +| Смещение | Тип | Поле | +|---:|---|---| +| 0 | u16 LE | `start_index` — порядковый номер записи, **не адрес** | +| 2 | u16 LE | `max_count` — сколько записей вернуть; 0 = сколько влезет | + +### Ответ и автопубликация (прибор → GUI) + +| Смещение | Тип | Поле | +|---:|---|---| +| 0 | u16 LE | `total` — всего записей в каталоге | +| 2 | u16 LE | `start_index` — индекс первой записи в пакете | +| 4 | u16 LE | `count` — записей в пакете | +| 6 | | `count` записей по 32 байта | + +Одна запись — 32 байта: + +| Смещение | Тип | Поле | +|---:|---|---| +| 0 | u16 LE | `address` — адрес в общем адресном пространстве | +| 2 | u8 | `type` — формат значения | +| 3 | u8 | `flags` — доступ | +| 4 | i8 | `scale_pow10` — значение = `raw * 10^scale` | +| 5 | u8 | `unit` — код единицы измерения | +| 6 | u16 LE | резерв, нули | +| 8 | 24 байта | `name` — UTF-8, дополнено нулями | + +При payload 512 байт в один пакет входит `(512 - 6) / 32 = 15` записей. + +Под имя отведено 24 байта — это 12 кириллических символов в UTF-8. +На 16 байтах не помещалось даже «Температура», поэтому поле шире, +чем кажется нужным для латиницы. + +### `type` + +| Код | Значение | Регистров | +|---:|---|---:| +| 0 | `U16` — беззнаковое | 1 | +| 1 | `I16` — знаковое | 1 | +| 2 | `U32` — беззнаковое, младшее слово первым | 2 | +| 3 | `I32` — знаковое, младшее слово первым | 2 | +| 4 | `BITS` — битовое поле | 1 | + +Многословные значения занимают подряд идущие адреса; в потоке они +приходят отдельными словами, GUI собирает их сам. + +### `flags` + +| Бит | Смысл | +|---:|---| +| 0 | доступно на чтение | +| 1 | доступно на запись | +| 2 | включить в подписку по умолчанию | + +### `unit` + +| Код | Единица | Код | Единица | +|---:|---|---:|---| +| 0 | — | 6 | мс | +| 1 | В | 7 | с | +| 2 | А | 8 | кбит/с | +| 3 | °C | 9 | шт. | +| 4 | % | 10 | об/мин | +| 5 | Гц | 11 | Вт | + +## `GAS_WATCH_SET` (0x12) + +### Запрос (GUI → прибор) + +| Смещение | Тип | Поле | +|---:|---|---| +| 0 | u16 LE | `period_ms` — период потока; **0 останавливает поток** | +| 2 | u16 LE | `count` — число адресов, не больше `GAS_WATCH_MAX` (64) | +| 4 | | `count` × u16 LE — адреса в нужном порядке | + +Порядок адресов сохраняется: значения в `GAS_WATCH_DATA` приходят +ровно в том же порядке, без повторной передачи адресов. + +### Ответ (прибор → GUI), 4 байта + +| Смещение | Тип | Поле | +|---:|---|---| +| 0 | u16 LE | `period_ms` — период, который прибор реально установил | +| 2 | u16 LE | `count` — сколько адресов принято | + +Прибор может принять меньше, чем попросили: адрес вне карты в подписку +не берётся. Расхождение `count` с запросом — сигнал GUI, что часть +адресов отвергнута. + +## `GAS_WATCH_DATA` (0x13) + +Прибор → GUI, `sequence = 0`, без запроса. + +| Смещение | Тип | Поле | +|---:|---|---| +| 0 | u32 LE | `timestamp_ms` — время прибора от старта | +| 4 | u16 LE | `count` | +| 6 | | `count` × u16 LE — значения в порядке подписки | + +`timestamp_ms` берётся у прибора, а не у хоста: по нему видно реальный +период и провалы, которые иначе замаскировала бы буферизация UART. + +## Замечания по реализации + +* **Каталог статичен.** Он описывает прошивку, а не состояние, поэтому + публикуется один раз при инициализации периферии. GUI может перечитать + его запросом в любой момент. +* **Подписка живёт до переподключения.** Прибор не сохраняет её в + энергонезависимой памяти: после сброса поток молчит, пока GUI не + пришлёт `GAS_WATCH_SET` снова. +* **Поток не должен забивать линию.** При периоде 10 мс и 64 адресах + выходит 134 байта на пакет и 13.4 кбайт/с — половина пропускной + способности 256000 бод. Прибор пропускает такт, если в очереди + передачи нет места для целого пакета, и это видно по разрыву + `timestamp_ms`. +* **Значения передаются сырыми.** Пересчёт в физические величины делает + GUI по `scale_pow10` и `unit` — прибор не тратит на это такты и не + теряет точность на промежуточном округлении. diff --git a/c/protocan-transport/include/gui_catalog.h b/c/protocan-transport/include/gui_catalog.h new file mode 100644 index 0000000..ebd9fe0 --- /dev/null +++ b/c/protocan-transport/include/gui_catalog.h @@ -0,0 +1,155 @@ +/** + * @file gui_catalog.h + * @brief Каталог общего адресного пространства и поток выбранных значений. + * + * Прибор объявляет GUI, какие регистры у него есть и как они называются; + * оператор отмечает нужное, и прибор шлёт только отмеченное пакетами. + * Схема повторяет реестр регистров ST Motor Control Workbench. + * + * Двоичный контракт: docs/GUI_CATALOG.md. + * Зеркало на Python: gui_desktop/core/gas_catalog.py. + */ +#ifndef GUI_CATALOG_H +#define GUI_CATALOG_H + +#include +#include +#include + +#include "gui_frame.h" +#include "pcan_gas.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Длина записи каталога на линии. */ +#define GUI_ENTRY_SIZE 32U +/** + * Длина поля имени. 24 байта - это 12 кириллических символов в UTF-8; + * на 16 байтах не помещалось даже "Температура". + */ +#define GUI_NAME_SIZE 24U +/** Заголовок ответа GAS_CATALOG: total, start_index, count. */ +#define GUI_CATALOG_HEADER 6U +/** Сколько записей входит в один кадр. */ +#define GUI_ENTRIES_PER_FRAME ((GUI_MAX_PAYLOAD - GUI_CATALOG_HEADER) / GUI_ENTRY_SIZE) + +/** Максимум адресов в подписке. */ +#ifndef GUI_WATCH_MAX +#define GUI_WATCH_MAX 64U +#endif + +/** Формат значения. */ +#define GUI_OBJ_U16 0U +#define GUI_OBJ_I16 1U +#define GUI_OBJ_U32 2U /* два регистра, младшее слово первым */ +#define GUI_OBJ_I32 3U +#define GUI_OBJ_BITS 4U + +/** Доступ и признаки. */ +#define GUI_OBJ_READABLE 0x01U +#define GUI_OBJ_WRITABLE 0x02U +#define GUI_OBJ_DEFAULT_WATCH 0x04U + +/** Коды единиц измерения; входят в контракт и не перенумеровываются. */ +#define GUI_UNIT_NONE 0U +#define GUI_UNIT_VOLT 1U +#define GUI_UNIT_AMPERE 2U +#define GUI_UNIT_CELSIUS 3U +#define GUI_UNIT_PERCENT 4U +#define GUI_UNIT_HERTZ 5U +#define GUI_UNIT_MS 6U +#define GUI_UNIT_SECOND 7U +#define GUI_UNIT_KBPS 8U +#define GUI_UNIT_COUNT 9U +#define GUI_UNIT_RPM 10U +#define GUI_UNIT_WATT 11U + +/** + * @brief Описание одного значения в общем адресном пространстве. + * + * Таблица объявляется `static const` и живёт во flash: на МК её незачем + * держать в ОЗУ. + */ +typedef struct { + uint16_t address; /**< адрес первого регистра */ + uint8_t type; /**< GUI_OBJ_* */ + uint8_t flags; /**< GUI_OBJ_READABLE и прочие */ + int8_t scale_pow10; /**< физическое = raw * 10^scale */ + uint8_t unit; /**< GUI_UNIT_* */ + const char *name; /**< UTF-8, не длиннее GUI_NAME_SIZE */ +} gui_object_t; + +typedef struct { + const gui_object_t *items; + uint16_t count; +} gui_catalog_t; + +/** + * @brief Проверяет таблицу на этапе старта. + * + * Ловит имена длиннее поля и адреса, которых нет в карте GAS: ошибку + * в таблице лучше увидеть при инициализации, чем гадать над пустой + * строкой в GUI. + * + * @param map карта GAS для сверки адресов; NULL - не сверять. + */ +bool gui_catalog_validate(const gui_catalog_t *catalog, const pcan_gas_map_t *map); + +/** + * @brief Собирает payload ответа GAS_CATALOG. + * + * @param start_index индекс первой записи; + * @param max_count сколько записей отдать; 0 - сколько влезет в кадр; + * @return длина payload либо 0, если start_index за концом каталога. + */ +size_t gui_catalog_encode(const gui_catalog_t *catalog, uint16_t start_index, + uint16_t max_count, uint8_t *out, size_t out_size); + +/* --- Подписка на поток ----------------------------------------------------- */ + +typedef struct { + uint16_t period_ms; /**< 0 - поток остановлен */ + uint16_t count; + uint16_t address[GUI_WATCH_MAX]; + uint32_t next_ms; /**< когда слать следующий пакет */ + uint32_t sent; + uint32_t skipped; /**< тактов пропущено из-за занятой линии */ +} gui_watch_t; + +void gui_watch_init(gui_watch_t *watch); + +/** + * @brief Применяет payload GAS_WATCH_SET. + * + * Адреса, которых нет в карте, в подписку не берутся - GUI увидит это + * по расхождению count в эхо-ответе. + * + * @return число принятых адресов; при неверном payload подписка не меняется + * и возвращается 0xFFFF. + */ +uint16_t gui_watch_apply(gui_watch_t *watch, const pcan_gas_map_t *map, + const uint8_t *payload, uint16_t size); + +/** Собирает 4 байта эхо-ответа: период и число принятых адресов. */ +size_t gui_watch_encode_ack(const gui_watch_t *watch, uint8_t *out, size_t out_size); + +/** Пора ли слать очередной пакет. */ +bool gui_watch_due(const gui_watch_t *watch, uint32_t now_ms); + +/** + * @brief Собирает payload GAS_WATCH_DATA из текущих значений карты. + * @return длина payload либо 0, если подписка пуста. + */ +size_t gui_watch_encode_data(const gui_watch_t *watch, const pcan_gas_map_t *map, + uint32_t timestamp_ms, uint8_t *out, size_t out_size); + +/** Сдвигает момент следующей отправки. */ +void gui_watch_advance(gui_watch_t *watch, uint32_t now_ms); + +#ifdef __cplusplus +} +#endif + +#endif /* GUI_CATALOG_H */ diff --git a/c/protocan-transport/include/gui_frame.h b/c/protocan-transport/include/gui_frame.h new file mode 100644 index 0000000..2b3d490 --- /dev/null +++ b/c/protocan-transport/include/gui_frame.h @@ -0,0 +1,185 @@ +/** + * @file gui_frame.h + * @brief Транспорт GUI-протокола SETGUI на стороне МК. + * + * A5 5A | ver | type | seq_hi seq_lo | size_hi size_lo | payload | CRC32 LE + * + * Заголовок big-endian, CRC32 (IEEE, как zlib) little-endian, payload + * до 512 байт. Байт в байт совпадает с gui_desktop/core/protocol.py. + * + * Это второй протокол библиотеки и он намеренно отдельный от pcan_frame: + * тот повторяет форму CAN-кадра (8 байт данных) и живёт на полевой шине, + * а здесь нужен большой payload для каталога и потока значений. Общего + * у них только платформенный слой pcan_io_t. + */ +#ifndef GUI_FRAME_H +#define GUI_FRAME_H + +#include +#include +#include + +#include "pcan_config.h" +#include "pcan_link.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define GUI_SOF0 0xA5U +#define GUI_SOF1 0x5AU +#define GUI_PROTOCOL_VERSION 0x01U + +#define GUI_HEADER_SIZE 8U /* SOF(2) + ver + type + seq(2) + size(2) */ +#define GUI_CRC_SIZE 4U +#define GUI_MAX_PAYLOAD 512U +#define GUI_FRAME_MAX (GUI_HEADER_SIZE + GUI_MAX_PAYLOAD + GUI_CRC_SIZE) + +/** + * @brief Наибольший payload, который прибор готов принять. + * + * Ограничивает буфер разборщика. 512 нужны только для WRITE_REGISTERS + * на 254 слова; если такие записи не используются, значение можно + * уменьшить и сэкономить ОЗУ. + */ +#ifndef GUI_RX_PAYLOAD_MAX +#define GUI_RX_PAYLOAD_MAX GUI_MAX_PAYLOAD +#endif + +/* Типы сообщений, нужные мосту. Полный список - в protocol.py. */ +#define GUI_MSG_PING 0x01U +#define GUI_MSG_DEVICE_INFO 0x02U +#define GUI_MSG_DIAGNOSTICS 0x08U +#define GUI_MSG_READ_REGISTERS 0x09U +#define GUI_MSG_WRITE_REGISTERS 0x0AU +#define GUI_MSG_GAS_CATALOG 0x11U +#define GUI_MSG_GAS_WATCH_SET 0x12U +#define GUI_MSG_GAS_WATCH_DATA 0x13U +#define GUI_MSG_NACK 0x80U +#define GUI_MSG_ACK 0x81U +#define GUI_MSG_ERROR 0x82U + +/* Коды результата, общие с GUITransport_ObjectResult и ObjectResult. */ +#define GUI_RESULT_OK 0U +#define GUI_RESULT_INVALID_ARGUMENT 1U +#define GUI_RESULT_INVALID_LENGTH 2U +#define GUI_RESULT_NOT_FOUND 3U +#define GUI_RESULT_ACCESS_DENIED 4U +#define GUI_RESULT_INTERNAL 7U + +/** CRC32 IEEE: poly 0xEDB88320 с рефлексией, init и финальный XOR 0xFFFFFFFF. */ +uint32_t gui_crc32(const uint8_t *data, size_t len); + +/** + * @brief Кодирует кадр целиком. + * @param out буфер приёмника; + * @param out_size его размер, нужно `GUI_HEADER_SIZE + size + GUI_CRC_SIZE`; + * @return число записанных байт либо 0 при неверных аргументах. + */ +size_t gui_frame_encode(uint8_t type, uint16_t sequence, + const uint8_t *payload, uint16_t size, + uint8_t *out, size_t out_size); + +/** Разобранный кадр; payload указывает внутрь буфера разборщика. */ +typedef struct { + uint8_t type; + uint16_t sequence; + uint16_t size; + const uint8_t *payload; +} gui_frame_t; + +typedef enum { + GUI_ST_SOF0 = 0, + GUI_ST_SOF1, + GUI_ST_HEADER, + GUI_ST_PAYLOAD, + GUI_ST_CRC +} gui_parse_state_t; + +typedef struct { + uint32_t frames; + uint32_t crc_errors; + uint32_t version_errors; + uint32_t length_errors; + uint32_t stray_bytes; +} gui_parse_stats_t; + +typedef struct { + gui_parse_state_t state; + uint8_t header[6]; /**< ver, type, seq(2), size(2) */ + uint16_t idx; + uint16_t size; + uint8_t crc_bytes[GUI_CRC_SIZE]; + uint8_t payload[GUI_RX_PAYLOAD_MAX]; + gui_parse_stats_t stats; +} gui_parser_t; + +void gui_parser_init(gui_parser_t *p); + +/** @return true, если кадр собран и проверен; payload лежит в p->payload. */ +bool gui_parser_push(gui_parser_t *p, uint8_t byte, gui_frame_t *out); + +typedef void (*gui_frame_cb_t)(const gui_frame_t *frame, void *user); + +size_t gui_parser_feed(gui_parser_t *p, const uint8_t *data, size_t len, + gui_frame_cb_t cb, void *user); + +/* --- Канал ---------------------------------------------------------------- */ + +/** + * @brief Экземпляр GUI-канала поверх того же pcan_io_t, что и полевой. + * + * Буфер передачи лежит внутри: кадр собирается целиком и отдаётся в порт + * одной записью, иначе половина пакета в линии сбила бы приёмник. + */ +typedef struct { + pcan_io_t io; + gui_parser_t parser; + gui_frame_cb_t on_frame; + void *user; + uint32_t tx_frames; + uint32_t tx_dropped; + uint8_t tx_buf[GUI_FRAME_MAX]; +} gui_link_t; + +bool gui_link_init(gui_link_t *link, const pcan_io_t *io, + gui_frame_cb_t on_frame, void *user); + +/** @return false, если в очереди передачи не хватило места на весь кадр. */ +bool gui_link_send(gui_link_t *link, uint8_t type, uint16_t sequence, + const uint8_t *payload, uint16_t size); + +/** Короткий отказ: NACK с кодом результата в payload. */ +bool gui_link_nack(gui_link_t *link, uint16_t sequence, uint16_t result); + +size_t gui_link_feed(gui_link_t *link, const uint8_t *data, size_t len); + +/** Помещается ли ещё кадр с payload указанной длины. */ +bool gui_link_can_send(const gui_link_t *link, uint16_t size); + +/* --- Мелкие помощники для payload ----------------------------------------- */ + +static inline uint16_t gui_get_u16(const uint8_t *p) +{ + return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); +} + +static inline void gui_put_u16(uint8_t *p, uint16_t value) +{ + p[0] = (uint8_t)(value & 0xFFU); + p[1] = (uint8_t)(value >> 8); +} + +static inline void gui_put_u32(uint8_t *p, uint32_t value) +{ + p[0] = (uint8_t)(value & 0xFFU); + p[1] = (uint8_t)((value >> 8) & 0xFFU); + p[2] = (uint8_t)((value >> 16) & 0xFFU); + p[3] = (uint8_t)((value >> 24) & 0xFFU); +} + +#ifdef __cplusplus +} +#endif + +#endif /* GUI_FRAME_H */ diff --git a/c/protocan-transport/include/pcan_config.h b/c/protocan-transport/include/pcan_config.h new file mode 100644 index 0000000..5d1df61 --- /dev/null +++ b/c/protocan-transport/include/pcan_config.h @@ -0,0 +1,45 @@ +/** + * @file pcan_config.h + * @brief Настройки времени компиляции. + * + * Значения по умолчанию рассчитаны на МК без ОС. Переопределяются + * либо через -D, либо через собственный pcan_config_user.h. + */ +#ifndef PCAN_CONFIG_H +#define PCAN_CONFIG_H + +#if defined(PCAN_USE_USER_CONFIG) +#include "pcan_config_user.h" +#endif + +/** Максимальная длина поля данных CAN. */ +#ifndef PCAN_DATA_MAX +#define PCAN_DATA_MAX 8U +#endif + +/** + * @brief Барьер компилятора для разделяемых с прерыванием переменных. + * + * Библиотека не отключает прерывания сама: очереди рассчитаны на схему + * "один писатель + один читатель", где индексы обновляются атомарно. + * Если у вас несколько писателей, оберните вызовы своей блокировкой. + */ +#ifndef PCAN_BARRIER +# if defined(__GNUC__) || defined(__clang__) || defined(__ARMCC_VERSION) +# define PCAN_BARRIER() __asm volatile("" ::: "memory") +# else +# define PCAN_BARRIER() do { } while (0) +# endif +#endif + +/** Таблица CRC (512 байт flash) вместо побитового расчёта. */ +#ifndef PCAN_CRC_TABLE +#define PCAN_CRC_TABLE 0 +#endif + +/** Количество регионов, которые может содержать одна карта GAS. */ +#ifndef PCAN_GAS_MAX_REGIONS +#define PCAN_GAS_MAX_REGIONS 16U +#endif + +#endif /* PCAN_CONFIG_H */ diff --git a/c/protocan-transport/include/pcan_crc.h b/c/protocan-transport/include/pcan_crc.h new file mode 100644 index 0000000..ac8de69 --- /dev/null +++ b/c/protocan-transport/include/pcan_crc.h @@ -0,0 +1,28 @@ +/** + * @file pcan_crc.h + * @brief CRC-16/CCITT-FALSE: poly 0x1021, init 0xFFFF, без рефлексии, + * без финального XOR. Контрольное значение для "123456789" - 0x29B1. + */ +#ifndef PCAN_CRC_H +#define PCAN_CRC_H + +#include +#include + +#include "pcan_config.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Начальное значение для инкрементального расчёта. */ +#define PCAN_CRC_INIT 0xFFFFU + +uint16_t pcan_crc16_update(uint16_t crc, uint8_t byte); +uint16_t pcan_crc16(const uint8_t *data, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* PCAN_CRC_H */ diff --git a/c/protocan-transport/include/pcan_frame.h b/c/protocan-transport/include/pcan_frame.h new file mode 100644 index 0000000..2c0a178 --- /dev/null +++ b/c/protocan-transport/include/pcan_frame.h @@ -0,0 +1,113 @@ +/** + * @file pcan_frame.h + * @brief Транспортный кадр и потоковый разборщик. + * + * AA 55 | LEN | SEQ | FLAGS | ID0 ID1 ID2 ID3 | DATA[0..8] | CRC_L CRC_H + * + * LEN = 6 + DLC (длина участка SEQ..DATA), диапазон 6..14 + * CRC = CRC-16/CCITT-FALSE по байтам LEN..DATA, little-endian + * + * Кадр самосинхронизирующийся: при мусоре в линии разборщик сдвигается + * на байт и продолжает искать сигнатуру, теряя не больше одного кадра. + * + * Формат не зависит ни от платформы, ни от физического уровня: он одинаково + * ложится на RS485, RS232, USB CDC и любой байтовый поток. + */ +#ifndef PCAN_FRAME_H +#define PCAN_FRAME_H + +#include +#include +#include + +#include "pcan_config.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define PCAN_SOF0 0xAAU +#define PCAN_SOF1 0x55U + +/** Расширенный (29-битный) идентификатор. */ +#define PCAN_FLAG_IDE 0x01U +/** Remote frame. */ +#define PCAN_FLAG_RTR 0x02U +/** 0: кадр пришёл из CAN; 1: кадр надо передать в CAN. */ +#define PCAN_FLAG_DIR 0x04U +/** Служебный кадр моста (диагностика), не относится к трафику CAN. */ +#define PCAN_FLAG_ERR 0x08U + +#define PCAN_LEN_MIN 6U +#define PCAN_LEN_MAX (PCAN_LEN_MIN + PCAN_DATA_MAX) +/** SOF(2) + LEN(1) + LEN_MAX + CRC(2). */ +#define PCAN_FRAME_MAX (PCAN_LEN_MAX + 5U) + +/** Разобранный кадр. */ +typedef struct { + uint8_t seq; + uint8_t flags; + uint32_t id; /**< 29-битный идентификатор */ + uint8_t dlc; + uint8_t data[PCAN_DATA_MAX]; +} pcan_frame_t; + +/** + * @brief Сериализует кадр. + * @param out буфер приёмника; + * @param out_size его размер, нужно не меньше PCAN_FRAME_MAX; + * @return число записанных байт либо 0, если буфер мал. + */ +size_t pcan_frame_encode(const pcan_frame_t *frame, uint8_t *out, size_t out_size); + +/* --- Потоковый разборщик -------------------------------------------------- */ + +typedef enum { + PCAN_ST_SOF0 = 0, + PCAN_ST_SOF1, + PCAN_ST_LEN, + PCAN_ST_BODY, + PCAN_ST_CRC_L, + PCAN_ST_CRC_H +} pcan_parse_state_t; + +/** Счётчики разборщика: полезны для диагностики линии. */ +typedef struct { + uint32_t frames; /**< успешно разобрано кадров */ + uint32_t crc_errors; /**< кадров отброшено по CRC */ + uint32_t bad_len; /**< LEN вне допустимого диапазона */ + uint32_t stray_bytes; /**< байт отброшено вне кадров */ +} pcan_parse_stats_t; + +typedef struct { + pcan_parse_state_t state; + uint8_t len; + uint8_t idx; + uint8_t body[PCAN_LEN_MAX + 1U]; /**< LEN + участок SEQ..DATA */ + uint16_t crc_rx; + pcan_parse_stats_t stats; +} pcan_parser_t; + +void pcan_parser_init(pcan_parser_t *p); + +/** + * @brief Скармливает разборщику один байт. + * @return true, если кадр собран целиком и записан в out. + */ +bool pcan_parser_push(pcan_parser_t *p, uint8_t byte, pcan_frame_t *out); + +/** Колбэк на готовый кадр. */ +typedef void (*pcan_frame_cb_t)(const pcan_frame_t *frame, void *user); + +/** + * @brief Скармливает разборщику блок байт, вызывая cb на каждый кадр. + * @return число разобранных кадров. + */ +size_t pcan_parser_feed(pcan_parser_t *p, const uint8_t *data, size_t len, + pcan_frame_cb_t cb, void *user); + +#ifdef __cplusplus +} +#endif + +#endif /* PCAN_FRAME_H */ diff --git a/c/protocan-transport/include/pcan_gas.h b/c/protocan-transport/include/pcan_gas.h new file mode 100644 index 0000000..88b0097 --- /dev/null +++ b/c/protocan-transport/include/pcan_gas.h @@ -0,0 +1,129 @@ +/** + * @file pcan_gas.h + * @brief Общее адресное пространство (General Address Space). + * + * Плоское пространство 16-битных регистров с адресом 0x0000..0xFFFF, + * собранное из регионов. Регион либо ссылается на массив в памяти, + * либо обслуживается колбэками - так в карту попадают и обычные + * переменные, и вычисляемые значения, и регистры периферии. + * + * Транспорт ProtoCAN (MsgType = 0b0011) кладёт в MsgBody адрес первого + * регистра, а в данные - до 4 регистров подряд, младшим байтом вперёд. + * Это ровно то, что делает PROTOCAN_SEND_GENERAL_ADDRESS_SPACE() + * в SETCAN/Src/protocan.c, поэтому обмен совместим с существующими + * устройствами. + * + * Запрос на чтение кодируется кадром GAS с DLC = 0: в исходном коде + * SETCAN такой кодировки нет, это расширение - см. docs/GAS.md. + */ +#ifndef PCAN_GAS_H +#define PCAN_GAS_H + +#include +#include +#include + +#include "pcan_config.h" +#include "pcan_frame.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Сколько регистров помещается в один кадр (8 байт / 2). */ +#define PCAN_GAS_REGS_PER_FRAME 4U + +typedef enum { + PCAN_GAS_OK = 0, + PCAN_GAS_NO_REG, /**< адрес не покрыт ни одним регионом */ + PCAN_GAS_READ_ONLY, /**< запись в регион только для чтения */ + PCAN_GAS_WRITE_ONLY, + PCAN_GAS_REJECTED /**< колбэк отверг значение */ +} pcan_gas_status_t; + +/** Регион только для чтения. */ +#define PCAN_GAS_RDONLY 0x01U +/** Регион только для записи. */ +#define PCAN_GAS_WRONLY 0x02U + +struct pcan_gas_region; + +typedef pcan_gas_status_t (*pcan_gas_read_fn)(const struct pcan_gas_region *region, + uint16_t offset, uint16_t *value); +typedef pcan_gas_status_t (*pcan_gas_write_fn)(const struct pcan_gas_region *region, + uint16_t offset, uint16_t value); + +/** + * @brief Непрерывный участок адресного пространства. + * + * Если storage != NULL, чтение и запись идут прямо в массив. + * Иначе вызываются read/write. + */ +typedef struct pcan_gas_region { + uint16_t base; /**< адрес первого регистра */ + uint16_t count; /**< число регистров */ + uint16_t *storage; /**< массив либо NULL */ + pcan_gas_read_fn read; + pcan_gas_write_fn write; + uint8_t flags; + void *user; + const char *name; /**< для отладки, может быть NULL */ +} pcan_gas_region_t; + +/** Карта: набор регионов. Регионы не должны перекрываться. */ +typedef struct { + const pcan_gas_region_t *regions; + uint16_t count; +} pcan_gas_map_t; + +/** @return true, если регионы отсортированы и не перекрываются. */ +bool pcan_gas_map_validate(const pcan_gas_map_t *map); + +const pcan_gas_region_t *pcan_gas_find(const pcan_gas_map_t *map, uint16_t addr); + +pcan_gas_status_t pcan_gas_read(const pcan_gas_map_t *map, uint16_t addr, + uint16_t *value); +pcan_gas_status_t pcan_gas_write(const pcan_gas_map_t *map, uint16_t addr, + uint16_t value); + +/** + * @brief Читает подряд идущие регистры. + * @return сколько регистров удалось прочитать, начиная с addr. + * + * Чтение прекращается на первом адресе, которого нет в карте, + * поэтому вызывающий всегда получает непрерывный блок. + */ +uint16_t pcan_gas_read_block(const pcan_gas_map_t *map, uint16_t addr, + uint16_t *out, uint16_t max); + +uint16_t pcan_gas_write_block(const pcan_gas_map_t *map, uint16_t addr, + const uint16_t *in, uint16_t count); + +/* --- Мост между картой и кадрами ProtoCAN ---------------------------------- */ + +/** Извлекает регистры из данных кадра GAS (младший байт первым). */ +uint16_t pcan_gas_frame_to_regs(const pcan_frame_t *frame, uint16_t *out, + uint16_t max); + +/** Заполняет кадр GAS: адрес в MsgBody, до 4 регистров в данных. */ +void pcan_gas_regs_to_frame(pcan_frame_t *frame, uint32_t base_id, + uint16_t addr, const uint16_t *regs, uint16_t count); + +/** + * @brief Обрабатывает входящий кадр GAS. + * + * DLC = 0 - запрос на чтение: в rsp кладётся до 4 регистров с адреса + * MsgBody, Route переключается на FROM_DEVICE. + * DLC > 0 - запись: регистры пишутся в карту, ответ не формируется. + * + * @param[out] rsp кадр ответа; заполняется, только если функция вернула true. + * @return true, если ответ нужно передать. + */ +bool pcan_gas_handle(const pcan_gas_map_t *map, const pcan_frame_t *req, + pcan_frame_t *rsp); + +#ifdef __cplusplus +} +#endif + +#endif /* PCAN_GAS_H */ diff --git a/c/protocan-transport/include/pcan_id.h b/c/protocan-transport/include/pcan_id.h new file mode 100644 index 0000000..c52749f --- /dev/null +++ b/c/protocan-transport/include/pcan_id.h @@ -0,0 +1,95 @@ +/** + * @file pcan_id.h + * @brief Упаковка и разбор 29-битного идентификатора ProtoCAN. + * + * Раскладка (соответствует ProtoCanId_t из SETCAN/Inc/protocan.h): + * + * биты 28 Priority + * 27 Route + * 26:24 DeviceType + * 23:20 DeviceID + * 19:16 MsgType + * 15:0 MsgBody + * + * Здесь намеренно не используются битовые поля: их раскладка в C + * определяется реализацией, и один и тот же заголовок на другом + * компиляторе может дать другой порядок. Сдвиги переносимы всегда. + */ +#ifndef PCAN_ID_H +#define PCAN_ID_H + +#include + +#include "pcan_config.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define PCAN_ID_MASK 0x1FFFFFFFUL + +/* --- Приоритет и маршрут --------------------------------------------------- */ +#define PCAN_PRIORITY_CRITICAL 0U +#define PCAN_PRIORITY_STANDARD 1U + +#define PCAN_ROUTE_FROM_PM 0U +#define PCAN_ROUTE_FROM_DEVICE 1U + +/* --- Типы сообщений -------------------------------------------------------- */ +typedef enum { + PCAN_MSG_BROADCAST = 0x0U, + PCAN_MSG_DISCRETE = 0x1U, + PCAN_MSG_ANALOG = 0x2U, + PCAN_MSG_GAS = 0x3U, /**< общее адресное пространство */ + PCAN_MSG_MODBUS_COIL = 0x4U, + PCAN_MSG_MODBUS_DISCR = 0x5U, + PCAN_MSG_MODBUS_HOLDING = 0x6U, + PCAN_MSG_MODBUS_INPUT = 0x7U, + PCAN_MSG_ERROR = 0x8U, + PCAN_MSG_PULSE = 0xFU +} pcan_msgtype_t; + +/** Разобранный идентификатор. */ +typedef struct { + uint16_t msg_body; + uint8_t msg_type; /**< pcan_msgtype_t */ + uint8_t device_id; /**< 0..15 */ + uint8_t device_type; /**< 0..7 */ + uint8_t route; + uint8_t priority; +} pcan_id_t; + +uint32_t pcan_id_pack(const pcan_id_t *id); +void pcan_id_unpack(uint32_t raw, pcan_id_t *id); + +/* --- Раскладки MsgBody ----------------------------------------------------- */ + +/** BROADCAST: Type[15:4], Body[3:0]. */ +static inline uint16_t pcan_body_broadcast(uint16_t type, uint8_t body) +{ + return (uint16_t)(((type & 0x0FFFU) << 4) | (body & 0x0FU)); +} + +/** DISCRETE / ANALOG: Type[15:12], Body[11:0]. */ +static inline uint16_t pcan_body_typed(uint8_t type, uint16_t body) +{ + return (uint16_t)(((uint16_t)(type & 0x0FU) << 12) | (body & 0x0FFFU)); +} + +/** MODBUS: StrAdr[15:4], RegCount[3:0]. */ +static inline uint16_t pcan_body_modbus(uint16_t addr, uint8_t count) +{ + return (uint16_t)(((addr & 0x0FFFU) << 4) | (count & 0x0FU)); +} + +/** ERROR: Info[15:8], Code[7:0]. */ +static inline uint16_t pcan_body_error(uint8_t info, uint8_t code) +{ + return (uint16_t)(((uint16_t)info << 8) | code); +} + +#ifdef __cplusplus +} +#endif + +#endif /* PCAN_ID_H */ diff --git a/c/protocan-transport/include/pcan_link.h b/c/protocan-transport/include/pcan_link.h new file mode 100644 index 0000000..9c8cedf --- /dev/null +++ b/c/protocan-transport/include/pcan_link.h @@ -0,0 +1,97 @@ +/** + * @file pcan_link.h + * @brief Экземпляр канала связи поверх произвольного байтового потока. + * + * Библиотека не знает ни про UART, ни про DMA, ни про ОС: платформа + * отдаёт две функции (записать байты, узнать свободное место) и сама + * решает, как их реализовать - опросом, по прерыванию или через DMA. + * + * Состояния в глобальных переменных нет, поэтому в одной прошивке + * поднимается сколько угодно каналов: полевой RS485, отдельный UART + * для GUI, USB CDC - каждый со своим pcan_link_t. + * + * Пример: + * @code + * static uint8_t txbuf[512]; + * pcan_link_t link; + * pcan_link_init(&link, &io, on_frame, NULL); + * pcan_link_send(&link, &frame); // из фонового кода + * pcan_link_feed(&link, chunk, n); // из порта, когда пришли байты + * @endcode + */ +#ifndef PCAN_LINK_H +#define PCAN_LINK_H + +#include +#include +#include + +#include "pcan_config.h" +#include "pcan_frame.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Интерфейс, который реализует платформа. + * + * write() обязана быть неблокирующей и принимать блок целиком либо + * не принимать вовсе - частично отданный кадр сломал бы приёмник. + */ +typedef struct { + /** @return число принятых байт: len (принято) либо 0 (не влезло). */ + size_t (*write)(void *ctx, const uint8_t *data, size_t len); + /** Свободное место в очереди передачи, байт. */ + size_t (*tx_space)(void *ctx); + void *ctx; +} pcan_io_t; + +typedef struct { + uint32_t tx_frames; + uint32_t tx_dropped; /**< кадров не влезло в очередь передачи */ +} pcan_link_tx_stats_t; + +typedef struct { + pcan_io_t io; + pcan_parser_t parser; + pcan_frame_cb_t on_frame; + void *user; + uint8_t tx_seq; + pcan_link_tx_stats_t tx; +} pcan_link_t; + +/** + * @brief Инициализирует канал. + * @param io интерфейс платформы, копируется внутрь; + * @param on_frame вызывается на каждый принятый кадр, может быть NULL; + * @param user произвольный контекст для on_frame. + */ +bool pcan_link_init(pcan_link_t *link, const pcan_io_t *io, + pcan_frame_cb_t on_frame, void *user); + +/** + * @brief Отправляет кадр, проставив в него очередной SEQ. + * @return false, если в очереди передачи не хватило места. + */ +bool pcan_link_send(pcan_link_t *link, pcan_frame_t *frame); + +/** Как pcan_link_send(), но SEQ берётся из кадра. */ +bool pcan_link_send_raw(pcan_link_t *link, const pcan_frame_t *frame); + +/** Помещается ли ещё хотя бы один кадр максимального размера. */ +bool pcan_link_can_send(const pcan_link_t *link); + +/** Скармливает каналу принятые из линии байты. @return число кадров. */ +size_t pcan_link_feed(pcan_link_t *link, const uint8_t *data, size_t len); + +static inline const pcan_parse_stats_t *pcan_link_rx_stats(const pcan_link_t *link) +{ + return &link->parser.stats; +} + +#ifdef __cplusplus +} +#endif + +#endif /* PCAN_LINK_H */ diff --git a/c/protocan-transport/include/pcan_ring.h b/c/protocan-transport/include/pcan_ring.h new file mode 100644 index 0000000..b02cd5a --- /dev/null +++ b/c/protocan-transport/include/pcan_ring.h @@ -0,0 +1,71 @@ +/** + * @file pcan_ring.h + * @brief Кольцевой буфер байтов: один писатель, один читатель. + * + * Размер обязан быть степенью двойки - индексы сворачиваются маской, + * поэтому head/tail обновляются одной записью и блокировки между + * фоновым кодом и обработчиком прерывания не нужны. + * + * Отдельно есть pcan_ring_linear(): отдаёт непрерывный участок данных + * без заворота - именно то, что требуется, чтобы отдать кусок в DMA. + */ +#ifndef PCAN_RING_H +#define PCAN_RING_H + +#include +#include +#include + +#include "pcan_config.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + uint8_t *buf; + uint16_t size; /**< степень двойки */ + volatile uint16_t head; /**< пишет производитель */ + volatile uint16_t tail; /**< пишет потребитель */ + volatile uint32_t dropped; /**< байт потеряно при переполнении */ +} pcan_ring_t; + +/** @return false, если size не является степенью двойки. */ +bool pcan_ring_init(pcan_ring_t *rb, uint8_t *storage, uint16_t size); + +uint16_t pcan_ring_count(const pcan_ring_t *rb); +uint16_t pcan_ring_free(const pcan_ring_t *rb); + +bool pcan_ring_put(pcan_ring_t *rb, uint8_t byte); +bool pcan_ring_get(pcan_ring_t *rb, uint8_t *byte); + +/** + * @brief Кладёт блок целиком либо не кладёт ничего. + * + * Всё-или-ничего принципиально: половина кадра в линии заставила бы + * приёмник ресинхронизироваться. + */ +bool pcan_ring_write(pcan_ring_t *rb, const uint8_t *data, uint16_t len); + +/** + * @brief Непрерывный участок готовых к выдаче данных (без заворота). + * @param[out] ptr указатель на начало участка; + * @return длина участка; 0 - данных нет. + * + * После того как участок передан, вызовите pcan_ring_consume(). + */ +uint16_t pcan_ring_linear(const pcan_ring_t *rb, const uint8_t **ptr); + +/** Помечает len байт как прочитанные. */ +void pcan_ring_consume(pcan_ring_t *rb, uint16_t len); + +static inline bool pcan_ring_empty(const pcan_ring_t *rb) +{ + return rb->head == rb->tail; +} + +#ifdef __cplusplus +} +#endif + +#endif /* PCAN_RING_H */ diff --git a/c/protocan-transport/include/protocan_transport.h b/c/protocan-transport/include/protocan_transport.h new file mode 100644 index 0000000..7ec1954 --- /dev/null +++ b/c/protocan-transport/include/protocan_transport.h @@ -0,0 +1,27 @@ +/** + * @file protocan_transport.h + * @brief Зонтичный заголовок библиотеки. Достаточно подключить его одного. + * + * Библиотека переносимая: C99, без динамической памяти, без ОС, + * без зависимостей от HAL и от конкретного МК. Платформа подключается + * через pcan_io_t (см. pcan_link.h) и примеры в ports/. + */ +#ifndef PROTOCAN_TRANSPORT_H +#define PROTOCAN_TRANSPORT_H + +#define PCAN_VERSION_MAJOR 1 +#define PCAN_VERSION_MINOR 0 +#define PCAN_VERSION_PATCH 0 +#define PCAN_VERSION_U16 ((PCAN_VERSION_MAJOR << 8) | \ + (PCAN_VERSION_MINOR << 4) | \ + PCAN_VERSION_PATCH) + +#include "pcan_config.h" +#include "pcan_crc.h" +#include "pcan_frame.h" +#include "pcan_gas.h" +#include "pcan_id.h" +#include "pcan_link.h" +#include "pcan_ring.h" + +#endif /* PROTOCAN_TRANSPORT_H */ diff --git a/c/protocan-transport/ports/stm32f4/pcan_uart_stm32f4.c b/c/protocan-transport/ports/stm32f4/pcan_uart_stm32f4.c new file mode 100644 index 0000000..f2e4c39 --- /dev/null +++ b/c/protocan-transport/ports/stm32f4/pcan_uart_stm32f4.c @@ -0,0 +1,287 @@ +#include + +#include "pcan_uart_stm32f4.h" + +/* --- Работа с флагами DMA ------------------------------------------------- + * У DMA на F4 флаги четырёх потоков лежат в LISR/LIFCR, ещё четырёх - + * в HISR/HIFCR, причём внутри регистра смещения неравномерны. + */ +static const uint8_t s_flag_shift[4] = { 0U, 6U, 16U, 22U }; + +#define DMA_FLAG_FEIF 0x01U +#define DMA_FLAG_DMEIF 0x04U +#define DMA_FLAG_TEIF 0x08U +#define DMA_FLAG_HTIF 0x10U +#define DMA_FLAG_TCIF 0x20U +#define DMA_FLAG_ALL (DMA_FLAG_FEIF | DMA_FLAG_DMEIF | DMA_FLAG_TEIF | \ + DMA_FLAG_HTIF | DMA_FLAG_TCIF) + +static inline uint32_t dma_isr(const DMA_TypeDef *dma, uint8_t stream) +{ + uint32_t reg = (stream < 4U) ? dma->LISR : dma->HISR; + return (reg >> s_flag_shift[stream & 3U]) & DMA_FLAG_ALL; +} + +static inline void dma_clear(DMA_TypeDef *dma, uint8_t stream, uint32_t flags) +{ + uint32_t value = (flags & DMA_FLAG_ALL) << s_flag_shift[stream & 3U]; + if (stream < 4U) { + dma->LIFCR = value; + } else { + dma->HIFCR = value; + } +} + +static inline void dma_disable(DMA_Stream_TypeDef *stream) +{ + stream->CR &= ~DMA_SxCR_EN; + /* Аппаратура снимает EN не мгновенно; трогать регистры потока + до этого момента нельзя. */ + while ((stream->CR & DMA_SxCR_EN) != 0U) { + } +} + +/* --- Управление драйвером RS485 ------------------------------------------- */ + +static inline void de_assert(const pcan_uart_t *u) +{ + if (u->cfg.de_port == NULL) { + return; + } + uint32_t pin = 1UL << u->cfg.de_pin; + u->cfg.de_port->BSRR = u->cfg.de_active_low ? (pin << 16) : pin; +} + +static inline void de_release(const pcan_uart_t *u) +{ + if (u->cfg.de_port == NULL) { + return; + } + uint32_t pin = 1UL << u->cfg.de_pin; + u->cfg.de_port->BSRR = u->cfg.de_active_low ? pin : (pin << 16); +} + +/* --- Передача ------------------------------------------------------------- + * Кольцо заворачивается, а DMA нужен непрерывный участок, поэтому за раз + * отдаём кусок до конца буфера. Остаток уйдёт следующей посылкой. + */ +static void tx_start(pcan_uart_t *u) +{ + if (u->tx_busy) { + return; + } + + const uint8_t *ptr = NULL; + uint16_t len = pcan_ring_linear(&u->tx, &ptr); + if (len == 0U) { + return; + } + + u->tx_busy = 1U; + u->tx_chunk = len; + + de_assert(u); + + /* Флаг TC мог остаться от прошлой посылки - иначе прерывание придёт + сразу и отпустит DE посреди новой. */ + (void)u->cfg.uart->SR; + u->cfg.uart->SR = ~USART_SR_TC; + + dma_clear(u->cfg.dma, u->cfg.tx_stream_idx, DMA_FLAG_ALL); + u->cfg.tx_stream->M0AR = (uint32_t)(uintptr_t)ptr; + u->cfg.tx_stream->NDTR = len; + u->cfg.tx_stream->CR |= DMA_SxCR_EN; +} + +void pcan_uart_dma_tx_irq(pcan_uart_t *u) +{ + uint32_t flags = dma_isr(u->cfg.dma, u->cfg.tx_stream_idx); + + if ((flags & (DMA_FLAG_TEIF | DMA_FLAG_DMEIF | DMA_FLAG_FEIF)) != 0U) { + dma_clear(u->cfg.dma, u->cfg.tx_stream_idx, DMA_FLAG_ALL); + dma_disable(u->cfg.tx_stream); + /* Ошибка передачи: кусок считаем отданным, иначе встанет очередь. */ + pcan_ring_consume(&u->tx, u->tx_chunk); + u->tx_busy = 0U; + u->uart_errors++; + tx_start(u); + return; + } + + if ((flags & DMA_FLAG_TCIF) == 0U) { + return; + } + + dma_clear(u->cfg.dma, u->cfg.tx_stream_idx, DMA_FLAG_ALL); + dma_disable(u->cfg.tx_stream); + pcan_ring_consume(&u->tx, u->tx_chunk); + u->tx_chunk = 0U; + u->tx_busy = 0U; + + if (!pcan_ring_empty(&u->tx)) { + tx_start(u); + return; + } + + /* Данных больше нет. DMA лишь дописал последний байт в DR - в линии он + ещё не весь. Отпускаем DE только по TC. */ + if (u->cfg.de_port != NULL) { + u->cfg.uart->CR1 |= USART_CR1_TCIE; + } +} + +void pcan_uart_irq(pcan_uart_t *u) +{ + uint32_t sr = u->cfg.uart->SR; + + if ((sr & (USART_SR_ORE | USART_SR_FE | USART_SR_NE | USART_SR_PE)) != 0U) { + (void)u->cfg.uart->DR; /* чтение SR + DR снимает флаги */ + u->uart_errors++; + } + + if (((sr & USART_SR_TC) != 0U) && + ((u->cfg.uart->CR1 & USART_CR1_TCIE) != 0U)) { + u->cfg.uart->SR = ~USART_SR_TC; + u->cfg.uart->CR1 &= ~USART_CR1_TCIE; + /* Если пока ждали TC успел встать новый кадр - DE не опускаем. */ + if (!u->tx_busy && pcan_ring_empty(&u->tx)) { + de_release(u); + } + } +} + +static size_t io_write(void *ctx, const uint8_t *data, size_t len) +{ + pcan_uart_t *u = (pcan_uart_t *)ctx; + + if (len > 0xFFFFU) { + return 0U; + } + if (!pcan_ring_write(&u->tx, data, (uint16_t)len)) { + return 0U; + } + /* Прерывание DMA может добраться до очереди одновременно с нами, + но tx_busy защищает от двойного запуска потока. */ + tx_start(u); + return len; +} + +static size_t io_space(void *ctx) +{ + pcan_uart_t *u = (pcan_uart_t *)ctx; + return pcan_ring_free(&u->tx); +} + +void pcan_uart_io(pcan_uart_t *u, pcan_io_t *io) +{ + io->write = io_write; + io->tx_space = io_space; + io->ctx = u; +} + +/* --- Приём ---------------------------------------------------------------- */ + +size_t pcan_uart_read(pcan_uart_t *u, uint8_t *out, size_t max) +{ + uint16_t head = (uint16_t)(u->rx_size - (uint16_t)u->cfg.rx_stream->NDTR); + uint16_t tail = u->rx_tail; + + if ((head == tail) || (max == 0U)) { + return 0U; + } + + uint16_t pending = (uint16_t)((head - tail) & (uint16_t)(u->rx_size - 1U)); + if (pending > (uint16_t)(u->rx_size - (u->rx_size / 4U))) { + /* Кольцо почти догнало нас: часть байт, вероятно, уже затёрта. + Точнее аппаратура не скажет - в circular-режиме DMA не + сигнализирует переполнение. */ + u->rx_overruns++; + } + + size_t n = (pending > max) ? max : pending; + for (size_t i = 0U; i < n; i++) { + out[i] = u->rx_buf[tail]; + tail = (uint16_t)((tail + 1U) & (uint16_t)(u->rx_size - 1U)); + } + u->rx_tail = tail; + return n; +} + +size_t pcan_uart_poll(pcan_uart_t *u, pcan_link_t *link) +{ + uint8_t chunk[64]; + size_t frames = 0U; + size_t n; + + while ((n = pcan_uart_read(u, chunk, sizeof chunk)) != 0U) { + frames += pcan_link_feed(link, chunk, n); + } + return frames; +} + +/* --- Инициализация -------------------------------------------------------- */ + +bool pcan_uart_init(pcan_uart_t *u, const pcan_uart_cfg_t *cfg, + uint8_t *tx_buf, uint16_t tx_size, + uint8_t *rx_buf, uint16_t rx_size) +{ + if ((u == NULL) || (cfg == NULL) || (cfg->uart == NULL) || + (cfg->dma == NULL) || (cfg->tx_stream == NULL) || + (cfg->rx_stream == NULL) || (cfg->baud == 0U)) { + return false; + } + if ((rx_buf == NULL) || (rx_size < 2U) || + ((rx_size & (uint16_t)(rx_size - 1U)) != 0U)) { + return false; + } + + u->cfg = *cfg; + if (!pcan_ring_init(&u->tx, tx_buf, tx_size)) { + return false; + } + u->rx_buf = rx_buf; + u->rx_size = rx_size; + u->rx_tail = 0U; + u->tx_chunk = 0U; + u->tx_busy = 0U; + u->rx_overruns = 0U; + u->uart_errors = 0U; + + /* -- USART: 8N1, OVER8 = 0 -- */ + cfg->uart->CR1 = 0U; + cfg->uart->CR2 = 0U; + cfg->uart->CR3 = 0U; + cfg->uart->BRR = (uint16_t)((cfg->pclk_hz + (cfg->baud / 2U)) / cfg->baud); + + /* -- DMA передачи: память -> периферия, обычный режим -- */ + dma_disable(cfg->tx_stream); + dma_clear(cfg->dma, cfg->tx_stream_idx, DMA_FLAG_ALL); + cfg->tx_stream->PAR = (uint32_t)(uintptr_t)&cfg->uart->DR; + cfg->tx_stream->FCR = 0U; /* прямой режим, без FIFO */ + cfg->tx_stream->CR = + ((uint32_t)cfg->tx_channel << DMA_SxCR_CHSEL_Pos) + | DMA_SxCR_DIR_0 /* мем -> периферия */ + | DMA_SxCR_MINC + | (2UL << DMA_SxCR_PL_Pos) /* высокий приоритет */ + | DMA_SxCR_TCIE | DMA_SxCR_TEIE; + + /* -- DMA приёма: периферия -> память, кольцевой, работает всегда -- */ + dma_disable(cfg->rx_stream); + dma_clear(cfg->dma, cfg->rx_stream_idx, DMA_FLAG_ALL); + cfg->rx_stream->PAR = (uint32_t)(uintptr_t)&cfg->uart->DR; + cfg->rx_stream->M0AR = (uint32_t)(uintptr_t)rx_buf; + cfg->rx_stream->NDTR = rx_size; + cfg->rx_stream->FCR = 0U; + cfg->rx_stream->CR = + ((uint32_t)cfg->rx_channel << DMA_SxCR_CHSEL_Pos) + | DMA_SxCR_MINC + | DMA_SxCR_CIRC + | (2UL << DMA_SxCR_PL_Pos); + cfg->rx_stream->CR |= DMA_SxCR_EN; + + de_release(u); + + cfg->uart->CR3 = USART_CR3_DMAT | USART_CR3_DMAR; + cfg->uart->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE; + return true; +} diff --git a/c/protocan-transport/ports/stm32f4/pcan_uart_stm32f4.h b/c/protocan-transport/ports/stm32f4/pcan_uart_stm32f4.h new file mode 100644 index 0000000..933cf19 --- /dev/null +++ b/c/protocan-transport/ports/stm32f4/pcan_uart_stm32f4.h @@ -0,0 +1,110 @@ +/** + * @file pcan_uart_stm32f4.h + * @brief Порт транспорта на USART + DMA для STM32F4. + * + * Передача - пакетная: DMA отдаёт непрерывный кусок очереди целиком, + * одно прерывание на пакет вместо одного на байт. + * Приём - кольцевой DMA, позиция вычисляется по NDTR, поэтому длина + * посылки заранее не нужна и байты не теряются между вызовами. + * + * Чего порт НЕ делает намеренно (это забота платы): + * - не включает такты RCC для USART, DMA и GPIO; + * - не настраивает выводы и альтернативные функции; + * - не разрешает прерывания в NVIC. + * Так один и тот же порт живёт на любой плате без правок. + * + * ВАЖНО про буферы: на STM32F407 DMA не имеет доступа к CCM RAM + * (0x10000000). Буферы обязаны лежать в основном SRAM (0x20000000), + * иначе передача молча не пойдёт. + * + * Прерывания, которые нужно прокинуть из вектора: + * DMAx_StreamN_IRQHandler -> pcan_uart_dma_tx_irq() + * USARTx_IRQHandler -> pcan_uart_irq() + */ +#ifndef PCAN_UART_STM32F4_H +#define PCAN_UART_STM32F4_H + +#include "stm32f4xx.h" + +#include "pcan_link.h" +#include "pcan_ring.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + USART_TypeDef *uart; + uint32_t pclk_hz; /**< частота шины этого USART */ + uint32_t baud; + + DMA_TypeDef *dma; + DMA_Stream_TypeDef *tx_stream; + uint8_t tx_stream_idx; /**< 0..7, нужен для флагов LISR/HISR */ + uint8_t tx_channel; /**< 0..7 */ + DMA_Stream_TypeDef *rx_stream; + uint8_t rx_stream_idx; + uint8_t rx_channel; + + /** Управление драйвером RS485. NULL - обычный полнодуплексный UART. */ + GPIO_TypeDef *de_port; + uint8_t de_pin; + uint8_t de_active_low; /**< 1, если DE активен низким уровнем */ +} pcan_uart_cfg_t; + +typedef struct { + pcan_uart_cfg_t cfg; + + pcan_ring_t tx; /**< очередь передачи */ + uint8_t *rx_buf; /**< кольцо приёма DMA */ + uint16_t rx_size; + uint16_t rx_tail; + + volatile uint16_t tx_chunk; /**< длина текущей DMA-посылки */ + volatile uint8_t tx_busy; + + uint32_t rx_overruns; /**< подозрение на потерю в кольце */ + uint32_t uart_errors; /**< ORE / FE / NE / PE */ +} pcan_uart_t; + +/** + * @brief Настраивает USART и оба потока DMA. + * @param tx_buf,tx_size очередь передачи, размер - степень двойки; + * @param rx_buf,rx_size кольцо приёма, размер - степень двойки. + * @return false при неверных аргументах. + */ +bool pcan_uart_init(pcan_uart_t *u, const pcan_uart_cfg_t *cfg, + uint8_t *tx_buf, uint16_t tx_size, + uint8_t *rx_buf, uint16_t rx_size); + +/** Заполняет интерфейс для pcan_link_init(). */ +void pcan_uart_io(pcan_uart_t *u, pcan_io_t *io); + +/** + * @brief Забирает принятые DMA байты в буфер вызывающего. + * + * Порт не знает, какой протокол поверх него живёт, поэтому отдаёт сырые + * байты: на одном и том же UART так работают и кадр полевой шины, + * и GUI-протокол. + * + * @return число скопированных байт; 0 - новых данных нет. + */ +size_t pcan_uart_read(pcan_uart_t *u, uint8_t *out, size_t max); + +/** + * @brief Отдаёт принятые DMA байты в канал pcan_link. Из главного цикла. + * @return число разобранных кадров. + */ +size_t pcan_uart_poll(pcan_uart_t *u, pcan_link_t *link); + +/** Обработчик прерывания потока DMA передачи. */ +void pcan_uart_dma_tx_irq(pcan_uart_t *u); + +/** Обработчик прерывания USART: снимает DE по флагу TC, считает ошибки. */ +void pcan_uart_irq(pcan_uart_t *u); + +#ifdef __cplusplus +} +#endif + +#endif /* PCAN_UART_STM32F4_H */ diff --git a/c/protocan-transport/src/gui_catalog.c b/c/protocan-transport/src/gui_catalog.c new file mode 100644 index 0000000..40030c2 --- /dev/null +++ b/c/protocan-transport/src/gui_catalog.c @@ -0,0 +1,195 @@ +#include "gui_catalog.h" + +static size_t name_len(const char *name) +{ + size_t n = 0U; + if (name == NULL) { + return 0U; + } + while ((name[n] != '\0') && (n <= GUI_NAME_SIZE)) { + n++; + } + return n; +} + +static uint8_t object_registers(uint8_t type) +{ + return ((type == GUI_OBJ_U32) || (type == GUI_OBJ_I32)) ? 2U : 1U; +} + +bool gui_catalog_validate(const gui_catalog_t *catalog, const pcan_gas_map_t *map) +{ + if ((catalog == NULL) || ((catalog->count != 0U) && (catalog->items == NULL))) { + return false; + } + + for (uint16_t i = 0U; i < catalog->count; i++) { + const gui_object_t *item = &catalog->items[i]; + if (name_len(item->name) > GUI_NAME_SIZE) { + return false; + } + if (item->type > GUI_OBJ_BITS) { + return false; + } + if (map != NULL) { + uint8_t words = object_registers(item->type); + for (uint8_t w = 0U; w < words; w++) { + uint32_t addr = (uint32_t)item->address + w; + if (addr > 0xFFFFUL) { + return false; + } + if (pcan_gas_find(map, (uint16_t)addr) == NULL) { + return false; + } + } + } + } + return true; +} + +size_t gui_catalog_encode(const gui_catalog_t *catalog, uint16_t start_index, + uint16_t max_count, uint8_t *out, size_t out_size) +{ + if ((catalog == NULL) || (out == NULL) || (start_index >= catalog->count)) { + return 0U; + } + + uint16_t available = (uint16_t)(catalog->count - start_index); + uint16_t count = ((max_count == 0U) || (max_count > available)) ? available : max_count; + if (count > GUI_ENTRIES_PER_FRAME) { + count = (uint16_t)GUI_ENTRIES_PER_FRAME; + } + size_t total = (size_t)GUI_CATALOG_HEADER + ((size_t)count * GUI_ENTRY_SIZE); + if (out_size < total) { + return 0U; + } + + gui_put_u16(&out[0], catalog->count); + gui_put_u16(&out[2], start_index); + gui_put_u16(&out[4], count); + + for (uint16_t i = 0U; i < count; i++) { + const gui_object_t *item = &catalog->items[start_index + i]; + uint8_t *dst = &out[GUI_CATALOG_HEADER + ((size_t)i * GUI_ENTRY_SIZE)]; + + gui_put_u16(&dst[0], item->address); + dst[2] = item->type; + dst[3] = item->flags; + dst[4] = (uint8_t)item->scale_pow10; + dst[5] = item->unit; + dst[6] = 0U; + dst[7] = 0U; + + size_t n = name_len(item->name); + if (n > GUI_NAME_SIZE) { + n = GUI_NAME_SIZE; + } + for (size_t c = 0U; c < n; c++) { + dst[8U + c] = (uint8_t)item->name[c]; + } + for (size_t c = n; c < GUI_NAME_SIZE; c++) { + dst[8U + c] = 0U; + } + } + return total; +} + +void gui_watch_init(gui_watch_t *watch) +{ + watch->period_ms = 0U; + watch->count = 0U; + watch->next_ms = 0U; + watch->sent = 0U; + watch->skipped = 0U; +} + +uint16_t gui_watch_apply(gui_watch_t *watch, const pcan_gas_map_t *map, + const uint8_t *payload, uint16_t size) +{ + if (size < 4U) { + return 0xFFFFU; + } + uint16_t period = gui_get_u16(&payload[0]); + uint16_t requested = gui_get_u16(&payload[2]); + if (size != (uint16_t)(4U + (requested * 2U))) { + return 0xFFFFU; + } + if (requested > GUI_WATCH_MAX) { + return 0xFFFFU; + } + + uint16_t accepted = 0U; + for (uint16_t i = 0U; i < requested; i++) { + uint16_t address = gui_get_u16(&payload[4U + (i * 2U)]); + /* Адрес вне карты молча пропускаем: иначе одна опечатка в GUI + отменяла бы всю подписку. */ + if ((map != NULL) && (pcan_gas_find(map, address) == NULL)) { + continue; + } + watch->address[accepted++] = address; + } + + watch->count = accepted; + watch->period_ms = (accepted == 0U) ? 0U : period; + watch->next_ms = 0U; + return accepted; +} + +size_t gui_watch_encode_ack(const gui_watch_t *watch, uint8_t *out, size_t out_size) +{ + if (out_size < 4U) { + return 0U; + } + gui_put_u16(&out[0], watch->period_ms); + gui_put_u16(&out[2], watch->count); + return 4U; +} + +bool gui_watch_due(const gui_watch_t *watch, uint32_t now_ms) +{ + if ((watch->period_ms == 0U) || (watch->count == 0U)) { + return false; + } + /* Разность беззнаковая: переполнение счётчика миллисекунд обрабатывается + само собой, сравнение с абсолютным временем - нет. */ + return (uint32_t)(now_ms - watch->next_ms) < 0x80000000UL; +} + +size_t gui_watch_encode_data(const gui_watch_t *watch, const pcan_gas_map_t *map, + uint32_t timestamp_ms, uint8_t *out, size_t out_size) +{ + if ((watch->count == 0U) || (out == NULL)) { + return 0U; + } + size_t total = 6U + ((size_t)watch->count * 2U); + if (out_size < total) { + return 0U; + } + + gui_put_u32(&out[0], timestamp_ms); + gui_put_u16(&out[4], watch->count); + for (uint16_t i = 0U; i < watch->count; i++) { + uint16_t value = 0U; + /* Регистр мог исчезнуть между подпиской и опросом - тогда 0, + а не мусор из стека. */ + (void)pcan_gas_read(map, watch->address[i], &value); + gui_put_u16(&out[6U + ((size_t)i * 2U)], value); + } + return total; +} + +void gui_watch_advance(gui_watch_t *watch, uint32_t now_ms) +{ + if (watch->period_ms == 0U) { + return; + } + uint32_t next = watch->next_ms + watch->period_ms; + /* Если отстали больше чем на такт (линия была занята), не пытаемся + нагнать пачкой пакетов - идём от текущего момента. */ + if ((uint32_t)(now_ms - next) >= 0x80000000UL) { + watch->next_ms = next; + } else { + watch->next_ms = now_ms + watch->period_ms; + } + watch->sent++; +} diff --git a/c/protocan-transport/src/gui_frame.c b/c/protocan-transport/src/gui_frame.c new file mode 100644 index 0000000..602fd92 --- /dev/null +++ b/c/protocan-transport/src/gui_frame.c @@ -0,0 +1,230 @@ +#include "gui_frame.h" + +uint32_t gui_crc32(const uint8_t *data, size_t len) +{ + uint32_t crc = 0xFFFFFFFFUL; + for (size_t i = 0U; i < len; i++) { + crc ^= data[i]; + for (uint8_t bit = 0U; bit < 8U; bit++) { + crc = (crc & 1UL) ? ((crc >> 1) ^ 0xEDB88320UL) : (crc >> 1); + } + } + return crc ^ 0xFFFFFFFFUL; +} + +size_t gui_frame_encode(uint8_t type, uint16_t sequence, + const uint8_t *payload, uint16_t size, + uint8_t *out, size_t out_size) +{ + if ((out == NULL) || (size > GUI_MAX_PAYLOAD) || + ((size > 0U) && (payload == NULL))) { + return 0U; + } + size_t total = (size_t)GUI_HEADER_SIZE + size + GUI_CRC_SIZE; + if (out_size < total) { + return 0U; + } + + out[0] = GUI_SOF0; + out[1] = GUI_SOF1; + /* CRC считается от version до конца payload; SOF в него не входит. */ + out[2] = GUI_PROTOCOL_VERSION; + out[3] = type; + out[4] = (uint8_t)(sequence >> 8); /* заголовок big-endian */ + out[5] = (uint8_t)(sequence & 0xFFU); + out[6] = (uint8_t)(size >> 8); + out[7] = (uint8_t)(size & 0xFFU); + for (uint16_t i = 0U; i < size; i++) { + out[GUI_HEADER_SIZE + i] = payload[i]; + } + + uint32_t crc = gui_crc32(&out[2], (size_t)6U + size); + gui_put_u32(&out[GUI_HEADER_SIZE + size], crc); /* CRC little-endian */ + return total; +} + +void gui_parser_init(gui_parser_t *p) +{ + p->state = GUI_ST_SOF0; + p->idx = 0U; + p->size = 0U; + p->stats.frames = 0U; + p->stats.crc_errors = 0U; + p->stats.version_errors = 0U; + p->stats.length_errors = 0U; + p->stats.stray_bytes = 0U; +} + +bool gui_parser_push(gui_parser_t *p, uint8_t byte, gui_frame_t *out) +{ + switch (p->state) { + case GUI_ST_SOF0: + if (byte == GUI_SOF0) { + p->state = GUI_ST_SOF1; + } else { + p->stats.stray_bytes++; + } + break; + + case GUI_ST_SOF1: + if (byte == GUI_SOF1) { + p->idx = 0U; + p->state = GUI_ST_HEADER; + } else if (byte == GUI_SOF0) { + /* A5 A5 5A - тоже валидное начало, остаёмся здесь. */ + p->stats.stray_bytes++; + } else { + p->stats.stray_bytes += 2U; + p->state = GUI_ST_SOF0; + } + break; + + case GUI_ST_HEADER: + /* Версию проверяем сразу на первом байте заголовка. Если ждать все + шесть, ложная сигнатура успеет проглотить начало настоящего кадра + и он будет потерян целиком. */ + if ((p->idx == 0U) && (byte != GUI_PROTOCOL_VERSION)) { + p->stats.version_errors++; + p->state = (byte == GUI_SOF0) ? GUI_ST_SOF1 : GUI_ST_SOF0; + break; + } + p->header[p->idx++] = byte; + if (p->idx < 6U) { + break; + } + p->size = (uint16_t)(((uint16_t)p->header[4] << 8) | p->header[5]); + if (p->size > GUI_RX_PAYLOAD_MAX) { + /* Либо мусор, либо кадр, который прибор обслужить не может. + И то и другое - повод вернуться к поиску сигнатуры. */ + p->stats.length_errors++; + p->state = GUI_ST_SOF0; + break; + } + p->idx = 0U; + p->state = (p->size > 0U) ? GUI_ST_PAYLOAD : GUI_ST_CRC; + break; + + case GUI_ST_PAYLOAD: + p->payload[p->idx++] = byte; + if (p->idx >= p->size) { + p->idx = 0U; + p->state = GUI_ST_CRC; + } + break; + + case GUI_ST_CRC: { + p->crc_bytes[p->idx++] = byte; + if (p->idx < GUI_CRC_SIZE) { + break; + } + p->state = GUI_ST_SOF0; + + uint32_t received = ((uint32_t)p->crc_bytes[0]) + | ((uint32_t)p->crc_bytes[1] << 8) + | ((uint32_t)p->crc_bytes[2] << 16) + | ((uint32_t)p->crc_bytes[3] << 24); + + /* CRC защищает header[0..5] и payload; считаем по частям, + чтобы не собирать кадр целиком во втором буфере. */ + uint32_t crc = 0xFFFFFFFFUL; + for (uint8_t i = 0U; i < 6U; i++) { + crc ^= p->header[i]; + for (uint8_t bit = 0U; bit < 8U; bit++) { + crc = (crc & 1UL) ? ((crc >> 1) ^ 0xEDB88320UL) : (crc >> 1); + } + } + for (uint16_t i = 0U; i < p->size; i++) { + crc ^= p->payload[i]; + for (uint8_t bit = 0U; bit < 8U; bit++) { + crc = (crc & 1UL) ? ((crc >> 1) ^ 0xEDB88320UL) : (crc >> 1); + } + } + crc ^= 0xFFFFFFFFUL; + + if (crc != received) { + p->stats.crc_errors++; + return false; + } + out->type = p->header[1]; + out->sequence = (uint16_t)(((uint16_t)p->header[2] << 8) | p->header[3]); + out->size = p->size; + out->payload = p->payload; + p->stats.frames++; + return true; + } + + default: + p->state = GUI_ST_SOF0; + break; + } + return false; +} + +size_t gui_parser_feed(gui_parser_t *p, const uint8_t *data, size_t len, + gui_frame_cb_t cb, void *user) +{ + gui_frame_t frame; + size_t count = 0U; + + for (size_t i = 0U; i < len; i++) { + if (gui_parser_push(p, data[i], &frame)) { + count++; + if (cb != NULL) { + cb(&frame, user); + } + } + } + return count; +} + +bool gui_link_init(gui_link_t *link, const pcan_io_t *io, + gui_frame_cb_t on_frame, void *user) +{ + if ((link == NULL) || (io == NULL) || (io->write == NULL)) { + return false; + } + link->io = *io; + link->on_frame = on_frame; + link->user = user; + link->tx_frames = 0U; + link->tx_dropped = 0U; + gui_parser_init(&link->parser); + return true; +} + +bool gui_link_can_send(const gui_link_t *link, uint16_t size) +{ + if (link->io.tx_space == NULL) { + return true; + } + return link->io.tx_space(link->io.ctx) >= ((size_t)GUI_HEADER_SIZE + size + GUI_CRC_SIZE); +} + +bool gui_link_send(gui_link_t *link, uint8_t type, uint16_t sequence, + const uint8_t *payload, uint16_t size) +{ + size_t n = gui_frame_encode(type, sequence, payload, size, + link->tx_buf, sizeof link->tx_buf); + if (n == 0U) { + link->tx_dropped++; + return false; + } + if (link->io.write(link->io.ctx, link->tx_buf, n) != n) { + link->tx_dropped++; + return false; + } + link->tx_frames++; + return true; +} + +bool gui_link_nack(gui_link_t *link, uint16_t sequence, uint16_t result) +{ + uint8_t payload[2]; + gui_put_u16(payload, result); + return gui_link_send(link, GUI_MSG_NACK, sequence, payload, sizeof payload); +} + +size_t gui_link_feed(gui_link_t *link, const uint8_t *data, size_t len) +{ + return gui_parser_feed(&link->parser, data, len, link->on_frame, link->user); +} diff --git a/c/protocan-transport/src/pcan_crc.c b/c/protocan-transport/src/pcan_crc.c new file mode 100644 index 0000000..7607378 --- /dev/null +++ b/c/protocan-transport/src/pcan_crc.c @@ -0,0 +1,66 @@ +#include "pcan_crc.h" + +#if PCAN_CRC_TABLE + +static const uint16_t s_table[256] = { + 0x0000U, 0x1021U, 0x2042U, 0x3063U, 0x4084U, 0x50A5U, 0x60C6U, 0x70E7U, + 0x8108U, 0x9129U, 0xA14AU, 0xB16BU, 0xC18CU, 0xD1ADU, 0xE1CEU, 0xF1EFU, + 0x1231U, 0x0210U, 0x3273U, 0x2252U, 0x52B5U, 0x4294U, 0x72F7U, 0x62D6U, + 0x9339U, 0x8318U, 0xB37BU, 0xA35AU, 0xD3BDU, 0xC39CU, 0xF3FFU, 0xE3DEU, + 0x2462U, 0x3443U, 0x0420U, 0x1401U, 0x64E6U, 0x74C7U, 0x44A4U, 0x5485U, + 0xA56AU, 0xB54BU, 0x8528U, 0x9509U, 0xE5EEU, 0xF5CFU, 0xC5ACU, 0xD58DU, + 0x3653U, 0x2672U, 0x1611U, 0x0630U, 0x76D7U, 0x66F6U, 0x5695U, 0x46B4U, + 0xB75BU, 0xA77AU, 0x9719U, 0x8738U, 0xF7DFU, 0xE7FEU, 0xD79DU, 0xC7BCU, + 0x48C4U, 0x58E5U, 0x6886U, 0x78A7U, 0x0840U, 0x1861U, 0x2802U, 0x3823U, + 0xC9CCU, 0xD9EDU, 0xE98EU, 0xF9AFU, 0x8948U, 0x9969U, 0xA90AU, 0xB92BU, + 0x5AF5U, 0x4AD4U, 0x7AB7U, 0x6A96U, 0x1A71U, 0x0A50U, 0x3A33U, 0x2A12U, + 0xDBFDU, 0xCBDCU, 0xFBBFU, 0xEB9EU, 0x9B79U, 0x8B58U, 0xBB3BU, 0xAB1AU, + 0x6CA6U, 0x7C87U, 0x4CE4U, 0x5CC5U, 0x2C22U, 0x3C03U, 0x0C60U, 0x1C41U, + 0xEDAEU, 0xFD8FU, 0xCDECU, 0xDDCDU, 0xAD2AU, 0xBD0BU, 0x8D68U, 0x9D49U, + 0x7E97U, 0x6EB6U, 0x5ED5U, 0x4EF4U, 0x3E13U, 0x2E32U, 0x1E51U, 0x0E70U, + 0xFF9FU, 0xEFBEU, 0xDFDDU, 0xCFFCU, 0xBF1BU, 0xAF3AU, 0x9F59U, 0x8F78U, + 0x9188U, 0x81A9U, 0xB1CAU, 0xA1EBU, 0xD10CU, 0xC12DU, 0xF14EU, 0xE16FU, + 0x1080U, 0x00A1U, 0x30C2U, 0x20E3U, 0x5004U, 0x4025U, 0x7046U, 0x6067U, + 0x83B9U, 0x9398U, 0xA3FBU, 0xB3DAU, 0xC33DU, 0xD31CU, 0xE37FU, 0xF35EU, + 0x02B1U, 0x1290U, 0x22F3U, 0x32D2U, 0x4235U, 0x5214U, 0x6277U, 0x7256U, + 0xB5EAU, 0xA5CBU, 0x95A8U, 0x8589U, 0xF56EU, 0xE54FU, 0xD52CU, 0xC50DU, + 0x34E2U, 0x24C3U, 0x14A0U, 0x0481U, 0x7466U, 0x6447U, 0x5424U, 0x4405U, + 0xA7DBU, 0xB7FAU, 0x8799U, 0x97B8U, 0xE75FU, 0xF77EU, 0xC71DU, 0xD73CU, + 0x26D3U, 0x36F2U, 0x0691U, 0x16B0U, 0x6657U, 0x7676U, 0x4615U, 0x5634U, + 0xD94CU, 0xC96DU, 0xF90EU, 0xE92FU, 0x99C8U, 0x89E9U, 0xB98AU, 0xA9ABU, + 0x5844U, 0x4865U, 0x7806U, 0x6827U, 0x18C0U, 0x08E1U, 0x3882U, 0x28A3U, + 0xCB7DU, 0xDB5CU, 0xEB3FU, 0xFB1EU, 0x8BF9U, 0x9BD8U, 0xABBBU, 0xBB9AU, + 0x4A75U, 0x5A54U, 0x6A37U, 0x7A16U, 0x0AF1U, 0x1AD0U, 0x2AB3U, 0x3A92U, + 0xFD2EU, 0xED0FU, 0xDD6CU, 0xCD4DU, 0xBDAAU, 0xAD8BU, 0x9DE8U, 0x8DC9U, + 0x7C26U, 0x6C07U, 0x5C64U, 0x4C45U, 0x3CA2U, 0x2C83U, 0x1CE0U, 0x0CC1U, + 0xEF1FU, 0xFF3EU, 0xCF5DU, 0xDF7CU, 0xAF9BU, 0xBFBAU, 0x8FD9U, 0x9FF8U, + 0x6E17U, 0x7E36U, 0x4E55U, 0x5E74U, 0x2E93U, 0x3EB2U, 0x0ED1U, 0x1EF0U +}; + +uint16_t pcan_crc16_update(uint16_t crc, uint8_t byte) +{ + return (uint16_t)((crc << 8) ^ s_table[(uint8_t)((crc >> 8) ^ byte)]); +} + +#else /* побитовый расчёт: 0 байт таблиц, ~8 итераций на байт */ + +uint16_t pcan_crc16_update(uint16_t crc, uint8_t byte) +{ + crc ^= (uint16_t)((uint16_t)byte << 8); + for (uint8_t bit = 0U; bit < 8U; bit++) { + crc = (crc & 0x8000U) ? (uint16_t)((crc << 1) ^ 0x1021U) + : (uint16_t)(crc << 1); + } + return crc; +} + +#endif + +uint16_t pcan_crc16(const uint8_t *data, size_t len) +{ + uint16_t crc = PCAN_CRC_INIT; + for (size_t i = 0U; i < len; i++) { + crc = pcan_crc16_update(crc, data[i]); + } + return crc; +} diff --git a/c/protocan-transport/src/pcan_frame.c b/c/protocan-transport/src/pcan_frame.c new file mode 100644 index 0000000..4d395c7 --- /dev/null +++ b/c/protocan-transport/src/pcan_frame.c @@ -0,0 +1,141 @@ +#include "pcan_frame.h" +#include "pcan_crc.h" + +size_t pcan_frame_encode(const pcan_frame_t *frame, uint8_t *out, size_t out_size) +{ + if ((frame == NULL) || (out == NULL) || (out_size < PCAN_FRAME_MAX)) { + return 0U; + } + + uint8_t dlc = (frame->dlc > PCAN_DATA_MAX) ? (uint8_t)PCAN_DATA_MAX : frame->dlc; + size_t n = 0U; + + out[n++] = PCAN_SOF0; + out[n++] = PCAN_SOF1; + + size_t body = n; + out[n++] = (uint8_t)(PCAN_LEN_MIN + dlc); + out[n++] = frame->seq; + out[n++] = frame->flags; + out[n++] = (uint8_t)(frame->id & 0xFFU); + out[n++] = (uint8_t)((frame->id >> 8) & 0xFFU); + out[n++] = (uint8_t)((frame->id >> 16) & 0xFFU); + out[n++] = (uint8_t)((frame->id >> 24) & 0x1FU); + for (uint8_t i = 0U; i < dlc; i++) { + out[n++] = frame->data[i]; + } + + uint16_t crc = pcan_crc16(&out[body], n - body); + out[n++] = (uint8_t)(crc & 0xFFU); + out[n++] = (uint8_t)(crc >> 8); + return n; +} + +void pcan_parser_init(pcan_parser_t *p) +{ + p->state = PCAN_ST_SOF0; + p->len = 0U; + p->idx = 0U; + p->crc_rx = 0U; + p->stats.frames = 0U; + p->stats.crc_errors = 0U; + p->stats.bad_len = 0U; + p->stats.stray_bytes = 0U; +} + +bool pcan_parser_push(pcan_parser_t *p, uint8_t byte, pcan_frame_t *out) +{ + switch (p->state) { + case PCAN_ST_SOF0: + if (byte == PCAN_SOF0) { + p->state = PCAN_ST_SOF1; + } else { + p->stats.stray_bytes++; + } + break; + + case PCAN_ST_SOF1: + if (byte == PCAN_SOF1) { + p->state = PCAN_ST_LEN; + } else if (byte == PCAN_SOF0) { + /* 0xAA 0xAA 0x55 - тоже валидное начало, остаёмся здесь. */ + p->stats.stray_bytes++; + } else { + p->stats.stray_bytes += 2U; + p->state = PCAN_ST_SOF0; + } + break; + + case PCAN_ST_LEN: + if ((byte < PCAN_LEN_MIN) || (byte > PCAN_LEN_MAX)) { + p->stats.bad_len++; + /* Байт мог оказаться началом настоящего кадра. */ + p->state = (byte == PCAN_SOF0) ? PCAN_ST_SOF1 : PCAN_ST_SOF0; + break; + } + p->len = byte; + p->body[0] = byte; + p->idx = 1U; + p->state = PCAN_ST_BODY; + break; + + case PCAN_ST_BODY: + p->body[p->idx++] = byte; + if (p->idx >= (uint8_t)(p->len + 1U)) { + p->state = PCAN_ST_CRC_L; + } + break; + + case PCAN_ST_CRC_L: + p->crc_rx = byte; + p->state = PCAN_ST_CRC_H; + break; + + case PCAN_ST_CRC_H: { + p->crc_rx |= (uint16_t)((uint16_t)byte << 8); + p->state = PCAN_ST_SOF0; + if (p->crc_rx != pcan_crc16(p->body, (size_t)p->len + 1U)) { + p->stats.crc_errors++; + return false; + } + out->seq = p->body[1]; + out->flags = p->body[2]; + out->id = ((uint32_t)p->body[3]) + | ((uint32_t)p->body[4] << 8) + | ((uint32_t)p->body[5] << 16) + | ((uint32_t)p->body[6] << 24); + out->id &= 0x1FFFFFFFUL; + out->dlc = (uint8_t)(p->len - PCAN_LEN_MIN); + for (uint8_t i = 0U; i < out->dlc; i++) { + out->data[i] = p->body[7U + i]; + } + for (uint8_t i = out->dlc; i < PCAN_DATA_MAX; i++) { + out->data[i] = 0U; + } + p->stats.frames++; + return true; + } + + default: + p->state = PCAN_ST_SOF0; + break; + } + return false; +} + +size_t pcan_parser_feed(pcan_parser_t *p, const uint8_t *data, size_t len, + pcan_frame_cb_t cb, void *user) +{ + pcan_frame_t frame; + size_t count = 0U; + + for (size_t i = 0U; i < len; i++) { + if (pcan_parser_push(p, data[i], &frame)) { + count++; + if (cb != NULL) { + cb(&frame, user); + } + } + } + return count; +} diff --git a/c/protocan-transport/src/pcan_gas.c b/c/protocan-transport/src/pcan_gas.c new file mode 100644 index 0000000..b941e40 --- /dev/null +++ b/c/protocan-transport/src/pcan_gas.c @@ -0,0 +1,200 @@ +#include "pcan_gas.h" +#include "pcan_id.h" + +bool pcan_gas_map_validate(const pcan_gas_map_t *map) +{ + if ((map == NULL) || ((map->count != 0U) && (map->regions == NULL))) { + return false; + } + if (map->count > PCAN_GAS_MAX_REGIONS) { + return false; + } + + uint32_t prev_end = 0U; + for (uint16_t i = 0U; i < map->count; i++) { + const pcan_gas_region_t *r = &map->regions[i]; + if (r->count == 0U) { + return false; + } + /* Регион не должен уходить за 0xFFFF. */ + if (((uint32_t)r->base + r->count) > 0x10000UL) { + return false; + } + if ((r->storage == NULL) && (r->read == NULL) && (r->write == NULL)) { + return false; + } + if ((i != 0U) && ((uint32_t)r->base < prev_end)) { + return false; /* не отсортировано либо перекрытие */ + } + prev_end = (uint32_t)r->base + r->count; + } + return true; +} + +const pcan_gas_region_t *pcan_gas_find(const pcan_gas_map_t *map, uint16_t addr) +{ + if ((map == NULL) || (map->regions == NULL)) { + return NULL; + } + /* Регионы отсортированы, поэтому двоичный поиск. */ + uint16_t lo = 0U; + uint16_t hi = map->count; + while (lo < hi) { + uint16_t mid = (uint16_t)(lo + ((hi - lo) / 2U)); + const pcan_gas_region_t *r = &map->regions[mid]; + if (addr < r->base) { + hi = mid; + } else if ((uint32_t)addr >= ((uint32_t)r->base + r->count)) { + lo = (uint16_t)(mid + 1U); + } else { + return r; + } + } + return NULL; +} + +pcan_gas_status_t pcan_gas_read(const pcan_gas_map_t *map, uint16_t addr, + uint16_t *value) +{ + const pcan_gas_region_t *r = pcan_gas_find(map, addr); + if (r == NULL) { + return PCAN_GAS_NO_REG; + } + if ((r->flags & PCAN_GAS_WRONLY) != 0U) { + return PCAN_GAS_WRITE_ONLY; + } + + uint16_t offset = (uint16_t)(addr - r->base); + if (r->read != NULL) { + return r->read(r, offset, value); + } + if (r->storage != NULL) { + *value = r->storage[offset]; + return PCAN_GAS_OK; + } + return PCAN_GAS_WRITE_ONLY; +} + +pcan_gas_status_t pcan_gas_write(const pcan_gas_map_t *map, uint16_t addr, + uint16_t value) +{ + const pcan_gas_region_t *r = pcan_gas_find(map, addr); + if (r == NULL) { + return PCAN_GAS_NO_REG; + } + if ((r->flags & PCAN_GAS_RDONLY) != 0U) { + return PCAN_GAS_READ_ONLY; + } + + uint16_t offset = (uint16_t)(addr - r->base); + if (r->write != NULL) { + return r->write(r, offset, value); + } + if (r->storage != NULL) { + r->storage[offset] = value; + return PCAN_GAS_OK; + } + return PCAN_GAS_READ_ONLY; +} + +uint16_t pcan_gas_read_block(const pcan_gas_map_t *map, uint16_t addr, + uint16_t *out, uint16_t max) +{ + uint16_t n = 0U; + while (n < max) { + /* Останавливаемся на 0xFFFF, чтобы адрес не завернулся в 0. */ + if (pcan_gas_read(map, (uint16_t)(addr + n), &out[n]) != PCAN_GAS_OK) { + break; + } + n++; + if (((uint32_t)addr + n) > 0xFFFFUL) { + break; + } + } + return n; +} + +uint16_t pcan_gas_write_block(const pcan_gas_map_t *map, uint16_t addr, + const uint16_t *in, uint16_t count) +{ + uint16_t n = 0U; + while (n < count) { + if (pcan_gas_write(map, (uint16_t)(addr + n), in[n]) != PCAN_GAS_OK) { + break; + } + n++; + if (((uint32_t)addr + n) > 0xFFFFUL) { + break; + } + } + return n; +} + +uint16_t pcan_gas_frame_to_regs(const pcan_frame_t *frame, uint16_t *out, + uint16_t max) +{ + uint16_t n = 0U; + for (uint8_t i = 0U; (uint8_t)(i + 1U) < frame->dlc; i = (uint8_t)(i + 2U)) { + if (n >= max) { + break; + } + out[n++] = (uint16_t)((uint16_t)frame->data[i] | + ((uint16_t)frame->data[i + 1U] << 8)); + } + return n; +} + +void pcan_gas_regs_to_frame(pcan_frame_t *frame, uint32_t base_id, + uint16_t addr, const uint16_t *regs, uint16_t count) +{ + if (count > PCAN_GAS_REGS_PER_FRAME) { + count = PCAN_GAS_REGS_PER_FRAME; + } + + pcan_id_t id; + pcan_id_unpack(base_id, &id); + id.msg_type = PCAN_MSG_GAS; + id.msg_body = addr; + + frame->id = pcan_id_pack(&id); + frame->flags = PCAN_FLAG_IDE; + frame->seq = 0U; + frame->dlc = (uint8_t)(count * 2U); + for (uint16_t i = 0U; i < count; i++) { + frame->data[i * 2U] = (uint8_t)(regs[i] & 0xFFU); + frame->data[(i * 2U) + 1U] = (uint8_t)(regs[i] >> 8); + } + for (uint8_t i = frame->dlc; i < PCAN_DATA_MAX; i++) { + frame->data[i] = 0U; + } +} + +bool pcan_gas_handle(const pcan_gas_map_t *map, const pcan_frame_t *req, + pcan_frame_t *rsp) +{ + pcan_id_t id; + pcan_id_unpack(req->id, &id); + if (id.msg_type != (uint8_t)PCAN_MSG_GAS) { + return false; + } + + if (req->dlc == 0U) { + uint16_t regs[PCAN_GAS_REGS_PER_FRAME]; + uint16_t n = pcan_gas_read_block(map, id.msg_body, regs, + PCAN_GAS_REGS_PER_FRAME); + if (n == 0U) { + return false; /* адреса нет в карте - отвечать нечем */ + } + pcan_gas_regs_to_frame(rsp, req->id, id.msg_body, regs, n); + pcan_id_unpack(rsp->id, &id); + id.route = PCAN_ROUTE_FROM_DEVICE; + rsp->id = pcan_id_pack(&id); + rsp->flags = (uint8_t)(req->flags & PCAN_FLAG_IDE); + return true; + } + + uint16_t regs[PCAN_GAS_REGS_PER_FRAME]; + uint16_t n = pcan_gas_frame_to_regs(req, regs, PCAN_GAS_REGS_PER_FRAME); + (void)pcan_gas_write_block(map, id.msg_body, regs, n); + return false; +} diff --git a/c/protocan-transport/src/pcan_id.c b/c/protocan-transport/src/pcan_id.c new file mode 100644 index 0000000..910b199 --- /dev/null +++ b/c/protocan-transport/src/pcan_id.c @@ -0,0 +1,22 @@ +#include "pcan_id.h" + +uint32_t pcan_id_pack(const pcan_id_t *id) +{ + return ((uint32_t)(id->priority & 0x1U) << 28) + | ((uint32_t)(id->route & 0x1U) << 27) + | ((uint32_t)(id->device_type & 0x7U) << 24) + | ((uint32_t)(id->device_id & 0xFU) << 20) + | ((uint32_t)(id->msg_type & 0xFU) << 16) + | ((uint32_t)id->msg_body); +} + +void pcan_id_unpack(uint32_t raw, pcan_id_t *id) +{ + raw &= PCAN_ID_MASK; + id->msg_body = (uint16_t)(raw & 0xFFFFU); + id->msg_type = (uint8_t)((raw >> 16) & 0xFU); + id->device_id = (uint8_t)((raw >> 20) & 0xFU); + id->device_type = (uint8_t)((raw >> 24) & 0x7U); + id->route = (uint8_t)((raw >> 27) & 0x1U); + id->priority = (uint8_t)((raw >> 28) & 0x1U); +} diff --git a/c/protocan-transport/src/pcan_link.c b/c/protocan-transport/src/pcan_link.c new file mode 100644 index 0000000..c3065ad --- /dev/null +++ b/c/protocan-transport/src/pcan_link.c @@ -0,0 +1,59 @@ +#include "pcan_link.h" + +bool pcan_link_init(pcan_link_t *link, const pcan_io_t *io, + pcan_frame_cb_t on_frame, void *user) +{ + if ((link == NULL) || (io == NULL) || (io->write == NULL)) { + return false; + } + link->io = *io; + link->on_frame = on_frame; + link->user = user; + link->tx_seq = 0U; + link->tx.tx_frames = 0U; + link->tx.tx_dropped = 0U; + pcan_parser_init(&link->parser); + return true; +} + +bool pcan_link_can_send(const pcan_link_t *link) +{ + if (link->io.tx_space == NULL) { + return true; + } + return link->io.tx_space(link->io.ctx) >= PCAN_FRAME_MAX; +} + +bool pcan_link_send_raw(pcan_link_t *link, const pcan_frame_t *frame) +{ + uint8_t raw[PCAN_FRAME_MAX]; + + size_t n = pcan_frame_encode(frame, raw, sizeof raw); + if (n == 0U) { + link->tx.tx_dropped++; + return false; + } + if (link->io.write(link->io.ctx, raw, n) != n) { + link->tx.tx_dropped++; + return false; + } + link->tx.tx_frames++; + return true; +} + +bool pcan_link_send(pcan_link_t *link, pcan_frame_t *frame) +{ + frame->seq = link->tx_seq; + if (!pcan_link_send_raw(link, frame)) { + return false; + } + /* SEQ двигаем только на успешно отданном кадре: иначе приёмник + засчитает потерю там, где кадра просто не было. */ + link->tx_seq++; + return true; +} + +size_t pcan_link_feed(pcan_link_t *link, const uint8_t *data, size_t len) +{ + return pcan_parser_feed(&link->parser, data, len, link->on_frame, link->user); +} diff --git a/c/protocan-transport/src/pcan_ring.c b/c/protocan-transport/src/pcan_ring.c new file mode 100644 index 0000000..374c88c --- /dev/null +++ b/c/protocan-transport/src/pcan_ring.c @@ -0,0 +1,88 @@ +#include "pcan_ring.h" + +bool pcan_ring_init(pcan_ring_t *rb, uint8_t *storage, uint16_t size) +{ + if ((rb == NULL) || (storage == NULL) || (size < 2U) || + ((size & (uint16_t)(size - 1U)) != 0U)) { + return false; + } + rb->buf = storage; + rb->size = size; + rb->head = 0U; + rb->tail = 0U; + rb->dropped = 0U; + return true; +} + +uint16_t pcan_ring_count(const pcan_ring_t *rb) +{ + return (uint16_t)((rb->head - rb->tail) & (uint16_t)(rb->size - 1U)); +} + +uint16_t pcan_ring_free(const pcan_ring_t *rb) +{ + return (uint16_t)(rb->size - 1U - pcan_ring_count(rb)); +} + +bool pcan_ring_put(pcan_ring_t *rb, uint8_t byte) +{ + uint16_t next = (uint16_t)((rb->head + 1U) & (uint16_t)(rb->size - 1U)); + if (next == rb->tail) { + rb->dropped++; + return false; + } + rb->buf[rb->head] = byte; + PCAN_BARRIER(); + rb->head = next; + return true; +} + +bool pcan_ring_get(pcan_ring_t *rb, uint8_t *byte) +{ + if (rb->head == rb->tail) { + return false; + } + *byte = rb->buf[rb->tail]; + PCAN_BARRIER(); + rb->tail = (uint16_t)((rb->tail + 1U) & (uint16_t)(rb->size - 1U)); + return true; +} + +bool pcan_ring_write(pcan_ring_t *rb, const uint8_t *data, uint16_t len) +{ + if (pcan_ring_free(rb) < len) { + rb->dropped += len; + return false; + } + + uint16_t mask = (uint16_t)(rb->size - 1U); + uint16_t head = rb->head; + for (uint16_t i = 0U; i < len; i++) { + rb->buf[head] = data[i]; + head = (uint16_t)((head + 1U) & mask); + } + /* Публикуем весь блок одной записью: читатель никогда не увидит + кадр наполовину. */ + PCAN_BARRIER(); + rb->head = head; + return true; +} + +uint16_t pcan_ring_linear(const pcan_ring_t *rb, const uint8_t **ptr) +{ + uint16_t head = rb->head; + uint16_t tail = rb->tail; + + if (head == tail) { + *ptr = NULL; + return 0U; + } + *ptr = &rb->buf[tail]; + return (head > tail) ? (uint16_t)(head - tail) + : (uint16_t)(rb->size - tail); +} + +void pcan_ring_consume(pcan_ring_t *rb, uint16_t len) +{ + rb->tail = (uint16_t)((rb->tail + len) & (uint16_t)(rb->size - 1U)); +} diff --git a/c/protocan-transport/tests/test_gui.c b/c/protocan-transport/tests/test_gui.c new file mode 100644 index 0000000..631bf08 --- /dev/null +++ b/c/protocan-transport/tests/test_gui.c @@ -0,0 +1,374 @@ +/** + * Тесты GUI-протокола и каталога общего адресного пространства. + * Эталонные байты совпадают с SETGUI/tests/test_gas_catalog.py. + * + * clang -std=c99 -Wall -Wextra -Iinclude tests/test_gui.c src/pcan_*.c src/gui_*.c -o test_gui + */ +#include +#include + +#include "gui_catalog.h" +#include "gui_frame.h" +#include "pcan_gas.h" + +static int g_fail; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \ + g_fail++; \ + } \ + } while (0) + +static void dump(const char *tag, const uint8_t *data, size_t len) +{ + printf("%s:", tag); + for (size_t i = 0; i < len; i++) { + printf(" %02X", data[i]); + } + printf("\n"); +} + +/* -- Транспорт ------------------------------------------------------------- */ + +static void test_crc32(void) +{ + /* Контрольное значение CRC32/ISO-HDLC, оно же zlib.crc32. */ + CHECK(gui_crc32((const uint8_t *)"123456789", 9) == 0xCBF43926UL); + CHECK(gui_crc32((const uint8_t *)"", 0) == 0U); +} + +static void test_ping_reference(void) +{ + /* Эталон из PROJECT_RELATIONS.md: + A5 5A 01 01 12 34 00 00 EE 89 8C 9E */ + static const uint8_t want[] = { + 0xA5, 0x5A, 0x01, 0x01, 0x12, 0x34, 0x00, 0x00, 0xEE, 0x89, 0x8C, 0x9E + }; + uint8_t out[GUI_FRAME_MAX]; + size_t n = gui_frame_encode(GUI_MSG_PING, 0x1234, NULL, 0, out, sizeof out); + if ((n != sizeof want) || (memcmp(out, want, sizeof want) != 0)) { + dump("получено", out, n); + dump("ожидалось", want, sizeof want); + } + CHECK(n == sizeof want); + CHECK(memcmp(out, want, sizeof want) == 0); +} + +static int s_rx_count; +static uint8_t s_rx_type; +static uint16_t s_rx_seq; +static uint16_t s_rx_size; +static uint8_t s_rx_payload[64]; + +static void on_frame(const gui_frame_t *frame, void *user) +{ + (void)user; + s_rx_count++; + s_rx_type = frame->type; + s_rx_seq = frame->sequence; + s_rx_size = frame->size; + if (frame->size <= sizeof s_rx_payload) { + memcpy(s_rx_payload, frame->payload, frame->size); + } +} + +static void test_parser(void) +{ + uint8_t payload[5] = { 1, 2, 3, 4, 5 }; + uint8_t out[GUI_FRAME_MAX]; + size_t n = gui_frame_encode(GUI_MSG_GAS_WATCH_DATA, 0, payload, 5, out, sizeof out); + + gui_parser_t p; + gui_parser_init(&p); + s_rx_count = 0; + /* Мусор и оборванная сигнатура перед настоящим кадром. */ + static const uint8_t junk[] = { 0x00, 0xFF, 0xA5, 0xA5, 0x5A, 0x02 }; + gui_parser_feed(&p, junk, sizeof junk, on_frame, NULL); + gui_parser_feed(&p, out, n, on_frame, NULL); + + CHECK(s_rx_count == 1); + CHECK(s_rx_type == GUI_MSG_GAS_WATCH_DATA); + CHECK(s_rx_size == 5U); + CHECK(memcmp(s_rx_payload, payload, 5) == 0); + CHECK(p.stats.version_errors == 1U); /* 0x02 вместо версии 0x01 */ + CHECK(p.stats.frames == 1U); + + /* Битый CRC отвергается. */ + out[n - 1U] ^= 0xFFU; + gui_parser_init(&p); + s_rx_count = 0; + gui_parser_feed(&p, out, n, on_frame, NULL); + CHECK(s_rx_count == 0); + CHECK(p.stats.crc_errors == 1U); +} + +static void test_max_payload(void) +{ + static uint8_t payload[GUI_MAX_PAYLOAD]; + for (size_t i = 0; i < sizeof payload; i++) { + payload[i] = (uint8_t)i; + } + static uint8_t out[GUI_FRAME_MAX]; + size_t n = gui_frame_encode(GUI_MSG_WRITE_REGISTERS, 0xFFFF, payload, + GUI_MAX_PAYLOAD, out, sizeof out); + CHECK(n == GUI_FRAME_MAX); + CHECK(out[6] == 0x02U && out[7] == 0x00U); /* size = 512 big-endian */ + + /* Больше максимума не кодируем и буфер не переполняем. */ + CHECK(gui_frame_encode(GUI_MSG_PING, 0, payload, GUI_MAX_PAYLOAD + 1U, + out, sizeof out) == 0U); + CHECK(gui_frame_encode(GUI_MSG_PING, 0, payload, 100U, out, 50U) == 0U); +} + +/* -- Каталог --------------------------------------------------------------- */ + +static uint16_t s_settings[4]; +static uint16_t s_diag[4] = { 0x0100, 0x003C, 0x0000, 0x0002 }; + +static const pcan_gas_region_t s_regions[] = { + { 0x0000U, 4U, s_settings, NULL, NULL, 0U, NULL, "settings" }, + { 0xFF00U, 4U, s_diag, NULL, NULL, PCAN_GAS_RDONLY, NULL, "diag" }, +}; +static const pcan_gas_map_t s_map = { s_regions, 2U }; + +static const gui_object_t s_objects[] = { + { 0xFF01U, GUI_OBJ_U16, GUI_OBJ_READABLE | GUI_OBJ_DEFAULT_WATCH, + 0, GUI_UNIT_SECOND, "Uptime" }, + { 0x0000U, GUI_OBJ_I16, GUI_OBJ_READABLE | GUI_OBJ_WRITABLE, + -2, GUI_UNIT_VOLT, "Uставка" }, + { 0xFF02U, GUI_OBJ_U32, GUI_OBJ_READABLE, 0, GUI_UNIT_COUNT, "Кадров" }, +}; +static const gui_catalog_t s_catalog = { s_objects, 3U }; + +static void test_catalog_validate(void) +{ + CHECK(gui_catalog_validate(&s_catalog, &s_map)); + + /* Адрес вне карты GAS. */ + static const gui_object_t bad_addr[] = { + { 0x8000U, GUI_OBJ_U16, GUI_OBJ_READABLE, 0, 0, "нет" }, + }; + const gui_catalog_t c1 = { bad_addr, 1U }; + CHECK(!gui_catalog_validate(&c1, &s_map)); + + /* U32 требует двух подряд идущих адресов; 0xFF03 есть, 0xFF04 - нет. */ + static const gui_object_t split[] = { + { 0xFF03U, GUI_OBJ_U32, GUI_OBJ_READABLE, 0, 0, "хвост" }, + }; + const gui_catalog_t c2 = { split, 1U }; + CHECK(!gui_catalog_validate(&c2, &s_map)); + + /* Имя длиннее поля: 13 кириллических символов - это 26 байт. */ + static const gui_object_t long_name[] = { + { 0x0000U, GUI_OBJ_U16, GUI_OBJ_READABLE, 0, 0, "тринадцать сим" }, + }; + const gui_catalog_t c3 = { long_name, 1U }; + CHECK(!gui_catalog_validate(&c3, &s_map)); +} + +static void test_catalog_encode(void) +{ + uint8_t out[GUI_MAX_PAYLOAD]; + size_t n = gui_catalog_encode(&s_catalog, 0U, 1U, out, sizeof out); + + CHECK(n == GUI_CATALOG_HEADER + GUI_ENTRY_SIZE); + CHECK(gui_get_u16(&out[0]) == 3U); /* total */ + CHECK(gui_get_u16(&out[2]) == 0U); /* start_index */ + CHECK(gui_get_u16(&out[4]) == 1U); /* count */ + + /* Запись Uptime совпадает с эталоном из Python-тестов. */ + static const uint8_t want[] = { + 0x01, 0xFF, 0x00, 0x05, 0x00, 0x07, 0x00, 0x00, + 'U', 'p', 't', 'i', 'm', 'e', 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + if (memcmp(&out[GUI_CATALOG_HEADER], want, sizeof want) != 0) { + dump("запись", &out[GUI_CATALOG_HEADER], GUI_ENTRY_SIZE); + } + CHECK(sizeof want == GUI_ENTRY_SIZE); + CHECK(memcmp(&out[GUI_CATALOG_HEADER], want, sizeof want) == 0); + + /* Отрицательный масштаб кодируется как int8. */ + n = gui_catalog_encode(&s_catalog, 1U, 1U, out, sizeof out); + CHECK(n == GUI_CATALOG_HEADER + GUI_ENTRY_SIZE); + CHECK(out[GUI_CATALOG_HEADER + 4U] == 0xFEU); /* -2 */ + + /* max_count = 0 отдаёт всё, что влезает. */ + n = gui_catalog_encode(&s_catalog, 0U, 0U, out, sizeof out); + CHECK(n == GUI_CATALOG_HEADER + (3U * GUI_ENTRY_SIZE)); + CHECK(gui_get_u16(&out[4]) == 3U); + + /* Индекс за концом каталога - пустой ответ, а не мусор. */ + CHECK(gui_catalog_encode(&s_catalog, 3U, 0U, out, sizeof out) == 0U); + /* Маленький буфер отвергается. */ + CHECK(gui_catalog_encode(&s_catalog, 0U, 1U, out, 8U) == 0U); + + CHECK(GUI_ENTRIES_PER_FRAME == 15U); +} + +/* -- Подписка -------------------------------------------------------------- */ + +static void test_watch(void) +{ + gui_watch_t watch; + gui_watch_init(&watch); + + /* period = 100, два адреса: один есть в карте, второй нет. */ + static const uint8_t request[] = { + 0x64, 0x00, 0x02, 0x00, 0x01, 0xFF, 0x00, 0x80 + }; + uint16_t accepted = gui_watch_apply(&watch, &s_map, request, sizeof request); + CHECK(accepted == 1U); + CHECK(watch.count == 1U); + CHECK(watch.address[0] == 0xFF01U); + CHECK(watch.period_ms == 100U); + + uint8_t ack[4]; + CHECK(gui_watch_encode_ack(&watch, ack, sizeof ack) == 4U); + CHECK(gui_get_u16(&ack[0]) == 100U); + CHECK(gui_get_u16(&ack[2]) == 1U); + + /* Значение берётся из карты GAS. */ + uint8_t data[GUI_MAX_PAYLOAD]; + size_t n = gui_watch_encode_data(&watch, &s_map, 0x12345678UL, data, sizeof data); + CHECK(n == 8U); + CHECK(data[0] == 0x78U && data[3] == 0x12U); /* время little-endian */ + CHECK(gui_get_u16(&data[4]) == 1U); + CHECK(gui_get_u16(&data[6]) == 0x003CU); /* s_diag[1] = uptime */ + + /* Битый payload не должен ломать действующую подписку. */ + static const uint8_t broken[] = { 0x64, 0x00, 0x05, 0x00, 0x01, 0xFF }; + CHECK(gui_watch_apply(&watch, &s_map, broken, sizeof broken) == 0xFFFFU); + CHECK(watch.count == 1U); + CHECK(watch.period_ms == 100U); + + /* Слишком длинная подписка отвергается целиком. */ + static uint8_t huge[4U + ((GUI_WATCH_MAX + 1U) * 2U)]; + gui_put_u16(&huge[0], 10U); + gui_put_u16(&huge[2], GUI_WATCH_MAX + 1U); + CHECK(gui_watch_apply(&watch, &s_map, huge, sizeof huge) == 0xFFFFU); + CHECK(watch.count == 1U); + + /* period = 0 останавливает поток. */ + static const uint8_t stop[] = { 0x00, 0x00, 0x00, 0x00 }; + CHECK(gui_watch_apply(&watch, &s_map, stop, sizeof stop) == 0U); + CHECK(watch.period_ms == 0U); + CHECK(!gui_watch_due(&watch, 1000U)); + CHECK(gui_watch_encode_data(&watch, &s_map, 0U, data, sizeof data) == 0U); +} + +static void test_watch_timing(void) +{ + gui_watch_t watch; + gui_watch_init(&watch); + static const uint8_t request[] = { 0x0A, 0x00, 0x01, 0x00, 0x01, 0xFF }; + CHECK(gui_watch_apply(&watch, &s_map, request, sizeof request) == 1U); + + watch.next_ms = 1000U; + CHECK(!gui_watch_due(&watch, 999U)); + CHECK(gui_watch_due(&watch, 1000U)); + CHECK(gui_watch_due(&watch, 1500U)); + + /* Идём в такт, пока успеваем. */ + gui_watch_advance(&watch, 1000U); + CHECK(watch.next_ms == 1010U); + + /* Отстали сильно - не нагоняем пачкой, а идём от текущего момента. */ + watch.next_ms = 1000U; + gui_watch_advance(&watch, 5000U); + CHECK(watch.next_ms == 5010U); + + /* Переполнение счётчика миллисекунд не должно вешать поток. */ + watch.next_ms = 0xFFFFFFF0UL; + CHECK(!gui_watch_due(&watch, 0xFFFFFFE0UL)); + CHECK(gui_watch_due(&watch, 0x00000010UL)); +} + +/* -- Канал ----------------------------------------------------------------- */ + +static uint8_t s_line[1024]; +static size_t s_line_len; +static size_t s_line_space = sizeof s_line; + +static size_t fake_write(void *ctx, const uint8_t *data, size_t len) +{ + (void)ctx; + if ((s_line_len + len) > s_line_space) { + return 0U; + } + memcpy(&s_line[s_line_len], data, len); + s_line_len += len; + return len; +} + +static size_t fake_space(void *ctx) +{ + (void)ctx; + return s_line_space - s_line_len; +} + +static void test_link(void) +{ + pcan_io_t io = { fake_write, fake_space, NULL }; + static gui_link_t link; + CHECK(gui_link_init(&link, &io, on_frame, NULL)); + + s_line_len = 0U; + s_rx_count = 0; + + uint8_t payload[3] = { 0xDE, 0xAD, 0xBE }; + CHECK(gui_link_send(&link, GUI_MSG_GAS_CATALOG, 7U, payload, 3U)); + CHECK(link.tx_frames == 1U); + + /* Поток обратно кусками произвольной длины. */ + size_t off = 0U; + while (off < s_line_len) { + size_t chunk = ((s_line_len - off) > 4U) ? 4U : (s_line_len - off); + gui_link_feed(&link, &s_line[off], chunk); + off += chunk; + } + CHECK(s_rx_count == 1); + CHECK(s_rx_type == GUI_MSG_GAS_CATALOG); + CHECK(s_rx_seq == 7U); + CHECK(s_rx_size == 3U); + + /* NACK - два байта кода результата. */ + s_line_len = 0U; + s_rx_count = 0; + CHECK(gui_link_nack(&link, 9U, GUI_RESULT_NOT_FOUND)); + gui_link_feed(&link, s_line, s_line_len); + CHECK(s_rx_count == 1); + CHECK(s_rx_type == GUI_MSG_NACK); + CHECK(gui_get_u16(s_rx_payload) == GUI_RESULT_NOT_FOUND); + + /* Не хватило места - кадр не уходит обрывком. */ + s_line_space = 8U; + s_line_len = 0U; + CHECK(!gui_link_can_send(&link, 100U)); + CHECK(!gui_link_send(&link, GUI_MSG_PING, 1U, payload, 3U)); + CHECK(s_line_len == 0U); + CHECK(link.tx_dropped == 1U); + s_line_space = sizeof s_line; +} + +int main(void) +{ + test_crc32(); + test_ping_reference(); + test_parser(); + test_max_payload(); + test_catalog_validate(); + test_catalog_encode(); + test_watch(); + test_watch_timing(); + test_link(); + + if (g_fail == 0) { + printf("all gui tests passed\n"); + return 0; + } + printf("%d check(s) failed\n", g_fail); + return 1; +} diff --git a/c/protocan-transport/tests/test_transport.c b/c/protocan-transport/tests/test_transport.c new file mode 100644 index 0000000..cc0f9bb --- /dev/null +++ b/c/protocan-transport/tests/test_transport.c @@ -0,0 +1,445 @@ +/** + * Тесты библиотеки. Платформенных зависимостей нет, собирается любым C99: + * + * clang -std=c99 -Wall -Wextra -Iinclude tests/test_transport.c src/pcan_*.c -o test + */ +#include +#include + +#include "protocan_transport.h" + +static int g_fail; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \ + g_fail++; \ + } \ + } while (0) + +/* -- CRC ------------------------------------------------------------------- */ + +static void test_crc(void) +{ + /* Контрольное значение CRC-16/CCITT-FALSE. */ + CHECK(pcan_crc16((const uint8_t *)"123456789", 9) == 0x29B1U); +} + +/* -- Кадр ------------------------------------------------------------------ */ + +static void test_encode(void) +{ + pcan_frame_t f; + memset(&f, 0, sizeof f); + f.seq = 1U; + f.flags = PCAN_FLAG_IDE; + f.id = 0x1234567UL; + f.dlc = 2U; + f.data[0] = 0xAAU; + f.data[1] = 0xBBU; + + uint8_t raw[PCAN_FRAME_MAX]; + size_t n = pcan_frame_encode(&f, raw, sizeof raw); + + /* Эталон согласован с gui/transport.py. */ + static const uint8_t want[] = { + 0xAA, 0x55, 0x08, 0x01, 0x01, 0x67, 0x45, 0x23, 0x01, 0xAA, 0xBB, + 0xFE, 0x14 + }; + CHECK(n == sizeof want); + CHECK(memcmp(raw, want, sizeof want) == 0); + + /* Маленький буфер должен отвергаться, а не портить память. */ + CHECK(pcan_frame_encode(&f, raw, 4U) == 0U); +} + +static void test_encode_max(void) +{ + pcan_frame_t f; + memset(&f, 0, sizeof f); + f.seq = 255U; + f.flags = PCAN_FLAG_IDE | PCAN_FLAG_DIR; + f.id = 0x1FFFFFFFUL; + f.dlc = 8U; + for (int i = 0; i < 8; i++) { + f.data[i] = (uint8_t)(0xF0 + i); + } + + uint8_t raw[PCAN_FRAME_MAX]; + size_t n = pcan_frame_encode(&f, raw, sizeof raw); + + static const uint8_t want[] = { + 0xAA, 0x55, 0x0E, 0xFF, 0x05, 0xFF, 0xFF, 0xFF, 0x1F, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0x6D, 0x0B + }; + CHECK(n == sizeof want); + CHECK(n == PCAN_FRAME_MAX); + CHECK(memcmp(raw, want, sizeof want) == 0); +} + +static void test_roundtrip(void) +{ + pcan_frame_t f; + memset(&f, 0, sizeof f); + f.flags = PCAN_FLAG_IDE | PCAN_FLAG_DIR; + f.id = 0x0ABCDEFUL; + f.dlc = 5U; + memcpy(f.data, "HELLO", 5); + + uint8_t raw[PCAN_FRAME_MAX]; + size_t n = pcan_frame_encode(&f, raw, sizeof raw); + + pcan_parser_t p; + pcan_parser_init(&p); + pcan_frame_t out; + memset(&out, 0, sizeof out); + int got = 0; + for (size_t i = 0; i < n; i++) { + if (pcan_parser_push(&p, raw[i], &out)) { + got++; + } + } + CHECK(got == 1); + CHECK(out.id == f.id); + CHECK(out.dlc == f.dlc); + CHECK(out.flags == f.flags); + CHECK(memcmp(out.data, "HELLO", 5) == 0); + /* Хвост за DLC должен быть обнулён, а не содержать прошлый кадр. */ + CHECK(out.data[5] == 0U && out.data[6] == 0U && out.data[7] == 0U); + CHECK(p.stats.frames == 1U); +} + +static void test_resync(void) +{ + pcan_frame_t f; + memset(&f, 0, sizeof f); + f.flags = PCAN_FLAG_IDE; + f.id = 0x100UL; + f.dlc = 1U; + f.data[0] = 0x42U; + + uint8_t raw[PCAN_FRAME_MAX]; + size_t n = pcan_frame_encode(&f, raw, sizeof raw); + + /* Мусор, оборванная сигнатура и битый LEN перед настоящим кадром. */ + static const uint8_t junk[] = { 0x00, 0xFF, 0xAA, 0xAA, 0x55, 0x03, 0x11, 0xAA }; + + pcan_parser_t p; + pcan_parser_init(&p); + pcan_frame_t out; + int got = 0; + for (size_t i = 0; i < sizeof junk; i++) { + if (pcan_parser_push(&p, junk[i], &out)) { + got++; + } + } + for (size_t i = 0; i < n; i++) { + if (pcan_parser_push(&p, raw[i], &out)) { + got++; + } + } + CHECK(got == 1); + CHECK(out.id == 0x100UL); + CHECK(out.data[0] == 0x42U); + CHECK(p.stats.bad_len == 1U); +} + +static void test_bad_crc(void) +{ + pcan_frame_t f; + memset(&f, 0, sizeof f); + f.flags = PCAN_FLAG_IDE; + f.id = 0x55UL; + f.dlc = 3U; + + uint8_t raw[PCAN_FRAME_MAX]; + size_t n = pcan_frame_encode(&f, raw, sizeof raw); + raw[n - 1U] ^= 0xFFU; + + pcan_parser_t p; + pcan_parser_init(&p); + pcan_frame_t out; + int got = 0; + for (size_t i = 0; i < n; i++) { + if (pcan_parser_push(&p, raw[i], &out)) { + got++; + } + } + CHECK(got == 0); + CHECK(p.stats.crc_errors == 1U); +} + +/* -- Идентификатор --------------------------------------------------------- */ + +static void test_id(void) +{ + pcan_id_t id = { 0x1234U, PCAN_MSG_GAS, 2U, 1U, + PCAN_ROUTE_FROM_DEVICE, PCAN_PRIORITY_STANDARD }; + uint32_t raw = pcan_id_pack(&id); + CHECK(raw == 0x19231234UL); + + pcan_id_t back; + pcan_id_unpack(raw, &back); + CHECK(back.msg_body == id.msg_body); + CHECK(back.msg_type == id.msg_type); + CHECK(back.device_id == id.device_id); + CHECK(back.device_type == id.device_type); + CHECK(back.route == id.route); + CHECK(back.priority == id.priority); + + /* Раскладки MsgBody из protocan.h */ + CHECK(pcan_body_broadcast(3U, 0U) == 0x0030U); + CHECK(pcan_body_typed(4U, 17U) == 0x4011U); + CHECK(pcan_body_modbus(0x100U, 3U) == 0x1003U); + CHECK(pcan_body_error(0U, 0xFFU) == 0x00FFU); +} + +/* -- Кольцевой буфер ------------------------------------------------------- */ + +static void test_ring(void) +{ + uint8_t storage[16]; + pcan_ring_t rb; + + CHECK(pcan_ring_init(&rb, storage, 16U)); + /* Размер не степень двойки - отказ, а не тихая порча индексов. */ + pcan_ring_t bad; + CHECK(!pcan_ring_init(&bad, storage, 15U)); + + CHECK(pcan_ring_free(&rb) == 15U); + CHECK(pcan_ring_write(&rb, (const uint8_t *)"0123456789", 10U)); + CHECK(pcan_ring_count(&rb) == 10U); + /* Не влезает - не должно попасть ни одного байта. */ + CHECK(!pcan_ring_write(&rb, (const uint8_t *)"ABCDEF", 6U)); + CHECK(pcan_ring_count(&rb) == 10U); + CHECK(rb.dropped == 6U); + + /* Линейный участок для DMA. */ + const uint8_t *ptr = NULL; + uint16_t lin = pcan_ring_linear(&rb, &ptr); + CHECK(lin == 10U); + CHECK(ptr == storage); + pcan_ring_consume(&rb, lin); + CHECK(pcan_ring_empty(&rb)); + + /* Заворот: линейный участок обязан обрываться на конце буфера. */ + CHECK(pcan_ring_write(&rb, (const uint8_t *)"ABCDEFGH", 8U)); + lin = pcan_ring_linear(&rb, &ptr); + CHECK(lin == 6U); /* от tail=10 до конца буфера (16) */ + CHECK(ptr == &storage[10]); + pcan_ring_consume(&rb, lin); + lin = pcan_ring_linear(&rb, &ptr); + CHECK(lin == 2U); + CHECK(ptr == storage); +} + +/* -- Общее адресное пространство ------------------------------------------- */ + +static uint16_t s_holding[8]; +static uint16_t s_computed_calls; + +static pcan_gas_status_t computed_read(const pcan_gas_region_t *r, + uint16_t offset, uint16_t *value) +{ + (void)r; + s_computed_calls++; + *value = (uint16_t)(0xA000U + offset); + return PCAN_GAS_OK; +} + +static const pcan_gas_region_t s_regions[] = { + { 0x0000U, 8U, s_holding, NULL, NULL, 0U, NULL, "holding" }, + { 0xFF00U, 4U, NULL, computed_read, NULL, PCAN_GAS_RDONLY, NULL, "diag" }, +}; + +static const pcan_gas_map_t s_map = { s_regions, 2U }; + +static void test_gas(void) +{ + CHECK(pcan_gas_map_validate(&s_map)); + + /* Перекрытие регионов должно отвергаться. */ + static const pcan_gas_region_t overlap[] = { + { 0x0000U, 8U, s_holding, NULL, NULL, 0U, NULL, NULL }, + { 0x0004U, 8U, s_holding, NULL, NULL, 0U, NULL, NULL }, + }; + const pcan_gas_map_t bad = { overlap, 2U }; + CHECK(!pcan_gas_map_validate(&bad)); + + uint16_t v = 0U; + CHECK(pcan_gas_write(&s_map, 0x0002U, 0x1234U) == PCAN_GAS_OK); + CHECK(pcan_gas_read(&s_map, 0x0002U, &v) == PCAN_GAS_OK); + CHECK(v == 0x1234U); + CHECK(s_holding[2] == 0x1234U); + + /* Дырка в карте. */ + CHECK(pcan_gas_read(&s_map, 0x0100U, &v) == PCAN_GAS_NO_REG); + /* Регион только для чтения. */ + CHECK(pcan_gas_write(&s_map, 0xFF00U, 1U) == PCAN_GAS_READ_ONLY); + /* Колбэк вместо памяти. */ + CHECK(pcan_gas_read(&s_map, 0xFF02U, &v) == PCAN_GAS_OK); + CHECK(v == 0xA002U); + CHECK(s_computed_calls == 1U); + + /* Блок обрывается на первом адресе вне карты. */ + uint16_t block[8]; + CHECK(pcan_gas_read_block(&s_map, 0x0006U, block, 8U) == 2U); +} + +static void test_gas_frames(void) +{ + for (int i = 0; i < 8; i++) { + s_holding[i] = (uint16_t)(0x1000 + i); + } + + /* Запрос на чтение: GAS, DLC = 0, адрес в MsgBody. */ + pcan_id_t id = { 0x0002U, PCAN_MSG_GAS, 2U, 1U, + PCAN_ROUTE_FROM_PM, PCAN_PRIORITY_STANDARD }; + pcan_frame_t req; + memset(&req, 0, sizeof req); + req.id = pcan_id_pack(&id); + req.flags = PCAN_FLAG_IDE; + req.dlc = 0U; + + pcan_frame_t rsp; + memset(&rsp, 0, sizeof rsp); + CHECK(pcan_gas_handle(&s_map, &req, &rsp)); + CHECK(rsp.dlc == 8U); /* 4 регистра */ + + pcan_id_t rid; + pcan_id_unpack(rsp.id, &rid); + CHECK(rid.msg_type == PCAN_MSG_GAS); + CHECK(rid.msg_body == 0x0002U); + CHECK(rid.route == PCAN_ROUTE_FROM_DEVICE); + + /* Регистры лежат младшим байтом вперёд - как в SETCAN. */ + CHECK(rsp.data[0] == 0x02U && rsp.data[1] == 0x10U); + uint16_t regs[4]; + CHECK(pcan_gas_frame_to_regs(&rsp, regs, 4U) == 4U); + CHECK(regs[0] == 0x1002U && regs[3] == 0x1005U); + + /* Запись: GAS с данными, ответа быть не должно. */ + pcan_frame_t wr; + memset(&wr, 0, sizeof wr); + id.msg_body = 0x0000U; + wr.id = pcan_id_pack(&id); + wr.flags = PCAN_FLAG_IDE; + wr.dlc = 4U; + wr.data[0] = 0xEFU; wr.data[1] = 0xBEU; + wr.data[2] = 0xADU; wr.data[3] = 0xDEU; + CHECK(!pcan_gas_handle(&s_map, &wr, &rsp)); + CHECK(s_holding[0] == 0xBEEFU); + CHECK(s_holding[1] == 0xDEADU); + + /* Чтение по адресу вне карты не должно порождать ответ. */ + id.msg_body = 0x0500U; + req.id = pcan_id_pack(&id); + CHECK(!pcan_gas_handle(&s_map, &req, &rsp)); +} + +/* -- Канал связи ----------------------------------------------------------- */ + +static uint8_t s_line[256]; +static size_t s_line_len; +static size_t s_line_space = sizeof s_line; + +static size_t fake_write(void *ctx, const uint8_t *data, size_t len) +{ + (void)ctx; + if ((s_line_len + len) > s_line_space) { + return 0U; /* всё-или-ничего */ + } + memcpy(&s_line[s_line_len], data, len); + s_line_len += len; + return len; +} + +static size_t fake_space(void *ctx) +{ + (void)ctx; + return s_line_space - s_line_len; +} + +static int s_rx_count; +static pcan_frame_t s_rx_last; + +static void on_frame(const pcan_frame_t *f, void *user) +{ + (void)user; + s_rx_count++; + s_rx_last = *f; +} + +static void test_link(void) +{ + pcan_io_t io = { fake_write, fake_space, NULL }; + pcan_link_t link; + CHECK(pcan_link_init(&link, &io, on_frame, NULL)); + + s_line_len = 0U; + s_rx_count = 0; + + /* SEQ должен инкрементироваться сам. */ + for (int i = 0; i < 3; i++) { + pcan_frame_t f; + memset(&f, 0, sizeof f); + f.flags = PCAN_FLAG_IDE; + f.id = (uint32_t)(0x200 + i); + f.dlc = 1U; + f.data[0] = (uint8_t)i; + CHECK(pcan_link_send(&link, &f)); + CHECK(f.seq == (uint8_t)i); + } + CHECK(link.tx.tx_frames == 3U); + + /* Тот же поток скармливаем обратно, по кускам произвольной длины. */ + size_t off = 0U; + while (off < s_line_len) { + size_t chunk = ((s_line_len - off) > 5U) ? 5U : (s_line_len - off); + pcan_link_feed(&link, &s_line[off], chunk); + off += chunk; + } + CHECK(s_rx_count == 3); + CHECK(s_rx_last.id == 0x202UL); + CHECK(s_rx_last.seq == 2U); + CHECK(pcan_link_rx_stats(&link)->frames == 3U); + + /* Переполнение очереди: кадр не должен уйти обрывком. */ + s_line_space = 4U; + s_line_len = 0U; + pcan_frame_t big; + memset(&big, 0, sizeof big); + big.flags = PCAN_FLAG_IDE; + big.dlc = 8U; + uint8_t seq_before = link.tx_seq; + CHECK(!pcan_link_send(&link, &big)); + CHECK(s_line_len == 0U); + CHECK(link.tx.tx_dropped == 1U); + /* SEQ не двигается на неотправленном кадре. */ + CHECK(link.tx_seq == seq_before); + CHECK(!pcan_link_can_send(&link)); + s_line_space = sizeof s_line; +} + +int main(void) +{ + test_crc(); + test_encode(); + test_encode_max(); + test_roundtrip(); + test_resync(); + test_bad_crc(); + test_id(); + test_ring(); + test_gas(); + test_gas_frames(); + test_link(); + + if (g_fail == 0) { + printf("all tests passed\n"); + return 0; + } + printf("%d check(s) failed\n", g_fail); + return 1; +}