# cotick **Repository Path**: Jumping99/cotick ## Basic Information - **Project Name**: cotick - **Description**: 一个适用于资源受限 C/C++ 项目的轻量级协作式多任务(协程)轮询框架。无需 RTOS — 仅需一个滴答计数器,加上基于 Duff's Device 实现的协程即可。 - **Primary Language**: C - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 3 - **Forks**: 1 - **Created**: 2026-09-18 - **Last Updated**: 2026-09-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # cotick(轻量级协作式多任务(协程)轮询框架) 一个适用于资源受限 C/C++ 项目的**轻量级协作式多任务(协程)轮询框架**。 无需 RTOS — 仅需一个滴答计数器,加上基于 Duff's Device 实现的协程即可。 --- ## 特性 - **无栈协程(Stackless Coroutine)** — 通过 `switch-case`(Duff's Device)纯 C 实现,无需汇编,无需堆分配。 - **协作式调度** — 任务主动让出(yield)/休眠(sleep);调度器以轮询方式运行。 - **基于滴答的定时** — 毫秒级分辨率滴答计数器,安全处理 `UINT32_MAX` 溢出。 - **任务状态模型** — 任务具有明确状态:`READY`、`RUNNING`、`PAUSED`、`SLEEP`、`COROUTINE_YIELD`、`COROUTINE_AWAIT_FLAG`。 - **标志等待(Flag Await)** — 协程可等待某个标志被置位(生产者-消费者模式)。 - **双向链表** — 通过侵入式链表实现 O(1) 的任务插入/移除。 - **双类型系统** — 默认使用 `stdint.h`,兼容模式(定义 `COTICK_USE_LEGACY`)使用自定义整数类型,适配裸机编译器。 --- ## 架构 ``` ┌──────────────────────────────────────────────────┐ │ cotick_init() │ │ (初始化任务链表和系统滴答) │ └──────────────────┬───────────────────────────────┘ │ ┌─────────────▼─────────────┐ │ cotick_tick_inc(ms) │ ← 由硬件定时器 ISR 调用 │ (递增系统滴答) │ └─────────────┬─────────────┘ │ ┌─────────────▼─────────────┐ │ cotick_task_handler() │ ← 在主循环中调用 │ (遍历并执行任务) │ └─────────────┬─────────────┘ │ ┌─────────────▼───────────────────────────────┐ │ task_list(双向链表) │ │ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │ │ │ 任务1 │◄►│ 任务2 │◄►│ 任务3 ... │ │ │ └─────────┘ └─────────┘ └─────────────┘ │ └─────────────────────────────────────────────┘ ``` 每个 `cotick_task_t` 包含: | 字段 | 说明 | |-------------------|----------------------------------------------| | `list_node` | 侵入式双向链表节点(prev/next 指针) | | `task_function` | 函数指针 — 任务体 | | `last_run_tick` | 任务上次运行时的系统滴答 | | `sleep_tick` | 任务需要休眠的毫秒数 | | `status` | 当前任务状态(见上方的状态枚举) | | `coroutine_frame` | 协程帧,保存协程状态和等待标志指针 | --- ## API 参考 ### 初始化 | 函数 | 描述 | |---------------------|--------------------------------| | `cotick_init()` | 初始化框架(必须最先调用) | | `cotick_tick_inc(n)`| 将系统滴答递增 `n` 毫秒 | ### 时间 | 函数 | 描述 | |-----------------------------------|------------------------------------------| | `cotick_get_millisec()` | 获取当前系统滴答(毫秒) | | `cotick_millisec_elaps(prev)` | 自 `prev` 以来经过的毫秒数(溢出安全) | ### 任务管理 | 函数 | 描述 | |----------------------------------------------------|-------------------------------------------------| | `cotick_task_create(task, func, frame)` | 注册一个任务(非协程任务传 `NULL` 作为 frame) | | `cotick_task_pause(task)` | 暂停任务(参数为 `NULL` 时暂停当前任务) | | `cotick_task_unpause(task)` | 恢复任务 | | `cotick_task_set_ready_delay(task, ms)` | 延迟 `ms` 毫秒后再次运行(仅非协程任务) | | `cotick_task_get_current()` | 获取当前正在运行的任务 | | `cotick_task_handler()` | 执行一次调度迭代(在循环中调用) | | `cotick_resume(task)` | 恢复一个被 yielded 的协程任务 | ### 协程宏 | 宏 | 描述 | |-------------------------------------|--------------------------------------------------------| | `cotick_begin()` | 每个协程任务函数的开头必须调用 | | `cotick_end()` | 每个协程任务函数的结尾必须调用 | | `cotick_sleep(ms)` | 挂起当前协程 `ms` 毫秒 | | `cotick_yield()` | 让出执行权给其他任务 | | `cotick_await_flag(flag)` | 等待(永久)直到 `flag` 变为非零 | | `cotick_await_flag_timeout(f, ms)` | 等待 `flag` 并设置超时时间 | | `cotick_no_coroutine_return()` | 从非协程任务返回(任务保持存活) | --- ## 快速开始 ### 构建 ```bash # 配置 cmake -B build -G "MinGW Makefiles" # 或其他你偏好的生成器 # 编译 cmake --build build # 运行示例 ./build/cotick_test.exe ``` ### 最小使用示例 ```c #include "cotick.h" // 1. 定义一个协程任务 static cotick_task_t my_task; static cotick_coroutine_frame_t my_frame; static cotick_task_result_t my_task_func(cotick_task_t *task) { cotick_begin(); while (1) { do_something(); cotick_sleep(100); // 休眠 100 毫秒 do_something_else(); } cotick_end(); } int main() { cotick_init(); cotick_task_create(&my_task, my_task_func, &my_frame); while (1) { cotick_tick_inc(1); // 1 毫秒已过去(实际使用中由 ISR 调用) cotick_task_handler(); // 运行调度器 } } ``` ### 任务模式 附带的 `main.cpp` 展示了五种模式: 1. **协程 + Yield** — 使用 `cotick_yield()` 在不同步骤之间交替执行 2. **协程 + Sleep** — 使用 `cotick_sleep(ms)` 实现定时延迟 3. **非协程任务** — 普通回调函数,可恢复其他任务;使用 `cotick_no_coroutine_return()` 返回 4. **协程 + Await Flag** — 使用 `cotick_await_flag()` 阻塞直到条件满足 5. **协程 + 自定义帧** — 扩展 `cotick_coroutine_frame_t` 添加每任务独立状态(如按键计数) --- ## 兼容性 - **标准模式**(默认):需要 `stdint.h` + `stddef.h` — 适用于 C11/C++17 及以上。 - **兼容模式**:在包含头文件前定义 `COTICK_USE_LEGACY` — 使用自定义整数类型和宏实现链表操作,适用于旧版/C89 编译器。 --- ## 注意事项 ### 1.作用域跨协程操作宏的变量处理 在协程区域内部定义,且作用域跨协程操作宏的变量,必须移动到协程帧中,否则编译会报错。 与C++20的协程不同,C++编译器会自动将作用域跨协程操作关键字的变量自动放入协程帧中。 而`cotick`则需要手动将变量放入协程帧中, 因为没有编译器兜底。 ```c static cotick_task_result_t task_function(cotick_task_t *p_task) { // 获取当前任务的协程帧 custom_task_frame_t *f = (custom_task_frame_t *)p_task->coroutine_frame; cotick_begin(); // 协程区域开始 // int i = 0; // i的作用域为此行到函数结尾,中间跨协程操作宏,必须放入协程帧中 while(1) { // i++; // 错误 f->i++; cotick_sleep(500); } cotick_end(); // 协程区域结束 } ``` 还有一种情况,虽然编译不会报错,但运行时会出现异常。 ```c static cotick_task_result_t task_function(cotick_task_t *p_task) { int i = 0; cotick_begin(); // 协程区域开始 while(1) { ++i; // i = 1 cotick_sleep(500); // 此处会导致函数返回 ++i; // i = 1 下次进来,i并非上次的值 + 1,而是从0开始计数 } cotick_end(); // 协程区域结束 } ``` 这种情况属于,变量状态依赖于协程操作之前的执行,为确保变量状态的连续性,必须将变量放入协程帧中。 ### 2.自定义协程帧 自定义协程帧可以用于存储任务的独立状态,如按键计数、对象状态等。 但自定义协程帧的结构第一个成员必须是 `cotick_coroutine_frame_t`,否则协程无法正常工作。 ```c struct task_coroutine_frame { cotick_coroutine_frame_t base; // base coroutine frame uint32_t press_count = 0; bool last_key_state; }; ``` ### 3.协程操作独占行数 协程操作宏 `cotick_sleep(ms)`、`cotick_yield()`、`cotick_await_flag(flag)`、`cotick_await_flag_timeout(f, ms)` 等,必须独占一行,否则会导致编译错误。 因为其内部是靠`__LINE__`宏来确保协程分割后的每一部分有唯一的id,以便`switch`块中进行跳转。 ```c cotick_sleep(100); cotick_sleep(100); // ❌ 同一物理行,case 标签冲突 ``` --- ## 许可证 MIT License