# Cyphal CAN

Когда устройство становится достаточно сложным, появляется потребность в коммуникации между контроллерами, или между контроллером и компьютером (например, Raspberry Pi). Для этого в микроконтроллерах STM32G4 встроен модуль FDCAN. Этот протокол является популярным и достаточно гибким способом построения коммуникационных сетей, однако он не предоставляет никаких абстракций над пакетами байтов, которыми оперирует.&#x20;

Для целей общения между устройствами, нами был выбран прокол [Cyphal](https://opencyphal.org/), создающий слой абстракции над FDCAN без потерь в скорости и дающий возможность передавать структурированные сообщения произвольной длины, а не просто байты.

{% hint style="info" %}
У этого протокола есть и множество других достоинств, описание всего протокола выходит за рамки этой докуменации, подробнее о нем можно прочитать [тут](https://opencyphal.org/specification/Cyphal_Specification.pdf).
{% endhint %}

***

## libcxxcanard

Нами была разработана и проверена на множестве устройств универсальная C++ библиотека для работы с cyphal (на основе *libcanard*) - [libcxxcanard](https://github.com/VBCores/libcxxcanard). Далее будет дан обзор возможностей и типичных примеров использования данной библиотеки.

{% hint style="info" %}
Подробная документация - [read-the-docs](https://libcxxcanard.readthedocs.io/en/latest/)

Примеры -  [libcyphal-docs](https://github.com/VBCores/libcyphal-docs/tree/master)
{% endhint %}

<details>

<summary>Короткое сравнение с libcanard</summary>

libcxxcanard основана на libcanard и призвана упростить ее использование. Далее несколько наглядных сравнений "на пальцах" (на картинки лучше нажать, чтоб рассмотреть подробнее):

![](https://3551773033-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FBjLyfPc4FcQUfXFi5fQr%2Fuploads%2FsxZ8r0lsGU4uHN7IcaOe%2F%D0%A1%D0%BD%D0%B8%D0%BC%D0%BE%D0%BA%20%D1%8D%D0%BA%D1%80%D0%B0%D0%BD%D0%B0%202024-12-17%20%D0%B2%2017.40.54.png?alt=media\&token=c6e939a9-c286-4beb-b83a-d5f3c21710dc)

![](https://3551773033-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FBjLyfPc4FcQUfXFi5fQr%2Fuploads%2FL901QARGFrE8qWI1mPKI%2F%D0%A1%D0%BD%D0%B8%D0%BC%D0%BE%D0%BA%20%D1%8D%D0%BA%D1%80%D0%B0%D0%BD%D0%B0%202024-12-17%20%D0%B2%2017.41.07.png?alt=media\&token=09e135ee-7ffd-4fc0-9d5b-b7e89505286c)

</details>

Дальше будут параллельно разбираться примеры для linux (raspberry pi) и stm32g4. Считается, что сделаны следующие инклюды:

```cpp
#include <cyphal/cyphal.h>
#include <cyphal/allocators/o1/o1_allocator.h>
#include <cyphal/subscriptions/subscription.h>
```

### Инициализация

Код инициализации короткий и кроссплатформенный, надо только выбрать нужный провайдер:

{% tabs %}
{% tab title="linux" %}

```cpp
#include <cyphal/providers/LinuxCAN.h>

std::shared_ptr<CyphalInterface> cyphal_interface;

void main() {
    cyphal_interface = std::shared_ptr<CyphalInterface>(CyphalInterface::create_heap<LinuxCAN, O1Allocator>(
        NODE_ID,
        "can0",         // SocketCAN
        200,            // длина очереди
        DEFAULT_CONFIG  // дефолтные настройки для линукса, объявлены в библиотеке
    ));
}
```

{% endtab %}

{% tab title="stm32" %}

```cpp
#include <cyphal/providers/G4CAN.h>

static std::shared_ptr<CyphalInterface> cyphal_interface;

// Необходимо определить для репорта ошибок - просто вызовем дефолтный хэндлер
// Но можно делать и что-то более разумное - например:
// https://github.com/voltbro/gyrobro_foc/blob/feature/mech-angle/App/communication.cpp#L47
void error_handler() {
    Error_Handler();
}
// micros_64 - надо объявить отдельно, с помощью таймера
// или для ленивых HAL_GetTick()*1000
UtilityConfig utilities(micros_64, cyphal_error_handler);

void main() {
    cyphal_interface = std::shared_ptr<CyphalInterface>(CyphalInterface::create_heap<G4CAN, O1Allocator>(
        NODE_ID,
        &hfdcan1,  // считаем что настроен, например через CubeMX
        200,       // длина очереди
        utilities  // объявлен выше
    ));
}
```

{% endtab %}
{% endtabs %}

### Подписки и отправка сообщений

По сравнению с libcanard, работа с отправкой/приемом сообщений гораздо проще. Есть единственное неудобство: из-за того что libcxxcanard работает поверх сишного libcanard, для любого используемого типа сообщений нужно (**единожды**) использовать макрос `TYPE_ALIAS` - `TYPE_ALIAS(УдобноеНовоеНазваниеТипа, сгенерированный_cyhal_тип_сообщения)`.

Минимальный пример, echo-сервиса:

{% tabs %}
{% tab title="linux" %}

```cpp
#include <voltbro/echo/echo_servoce_1_0.h>

TYPE_ALIAS(EchoRequest, voltbro_echo_echo_service_Request_1_0)
TYPE_ALIAS(EchoResponse, voltbro_echo_echo_service_Response_1_0)

// Подписки наследуются от шаблонного класса AbstractSubscription
// 1000 - port_id для сервиса
class EchoSub: public AbstractSubscription<EchoRequest> {
public:
    EchoSub(InterfacePtr interface):
        AbstractSubscription<EchoRequest>(interface, 1000) 
        {};
    void handler(const EchoRequest::Type& request, CanardRxTransfer* transfer) override {
        EchoResponse::Type response = {.pong = request.ping};
        interface->send_response<EchoResponse>(&response, transfer);
    }
};

std::shared_ptr<EchoSub> echo_sub;

void main() {
    // ... сначала тут описанная выше инициализация cyphal
    echo_sub = std::make_shared<EchoSub>(cyphal_interface);
}
```

{% endtab %}

{% tab title="stm32" %}

```cpp
#include <voltbro/echo/echo_servoce_1_0.h>

TYPE_ALIAS(EchoRequest, voltbro_echo_echo_service_Request_1_0)
TYPE_ALIAS(EchoResponse, voltbro_echo_echo_service_Response_1_0)

// Подписки наследуются от шаблонного класса AbstractSubscription
// 1000 - port_id для сервиса
class EchoSub: public AbstractSubscription<EchoRequest> {
public:
    EchoSub(InterfacePtr interface):
        AbstractSubscription<EchoRequest>(interface, 1000) 
        {};
    void handler(const EchoRequest::Type& request, CanardRxTransfer* transfer) override {
        EchoResponse::Type response = {.pong = request.ping};
        interface->send_response<EchoResponse>(&response, transfer);
    }
};

std::shared_ptr<EchoSub> echo_sub;

void main() {
    // ... сначала тут описанная выше инициализация cyphal
    echo_sub = std::make_shared<EchoSub>(cyphal_interface);
}

// Пример настройки хардварных фильтров для подписки, актуально только для STM32
void setup_filters() {
    HAL_FDCAN_ConfigGlobalFilter(
        &hfdcan1,
        FDCAN_REJECT,
        FDCAN_REJECT,
        FDCAN_REJECT_REMOTE,
        FDCAN_REJECT_REMOTE
    );

    static FDCAN_FilterTypeDef sFilterConfig;

    // Хардварный фильтр для пордписки. apply_filter - из libcxxcanard
    apply_filter(&hfdcan1, &sFilterConfig,echo_sub->make_filter(NODE_ID))
}
```

{% endtab %}
{% endtabs %}

Развернутый пример подписчика на обычные сообщения, связанного с ROS (для linux):

```cpp
#include <voltbro/battery/state_1_0.h>
#include <sensor_msgs/msg/battery_state.hpp>

TYPE_ALIAS(BatteryState, voltbro_battery_state_1_0)

class BatteryService : public AbstractSubscription<BatteryState> {
private:
    rclcpp::Publisher<sensor_msgs::msg::BatteryState>::SharedPtr publisher;
    rclcpp::Logger logger;
public:
    BatteryService(rclcpp::Node* node, const std::shared_ptr<CyphalInterface> interface)
        : AbstractSubscription<BatteryState>(interface, 7993),
          logger(node->get_logger().get_child("battery")) {
        std::string topic_name = "/bat";
        publisher = node->create_publisher<sensor_msgs::msg::BatteryState>(topic_name, 5);
        RCLCPP_INFO(logger, "Publishing topic <%s>", topic_name.c_str());
    }
    void handler(
        const BatteryState::Type& bat_info,
        CanardRxTransfer* transfer
    ) {
        sensor_msgs::msg::BatteryState battery;
        
        // Копируем поля
        battery.voltage = bat_info.voltage.volt;
        // ... и т.д.
        
        publisher->publish(battery);
    }
};
```
