基于嵌入式中经典的前后台轮询,和pt,lw_coroutine 两位大佬开源的纯C语言实现协程的编程思想,有了CoTask这个开源的代码框架。
CoTask主要实现了以下功能:
在CoTask回调函数的代码中,可以通过COTASK_AWAIT_TICK() 这个宏进行非阻塞延时固定的TICK,且只要延时到期后,没有其他CoTask任务占用MCU运行的话,MCU会从延时的这个地方往后运行,直至回调函数结束。
回调函数运行结束后会重新运行该回调函数,直至这个CoTask的回调函数主动调用AWAIT 这���的接口函数才会主动让出MCU,运行其他CoTask的回调函数。
如果不想回调函数运行结束后重新运行的话,可以在回调函数中使用CoTask_Delete(p_cotask) 删除自己这个CoTask。这里的p_cotask 是回调函数的参数,传递进来的参数是CoTask自身的指针。
为了使CoTask更像协程,我还其中加入了信号量机制,用于不同协程间通信(需要在CoTask.h 中开启COTASK_SEMAPHORE_ENABLE 这个宏定义)。
当一个CoTask回调函数中调用 COTASK_AWAIT_SEM 或者 COTASK_AWAIT_SEM_UNTIL 这两个宏函数后,MCU会退出这个CoTask去执行其他CoTask,直到其他CoTask或者中断中有人调用 COTASK_RELEASE_SEM 释放这个信号量。当MCU空闲时,会逐个调用等待信号量的回调函数,和非阻塞延时一样,它从等待信号量处往后执行。
同样的,为了使CoTask更像协程,我在其中加入了MCU执行利用率的统计,通过 CoTask_GetUtilization 这个函数,可以得到一个float类型的返回值,表示当前这一时刻,MCU执行的利用率,如果某个CoTask中十分耗时的操作的,这个数值会变大,最大为1,表示当前MCU一直被某个CoTask占用着。
由于整个CoTask的实现很简单,全部(包括空行和注释也就260行代码),所以这里效仿了著名的单文件开源库nothings/stb: stb single-file public domain libraries for C/C++ 的实现方式。整个CoTask实现只需要一个CoTask.h 文件。
需要找一个.c文件作为CoTask的实现文件,这里建议直接在中断函数所在文件中实现,只需要在中断函数所在的.c文件中定义 COTASK_IMPLEMENT 这个宏,并包含CoTask.h 头文件,就算完成了 CoTask的实现。
接下来,需要在中断函数中定时调用 CoTask_Tick() 这个函数,为CoTask提供时基,并且需要根据提供时基的中断函数的中断频率,修改CoTask.h 中的FREQ_OF_TICK_COLCK 宏。
同时,需要在主循环中调用CoTask_Proc() 这个函数,用于处理信号量和非阻塞延时到期时的回调函数调用。
至此,移植完成,可以开始使用CoTask提供的接口函数了。下面是stm32 Hal库的移植例子截图:
顺便一提,这个配置,是可以在keil中打开可视化查看和配置的。如下图:
-
MS2TICK(ms):毫秒到TICK的换算。用于将延时的毫秒数转换为CoTask的tick单位。例如,MS2TICK(100)表示延时100ms对应的tick数。 -
COTASK_ENTER:进入每个CoTask回调函数域时需要调用。建议在回调函数的最开始处调用,用于保存当前任务的状态。 -
COTASK_EXIT:退出每个CoTask回调函数域时调用。建议在回调函数的最后调用,用于重置任务状态并返回。 -
COTASK_AWIAT_TICK(tick_of_delay):在CoTask回调函数中调用,实现非阻塞延时。会立即退出当前任务,并在指定tick数后重新从当前位置继续执行。 -
CoTask_Create(CoTask *ptask, CoTaskFun task_callback):创建一个CoTask任务,将任务插入任务列表。ptask为任务结构体指针,task_callback为任务回调函数。 -
CoTask_Delete(CoTask *ptask):删除一个CoTask任务,从任务列表移除。 -
CoTask_Proc(void):遍历所有任务,执行满足条件的任务回调函数。通常在主循环中调用。 -
CoTask_Tick(void):定时调用,为CoTask提供时基。建议在定时器或SysTick中断中调用。
-
COTASK_RELEASE_SEM(sem):在任何地方调用,用于释放一个信号量。 -
COTASK_AWAIT_SEM(sem):在CoTask回调函数中调用,等待一个信号量被释放。任务会挂起,直到信号量可用。 -
COTASK_AWAIT_SEM_UNTIL(sem, tick_of_delay):等待信号量并设置超时时间。超时后任务会继续执行。 -
COTASK_WAIT_SEM_IS_TIMEOUT(sem):检查等待信号量是否超时。
CoTask_GetUtilization(void):获取当前MCU执行CoTask的利用率,返回值为float类型,范围0~1。
使用建议:
- 每个CoTask任务建议使用静态变量保存状态,避免局部变量丢失。
- 不支持在CoTask回调函数中使用
switch-case语法。 - 任务优先级无法指定,采用头插法,后创建的任务优先执行。
| 优点 | 缺点 |
|---|---|
| 移植和实现简单 | 预编译后的文件可读性差(因为大部分接口都是宏) |
| 伪协程的接口能简化一部分编程思路 | 很多时候需要使用静态变量来代替局部变量 (参考案例中的for循环) |
| 整体代码量很小,占用小 | 不能在CoTask的回调函数中使用switch-case语法 |
| CoTask执行的优先级无法指定(因为列表使用头插法,晚插入的CoTask先执行) |

