diff --git a/Inc/protocan.h b/Inc/protocan.h index 43ac9bf..8cd360d 100644 --- a/Inc/protocan.h +++ b/Inc/protocan.h @@ -11,8 +11,8 @@ #define OFFLINE 0; #define ID_MAIN_DEVICE 0x000 -#define CURRENT_TYPE_DEVICE 0b001 -#define CURRENT_ID_DEVICE 0b0010 +#define CURRENT_TYPE_DEVICE 0b111 +#define CURRENT_ID_DEVICE 0b1111 #define PROTOCAN_RX_BUFFER_SIZE 128 @@ -81,9 +81,49 @@ typedef enum { PROTOCAN_MSGTYPE_MODBUS_HOLDING = 0b0110, /**< Сообщение Modbus - Holding. */ PROTOCAN_MSGTYPE_MODBUS_INPUT = 0b0111, /**< Сообщение Modbus - Input. */ PROTOCAN_MSGTYPE_ERROR = 0b1000, /**< Сообщение об ошибке. */ + PROTOCAN_MSGTYPE_SETTINGS = 0b1110, /**< Привязка датчика к локации. */ PROTOCAN_MSGTYPE_PULSE = 0b1111, /**< Импульсное сообщение. */ }ProtoCanMsgType; +/** Длина ROM-кода 1-Wire и payload успешного ответа SETTINGS. */ +#define PROTOCAN_SETTINGS_ROM_SIZE 8U + +/** Фиксированные базы выбранного DeviceType=0x7, DeviceID=0xF, MsgType=0xE. */ +#define PROTOCAN_SETTINGS_REQUEST_BASE_ID 0x17FE0000UL +#define PROTOCAN_SETTINGS_RESPONSE_BASE_ID 0x1FFE0000UL +#define PROTOCAN_SETTINGS_HEADER_MASK 0x1FFF0000UL + +/** Операции SETTINGS, определяемые по направлению, DLC и payload. */ +typedef enum { + PROTOCAN_SETTINGS_GET = 0, + PROTOCAN_SETTINGS_WRITE = 1, + PROTOCAN_SETTINGS_CLEAR = 2, + PROTOCAN_SETTINGS_RESPONSE = 3, + PROTOCAN_SETTINGS_ERROR = 4, + PROTOCAN_SETTINGS_INVALID = 5 +} ProtoCanSettingsOperationType; + +/** Коды результата SETTINGS в ответе с DLC=1. */ +typedef enum { + PROTOCAN_SETTINGS_RESULT_OK = 0x00U, + PROTOCAN_SETTINGS_RESULT_INVALID_DLC = 0x01U, + PROTOCAN_SETTINGS_RESULT_INVALID_ROM_CRC = 0x02U, + PROTOCAN_SETTINGS_RESULT_NOT_FOUND = 0x03U, + PROTOCAN_SETTINGS_RESULT_ALREADY_ASSIGNED = 0x04U, + PROTOCAN_SETTINGS_RESULT_EEPROM_ERROR = 0x05U, + PROTOCAN_SETTINGS_RESULT_INVALID_LOCATION = 0x06U, + PROTOCAN_SETTINGS_RESULT_BUSY = 0x07U +} ProtoCanSettingsResultType; + +/** Разобранная команда SETTINGS с локацией и ROM. */ +typedef struct { + ProtoCanSettingsOperationType Operation; + uint8_t AssemblySerial; /**< Body[15:8], номер сборки Z. */ + uint8_t Position; /**< Body[7:0], позиция Y. */ + ProtoCanSettingsResultType Result; + uint8_t Rom[PROTOCAN_SETTINGS_ROM_SIZE]; +} ProtoCanSettingsMsg_t; + /** * @brief Типы широковещательных сообщений CAN. * @@ -240,6 +280,10 @@ typedef struct{ uint16_t Info; uint16_t Code; } ErrorData; + struct ProtoCanSettingsData{ + const uint8_t *Data; + uint8_t DataCount; + } SettingsData; } ProtoCanData_t; /** @@ -307,6 +351,29 @@ PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogUSens(struct RXMsg _rxMsg); PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogISens(struct RXMsg _rxMsg); PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogTSens(struct RXMsg _rxMsg); +/** Разбирает запрос SETTINGS и вызывает прикладной обработчик. */ +PROTOCAN_StatusTypeDef PROTOCAN_SettingsProcessing(struct RXMsg _rxMsg); + +/** + * Прикладной обработчик GET/WRITE/CLEAR. Приложение должно переопределить + * слабую реализацию и после проверки датчика/EEPROM отправить ответ или ошибку. + */ +PROTOCAN_StatusTypeDef ProtoCanMsgToSettings(const ProtoCanSettingsMsg_t *message); + +/** Отправляет произвольный SETTINGS-кадр с уже заполненным Body. */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS(ProtoCanId_t id, + const struct ProtoCanSettingsData data); + +/** Отправляет успешный ответ 0x1FFEZZYY, DLC=8, с текущим ROM локации. */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS_RESPONSE(uint8_t assemblySerial, + uint8_t position, + const uint8_t *rom); + +/** Отправляет ошибку 0x1FFEZZYY, DLC=1, Data[0]=result. */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS_ERROR(uint8_t assemblySerial, + uint8_t position, + ProtoCanSettingsResultType result); + PROTOCAN_StatusTypeDef ProtoCanMsgToGeneralAddressSpace(struct RXMsg _rxMsg); PROTOCAN_StatusTypeDef PROTOCAN_RequestError(struct RXMsg _rxMsg); diff --git a/README.md b/README.md new file mode 100644 index 0000000..96b179e --- /dev/null +++ b/README.md @@ -0,0 +1,372 @@ +# SETCAN + +`SETCAN` — небольшой модуль прикладного протокола поверх классического CAN для микроконтроллеров STM32 и библиотеки STM32 HAL. Модуль принимает только кадры с расширенным 29-битным идентификатором, складывает их в кольцевой буфер и передаёт обработчикам по типу сообщения. Также он умеет отправлять широковещательные, дискретные, аналоговые, адресные, Modbus-подобные сообщения, команды привязки датчиков `SETTINGS`, ошибки и периодический «пульс» устройства. + +Это не самостоятельный проект STM32CubeIDE: здесь нет `.ioc`, startup-файлов, linker script или настроек тактирования. Каталоги `Inc` и `Src` нужно добавить в существующую прошивку STM32. + +## Состав + +```text +SETCAN/ +├── Inc/ +│ └── protocan.h # типы протокола, настройки устройства и публичный API +└── Src/ + └── protocan.c # приём, буферизация, разбор и отправка CAN-кадров +``` + +Модуль зависит от файлов `main.h` и `can.h`, сгенерированных STM32CubeMX, а также от HAL-драйверов CAN, RTC и TIM. + +## Формат расширенного CAN ID + +Протокол размещает служебные поля в 29-битном Extended ID: + +| Биты | Поле | Размер | Назначение | +|---:|---|---:|---| +| 28 | `Priority` | 1 | `0` — критический, `1` — стандартный приоритет | +| 27 | `Route` | 1 | `0` — от управляющего модуля, `1` — от устройства | +| 26..24 | `DeviceType` | 3 | тип устройства, `0..7` | +| 23..20 | `DeviceID` | 4 | номер устройства, `0..15` | +| 19..16 | `MsgType` | 4 | тип сообщения | +| 15..0 | `MsgBody` | 16 | тип команды, адрес, ID датчика или код ошибки | + +Типы сообщений: + +| Значение | Тип | +|---:|---| +| `0x0` | широковещательное (`BROADCAST`) | +| `0x1` | дискретное (`DISCRETE`) | +| `0x2` | аналоговое (`ANALOG`) | +| `0x3` | общее адресное пространство | +| `0x4` | Modbus Coil | +| `0x5` | Modbus Discrete | +| `0x6` | Modbus Holding | +| `0x7` | Modbus Input | +| `0x8` | ошибка | +| `0xE` | настройка привязки датчика (`SETTINGS`) | +| `0xF` | пульс присутствия устройства | + +Разметка `MsgBody` зависит от типа сообщения: + +- broadcast: младшие 4 бита — дополнительное поле, старшие 12 бит — команда; +- discrete: младшие 12 бит — данные/адрес, старшие 4 бита — подтип; +- analog: младшие 12 бит — ID датчика, старшие 4 бита — тип величины; +- Modbus: младшие 4 бита — количество регистров, старшие 12 бит — начальный адрес; +- SETTINGS: старший байт — номер сборки `Z`, младший байт — позиция `Y`; +- error: младший байт — код ошибки, старший байт — дополнительная информация. + +Для регистров `uint16_t` в CAN payload используется порядок байтов little-endian: сначала младший байт, затем старший. + +> Разметка ID реализована C-битовыми полями. Она соответствует используемому STM32 GCC ABI, но не является переносимой между произвольными компиляторами без проверки фактического расположения битов. + +## Подключение к STM32-проекту + +### 1. Добавить файлы + +Скопировать или подключить к сборке: + +- `Inc/protocan.h`; +- `Src/protocan.c`. + +Каталог `Inc` добавить в include paths компилятора. + +### 2. Настроить устройство + +В `Inc/protocan.h` для прибора привязки DS18B20 заданы выбранные значения: + +```c +#define CURRENT_TYPE_DEVICE 0b111 /* DeviceType = 0x7 */ +#define CURRENT_ID_DEVICE 0b1111 /* DeviceID = 0xF */ +``` + +При необходимости изменить размер программного RX-буфера: + +```c +#define PROTOCAN_RX_BUFFER_SIZE 128 +``` + +Полезная ёмкость кольцевого буфера на один элемент меньше заданного размера, то есть при значении `128` в нём помещается 127 ожидающих кадров. + +### 3. Настроить CubeMX + +В проекте должны быть инициализированы: + +- CAN с поддержкой Extended ID; +- RTC; +- базовый таймер, задающий период отправки пульса. + +Модуль использует CAN FIFO0 и фильтры с номерами `0`, `1`, `2`. Адресованный фильтр принимает Route=0 (запрос от ПМ) для текущих `DeviceType/DeviceID`; отдельные фильтры принимают broadcast и pulse. В `PROTOCAN_CONFIG_FILTER()` также жёстко задано `SlaveStartFilterBank = 14`. Если приложение уже использует фильтры, номера и границу банков нужно согласовать. + +### 4. Инициализировать и запустить периферию + +Пример для `main.c`: + +```c +#include "protocan.h" + +int main(void) +{ + HAL_Init(); + SystemClock_Config(); + + MX_GPIO_Init(); + MX_CAN_Init(); + MX_RTC_Init(); + MX_TIM2_Init(); + + PROTOCAN_INIT_StatusTypeDef init_status = + PROTOCAN_INIT(&hcan, &hrtc, &htim2); + + if (init_status != PROTOCAN_INIT_OK) { + Error_Handler(); + } + + /* PROTOCAN_INIT() этого не делает. */ + if (HAL_CAN_Start(&hcan) != HAL_OK) { + Error_Handler(); + } + if (HAL_CAN_ActivateNotification( + &hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK) { + Error_Handler(); + } + if (HAL_TIM_Base_Start_IT(&htim2) != HAL_OK) { + Error_Handler(); + } + + while (1) { + (void)PROTOCAN_ProcessAllRxMsgs(); + } +} +``` + +`PROTOCAN_INIT()` требует ненулевые указатели на CAN, RTC и TIM, настраивает CAN-фильтры и включает отправку пульса. Функцию следует вызвать после `MX_CAN_Init()`, `MX_RTC_Init()` и `MX_TIMx_Init()`, но до запуска CAN. + +### 5. Подключить callback’и HAL + +Если в HAL включены `USE_HAL_CAN_REGISTER_CALLBACKS == 1` и `USE_HAL_TIM_REGISTER_CALLBACKS == 1`, `PROTOCAN_INIT()` зарегистрирует callback’и автоматически. + +Если регистрация callback’ов отключена, добавить перенаправление из стандартных HAL-callback’ов: + +```c +void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan_ptr) +{ + if (hcan_ptr == &hcan) { + ProtoCanRxFifo0MsgPendingCallback(hcan_ptr); + } +} + +void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim_ptr) +{ + if (htim_ptr == &htim2) { + ProtoCanPulseCallback(htim_ptr); + } +} +``` + +Не следует одновременно регистрировать callback через HAL и вручную вызывать его из стандартного callback — иначе один кадр или событие таймера может быть обработано дважды. + +## Приём сообщений + +В прерывании `ProtoCanRxFifo0MsgPendingCallback()` модуль вычитывает все кадры из FIFO0: + +- стандартные CAN ID игнорируются; +- `PULSE` не занимает место в RX-буфере, а сразу обновляет внутреннюю таблицу присутствующих устройств; +- остальные Extended-кадры помещаются в кольцевой буфер; +- при переполнении новые кадры молча отбрасываются. + +Обработку прикладной логики следует выполнять вне прерывания одним из трёх способов: + +```c +PROTOCAN_ProcessSingleRxMsg(); /* обработать не более одного кадра */ +PROTOCAN_ProcessAllRxMsgs(); /* обработать все накопленные кадры */ +PROTOCAN_LoopProcessRxMsgs(); /* бесконечный блокирующий цикл */ +``` + +Для обычного `while (1)` удобнее `PROTOCAN_ProcessAllRxMsgs()`. Она возвращает `PROTOCAN_TIMEOUT`, если очередь была пуста, `PROTOCAN_OK` после успешной обработки или код первой ошибки. + +## Свои обработчики входящих команд + +Большинство обработчиков объявлены как `__weak`. Приложение может определить функцию с тем же именем и заменить демонстрационную реализацию своей логикой. + +Пример обработки запроса температуры: + +```c +PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogTSens(struct RXMsg msg) +{ + msgBodyAnalogType body = {0}; + body.Body = msg.eID.Fields.MsgBody; + + uint16_t sensor_id = body.Fields.SensorID; + /* Прочитать датчик sensor_id и сформировать нужный ответ. */ + + return PROTOCAN_OK; +} +``` + +Доступные точки переопределения находятся в `protocan.h`: + +- `ProtoCanMsgToBroadcast...()` — статус, включение/выключение, restart, RTC; +- `ProtoCanMsgToDiscrete...()` — аварии, предупреждения, флаги и команды; +- `ProtoCanMsgToAnalog...()` — универсальные данные, настройки, U/I/T; +- `ProtoCanMsgToSettings()` — GET/WRITE/CLEAR привязки ROM к локации; +- `ProtoCanMsgToGeneralAddressSpace()`; +- `ProtoCanMsgToModbus...()`; +- `PROTOCAN_RequestError()`. + +Стандартные слабые обработчики в основном являются демонстрационными: часть возвращает `PROTOCAN_OK` без действий, часть отправляет текстовые ответы вроде `TS0001` или `GAS-0001`. Для рабочего изделия их обычно нужно переопределить. + +## SETTINGS: привязка DS18B20 к локации + +Для прибора `DeviceType=0x7`, `DeviceID=0xF` используется `MsgType=0xE`: + +```text +Body = (Z << 8) | Y + +запрос от ПМ = 0x17FEZZYY +ответ прибора = 0x1FFEZZYY +``` + +| Запрос | Операция | +|---|---| +| `DLC=0` | `GET`: прочитать ROM локации | +| `DLC=8`, ROM не нулевой | `WRITE/REPLACE`: записать или заменить датчик | +| `DLC=8`, восемь нулей | `CLEAR`: очистить локацию | +| другой DLC или RTR | автоматический ответ `INVALID_DLC` | + +Успешный ответ имеет DLC=8 и содержит текущее состояние локации: ROM после +GET/WRITE либо восемь нулей после CLEAR. Ошибка сохраняет тот же Body, имеет +DLC=1 и передаёт `ProtoCanSettingsResultType` в `Data[0]`. + +`PROTOCAN_SettingsProcessing()` выполняет транспортный разбор и вызывает +слабую функцию `ProtoCanMsgToSettings()`. Приложение переопределяет её и +выполняет проверку family code/CRC8, поиск ROM на 1-Wire и запись EEPROM: + +```c +PROTOCAN_StatusTypeDef ProtoCanMsgToSettings( + const ProtoCanSettingsMsg_t *message) +{ + uint8_t current_rom[PROTOCAN_SETTINGS_ROM_SIZE] = {0}; + + switch (message->Operation) { + case PROTOCAN_SETTINGS_GET: + /* Загрузить ROM локации в current_rom; свободная локация = нули. */ + return PROTOCAN_SEND_SETTINGS_RESPONSE( + message->AssemblySerial, message->Position, current_rom); + + case PROTOCAN_SETTINGS_WRITE: + /* Проверить ROM и атомарно записать его в каталог/EEPROM. */ + return PROTOCAN_SEND_SETTINGS_RESPONSE( + message->AssemblySerial, message->Position, message->Rom); + + case PROTOCAN_SETTINGS_CLEAR: + /* Удалить локацию из каталога/EEPROM. */ + return PROTOCAN_SEND_SETTINGS_RESPONSE( + message->AssemblySerial, message->Position, current_rom); + + default: + return PROTOCAN_SEND_SETTINGS_ERROR( + message->AssemblySerial, message->Position, + PROTOCAN_SETTINGS_RESULT_INVALID_DLC); + } +} +``` + +Слабая реализация специально не подтверждает операцию: без прикладного +каталога библиотека не должна сообщать, что ROM был сохранён. + +## Отправка + +Публичная точка отправки — `PROTOCAN_SEND(id, data)`. Сначала заполняется общий Extended ID, затем соответствующая часть `ProtoCanData_t`. + +Пример отправки диапазона 16-битных регистров общего адресного пространства: + +```c +uint16_t registers[] = { 0x1234, 0x5678, 0x9ABC }; + +ProtoCanId_t id = {0}; +id.Fields.Priority = PROTOCAN_PRIORITY_STANDARD; +id.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; +id.Fields.DeviceType = CURRENT_TYPE_DEVICE; +id.Fields.DeviceID = CURRENT_ID_DEVICE; +id.Fields.MsgType = PROTOCAN_MSGTYPE_GENERAL_ADDRESS_SPACE; + +ProtoCanData_t tx = {0}; +tx.GeneralAddressSpaceData.RegStartAdr = 100; +tx.GeneralAddressSpaceData.Data = registers; +tx.GeneralAddressSpaceData.RegCount = 3; + +if (PROTOCAN_SEND(id, tx) != PROTOCAN_OK) { + /* Очередь CAN занята или произошла ошибка HAL. */ +} +``` + +Массив автоматически разбивается на кадры максимум по четыре регистра (8 байт) с увеличением адреса в `MsgBody`. + +Пример сообщения об ошибке без payload: + +```c +ProtoCanId_t id = {0}; +id.Fields.Priority = PROTOCAN_PRIORITY_CRITICAL; +id.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE; +id.Fields.DeviceType = CURRENT_TYPE_DEVICE; +id.Fields.DeviceID = CURRENT_ID_DEVICE; +id.Fields.MsgType = PROTOCAN_MSGTYPE_ERROR; + +ProtoCanData_t tx = {0}; +tx.ErrorData.Code = 0x12; +tx.ErrorData.Info = 0x34; + +(void)PROTOCAN_SEND(id, tx); +``` + +Для `BROADCAST`, `DISCRETE` и `ANALOG` используется `tx.CoreData`; для Modbus-типов — `tx.ModbusData`, для `SETTINGS` — `tx.SettingsData`. Для успешных ответов и ошибок SETTINGS удобнее использовать готовые функции `PROTOCAN_SEND_SETTINGS_RESPONSE()` и `PROTOCAN_SEND_SETTINGS_ERROR()`. Если количество передаваемых элементов больше нуля, соответствующий указатель `Data` должен быть валиден до завершения вызова. + +## Пульс устройства + +Каждое прерывание переданного в `PROTOCAN_INIT()` таймера вызывает `ProtoCanPulseCallback()` и отправляет кадр: + +- `MsgType = PROTOCAN_MSGTYPE_PULSE`; +- `DLC = 1`; +- `Data[0]` — циклический счётчик `0..255`. + +Период пульса полностью определяется настройками таймера. Broadcast-команда `PROTOCAN_BROADCAST_ONOFF` в стандартной реализации переключает его отправку. + +Полученный пульс помечает удалённое устройство как активное и обнуляет `TimeFromLastPulse`. Увеличение этого времени и перевод устройства в offline в текущем модуле не реализованы — если это требуется, контроль таймаута нужно добавить в приложение. + +## Синхронизация RTC + +Broadcast-команда `PROTOCAN_BROADCAST_RTCSETUP` ожидает ровно 7 байт: + +| Индекс | Значение | +|---:|---| +| 0 | часы, `0..23` | +| 1 | минуты, `0..59` | +| 2 | секунды, `0..59` | +| 3 | год как смещение от 2000, `0..99` | +| 4 | месяц, `1..12` | +| 5 | число месяца | +| 6 | день недели в формате, ожидаемом данной прошивкой | + +Перед записью проверяются диапазоны и количество дней с учётом високосного года. + +## Важные ограничения текущей реализации + +- Модуль рассчитан на classic CAN с payload до 8 байт, не на CAN FD. +- `PROTOCAN_INIT()` не запускает CAN/таймер и не включает CAN notification. +- Все три аппаратных фильтра и номера банков заданы внутри библиотеки. +- Фильтр адресованных сообщений рассчитан на устройство: он принимает Route=0 от ПМ. Для использования библиотеки на стороне ПМ потребуется отдельная конфигурация фильтров Route=1. +- Проверку ROM, физического датчика и EEPROM выполняет приложение в переопределённом `ProtoCanMsgToSettings()`. +- Таблица присутствующих устройств обновляется при пульсе, но автоматического offline-таймаута нет. +- Буфер переполнения не ведёт счётчик потерь и не сообщает ошибку приложению. +- В репозитории нет тестов, примера полного STM32-проекта и системы сборки. +- Несколько внутренних функций отправки реализованы только в `protocan.c` и не входят в публичный заголовок; для прикладного кода следует использовать `PROTOCAN_SEND()`. + +## Краткий порядок запуска + +1. Добавить `protocan.c/.h` в STM32-проект. +2. Задать `CURRENT_TYPE_DEVICE` и `CURRENT_ID_DEVICE`. +3. Настроить CAN, RTC и периодический TIM в CubeMX. +4. Вызвать `PROTOCAN_INIT()` после `MX_..._Init()`. +5. Запустить CAN, активировать `CAN_IT_RX_FIFO0_MSG_PENDING` и запустить таймер с прерыванием. +6. При необходимости подключить HAL-callback’и вручную. +7. Переопределить нужные `__weak`-обработчики. +8. Вызывать `PROTOCAN_ProcessAllRxMsgs()` в основном цикле. diff --git a/Src/protocan.c b/Src/protocan.c index 16f6033..731444c 100644 --- a/Src/protocan.c +++ b/Src/protocan.c @@ -164,6 +164,10 @@ void PROTOCAN_LoopProcessRxMsgs(void) { ProtoCanMsgToGeneralAddressSpace(rxMsg[CurrentStep]); } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_SETTINGS) + { + PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]); + } else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_COIL || rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING || @@ -216,6 +220,10 @@ PROTOCAN_StatusTypeDef PROTOCAN_ProcessAllRxMsgs(void) { result = ProtoCanMsgToGeneralAddressSpace(rxMsg[CurrentStep]); } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_SETTINGS) + { + result = PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]); + } else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_COIL || rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING || @@ -269,6 +277,10 @@ PROTOCAN_StatusTypeDef PROTOCAN_ProcessSingleRxMsg(void) { result = ProtoCanMsgToGeneralAddressSpace(rxMsg[CurrentStep]); } + else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_SETTINGS) + { + result = PROTOCAN_SettingsProcessing(rxMsg[CurrentStep]); + } else if(rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_COIL || rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_DISCRETE || rxMsg[CurrentStep].eID.Fields.MsgType == PROTOCAN_MSGTYPE_MODBUS_HOLDING || @@ -868,7 +880,140 @@ PROTOCAN_StatusTypeDef PROTOCAN_SEND_CORE(const ProtoCanId_t id, const struct Pr return PROTOCAN_ERROR; } } - return PROTOCAN_OK; + return PROTOCAN_OK; +} + +/** + * @brief Разбирает однокадровый запрос SETTINGS для текущего устройства. + * + * Body[15:8] содержит номер сборки, Body[7:0] — позицию. DLC=0 означает + * GET, DLC=8 с ненулевым ROM — WRITE/REPLACE, DLC=8 с нулями — CLEAR. + * Проверка физического датчика и сохранение EEPROM выполняются приложением в + * переопределённой функции ProtoCanMsgToSettings(). + */ +PROTOCAN_StatusTypeDef PROTOCAN_SettingsProcessing(struct RXMsg _rxMsg) +{ + ProtoCanSettingsMsg_t message; + uint8_t index; + uint8_t romIsZero = 1U; + + if((_rxMsg.eID.BitAll & PROTOCAN_SETTINGS_HEADER_MASK) + != PROTOCAN_SETTINGS_REQUEST_BASE_ID) + { + return PROTOCAN_ERROR; + } + + message.AssemblySerial = (uint8_t)((_rxMsg.eID.Fields.MsgBody >> 8U) & 0xFFU); + message.Position = (uint8_t)(_rxMsg.eID.Fields.MsgBody & 0xFFU); + message.Result = PROTOCAN_SETTINGS_RESULT_OK; + for(index = 0U; index < PROTOCAN_SETTINGS_ROM_SIZE; index++) + { + message.Rom[index] = 0U; + } + + if((_rxMsg.info.RTR != 0U) + || ((_rxMsg.DLC != 0U) && (_rxMsg.DLC != PROTOCAN_SETTINGS_ROM_SIZE))) + { + message.Operation = PROTOCAN_SETTINGS_INVALID; + message.Result = PROTOCAN_SETTINGS_RESULT_INVALID_DLC; + return PROTOCAN_SEND_SETTINGS_ERROR(message.AssemblySerial, message.Position, + message.Result); + } + if(_rxMsg.DLC == 0U) + { + message.Operation = PROTOCAN_SETTINGS_GET; + return ProtoCanMsgToSettings(&message); + } + + for(index = 0U; index < PROTOCAN_SETTINGS_ROM_SIZE; index++) + { + message.Rom[index] = _rxMsg.Data[index]; + if(message.Rom[index] != 0U) + { + romIsZero = 0U; + } + } + message.Operation = (romIsZero != 0U) ? PROTOCAN_SETTINGS_CLEAR + : PROTOCAN_SETTINGS_WRITE; + return ProtoCanMsgToSettings(&message); +} + +/** + * @brief Слабая точка подключения каталога датчиков приложения. + */ +__weak PROTOCAN_StatusTypeDef ProtoCanMsgToSettings( + const ProtoCanSettingsMsg_t *message) +{ + (void)message; + return PROTOCAN_ERROR; +} + +/** Отправляет один SETTINGS-кадр с Body из id и payload длиной 0..8. */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS( + ProtoCanId_t id, const struct ProtoCanSettingsData data) +{ + CAN_TxHeaderTypeDef TxHeader; + uint32_t TxMailBox = 0U; + uint8_t canData[PROTOCAN_SETTINGS_ROM_SIZE] = {0U}; + uint8_t index; + + if((id.Fields.MsgType != PROTOCAN_MSGTYPE_SETTINGS) + || (data.DataCount > PROTOCAN_SETTINGS_ROM_SIZE) + || ((data.DataCount != 0U) && (data.Data == 0))) + { + return PROTOCAN_ERROR; + } + for(index = 0U; index < data.DataCount; index++) + { + canData[index] = data.Data[index]; + } + TxHeader.ExtId = id.BitAll; + TxHeader.IDE = CAN_ID_EXT; + TxHeader.RTR = CAN_RTR_DATA; + TxHeader.TransmitGlobalTime = DISABLE; + TxHeader.DLC = data.DataCount; + if(HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox) != HAL_OK) + { + return PROTOCAN_ERROR; + } + return PROTOCAN_OK; +} + +/** Отправляет успешный ответ SETTINGS с текущим ROM целевой локации. */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS_RESPONSE(uint8_t assemblySerial, + uint8_t position, const uint8_t *rom) +{ + ProtoCanId_t id; + ProtoCanData_t data; + + if(rom == 0) + { + return PROTOCAN_ERROR; + } + id.BitAll = PROTOCAN_SETTINGS_RESPONSE_BASE_ID + | ((uint32_t)assemblySerial << 8U) | position; + data.SettingsData.Data = rom; + data.SettingsData.DataCount = PROTOCAN_SETTINGS_ROM_SIZE; + return PROTOCAN_SEND_SETTINGS(id, data.SettingsData); +} + +/** Отправляет однобайтовую ошибку SETTINGS с сохранением Body запроса. */ +PROTOCAN_StatusTypeDef PROTOCAN_SEND_SETTINGS_ERROR(uint8_t assemblySerial, + uint8_t position, ProtoCanSettingsResultType result) +{ + ProtoCanId_t id; + ProtoCanData_t data; + const uint8_t error = (uint8_t)result; + + if(result == PROTOCAN_SETTINGS_RESULT_OK) + { + return PROTOCAN_ERROR; + } + id.BitAll = PROTOCAN_SETTINGS_RESPONSE_BASE_ID + | ((uint32_t)assemblySerial << 8U) | position; + data.SettingsData.Data = &error; + data.SettingsData.DataCount = 1U; + return PROTOCAN_SEND_SETTINGS(id, data.SettingsData); } /** @@ -1291,8 +1436,8 @@ void TakeRxMsgToBuffer(ProtoCanId_t tmp_eID, uint32_t tmp_IDE, uint32_t tmp_RTR, tmp_DLC = 8u; } rxMsg[tmp_LastStep].eID.BitAll = tmp_eID.BitAll; - rxMsg[tmp_LastStep].info.EXT = tmp_IDE; - rxMsg[tmp_LastStep].info.RTR = tmp_RTR; + rxMsg[tmp_LastStep].info.EXT = (tmp_IDE == CAN_ID_EXT) ? 1U : 0U; + rxMsg[tmp_LastStep].info.RTR = (tmp_RTR == CAN_RTR_REMOTE) ? 1U : 0U; rxMsg[tmp_LastStep].DLC = tmp_DLC; for(uint32_t i = 0; i < tmp_DLC; i++) { @@ -1445,7 +1590,8 @@ void PROTOCAN_FILTERS() #define BITS_20_23_MASK (0xFUL << 20) // 4 бита (20..23) #define BITS_24_26_MASK (0x7UL << 24) // 3 бита (24..26) - uint32_t filter1_id = BIT27 | (CURRENT_TYPE_DEVICE << 24) | (CURRENT_ID_DEVICE << 20); + /* Адресованный запрос приходит от ПМ, поэтому Route (бит 27) равен нулю. */ + uint32_t filter1_id = (CURRENT_TYPE_DEVICE << 24) | (CURRENT_ID_DEVICE << 20); uint32_t filter1_mask = BIT27 | BITS_24_26_MASK | BITS_20_23_MASK; // все проверяемые биты #define BITS_16_19_MASK (0xFUL << 16) @@ -1456,7 +1602,7 @@ void PROTOCAN_FILTERS() uint32_t filter3_id = PROTOCAN_MSGTYPE_PULSE << 16; uint32_t filter3_mask = BITS_16_19_MASK; - // Первый фильтр: проверяем 20-27 биты, где 27-й всегда 1 + // Первый фильтр: Route=0 (от ПМ), DeviceType и DeviceID текущего устройства. PROTOCAN_CONFIG_FILTER(0, filter1_id, filter1_mask); // Второй фильтр: проверяем 16-19 биты, равны PROTOCAN_MSGTYPE_BROADCASTE @@ -1510,8 +1656,12 @@ PROTOCAN_StatusTypeDef PROTOCAN_SEND(ProtoCanId_t id, ProtoCanData_t data) return PROTOCAN_SEND_ERROR(id, data.ErrorData); break; } + case PROTOCAN_MSGTYPE_SETTINGS: + { + return PROTOCAN_SEND_SETTINGS(id, data.SettingsData); + } default: return PROTOCAN_ERROR; } return PROTOCAN_ERROR; -} \ No newline at end of file +} diff --git a/Протокол CAN и ОАП.xlsx b/Протокол CAN и ОАП.xlsx new file mode 100644 index 0000000..68a666d Binary files /dev/null and b/Протокол CAN и ОАП.xlsx differ