前言
本文续接上一篇文章,在之前的工程中,我们实现了can驱动的封装,离我们移植canopen更近一步了,但canopen的启动,sdo,pdo等调度都与时间密切相关,我们还需要实现一个定时器给canopen协议栈做心跳使用,本文我们来使用esp32 + 现代c++封装一个好用的timer类出来,为移植canopen打基础。
一、创建timer组件
先看下我们现在的工程目录。
我们需要在components
目录下创建一个新的文件夹timer
, 在timer
文件夹下,新建timer.hpp
的cpp文件,如下图所示。
二、timer接口设计
我们得timer基于esp32的高精度定时器实现。我们理想的效果是类似如下方式
以下是伪代码
auto handle = new Timer(...);
handle->startOnce(...);
handle->startPeriod(...);
if (handle->isStart()) {
handle->stop();
}
大概需要这几个接口
三、timer接口实现
esp32的高精度定时器需要init,我们把这一部分放到Timer的构造函数中。但是我们每次申请一个定时器的时候都需要走构造函数,这个init我们全局只需要调用一次。这种场景可以考虑c++的单例模式,但貌似不是那么好,想一想也许std::call_once更为适合。
因为定时器有两种模式,单次和周期,我们可以用一个枚举类代表这两种模式。
未知,单次,周期三种状态,构造函数中状态设置为UNKNOW
。
我们还需要一个回调函数,保存用户传入的函数。在启动的时候我们要求用户要传入函数,以及函数的参数。
接下来我们实现startOnce函数,我们需要用户传入时间和callback, 意为多久之后执行这个callback. 也就是我们需要一个时间间隔,callback, 以及callback的参数。但这样就有问题了,因为用户传入的函数不固定,函数的参数也不固定,但我们这边确实固定的void(*)(void)啊。还好我们用的是现代c++,可以使用变长函数模板+参数绑定来实现这个目的。因为esp32的timer的回调是个c语言的函数指针,我们需要使用一个静态的成员函数来封装我们c++的function(可调用对象)。于是我们得startOnce可以写成如下
定义一个_handle来操作定时器的启停状态。定义一个静态成员函数来作为c语言函数指针,参数为this,即把本对象传入到函数里。利用c++的万能引用和参数完美转发实现callback的参数绑定。也实现了isStart接口(判断是否启动过定时器)。
接下来我们实现startPeriod接口,类似startOnce接口。
接下来我们实现我们得实现stop接口
我们得在析构函数中判断定时器是否被销毁,如果没有,我们要手动销毁。
为了防止重复启动定时器,我们需要再启动时判断该定时器是否被启用了,这样就可以覆盖了。
为了多线程并发考虑,我们需要再启动定时器这个阶段中进行加锁,保证这个操作的原子性。
最后要禁用拷贝,只准移动。
timer.hpp完整源码如下
#pragma once
#include "esp_timer.h"
#include <chrono>
#include <mutex>
#include <functional>
class Timer {
private:
enum class TimerMode : uint8_t {
UNKNOW,
ONCE,
PERIOD,
};
using Callback = std::function<void()>;
private:
static inline std::once_flag _flag;
TimerMode _mode;
Callback _callback;
esp_timer_handle_t _handle {nullptr};
std::mutex _mutex;
private:
static void defaultTimerCallback(void *arg) {
static_cast<Timer *>(arg)->_callback();
}
public:
explicit Timer() : _mode(TimerMode::UNKNOW) {
std::call_once(_flag, esp_timer_init);
}
Timer(const Timer &) = delete;
Timer &operator=(const Timer &) = delete;
Timer(Timer &&) noexcept = default;
Timer& operator=(Timer &&) noexcept = default;
bool isStart(void) const {
return nullptr != _handle;
}
template <typename F, typename ...Args>
bool startOnce(const std::chrono::system_clock::duration &duration, F &&f, Args &&...args) {
stop();
std::lock_guard<std::mutex> lock(_mutex);
_mode = TimerMode::ONCE;
_callback = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
esp_timer_create_args_t onceArg = {
.callback = defaultTimerCallback,
.arg = this,
};
esp_timer_create(&onceArg, &_handle);
return esp_timer_start_once(_handle, std::chrono::duration_cast<std::chrono::microseconds>(duration).count()) == ESP_OK;
}
template <typename F ,typename ...Args>
bool startPeriod(const std::chrono::system_clock::duration &duration, F &&f, Args &&...args) {
stop();
std::lock_guard<std::mutex> lock(_mutex);
_mode = TimerMode::PERIOD;
_callback = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
esp_timer_create_args_t periodArg = {
.callback = defaultTimerCallback,
.arg = this,
};
esp_timer_create(&periodArg, &_handle);
return esp_timer_start_periodic(_handle, std::chrono::duration_cast<std::chrono::microseconds>(duration).count()) == ESP_OK;
}
void stop(void) {
std::lock_guard<std::mutex> lock(_mutex);
if (_handle) {
esp_timer_stop(_handle);
esp_timer_delete(_handle);
_handle = nullptr;
}
}
~Timer() {
stop();
}
};
四、验证
在main目录的CMakeLists.txt里包含我们刚写好的timer.hpp的路径
在app_main函数中写下如下验证代码
编译并下载到板子中,查看打印,确实和我们想的一致。
main.cpp的源码如下
#include <iostream>
#include "can.hpp"
#include <thread>
#include <chrono>
#include "timer.hpp"
int print(const char *s) {
std::cout << s << std::endl;
return 0;
}
extern "C" auto app_main() {
using namespace std::chrono_literals;
auto timer = new Timer();
timer->startOnce(1s, print, "hello,world 111\n");
std::this_thread::sleep_for(2s);
std::cout << "timer is run ? " << std::boolalpha << timer->isStart() << std::endl;
timer->stop();
std::cout << "timer is run ? " << std::boolalpha << timer->isStart() << std::endl;
auto timer2 = new Timer();
timer2->startPeriod(2s, []{std::cout << "wo shi ni die" << std::endl;});
std::cout << "timer2 is run ? " << std::boolalpha << timer2->isStart() << std::endl;
std::this_thread::sleep_for(5s);
timer2->stop();
std::cout << "timer2 is run ? " << std::boolalpha << timer2->isStart() << std::endl;
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;
delete timer;
delete timer2;
}
五、总结
本文使用esp32封装了一个定时器,实际验证可以运行,下一步移植canopen(canfestival)协议栈。