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:
155
c/protocan-transport/include/gui_catalog.h
Normal file
155
c/protocan-transport/include/gui_catalog.h
Normal 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 */
|
||||
185
c/protocan-transport/include/gui_frame.h
Normal file
185
c/protocan-transport/include/gui_frame.h
Normal 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 */
|
||||
45
c/protocan-transport/include/pcan_config.h
Normal file
45
c/protocan-transport/include/pcan_config.h
Normal 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 */
|
||||
28
c/protocan-transport/include/pcan_crc.h
Normal file
28
c/protocan-transport/include/pcan_crc.h
Normal 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 */
|
||||
113
c/protocan-transport/include/pcan_frame.h
Normal file
113
c/protocan-transport/include/pcan_frame.h
Normal 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 */
|
||||
129
c/protocan-transport/include/pcan_gas.h
Normal file
129
c/protocan-transport/include/pcan_gas.h
Normal 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 */
|
||||
95
c/protocan-transport/include/pcan_id.h
Normal file
95
c/protocan-transport/include/pcan_id.h
Normal 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 */
|
||||
97
c/protocan-transport/include/pcan_link.h
Normal file
97
c/protocan-transport/include/pcan_link.h
Normal 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 */
|
||||
71
c/protocan-transport/include/pcan_ring.h
Normal file
71
c/protocan-transport/include/pcan_ring.h
Normal 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 */
|
||||
27
c/protocan-transport/include/protocan_transport.h
Normal file
27
c/protocan-transport/include/protocan_transport.h
Normal 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 */
|
||||
Reference in New Issue
Block a user