A Simple Real-Time Executive



November 01, 1992
URL:http://www.drdobbs.com/a-simple-real-time-executive/184402613

November 1992/A Simple Real-Time Executive

Charles B. Allison has been working with microprocessor hardware and firmware in embedded systems since 1975. He has a B.S. in physics and an M.B.A.. Charles has a microprocessor consulting business, Allison Technical Services, where he has been developing embedded control and monitoring products since 1984. Charles can be reached through his BBS/FAX line at (713) 777-4746 or his company, ATS, 8343 Carvel, Houston, TX 77036.

Introduction

Many programmers use commercial real-time executives. Some use public domain execs while others use in-house systems developed over the years. Few programmers still try to roll their own. The presence of exec debugging tools and factory support virtually dictate the use of commercial real-time executives when feasible.

Still, there are two factors in favor of the home grown. First, it is often a very good idea to understand what is inside that little black box and building one can be very instructive. Second, one size of anything never truly fits all. That size will be too small for some applications and too large for others.

The purpose of this article is to introduce the reader to the basic concepts behind a real-time task executive by providing a very simple preemptive task executive. Tiny Exec (Listing 1 and Listing 2) is written in C. It is useful for many low-complexity applications using either standalone or MS-DOS-based platforms. Understanding this executive can help you to understand the much more complex commercially-available executives. I will attempt to accomplish this feat by first describing Tiny Exec, contrasting its approach with more complex commercial execs, and then discuss some of the considerations of using MS-DOS and its typical C compilers for real-time programming applications.

Real-time programming is based on a preemptive programming approach. Preemptive means that the processor can decide to pause execution of a section of the code and begin running another section.

The simplest example of a preemptive program is a main loop with a hardware interrupt. Whenever an interrupt occurs, the processor will stop executing the main loop and start the interrupt routine. The interrupt routine will run until completion and then return to the point in the main program where it left off. The processor will automatically save its internal flags to the stack. The interrupt program will then execute instructions that save to the stack any registers that may be modified during the interrupt routine.

Tiny Exec

Tiny Exec is a very small and rudimentary real-time executive. It is easy to rewrite in assembly language because of its simplicity. Tiny Exec may be run using a faster interrupt time than the normal MS-DOS 18.2 Hz clock rate. In some MS-DOS-based cases it may be run at rates up to around 180 Hz. In non-DOS embedded designs it is possible to run beyond 200 Hz. In Tiny Exec, this interrupt routine is based upon the hardware timer at interrupt vector 0x08.

Rather than running multiple programs, Tiny Exec runs multiple routines inside a single program. A routine may consist of a single C function or one that calls other C functions. There are two basic types of routines, task routines and timer routines. They differ only slightly.

Timer routines must be short since they are run as extensions to the timer interrupt and run while interrupts are disabled. Task routines are executed with interrupts enabled. They do not have the severe time constraints of the timer routines. Often, a timer routine is used to request that a task be run. Timer and task routines are defined by their presence in the timer and task arrays, respectively.

The task and timer lists each consist of two arrays. The first array for tasks consists of task flags, which indicate the state of the task, and integer down-counters for timers. The second arrays are function pointers containing the execution addresses of the various tasks and timers. Tiny Exec hard codes the number of tasks and timers along with their execution addresses. This hard coding eliminates much complexity.

Timer operation consists of setting the counter to a non-zero value. The counter is then decremented once each interrupt until it reaches zero. During the interrupt in which a timer decrements from one to zero, the associated timer routine is executed as a part of the timer interrupt.

Task routines do the processing work. These are run with interrupts enabled and may run many clock ticks. Each task has a priority which is its position in the task list. A lower priority task will be preempted and the higher priority task run whenever that higher priority task is ready to run. A higher priority task must be run to completion before Tiny Exec returns back to start or finishes a lower priority task.

The lowest priority task is called the background. In Tiny Exec, it is a loop in the main function that is permanantly in the run state. The background task may do nothing or may be used to do a multitude of operations that are not as time critical as those of higher priority tasks. I have often used the background task as a timing loop to determine the percentage of time still available for additional processing functions. This timing function is processor speed dependent since it relies on execution times for a counting loop and delay loop.

Task States

Tasks may exist in one of three states: REQUESTED, INPROCESS, and SUSPENDED. Requested indicates that the task will be started at the first available opportunity, i.e., no other higher priority tasks in operation. SUSPENDED means that the task is neither REQUESTED nor is it currently being run. INPROCESS indicates that the task has been started but not yet finished execution. More than one task may be INPROCESS at any point in time. Only the highest priority task whose state is INPROCESS will be executing at that particular moment.

Tasks, except for the background task, are normally in the SUSPENDED state. A task is REQUESTED by setting its REQUESTED bit. During each timer interrupt, the task list is scanned from highest priority to lowest looking for either the REQUESTED task bit or the INPROCESS task bit. The first time either one is found causes the exec to end its search and take the appropriate action.

For a task INPROCESS, the appropriate action is to return to that task. By definition, doing an interrupt return from the timer interrupt will return the program to that point. Since it was currently running the highest priority task which was INPROCESS when the timer interrupt occurred, the program simply does an interrupt return to where it was before the interrupt.

If the task exec finds a REQUESTED task before it finds an INPROCESS flag, the program has a higher priority task that must be executed before it can return to the lower priority task, which was INPROCESS. At this point, it saves the current task number, enables interrupts, and calls the newly REQUESTED higher priority task.

This new higher priority task may or may not run to completion before the next timer interrupt. If it finishes before the next interrupt, it returns back to the timer interrupt at the point where it was called. This point is in the exec loop that will continue to look at lower priority tasks for REQUESTED and INPROCESS flag bits. If the new task does not complete before the next timer interrupt then it will be interrupted and the process described above begins again.

At some point in time, the task executive will again reach the original task which was INPROCESS and the executive will do a return interrupt to continue its execution. Eventually, the task exec will return to the background loop which is always INPROCESS.

This approach allows the C compiler's stack and interrupt handling abilities to be used to take care of all the context switching necessary for the preemptive multi-tasking. Contexts of all preempted lower priority tasks reside on the system stack, waiting to be popped back off on a return interrupt to that task.

Commercial Execs

There are several significant differences between Tiny Exec and the common real-time execs available today. Many common features found in most execs are not in Tiny Exec. Some of these features are missing because the fundamental approach is a bit different from the typical real-time executive. Others are not included in an attempt to keep the presentation simple.

Commercial real-time executives offer many functions. There are so many features and functions that often there are several different ways to do most anything. Having so many options can be a joy or a nightmare.

The most fundamental difference between Tiny Exec and the typical real-time exec deals with context switching. All information from preempted tasks resides on the main program stack. Because of this, Tiny Exec is a "last in first out," or LIFO, application. Only the most recent preempted task on the stack may be continued and no lower priority tasks may be run until all higher priority tasks have completed. Also, the task wait function must be handled differently.

Typical real-time executives have a more sophisticated preemptive approach. Either separate stacks are used with the different tasks or adequate space on the program stack is initially reserved for each task. During the exec's operation, context switching from one task to another will include switching the stack pointer besides the usual registers. This approach exceeds the capability of an interrupt function and enters the realm of assembly-language subroutines and compiler-specific coding.

Most commercial executives are much more elegant than Tiny Exec in their handling of tasks and timers. In Tiny Exec, all timers and tasks are tested during the interrupt. That can be a significant waste of time in larger applications. Commercial executives normally create dynamic lists for active tasks and timers. Their approach makes great sense for these larger applications, but it can become a burden to the simple ones. It also makes for more difficulty in understanding.

Programming With Tiny Exec

Programming with Tiny Exec requires a little bit of planning and design. Real-time programs tend to be more complex than the usual big loop programs.

A real-time system has two types of processing-time constraints. It has an average processing ability, which is what can be processed during a long period of time compared to the length of instruction times and interrupt routines. It also has an instantaneous-peak-processing requirement associated with latency times for interrupts and for time-critical task execution. A workable design must meet both of these criteria.

The basic design requirement is to break the overall problem into tasks and timers suitable for running under Tiny Exec. The main function contains program setup actions and the background task. This background task contains functions that either have minimal real-time significance or contain operations that are long compared to other activities. Calculations and setup for printing hourly reports is an example of a task well suited for the background.

Higher priority tasks should be much shorter and have much more stringent time requirements than do lower priority ones. The typical real-time exec provides a wait function for tasks that cannot simply execute from beginning to end without delays. Because of the context-switch design of Tiny Exec, tasks that need to wait, either for time or external events, must be broken apart. The break up may be either into tasks or subtasks. Using subtasks keeps the number of task flags low and reduces the amount of timer-interrupt overhead necessary to return to lower priority tasks. In very simple applications this may not be of concern so multiple tasks and flags may be used instead of an internal breakup of the task into subtasks.

Another resource saving idea is to run slower timers in a task. Every timer in the system probably does not require the immediate access to execution as do the high speed timers. These slower ones can be done as high priority tasks with much reduced timing constraints. Depending on the speed requirements, hundreds of slower speed timers may be implemented.

One serious programming consideration is stack size. Since, in principle, most of the tasks could all be INPROCESS, there can be a much larger burden placed on its size than in simple big loop programs. You can estimate stack size by adding the memory used by local variables and function levels together for all tasks that could be running simultaneously and then adding extra stack memory for interrupt routines.

A Tiny Exec application consists of one linked object module. This means there is only one copy of the runtime library functions used. Most compilers have runtime library functions that are not reentrant. None of the floating-point math routines or I/O routines should be assumed to be reentrant. Those items that are not reentrant should be used in only one task.

Borland C++ documentation states that their floating-point operations can be used in interrupt routines in all memory models. If a floating-point processor chip is used, the chip's registers must be saved on entry and restored on exit for any task that uses floating point. Microsoft's C does not appear to have reentrant floating-point capabilities except possibly with specific compiler options when using the coprocessor.

Program Description

The bulk of Tiny Exec is in the timer interrupt routine, rtcint. init_rtc, close_rtc, and init_cntr provide the startup and shut down activities necessary for the timer chip and interrupt vectors. #define macros create three inline functions used for task and timer control:

These functions are also duplicated for use within sections of the code where interrupts are disabled. Their alternate forms are ireq_task, iresume_task, and iset_timer.

rtcint is the real-time clock routine and exec scheduler. It is the interrupt routine generated by the 8253 real-time clock chip. rtcint can be run at the standard 18.2 Hz PC interrupt rate or can easily be any multiple of that rate. Using values other than integer multiples will result in loss of accurate time-of-day clock information. Running at higher rates can also cause loss of time through the missing of interrupt requests.

There are three parts to rtcint. The first runs Tiny Execs timers where all nonzero timers are decremented. The second section tests for the 18.2 Hz clock tick and calls the original PC clock interrupt if it is time. The third section runs the task exec if it is time. The task exec runs through all task flag words searching for the first INPROCESS or REQUESTED flag bit. Finding an INPROCESS bit indicates that this timer interrupt came from the highest priority task currently scheduled to run. A return interrupt is used to continue it. Finding a REQUESTED bit indicates that a new higher priority task has been requested to run so interrupts are enabled and the task function is called.

Programming Example

An example main is provided with several tasks and timers. It is strictly instructive as it accomplishes no useful purpose. The job of providing MS-DOS I/O has been allocated to the loop in main which is the lowest priority task.

main begins with initialization of Tiny Exec. It then kicks off timer0 and timer1 and requests task0 to execute. The loop in function main monitors your keystrokes to test for the escape key or the space bar. Pressing the escape key ends the infinite loop and terminates the program. Pressing the space bar requests execution of task1. Additionally, main outputs data to the screen during each loop when t0_flag, controlled by task0, indicates that task0 has run.

task0 is our highest priority task. Each time task0 is run, it copies the current state of all timers and task flags to arrays which are displayed to the screen by main. Having access to this information can be very helpful when you are trying to debug real-time applications. task0 requests are handled by timer0 after the first request made in main.

task1 is the second highest priority task. It consists of two subtasks, tsk1sub1 and tsk1sub2. This approach is Tiny Exec's equivalent of a wait function. When task1 is first REQUESTED, it runs tsk1sub1, the first subtask. When task1 is told to resume, then it runs the second subtask, tsk1sub2.

task1 controls the PC's speaker. When tsk1sub1 executes, it turns the speaker on, starting a beep. tsk1sub1 sets timer2 to run for beep_len ticks. At the end of beep_len ticks, timer2 requests task1 to resume with tsk1sub2 which shuts off the PC speaker's tone.

There are three timers used in this example. timer2 is dedicated to beep length. timer1 is used to periodically schedule task0. timer0 just sits there and reschedules itself about once per second. It is monitored by main to determine when to get and display the current clock time.

Problems and Considerations

Fate has ways of dealing with those who venture where the brave fear to tread. The number of ways to suffer from real-time bugs is far greater than the typical program. Not only do you have those potential bugs, there are now many more. BIOS and MS-DOS were designed to be single tasking. Also, your compiler and runtime library were intended to be single tasking.

Variables used at more than one task level, or at the task and interrupt level, are at special risk. Not only must you be careful in handling them so as to prevent those most insidious "once in a blue moon" bugs, but the compiler must not be allowed to tinker with these variables in the name of optimization. The volatile keyword may not have any effect in some older compilers.

Beware of debugger-induced problems. While it is possible to use debuggers to test your application, problems may occur. A debugger may not recapture interrupt vectors when it hits a breakpoint. Initial testing is usually accomplished without taking over the interrupt, using a call to the timer function in the main loop instead.

When replacing interrupt vectors, never restart the program without running it to completion. Doing so will fail to restore the interrupt vectors modified by the program. This usually causes the computer to eventually crash.

Conclusions

Despite its simplicity, Tiny Exec is a powerful tool for many low-end applications. Its apparent weaknesses, the lack of sophistication and features, can also be its strongest assets. Porting Tiny Exec to other languages and platforms usually is neither complicated nor time-consuming.

I have used Tiny Exec for years in embedded systems. It has been totally adequate for many applications and has become more of an approach to solving a problem than a specific set of functions. Extending its features to handle more difficult applications is often easily accomplished because of its fundamental simplicity. For sophisticated applications, a commercial executive is highly recommended.

Sidebar: Real-Time Resources

Sidebar: Floating-Point and Coprocessors

Sidebar: DOS and Multitasking

Sidebar: Reentrant Function Lists

November 1992/A Simple Real-Time Executive/Listing 1

Listing 1 tinyexec.h

/* ----------- tinyexec.h------------ */
#define TIMER 8
/* interrupt vector for this real time clock */
#define TIMR1_C  0x43
/* control register for xt timer intel 8253 */
#define TIMR1_T1 0x40
/* timer register for xt timer intel 8253 */
#define LOAD_T1 0x34
/* select load timer 1 register 2 bytes */
#define XT8259 0x20
/* define port to clear xt interrupt controller */
#define CLR_INT 0x20
/* define nonspecific interrupt clear for intel 8259 */
#define IN PROC 0x80
#define REQ_TASK 0X40
/* ----------- end tinyexec.h------------ */

/* End of File */
November 1992/A Simple Real-Time Executive/Listing 2

Listing 2 tinyexec.c

/***************************************
*                                      *
*              tiny exec               *
*                                      *
****************************************
  written by: Charles B. Allison
  Allison Technical Services
  8343 Carvel
  Houston, TX 77036
  PHONE#  (713) 777-0401 | BBS/FAX (713) 77-4746
****************************************
version 1.1
last change 6-3-92 11:35
*/
/* ***** include files ****** */
#include <dos.h>
#include <conio.h>
#include <stdio.h>
#include <time.h>
#include "tinyexec.h"
/* ********* define statements ********* */
/* #define DEBUG 1*/
#define MICROSOFT 1
#define NUM_TIMERS 3 /* number of timers */
#define NUM_TASKS 3  /* number of tasks  */
#define TASK_TICKS 2 /* TASK_TICKS is the # of */
       /* rtc clock ticks between task exec runs */
#define OLD_TICKS 1
/* OLD_TICKS is the number of rtc clock ticks between
running oldtimer, the system clock at 18.2Hz */
#define NORMAL_COUNT 0
/* value normally used in counter 65536 (= 0)*/
#define RTC_COUNT (unsigned)((65536L/OLD_TICKS)&0xffff)
/* value to be used in rtc interrupt rate
      -- relates to OLD_TICKS */
#ifdef MICROSOFT /* Microsoft compatibility */
#include <graph.h>
#pragma intrinsic (inp, outp, _enable, _disable)
#define gotoxy(x,y) _settextposition(y,x)
#define clrscr()_clearscreen(_GWINDOW)
#endif
/* ********** Inline Functions ************ */
/* -------- set_timer ----------- */
/* set timer i to count down in time t */
#define set timer(i,t) _disable();\
timers [i] =t;\
_enable();
/* -------- req_task ----------- */
/* request task i */
#define req_task(i)_disable();\
task_flag[i] = REQ_TASK;\
_enable();
/* -------resume_task -------- */
#define resume_task(i)_disable();\
task_flag[i] |= REQ_TASK;\
task_flag[i]++;\
_enable();
/* use following inline when ints are disabled */
/* -------- iset_timer ----------- */
/* set timer i to count down in time t */
#define iset_timer(i,t) timers[i]=t;
/* -------- ireq_task ----------- */
/* request task i ints disabled */
#define ireq_task(i) task_flag[i] = REQ_TASK;
/* -------iresume_task -------- */
/* resume task - interrupts disabled */
#define iresume_task(i) task_flag[i] |= REQ_TASK;\
task_flag[i]++;
/* ******** function prototypes ************* */
void main(void);
void task0(void);
void task1(void);
void init_rtc(void);  /* set up rtc/tasker */
void close_rtc(void); /* shut down rtc/tasker */
void init_cntr(unsigned count);
void interrupt rtcint(void);
void timr0(void);
void timr1(void);
void timr2(void);
/* ******* tiny exec variables ******** */
/* timer counter and function arrays */
unsigned timers[NUM_TIMERS] = { 0, 0, 0 };
void (*timer[NUM_TIMERS]) (void)={timr0,timr1,timr2};
/* ticks between task exec */
unsigned task_time = TASK_TICKS;
/* new timer ticks between old timer calls */
unsigned old_time=OLD_TICKS;
/* storage for old system clock int vector*/
void (interrupt far *oldtimer)(void);
/* task flag and function arrays */
unsigned task_flag[NUM_TASKS] = {0,0,IN_PROC};
void (*task[NUM_TASKS])(void) = {task0,task1,main};
int cur_task_num;/* indicates what is current task */
/* *************** functions ***************** */
/* combination real time clock and task exec */
void interrupt rtcint(void)
{
int i; /* local stack variable */
/* handle timers here, then 18.2Hz system clock
then do tasks
***************************************** */
for(i = 0;i < NUM_TIMERS;i++ )
   {
    if (timers[i])
      {
       if(!(--timers[i])) (*timer[i])();
      }
   }
/* call system clock 18.2 x per second */
if(!(--old_time))
  {
   old_time=OLD_TICKS;
#ifndef DEBUG
  (*oldtimer)();      /* call old timer here */
  }  else {
    outp (XT8259,CLR_INT);  /* clear int req */
#endif
  }
/* ------end of timers ---------- */
task_time--;    /* one less time tick */
if(task_time) return; /*not time for task exec */
task_time = TASK_TICKS; /* do tasks now*/
/* --------- task exec section ---------- */
for(i = 0; i < NUM_TASKS;i++)
 {
 if (task_flag[i] & IN_PROC)
  {
   cur_task_num = i;
   return;  /* found one in process so return */
  }
 if(task_flag[i] & REQ_TASK)
  {
   task_flag[i]=IN_PROC | (task_flag[i] & ~REQ_TASK);
       /* begin task */
       cur_task_num = i; /* current task in process*/
       _enable();        /* now allow interrupts */
       (*task[i])();     /* do task */
       _disable();
       task_flag[i] &: ~IN_PROC;  /* done */
      }
  }
}
/* ***************** init_rtc ******************** */
void init_rtc(void)  /* routine to set up rtc/tasker */
{
 _disable();
 oldtimer = _dos_getvect(TIMER); /* get timer vector*/
 _dos_setvect(TIMER,rtcint); /* set real time timer */
 init_cntr( RTC_COUNT);
 _enable();
}
/* ****************** close_rtc ******************* */
void close_rtc(void) /* shut down rtc/tasker */
{
 _disable();
 _dos_setvect(TIMER,oldtimer); /* restore timer */
 init_cntr( NORMAL_COUNT);
 _enable();
}
/* *************** init_cntr ***************** */
void init_cntr(unsigned count)
{
 int lowb, highb;
 lowb = count % 256;
 highb = count / 256;
 outp (TIMR1_C,LOAD_T1); /* load count for 8253 */
 outp (TIMRI_T1, lowb);
 outp (TIMRI_T1, highb);
}
/* **** Example main and tasks *********** */
void set_beep(void);
void clr_beep(void);
void tsk1sub1 (void); /*subtasks for task 1 */
void tsk1sub2 (void);
int beep_len = 6; /* default to about 1/3 sec */

#define SPKR_CNTRL 0x61
#define SPKR_ENABLE 0x03
#define SPKR_DISABLE 0xfc

int t0_flag =0, t1_flag = 0;
int timer0= 0,timer1 = 0,timer2 = 0;
int state_of_tasks[NUM_TASKS];
int state_of_timers[NUM_TIMERS];
/* --------- main ------------ */
void main (void)
{
int i,oldt0,oldtmr0=0,endit = 1;
time_t t;
#ifndef DEBUG
 init_rtc(); /* open rtc */
#endif
clrscr();
gotoxy(1,1);
printf("Counts for t0_flag t1_flag ");
printf("timer0 timer1 timer2");
gotoxy(1,4);
printf("task and timer flag images");
gotoxy(1,5);
printf("task0 task1 timer0 timer1 timer2");
set_timer(0,18); /* begin timer 0 in 12 ticks */
req_task(0);     /* request task 0 to run once */
set_timer(1,90); /*begin timer 1 in 90 ticks */
while(endit)
 {
  if(kbhit())
   {
    switch(getch()){
     case '\x1b':  /* exit on Esc */
      endit = 0;
      break;
     case ' ':
      req_task(1); /* do a beep*/
      break;
     default:
      break;
    } /* end of switch */
  } /* end of if */
if(t0_flag != oldt0)
 {
  oldt0=t0_flag;
  gotoxy(1,6); /* show current tasks and timers */
  for(i=0;i<NUM_TASKS-1;i++)
    printf("%4x ",state_of_tasks[i]);
  for(i=0;i<NUM_TIMERS;i++)
    printf(" %4d ",state_of_timers[i]);
 }
#ifdef DEBUG
 rtcint();
#endif
gotoxy(1,2);
printf("           %4d    %4d     %4d   %4d   %4d",\
     t0_flag,t0_flag,timer0,timer1,timer2);
if(timer0 != oldtmr0)
   {
    oldtmr0 = timer0;
    t=time(NULL);
    gotoxy(1,20);
    printf("%s",ctime(&t));
   }
 } /* end of while */
#ifndef DEBUG
 close_rtc();  /* restore rtc */
#endif
}
/* *********** tasks ************** */
/* -------- task0 ----------- */
void task0(void)
{
/* get current timer and task states */
int i;
t0_flag++;
for(i=0;i<NUM_TIMERS;i++)
  state_of_timers[i] = timers[i];
for(i=0;i<NUM_TASKS;i++)
 state_of_tasks[i] = task_flag[i];
}
/* ------- task1 --------- */
void task1(void)
{ /* handle beeps - 2 subtasks */
static void (*subexec1[])(void) = {
  tsk1sub1,
  tsk1sub2
  };
t1_flag++;
(*subexec1[task_flag[cur_task_num] & 0x01])();
} /* end task 1 */
void tsk1sub1 (void)
{                /* turn on beep */
outp(SPKR_CNTRL,inp(SPKR_CNTRL)| SPKR_ENABLE);
set_timer(2,beep_len); /*beep length*/
}
void tsk1sub2(void)
{              /* turn off beep here */
outp(SPKR_CNTRL, inp(SPKR_CNTRL) & SPKR_DISABLE);
}
/* ***************** timers ********************** */
/*Note - timers are run with interrupts disabled*/
void timr0(void)
{
timer0++; /* count of timer runs */
timers[0]=18; /* set time again */
}
void timr1(void)
{
timer1++; /* count of timer runs */
timers[1]=2; /* set timer again */
ireq_task(0);  /* request task 0 */
}
void timr2(void)
/* beep turn off timer */
{
timer2++; /* count of timer runs */
iresume_task(1);
}
/* End of File */

November 1992/A Simple Real-Time Executive/Sidebar

Real-Time Resources


An important facet to working in real-time is how resources can be used. Resources include both software, such as the runtime library functions, and computer hardware. In MS-DOS, resource allocation may requires extra consideration since both MS-DOS and the typical compiler libraries were not designed for sharing. There is often little specific documentation provided concerning the multi-tasking of applications. Most C compilers do provide interrupt programming capabilities, so some documentation exists concerning their use for that purpose.

Real-time systems essentially operate as sophisticated interrupts. Those functions and activities that are available for use inside interrupts should be usable without restrictions. Those that cannot be used inside interrupts must only be used in a restricted fashion.

The term reentrant is used to describe those functions that may be used without restriction. A reentrant function is one that may be run, during an interrupt for example, while the processor was already running the function in the main program. The basic criterion for reentrancy is whether or not a function affects something that is shared. Functions that modify hardware or variables with a scope beyond the function should be automatically suspect. Both Microsoft and Borland provide lists of reentrant routines, or those that can be made reentrant by compiler switches.

There are several ways to work around sharing problems. The simplest is to keep all activities of a particular type within one task level. Another technique is to disable interrupts during small sections of nonreentrant code. Flags can be used to control program flow in order to avoid problems. Overall program design also provides you with the ability to work around problem areas.

Your code will also contain sections that are not reentrant. Manipulation of timers and task flags is not reentrant, so the provided functions disable interrupts during the time while they are doing the manipulation.

Any variable that can change in an interrupt or higher priority task may need protection for both reading and writing during lower priority tasks. Variables modified in more than one task can cause problems when a lower priority task is interrupted between the reading and writing of a variable which is then modified during the higher priority task. The modification done in the higher priority task will be overwritten by the lower priority. For variables that require more than one machine language instruction to read, it is possible to end up with halves of two different numbers if the variable is changed by a higher priority task.

Errors causes by these problems can be severe. They may also occur on an infrequent basis and never be observed during development and testing.

November 1992/A Simple Real-Time Executive/Sidebar

Floating-Point and Coprocessors


There are three ways to deal with floating point:

union {
char state[108];
int control;
} coproc;
Tasks using the coprocessor must save and restore its context using assembly language instructions.

/* Coprocessor Context Save *
* for in line assembly     */
asm fsave coproc.state
asm fldcw coproc.control
.
/* floating point operations */
.
.
/* Coprocessor Context Restore */

asm frstor coproc.state
Coprocessor hardware provides a reentrancy problem for multitasking. To save the context of an 80x87 coprocessor, 94 bytes are needed for the 8087 or 80287, 108 bytes for 80387 coprocessors.

November 1992/A Simple Real-Time Executive/Sidebar

DOS and Multitasking


MS-DOS access provides a good example of the need to keep activities at a particular level. Generating all DOS calls in the background task will prevent any inadvertent research into MS-DOS reentrancy.

While there is an indos flag that can permit DOS to have some reentrancy, for the purpose of this article it is best to assume DOS has none. Refer to books and articles about "terminate and stay resident" programs for details.

Many standard library functions also have error calls to MS-DOS in routines that do not normally access MS-DOS. Even floating-point operations can generate exceptions such as divide by zero. In real-time applications it is usually best to prevent the program from encountering fault conditions, where possible.

November 1992/A Simple Real-Time Executive/Sidebar

Reentrant Function Lists


Both Microsoft and Borland provide lists of functions that are reentrant. There appear to be a number of additional functions that are reentrant except when they report error messages via DOS.

Reentrant Functions for Microsoft C 5.1

abs      labs     memset    strcmpi   strnset
atoi     lfind    mkdir     strcpy    strrchr
atol     lsearch  movedata  stricmp   strrev
bsearch  memccpy  putch     strlen    strset
chdir    memcpy   strcat    strncmp   srstr
getpid   memcmp   segread   strncat   strupr
halloc   memcpy   strcat    strncmp   swab
hfree    memicmp  strchr    strncpy   tolower
itoa     memmove  strcmp    strnicmp  toupper

Reentrant Functions for Borland C++ 3.0

Note: /oi option in-line functions

memchr   memcmp   memset   stpcpy   strcat
strchr   strcmp   strcpy   strlen   strncat
strncmp  strncpy  strnset  strrchr  rotl
rotr     fabs     alloca

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.