ANTIRTOS_C is an ultra-lightweight, robust, and efficient pure C library for task management in IoT and embedded applications. Implemented as a single header file of preprocessor macros, it requires no dynamic memory allocation, no C++ compiler, and no RTOS — just drop in antirtos_c.h and go.
Four macro variants cover every common need: fQ is the basic no-parameter queue, fQP adds a typed argument to queued functions, del_fQ adds a delay before execution, and del_fQP adds both.
Each queue expands to a statically allocated array of function pointers and a pair of generated functions: _Push adds a task to the back, _Pull executes the next task and removes it. Several queues can coexist, and all execution happens outside interrupts in a fully non-blocking way.
Prefer C++? See ANTIRTOS and ANTIRTOS_MODERN.
Also on Hackaday.io.
- Interrupts kept fast and controllable — push a pointer and return immediately, no logic executes in the ISR.
- Easy to debug and understand — no hidden scheduler, no task stacks, no magic.
- Simple multitasking without critical sections, mutexes, or semaphores.
- No dummy waiting or blocking. Wait by doing!
- No heap allocation — all state is statically declared by the macro.
- Pure C (C99) — works on AVR, PIC, STM32, RISC-V, and any toolchain that has a C compiler.
#include "antirtos_c.h"
fQ(Q1, 8); // queue named Q1, 8 slots (holds up to 7 tasks)
fQ(Q2, 8); // queue named Q2, 8 slotsOne slot is always reserved to distinguish full from empty, so a queue of size N holds N−1 tasks.
void yourTaskOne(void) {
// whatever you need to do
}
void yourTaskTwo(void) {
// whatever you need to do
}int main(void) { // or loop(){} on Arduino
while (1) {
Q1_Pull();
Q2_Pull();
}
}void ISR_1(void) {
Q1_Push(yourTaskOne); // ISR returns immediately; task runs in main loop
}
void ISR_2(void) {
Q2_Push(yourTaskTwo);
}That's it. Interrupts stay fast; tasks are handled safely outside the ISR.
#include "antirtos_c.h"
fQP(Q1, 8, int); // Q1: 8 slots, functions receive int
fQP(Q2, 8, char); // Q2: 8 slots, functions receive charvoid blinkLED(int led) {
switch (led) {
case 0: HAL_GPIO_TogglePin(LED_YELLOW_PORT, LED_YELLOW_PIN); break;
case 1: HAL_GPIO_TogglePin(LED_GREEN_PORT, LED_GREEN_PIN); break;
}
}
void printSymbol(char ch) {
printf("Symbol: %c\n", ch);
}int main(void) {
while (1) {
Q1_Pull();
Q2_Pull();
}
}void ISR_1(void) { Q1_Push(blinkLED, 0); }
void ISR_2(void) { Q2_Push(printSymbol, 'a'); }typedef struct { int index; int logic; } pinout;
void myTask(pinout p) { /* ... */ }
fQP(Q3, 8, pinout); // queue with struct type
// push:
Q3_Push(myTask, (pinout){0, 1});
// pull in main loop:
Q3_Pull();Do you need to delay a function from execution? Do not wait any more!
del_fQ(Q4, 8); // 8-slot delayed queueQ4_Push_delayed(your_func_1, 1000); // your_func_1() executes after 1000 ticks
Q4_Push_delayed(your_func_2, 2000); // your_func_2() executes after 2000 ticksQ4_Pull();void TimerISR(void) {
Q4_Tick(); // call once per tick period (e.g. every 1 ms)
}Q4_Revoke(your_func_1); // cancels all pending your_func_1 entriesdel_fQP(Q5, 8, int); // 8-slot delayed queue, functions receive intQ5_Push_delayed(your_func_4, -15, 1000); // your_func_4(-15) after 1000 ticks
Q5_Push_delayed(your_func_5, -27, 2000); // your_func_5(-27) after 2000 ticksQ5_Pull();void TimerISR(void) {
Q5_Tick();
}Q5_Revoke(your_func_4);Instead of blocking with delay() or a busy loop, keep pulling tasks while you wait:
void waitOnQ1(unsigned int ms) {
unsigned int target = millis() + ms;
while (millis() < target)
Q1_Pull();
}
// usage:
digitalWrite(13, HIGH);
waitOnQ1(1000); // 1 second passes — tasks still run
val = analogRead(3);Or waiting for a condition:
while (!neededFlag) {
Q1_Pull();
Q2_Pull();
}-
Use queue sizes that are powers of 2 (4, 8, 16, 32, 64, 128) — the compiler replaces
% Nwith a fast bitwise AND. -
One queue per interrupt if interrupt priorities are uncertain. Mixing multiple ISRs into one queue is safe only when pushes cannot preempt each other.
-
Group tasks by execution time for better responsiveness:
- Split functions into fast, medium, and slow groups, and give each group its own queue.
- Pull fast tasks while waiting inside medium ones.
- Pull medium and fast tasks while waiting inside slow ones.
-
Maximum Q_SIZE is 256, holding up to 255 tasks. Queue indices are
volatile unsigned char— a single-instruction atomic read/write on both 8-bit (AVR, PIC) and 32-bit (Cortex-M, RISC-V) MCUs.
Try it: Tinkercad simulation (Arduino/AVR)
Keep all interrupts extremely fast and never lose them. Use ANTIRTOS_C!
