4
\$\begingroup\$

I'm familiar with several real-time kernels: AVIX, FreeRTOS, TNKernel, and in all of them we have 2 versions of nearly all functions: one for calling from task, and second one for calling from ISR.

Of course it makes sense for functions that could switch context and/or sleep: obviously, ISR can't sleep, and context switch should be done in different manner. But there are several functions that do not switch context nor sleep: say, it may return system tick count, or set up software timer, etc.

Now, I'm implementing my own kernel: TNeoKernel, which has well-formed code and is carefully tested, and I'm considering to invent "universal" functions sometimes: the ones that can be called from either task or ISR context. But since all three aforementioned kernels use separate functions, I'm afraid I'm going to do something wrong.

Say, in task and ISR context, TNKernel uses different routines for disabling/restoring interrupts, but as far as I see, the only possible difference is that ISR functions may be "compiled out" as an optimization if the target platform doesn't support nested interrupts. But if target platform supports nested interrupts, then disabling/restoring interrupts looks absolutely the same for task and ISR context.

So, my question is: are there platforms on which disabling/restoring interrupts from ISR should be done differently than from non-ISR context?

If there are no such platforms, I'd prefer to go with "universal" functions. If you have any comments on this approach, they are highly appreciated.

UPD: I don't like to have two set of functions because they lead to notable code duplication and complication. Say, I need to provide a function that should start software timer. Here is what it looks like:

enum TN_RCode _tn_timer_start(struct TN_Timer *timer, TN_Timeout timeout)
{
   /* ... real job is done here ... */
}

/*
 * Function to be called from task
 */
enum TN_RCode tn_timer_start(struct TN_Timer *timer, TN_Timeout timeout)
{
   TN_INTSAVE_DATA;  //-- define the variable to store interrupt status,
                     //   it is used by TN_INT_DIS_SAVE()
                     //   and TN_INT_RESTORE()
   enum TN_RCode rc = TN_RC_OK;

   //-- check that function is called from right context
   if (!tn_is_task_context()){
      rc = TN_RC_WCONTEXT;
      goto out;
   }

   //-- disable interrupts
   TN_INT_DIS_SAVE();

   //-- perform real job, after all
   rc = _tn_timer_start(timer, timeout);

   //-- restore interrupts state
   TN_INT_RESTORE();

out:
   return rc;
}

/*
 * Function to be called from ISR
 */
enum TN_RCode tn_timer_istart(struct TN_Timer *timer, TN_Timeout timeout)
{
   TN_INTSAVE_DATA_INT;    //-- define the variable to store interrupt status, 
                           //   it is used by TN_INT_DIS_SAVE()                
                           //   and TN_INT_RESTORE()                           
   enum TN_RCode rc = TN_RC_OK;

   //-- check that function is called from right context
   if (!tn_is_isr_context()){
      rc = TN_RC_WCONTEXT;
      goto out;
   }

   //-- disable interrupts
   TN_INT_IDIS_SAVE();

   //-- perform real job, after all
   rc = _tn_timer_start(timer, timeout);

   //-- restore interrupts state
   TN_INT_IRESTORE();

out:
   return rc;
}

So, we need wrappers like the ones above for nearly all system function. This is a kind of inconvenience, for me as a kernel developer as well as for kernel users.

The only difference is that different macros are used: for task, these are TN_INTSAVE_DATA, TN_INT_DIS_SAVE(), TN_INT_RESTORE(); for interrupts these are TN_INTSAVE_DATA_INT, TN_INT_IDIS_SAVE(), TN_INT_IRESTORE().

For the platforms that support nested interrupts (ARM, PIC32), these macros are identical. For other platforms that don't support nested interrupts, TN_INTSAVE_DATA_INT, TN_INT_IDIS_SAVE() and TN_INT_IRESTORE() are expanded to nothing. So it is a bit of performance optimization, but the cost is too high in my opinion: it's harder to maintain, it's not so convenient to use, and the code size increases.

\$\endgroup\$
  • \$\begingroup\$ I believe that keeping interrupt latency as low as possible is one parameter to take into account. Dividing into two routines is one way that allows an optimization, in this case in keeping interrupts off time as low as possible. We all know that premature optimization is a bad thing, so you may want to wait doing that for later. But it is a very good idea to divide the routines into one set for interrupt context, and one for task context, even if they initially is the same routine (only different names pointing to the same entry). \$\endgroup\$ – ghellquist Jul 16 '18 at 19:20
  • \$\begingroup\$ The second point is that disabling interrupts is a privileged function. If you ever want to divide your "system" into user and privileged mode the the functions will be different -- in order to disable interrupts from user mode, you need to "trap" to privileged mode. \$\endgroup\$ – ghellquist Jul 16 '18 at 19:42
  • \$\begingroup\$ There is actually yet a third reason available sometimes as an optimization in an OS that could be ported to different architectures. Some processors have primitives allowing you to, say, add an element to a queue without disabling interrupts at all. In a portable OS you would have this as two separate routines, user mode and interrupt mode. On some processors they would be the same, on others you would need to trap from user mode. Trapping from interrupt context when not needed is expensive. \$\endgroup\$ – ghellquist Jul 16 '18 at 19:45
2
\$\begingroup\$

I believe there are several more driving forces for having two sets of functions, than you have mentioned.

  1. Those RTOSs are designed to be portable to many different processor Instruction Set Architectures (ISAs), and they also try to ensure developers can write systems which are portable. So they have to accommodate all of the variability of processor ISAs in ways which hide the differences between ISAs from the RTOS users.
  2. Some of the ISAs used under an RTOS have both 'user' mode, and 'privileged' mode. 'User' mode will not have access to all of the system, and specifically might not be able to block interrupts. Further some of the functions might not be able to run in 'user' mode, and may 'trap' to a privileged mode exception handler to actually do the work. So having two 'name spaces' aligned to the two CPU privilege states might be helpful to the RTOS and application developers.
  3. IMHO it is much easier to remember to use a function from one 'name space' (e.g. 'user' mode) than it is to figure out exactly which function can be used in any specific context. Specifically, it is much easier for all the functions to come in a 'user application' name space, and a 'privilaged-system-component' name space. So, even if the code is identical, it is simpler for the application developer to use the functions in the 'application' or 'user' name space. If that is an advantage for the application developer, then it is more helpful for for the RTOS provider to 'duplicate' the code, into a function in the other 'name space' than not have the duplicated function.

Summary: IMHO having two sets of functions, effectively constituting two different 'name spaces' ('user application' and 'systemcomponent'), makes it easier for the 'application' developer, and developers adding extra 'privileged-system-component' to use.

It might also be easier to have two different function sets to make it easier to manage interrupts raised during the code running in two different system states.

It may be relatively straightforward to create the two alternatives using some carefully thought out pre-processor macros, to minimise code duplication.

If you are not intending anyone else to use your OS, or your OS will treat exceptions raised during 'user' mode and 'system' mode identically, then you can probably ignore the idea.

However, if you don't know how it might all work (for example this is the first time you have written an RTOS), or you think it might change, then you might want to think carefully about how you would re-introduce the two 'name spaces' part way through developing your OS.

\$\endgroup\$
  • \$\begingroup\$ Firstly, I can't understand the following: you've said: "IMHO it is much easier to remember to use a function from one 'name space' than it is to figure out exactly which function can be used in any specific context", and I assumed you mean that it's better to have just one set of functions instead of two. But then, you said that two function sets is better. \$\endgroup\$ – Dmitry Frank Oct 12 '14 at 19:47
  • \$\begingroup\$ And secondly: you said that having two sets is "easier" for me as a RTOS developer as well as for RTOS users. I disagree with it: it is really not convenient for me as a kernel developer to maintain two sets of functions (please see my updated question), and it is not convenient for me as a user to think whether I should use function for task or the one for ISR. Isn't it actually easier to remember just one function instead of two? \$\endgroup\$ – Dmitry Frank Oct 12 '14 at 19:48
  • \$\begingroup\$ As to user mode / kernel mode: from my experience (I mostly worked with PIC24 and PIC32), real-time kernels like the aforementioned ones are usually used in embedded world with insufficient resources and so on, so, the code actually never leaves kernel mode. Do you have different experience? \$\endgroup\$ – Dmitry Frank Oct 12 '14 at 19:51
  • \$\begingroup\$ I've aimed to clarify. A key point is some functions must have two versions, one for 'user' and one for 'kernel'. So there must be a simple convention (or name space) for the user to select the right one. \$\endgroup\$ – gbulmer Oct 12 '14 at 23:43
  • \$\begingroup\$ Thanks. Well, returning to actual question: are you aware of any CPU that requires disabling/restoring interrupts from an ISR to be done differently than from non-ISR context? \$\endgroup\$ – Dmitry Frank Oct 14 '14 at 12:12
0
\$\begingroup\$

Some vendor-supplied I/O libraries have their own ways of enabling and disabling interrupts. In some cases, they are written in an unfortunate fashion that is prone to malfunction unless all code that enables or disables interrupts does so through such libraries. If the I/O routines make use of such routines, then unless the routines can be patched to behave in reasonable fashion, it may be necessary to make sure all other code system-wide conforms to the needs of such libraries.

I'm not sure why vendor libraries don't simply provide a function that captures the interrupt-enable state, disables interrupts, and returns the captured state value, along with one that will restore a captured state, all without using any static or global objects. Modules which use this general approach will be naturally compatible with other modules that do likewise, without any need for them to coordinate on global object usage. Nonetheless, poorly designed libraries exist, and it is sometimes necessary to write programs that accommodate them.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.