A Simple Event Scheduler Using a Circular Task Queue
- 多实例支持:不透明句柄模式,可同时创建多个独立的调度器实例
- 双模式任务管理:静态数组模式(编译期确定容量)和动态分配模式(运行时弹性扩展)
- 依赖注入:内存分配器通过回调注入,核心模块不依赖
stdlib.h(仅保留memset/memcpy) - 入侵式链表:基于 Linux 内核风格的
c-linked-list,类型安全、零额外分配 - TMOS 友好:非阻塞
TmosPoll接口适用于协作式多任务调度(无中断保存上下文) - 事件区间校验:任务支持连续区间
[start, end)或回绕区间start > end - 内置时基:注入 tick 源即可获得 1ms 心跳广播(
EVT_TICK)与软件定时器(单次/周期),无需应用侧手写 tick 广播样板
┌─────────────────────────────────────────────┐
│ main.c │
│ ┌─────────┐ ┌────────────────────────┐ │
│ │ MGP │──▶│ EventSchedul_Allocator│ │
│ │ 内存池 │ │ {malloc, free} │ │
│ └─────────┘ └───────────┬────────────┘ │
├──────────────────────────────┼──────────────┤
│ EventSchedul │
│ ┌──────────────┐ ┌───────┴───────────┐ │
│ │ taskPool[] │ │ ringTaskQueue │ │
│ │ (静态数组) │ │ (环形缓冲区) │ │
│ └──────────────┘ └───────────────────┘ │
│ ┌──────────────────────────────────────┐ │
│ │ c-linked-list │ │
│ │ (入侵式双向链表,无堆分配) │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
#include "EventSchedul.h"
/* 任务回调 */
static void myTask(EventSchedul_EventId evt, void* arg) {
printf("Received event: 0x%04x\n", evt);
}
int main(void) {
/* 创建调度器实例,注入 malloc/free */
EventSchedul_Context* ctx = EventSchedul_Create(
&(EventSchedul_Allocator){malloc, free});
/* 注册任务(监听事件区间 [0x0001, 0x0010)) */
EventSchedul_TaskNode cfg = {
.pTaskFunc = myTask,
.pTaskFuncArg = NULL,
.info = { .eventStart = 0x0001, .eventEnd = 0x0010 }
};
EventSchedul_TaskNode* handle = EventSchedul_TaskRegister(ctx, &cfg);
/* 投递事件 */
EventSchedul_setEventToTask(ctx, handle, 0x0005);
/* TMOS 非阻塞轮询 */
EventSchedul_TmosPoll(ctx); // → myTask(0x0005, NULL)
/* 清理 */
EventSchedul_TaskUnRegister(ctx, handle);
EventSchedul_Destroy(ctx);
return 0;
}调度器内置时基,把"SysTick 心跳 → EVT_TICK 广播 → 软件定时器"整合进组件,
应用不再需要手写 tick 广播样板(参考门禁 tickBroadcast / 键盘 idle 钩子)。
#include "EventSchedul.h"
static volatile EventSchedul_TickCount g_tick_ms = 0; /* SysTick ISR 递增 */
static EventSchedul_TickCount sysTick_getTick(void) { return g_tick_ms; }
/* 订阅 EVT_TICK: 事件区间包含 EVTSCHEDUL_TICK_EVT 即订阅时基广播 */
static void onTick(EventSchedul_EventId evt, void* arg) {
/* evt == EVTSCHEDUL_TICK_EVT(1), 每 tick 调用一次 */
}
int main(void) {
EventSchedul_Context* ctx = EventSchedul_Create(
&(EventSchedul_Allocator){malloc, free});
EventSchedul_RegTickSource(ctx, sysTick_getTick); /* 注入 tick 源 */
EventSchedul_TaskNode cfg = {
.pTaskFunc = onTick,
.info = { .eventStart = EVTSCHEDUL_TICK_EVT,
.eventEnd = EVTSCHEDUL_TICK_EVT + 1 } /* 订阅广播 */
};
EventSchedul_TaskNode* h = EventSchedul_TaskRegister(ctx, &cfg);
/* 软件定时器内嵌于任务节点(每任务一个):
* 单次 100 tick 后向 h 投 0x0100; 周期 500 tick 投 0x0200(须分挂不同任务) */
EventSchedul_TimerStart(ctx, h, 0x0100, 100);
...
EventSchedul_TimerStop(ctx, h);
}- ISR 安全:遵循"中断只置标志"原则——tick 计数器由 SysTick ISR 递增,
调度器只在
TmosPoll/MainLoop主循环上下文中读取并广播,无需加锁。 - 订阅即区间:任务的事件区间含
EVTSCHEDUL_TICK_EVT(1)即订阅 EVT_TICK, 无需额外订阅表。 - 绝对 deadline:定时器基于 tick 源绝对计数,主循环卡顿漏检仍能按期触发。
- 每任务一个定时器:定时器内嵌于任务节点(不透明指针,计数状态私有), 容量天然 == 任务数;重复启动同一任务定时器会覆盖旧定时器。
- 周期保相位:周期定时器到期后推进到下一个绝对触发点,即使主循环卡顿跨过 多个相位,也每轮只触发一次(不突发),且下次触发仍对齐绝对相位(不漂移)。
时基靠主循环轮询检测推进,若某个任务回调执行时间较长,会使各轮检测到的 tick 差非均等。本组件对这类场景的行为与对策:
| 场景 | 行为 | 对策 |
|---|---|---|
| 单次定时器 | 绝对 deadline,延迟但不错过 | 无需处理 |
| 周期定时器 | 每轮最多触发一次,保相位不漂移 | 无需处理(不突发、不积累) |
| EVT_TICK 计数 | 每轮最多广播一次,长任务下"数 EVT_TICK"会少算 | 改用绝对 tick 分频(见下) |
| 队列满 | 入队失败静默 | 保持任务回调尽量短 |
EVT_TICK 只是唤醒触发,不是精确计数器。需要精确计时的任务应改用
EventSchedul_GetTick() 的绝对计数做分频/超时,即使一轮跨过多个 tick 也准确:
static void task(EventSchedul_EventId evt, void* arg) {
EventSchedul_Context* ctx = (EventSchedul_Context*)arg;
static EventSchedul_TickCount last = 0;
EventSchedul_TickCount now = EventSchedul_GetTick(ctx);
if (now - last >= 10) { /* 每 10 tick 处理一次, 卡顿不丢 */
last = now;
/* ... */
}
}边界说明:主循环卡顿期间任务本身无法被唤醒,做不到"卡顿时立刻响应"; 但一旦唤醒,
GetTick能精确告知流逝了多少 tick。如需严格实时(硬期限 抢占),需改为 RTOS/中断驱动调度,超出本协作式调度组件的范畴。
| 函数 | 说明 |
|---|---|
EventSchedul_Create(allocator) |
创建调度器实例,注入内存分配器 |
EventSchedul_Destroy(ctx) |
销毁实例,释放所有资源 |
EventSchedul_TaskRegister(ctx, cfg) |
注册任务,返回句柄 |
EventSchedul_TaskUnRegister(ctx, handle) |
注销任务 |
EventSchedul_setEventToTask(ctx, handle, evt) |
向任务投递事件(入队) |
EventSchedul_TmosPoll(ctx) |
非阻塞取一个事件并执行(TMOS 适用) |
EventSchedul_MainLoop(ctx) |
阻塞式主循环(需先 RegSleepMethod) |
EventSchedul_RegSleepMethod(ctx, fn) |
注册阻塞主循环的休眠回调 |
EventSchedul_RegTickSource(ctx, getTick) |
注入 tick 源回调(NULL 注销时基) |
EventSchedul_GetTick(ctx) |
获取当前 tick 计数 |
EventSchedul_TimerStart(ctx, task, evt, delay) |
单次定时器:delay tick 后向 task 投 evt,返回错误码 |
EventSchedul_TimerStartPeriodic(ctx, task, evt, period) |
周期定时器:每 period tick 向 task 投 evt,返回错误码 |
EventSchedul_TimerStop(ctx, task) |
停止 task 的内嵌定时器(幂等) |
typedef enum {
EVTSCHEDUL_OK = 0, // 成功
EVTSCHEDUL_ERR_FAIL, // 通用失败
EVTSCHEDUL_ERR_ARG, // 参数错误
EVTSCHEDUL_ERR_MEM, // 内存不足
EVTSCHEDUL_ERR_NOTHING, // 无数据
EVTSCHEDUL_ERR_EVENT, // 事件错误(不在区间内)
EVTSCHEDUL_ERR_TASK, // 任务错误(未注册)
} EventSchedul_ErrCode;| 类型 | 底层 | 说明 |
|---|---|---|
EventSchedul_EventId |
unsigned short |
事件编号 (0x0000–0xFFFF) |
EventSchedul_TaskId |
short |
任务编号 |
EventSchedul_ExecCount |
unsigned short |
执行次数计数器 |
EventSchedul_Allocator |
{malloc_fn, free_fn} |
注入的内存分配器 |
EventSchedul_TickCount |
uint32_t |
tick 计数(由应用定义 tick→时间映射) |
EventSchedul_Timer |
不透明 | 内嵌于任务节点的软件定时器(计数状态私有) |
宏定义位于 include/EventSchedul.h:
| 宏 | 默认值 | 说明 |
|---|---|---|
EVTSCHEDUL_TASKS_MODE |
EVTSCHEDUL_STATIC |
任务管理模式:EVTSCHEDUL_STATIC(0) / EVTSCHEDUL_DYNAMIC(1);ll_t 仅动态模式使用 |
EVTSCHEDUL_TASKS_MAX |
8 |
静态模式最大任务数 |
EVTSCHEDUL_TASKS_QUEUE_MAX |
EVTSCHEDUL_TASKS_MAX * 2 |
环形事件队列深度 |
EVTSCHEDUL_TIME_BASE_ENABLE |
1 |
内置时基开关(0=全部编译剔除,行为与旧版一致) |
EVTSCHEDUL_TICK_EVT |
1 |
保留时基事件 ID(EVT_TICK,可 -D 覆盖) |
EVTSCHEDUL_INVALID_EVT |
0xFFFF |
无效事件号 |
EVTSCHEDUL_INVALID_TASK_ID |
-1 |
无效任务编号 |
定时器容量恒等于任务数(每任务内嵌一个),无独立定时器池配置宏。
mkdir build && cd build
cmake .. -G Ninja # 或用 -G "MinGW Makefiles"
cmake --build .产物在 build/bin/EventScheduling.exe。
.\build\bin\EventScheduling.exe内置 15 组测试用例,覆盖所有公共 API(121 个断言)。
| 库 | 仓库 | 用途 |
|---|---|---|
| ringBuffer | PING020903/ringBuffer | 环形 FIFO 缓冲区,用于事件队列 |
| MemoryGroundPlus | PING020903/MemoryGroundPlus | 内存池,动态模式下替代 malloc |
| c-linked-list | embeddedartistry/c-linked-list | Linux 内核风格入侵式双向链表 |
EventScheduling/
├── include/
│ ├── EventSchedul.h # 公共头文件
│ └── DBG_macro.h # 调试宏
├── sources/
│ ├── EventSchedul.c # 调度器核心实现
│ ├── main.c # 示例入口
│ └── test.c # API 测试套件
├── thirdparty/
│ ├── ringBuffer/ # 环形缓冲区
│ ├── memoryGroundPlus/ # 内存池
│ └── c-linked-list/ # 入侵式链表
├── CmakeLists.txt
└── README.md