INOS
|
INOS bases on a fully preemptive multicore kernel with 32 priorities. The number of tasks it can handle is only limited by the available target memory. Task switch times are independent of the number of tasks currently running in the system. The system behaviour is therefore fully deterministic in all situations.
The kernel and the application are written in C++ and compiled with GNU Compiler Collection (GCC). Currently with gcc 4.7.4.
To get a defined booting sequence, INOS provides a so called StartFunction mechanism. It bases on a macro which allows to define named global functions, which are called at boot time in alphabetical order. Example:
The function name in this example is _INI_1000_MyInit. As mentioned above, all functions are called in alphabetical order. Any naming could therefore be used. Due to the fact, that also INOS itself is using this mechanism to boot itself, there is a predefined naming schema, which has to be used to get a defined behaviour. It looks like this :
The _INI_nnnn part is mandatory and defines the sequence. Numbers from 0000 til 0999 are reserved by INOS and should not be used. Numbers 1000 til 9999 are reserved for the application.
Hint: To get an overview over all defined start functions in the system (including INOS ones), you can press shift-ctrl-T in iDev which opens a global element search dialog. In the Choose an element field just write _INI_. After a few seconds you get a list of all startfunctions in the Matching elements part.
Multicore systems are booted like single core ones, but one core after the other, or in other words first all start functions of core 0, then all of core 1, ... . To be able to tell the system which start functions need to be called by which core, we introduced an extended macro called StartFunctionEx, which allows to additionaly define a coremask. Example :
The coremask in the example is 0x9 (bit 0 and bit 3 are set to 1), which means, that this function is called by core 0 and core 3 on e.g. a GIN-SAM4 Quad. The core mask has the following meaning :
Hint: To have multiple cores available on a multicore target (currently COP-MAS2, IMP-MAS4, GIN-PCIe and GIN-SAM4), you have to activate Enable multicore support in your iDev project.
To get your application running, you always need to create at least one task. A task aka thread has always one of three states :
A task can have the following different suspended states
Hint: After creation of a task, it is in the state 1 (pure suspended) and needs to be resumed to get it running.
There are two possibilities to create an own task. You can either tell the system which function to use as your main task loop or create an own class derived from CINOSTask and overwrite the virtual method CINOSTask::Action().
See also CINOSTask::CINOSTask
Example with simple task loop :
Example with own class :
A task is always running on the on core it was created with.
Example with multiple tasks on multiple cores :
This example (running on a GIN-SAM4 Quad) creates on core 0 and core 1 a task running whithin MyTask1 and on core 2 and core 3 a task running whithin MyTask2. Due to the fact that INOS has an own scheduler per core, it would be possible to use the same name for all tasks (e.g. only 'Cool'), but we do NOT recommend that.
A task can live switch to an other core using the function SwitchCoreish(Core).
Example :
Hint: To debug a task running on a core different from 0, you need to change your target to MyTarget\@n where n defines the required core number, e.g. MyTarget\@2 for core 2.
The importance of a task is defined by its priority. Task priorities are numerical values from 0 to 31. The lower the value, the higher its priority. Priority 0 is therefore the highest possible one. INOS always gives cpu power to the task with highest priority which is currently in state ready. Tasks with the same priority are given cpu power in the sequence they were put into ready state.
A task only gets cpu power if its the one with currently highest priority of all ready tasks in the system. Assume the following situation: One or multiple tasks with a given priority always stay in state ready. This would lead to starvation of all other ready tasks with lower priority. There are different posibilities to avoid such situations. Tasks with a high priority need to suspend themself to allow lower priority tasks to get cpu power. Task which need to run continuously must use a low or better the lowest priority.
Hint: If you assign a priority different from lowest to your task, you need to be very careful. It could lead to starvation of other tasks or even to a non functional system.
INOS defines different priority groups.
Hint: You should only work with user task priorities unless you really know what you are doing.
Hint: On GIN-SAM3 and INFO-PCIe, one has to select the iDev feature INOS Kernel Type = standard to get 32 priorities.
INOS works with fixed priorites or in other words, it does not implement dynamic priority changes to avoid priority inversion situations. Priority inversion is defined by the following situation. Assume you have a low and a high priority task which need to access the same object and mutual exclusion is done with a mutex. If the low priority task owns the mutex, the high priority task is suspended till the mutex gets released. Or in other words: allthoug the task has a high priority, it gets blocked by a lower prio task.
Hint: Avoid priority inversions by accessing sync objects (e.g. mutexes) from tasks with different priorites, as this could lead to a non functional system or at least to fieldbus task overruns. You could avoid such situations by using INOSDisableInterrupts and INOSEnableInterrupts at least if the two tasks are running on the same core. But the mentioned two functions need to be used with care. Ask Indel if your not sure!.
Preemption occures if a currently running task is interrupted due to a state change from suspended to ready of a higher priority task, or if the timeslice of the actual running task has expired. Examples :
This mechanism allows to share the cpu power with multiple tasks of the same priority. If a task calls Relinquish(), all other tasks with the same priority get cpu power before the caller gets cpu power again. This is a more efficient way to share cpu power with other tasks, than to rely on the timeslicing mechanism. Note that by calling this function, the calling tasks time slice will be reset. One implication of that is that by calling this function, one can assure that when getting back CPU power (and probably it will never be taken away from the calling task, if there no other tasks with the same priority request cpu power), this task will have the full time slice available till it'll be preemted.
Example of two tasks sharing cpu power with Relinquish :
Hint: You can visualise the behaviour with the Indel Tasklog tool.
An other mechanism to share the cpu power with multiple tasks of the same priority is the timeslicing mechanism of INOS. A task is preempted by the system if it didn't release cpu power with e.g. Relinquish() or Suspend() for the defined timeslice. The system then preempts the task and gives cpu power to all other task with the same priority before the preempted task gets cpu power again.
Example of two tasks sharing cpu power with time slicing mechanism :
In the example Worker1 is preempted after 100us and Worker2 after 200us. This is also a possibility to give tasks a pseudo priority or more cpu power.
Hint: You can visualise the behaviour with the Indel Tasklog tool.
Task switching time is constant. It is independent of the number of tasks currently in state ready. It is also well defined when a task gets cpu power. Higher priority based tasks are running before lower prior tasks. Tasks with the same priority are executed in the sequence they were put into ready state.
An other possibility to get CPU power for the application is to work with hooks. A hook is a function or class method witch is called in the context of a cyclic running fieldbus task. A hook is registred at the corresponding fieldbus instance. There are two main parameters, the requested cyceltime and the cycle id. The cycle id is a uint16 value with the following meanings
There are several macros available to handle the cycleid value, see INOS_CYCLEID.
Example registering a 4kHz hook at GinLink category 2 :
One has to be aware of the fact, that the function MyHook in the example is called in the context of the high priority realtime task GINLink2. This task is also responsible for all the process image stuff as well as axes controlling. One should therefore only code things that are really necessary to be done cyclic. Using too much time in a hook results in possible fieldbus overruns and ends up in a non functional system.
Hint: You should only work with breakpoints in a realtime hook if you either really know what you are doing or if you work without real fieldbus hardware and therefore fully simulated. To measure the time needed for code parts, use testpoints instead (in iDev : Breakpoint Types -> INOS Testpoint)
To reduce the just mentioned risk, one has the possibility to register a so called post hook. It has pretty much the same behaviour but is called in the context of a lower priority task that is just resumed after the corresponding main cyclic task has done it's work.
Example registering a 4kHz post hook at GinLink category 2 :
The advantage of working with a post hook is that one can work with breakpoints in its hook without disturbing the system.
Hint: If you set a breakpoint in MyHook, you'll see that the task GINLinkPost2 stops at the required point.
A critical section is a code part where multiple tasks or even cores need to access a shared resource. One way to define a critical section is to work with a mutex. With a mutex you can ensure that always only one task is running through a code snippet, independent of the core the task is running on.
Example :
The example has the small disadvantage, that one has to ensure, that every return path releases the mutex, otherwise the mutex would be locked forever. To make the code even simpler, one can work with the CINOSLock class.
Example :
The mutex is requested in the CINOSLock constructor and released in its desctructor.
An other way to define a critical section is to work with INOSDisableInterrupts and INOSEnableInterrupts.
Example :
This is the most efficient but also the most dangerous way. If you disable interrupts, you need to be aware of the fact, that everything is blocked till you reenable them again! Disabling interrupts only works on one core. If you need to define critical sections for multiple cores, you have to work with mutexes.
Use INOSDisableInterrupts and INOSEnableInterrupts if :
Use mutexes if :
Hint: Ask Indel if you're not sure how to handle a critical section issue.
Sync objects are used to interact between tasks even running on different cores without having to actively poll for a corresponding variable for example. They are represented by the class CINOSSync. The INOS motion library uses sync objects to handle synchronous and asynchronous library calls.
Example :
All functions or class methods accepting a pointer to a sync object also accept the two special values
Example :
IMPORTANT: If you provide a sync object pointer to a function, always ensure that the sync object stays valid, till it is signaled.
Example (this leads to unspecified bahaviour):
The example above ends up in unspecified behaviour, due to the fact that the motion library will signal a sync object that is NOT valid anymore.
Semaphores are used to control the access to common resources by multiple tasks. They are represented by the class CINOSSemaphore. a typical use case is the producer consumer problem, see https://en.wikipedia.org/wiki/Producer%E2%80%93consumer_problem
The above example shows a possible implementation of the producer consumer problem with CINOSSemaphore with a buffer size of 10.
Queues implement besides sync objects the most important syncing mechanism in INOS. They are the base of the active object approach of the McRobot framework and are used to build FIFO (first in first out) data channels between tasks.
Example of a simplest active object :
The example creates an active object which provides commands. The idea of an active object is to implement a software module with a defined interface and an own task. All commands are handled in the context of the modules task. The big advantage of this approach is, that no critical sections are needed due to the fact, that only one task is handling the module's resources. This massively simplifies also debugging. In the above example the method Command1() is called in the context of the caller task. It only writes the required command into the modules command queue. The command itself is implemented in the internal protected method iCommand1().
In a realtime operating system it is important to have a deterministic memory management and to avoid memory fragmentation. To solve these issues, INOS provides an own partition based memory managment, implemented by CINOSPartitionMemory. For a seamless integration with arbitrary C/C++ code, INOS has implemented mechanisms so that any call to malloc, free, operator new/delete & friend will actually use INOS heap management.
Every allocation is of a specific size. INOS first rounds that size up to a certain supported values, so called chunk sizes. E.g. allocations between 1..8 Bytes may all fall into the '8 Byte chunk', sizes of 33..40 may fall into the '40 Byte chunk', etc. For every such memory chunk size that is used by either the application or INOS itself, an instance of CINOSPartitionMemory is created which is responsible for this allocation size. Internally all memory blocks of a corresponding chunk size are connected in a linked list. Whenever the application or INOS allocates a memory chunk, the class simply removes the head chunk of the list and returns a pointer to it. If a block is freed, it is added to the head of the linked list again. If there are no more free blocks in the list, the memory is allocated directly from the heap. Memory blocks therefore belong always to the same partition pool and will never be returned to the main heap.
Although global operator new/delete and malloc/free can be used, there's a better way of handling heap if you implement your own struct or class by using the macros DECLARE_DYNAMIC in the declaration part and IMPLEMENT_DYNAMIC in the implementation part. Technically, these macros add class specific 'operator new' & 'operator delete' The benefits of using these macros are many fold:
Example :
Declaration part (e.g. myclass.h)
Implementation part (e.g. myclass.cpp)
These two macros simply overwrite the new and delete operators and implement them with the described partition pool allocator.
Alloc/Free & Real-time tasks :
INOS heap management is quite low overhead and pretty deterministic. Still, performing heap operations from real-time task should be done with care. While it's certainly not forbidden, it should be limited to a minimum, as it takes some time to be done.
On multicore systems we differ between symmetric and asymmetric running systems. A system is called symmetric if the one binary is running on multiple cores. It is called asymmetric if different binaries are running the different cores. Currently only COP-MAS2 based systems are able to run asymmetric.
Symmetric running systems are :
Asymmetric running systems are :
INOS implements symmetric multicore functionality by creating an own kernel instance for each core at startup. Tasks are always running on a fixed core. INOS does not try to optimize CPU power by moving tasks from one core to an other. It is in the responsibility of the user to define which task is running on which core, see also Multicore.
The only exception are GinLink based systems, where one can configure the GinLink to be distributed to all available cores. This can be done by setting Bit 6 in OptionsEx in ginlink.dt2.
Debugging multicore systems is currently still some kind of rudimentary. Each core is handled as an own target. So if you want to debug a task on a different core than 0, you have to select the corresponding core target first in iDev, e.g. MyTarget@1 for core 1, MyTarget@2 for core 2, ...