Skip to content

ezasm/ObjectiveRTOS

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ObjectiveRTOS

A lightweight, object-oriented real-time operating system (RTOS) designed for low-resource microcontrollers. The RTOS was created to run a large number of tasks simultaneously with minimal load on CPU and memory resources. As a result, it can be a powerful tool for developing games or simulating multiple physical systems at the same time.

Features

  • Lightweight design: optimized for extremely low RAM and CPU usage. You can run up to 150 tasks with just 2 KB of RAM. The library is designed to minimize memory footprint while maintaining efficient task execution.
  • Dynamically allocated stack: each task allocates exactly as much memory as it needs. The scheduler measures actual task stack usage and stores only the necessary data, avoiding memory waste.
  • Object-oriented architecture: easy task management with object-oriented design patterns. Tasks can be easily created, scaled, and managed dynamically at runtime without complex configuration.
  • Flexible scheduling configuration: the scheduler is easy to configure in many ways to suit user needs. It can be driven from different interrupt sources, not only a single timer, allowing integration with diverse hardware timers or external interrupts.
  • Can be easily ported: the structure of the library allows you to easily add specialized functions to your MCU architecture. Currently, only the AVR architecture is now implemented.

Installation

  1. Download the ObjectiveRTOS library
  2. Copy the library folder to your Arduino libraries directory:
    • Windows: Documents\Arduino\libraries
    • Linux: ~/Arduino/libraries
    • macOS: ~/Documents/Arduino/libraries
  3. Restart the Arduino IDE
  4. The library will appear under Sketch > Include Library > ObjectiveRTOS

Getting started

Before reading the tutorial and using this library it is helpful to be familiar with basic object-oriented programming concepts: class, instance (objects), inheritance, and abstract classes (interfaces) and you know what programming with dynamic memory allocation is (pointers).

You can also see a tutorial with a practical example in the file ORTOS_tutorial.ino. The file contains a step-by-step tutorial that demonstrates and discusses how ObjectiveRTOS can be used in practice.

Core concepts

The process of interrupting and switching to another task is very resource-intensive, because it requires saving the execution context of the current task (register contents and stack). For the AVR ATmega328p microcontroller, 2 KB of RAM is a very small amount, so it is worth saving every possible byte of memory. The simplest approach is to eliminate saving the task context as much as possible. This is the idea behind the object-oriented design of this library. All local variables of the function are packed and moved into a class. As a result, exactly as much memory is reserved as the task actually needs, and the scheduler does not have to guess which registers contain local variables.

The runnable class

The runnable class provides an interface for tasks. This allows the scheduler to invoke the inherited run() method, where you define what the task should do.

class MyTask : public runnable {
public:
  //your data member
  MyTask() {
    //initialization of class members
    start_task();
  }
  void run() {
    //your task code
  }
};

Key methods:

  • start_task(delay = 0); – starts the task with optional initial delay. Informs the scheduler that all variables have already been initialized and the task is ready to run.
  • run() { } – this is where you define the task to be executed.

By default, a task in this form finishes its execution, but it may also perform several other actions. It has the following options available:

  • rerun_task(delay) – stops the task execution at this point and starts it again after the specified delay
  • delay_task(delay) – suspends the task for a specified period of time; after the delay expires, the task resumes execution
  • delete current_task – the task may delete itself and be fully self-managed; the scheduler provides a pointer to the currently executing task via the global variable current_task
  • nothing – if the task should remain in memory so that another task can read the contents of the object

Difference between rerun_task() and delay_task()

Both functions provide similar functionality. They allow the program to create a delay during which the scheduler can switch to executing other tasks. The difference lies in the way memory is managed and, as a result, in how the task is resumed after a period of time:

rerun_task() delay_task()
Behavior Restarts the task from the beginning, so there is no need to preserve the task context (register contents and stack). Suspends task execution for the duration of the delay. In order for the scheduler to switch to executing other tasks, additional memory must be reserved during the delay to store the task context (registers and stack). After the delay has elapsed, the task continues executing the code sequence following the call to this function.
Memory management No additional memory allocation is required. During the delay, at least an additional 22 bytes (for the AVR architecture) + stack space are dynamically allocated.
Evaluation Faster and allows more tasks to run concurrently. Usually slower, consumes more memory, but is much more convenient to program.
Recommended use Suitable for cyclic tasks that must perform the same operation at fixed time intervals. Suitable for sequential tasks whose complexity would be difficult to implement using rerun_task().

The Task Class

Tasks can also be defined in another way. You can define a function and then use the task class. The task class provides a lightweight wrapper for executing tasks as functions:

void my_function() {
  //your task code
}

//create a task instance from a function
task my_task(my_function, 200);

Creating an instance of this class automatically runs this function as a task. The second parameter of this constructor is optional and specifies the time in ticks after which this function should be automatically executed. By default, the time parameter is set to 0, which is equivalent to starting the task immediately.

Global Variables and Functions

Scheduler Functions:

  • void ORTOS_next_tick()the most important function that should be called periodically to ensure continuous operation of the scheduler. Advances the scheduler by one tick and executes any tasks scheduled for this time. This function is typically called by a timer interrupt handler and should be called with interrupts disabled. While executing tasks, the scheduler leaves interrupts enabled.

Scheduler Global Variables:

  • volatile unsigned long tick_counter – do not modify, read only. Current time in ticks since startup. Automatically incremented by the scheduler at each tick (counts the number of times the ORTOS_next_tick() function is called).
  • volatile runnable* current_task – Pointer to the task currently being executed. It is equivalent to the this pointer. The current_task is nullptr when the scheduler is not executing any task.

Other task functions:

  • void time_stamp() – capture the current scheduler time as a reference point for subsequent delay functions (rerun_task(), delay_task()). By default, the reference measurement point is automatically established when the delay functions are last called or the task object is created.
    It should be noted that the scheduler system will not always be able to start a task exactly on time, especially when there is a large number of tasks and heavy CPU load. If a task is late, its next execution may be relatively accelerated to maintain time consistency. This is beneficial when maintaining a constant frequency is important (e.g., blinking an LED), but in some control systems, such as those involving stepper motors, this behavior may be inappropriate (a stepper motor could lose steps). In such cases, it is recommended to always use the time_stamp() after calling the delay functions to ensure that the delay is never shorter than the time defined in the delay functions.
  • bool is_task_finished(runnable* task) – return true if the given task object is not currently enqueued (i.e., it has finished or was removed from the scheduler queue). Returns false otherwise.

Macros

Time Conversion Macros:

System time is measured in ticks. The number of ticks may vary depending on the implementation used. Therefore, to allow a program to measure fixed time intervals, helper macros were introduced to convert time values (seconds, milliseconds, microseconds) into values expressed in ticks.

When implementing an interrupt to handle the scheduler's work, you should also add a constant that defines how many times per second the ORTOS_next_tick() function will be called (how many ticks there are per second).

#define SCHEDULER_TICKS_PER_SECOND 1000

These macros calculate the correct number of ticks based on the SCHEDULER_TICKS_PER_SECOND configuration.

SECONDS(1)        // 1 second
MILLISECONDS(500) // 500 milliseconds
MICROSECONDS(x)   // x microseconds

Best Practices

1. Don't block other tasks

It should be remembered that the scheduler never preempts a task automatically and never switches tasks on its own. It is the responsibility of the task to decide when the scheduler may execute another task. A task runs until one of the following occurs:

  • delay_task() is called
  • rerun_task() is called
  • end_task() is called
  • the function simply ends

Only then can the scheduler execute other available tasks during the delay period. Using while(1) {…} without any of the above functions is not allowed, as it will completely block the execution of other tasks. If necessary, use while(1) { delay_task(1); }

Why this solution?

It saves CPU time that would otherwise be spent managing access to shared resources. Therefore, mutexes or semaphores are not required, but tasks must use delay functions. This approach is much more memory-efficient, because the delay function suspends the task exactly at a point where no computation is being performed. If the scheduler were preemptive, it would not know what the function was doing at a given moment and would have to store all register information, which would require a minimum of 37 bytes per task. Without preemption, only the registers required by the ABI convention need to be saved, which requires at least 22 bytes per task. The numerical values ​​for memory usage are calculated for the AVR architecture.

2. Avoid using pointers to local variables

Local variables are typically allocated in registers, and in that form it is not possible to generate a pointer to such a variable. In order for the compiler to generate a pointer, it must move the variable onto the stack. A problem may arise when the delay_task() function is used. To minimize memory usage to an absolute minimum, delay_task() does not guarantee that, when a task context is restored, the task’s stack will be located at the same memory address. If the task stack is rebuilt at a slightly different location, pointers generated earlier may now refer to an incorrect region of the stack, leading to modification of unintended data. In such cases, it is recommended to use the new operator instead of using local variables, which allocates variables on the heap and avoids potential bugs and errors related to stack reallocation. This also applies to scenarios in which a function is called with reference parameters to local variables, and that function internally calls delay_task().

Architecture

File Structure

  • ObjectiveRTOS.h: Main include file that combines all library headers
  • runnable.h/runnable.cpp: Base class for custom tasks
  • task.h/task.cpp: Simple task implementation using function pointers
  • scheduler.h/scheduler.cpp: Task scheduling and execution engine
  • management_functions.h: Utility functions for task and scheduler management
  • definitions_and_macros.h: Time conversion macros
  • compatibility_functions.h: Platform-specific functions and macros
  • compatibility_functions/: Architecture-specific implementations (e.g., AVR assembly)

License

This library is licensed under the custom non-commercial license . If you want a commercial license you must contact the author. See the LICENSE.txt file for details.

Author

Paweł Sokół
contact: pawel.sokol.job@gmail.com
YouTube: @sezam

Support

For issues, feature requests, or questions, please refer to the library documentation and examples included in the package.

Changelog

Version 0.1.0

  • Initial release
  • Core scheduling engine
  • Task management with delay support

Enjoy using ObjectiveRTOS for your projects! ☺

About

This RTOS was designed to have the lowest possible overhead on the processor and memory in order to run as many tasks as possible. It is a powerful tool for developing games or simulating multiple physical systems at the same time.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages