Compare commits
10 Commits
ce6c4327a4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c8eec55978 | |||
| ab60e58318 | |||
| 94fc8c5ef2 | |||
| 58da8aa2fe | |||
| bd8d22312e | |||
| 01de7651e8 | |||
| 34c46ec679 | |||
| 23e37d1c4d | |||
| aea2a79d2c | |||
| bb7c94f1b7 |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "third_party/templates"]
|
||||
path = third_party/templates
|
||||
url = https://git.rd12.ru/Andrey/templates.git
|
||||
111
Inc/protocan.h
111
Inc/protocan.h
@@ -1,7 +1,10 @@
|
||||
//тест
|
||||
|
||||
#ifndef _protocan
|
||||
#define _protocan
|
||||
#include "main.h"
|
||||
#include "can.h"
|
||||
#include "../third_party/templates/c/set-protocol/include/pcan_id.h"
|
||||
|
||||
#define _DEMO
|
||||
|
||||
@@ -9,8 +12,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
|
||||
|
||||
@@ -79,9 +82,54 @@ typedef enum {
|
||||
PROTOCAN_MSGTYPE_MODBUS_HOLDING = 0b0110, /**< Сообщение Modbus - Holding. */
|
||||
PROTOCAN_MSGTYPE_MODBUS_INPUT = 0b0111, /**< Сообщение Modbus - Input. */
|
||||
PROTOCAN_MSGTYPE_ERROR = 0b1000, /**< Сообщение об ошибке. */
|
||||
PROTOCAN_MSGTYPE_BOOT_CONTROL = 0b1001, /**< Команды ProtoCAN Boot. */
|
||||
PROTOCAN_MSGTYPE_BOOT_DATA_A = 0b1010, /**< Блок образа для слота A. */
|
||||
PROTOCAN_MSGTYPE_BOOT_DATA_B = 0b1011, /**< Блок образа для слота B. */
|
||||
PROTOCAN_MSGTYPE_BOOT_STATUS = 0b1100, /**< Статус команды загрузчика. */
|
||||
PROTOCAN_MSGTYPE_BOOT_DISCOVERY = 0b1101, /**< Идентификация загрузчика/приложения. */
|
||||
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.
|
||||
*
|
||||
@@ -212,6 +260,38 @@ typedef union{
|
||||
unsigned int BitAll:29; /**< Общее представление 29-битного идентификатора. */
|
||||
} ProtoCanId_t;
|
||||
|
||||
/**
|
||||
* Переходная граница legacy API -> переносимый формат линии.
|
||||
*
|
||||
* Прикладной код SETCAN пока может заполнять исторические Fields, но CAN ID
|
||||
* всегда собирается и разбирается общим pcan_id без зависимости от порядка
|
||||
* битовых полей конкретного компилятора.
|
||||
*/
|
||||
static inline uint32_t PROTOCAN_IdPack(const ProtoCanId_t *legacy)
|
||||
{
|
||||
pcan_id_t shared;
|
||||
shared.msg_body = (uint16_t)legacy->Fields.MsgBody;
|
||||
shared.msg_type = (uint8_t)legacy->Fields.MsgType;
|
||||
shared.device_id = (uint8_t)legacy->Fields.DeviceID;
|
||||
shared.device_type = (uint8_t)legacy->Fields.DeviceType;
|
||||
shared.route = (uint8_t)legacy->Fields.Route;
|
||||
shared.priority = (uint8_t)legacy->Fields.Priority;
|
||||
return pcan_id_pack(&shared);
|
||||
}
|
||||
|
||||
static inline void PROTOCAN_IdUnpack(uint32_t raw, ProtoCanId_t *legacy)
|
||||
{
|
||||
pcan_id_t shared;
|
||||
pcan_id_unpack(raw, &shared);
|
||||
legacy->BitAll = 0U;
|
||||
legacy->Fields.MsgBody = shared.msg_body;
|
||||
legacy->Fields.MsgType = (ProtoCanMsgType)shared.msg_type;
|
||||
legacy->Fields.DeviceID = shared.device_id;
|
||||
legacy->Fields.DeviceType = shared.device_type;
|
||||
legacy->Fields.Route = (ProtoCanRouteType)shared.route;
|
||||
legacy->Fields.Priority = (ProtoCanPriorityType)shared.priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Структура для общего адресного пространства CAN.
|
||||
*
|
||||
@@ -238,6 +318,10 @@ typedef struct{
|
||||
uint16_t Info;
|
||||
uint16_t Code;
|
||||
} ErrorData;
|
||||
struct ProtoCanSettingsData{
|
||||
const uint8_t *Data;
|
||||
uint8_t DataCount;
|
||||
} SettingsData;
|
||||
} ProtoCanData_t;
|
||||
|
||||
/**
|
||||
@@ -305,6 +389,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);
|
||||
|
||||
204
Src/protocan.c
204
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 ||
|
||||
@@ -348,7 +360,7 @@ __weak PROTOCAN_StatusTypeDef ProtoCanMsgToBroadcastStatus(struct RXMsg _rxMsg)
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
tmp_eID.Fields.DeviceType = CURRENT_TYPE_DEVICE;
|
||||
tmp_eID.Fields.DeviceID = CURRENT_ID_DEVICE;
|
||||
TxHeader.ExtId = tmp_eID.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
RTC_TimeTypeDef sTime = {0};
|
||||
HAL_RTC_GetTime(_HRTC, &sTime, RTC_FORMAT_BIN);
|
||||
data[0] = sTime.Hours;
|
||||
@@ -640,7 +652,7 @@ __weak PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogUniversal(struct RXMsg _rxMsg)
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = tmp_eID.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 6;
|
||||
data[0] = 'U';
|
||||
data[1] = 'N';
|
||||
@@ -668,7 +680,7 @@ __weak PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogSettings(struct RXMsg _rxMsg)
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = tmp_eID.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 7;
|
||||
data[0] = 'U';
|
||||
data[1] = 'S';
|
||||
@@ -697,7 +709,7 @@ __weak PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogUSens(struct RXMsg _rxMsg)
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = tmp_eID.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 6;
|
||||
msgBodyAnalogType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
@@ -727,7 +739,7 @@ __weak PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogISens(struct RXMsg _rxMsg)
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = tmp_eID.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 6;
|
||||
msgBodyAnalogType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
@@ -757,7 +769,7 @@ __weak PROTOCAN_StatusTypeDef ProtoCanMsgToAnalogTSens(struct RXMsg _rxMsg)
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = tmp_eID.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 6;
|
||||
msgBodyAnalogType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
@@ -801,7 +813,7 @@ PROTOCAN_StatusTypeDef PROTOCAN_SEND_CORE(const ProtoCanId_t id, const struct Pr
|
||||
body.Fields.Type = coreData.Type;
|
||||
body.Fields.Body = coreData.Body;
|
||||
localId.Fields.MsgBody = body.Body;
|
||||
TxHeader.ExtId = localId.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&localId);
|
||||
TxHeader.DLC = 0;
|
||||
if(HAL_CAN_AddTxMessage(_HCAN, &TxHeader, canData, &TxMailBox) != HAL_OK)
|
||||
{
|
||||
@@ -820,7 +832,7 @@ PROTOCAN_StatusTypeDef PROTOCAN_SEND_CORE(const ProtoCanId_t id, const struct Pr
|
||||
uint8_t regsInPacket = (regsRemaining > 8) ? 8 : regsRemaining;
|
||||
body.Fields.Body = coreData.Body + currentIndex;
|
||||
localId.Fields.MsgBody = body.Body;
|
||||
TxHeader.ExtId = localId.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&localId);
|
||||
TxHeader.DLC = regsInPacket;
|
||||
for(int i = 0; i < regsInPacket; i++)
|
||||
{
|
||||
@@ -847,7 +859,7 @@ PROTOCAN_StatusTypeDef PROTOCAN_SEND_CORE(const ProtoCanId_t id, const struct Pr
|
||||
uint8_t regsInPacket = (regsRemaining > 8) ? 8 : regsRemaining;
|
||||
body.Fields.SensorID = coreData.Body + currentIndex;
|
||||
localId.Fields.MsgBody = body.Body;
|
||||
TxHeader.ExtId = localId.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&localId);
|
||||
TxHeader.DLC = regsInPacket;
|
||||
for(int i = 0; i < regsInPacket; i++)
|
||||
{
|
||||
@@ -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 = PROTOCAN_IdPack(&id);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -887,7 +1032,7 @@ __weak PROTOCAN_StatusTypeDef ProtoCanMsgToGeneralAddressSpace(struct RXMsg _rxM
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = tmp_eID.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 8;
|
||||
data[0] = 'G';
|
||||
data[1] = 'A';
|
||||
@@ -939,7 +1084,7 @@ PROTOCAN_StatusTypeDef PROTOCAN_SEND_GENERAL_ADDRESS_SPACE(ProtoCanId_t id, uint
|
||||
uint8_t regsInPacket = (regsRemaining > 4) ? 4 : regsRemaining;
|
||||
|
||||
tmp_eID.Fields.MsgBody = regStartAdr + currentIndex;
|
||||
TxHeader.ExtId = tmp_eID.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = regsInPacket*2;
|
||||
|
||||
for(int i = 0; i < regsInPacket; i++)
|
||||
@@ -1017,7 +1162,7 @@ __weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbusCoil(struct RXMsg _rxMsg)
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = tmp_eID.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 8;
|
||||
msgBodyModbusType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
@@ -1049,7 +1194,7 @@ __weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbusDiscrete(struct RXMsg _rxMsg)
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = tmp_eID.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 8;
|
||||
msgBodyModbusType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
@@ -1081,7 +1226,7 @@ __weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbusHolding(struct RXMsg _rxMsg)
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = tmp_eID.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 8;
|
||||
msgBodyModbusType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
@@ -1113,7 +1258,7 @@ __weak PROTOCAN_StatusTypeDef ProtoCanMsgToModbusInput(struct RXMsg _rxMsg)
|
||||
ProtoCanId_t tmp_eID;
|
||||
tmp_eID.BitAll = _rxMsg.eID.BitAll;
|
||||
tmp_eID.Fields.Route = PROTOCAN_ROUTE_FROM_DEVICE;
|
||||
TxHeader.ExtId = tmp_eID.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 8;
|
||||
msgBodyModbusType msg;
|
||||
msg.Body = _rxMsg.eID.Fields.MsgBody;
|
||||
@@ -1158,7 +1303,7 @@ PROTOCAN_StatusTypeDef PROTOCAN_SEND_MODBUS(const ProtoCanId_t id, const struct
|
||||
// Устанавливаем коллизию ExtId с body.Body
|
||||
localId.Fields.MsgBody = (body.Body & 0xFFFF);
|
||||
// Обновляем ExtId
|
||||
TxHeader.ExtId = localId.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&localId);
|
||||
// Остальные настройки
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
@@ -1187,7 +1332,7 @@ PROTOCAN_StatusTypeDef PROTOCAN_SEND_MODBUS(const ProtoCanId_t id, const struct
|
||||
body.Fields.RegCount = regsInPacket;
|
||||
// Обновляем ExtId
|
||||
localId.Fields.MsgBody = (body.Body & 0xFFFF);
|
||||
TxHeader.ExtId = localId.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&localId);
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
@@ -1236,7 +1381,7 @@ __weak PROTOCAN_StatusTypeDef PROTOCAN_RequestError(struct RXMsg _rxMsg)
|
||||
msg.Fields.ErrorCode = 0xFF;
|
||||
msg.Fields.Info = 0;
|
||||
tmp_eID.Fields.MsgBody = msg.Body;
|
||||
TxHeader.ExtId = tmp_eID.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&tmp_eID);
|
||||
TxHeader.DLC = 0;
|
||||
return (PROTOCAN_StatusTypeDef)HAL_CAN_AddTxMessage(_HCAN, &TxHeader, data, &TxMailBox);
|
||||
}
|
||||
@@ -1261,7 +1406,7 @@ PROTOCAN_StatusTypeDef PROTOCAN_SEND_ERROR(const ProtoCanId_t id, const struct P
|
||||
uint8_t canData[8];
|
||||
localId.Fields.MsgBody = ((errorData.Info << 8) | errorData.Code);
|
||||
// Обновляем ExtId
|
||||
TxHeader.ExtId = localId.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(&localId);
|
||||
// Остальные настройки
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.RTR = CAN_RTR_DATA;
|
||||
@@ -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++)
|
||||
{
|
||||
@@ -1321,7 +1466,7 @@ void ProtoCanRxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
|
||||
if(RxHeader.IDE == CAN_ID_EXT)
|
||||
{
|
||||
ProtoCanId_t ExtID_Of_RX_MSG;
|
||||
ExtID_Of_RX_MSG.BitAll = RxHeader.ExtId;
|
||||
PROTOCAN_IdUnpack(RxHeader.ExtId, &ExtID_Of_RX_MSG);
|
||||
//Полученное сообщение - пульс устройств в сети.
|
||||
//Место в буфере не занимает, поэтому проверяется до проверки переполнения.
|
||||
if(ExtID_Of_RX_MSG.Fields.MsgType == PROTOCAN_MSGTYPE_PULSE)
|
||||
@@ -1365,7 +1510,7 @@ void ProtoCanPulseCallback(TIM_HandleTypeDef *htim)
|
||||
currentID.Fields.DeviceID = CURRENT_ID_DEVICE;
|
||||
currentID.Fields.MsgType = PROTOCAN_MSGTYPE_PULSE;
|
||||
currentID.Fields.MsgBody = 0;
|
||||
TxHeader.ExtId = currentID.BitAll;
|
||||
TxHeader.ExtId = PROTOCAN_IdPack(¤tID);
|
||||
uint8_t data[8];
|
||||
TxHeader.IDE = CAN_ID_EXT;
|
||||
TxHeader.TransmitGlobalTime = DISABLE;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
13
doc/build-html.bat
Normal file
13
doc/build-html.bat
Normal file
@@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
|
||||
where pwsh.exe >nul 2>nul
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
pwsh.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%build-html.ps1"
|
||||
) else (
|
||||
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%build-html.ps1"
|
||||
)
|
||||
|
||||
exit /b %ERRORLEVEL%
|
||||
87
doc/build-html.ps1
Normal file
87
doc/build-html.ps1
Normal file
@@ -0,0 +1,87 @@
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$outputPath = Join-Path $PSScriptRoot 'index.html'
|
||||
$sourceDirectory = Join-Path $PSScriptRoot 'protocan'
|
||||
$documents = @(
|
||||
'README.md',
|
||||
'PROTOCOL.md',
|
||||
'BOOTLOADER.md',
|
||||
'OAP.md',
|
||||
'CHANGELOG.md'
|
||||
)
|
||||
|
||||
function Convert-LocalLinks {
|
||||
param(
|
||||
[string]$Html,
|
||||
[string]$SourcePath
|
||||
)
|
||||
|
||||
$sourceParent = Split-Path -Parent $SourcePath
|
||||
$outputParent = Split-Path -Parent $outputPath
|
||||
$pattern = '(?<attribute>href|src)="(?<target>(?![a-z]+:|/|#)[^"]+)"'
|
||||
|
||||
return [regex]::Replace($Html, $pattern, {
|
||||
param($match)
|
||||
|
||||
$target = $match.Groups['target'].Value
|
||||
$parts = $target -split '#', 2
|
||||
$targetPath = [Uri]::UnescapeDataString($parts[0])
|
||||
$absoluteTarget = [System.IO.Path]::GetFullPath((Join-Path $sourceParent $targetPath))
|
||||
$relativeTarget = [System.IO.Path]::GetRelativePath($outputParent, $absoluteTarget).Replace('\', '/')
|
||||
if ($parts.Count -eq 2) {
|
||||
$relativeTarget += '#' + $parts[1]
|
||||
}
|
||||
|
||||
return $match.Groups['attribute'].Value + '="' + $relativeTarget + '"'
|
||||
})
|
||||
}
|
||||
|
||||
$sections = foreach ($document in $documents) {
|
||||
$path = Join-Path $sourceDirectory $document
|
||||
$markdown = Get-Content -Raw -LiteralPath $path -Encoding UTF8
|
||||
$html = (ConvertFrom-Markdown -InputObject $markdown).Html
|
||||
$html = Convert-LocalLinks -Html $html -SourcePath $path
|
||||
"<article class=`"card protocan-document`" data-source=`"$document`">$html</article>"
|
||||
}
|
||||
|
||||
$vectorsPath = Join-Path $sourceDirectory 'examples\test-vectors.json'
|
||||
$vectors = [System.Net.WebUtility]::HtmlEncode(
|
||||
(Get-Content -Raw -LiteralPath $vectorsPath -Encoding UTF8)
|
||||
)
|
||||
$sections += @"
|
||||
<article class="card protocan-document" data-source="examples/test-vectors.json">
|
||||
<h1>Тестовые векторы</h1>
|
||||
<p>Машинные эталоны из <code>examples/test-vectors.json</code>.</p>
|
||||
<pre><code>$vectors</code></pre>
|
||||
</article>
|
||||
"@
|
||||
|
||||
$generatedBlock = @"
|
||||
<!-- PROTOCAN:START -->
|
||||
<section id="protocan-full" class="section">
|
||||
<h2>Полная документация ProtoCAN</h2>
|
||||
<p class="lead">Нормативные документы и тестовые векторы собраны в эту страницу из исходников <code>doc/protocan</code>.</p>
|
||||
<div class="grid protocan-grid">
|
||||
$($sections -join "`n")
|
||||
</div>
|
||||
</section>
|
||||
<!-- PROTOCAN:END -->
|
||||
"@
|
||||
|
||||
$page = Get-Content -Raw -LiteralPath $outputPath -Encoding UTF8
|
||||
$pattern = '(?s)<!-- PROTOCAN:START -->.*?<!-- PROTOCAN:END -->'
|
||||
if ($page -notmatch $pattern) {
|
||||
throw 'Не найдены маркеры PROTOCAN:START/END в doc/index.html.'
|
||||
}
|
||||
|
||||
$page = [regex]::Replace($page, $pattern, [System.Text.RegularExpressions.MatchEvaluator]{
|
||||
param($match)
|
||||
$generatedBlock
|
||||
}, 1)
|
||||
|
||||
$page = $page.TrimEnd("`r", "`n") + [Environment]::NewLine
|
||||
$utf8WithoutBom = [System.Text.UTF8Encoding]::new($false)
|
||||
[System.IO.File]::WriteAllText($outputPath, $page, $utf8WithoutBom)
|
||||
Write-Host "[DONE] Создан единый HTML: $outputPath"
|
||||
1202
doc/index.html
Normal file
1202
doc/index.html
Normal file
File diff suppressed because one or more lines are too long
183
doc/protocan/BOOTLOADER.md
Normal file
183
doc/protocan/BOOTLOADER.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# ProtoCAN Boot Protocol
|
||||
|
||||
Статус: **Draft**
|
||||
Версия протокола: **1.0**
|
||||
Совместимость: **classic CAN 2.0B, Extended ID, DLC 0…8**
|
||||
Реализация: [`templates/c/protocan-boot`](../../../templates/c/protocan-boot)
|
||||
|
||||
## Назначение
|
||||
|
||||
Сервис обновляет адресованный прибор по CAN и поддерживает два логических
|
||||
слота A/B. Активный слот не стирается: новый образ записывается в неактивный,
|
||||
проверяется и атомарно назначается кандидатом на запуск.
|
||||
|
||||
## Карта сообщений
|
||||
|
||||
| `MsgType` | Имя | `MsgBody` | Payload |
|
||||
|---:|---|---|---|
|
||||
| `0x9` | `BOOT_CONTROL` | `SessionID[15:8] \| Command[7:0]` | параметры команды |
|
||||
| `0xA` | `BOOT_DATA_A` | `BlockIndex[15:0]` | 8 байт слота A |
|
||||
| `0xB` | `BOOT_DATA_B` | `BlockIndex[15:0]` | 8 байт слота B |
|
||||
| `0xC` | `BOOT_STATUS` | `SessionID[15:8] \| Command[7:0]` | статус и прогресс |
|
||||
| `0xD` | `BOOT_DISCOVERY` | подтип ответа | идентификация |
|
||||
|
||||
Все команды записи адресуются конкретному `DeviceType/DeviceID` и имеют
|
||||
`Route=0`. Ответы сохраняют адрес прибора и имеют `Route=1`.
|
||||
|
||||
## Адресация образа
|
||||
|
||||
`MsgBody` кадра данных — номер 8-байтового блока:
|
||||
|
||||
```c
|
||||
offset = (uint32_t)BlockIndex * 8U;
|
||||
address = SLOT_X_BASE + offset;
|
||||
```
|
||||
|
||||
```text
|
||||
512 КиБ = 524 288 байт
|
||||
524 288 / 8 = 65 536 блоков
|
||||
BlockIndex = 0x0000…0xFFFF
|
||||
```
|
||||
|
||||
| `BlockIndex` | Смещение | Диапазон байтов |
|
||||
|---:|---:|---:|
|
||||
| `0x0000` | `0x00000` | `0x00000…0x00007` |
|
||||
| `0x0001` | `0x00008` | `0x00008…0x0000F` |
|
||||
| `0xFFFF` | `0x7FFF8` | `0x7FFF8…0x7FFFF` |
|
||||
|
||||
`0x80000` является первой позицией за границей слота. Последний кадр
|
||||
дополняется `0xFF`, но CRC32 вычисляется только по `ImageSize` байтам.
|
||||
|
||||
## Команды `BOOT_CONTROL`
|
||||
|
||||
| Код | Команда | DLC | Payload | Допустимое состояние |
|
||||
|---:|---|---:|---|---|
|
||||
| `0x01` | `IDENTIFY` | 0 | отсутствует | любое |
|
||||
| `0x02` | `ENTER_BOOT` | 0 | отсутствует | любое; `SessionID != 0` |
|
||||
| `0x03` | `BEGIN_IMAGE` | 8 | размер и CRC32 | metadata |
|
||||
| `0x04` | `BEGIN_COMPAT` | 8 | совместимость и версия | metadata |
|
||||
| `0x05` | `ERASE` | 0 | отсутствует | ready-to-erase |
|
||||
| `0x06` | `VERIFY` | 0 | отсутствует | образ получен |
|
||||
| `0x07` | `COMMIT` | 0 | отсутствует | verified |
|
||||
| `0x08` | `CONFIRM` | 0 | отсутствует | запущенное приложение |
|
||||
| `0x09` | `REBOOT` | 0 | отсутствует | активная сессия |
|
||||
| `0x0A` | `ABORT` | 0 | отсутствует | активная сессия |
|
||||
| `0x0B` | `QUERY_PROGRESS` | 0 | отсутствует | активная сессия |
|
||||
|
||||
### `BEGIN_IMAGE`
|
||||
|
||||
```text
|
||||
DATA[0..3] ImageSize, uint32 little-endian
|
||||
DATA[4..7] ImageCRC32, uint32 little-endian
|
||||
```
|
||||
|
||||
### `BEGIN_COMPAT`
|
||||
|
||||
```text
|
||||
DATA[0..1] ProductType, uint16 little-endian
|
||||
DATA[2] HardwareRevisionMin
|
||||
DATA[3] HardwareRevisionMax
|
||||
DATA[4..7] FirmwareVersion, uint32 little-endian
|
||||
```
|
||||
|
||||
До `ERASE` прибор обязан получить обе части метаданных и проверить размер,
|
||||
тип изделия, аппаратную ревизию, версию и политику anti-rollback.
|
||||
|
||||
## `BOOT_STATUS`
|
||||
|
||||
```text
|
||||
MsgBody[15..8] SessionID
|
||||
MsgBody[7..0] команда, на которую дан ответ
|
||||
|
||||
DATA[0] Status
|
||||
DATA[1] TargetSlot: 0=A, 1=B, 0xFF=не выбран
|
||||
DATA[2..3] NextBlock, uint16 little-endian
|
||||
DATA[4..7] RunningCRC32, uint32 little-endian
|
||||
```
|
||||
|
||||
| Код | Статус | Повтор допустим |
|
||||
|---:|---|---|
|
||||
| `0x00` | `OK` | — |
|
||||
| `0x01` | `BUSY` | да, после задержки |
|
||||
| `0x02` | `INVALID_COMMAND` | после исправления |
|
||||
| `0x03` | `WRONG_DEVICE` | нет для этого образа |
|
||||
| `0x04` | `WRONG_HARDWARE` | нет для этого образа |
|
||||
| `0x05` | `INVALID_SIZE` | нет для этого образа |
|
||||
| `0x06` | `CRC_ERROR` | новая передача |
|
||||
| `0x07` | `FLASH_ERROR` | зависит от платформы |
|
||||
| `0x08` | `SEQUENCE_ERROR` | да, с `NextBlock` |
|
||||
| `0x09` | `SIGNATURE_ERROR` | нет |
|
||||
| `0x0A` | `SESSION_ERROR` | открыть новую сессию |
|
||||
| `0x0B` | `VOLTAGE_ERROR` | да после нормализации питания |
|
||||
| `0x0C` | `INVALID_STATE` | выполнить правильный переход |
|
||||
|
||||
## State machine
|
||||
|
||||
```text
|
||||
IDLE
|
||||
└─ ENTER_BOOT ─> METADATA
|
||||
├─ BEGIN_IMAGE
|
||||
└─ BEGIN_COMPAT
|
||||
│
|
||||
v
|
||||
READY_TO_ERASE
|
||||
│ ERASE
|
||||
v
|
||||
RECEIVING
|
||||
│ VERIFY
|
||||
v
|
||||
VERIFIED
|
||||
│ COMMIT
|
||||
v
|
||||
PENDING + REBOOT
|
||||
│ CONFIRM
|
||||
v
|
||||
CONFIRMED
|
||||
```
|
||||
|
||||
Ошибка Flash, CRC, совместимости или подписи переводит сессию в `FAILED`.
|
||||
Новая `ENTER_BOOT` создаёт чистую сессию. `ABORT` прекращает текущую передачу,
|
||||
не активируя частично записанный слот.
|
||||
|
||||
## Надёжность и повторы
|
||||
|
||||
- Блоки передаются строго по возрастанию `BlockIndex`.
|
||||
- Дубликат или пропуск возвращает `SEQUENCE_ERROR` и ожидаемый `NextBlock`.
|
||||
- Базовый режим подтверждает каждый блок.
|
||||
- Рабочий режим может подтверждать окно из 16 блоков.
|
||||
- После потери связи `QUERY_PROGRESS` возвращает следующий ожидаемый блок,
|
||||
пока состояние загрузчика сохранено.
|
||||
- Для продолжения после перезагрузки порт должен сохранять session metadata
|
||||
и восстановить её при инициализации; ядро версии 1.0 само это не делает.
|
||||
|
||||
## Безопасность и A/B-обновление
|
||||
|
||||
```text
|
||||
active=A -> target=B -> verify -> pending=B
|
||||
active=B -> target=A -> verify -> pending=A
|
||||
```
|
||||
|
||||
CRC32 защищает только от случайного повреждения. Серийный загрузчик должен
|
||||
дополнительно проверить подпись контейнера, границы вектора, совместимость и
|
||||
anti-rollback. Bootloader не обновляется командами `BOOT_DATA_A/B`.
|
||||
|
||||
Boot metadata должна атомарно хранить:
|
||||
|
||||
- активный слот;
|
||||
- pending-слот;
|
||||
- подтверждение запуска;
|
||||
- число неудачных попыток;
|
||||
- версию и CRC32 образа.
|
||||
|
||||
Если приложение не выполняет `CONFIRM` за установленное число запусков,
|
||||
загрузчик возвращается к предыдущему подтверждённому слоту.
|
||||
|
||||
## Эталонный сценарий
|
||||
|
||||
1. ПМ адресно отправляет `IDENTIFY`.
|
||||
2. ПМ открывает ненулевой `SessionID` командой `ENTER_BOOT`.
|
||||
3. ПМ отправляет `BEGIN_IMAGE` и `BEGIN_COMPAT`.
|
||||
4. Прибор сообщает выбранный неактивный слот.
|
||||
5. ПМ выполняет `ERASE` и передаёт `BOOT_DATA_A` либо `BOOT_DATA_B`.
|
||||
6. ПМ выполняет `VERIFY`, затем `COMMIT` и `REBOOT`.
|
||||
7. Новое приложение после самопроверки выполняет `CONFIRM`.
|
||||
21
doc/protocan/CHANGELOG.md
Normal file
21
doc/protocan/CHANGELOG.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# История изменений ProtoCAN
|
||||
|
||||
Формат основан на Keep a Changelog. Версия относится к спецификации, а не к
|
||||
версии прошивки отдельного прибора.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Структурированный комплект документации `doc/protocan`.
|
||||
- Загрузочный сервис `MsgType=0x9…0xD`.
|
||||
- Два логических слота по 512 КиБ, 65 536 блоков по 8 байт каждый.
|
||||
- Машинные эталоны CAN ID в `examples/test-vectors.json`.
|
||||
|
||||
## [1.0] — 2026-08-29
|
||||
|
||||
### Added
|
||||
|
||||
- Зафиксирована 29-битная структура ProtoCAN ID.
|
||||
- Зафиксирована адресация 8 типов по 16 экземпляров.
|
||||
- Существующие сообщения `0x0…0x8`, `0xE`, `0xF` сохранены.
|
||||
56
doc/protocan/OAP.md
Normal file
56
doc/protocan/OAP.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Общее адресное пространство
|
||||
|
||||
Статус: **Stable, данные ведутся в XLSX**
|
||||
Порядок значений: **16-битные регистры, little-endian в CAN payload**
|
||||
|
||||
Редактируемый источник реестра:
|
||||
[`Протокол CAN и ОАП.xlsx`](../../Протокол%20CAN%20и%20ОАП.xlsx).
|
||||
|
||||
Просматриваемая большая таблица находится в
|
||||
[`Протокол CAN и ОАП.html`](../../Протокол%20CAN%20и%20ОАП.html) и
|
||||
[`Протокол CAN и ОАП.md`](../../Протокол%20CAN%20и%20ОАП.md).
|
||||
|
||||
## Назначение
|
||||
|
||||
ОАП связывает 16-битный адрес регистра с его типом, назначением, доступом и
|
||||
масштабом. В ProtoCAN используется `MsgType=0x3`, а `MsgBody` содержит адрес
|
||||
первого регистра.
|
||||
|
||||
## Обязательные поля реестра
|
||||
|
||||
| Поле | Требование |
|
||||
|---|---|
|
||||
| AddressHex | `0x0000…0xFFFF`, уникальное значение |
|
||||
| AddressDec | десятичный эквивалент AddressHex |
|
||||
| Group | функциональная группа |
|
||||
| Name | однозначное имя параметра |
|
||||
| Type | `u16`, `i16`, `u32`, `i32`, `float32`, bitmap или массив |
|
||||
| Registers | число занятых 16-битных регистров |
|
||||
| Access | `R`, `W` или `RW` |
|
||||
| Unit | физическая единица либо `—` |
|
||||
| Scale | множитель/делитель представления |
|
||||
| Default | значение после сброса, если применимо |
|
||||
| Description | семантика, диапазон и особые значения |
|
||||
|
||||
## Правила ведения
|
||||
|
||||
- Адрес не переиспользуется с другим смыслом после выпуска стабильной версии.
|
||||
- Многорегистровое значение занимает непрерывный диапазон.
|
||||
- Порядок 16-битных слов для 32-битного значения фиксируется в строке типа.
|
||||
- Резервные диапазоны явно отмечаются и не используются без изменения версии.
|
||||
- Удалённый параметр помечается deprecated, а не исчезает молча.
|
||||
- Изменение адреса, типа или масштаба отражается в `CHANGELOG.md`.
|
||||
|
||||
## Экспорт
|
||||
|
||||
Для программной генерации каталог следует экспортировать из XLSX в CSV с
|
||||
UTF-8 и фиксированными английскими именами колонок. CSV должен проверяться на:
|
||||
|
||||
- уникальность адресов;
|
||||
- пересечение многорегистровых значений;
|
||||
- допустимые типы и права доступа;
|
||||
- равенство шестнадцатеричного и десятичного адреса;
|
||||
- попадание адреса в диапазон `0x0000…0xFFFF`.
|
||||
|
||||
До появления автоматического экспортёра нормативным источником адресов
|
||||
остаётся XLSX, а HTML/Markdown считаются представлением.
|
||||
114
doc/protocan/PROTOCOL.md
Normal file
114
doc/protocan/PROTOCOL.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# ProtoCAN — базовый протокол
|
||||
|
||||
Статус: **Stable с зарезервированным загрузочным расширением**
|
||||
Версия: **1.0**
|
||||
Порядок байтов payload: **little-endian**, если явно не указано иное
|
||||
|
||||
## Назначение
|
||||
|
||||
ProtoCAN — прикладной протокол поверх classic CAN 2.0B. Используются только
|
||||
расширенные 29-битные идентификаторы (`IDE=1`) и payload длиной 0…8 байт.
|
||||
|
||||
## Термины
|
||||
|
||||
| Термин | Значение |
|
||||
|---|---|
|
||||
| ПМ | управляющий модуль |
|
||||
| прибор | адресуемый узел на шине |
|
||||
| `DeviceType` | тип прибора, 0…7 |
|
||||
| `DeviceID` | экземпляр прибора данного типа, 0…15 |
|
||||
| `MsgType` | класс сообщения или сервис |
|
||||
| `MsgBody` | 16-битное поле, формат которого зависит от `MsgType` |
|
||||
|
||||
Пара `DeviceType/DeviceID` задаёт до `8 × 16 = 128` уникальных адресов.
|
||||
|
||||
## Расширенный CAN ID
|
||||
|
||||
```text
|
||||
28 27 26...24 23...20 19...16 15........0
|
||||
Priority Route DeviceType DeviceID MsgType MsgBody
|
||||
1 бит 1 бит 3 бита 4 бита 4 бита 16 бит
|
||||
```
|
||||
|
||||
```c
|
||||
can_id =
|
||||
((uint32_t)priority << 28) |
|
||||
((uint32_t)route << 27) |
|
||||
((uint32_t)device_type << 24) |
|
||||
((uint32_t)device_id << 20) |
|
||||
((uint32_t)msg_type << 16) |
|
||||
msg_body;
|
||||
```
|
||||
|
||||
| Поле | Значения | Назначение |
|
||||
|---|---|---|
|
||||
| `Priority` | `0` critical, `1` standard | CAN-арбитраж |
|
||||
| `Route` | `0` от ПМ, `1` от прибора | логическое направление |
|
||||
| `DeviceType` | `0…7` | тип прибора |
|
||||
| `DeviceID` | `0…15` | номер экземпляра |
|
||||
| `MsgType` | `0…15` | тип сообщения |
|
||||
| `MsgBody` | `0…65535` | команда, адрес или номер блока |
|
||||
|
||||
`Route` не является направлением физического трансивера. Ответ прибора
|
||||
сохраняет адрес `DeviceType/DeviceID` и устанавливает `Route=1`.
|
||||
|
||||
## Реестр `MsgType`
|
||||
|
||||
| Код | Имя | Основное направление | DLC | Статус |
|
||||
|---:|---|---|---:|---|
|
||||
| `0x0` | `BROADCAST` | ПМ → все | зависит от команды | stable |
|
||||
| `0x1` | `DISCRETE` | оба | 0…8 | stable |
|
||||
| `0x2` | `ANALOG` | оба | 0…8 | stable |
|
||||
| `0x3` | `GAS` | оба | 0/2/4/6/8 | stable |
|
||||
| `0x4` | `MODBUS_COIL` | оба | 0…8 | stable |
|
||||
| `0x5` | `MODBUS_DISCRETE` | оба | 0…8 | stable |
|
||||
| `0x6` | `MODBUS_HOLDING` | оба | 0…8 | stable |
|
||||
| `0x7` | `MODBUS_INPUT` | оба | 0…8 | stable |
|
||||
| `0x8` | `ERROR` | прибор → ПМ | 0 | stable |
|
||||
| `0x9` | `BOOT_CONTROL` | ПМ → прибор | 0/8 | draft |
|
||||
| `0xA` | `BOOT_DATA_A` | ПМ → прибор | 8 | draft |
|
||||
| `0xB` | `BOOT_DATA_B` | ПМ → прибор | 8 | draft |
|
||||
| `0xC` | `BOOT_STATUS` | прибор → ПМ | 8 | draft |
|
||||
| `0xD` | `BOOT_DISCOVERY` | прибор → ПМ | 8 | draft |
|
||||
| `0xE` | `SETTINGS` | оба | 0/1/8 | stable |
|
||||
| `0xF` | `PULSE` | прибор → сеть | 1 | stable |
|
||||
|
||||
Подробный формат `0x9…0xD` находится в [BOOTLOADER.md](BOOTLOADER.md).
|
||||
|
||||
## Разметки `MsgBody`
|
||||
|
||||
| `MsgType` | Биты `MsgBody` |
|
||||
|---|---|
|
||||
| broadcast | команда `[15:4]`, параметр `[3:0]` |
|
||||
| discrete/analog | подтип `[15:12]`, значение/адрес `[11:0]` |
|
||||
| Modbus | начальный адрес `[15:4]`, количество `[3:0]` |
|
||||
| GAS | адрес первого 16-битного регистра `[15:0]` |
|
||||
| error | дополнительная информация `[15:8]`, код `[7:0]` |
|
||||
| settings | номер сборки `[15:8]`, позиция `[7:0]` |
|
||||
| boot control/status | `SessionID[15:8]`, команда `[7:0]` |
|
||||
| boot data | `BlockIndex[15:0]` |
|
||||
|
||||
## Общие правила обмена
|
||||
|
||||
- Многобайтовые значения в `DATA` передаются little-endian.
|
||||
- Узел игнорирует адресованные кадры с чужим `DeviceType/DeviceID`.
|
||||
- Прибор принимает команды ПМ с `Route=0`; ПМ принимает ответы с `Route=1`.
|
||||
- Стандартные 11-битные CAN ID не являются кадрами ProtoCAN.
|
||||
- RTR для загрузочного сервиса запрещён.
|
||||
- Неописанные комбинации `MsgType/MsgBody/DLC` должны отвергаться.
|
||||
|
||||
## Эталон упаковки ID
|
||||
|
||||
```text
|
||||
Priority = 1
|
||||
Route = 0
|
||||
DeviceType = 3
|
||||
DeviceID = 5
|
||||
MsgType = 0x9
|
||||
MsgBody = 0x0702
|
||||
|
||||
CAN ID = 0x13590702
|
||||
```
|
||||
|
||||
Этот пример соответствует `ENTER_BOOT`, `SessionID=7`. Машинные варианты
|
||||
находятся в [examples/test-vectors.json](examples/test-vectors.json).
|
||||
42
doc/protocan/README.md
Normal file
42
doc/protocan/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Документация ProtoCAN
|
||||
|
||||
Статус комплекта: **Draft**
|
||||
Версия комплекта: **1.0**
|
||||
Дата редакции: **2026-08-29**
|
||||
Транспорт: **Classic CAN 2.0B, Extended ID, DLC 0…8**
|
||||
|
||||
Этот каталог разделяет нормативное описание протокола, загрузчик и реестр
|
||||
общего адресного пространства. Большой исходный документ
|
||||
[`Протокол CAN и ОАП.md`](../../Протокол%20CAN%20и%20ОАП.md) сохранён как
|
||||
совместимое представление таблиц из Excel.
|
||||
|
||||
## Документы
|
||||
|
||||
| Документ | Назначение | Статус источника |
|
||||
|---|---|---|
|
||||
| [PROTOCOL.md](PROTOCOL.md) | 29-битный CAN ID, адресация, реестр `MsgType`, порядок байтов | нормативный |
|
||||
| [BOOTLOADER.md](BOOTLOADER.md) | обновление прошивки, кадры, состояния, ошибки и A/B-слоты | нормативный draft |
|
||||
| [OAP.md](OAP.md) | правила ведения общего адресного пространства | нормативный индекс |
|
||||
| [../../Протокол CAN и ОАП.xlsx](../../Протокол%20CAN%20и%20ОАП.xlsx) | редактируемый реестр ОАП | источник таблиц |
|
||||
| [examples/test-vectors.json](examples/test-vectors.json) | машинные эталоны CAN ID и payload | нормативные примеры |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | история версий документа | нормативный |
|
||||
|
||||
## Приоритет источников
|
||||
|
||||
При расхождении данных действует следующий порядок:
|
||||
|
||||
1. `PROTOCOL.md` — структура ProtoCAN и реестр типов сообщений.
|
||||
2. `BOOTLOADER.md` — загрузочный сервис `0x9…0xD`.
|
||||
3. XLSX — адреса и свойства регистров ОАП.
|
||||
4. Сгенерированный [`../index.html`](../index.html) — только представление, не самостоятельный источник.
|
||||
|
||||
## Сборка HTML
|
||||
|
||||
Из корня проекта:
|
||||
|
||||
```powershell
|
||||
./doc/build-html.bat
|
||||
```
|
||||
|
||||
Все документы и тестовые векторы включаются в единый файл `doc/index.html`.
|
||||
Скрипт не изменяет исходные Markdown/XLSX и пригоден для запуска в CI.
|
||||
45
doc/protocan/examples/test-vectors.json
Normal file
45
doc/protocan/examples/test-vectors.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"byte_order": "little-endian",
|
||||
"frames": [
|
||||
{
|
||||
"name": "enter_boot_session_7",
|
||||
"direction": "pm_to_device",
|
||||
"priority": 1,
|
||||
"route": 0,
|
||||
"device_type": 3,
|
||||
"device_id": 5,
|
||||
"msg_type": 9,
|
||||
"msg_body": 1794,
|
||||
"can_id_hex": "0x13590702",
|
||||
"dlc": 0,
|
||||
"data_hex": ""
|
||||
},
|
||||
{
|
||||
"name": "slot_b_block_1",
|
||||
"direction": "pm_to_device",
|
||||
"priority": 1,
|
||||
"route": 0,
|
||||
"device_type": 3,
|
||||
"device_id": 5,
|
||||
"msg_type": 11,
|
||||
"msg_body": 1,
|
||||
"can_id_hex": "0x135B0001",
|
||||
"dlc": 8,
|
||||
"data_hex": "1011121314151617"
|
||||
},
|
||||
{
|
||||
"name": "enter_boot_ok",
|
||||
"direction": "device_to_pm",
|
||||
"priority": 1,
|
||||
"route": 1,
|
||||
"device_type": 3,
|
||||
"device_id": 5,
|
||||
"msg_type": 12,
|
||||
"msg_body": 1794,
|
||||
"can_id_hex": "0x1B5C0702",
|
||||
"dlc": 8,
|
||||
"data_hex": "00FF000000000000"
|
||||
}
|
||||
]
|
||||
}
|
||||
13405
doc/Протокол CAN и ОАП.html
Normal file
13405
doc/Протокол CAN и ОАП.html
Normal file
File diff suppressed because it is too large
Load Diff
13276
doc/Протокол CAN и ОАП.md
Normal file
13276
doc/Протокол CAN и ОАП.md
Normal file
File diff suppressed because it is too large
Load Diff
BIN
doc/Протокол CAN и ОАП.xlsx
Normal file
BIN
doc/Протокол CAN и ОАП.xlsx
Normal file
Binary file not shown.
1
doc/ссылки на док.md
Normal file
1
doc/ссылки на док.md
Normal file
@@ -0,0 +1 @@
|
||||
https://disk.yandex.ru/i/TMyHlpYxIP75YQ
|
||||
1
third_party/templates
vendored
Submodule
1
third_party/templates
vendored
Submodule
Submodule third_party/templates added at 19becd7b8c
Reference in New Issue
Block a user