Version: 1.0
Author: Dzuin M.I.
Date: 15.06.2026
License: MIT / Free for commercial and open-source projects
Simple Tasker is a lightweight, platform-independent, cooperative real-time kernel (RTC). It implements the Active Objects paradigm. Designed for soft real-time systems with limited resources or restricted hardware access, where using a classic RTOS is not feasible.
- Zero dynamic memory allocation: No
malloc/free. All memory (queues, subscription pools, timers) is statically allocated by the user. - Flexible event system without global pool: Support for ordinary and mutable events stored directly in the task queue.
- Built-in timers: Support for periodic and one-shot timers.
- Pub/Sub mechanism: Loosely coupled communication between tasks via signals.
- Priority-based scheduling: Tasks with higher priority are always processed first.
#include "simple_tasker.h"
#include <stdio.h>
// 1. Declare a task and its queue (size must be power of 2)
static st_task_t my_task;
static st_event_t my_queue[16];
// 2. Write the event handler
static void my_handler(st_task_t *me, st_event_t const *e) {
if (e->sig == ST_SIGNAL_INIT) {
printf("Task initialized!\n");
} else if (e->sig == ST_SIGNAL_USER) {
printf("User event received!\n");
}
}
int main(void) {
// 3. Initialize the kernel
st_init(NULL);
// 4. Construct and start the task
st_task_ctor(&my_task, 1, my_handler, my_queue, 16);
st_task_start(&my_task);
// 5. Main loop
while (1) {
st_tick(); // Call from system timer interrupt (e.g., every 1 ms)
st_run(); // Cooperative event processing loop
}
}-
void st_init(st_idle_t fn_idle)
Initializes internal kernel structures.fn_idleis a function called when no tasks have pending events (ideal for putting MCU to sleep, e.g.,__WFI()). -
void st_task_ctor(st_task_t *me, uint8_t prio, st_handler_t handler, st_event_t *queue, uint8_t qsize)
Registers a task in the scheduler.
IMPORTANT:qsize(queue size in chunks) must be a power of 2 (2, 4, 8, 16, 32, 64, 128, 256). Maximum value is 255. -
void st_task_start(st_task_t *me)
Starts the task by synchronously calling its handler withST_SIGNAL_INIT. Used for initial task setup (starting timers, subscribing to signals).
-
bool st_post(st_task_t *me, st_event_t const *e)
Posts an ordinary event (occupies exactly 1 chunk) to a specific task's queue. Returnsfalseif queue is full. -
bool st_post_mutable(st_task_t *me, void const *e, uint8_t size)
Posts a mutable event with payload. The kernel automatically calculates required memory from the task's event buffer.
IMPORTANT: Event structureemust start with a field of typest_event_t(or have identical layout of first 4 bytes).
-
void st_subscribe(st_signal_t sig, st_task_t *me)
Subscribes taskmeto receive events with signalsig. -
void st_unsubscribe(st_signal_t sig, st_task_t *me)
Unsubscribes the task. Frees internal subscription node for reuse. -
void st_publish(st_event_t const *e)
Sends a copy of eventeto all tasks subscribed toe->sig. Works only with ordinary (1 chunk) events.
-
void st_timer_start(st_timer_t *tmr, st_task_t *task, st_signal_t sig, uint32_t interval, uint32_t counter)
Starts a timer.counteris initial delay in ticks (>0).intervalis repetition period (0 = one-shot).
IMPORTANT: Safe to call restart for already running timer (kernel automatically removes old entry from list). -
void st_timer_stop(st_timer_t *tmr)
Stops the timer and removes it from internal list to save CPU time. -
void st_tick(void)
Decrements counters of all active timers. Must be called periodically (e.g., from SysTick interrupt every 1 ms).
void st_run(void)
Infinite cooperative loop. Finds highest-priority task with pending events, extracts event and passes it to handler. If no events, callsfn_idle.
Structure st_event_t has size of 4 bytes. If you create mutable events containing 32-bit fields (uint32_t, float, pointers), task queue array must be aligned to 4-byte boundary. On architectures like RISC-V, misaligned access will cause HardFault when handler tries to read data.
// Correct (GCC / ARM):
__attribute__((aligned(4))) static st_event_t my_queue[32];
// Correct (C11):
_Alignas(4) static st_event_t my_queue[32];Functions st_post, st_post_mutable, st_publish and st_tick are not thread-safe by default. They don't use mutexes to minimize overhead.
If you call them from interrupt handler (ISR), you must define critical section macros:
ST_ENTER_CRITICAL(); // Example: __disable_irq();
ST_EXIT_CRITICAL(); // Example: __enable_irq();Macros are already used inside functions.
Task event queue is divided into chunks sized according to event structure size. When posting ordinary event, only one chunk is used. When posting mutable event, corresponding number of chunks is allocated depending on event structure size. You need to ensure that mutable event structure size doesn't exceed remaining space in task's event queue, otherwise it won't be added.
Simple Tasker is designed considering strict constraints of embedded systems. Below is memory consumption estimate for 32-bit architecture (e.g., ARM Cortex-M, where pointer size is 4 bytes).
This memory is reserved by kernel automatically during compilation, regardless of number of created tasks.
| Data Structure | Size | Description |
|---|---|---|
st_task_reg |
64 bytes | Array of task pointers 16(ST_MAX_TASKS) (16 × 4 bytes) |
st_sub_pool |
800 bytes | Static pool of 100(ST_MAX_SUBS) subscription nodes (100 × 8 bytes) |
st_sub_list |
256 bytes | Array of 64(ST_MAX_SIGNALS) subscription list heads (64 × 4 bytes) |
st_tmr_pool |
256 bytes | Static pool of 16(ST_MAX_TIMERS) timers (16 × 16 bytes with alignment) |
| Service variables | ~20 bytes | Counters, list head pointers, flags |
| TOTAL (Kernel) | ~1.4 KB | Fixed RAM consumption by kernel with this configuration |
You allocate this memory yourself when declaring tasks and timers.
| Object | Size per instance | Note |
|---|---|---|
Structure st_task_t |
12 bytes | Task itself (without queue) |
Structure st_timer_t |
16 bytes | Timer instance |
| Task queue | N × 4 bytes |
Where N is queue size (qsize). 1 chunk = 4 bytes. |
| Task example | ~76 bytes | Task + queue for 16 events (12 + 16×4) |
When compiling with size optimization (-Os in GCC/Clang):
- Base kernel: ~1.5 – 2.0 KB.
- If you don't use timers or Pub/Sub, linker with flags
-ffunction-sectionsand-fdata-sections(and subsequent--gc-sections) will automatically cut unused functions, reducing size to ~1.0 – 1.2 KB.
If 1.4 KB static RAM is too much for your chip, you can easily reduce it by changing macros at the beginning of simple_tasker.h file to match your actual needs:
// Default values:
#define ST_MAX_TASKS 16U // Reduce to 4-8 if few tasks (-48 bytes RAM)
#define ST_MAX_SIGNALS 64U // Reduce to 16-32 if few signals (-128 bytes RAM)
#define ST_MAX_SUBS 100U // Reduce to 20-30 if few subscriptions (-560 bytes RAM!)
#define ST_MAX_TIMERS 16U // Reduce to 4-8 if few timers (-128 bytes RAM)Example: Setting ST_MAX_SUBS = 30 and ST_MAX_TIMERS = 8 reduces kernel static overhead to ~600 bytes, making scheduler suitable even for low-power MCUs with 2-4 KB RAM.
This section describes internal kernel workings for those planning to modify or port it.
Unlike classic implementations with global event pool, Simple Tasker stores events directly in task queue flat array.
- Chunk structure: Queue is
st_event_tarray. Each event starts with 4-byte header:typedef struct st_event_t { st_signal_t sig; // Signal uint8_t slots; // Number of contiguous chunks occupied by event (>= 1) uint8_t _reserved[2]; // Alignment to 4 bytes } st_event_t;
- Mechanics:
st_post_mutablecalculates required chunks viaST_EVENT_CHUNKS(size)macro and performsmemcpyof user data directly tome->queue[me->head]. Fieldslotsis forcibly overwritten by kernel to guarantee correct memory deallocation. - Defragmentation: When extracting event,
tailshifts bye->slots. If after thisnused == 0,headandtailreset to0, eliminating any fragmentation.
Subscriptions implemented with protection against fragmentation and memory leaks, without using malloc.
- Head array:
st_sub_list[ST_MAX_SIGNALS]stores pointers to heads of singly-linked lists for each signal. - Static pool + Free List: Uses array
st_sub_pool[ST_MAX_SUBS]and pointerst_sub_free_list.- On
st_subscribe: Node taken from head ofst_sub_free_list(O(1)). If list empty, new node taken fromst_sub_pool. Node added to head of listst_sub_list[sig]. - On
st_unsubscribe: Node found in list, removed from it and returned to head ofst_sub_free_list(O(1)). This completely prevents pool exhaustion during frequent subscribe/unsubscribe operations.
- On
- Timers stored in singly-linked list
st_tmr_head. - Duplication protection: Function
st_timer_startexplicitly searches for timer in list and removes it before adding. This prevents creation of circular references or duplicates when callingst_timer_startagain for one-shot timer. - CPU optimization: When one-shot timer fires (
interval == 0), it automatically removed from listst_tmr_headinsidest_tick(), so in future no CPU cycles wasted checking inactive timers.
- Tasks registered in array
st_task_regand automatically sorted by descending priority (Insertion Sort) when callingst_task_ctor. - Function
st_sched()simply iterates this array top-down and returns first task wherenused > 0. This guarantees strict priority execution without complex ready bitmasks, saving ROM and simplifying code.