311 lines
16 KiB
Markdown
311 lines
16 KiB
Markdown
# SETCAN
|
||
|
||
`SETCAN` — небольшой модуль прикладного протокола поверх классического CAN для микроконтроллеров STM32 и библиотеки STM32 HAL. Модуль принимает только кадры с расширенным 29-битным идентификатором, складывает их в кольцевой буфер и передаёт обработчикам по типу сообщения. Также он умеет отправлять широковещательные, дискретные, аналоговые, адресные, Modbus-подобные сообщения, ошибки и периодический «пульс» устройства.
|
||
|
||
Это не самостоятельный проект 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` | ошибка |
|
||
| `0xF` | пульс присутствия устройства |
|
||
|
||
Разметка `MsgBody` зависит от типа сообщения:
|
||
|
||
- broadcast: младшие 4 бита — дополнительное поле, старшие 12 бит — команда;
|
||
- discrete: младшие 12 бит — данные/адрес, старшие 4 бита — подтип;
|
||
- analog: младшие 12 бит — ID датчика, старшие 4 бита — тип величины;
|
||
- Modbus: младшие 4 бита — количество регистров, старшие 12 бит — начальный адрес;
|
||
- 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` задать значения конкретного узла:
|
||
|
||
```c
|
||
#define CURRENT_TYPE_DEVICE 0b001 /* 3 бита: 0..7 */
|
||
#define CURRENT_ID_DEVICE 0b0010 /* 4 бита: 0..15 */
|
||
```
|
||
|
||
При необходимости изменить размер программного RX-буфера:
|
||
|
||
```c
|
||
#define PROTOCAN_RX_BUFFER_SIZE 128
|
||
```
|
||
|
||
Полезная ёмкость кольцевого буфера на один элемент меньше заданного размера, то есть при значении `128` в нём помещается 127 ожидающих кадров.
|
||
|
||
### 3. Настроить CubeMX
|
||
|
||
В проекте должны быть инициализированы:
|
||
|
||
- CAN с поддержкой Extended ID;
|
||
- RTC;
|
||
- базовый таймер, задающий период отправки пульса.
|
||
|
||
Модуль использует CAN FIFO0 и фильтры с номерами `0`, `1`, `2`. В `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;
|
||
- `ProtoCanMsgToGeneralAddressSpace()`;
|
||
- `ProtoCanMsgToModbus...()`;
|
||
- `PROTOCAN_RequestError()`.
|
||
|
||
Стандартные слабые обработчики в основном являются демонстрационными: часть возвращает `PROTOCAN_OK` без действий, часть отправляет текстовые ответы вроде `TS0001` или `GAS-0001`. Для рабочего изделия их обычно нужно переопределить.
|
||
|
||
## Отправка
|
||
|
||
Публичная точка отправки — `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`. Если количество передаваемых элементов больше нуля, соответствующий указатель `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` (бит 27). Это стоит сверить с принятой в системе трактовкой `PROTOCAN_ROUTE_FROM_PM`/`PROTOCAN_ROUTE_FROM_DEVICE`.
|
||
- Таблица присутствующих устройств обновляется при пульсе, но автоматического 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()` в основном цикле.
|
||
|