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:

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:

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. I have plans to offer a way to connect events group to queue and probably other kernel objects as well, so that queue will set and clear particular flag in the events group automatically, depending on whether a queue is empty. By means of that, it is quite easy to wait for data from multiple queues with just a single call to tn_eventgrp_wait().

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.

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.

Compatible API changes

Macro MAKE_ALIG()

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_?

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:

All the system service names follow the naming scheme tn_<noun>_<verb>[_<adjustment>](), but the tn_start_system() is special, for some strange reason. To make it consistent, it should be named tn_system_start() or tn_sys_start();

A lot of macros don't have TN_ prefix;

etc

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.

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):

CS0 ISR is immediately called, so full context of tn_timer_task gets saved in its stack, and then, after all, context of my own interrupted task gets restored and my task continues to run.

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.

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.