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