TNeo  BETA v1.08-11-g97e5a6d
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 ... */
my_stack,
/* ... several arguments omitted ... */);

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

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 TNeo 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, TNeo returns TN_RC_WPARAM in these cases. User must provide start_addr and block_size that are properly aligned.

TNeo 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
#define MY_MEMORY_BUF_SIZE 8
//-- 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,
my_fmem_buf,
TN_MAKE_ALIG_SIZE(sizeof(struct MyMemoryItem)),
MY_MEMORY_BUF_SIZE );
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, TNeo 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 TNeo, there are three possible return codes:

Events API is changed almost completely

Note: for old TNKernel projects, there is a compatibility mode, see TN_OLD_EVENT_API.

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 TNeo, 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. Instead, you may set the flag TN_EVENTGRP_WMODE_AUTOCLR when task is going to wait for some event bit(s), and then these event bit(s) will be atomically cleared automatically when task successfully finishes waiting for these bits.

TNeo also offers a very useful feature: connecting an event group to other kernel objects. Refer to the section Connecting an event group to other system objects.

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 TNeo system functions allows timeout to be zero: in this case, function doesn't wait.

New features

Well, I'm tired of maintaining this additional list of features, so I just say that there is a lot of new features: timers, event group connection, stack overflow check, recursive mutexes, mutex deadlock detection, profiler, dynamic tick, etc.

Refer to the generic feature list.

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

TNeo 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.