esp32移植canopen协议栈(二)

前言


本文续接上一篇文章,之前我们已经搭建好了esp32的开发环境,以及初始工程。接下来我们开始实现can驱动相关,以及使用c++封装can接口。

一、编程之前先看原理图

在这里插入图片描述
can转换芯片的tx与rx分别接在TWAI_TX, TWAI_RX上,再看主控部分原理图
在这里插入图片描述
从原理图上看到

tx --> IO2 --> GPIO_NUM_2
rx --> IO42 --> GPIO_NUM_42

二、封装can驱动

先看下我们基础工程的目录结构
在这里插入图片描述
我们需要创建一个新的文件夹来存放我们的can驱动(他们一般叫组件),我们在项目根目录下创建一个名为components的文件夹,创好后的目录结构如下
在这里插入图片描述
我们在这个components组件目录下创建一个can文件夹,用来存放我们得can驱动。
然后在can文件夹下,创建一个can.hpp的c++文件,.hpp可以把源码和声明放在一起。可以先写成下图所示
在这里插入图片描述
此时假如我们编译,应该是要报错,报错原因是找不到can.hpp
在这里插入图片描述
我们在main目录下的CMakeLists.txt中加入can.hpp的路径,便可以编译通过
在这里插入图片描述
接下来我们接着封装can驱动
一般来说can需要指定TX,RX两个引脚,而在我们编程阶段这两个引脚一定是写死的,因为这个时候开发板都已经出来了,can的引脚也改变不了,这非常符合常量的定义,且是编译期常量,联系到c++,说到编译期,第一个想到的关键字是constexpr,然后就是模板。因为模板的参数可以是常量,与该场景非常契合,且效率无敌。于是我们可以将can类改为模板类,如下图所示
在这里插入图片描述
我们接下来封装can驱动,按照基本接口来,我们应该是需要4个接口,分别是
init, read, write, deinit。
init对应构造函数,deinit对应析构函数,read从总线读取数据,write为向can总线写入数据。
我们首先来写构造函数(init),在构造函数中我们需要传入can的速率,需要有参构造。
在这里插入图片描述

#pragma once 

#include "driver/twai.h"

template <int tx, int rx>
class Can {
private:
    twai_handle_t _handler {nullptr};
public:
    explicit Can(int kbps) {
        twai_general_config_t gConfig = TWAI_GENERAL_CONFIG_DEFAULT(static_cast<gpio_num_t>(tx), static_cast<gpio_num_t>(rx), TWAI_MODE_NORMAL);
        twai_timing_config_t tConfig;
        switch (kbps) {
            case 1:
                tConfig = TWAI_TIMING_CONFIG_1KBITS();
                break;

            case 5:
                tConfig = TWAI_TIMING_CONFIG_5KBITS();
                break;

            case 10:
                tConfig = TWAI_TIMING_CONFIG_10KBITS();
                break;

            case 16:
                tConfig = TWAI_TIMING_CONFIG_16KBITS();
                break;

            case 20:
                tConfig = TWAI_TIMING_CONFIG_20KBITS();
                break;

            case 25:
                tConfig = TWAI_TIMING_CONFIG_25KBITS();
                break;

            case 50:
                tConfig = TWAI_TIMING_CONFIG_50KBITS();
                break;

            case 100:
                tConfig = TWAI_TIMING_CONFIG_100KBITS();
                break;

            case 125:
                tConfig = TWAI_TIMING_CONFIG_125KBITS();
                break;

            case 250:
                tConfig = TWAI_TIMING_CONFIG_250KBITS();
                break;

            case 500:
                tConfig = TWAI_TIMING_CONFIG_500KBITS();
                break;

            case 800:
                tConfig = TWAI_TIMING_CONFIG_800KBITS();
                break;

            case 1000:
                tConfig = TWAI_TIMING_CONFIG_1MBITS();
                break;

            case 1024:
                tConfig = TWAI_TIMING_CONFIG_1MBITS();
                break;

            default:
                tConfig = TWAI_TIMING_CONFIG_500KBITS();
                break;
        }

        twai_filter_config_t fConfig = TWAI_FILTER_CONFIG_ACCEPT_ALL();
        ESP_ERROR_CHECK(twai_driver_install_v2(&gConfig, &tConfig, &fConfig, &_handler));
        ESP_ERROR_CHECK(twai_start_v2(_handler));
    }         
};

把拷贝构造,拷贝赋值删除,不允许拷贝, 但允许移动操作。移动使用默认的就行。

	Can(const Can &) = delete;
    Can &operator=(const Can &) = delete;
    Can(Can &&) noexcept = default;
    Can &operator=(Can &&) noexcept = default; 

这样初始化接口就写好了。
接下来我们写析构函数。
因为之前我们维护了一个_handler的一个变量作为can操作的句柄,初始化时是为空指针的状态,我们先判断这个指针是否非空,就能知道是否初始化成功。如果成功,我们需要反初始化。

    ~Can() {
        if (_handler) {
            twai_stop_v2(_handler);
            twai_driver_uninstall_v2(_handler);
        }
    }

接下来我们需要实现write接口,也就是向can总线发送数据。
由于write接口需要构建一个can协议的message发送,我们可以在can协议的基础上封装我们自己的消息包,这样也方便canopen消息与esp32 can驱动消息的转换。我们新建一个名为canMessage.hpp的文件,然后我们新建一个类名为CanMessage
在这里插入图片描述

#pragma once 

#include "driver/twai.h"
#include <vector>

class CanMessage {
private:
    uint32_t _identifier;
    std::vector<uint8_t> _data;
    uint8_t _rtr;
public:
    CanMessage(uint32_t id, const std::vector<uint8_t> &d, uint8_t rtr = 0) : _identifier(id), _data(std::move(d)), _rtr(rtr){

    }

    operator twai_message_t() const {
        twai_message_t m {
            .identifier = _identifier,
        };
        m.rtr = _rtr;
        std::copy(_data.begin(), _data.end(), m.data);
        m.data_length_code = _data.size();
        return m;
    }
};

接着完善我们得write函数

    bool write(const CanMessage &m, uint32_t timeout = portMAX_DELAY) {
        if (!_handler) {
            return false;
        }
        twai_message_t message = m;
        return  twai_transmit_v2(_handler, &message, timeout) == ESP_OK;
    }

接下来我们完善我们得read函数。
read意为从can总线上获取一个can_message,可能返回一个读到数据,(如果非组塞的话)也可能读超时,这个时候我们返回一个空对象,这个场景非常的契合 现代c++的optional(c++17) std::optional, 如果读到数据我们返回canMessage消息对象,如果没有得到数据我们返回std::nullopt。

  std::optional<CanMessage> read(uint32_t timeout = portMAX_DELAY) {

        twai_message_t message{};

        if (auto ret = twai_receive_v2(_handler, &message, timeout); ret == ESP_OK) {
            return message;
        }

        return std::nullopt;
    }

这里twai_message_t对象是不能直接转成CanMessage对象的,因此我们要在CanMessage类中加入由twai_message_t 来构造出CanMessage的方法。

#pragma once 

#include "driver/twai.h"
#include <vector>

class CanMessage { 
private:
    uint32_t _identifier;
    std::vector<uint8_t> _data;
    uint8_t _rtr;
public:
    CanMessage(uint32_t id, const std::vector<uint8_t> &d, uint8_t rtr = 0) : _identifier(id), _data(std::move(d)), _rtr(rtr) {

    }

    CanMessage(const twai_message_t &t) : _identifier(t.identifier), _data(t.data, t.data + t.data_length_code), _rtr(t.rtr) {
        
    }


    operator twai_message_t() const {
        twai_message_t m {
            .identifier = _identifier,
        };
        m.rtr = _rtr;
        std::copy(_data.begin(), _data.end(), m.data);
        m.data_length_code = _data.size();
        return m;
    }
};

三、验证can驱动

为了实例化方便
我们可以定义引脚相关的宏
static constexpr auto canTx = 2;
static constexpr auto canRx = 42;
constexpr 是c++关键字,定义编译期的常量(和#define差不多),且有检查功能,比#define好,而且都使用现在c++了,当然要用现代c++的语法啦。
using Can0 = Can<canTx, canTx>;

static constexpr auto canTx = 2;
static constexpr auto canRx = 42;
using Can0 = Can<canTx, canRx>;

在main函数中写下如下代码
在这里插入图片描述

#include <iostream>
#include "can.hpp"
#include <thread>
#include <chrono>

extern "C" auto app_main() {
    std::cout << "hello,world" << std::endl;

    using namespace std::chrono_literals;
    auto can = new Can0(500);

    while (true) {

        if (auto rx = can->read()) {
            can->write(*rx); // 回显
        }

        // 以上代码等同于
        /*
            std::optional<CanMessage> rx = can->read();
            if (rx.has_value()) {
                can->write(rx.value());
            }
        */

        std::this_thread::sleep_for(1s);
    }

    delete can;
}

同时打开usbcan 上位机进行测试

在这里插入图片描述

回显成功哦

四、文件源码如下

  • can.hpp
#pragma once 

#include "driver/twai.h"
#include "canMessage.hpp"
#include <optional>

template <int tx, int rx>
class Can {
private:
    twai_handle_t _handler {nullptr};
public:
    explicit Can(int kbps) {
        twai_general_config_t gConfig = TWAI_GENERAL_CONFIG_DEFAULT(static_cast<gpio_num_t>(tx), static_cast<gpio_num_t>(rx), TWAI_MODE_NORMAL);
        twai_timing_config_t tConfig;
        switch (kbps) {
            case 1:
                tConfig = TWAI_TIMING_CONFIG_1KBITS();
                break;

            case 5:
                tConfig = TWAI_TIMING_CONFIG_5KBITS();
                break;

            case 10:
                tConfig = TWAI_TIMING_CONFIG_10KBITS();
                break;

            case 16:
                tConfig = TWAI_TIMING_CONFIG_16KBITS();
                break;

            case 20:
                tConfig = TWAI_TIMING_CONFIG_20KBITS();
                break;

            case 25:
                tConfig = TWAI_TIMING_CONFIG_25KBITS();
                break;

            case 50:
                tConfig = TWAI_TIMING_CONFIG_50KBITS();
                break;

            case 100:
                tConfig = TWAI_TIMING_CONFIG_100KBITS();
                break;

            case 125:
                tConfig = TWAI_TIMING_CONFIG_125KBITS();
                break;

            case 250:
                tConfig = TWAI_TIMING_CONFIG_250KBITS();
                break;

            case 500:
                tConfig = TWAI_TIMING_CONFIG_500KBITS();
                break;

            case 800:
                tConfig = TWAI_TIMING_CONFIG_800KBITS();
                break;

            case 1000:
                tConfig = TWAI_TIMING_CONFIG_1MBITS();
                break;

            case 1024:
                tConfig = TWAI_TIMING_CONFIG_1MBITS();
                break;

            default:
                tConfig = TWAI_TIMING_CONFIG_500KBITS();
                break;
        }

        twai_filter_config_t fConfig = TWAI_FILTER_CONFIG_ACCEPT_ALL();
        ESP_ERROR_CHECK(twai_driver_install_v2(&gConfig, &tConfig, &fConfig, &_handler));
        ESP_ERROR_CHECK(twai_start_v2(_handler));
    }   

    Can(const Can &) = delete;
    Can &operator=(const Can &) = delete;
    Can(Can &&) noexcept = default;
    Can &operator=(Can &&) noexcept = default; 

    ~Can() {
        if (_handler) {
            twai_stop_v2(_handler);
            twai_driver_uninstall_v2(_handler);
        }
    }
    
public:
    bool write(const CanMessage &m, uint32_t timeout = portMAX_DELAY) {
        if (!_handler) {
            return false;
        }
        twai_message_t message = m;
        return  twai_transmit_v2(_handler, &message, timeout) == ESP_OK;
    }

    std::optional<CanMessage> read(uint32_t timeout = portMAX_DELAY) {

        twai_message_t message{};

        if (auto ret = twai_receive_v2(_handler, &message, timeout); ret == ESP_OK) {
            return message;
        }

        return std::nullopt;
    }

};

static constexpr auto canTx = 2;
static constexpr auto canRx = 42;

using Can0 = Can<canTx, canRx>;
  • canMessage.hpp
#pragma once 

#include "driver/twai.h"
#include <vector>

class CanMessage { 
private:
    uint32_t _identifier;
    std::vector<uint8_t> _data;
    uint8_t _rtr;
public:
    CanMessage(uint32_t id, const std::vector<uint8_t> &d, uint8_t rtr = 0) : _identifier(id), _data(std::move(d)), _rtr(rtr) {

    }

    CanMessage(const twai_message_t &t) : _identifier(t.identifier), _data(t.data, t.data + t.data_length_code), _rtr(t.rtr) {
        
    }

    operator twai_message_t() const {
        twai_message_t m {
            .identifier = _identifier,
        };
        m.rtr = _rtr;
        std::copy(_data.begin(), _data.end(), m.data);
        m.data_length_code = _data.size();
        return m;
    }

    friend std::ostream &operator<<(std::ostream &out, const CanMessage &c) {
        out << "len --> " << c._data.size() << "\n";
        for (auto &item : c._data) {
            out << (int)item << ",";
        }    

        return out << "id --> " << c._identifier << "\n";
    }
};
  • main.cpp
#include <iostream>
#include "can.hpp"
#include <thread>
#include <chrono>

extern "C" auto app_main() {
    std::cout << "hello,world" << std::endl;

    using namespace std::chrono_literals;
    auto can = new Can0(500);

    while (true) {

        if (auto rx = can->read()) {
            can->write(*rx); // 回显
        }

        // 以上代码等同于
        /*
            std::optional<CanMessage> rx = can->read();
            if (rx.has_value()) {
                can->write(rx.value());
            }
        */

        // std::this_thread::sleep_for(1s);
    }

    delete can;
}


五、总结

本文使用esp32使用c++实现can驱动层的封装。下一步移植canopen!!!嘻嘻。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值