Cyphal CAN

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

Для целей общения между устройствами, нами был выбран прокол Cyphal, создающий слой абстракции над FDCAN без потерь в скорости и дающий возможность передавать структурированные сообщения произвольной длины, а не просто байты.

У этого протокола есть и множество других достоинств, описание всего протокола выходит за рамки этой докуменации, подробнее о нем можно прочитать тут.


libcxxcanard

Нами была разработана и проверена на множестве устройств универсальная C++ библиотека для работы с cyphal (на основе libcanard) - libcxxcanard. Далее будет дан обзор возможностей и типичных примеров использования данной библиотеки.

Подробная документация - read-the-docs

Примеры - libcyphal-docs

Короткое сравнение с libcanard

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

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

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

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

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

#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  // дефолтные настройки для линукса, объявлены в библиотеке
    ));
}

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

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

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

#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);
}

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

#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);
    }
};