前言
本文是自己最近整理所学的注册函数的总结,因此并非从最基础的指针
函数指针
回调函数
注册函数
的顺序进行讲解。
因为对于前面的概念有很多人写了很多很好的文章进行参考,写太多难免有一些重复的感觉,本人也感觉很无趣,因此本文仅对注册函数
进行一些详细而深刻的说明。在文章的最开始介绍一些其他的相似文章可以进行预习。
注册回调函数实现方法_ 世外桃源的博客-CSDN博客_注册回调函数
C语言 - 注册函数、回调函数(callback)以及多态的实现_Steven&Aileen的博客-CSDN博客_注册回调函数
1 注册函数的意义(中间层函数)
首先得考虑什么时候用注册函数,为什么要用注册函数?
首先处理大量相似任务的时候才会用到注册函数!
想象一下如果你有一个项目,有10个相同的传感器接口,但这些传感器不一定每次都用,有可能A项目需要使用第1、3、5,B地使用其他的传感器1、2、3、4、6,此时你该怎么设计程序?
如果设计一个GUI,其中包含很多的page,有的项目一些界面需要用,而其它项目不需要,此时你该如何设计你的程序?
and so on…
此时是不是特别希望有一个管理者,一个
牢头典狱长
。帮你管理你的传感器
page
,这样的话你想用谁的时候就让他出列
,然后去干活。这个被管理
·出列
的过程就是注册
。这个注册更多的是一个中间层,管理底层驱动与上层应用之间的关系。
2 注册对象是什么?(结构体/类)
在注册之前我们应该先明确要注册的对象是什么?在编程语言中应该如何表示?
通过刚才的例子应该很明显的感觉到要进行注册的个体就是前面所说的传感器和page界面,下面我们分别用c/cpp完成传感器和page界面的表示与操作。
1️⃣ 在c语言中一般通过结构体的概念来表达注册的个体,而结构体中的成员无非基础数据类型和函数两种;对于传感器而言比较简单,我们在本例中仅通过三个成员对传感器结构体进行表示。
typedef struct senser_op{
char name[10];
int data;
int (*Read_sensor)(void);
}sensor_Type;
2️⃣ 在cpp语言中一般通过类来进行注册对象的说明,如cpp中对page进行说明。
class PageManager {
typedef void(*CallbackFunction_t)(void);
typedef void(*EventFunction_t)(void*,int);
typedef struct {
CallbackFunction_t SetupCallback;
CallbackFunction_t LoopCallback;
CallbackFunction_t ExitCallback;
EventFunction_t EventCallback;
} PageList_TypeDef;/*其实还是这个结构体是核心*/
public:
PageManager(uint8_t pageMax, uint8_t pageStackSize = 10);
~PageManager();
...
private:
...
};
3 注册对象编号(枚举)
管理之前先给传感器
page
进行编号处理,方便进行后续的管理,相当于出去干活之前先给这些注册对象分配代号,从此换个统一点的名字方便管理。这个工作在c/cpp中通过枚举的方式完成,如下所述:
typedef enum
{
/*保留*/
PAGE_NONE,
/*用户页面*/
PAGE_DialPlate,
PAGE_MainMenu,
PAGE_TimeCfg,
PAGE_Backlight,
PAGE_StopWatch,
PAGE_Altitude,
PAGE_About,
PAGE_Game,
/*保留*/
PAGE_MAX
} Page_Type;
4 注册的过程(数组/链表)
正如我们前面所说的那样,注册的过程就是让一些人出列,这些人在本工程中是被选定干活的人。这个出列的最终队列一般是通过数组或链表的数据结构完成。
数组
的注册过程如下所述:
/**
* @brief 注册一个基本页面,包含一个初始化函数,循环函数,退出函数,事件函数
* @param pageID: 页面编号
* @param setupCallback: 初始化函数回调
* @param loopCallback: 循环函数回调
* @param exitCallback: 退出函数回调
* @param eventCallback: 事件函数回调
* @retval true:成功 false:失败
*/
bool PageManager::PageRegister(
uint8_t pageID,
CallbackFunction_t setupCallback,
CallbackFunction_t loopCallback,
CallbackFunction_t exitCallback,
EventFunction_t eventCallback
)
{
if(!IS_PAGE(pageID))
return false;
PageList[pageID].SetupCallback = setupCallback;
PageList[pageID].LoopCallback = loopCallback;
PageList[pageID].ExitCallback = exitCallback;
PageList[pageID].EventCallback = eventCallback;
return true;
}
结构体
的注册过程如下所述:
/**
* @brief 往任务链表添加一个任务,设定间隔执行时间
* @param func:任务函数指针
* @param timeMs:周期时间设定(毫秒)
* @param state:任务开关
* @retval 任务节点地址
*/
MillisTaskManager::Task_t* MillisTaskManager::Register(TaskFunction_t func, uint32_t timeMs, bool state)
{
/*寻找当前函数*/
Task_t* task = Find(func);
/*如果被注册*/
if(task != NULL)
{
/*更新信息*/
task->Time = timeMs;
task->State = state;
return task;
}
/*为新任务申请内存*/
TASK_NEW(task);
/*是否申请成功*/
if(task == NULL)
{
return NULL;
}
task->Function = func; //任务回调函数
task->Time = timeMs; //任务执行周期
task->State = state; //任务状态
task->TimePrev = 0; //上一次时间
task->TimeCost = 0; //时间开销
task->TimeError = 0; //误差时间
task->Next = NULL; //下一个节点
/*如果任务链表为空*/
if(Head == NULL)
{
/*将当前任务作为链表的头*/
Head = task;
}
else
{
/*从任务链表尾部添加任务*/
Tail->Next = task;
}
/*将当前任务作为链表的尾*/
Tail = task;
return task;
}
为了进行进一步说明,本文贴出了find函数
的源码进行参考,归根结底就是链表增删查改
那一套,具体到下面的find函数就是根据链表节点的函数
寻找链表节点
的过程。链表的具体操作可以参考我以前的一篇博客: C语言链表:万字“链表”从概念开始到实际应用的“超详细”保姆指南(如果看不懂直接评论指出来
/**
* @brief 寻找任务,返回任务节点
* @param func:任务函数指针
* @retval 任务节点地址
*/
MillisTaskManager::Task_t* MillisTaskManager::Find(TaskFunction_t func)
{
Task_t* now = Head;
Task_t* task = NULL;
while(true)
{
if(now == NULL)//当前节点是否为空
break;
if(now->Function == func)//判断函数地址是否相等
{
task = now;
break;
}
now = now->Next;//移动到下一个节点
}
return task;
}
进一步对task的注册对象(相当前面的page)进行说明:
class MillisTaskManager
{
public:
typedef void(*TaskFunction_t)(void);//任务回调函数
struct Task
{
bool State; //任务状态
TaskFunction_t Function; //任务函数指针
uint32_t Time; //任务时间
uint32_t TimePrev; //任务上一次触发时间
uint32_t TimeCost; //任务时间开销(us)
uint32_t TimeError; //误差时间
struct Task* Next; //下一个节点
};
typedef struct Task Task_t;//任务类型定义
MillisTaskManager(bool priorityEnable = false);
~MillisTaskManager();
Task_t* Register(TaskFunction_t func, uint32_t timeMs, bool state = true);
Task_t* Find(TaskFunction_t func);
Task_t* GetPrev(Task_t* task);
bool Logout(TaskFunction_t func);
bool SetState(TaskFunction_t func, bool state);
bool SetIntervalTime(TaskFunction_t func, uint32_t timeMs);
uint32_t GetTimeCost(TaskFunction_t func);
uint32_t GetTickElaps(uint32_t nowTick, uint32_t prevTick);
#if (MTM_USE_CPU_USAGE == 1)
float GetCPU_Usage();
#endif
void Running(uint32_t tick);
private:
Task_t* Head; //任务链表头
Task_t* Tail; //任务链表尾
bool PriorityEnable; //优先级使能
};
5 注册函数的使用
5.1 任务调度器(链表)使用示例
cpp中就是类的声明,在声明时会进行构造函数的初始化,从而初始化很多需要初始化的内容:
<main.cpp>
static MillisTaskManager mtmMain;
相应的构造函数(尤其要初始化与之相关的链表和数组)与析构函数如下所示:
#define TASK_NEW(task) do{task = new Task_t;}while(0)
#define TASK_DEL(task) do{delete task;}while(0)
/**
* @brief 初始化任务列表
* @param priorityEnable:设定是否开启优先级
* @retval 无
*/
MillisTaskManager::MillisTaskManager(bool priorityEnable)
{
PriorityEnable = priorityEnable;
Head = NULL;
Tail = NULL;
}
/**
* @brief 调度器析构,释放任务链表内存
* @param 无
* @retval 无
*/
MillisTaskManager::~MillisTaskManager()
{
/*移动到链表头*/
Task_t* now = Head;
while(true)
{
/*当前节点是否为空*/
if(now == NULL)
break;
/*将当前节点缓存,等待删除*/
Task_t* now_del = now;
/*移动到下一个节点*/
now = now->Next;
/*删除当前节点内存*/
TASK_DEL(now_del);
}
}
接下来就是要进行注册的过程了,即出列一些任务进入到链表中:
/*任务注册*/
mtmMain.Register(Display_Update, 1); //屏幕刷新
mtmMain.Register(Button_Update, 10); //按键事件监控
mtmMain.Register(Power_AutoShutdownUpdate, 100); //自动关机监控
mtmMain.Register(CPU_UsageUpdate, 1000); //CPU占用率监控
然后注册完就应该通过一系列的操作驱动运行
该链表,只不过该任务以时间
为驱动,而且运行函数中也没有相应的链表节点的删除
增添
的操作,各个注册进去的任务只能让任务顺序执行:
mtmMain.Running(millis());
/**
* @brief 调度器(内核),调度的逻辑简单讲述就是如果该任务剩余时间为空的话,就执行该任务中的\
\函数,若时间未到,则区判断下一个节点的剩余时间
* @param tick:提供一个精确到毫秒的系统时钟变量
* @retval 无
*/
void MillisTaskManager::Running(uint32_t tick)
{
Task_t* now = Head;
while(true)
{
/*当前节点是否为空*/
if(now == NULL)
{
/*遍历结束*/
break;
}
if(now->Function != NULL && now->State)
{
uint32_t elapsTime = GetTickElaps(tick, now->TimePrev);
if(elapsTime >= now->Time)
{
/*获取时间误差,误差越大实时性越差*/
now->TimeError = elapsTime - now->Time;
/*记录时间点*/
now->TimePrev = tick;
#if (MTM_USE_CPU_USAGE == 1)
/*记录开始时间*/
uint32_t start = micros();
/*执行任务*/
now->Function();
/*获取执行时间*/
uint32_t timeCost = micros() - start;
/*记录执行时间*/
now->TimeCost = timeCost;
/*总时间累加*/
UserFuncLoopUs += timeCost;
#else
now->Function();
#endif
/*判断是否开启优先级*/
if(PriorityEnable)
{
/*遍历结束*/
break;
}
}
}
/*移动到下一个节点*/
now = now->Next;
}
}
5.2 任务调度器(数组)使用示例
首先page初始化:
/*实例化页面调度器,注意这个后面的括号不是代表数组的意思,而是代表一个构造函数,PAGE_MAX是函数的参数,具体看下面class的定义*/
PageManager page(PAGE_MAX);
类里面有一个数组进行管理:
class PageManager {
typedef void(*CallbackFunction_t)(void);
typedef void(*EventFunction_t)(void*,int);
typedef struct {
CallbackFunction_t SetupCallback;
CallbackFunction_t LoopCallback;
CallbackFunction_t ExitCallback;
EventFunction_t EventCallback;
} PageList_TypeDef;
public:
PageManager(uint8_t pageMax, uint8_t pageStackSize = 10);//构造函数
~PageManager();
。。。
bool PageRegister(
uint8_t pageID,
CallbackFunction_t setupCallback,
CallbackFunction_t loopCallback,
CallbackFunction_t exitCallback,
EventFunction_t eventCallback
);
。。。
private:
PageList_TypeDef* PageList; //管理的队列
。。。
};
初始化的过程中进行注册:
/*页面注册器,*/
#define PAGE_REG(name)\
do{\
extern void PageRegister_##name(uint8_t pageID);\
PageRegister_##name(PAGE_##name);\
}while(0)
/**
* @brief 页面初始化
* @param 无
* @retval 无
*/
static void Pages_Init()
{
PAGE_REG(DialPlate); //表盘
。。。
page.PagePush(PAGE_DialPlate);//打开表盘
}
注册的过程如下所述,就是在Pagelist列表中将每个pageID的各个函数进行对应:
/**
* @brief 注册一个基本页面,包含一个初始化函数,循环函数,退出函数,事件函数
* @param pageID: 页面编号
* @param setupCallback: 初始化函数回调
* @param loopCallback: 循环函数回调
* @param exitCallback: 退出函数回调
* @param eventCallback: 事件函数回调
* @retval true:成功 false:失败
*/
bool PageManager::PageRegister(
uint8_t pageID,
CallbackFunction_t setupCallback,
CallbackFunction_t loopCallback,
CallbackFunction_t exitCallback,
EventFunction_t eventCallback
)
{
if(!IS_PAGE(pageID))
return false;
PageList[pageID].SetupCallback = setupCallback;
PageList[pageID].LoopCallback = loopCallback;
PageList[pageID].ExitCallback = exitCallback;
PageList[pageID].EventCallback = eventCallback;
return true;
}
然就发现在前面init函数中最后一步执行的是Push压栈,具体函数如下所示:
/**
* @brief 页面压栈,跳转至该页面
* @param pageID: 页面编号
* @retval true:成功 false:失败
*/
bool PageManager::PagePush(uint8_t pageID)
{
if(!IS_PAGE(pageID))
return false;
/*检查页面是否忙碌*/
if(IsPageBusy)
return false;
/*防止栈溢出*/
if(PageStackTop >= PageStackSize - 1)
return false;
/*防止重复页面压栈*/
if(pageID == PageStack[PageStackTop])
return false;
/*栈顶指针上移*/
PageStackTop++;
/*页面压栈*/
PageStack[PageStackTop] = pageID;
/*页面跳转,在此处进行跳转工作,开始运行*/
PageChangeTo(PageStack[PageStackTop]);
return true;
}
最后注册函数的运行与切换是通过事件进行驱动的,相应的事件处理函数如下所示,本节展示的是当按键按下之后进行页面切换的操作:
/**
* @brief 页面事件
* @param btn:发出事件的按键
* @param event:事件编号
* @retval 无
*/
static void Event(void* btn, int event)
{
/*当有按键点击或长按时*/
if(event == ButtonEvent::EVENT_ButtonClick || event == ButtonEvent::EVENT_ButtonLongPressed)
{
/*进入主菜单*/
page.PagePush(PAGE_MainMenu);
}
}
参考致谢
本文绝大多数代码参考开源代码WatchX:https://github.com/FASTSHIFT/WatchX,衷心感谢作者的开源精神