TNeo  v1.09
Differences from TNKernel API

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:

  • #TN_RC_TIMEOUT if timeout is actually in effect;
  • #TN_RC_OK if task was woken up by some other task with tn_task_wakeup();
  • #TN_RC_FORCED if task was woken up forcibly by some other task with tn_task_release_wait();

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)

  • Current context (23 words) is saved to the interrupt stack;
  • ISR called: particularly, tn_tick_int_processing() is called;
  • tn_tick_int_processing() disables interrupts, manages round-robin (if needed), then it wakes up tn_timer_task, sets tn_next_task_to_run, and enables interrupts back;
  • tn_tick_int_processing() finishes, so ISR macro checks that tn_next_task_to_run is different from tn_curr_run_task, and sets CS0 interrupt bit, so that context should be switched as soon as possible;
  • Context (23 words) gets restored to whatever task we interrupted;
  • CS0 ISR is immediately called, so full context (32 words) gets saved on task's stack, and context of tn_timer_task is restored;
  • tn_timer_task disables interrupts, performs its not so big job (manages tn_wait_timeout_list), puts itself to wait, enables interrupts and pends context switching again;
  • 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.

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

  • Current context (23 words) is saved to the interrupt stack;
  • ISR called: particularly, tn_tick_int_processing() is called;
  • tn_tick_int_processing() disables interrupts, manages round-robin (if needed), manages tn_wait_timeout_list, and enables interrupts back;
  • tn_tick_int_processing() finishes, ISR macro checks that tn_next_task_to_run is the same as tn_curr_run_task
  • Context (23 words) gets restored to whatever task we interrupted;

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.

TN_RC_OK
@ TN_RC_OK
Successful operation.
Definition: tn_common.h:84
TN_UWord
unsigned int TN_UWord
Unsigned integer type whose size is equal to the size of CPU register.
Definition: tn_arch_example.h:105
TN_ARCH_STK_ATTR_BEFORE
#define TN_ARCH_STK_ATTR_BEFORE
Compiler-specific attribute that should be placed before declaration of array used for stack.
Definition: tn_arch_example.h:77
tn_fmem_create
enum TN_RCode tn_fmem_create(struct TN_FMem *fmem, void *start_addr, unsigned int block_size, int blocks_cnt)
Construct fixed memory blocks pool.
TN_MAKE_ALIG_SIZE
#define TN_MAKE_ALIG_SIZE(a)
Macro for making a number a multiple of sizeof(#TN_UWord), should be used with fixed memory block poo...
Definition: tn_common.h:231
TN_RCode
TN_RCode
Result code returned by kernel services.
Definition: tn_common.h:81
TN_FMem
Fixed memory blocks pool.
Definition: tn_fmem.h:80
TN_FMEM_BUF_DEF
#define TN_FMEM_BUF_DEF(name, item_type, size)
Convenience macro for the definition of buffer for memory pool.
Definition: tn_fmem.h:146
TN_ARCH_STK_ATTR_AFTER
#define TN_ARCH_STK_ATTR_AFTER
Compiler-specific attribute that should be placed after declaration of array used for stack.
Definition: tn_arch_example.h:88
tn_task_create
enum TN_RCode tn_task_create(struct TN_Task *task, TN_TaskBody *task_func, int priority, TN_UWord *task_stack_low_addr, int task_stack_size, void *param, enum TN_TaskCreateOpt opts)
Construct task and probably start it (depends on options, see below).