feat(protocan-transport): транспортный уровень ProtoCAN и каталог GUI
Перенесён из репозитория protocan-transport, который подключался сабмодулем в CAN_to_RS485. Кадрирование AA 55 с CRC16 поверх любого байтового потока (RS485, RS232, USB CDC), разбор 29-битного идентификатора, общее адресное пространство регистров и каталог с подпиской на поток значений для SETGUI. Состояние живёт в структурах вызывающего, поэтому в одной прошивке поднимается сколько угодно независимых каналов. Порт STM32F4 (USART + DMA) в комплекте. Хостовые тесты test_transport и test_gui проходят.
This commit is contained in:
78
c/protocan-transport/docs/FRAME.md
Normal file
78
c/protocan-transport/docs/FRAME.md
Normal file
@@ -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`.
|
||||
87
c/protocan-transport/docs/GAS.md
Normal file
87
c/protocan-transport/docs/GAS.md
Normal file
@@ -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()`
|
||||
возвращает число фактически записанных регистров.
|
||||
- Атомарности между регистрами нет. Если два регистра обязаны меняться
|
||||
вместе, заведите колбэк, который применяет их по записи второго.
|
||||
158
c/protocan-transport/docs/GUI_CATALOG.md
Normal file
158
c/protocan-transport/docs/GUI_CATALOG.md
Normal file
@@ -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` — прибор не тратит на это такты и не
|
||||
теряет точность на промежуточном округлении.
|
||||
Reference in New Issue
Block a user