TNeoKernel  v1.04
 All Data Structures Files Functions Variables Typedefs Enumerations Enumerator Macros Pages
Differences from TNKernel API

Table of Contents

If you have experience of using TNKernel, you really want to read this.

Incompatible API changes

System startup

Original TNKernel code designed to be built together with main project only, there's no way to build as a separate library: at least, arrays for idle and timer task stacks are allocated statically, so size of them is defined at tnkernel compile time.

It's much better if we could pass these things to tnkernel at runtime, so, tn_sys_start() now takes pointers to stack arrays and their sizes. Refer to Starting the kernel section for the details.

Task creation API

In original TNKernel, one should give bottom address of the task stack to tn_task_create(), like this:

#define MY_STACK_SIZE 0x100
static unsigned int my_stack[ MY_STACK_SIZE ];
tn_task_create(/* ... several arguments omitted ... */
&(my_stack[ MY_STACK_SIZE - 1]),
/* ... several arguments omitted ... */);

Alex Borisov implemented it more conveniently in his port: one should give just array address, like this:

tn_task_create(/* ... several arguments omitted ... */
/* ... several arguments omitted ... */);

TNeoKernel uses the second way (i.e. the way used in port by Alex Borisov), and it does so independently of architecture.

Task wakeup count, activate count, suspend count

In original TNKernel, requesting non-sleeping task to wake up is quite legal and causes next call to tn_task_sleep() to not sleep. The same is with suspending/resuming tasks.

So, if you call tn_task_wakeup() on non-sleeping task first time, TERR_NO_ERR is returned. If you call it second time, before target task called tn_task_sleep(), TERR_OVERFLOW is returned.

All of this seems to me as a complete dirty hack, it probably might be used as a workaround to avoid race condition problems, or as a hacky replacement for semaphore.

It just encourages programmer to go with hacky approach, instead of creating straightforward semaphore and provide proper synchronization.

In TNeoKernel these "features" are removed, and if you try to wake up non-sleeping task, or try to resume non-suspended task, TN_RC_WSTATE is returned.

By the way, suspend_count is present in TCB structure, but is never used, so, it is just removed. And comments for wakeup_count, activate_count, suspend_count suggested that these fields are used for statistics, which is clearly not true.

Fixed memory pool: non-aligned address or block size

In original TNKernel it's illegal to pass block_size that is less than sizeof(int). But, it is legal to pass some value that isn't multiple of sizeof(int): in this case, block_size is silently rounded up, and therefore block_cnt is silently decremented to fit as many blocks of newly calculated block_size as possible. If resulting block_cnt is at least 2, it is assumed that everything is fine and we can go on.

Why I don't like it: firstly, silent behavior like this is generally bad practice that leads to hard-to-catch bugs. Secondly, it is inconsistency again: why is it legal for block_size not to be multiple of sizeof(int), but it is illegal for it to be less than sizeof(int)? After all, the latter is the partucular case of the former.

So, TNeoKernel returns TN_RC_WPARAM in these cases. User must provide start_addr and block_size that are properly aligned.

TNeoKernel also provides convenience macro TN_FMEM_BUF_DEF() for buffer definition, so, as a generic rule, it is good practice to define buffers for memory pool like this:

//-- number of blocks in the pool
//-- type for memory block
struct MyMemoryItem {
// ... arbitrary fields ...
//-- define buffer for memory pool
TN_FMEM_BUF_DEF(my_fmem_buf, struct MyMemoryItem, MY_MEMORY_BUF_SIZE);
//-- define memory pool structure
struct TN_FMem my_fmem;

And then, construct your my_fmem as follows:

enum TN_RCode rc;
rc = tn_fmem_create( &my_fmem,
TN_MAKE_ALIG_SIZE(sizeof(struct MyMemoryItem)),
if (rc != TN_RC_OK){
//-- handle error

Task service return values cleaned

In original TNKernel, TERR_WCONTEXT is returned in the following cases:

The actual error is, of course, wrong state, not wrong context; so, TNeoKernel returns TN_RC_WSTATE in these cases.

Force task releasing from wait

In original TNKernel, a call to tn_task_release_wait() / tn_task_irelease_wait() causes waiting task to wake up, regardless of wait reason, and TERR_NO_ERR is returned as a wait result. Actually I believe it is bad idea to ever use tn_task_release_wait(), but if we have this service, error code surely should be distinguishable from normal wait completion, so, new code is added: TN_RC_FORCED, and it is returned when task wakes up because of tn_task_release_wait() call.

Return code of tn_task_sleep()

In original TNKernel, tn_task_sleep() always returns TERR_NO_ERR, independently of what actually happened. In TNeoKernel, there are three possible return codes:

Events API is changed almost completely

In original TNKernel, I always found events API somewhat confusing. Why is this object named "event", but there are many flags inside, so that they can actually represent many events?

Meanwhile, attributes like TN_EVENT_ATTR_SINGLE, TN_EVENT_ATTR_CLR imply that "event" object is really just a single event, since it makes no sense to clear just all event bits when some particular event happened.

After all, when we call tn_event_clear(&my_event_obj, flags), we might expect that flags argument actually specifies flags to clear. But in fact, we must invert it, to make it work: ~flags. This is really confusing.

In TNeoKernel, there is no such event object. Instead, there is object events group. Attributes like ...SINGLE, ...MULTI, ...CLR are removed, since they make no sense for events group.

TNeoKernel also offers a very useful feature: connecting an event group to other kernel objects. Read below.

For detailed API reference, refer to the tn_eventgrp.h.

Zero timeout given to system functions

In original TNKernel, system functions refused to perform job and returned TERR_WRONG_PARAM if timeout is 0, but it is actually neither convenient nor intuitive: it is much better if the function behaves just like ...polling() version of the function. All TNeoKernel system functions allows timeout to be zero: in this case, function doesn't wait.

New features

Event group connection

Sometimes task needs to wait for different system events, the most common examples are:

If the kernel doesn't offer a mechanism for that, programmer usually have to use polling services on these queues and sleep for a few system ticks. Obviously, this approach has serious drawbacks: we have a lot of useless context switches, and response for the message gets much slower. Actually, we lost the main goal of the preemptive kernel when we use polling services like that.

TNeoKernel (since v1.03) offers a solution: an event group can be connected to other kernel objects, and these objects will maintain certain flags inside that event group automatically.

Refer to the section Connecting an event group to other system objects for details.


Support of timers was added since TNeoKernel v1.02.

Timer is a kernel object that is used to ask the kernel to call some user-provided function at a particular time in the future, based on the system timer tick.

If you need to repeatedly wake up particular task, you can create semaphore which you should wait for in the task, and signal in the timer callback.

If you need to perform rather fast action (such as toggle some pin, or the like), consider doing that right in the timer callback, in order to avoid context switch overhead.

The timer callback approach provides ultimate flexibility.

For details, refer to the timers documentation.

Recursive mutexes

Sometimes I feel lack of mutexes that allow recursive locking. I know there are developers who believe that recursive locking leads to the code of lower quality, and I understand it. Even Linux kernel doesn't have recursive mutexes.

Sometimes they are really useful though (say, if you want to use some third-party library that requires locking primitives to be recursive), so I decided to implement an option for that: TN_MUTEX_REC. If it is non-zero, mutexes allow recursive locking; otherwise you get TN_RC_ILLEGAL_USE when you try to lock mutex that is already locked by this task. Default value: 1.

Mutex deadlock detection

TNeoKernel can notify you by calling your callback function if deadlock occurs. See:

New system services added

Several system services were added:

Compatible API changes


There is a terrible mess with MAKE_ALIG() macro: TNKernel docs specify that the argument of it should be the size to align, but almost all ports, including original one, defined it so that it takes type, not size.

But the port by AlexB implemented it differently (i.e. accordingly to the docs) : it takes size as an argument.

When I was moving from the port by AlexB to another one, do you have any idea how much time it took me to figure out why do I have rare weird bug? :)

By the way, additional strange thing: why doesn't this macro have any prefix like TN_?

TNeoKernel provides macro TN_MAKE_ALIG_SIZE() whose argument is size, so, its usage is as follows: TN_MAKE_ALIG_SIZE(sizeof(struct MyStruct)). This macro is preferred.

But for compatibility with messy MAKE_ALIG() from original TNKernel, there is an option TN_API_MAKE_ALIG_ARG with two possible values;

By the way, I wrote to the author of TNKernel (Yuri Tiomkin) about this mess, but he didn't answer anything. It's a pity of course, but we have what we have.

Convenience macros for stack arrays definition

You can still use "manual" definition of stack arrays, like that:

TN_UWord my_task_stack[ MY_TASK_STACK_SIZE ]

Although it is recommended to use convenience macro for that: TN_STACK_ARR_DEF(). See tn_task_create() for the usage example.

Convenience macros for fixed memory block pool buffers definition

Similarly to the previous section, you can still use "manual" definition of the buffer for fixed memory block pool, it is recommended to use convenience macro for that: TN_FMEM_BUF_DEF(). See tn_fmem_create() for usage example.

Things renamed

There is a lot of inconsistency with naming stuff in original TNKernel:

So, a lot of things (functions, macros, etc) has renamed. Old names are also available through tn_oldsymbols.h, which is included automatically if TN_OLD_TNKERNEL_NAMES option is non-zero.

We should wait for semaphore, not acquire it

One of the renamings deserves special mentioning: tn_sem_acquire() and friends are renamed to tn_sem_wait() and friends. That's because names acquire/release are actually misleading for the semaphore: semaphore is a signaling mechanism, and not the locking mechanism.

Actually, there's a lot of confusion about usage of mutexes/semaphores, so it's quite recommended to read small article by Michael Barr: Mutexes and Semaphores Demystified.

Old names (tn_sem_acquire() and friends) are still available through tn_oldsymbols.h.

Changes that do not affect API directly

No timer task

Yes, timer task's job is important: it manages tn_wait_timeout_list, i.e. it wakes up tasks whose timeout is expired. But it's actually better to do it right in tn_tick_int_processing() that is called from timer ISR, because presence of the special task provides significant overhead. Look at what happens when timer interrupt is fired (assume we don't use shadow register set for that, which is almost always the case):

(measurements were made at PIC32 port)

I've measured with MPLABX's stopwatch how much time it takes: with just three tasks (idle task, timer task, my own task with priority 6), i.e. without any sleeping tasks, all this routine takes 682 cycles. So I tried to get rid of tn_timer_task and perform its job right in the tn_tick_int_processing().

Previously, application callback was called from timer task; since it is removed now, startup routine has changed, refer to Starting the kernel for details.

Now, the following steps are performed when timer interrupt is fired:

That's all. It takes 251 cycles: 2.7 times less.

So, we need to make sure that interrupt stack size is enough for this (not big) job. As a result, RAM is saved (since you don't need to allocate stack for timer task) and things work much faster. Win-win.