轻量级裸机协作式任务调度框架
sloopLite 是一个专为资源受限型微控制器设计的轻量级嵌入式任务调度框架。它采用单线程协作式调度模型,提供了丰富的任务管理功能和协作式工作流编程能力,同时保持极低的资源消耗,非常适合资源受限的嵌入式应用开发。
- 超时任务:延迟指定时间后执行一次
- 周期任务:按照固定时间间隔重复执行
- 多次任务:执行指定次数的周期任务
- 并行任务:在主循环中并行执行
- 单次任务:只执行一次,适合中断中复杂逻辑下放
- 互斥任务:主要业务逻辑执行载体
- RTT 日志输出:基于 SEGGER RTT 的高效日志系统
- CPU 负载监测:实时计算和显示系统负载
- 系统心跳:定期输出系统状态
- 非阻塞等待:支持任务间的延时和同步
- 时间戳功能:提供系统时间获取接口
- 简化语法:基于宏定义的工作流编程框架
- 非阻塞等待:支持延时、条件和事件等待
- 状态管理:自动维护工作流生命周期
- 事件驱动:支持工作流间通信
- 线性结构:逻辑清晰易读,避免复杂状态机
主循环持续调用 sloop(),内部顺序执行:
- mutex_task_run():运行当前唯一主任务
- parallel_task_run():轮询所有并行任务
同时,tick 中断触发 soft_timer,驱动:
- 超时任务(一次性)
- 周期任务
- 多次任务
形成 "时间驱动 + 主流程" 的组合执行模型。
通过 SL_INIT/SL_FREE/SL_RUN 将任务拆成三个阶段:
- SL_INIT:首次进入执行一次(初始化资源、启动定时器等)
- SL_RUN:主逻辑区(循环执行)
- SL_FREE:任务切换时执行(释放资源)
任务切换由 sl_goto() 触发,本质是:
- 标记
sl_free - 下一轮执行
SL_FREE - 加载新任务(
sl_load_new_task) - 重新进入
SL_INIT
通过 sl_wait/sl_wait_bare 提供 "伪阻塞" 能力:
- 当前任务进入等待循环
- 内部持续执行并行任务
- 通过
sl_wait_break/continue进行唤醒
特点是:写法阻塞,但系统不阻塞。
Flow 是一个用宏构建的协作式工作流框架,将任务调度、状态机和同步原语统一在同一套语义下运行。每个 Flow 都是一个 "可挂起的执行体",通过 switch-case + 静态局部变量保存执行上下文,借助 __LINE__ 生成唯一状态,实现类似协程的断点续执行。
- Flow 状态:每个 Flow 都有一个状态变量,用于保存执行位置
- Flow 事件:用于 Flow 之间的通信
- Flow 上下文:每个 Flow 可以定义自己的静态变量,作为工作流的上下文
- FLOW_INIT:只执行一次,用于初始化上下文
- FLOW_RUN:主运行阶段,逻辑按 "线性代码" 展开
- FLOW_FREE:退出阶段,用于资源释放
外部通过 FLOW_START/FLOW_STOP 控制生命周期切换,内部也可用 FLOW_EXIT 主动结束。
非阻塞等待:
- FLOW_UNTIL:条件不满足时让出调度,满足后从断点继续
- FLOW_WAIT:基于时间的非阻塞等待
- FLOW_WAIT_EVENT:基于事件的非阻塞等待
线性同步写法:传统状态机需要拆成多个 state + 跳转,而这里可以用 "顺序代码" 表达复杂流程,逻辑更接近人脑思维路径,显著降低状态爆炸和可读性成本。
Flow工作流采用标准范式编写,包含上下文定义、初始化、释放和运行逻辑等部分:
void flow2(void)
{
/* 工作流上下文,工作流需要的数据在此静态定义 */
SL_FLOW_CONTEXT(flow2);
/* 初次进入工作流,执行一次,初始化工作流上下文 */
SL_FLOW_INIT;
/* 工作流结束,不再执行时,释放资源 */
SL_FLOW_FREE(flow2);
/* 下方开始进入工作流运行逻辑 */
SL_FLOW_RUN;
FLOW_UNTIL(var > 3);
FLOW_WAIT_EVENT(evt1);
FLOW_WAIT(2000);
SL_FLOW_END;
}Flow 框架提供了以下核心宏:
| 宏 | 描述 |
|---|---|
FLOW_STATE_DEFINE(flow_name) |
定义 Flow 状态变量 |
FLOW_EVENT_DEFINE(event_name) |
定义 Flow 事件变量 |
FLOW_START(flow_name) |
启动 Flow |
FLOW_STOP(flow_name) |
停止 Flow |
SL_FLOW_CONTEXT(flow_name) |
定义 Flow 上下文 |
SL_FLOW_INIT |
标记初始化代码 |
SL_FLOW_FREE(flow_name) |
标记释放代码 |
SL_FLOW_RUN |
标记运行逻辑 |
SL_FLOW_END |
标记 Flow 结束 |
FLOW_UNTIL(condition) |
等待条件满足 |
FLOW_WAIT(ms) |
非阻塞等待指定时间 |
FLOW_WAIT_EVENT(event_name) |
等待事件触发 |
FLOW_SEND_EVENT(event_name) |
发送事件 |
FLOW_EXIT() |
主动结束 Flow |
Flow 机制特别适合以下场景:
- 复杂业务逻辑:需要多个步骤、条件判断和等待的复杂业务流程
- 状态管理:需要维护状态的应用,如状态机、协议处理等
- 事件驱动:基于事件的异步处理
- 资源管理:需要在特定时机初始化和释放资源的场景
通过 Flow 机制,开发者可以用线性的代码结构表达复杂的异步流程,提高代码的可读性和可维护性。
- 高效输出:基于 SEGGER RTT 技术,无需占用串口资源
- 彩色日志:支持不同级别的彩色日志输出
- 时间戳:每条日志自动添加时间戳
- 分级显示:支持不同级别的日志输出
// 打印日志(带时间戳)
sl_printf("Hello, sloopLite!");
// 打印变量
sl_prt_var(counter);
// 打印浮点数
sl_prt_float(temperature);通过 J-Link RTT Viewer 可以实时查看框架输出的日志信息,包括系统初始化、任务执行、系统状态等。
- 微控制器:STM32G0 系列
- 内存:至少 4KB RAM
- Flash:至少 32KB Flash
- 开发环境:Keil MDK-ARM 5.x+
- 编译器:ARM Compiler 6
- STM32 HAL 库:STM32G0xx HAL Driver
Total RO Size (Code + RO Data): 18648 bytes (18.21kB)
Total RW Size (RW Data + ZI Data): 3840 bytes (3.75kB)
Total ROM Size (Code + RO Data + RW Data): 18660 bytes (18.22kB)
- 安装 Keil MDK-ARM 5.x+
- 安装 STM32G0 系列支持包
- 克隆或下载 sloopLite 项目
- 使用 Keil MDK 打开
project/MDK-ARM/project.uvprojx
- 选择目标设备(STM32G030K8Tx)
- 配置编译选项
- 点击编译按钮(F7)
- 生成的固件位于
project/MDK-ARM/bin/project.hex
├── project/
│ ├── Core/ # 核心系统文件
│ ├── Drivers/ # STM32 HAL 和 CMSIS 库
│ ├── MDK-ARM/ # Keil 项目文件
│ └── user/ # 用户应用代码
│ ├── app/ # 应用层
│ │ ├── config/ # 配置文件
│ │ └── tasks/ # 任务实现
│ └── sloop/ # 框架核心
│ ├── RTT/ # SEGGER RTT 库
│ └── kernel/ # 内核实现
├── LICENSE # 许可证文件
└── README.md # 项目文档
// 初始化 sloopLite 框架
void sloop_init(void);
// 运行 sloopLite 框架
void sloop(void);// 启动超时任务
void sl_timeout_start(int ms, pfunc task);
// 停止超时任务
void sl_timeout_stop(pfunc task);// 启动周期任务
void sl_cycle_start(int ms, pfunc task);
// 停止周期任务
void sl_cycle_stop(pfunc task);// 启动多次任务
void sl_multiple_start(int num, int ms, pfunc task);
// 停止多次任务
void sl_multiple_stop(pfunc task);// 启动并行任务
void sl_task_start(pfunc task);
// 停止并行任务
void sl_task_stop(pfunc task);// 启动单次任务
void sl_task_once(pfunc task);// 切换到指定任务
void sl_goto(pfunc task);// 获取系统时间戳
uint32_t sl_get_tick(void);
// 阻塞式延时
void sl_delay(int ms);
// 非阻塞等待
char sl_wait(int ms);
// 非阻塞裸等待
char sl_wait_bare(void);主要配置文件位于 project/user/app/config/sl_config.h,可以根据需要调整以下参数:
// 超时任务上限
#define SL_TIMEOUT_LIMIT 16
// 周期任务上限
#define SL_CYCLE_LIMIT 16
// 多次任务上限
#define SL_MULTIPLE_LIMIT 16
// 并行任务上限
#define SL_PARALLEL_LIMIT 32
// 单次任务上限
#define SL_ONCE_LIMIT 16
// 启用 RTT 打印
#define SL_RTT_ENABLE 1- 任务设计:互斥任务需要使用
SL_INIT、SL_FREE和SL_RUN宏来管理任务的生命周期 - 资源限制:每种任务类型都有数量上限,超过上限会导致任务创建失败
- 实时性:由于采用协作式调度,任务需要主动让出 CPU 资源
- 中断处理:中断中应避免执行复杂逻辑,建议使用
sl_task_once()将复杂逻辑下放 - 内存管理:框架不提供动态内存管理,需要用户自行管理内存
-
系统时钟配置:确保系统时钟已正确配置,systick 中断周期为 1ms
-
Systick 中断处理:在 systick 中断处理函数中添加
sl_tick_irq();调用,示例:void SysTick_Handler(void) { HAL_IncTick(); sl_tick_irq(); // 调用 sloopLite 时钟更新函数 }
-
框架初始化:在主函数中初始化 sloopLite 框架并启动主循环:
int main(void) { // 系统初始化代码 sloop_init(); // 初始化 sloopLite 框架 while (1) { sloop(); // 运行 sloopLite 主循环 } }
-
极简的依赖 :sloopLite 几乎没有外部依赖,只需要一个 1ms 精度的系统时钟,这在大多数微控制器上都能轻松实现。
-
统一的接口 :无论目标设备是什么,移植步骤基本一致:
- 配置系统时钟
- 在中断中添加 sl_tick_irq() 调用
- 初始化框架并启动主循环
-
灵活的配置 :通过 sl_config.h 文件,可以根据目标设备的资源情况调整任务数量等参数,适应不同硬件的能力。
-
模块化设计 :框架核心与硬件相关的部分被隔离,移植时主要关注时钟和中断处理,其他部分无需修改。
-
RTT 日志的可选性 :对于不支持 J-Link 的设备,可以通过简单修改宏定义来禁用 RTT 日志功能,不影响核心功能。
从实际操作来看,移植 sloopLite 到新的 MCU 通常只需要:
- 几行代码的修改(主要是中断处理函数)
- 简单的配置调整
- 无需修改框架核心代码 这种设计理念非常符合嵌入式开发的需求,尤其是在资源受限的环境中,简单、可靠、易于移植的框架能够大大减少开发和维护成本。
sloopLite 的移植友好性也体现了其轻量级的设计目标,通过最小化硬件依赖和简化移植流程,让开发者能够更专注于应用逻辑的实现,而不是框架本身的适配问题。
-
RTT 日志支持:RTT 日志功能基于 SEGGER RTT 技术,仅支持 J-Link 支持的设备,如 ARM Cortex-M0、M3、M4、M7 等系列微控制器
-
时钟精度:sloopLite 依赖于 1ms 精度的系统时钟,确保 systick 配置正确
-
任务数量:根据目标设备的资源情况,合理调整
sl_config.h中的任务数量上限 -
中断优先级:确保 systick 中断优先级设置合理,避免影响系统实时性
sloopLite 框架适用于以下类型的设备:
- ARM Cortex-M 系列:M0、M0+、M3、M4、M7 等
- 其他支持 C 语言的微控制器:只要能提供 1ms 精度的系统时钟,均可移植使用
对于不支持 J-Link 的设备,可以通过修改 sl_config.h 中的 SL_RTT_ENABLE 宏为 0 来禁用 RTT 日志功能。
本项目采用 MIT 许可证,详见 LICENSE 文件。
欢迎提交 Issue 和 Pull Request 来改进 sloopLite 框架。
sloopLite - 轻量级嵌入式任务调度框架
Lightweight Embedded Bare-Metal Cooperative Task Scheduling Framework
sloopLite is a lightweight embedded task scheduling framework designed specifically for resource-constrained microcontrollers. It adopts a single-threaded cooperative scheduling model, providing rich task management functions and collaborative workflow programming capabilities while maintaining extremely low resource consumption, making it very suitable for resource-constrained embedded application development.
- Timeout Task: Execute once after a specified delay
- Cycle Task: Repeat execution at fixed time intervals
- Multiple Task: Execute periodic tasks for a specified number of times
- Parallel Task: Execute in parallel in the main loop
- Once Task: Execute only once, suitable for offloading complex logic from interrupts
- Mutex Task: Main business logic execution carrier
- RTT Log Output: Efficient logging system based on SEGGER RTT
- CPU Load Monitoring: Real-time calculation and display of system load
- System Heartbeat: Periodic output of system status
- Non-blocking Wait: Support for delay and synchronization between tasks
- Timestamp Function: Provides system time acquisition interface
- Simplified Syntax: Macro-based workflow programming framework
- Non-blocking Wait: Supports delay, condition, and event waiting
- State Management: Automatically maintains workflow lifecycle
- Event Driven: Supports inter-workflow communication
- Linear Structure: Clear and readable logic, avoiding complex state machines
The main loop continuously calls sloop(), which internally executes in sequence:
- mutex_task_run(): Runs the current unique main task
- parallel_task_run(): Polls all parallel tasks
At the same time, the tick interrupt triggers soft_timer, driving:
- Timeout tasks (one-time)
- Cycle tasks
- Multiple tasks
Forming a "time-driven + main process" combined execution model.
Tasks are split into three stages through SL_INIT/SL_FREE/SL_RUN:
- SL_INIT: Executes once on first entry (initialize resources, start timers, etc.)
- SL_RUN: Main logic area (executed in a loop)
- SL_FREE: Executed during task switching (release resources)
Task switching is triggered by sl_goto(), essentially:
- Mark
sl_free - Execute
SL_FREEin the next round - Load new task (
sl_load_new_task) - Re-enter
SL_INIT
Provides "pseudo-blocking" capability through sl_wait/sl_wait_bare:
- Current task enters waiting loop
- Internally continues to execute parallel tasks
- Wake up through
sl_wait_break/continue
Characteristic: Writing style is blocking, but the system is not blocked.
Flow is a macro-based collaborative workflow framework that unifies task scheduling, state machines, and synchronization primitives under the same semantic system. Each Flow is a "suspendable execution unit" that uses switch-case + static local variables to save execution context, and generates unique states using __LINE__ to achieve coroutine-like breakpoint continuation.
- Flow State: Each Flow has a state variable for saving execution position
- Flow Event: Used for communication between Flows
- Flow Context: Each Flow can define its own static variables as workflow context
- FLOW_INIT: Executes only once, used to initialize context
- FLOW_RUN: Main running phase, where logic unfolds as "linear code"
- FLOW_FREE: Exit phase, used for resource release
Lifecycle switching is controlled externally via FLOW_START/FLOW_STOP, and can also be actively ended internally using FLOW_EXIT.
Non-blocking waiting:
- FLOW_UNTIL: Yields scheduling when conditions are not met, continues from breakpoint when met
- FLOW_WAIT: Time-based non-blocking wait
- FLOW_WAIT_EVENT: Event-based non-blocking wait
Linear synchronous writing: Traditional state machines need to be split into multiple states + jumps, while here complex processes can be expressed with "sequential code", logic is closer to human thinking paths, significantly reducing state explosion and readability costs.
Flow workflow is written using a standard paradigm, including context definition, initialization, release, and running logic:
void flow2(void)
{
/* Workflow context, workflow data is statically defined here */
SL_FLOW_CONTEXT(flow2);
/* First entry into workflow, execute once, initialize workflow context */
SL_FLOW_INIT;
/* Workflow ends, no longer execute, release resources */
SL_FLOW_FREE(flow2);
/* Below starts entering workflow running logic */
SL_FLOW_RUN;
FLOW_UNTIL(var > 3);
FLOW_WAIT_EVENT(evt1);
FLOW_WAIT(2000);
SL_FLOW_END;
}The Flow framework provides the following core macros:
| Macro | Description |
|---|---|
FLOW_STATE_DEFINE(flow_name) |
Define Flow state variable |
FLOW_EVENT_DEFINE(event_name) |
Define Flow event variable |
FLOW_START(flow_name) |
Start Flow |
FLOW_STOP(flow_name) |
Stop Flow |
SL_FLOW_CONTEXT(flow_name) |
Define Flow context |
SL_FLOW_INIT |
Mark initialization code |
SL_FLOW_FREE(flow_name) |
Mark release code |
SL_FLOW_RUN |
Mark running logic |
SL_FLOW_END |
Mark Flow end |
FLOW_UNTIL(condition) |
Wait until condition is met |
FLOW_WAIT(ms) |
Non-blocking wait for specified time |
FLOW_WAIT_EVENT(event_name) |
Wait for event trigger |
FLOW_SEND_EVENT(event_name) |
Send event |
FLOW_EXIT() |
Actively end Flow |
Flow mechanism is particularly suitable for the following scenarios:
- Complex Business Logic: Complex business processes requiring multiple steps, condition judgments, and waiting
- State Management: Applications that need to maintain state, such as state machines, protocol processing, etc.
- Event Driven: Event-based asynchronous processing
- Resource Management: Scenarios that require initialization and release of resources at specific times
Through the Flow mechanism, developers can express complex asynchronous processes with linear code structures, improving code readability and maintainability.
- Efficient Output: Based on SEGGER RTT technology, no serial port resources occupied
- Colorful Logs: Supports different levels of colorful log output
- Timestamp: Each log automatically adds timestamp
- Level Display: Supports different levels of log output
// Print log (with timestamp)
sl_printf("Hello, sloopLite!");
// Print variable
sl_prt_var(counter);
// Print float
sl_prt_float(temperature);Real-time log information output by the framework can be viewed through J-Link RTT Viewer, including system initialization, task execution, system status, etc.
- Microcontroller: STM32G0 series
- Memory: At least 4KB RAM
- Flash: At least 32KB Flash
- Development Environment: Keil MDK-ARM 5.x+
- Compiler: ARM Compiler 6
- STM32 HAL Library: STM32G0xx HAL Driver
Total RO Size (Code + RO Data): 18648 bytes (18.21kB)
Total RW Size (RW Data + ZI Data): 3840 bytes (3.75kB)
Total ROM Size (Code + RO Data + RW Data): 18660 bytes (18.22kB)
- Install Keil MDK-ARM 5.x+
- Install STM32G0 series support package
- Clone or download the sloopLite project
- Open
project/MDK-ARM/project.uvprojxusing Keil MDK
- Select the target device (STM32G030K8Tx)
- Configure compilation options
- Click the compile button (F7)
- The generated firmware is located at
project/MDK-ARM/bin/project.hex
├── project/
│ ├── Core/ # Core system files
│ ├── Drivers/ # STM32 HAL and CMSIS libraries
│ ├── MDK-ARM/ # Keil project files
│ └── user/ # User application code
│ ├── app/ # Application layer
│ │ ├── config/ # Configuration files
│ │ └── tasks/ # Task implementations
│ └── sloop/ # Framework core
│ ├── RTT/ # SEGGER RTT library
│ └── kernel/ # Kernel implementation
├── LICENSE # License file
└── README.md # Project documentation
// Initialize the sloopLite framework
void sloop_init(void);
// Run the sloopLite framework
void sloop(void);// Start a timeout task
void sl_timeout_start(int ms, pfunc task);
// Stop a timeout task
void sl_timeout_stop(pfunc task);// Start a cycle task
void sl_cycle_start(int ms, pfunc task);
// Stop a cycle task
void sl_cycle_stop(pfunc task);// Start a multiple task
void sl_multiple_start(int num, int ms, pfunc task);
// Stop a multiple task
void sl_multiple_stop(pfunc task);// Start a parallel task
void sl_task_start(pfunc task);
// Stop a parallel task
void sl_task_stop(pfunc task);// Start a once task
void sl_task_once(pfunc task);// Switch to the specified task
void sl_goto(pfunc task);// Get system timestamp
uint32_t sl_get_tick(void);
// Blocking delay
void sl_delay(int ms);
// Non-blocking wait
char sl_wait(int ms);
// Non-blocking bare wait
char sl_wait_bare(void);The main configuration file is located at project/user/app/config/sl_config.h, and you can adjust the following parameters as needed:
// Timeout task limit
#define SL_TIMEOUT_LIMIT 16
// Cycle task limit
#define SL_CYCLE_LIMIT 16
// Multiple task limit
#define SL_MULTIPLE_LIMIT 16
// Parallel task limit
#define SL_PARALLEL_LIMIT 32
// Once task limit
#define SL_ONCE_LIMIT 16
// Enable RTT print
#define SL_RTT_ENABLE 1- Task Design: Mutex tasks need to use
SL_INIT,SL_FREE, andSL_RUNmacros to manage task lifecycle - Resource Limits: Each task type has a predefined quantity limit, exceeding the limit will cause task creation to fail
- Real-time Performance: Due to the adoption of cooperative scheduling, tasks need to actively yield CPU resources
- Interrupt Handling: Complex logic should be avoided in interrupts; it is recommended to use
sl_task_once()to offload complex logic - Memory Management: The framework does not provide dynamic memory management; users need to manage memory themselves
-
System Clock Configuration: Ensure the system clock is correctly configured with a 1ms systick interrupt period
-
Systick Interrupt Handling: Add
sl_tick_irq();call in the systick interrupt handler, example:void SysTick_Handler(void) { HAL_IncTick(); sl_tick_irq(); // Call sloopLite clock update function }
-
Framework Initialization: Initialize the sloopLite framework and start the main loop in the main function:
int main(void) { // System initialization code sloop_init(); // Initialize sloopLite framework while (1) { sloop(); // Run sloopLite main loop } }
-
Minimal Dependencies: sloopLite has almost no external dependencies, only requiring a 1ms precision system clock, which can be easily implemented on most microcontrollers.
-
Unified Interface: Regardless of the target device, the porting steps are basically the same:
- Configure system clock
- Add
sl_tick_irq()call in interrupt handler - Initialize framework and start main loop
-
Flexible Configuration: Through the
sl_config.hfile, parameters such as task quantity can be adjusted according to the resource situation of the target device to adapt to different hardware capabilities. -
Modular Design: The framework core is isolated from hardware-related parts, so porting mainly focuses on clock and interrupt handling, and other parts do not need to be modified.
-
Optional RTT Logging: For devices that do not support J-Link, the RTT logging function can be disabled through simple macro definition modification without affecting core functionality.
From practical operation, porting sloopLite to a new MCU usually only requires:
- A few lines of code modification (mainly the interrupt handler function)
- Simple configuration adjustments
- No need to modify the framework core code
This design concept is very suitable for embedded development needs, especially in resource-constrained environments. A simple, reliable, and easy-to-port framework can greatly reduce development and maintenance costs.
The porting friendliness of sloopLite also reflects its lightweight design goal. By minimizing hardware dependencies and simplifying the porting process, developers can focus more on implementing application logic rather than framework adaptation issues.
-
RTT Log Support: RTT log functionality is based on SEGGER RTT technology and only supports devices supported by J-Link, such as ARM Cortex-M0, M3, M4, M7 series microcontrollers
-
Clock Accuracy: sloopLite relies on a 1ms accuracy system clock, ensure systick is configured correctly
-
Task Quantity: According to the resource situation of the target device, reasonably adjust the task quantity limits in
sl_config.h -
Interrupt Priority: Ensure the systick interrupt priority is set appropriately to avoid affecting system real-time performance
sloopLite framework is suitable for the following types of devices:
- ARM Cortex-M series: M0, M0+, M3, M4, M7, etc.
- Other C language supported microcontrollers: As long as they can provide a 1ms accuracy system clock, they can be ported
For devices that do not support J-Link, the RTT log function can be disabled by modifying the SL_RTT_ENABLE macro in sl_config.h to 0.
This project adopts the MIT license, please refer to the LICENSE file for details.
Welcome to submit Issues and Pull Requests to improve the sloopLite framework.
sloopLite - Lightweight Embedded Task Scheduling Framework
