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:
2026-08-23 01:15:35 +03:00
parent 873ac438f3
commit 3dc636e012
27 changed files with 3646 additions and 0 deletions

View File

@@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#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 */

View File

@@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#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 */

View File

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

View File

@@ -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 <stddef.h>
#include <stdint.h>
#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 */

View File

@@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#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 */

View File

@@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#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 */

View File

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

View File

@@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#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 */

View File

@@ -0,0 +1,71 @@
/**
* @file pcan_ring.h
* @brief Кольцевой буфер байтов: один писатель, один читатель.
*
* Размер обязан быть степенью двойки - индексы сворачиваются маской,
* поэтому head/tail обновляются одной записью и блокировки между
* фоновым кодом и обработчиком прерывания не нужны.
*
* Отдельно есть pcan_ring_linear(): отдаёт непрерывный участок данных
* без заворота - именно то, что требуется, чтобы отдать кусок в DMA.
*/
#ifndef PCAN_RING_H
#define PCAN_RING_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#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 */

View File

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