![]() |
|
| << Previous | Index | Next >> | |
| | |
In a multitasking environment, more than one task (each representing a sequence of operations) can appear to execute in parallel. In reality, a single processor can only execute one instruction at a time. If an application has multiple tasks to perform, multitasking software can usually take advantage of natural delays in each task to increase the overall performance of the system. Each task can do some of its work while the other tasks are waiting for an event, or for something to do. In this way, the tasks execute almost in parallel.
There are two types of multitasking available for developing applications in Dynamic C: preemptive and cooperative. In a cooperative multitasking environment, each well-behaved task voluntarily gives up control when it is waiting, allowing other tasks to execute. Dynamic C has language extensions, costatements and cofunctions, to support cooperative multitasking.
Preemptive multitasking is supported by the slice statement, which allows a computation to be divided into small slices of a few milliseconds each, and by the µC/OS-II real-time kernel.
5.1 Cooperative Multitasking
In the absence of a preemptive multitasking kernel or operating system, a programmer given a real-time programming problem that involves running separate tasks on different time scales will often come up with a solution that can be described as a big loop driving state machines.
Within this endless loop, tasks are accomplished by small fragments of a program that cycle through a series of states. The state is typically encoded as numerical values in C variables.
State machines can become quite complicated, involving a large number of state variables and a large number of states. The advantage of the state machine is that it avoids busy waiting, which is waiting in a loop until a condition is satisfied. In this way, one big loop can service a large number of state machines, each performing its own task, and no one is busy waiting.
The cooperative multitasking language extensions added to Dynamic C use the big loop and state machine concept, but C code is used to implement the state machine rather than C variables. The state of a task is remembered by a statement pointer that records the place where execution of the block of statements has been paused to wait for an event.
To multitask using Dynamic C language extensions, most application programs will have some flavor of this simple structure:
main() {
int i;
while(1) { // endless loop for multitasking framework
costate { // task 1
. . . // body of costatement
}
costate { // task 2
... // body of costatement
}
}
}
5.2 A Real-Time Problem
The following sequence of events is common in real-time programming.
- Wait for a pushbutton to be pressed.
- Turn on the first device.
- Wait 60 seconds.
- Turn on the second device.
- Wait 60 seconds.
- Turn off both devices.
- Go back to the start.
The most rudimentary way to perform this function is to idle ("busy wait") in a tight loop at each of the steps where waiting is specified. But most of the computer time will used waiting for the task, leaving no execution time for other tasks.
5.2.1 Solving the Real-Time Problem with a State Machine
Here is what a state machine solution might look like.
If there are other tasks to be run, this control problem can be solved better by creating a loop that processes a number of tasks. Now each task can relinquish control when it is waiting, thereby allowing other tasks to proceed. Each task then does its work in the idle time of the other tasks.
5.3 Costatements
Costatements are Dynamic C extensions to the C language which simplify implementation of state machines. Costatements are cooperative because their execution can be voluntarily suspended and later resumed. The body of a costatement is an ordered list of operations to perform -- a task. Each costatement has its own statement pointer to keep track of which item on the list will be performed when the costatement is given a chance to run. As part of the startup initialization, the pointer is set to point to the first statement of the costatement.
The statement pointer is effectively a state variable for the costatement or cofunction. It specifies the statement where execution is to begin when the program execution thread hits the start of the costatement.
All costatements in the program, except those that use pointers as their names, are initialized when the function chain
_GLOBAL_INITis called._GLOBAL_INITis called automatically bypremainbeforemainis called. Calling_GLOBAL_INITfrom an application program will cause reinitialization of anything that was initialized in the call made bypremain.5.3.1 Solving the Real-Time Problem with Costatements
The Dynamic C costatement provides an easier way to control the tasks. It is relatively easy to add a task that checks for the use of an emergency stop button and then behaves accordingly.
The solution is elegant and simple. Note that the second costatement looks much like the original description of the problem. All the branching, nesting and variables within the task are hidden in the implementation of the costatement and its
waitforstatements.5.3.2 Costatement Syntax
The keyword
costateidentifies the statements enclosed in the curly braces that follow as a costatement.costate [ name [state] ] { [ statement | yield; | abort; |
waitfor( expression ); ] . . .}
namecan be one of the following:
A valid C name not previously used. This results in the creation of a structure of type
CoDataof the same name.The name of a local or global
CoDatastructure that has already been definedA pointer to an existing structure of type
CoDataCostatements can be named or unnamed. If
nameis absent the compiler creates an unnamed structure of typeCoDatafor the costatement.
statecan be one of the following:If
stateis absent, a named costatement is initialized in a pausedinit_oncondition. This means that the costatement will not execute untilCoBegin()orCoResume()is executed. It will then execute once and become inactive again.Unnamed costatements are
always_on. You cannot specifyinit_onwithout specifying a costatement name.5.3.3 Control Statements
This section describes the control statements identified by the keywords: waitfor, yield and abort.
waitfor ( expression );
The keyword
waitforindicates a specialwaitforstatement and not a function call. Each timewaitforis executed, expression is evaluated. If true (non-zero), execution proceeds to the next statement; otherwise a jump is made to the closing brace of the costatement or cofunction, with the statement pointer continuing to point to thewaitforstatement. Any valid C function that returns a value can be used in awaitforstatement.Figure 5-2 shows the execution thread through a costatement when a
waitforevaluates to false. The diagram on the left side shows which statements are executed the first time through the costatement. The diagram on the right shows that when the execution thread again reaches the costatement the only statement executed is thewaitfor. As long as thewaitforcontinues to evaluate to false, it will be the only statement executed within the costatement.
Figure 5-3 shows the execution thread through a costatement when a
waitforevaluates to true.
yield
The
yieldstatement makes an unconditional exit from a costatement or a cofunction. Execution continues at the statement followingyieldthe next time the costatement or cofunction is encountered by the execution thread.
abort
The
abortstatement causes the costatement or cofunction to terminate execution. If a costatement isalways_on, the next time the program reaches it, it will restart from the top. If the costatement is notalways_on, it becomes inactive and will not execute again until turned on by some other software.
A costatement can have as many C statements, including
abort,yield, andwaitforstatements, as needed. Costatements can be nested.5.4 Advanced Costatement Topics
Each costatement has a structure of type
CoData. This structure contains state and timing information. It also contains the address inside the costatement that will execute the next time the program thread reaches the costatement. A value of zero in the address location indicates the beginning of the costatement.5.4.1 The CoData Structure
typedef struct {
char CSState;
unsigned int lastlocADDR;
char lastlocCBR;
char ChkSum;
char firsttime;
union{
unsigned long ul;
struct {
unsigned int u1;
unsigned int u2;
} us;
} content;
char ChkSum2;
} CoData;5.4.2 CoData Fields
This section describes the fields of the CoData structure.
CSState
The
CSStatefield contains two flags,STOPPEDandINIT. The possible flag values and their meaning are in the table below.
The function
isCoDone()returns true (1) if both theSTOPPEDandINITflags are set. The functionisCoRunning()returns true (1) if theSTOPPEDflag is not set.The
CSStatefield applies only if the costatement has a name. TheCSStateflag has no meaning for unnamed costatements or cofunctions.Last Location
The two fields
lastlocADDRandlastlocCBRrepresent the 24-bit address of the location at which to resume execution of the costatement. IflastlocADDRis zero (as it is when initialized), the costatement executes from the beginning, subject to theCSStateflag. IflastlocADDRis nonzero, the costatement resumes at the 24-bit address represented bylastlocADDRandlastlocCBR.These fields are zeroed whenever one of the following is true:
the
CoDatastructure is initialized by a call to _GLOBAL_INIT,CoBeginorCoResetthe costatement is executed to completion
the costatement is aborted.
Check Sum
The
ChkSumfield is a one-byte check sum of the address. (It is the exclusive-or result of the bytes inlastlocADDRandlastlocCBR.) IfChkSumis not consistent with the address, the program will generate a run-time error and reset. The check sum is maintained automatically. It is initialized by_GLOBAL_INIT,CoBeginandCoReset.First Time
The
firsttimefield is a flag that is used by awaitfor, orwaitfordonestatement. It is set to 1 before the statement is evaluated the first time. This aids in calculating elapsed time for the functionsDelayMs,DelaySec,DelayTicks,IntervalTick,IntervalMs, andIntervalSec.Content
The
contentfield (a union) is used by the costatement or cofunction delay routines to store a delay count.Check Sum 2
The
ChkSum2field is currently unused.5.4.3 Pointer to CoData Structure
To obtain a pointer to a named costatement's CoData structure, do the following:
The storage class of a named CoData structure must be
static.5.4.4 Functions for Use With Named Costatements
For detailed function descriptions, please see the Dynamic C Function Reference Manual or select Function Lookup/Insert from Dynamic C's Help menu (keyboard shortcut is <Ctrl-H>).
All of these functions are in
COSTATE.LIB. Each one takes a pointer to aCoDatastruct as its only parameter.int isCoDone(CoData* p);
- This function returns true if the costatement pointed to by
phas completed.int isCoRunning(CoData* p);
- This function returns true if the costatement pointed to by
pwill run if given a continuation call.void CoBegin(CoData* p);
- This function initializes a costatement's
CoDatastructure so that the costatement will be executed next time it is encountered.void CoPause(CoData* p);
- This function will change
CoDataso that the associated costatement is paused. When a costatement is called in this state it does an implicit yield until it is released by a call fromCoResumeorCoBegin.void CoReset(CoData* p);
- This function initializes a costatement's CoData structure so that the costatement will not be executed the next time it is encountered unless the costatement is declared
always_on.void CoResume(CoData* p);
- This function unpauses a paused costatement. The costatement resumes the next time it is called.
5.4.5 Firsttime Functions
In a function definition, the keyword
firsttimecauses the function to have an implicit first parameter: a pointer to the CoData structure of the costatement that calls it. User-definedfirsttimefunctions are allowed.The following
firsttimefunctions are defined inCOSTATE.LIB.
DelayMs(), DelaySec(), DelayTicks()
IntervalMs(), IntervalSec(), IntervalTick()For more information see the Dynamic C Function Reference Manual. These functions should be called inside a
waitforstatement because they do not yield while waiting for the desired time to elapse, but instead return 0 to indicate that the desired time has not yet elapsed.5.4.6 Shared Global Variables
The variables
SEC_TIMER,MS_TIMERandTICK_TIMERare shared, making them atomic when being updated. They are defined and initialized inVDRIVER.LIB. They are updated by the periodic interrupt and are used byfirsttimefunctions. They should not be modified by an application program. Costatements and cofunctions depend on these timer variables being valid for use inwaitforstatements that call functions that read them. For example, the following statement will accessSEC_TIMER.waitfor(DelaySec(3));5.5 Cofunctions
Cofunctions, like costatements, are used to implement cooperative multitasking. But, unlike costatements, they have a form similar to functions in that arguments can be passed to them and a value can be returned (but not a structure).
The default storage class for a cofunction's variables is
Instance. Aninstancevariable behaves like astaticvariable, i.e., its value persists between function calls. Each instance of an Indexed Cofunction has its own set of instance variables. The compiler directive#classdoes not change the default storage class for a cofunction's variables.All cofunctions in the program are initialized when the function chain
_GLOBAL_INITis called. This call is made bypremain.5.5.1 Cofunction Syntax
A cofunction definition is similar to the definition of a C function.
cofunc|scofunc type [name][[dim]]([type arg1, ..., type argN])
{ [ statement | yield; | abort; | waitfor(expression);]... }cofunc, scofunc
The keywords
cofuncorscofunc(a single-user cofunction) identify the statements enclosed in curly braces that follow as a cofunction.type
Whichever keyword (
cofuncorscofunc)is used is followed by the data type returned (void,int, etc.).name
A
namecan be any valid C name not previously used. This results in the creation of a structure of typeCoDataof the same name.dim
The cofunction
namemay be followed by a dimension if an indexed cofunction is being defined.cofunction arguments (arg1, . . ., argN)
As with other Dynamic C functions, cofunction arguments are passed by value.
cofunction body
A cofunction can have as many C statements, including
abort,yield,waitfor, andwaitfordonestatements, as needed. Cofunctions can contain calls to other cofunctions.5.5.2 Calling Restrictions
You cannot assign a cofunction to a function pointer then call it via the pointer.
Cofunctions are called using a
waitfordonestatement. Cofunctions and thewaitfordonestatement may return an argument value as in the following example.
int j,k,x,y,z;
j = waitfordone x = Cofunc1;
k = waitfordone{ y=Cofunc2(...); z=Cofunc3(...); }
The keyword
waitfordone(can be abbreviated to the keywordwfd) must be inside a costatement or cofunction. Since a cofunction must be called from inside awfdstatement, ultimately awfdstatement must be inside a costatement. If only one cofunction is being called bywfdthe curly braces are not needed.The
wfdstatement executes cofunctions andfirsttimefunctions. When all the cofunctions andfirsttimefunctions listed in thewfdstatement are complete (or one of them aborts), execution proceeds to the statement followingwfd. Otherwise a jump is made to the ending brace of the costatement or cofunction where thewfdstatement appears and when the execution thread comes around again control is given back towfd.In the example above,
x,yandzmust be set byreturnstatements inside the called cofunctions. Executing a return statement in a cofunction has the same effect as executing the end brace. In the example above, the variablekis a status variable that is set according to the following scheme. If no abort has taken place in any cofunction,kis set to 1, 2, ..., n to indicate which cofunction inside the braces finished executing last. If an abort takes place,kis set to -1, -2, ..., -n to indicate which cofunction caused the abort.5.5.2.1 Costate Within a Cofunc
In all but trivial cases (where the costate is really not necessary), a costate within a cofunc causes execution problems ranging from never completing the cofunc to unexpected interrupts or target lockups. To avoid these problems, do not introduce costates with nested wfd cofuncs into a cofunc. If you find yourself coding such a thing, consider these alternatives:
Intermediate regular functions can be used between the cofuncs to isolate them.
A regular
waitfor(function)can be substituted for the top level costate'swfdcofunction.The nested costates with wfd cofuncs can be moved up into the body of the calling function, replacing the top-level costate with the wfd cofunc.
A compiler error will be generated if a costate is found within a cofunction.
5.5.2.2 Using the IX Register
Functions called from within a cofunction may use the IX register if they restore it before the cofunction is exited, which includes an exit via an incomplete
waitfordonestatement.In the case of an application that uses the #useix directive, the IX register will be corrupted when any stack-variable using function is called from within a cofunction, or if a stack-variable using function contains a call to a cofunction.
5.5.3 CoData Structure
The CoData structure discussed in Section 5.4.1 applies to cofunctions; each cofunction has an associated CoData structure.
5.5.4 Firsttime Functions
The
firsttimefunctions discussed in Firsttime Functions can also be used inside cofunctions. They should be called inside awaitforstatement. If you call these functions from inside awfdstatement, no compiler error is generated, but, since these delay functions do not yield while waiting for the desired time to elapse, but instead return 0 to indicate that the desired time has not yet elapsed, thewfdstatement will consider a return value to be completion of thefirsttimefunction and control will pass to the statement following thewfd.5.5.5 Types of Cofunctions
There are three types of cofunctions: simple, indexed and single-user. Which one to use depends on the problem that is being solved. A single-user, indexed cofunction is not valid.
5.5.5.1 Simple Cofunction
A simple cofunction has only one instance and is similar to a regular function with a costate taking up most of the function's body.
5.5.5.2 Indexed Cofunction
An indexed cofunction allows the body of a cofunction to be called more than once with different parameters and local variables. The parameters and the local variable that are not declared static have a special lifetime that begins at a first time call of a cofunction instance and ends when the last curly brace of the cofunction is reached or when an
abortorreturnis encountered.The indexed cofunction call is a cross between an array access and a normal function call, where the array access selects the specific instance to be run.
Typically this type of cofunction is used in a situation where N identical units need to be controlled by the same algorithm. For example, a program to control the door latches in a building could use indexed cofunctions. The same cofunction code would read the key pad at each door, compare the passcode to the approved list, and operate the door latch. If there are 25 doors in the building, then the indexed cofunction would use an index ranging from 0 to 24 to keep track of which door is currently being tested. An indexed cofunction has an index similar to an array index.
waitfordone{ ICofunc[n](...); ICofunc2[m](...); }The value between the square brackets must be positive and less than the maximum number of instances for that cofunction. There is no runtime checking on the instance selected, so, like arrays, the programmer is responsible for keeping this value in the proper range.
5.5.5.2.1 Indexed Cofunction Restrictions
Costatements are not supported inside indexed cofunctions. Single user cofunctions can not be indexed.
5.5.5.3 Single User Cofunction
Since cofunctions are executing in parallel, the same cofunction normally cannot be called at the same time from two places in the same big loop. For example, the following statement containing two simple cofunctions will generally cause a fatal error.
waitfordone{ cofunc_nameA(); cofunc_nameA();}This is because the same cofunction is being called from the second location after it has already started, but not completed, execution for the call from the first location. The cofunction is a state machine and it has an internal statement pointer that cannot point to two statements at the same time.
Single-user cofunctions can be used instead. They can be called simultaneously because the second and additional callers are made to wait until the first call completes. The following statement, which contains two calls to single-user cofunction, is okay.
waitfordone( scofunc_nameA(); scofunc_nameA();}loopinit()
This function should be called in the beginning of a program that uses single-user cofunctions. It initializes internal data structures that are used by
loophead().loophead()
This function should be called within the "big loop" in your program. It is necessary for proper single-user cofunction abandonment handling.
Example
// echoes characters
main() {
int c;
serAopen(19200);
loopinit();
while (1) {
loophead();
costate {
wfd c = cof_serAgetc();
wfd cof_serAputc(c);
}
}
serAclose();
}5.5.6 Types of Cofunction Calls
A
wfdstatement makes one of three types of calls to a cofunction.5.5.6.1 First Time Call
A first time call happens when a
wfdstatement calls a cofunction for the first time in that statement. After the first time, only the originalwfdstatement can give this cofunction instance continuation calls until either the instance is complete or until the instance is given another first time call from a different statement. The lifetime of a cofunction instance stretches from a first time call until its terminal call or until its next first time call.5.5.6.2 Continuation Call
A continuation call is when a cofunction that has previously yielded is given another chance to run by the enclosing
wfdstatement. These statements can only call the cofunction if it was the last statement to give the cofunction a first time call or a continuation call.5.5.6.3 Terminal Call
A terminal call ends with a cofunction returning to its
wfdstatement without yielding to another cofunction. This can happen when it reaches the end of the cofunction and does an implicit return, when the cofunction does an explicit return, or when the cofunction aborts.5.5.7 Special Code Blocks
The following special code blocks can appear inside a cofunction.
everytime
{ statements }
- This must be the first statement in the cofunction. The everytime statement block will be executed on every
cofunccontinuation call no matter where the statement pointer is pointing. After the everytime statement block is executed, control will pass to the statement pointed to by the cofunction's statement pointer.
- The everytime statement block will not be executed during the initial
cofuncentry call.abandon { statements }
- This keyword applies to single-user cofunctions only and must be the first statement in the body of the cofunction. The statements inside the curly braces will be executed if the single-user cofunction is forcibly abandoned. A call to
loophead()(defined inCOFUNC.LIB) is necessary for abandon statements to execute.Example
Samples/COFUNC/COFABAND.Cillustrates the use ofabandon.In this example two tasks in
main()are requesting access toSCofTest. The first request is honored and the second request is held. Whenloophead()notices that the first caller is not being called each time around the loop, it cancels the request, calls the abandonment code and allows the second caller in.5.5.8 Solving the Real-Time Problem with Cofunctions
Cofunctions, with their ability to receive arguments and return values, provide more flexibility and specificity than our previous solutions.
Using cofunctions, new machines can be added with only trivial code changes. Making
buttonpushed()a cofunction allows more specificity because the value returned can indicate a particular button in an array of buttons. Then that value can be passed as an argument to the cofunctionsturnondeviceandturnoffdevice.5.6 Patterns of Cooperative Multitasking
Sometimes a task may be something that has a beginning and an end. For example, a cofunction to transmit a string of characters via the serial port begins when the cofunction is first called, and continues during successive calls as control cycles around the big loop. The end occurs after the last character has been sent and the
waitfordonecondition is satisified. This type of a call to a cofunction might look like this:waitfordone{ SendSerial("string of characters"); }
[ next statement ]The next statement will execute after the last character is sent.
Some tasks may not have an end. They are endless loops. For example, a task to control a servo loop may run continuously to regulate the temperature in an oven. If there are a a number of tasks that need to run continuously, then they can be called using a single
waitfordonestatement as shown below.costate {
waitfordone { Task1(); Task2(); Task3(); Task4(); }
[ to come here is an error ]
}Each task will receive some execution time and, assuming none of the tasks is completed, they will continue to be called. If one of the cofunctions should abort, then the waitfordone statement will abort, and corrective action can be taken.
5.7 Timing Considerations
In most instances, costatements and cofunctions are grouped as periodically executed tasks. They can be part of a real-time task, which executes every n milliseconds as shown below using costatements.
If all goes well, the first costatement will be executed at the periodic rate. The second costatement will, however, be delayed by the first costatement. The third will be delayed by the second, and so on. The frequency of the routine and the time it takes to execute comprise the granularity of the routine.
If the routine executes every 25 milliseconds and the entire group of costatements executes in 5 to 10 milliseconds, then the granularity is 30 to 35 milliseconds. Therefore, the delay between the occurrence of a
waitforevent and the statement following thewaitforcan be as much as the granularity, 30 to 35 ms. The routine may also be interrupted by higher priority tasks or interrupt routines, increasing the variation in delay.The consequences of such variations in the time between steps depends on the program's objective. Suppose that the typical delay between an event and the controller's response to the event is 25 ms, but under unusual circumstances the delay may reach 50 ms. An occasional slow response may have no consequences whatsoever. If a delay is added between the steps of a process where the time scale is measured in seconds, then the result may be a very slight reduction in throughput.
If there is a delay between sensing a defective product on a moving belt and activating the reject solenoid that pushes the object into the reject bin, the delay could be serious. If a critical delay cannot exceed 40 ms, then a system will sometimes fail if its worst-case delay is 50 ms.
5.7.1 waitfor Accuracy Limits
If an idle loop is used to implement a delay, the processor continues to execute statements almost immediately (within nanoseconds) after the delay has expired. In other words, idle loops give precise delays. Such precision cannot be achieved with
waitfordelays.A particular application may not need very precise delay timing. Suppose the application requires a 60-second delay with only 100 ms of delay accuracy; that is, an actual delay of 60.1 seconds is considered acceptable. Then, if the processor guarantees to check the delay every 50 ms, the delay would be at most 60.05 seconds, and the accuracy requirement is satisfied.
5.8 Overview of Preemptive Multitasking
In a preemptive multitasking environment, tasks do not voluntarily relinquish control. Tasks are scheduled to run by priority level and/or by being given a certain amount of time.
There are two ways to accomplish preemptive multitasking using Dynamic C. The first way is via a Dynamic C construct called the "slice" statement (described in Section 5.9). The second way is µC/OS-II, a real-time, preemptive kernel that runs on the Rabbit microprocessor and is fully supported by Dynamic C (described in Section 5.10).
5.9 Slice Statements
The
slicestatement, based on the costatement language construct, allows the programmer to run a block of code for a specific amount of time.5.9.1 Slice Syntax
slice ([context_buffer,] context_buffer_size, time_slice) [name]{[statement|yield;|abort;|waitfor(expression);]}context_buffer_size
This value must evaluate to a constant integer. The value specifies the number of bytes for the buffer
context_buffer. It needs to be large enough for worst-case stack usage by the user program and interrupt routines.time_slice
The amount of time in ticks for the slice to run. One tick = 1/1024 second.
name
When defining a named
slicestatement, you supply a context buffer as the first argument. When you define an unnamedslicestatement, this structure is allocated by the compiler.[statement | yield; | abort; | waitfor(expression);]
- The body of a
slicestatement may contain:
Regular C statements
yieldstatements to make an unconditional exit.
abortstatements to make an execution jump to the very end of the statement.
waitforstatements to suspend progress of the slice statement pending some condition indicated by the expression.5.9.2 Usage
The
slicestatement can run both cooperatively and preemptively all in the same framework. A slice statement, like costatements and cofunctions, can suspend its execution with anabort,yield, orwaitfor. It can also suspend execution with an implicityielddetermined by thetime_sliceparameter that was passed to it. A routine called from the periodic interrupt forms the basis for scheduling slice statements. It counts down the ticks and changes theslicestatement's context.5.9.3 Restrictions
Since a
slicestatement has its own stack, local auto variables and parameters cannot be accessed while in the context of aslicestatement. Any function called from the slice statement performs normally.Only one
slicestatement can be active at any time, which eliminates the possibility of nestingslicestatements or using aslicestatement inside a function that is either directly or indirectly called from aslicestatement. The only methods supported for leaving aslicestatement are completely executing the last statement in theslice, or executing anabort,yieldorwaitforstatement.The
return,continue,break, andgotostatements are not supported.Slice statements cannot be used with µC/OS-II or TCP/IP.
5.9.4 Slice Data Structure
Internally, the
slicestatement uses two structures to operate. When defining a namedslicestatement, you supply a context buffer as the first argument. When you define an unnamedslicestatement, this structure is allocated by the compiler. Internally, the context buffer is represented by theSliceBufferstructure below.
struct SliceData {
int time_out;
void* my_sp;
void* caller_sp;
CoData codata;
}struct SliceBuffer {
SliceData slice_data;
char stack[]; // fills rest of the slice buffer
};
5.9.5 Slice Internals
When a
slicestatement is given control, it saves the current context and switches to a context associated with theslicestatement. After that, the driving force behind theslicestatement is the timer interrupt. Each time the timer interrupt is called, it checks to see if aslicestatement is active. If aslicestatement is active, the timer interrupt decrements thetime_outfield in theslice'sSliceData. When the field is decremented to zero, the timer interrupt saves theslicestatement's context into theSliceBufferand restores the previous context. Once the timer interrupt completes, the flow of control is passed to the statement directly following theslicestatement. A similar set of events takes place when theslicestatement does an explicityield/abort/waitfor.5.9.5.1 Example 1
Two
slicestatements and a costatement will appear to run in parallel. Each block will run independently, but theslicestatement blocks will suspend their operation after 20 ticks forslice_aand 40 ticks forslice_b. Costate a will not release control until it either explicitly yields, aborts, or completes. In contrast,slice_awill run for at most 20 ticks, thenslice_bwill begin running. Costate a will get its next opportunity to run about 60 ticks after it relinquishes control.
main () {
int x, y, z;
...
for (;;) {
costate a {
...
}
slice(500, 20) { // slice_a
...
}
slice(500, 40) { // slice_b
...
}
}
}
5.9.5.2 Example 2
This code guarantees that the first slice starts on
TICK_TIMERevenly divisible by 80 and the second starts onTICK_TIMERevenly divisible by 105.
main() {
for(;;) {
costate {
slice(500,20) { // slice_a
waitfor(IntervalTick(80));
...
}
slice(500,50) { // slice_b
waitfor(IntervalTick(105);
...
}
}
}
}
5.9.5.3 Example 3
This approach is more complicated, but will allow you to spend the idle time doing a low-priority background task.
5.10 µC/OS-II
µC/OS-II is a simple, clean, efficient, easy-to-use real-time operating system that runs on the Rabbit microprocessor and is fully supported by the Dynamic C development environment. With Dynamic C, there is no fee to pay for the "Object Code Distribution License" that is usually required for embedding µC/OS-II in a product.
µC/OS-II is capable of intertask communication and synchronization via the use of semaphores, mailboxes, and queues. User-definable system hooks are supplied for added system and configuration control during task creation, task deletion, context switches, and time ticks.
For more information on µC/OS-II, please refer to Jean J. Labrosse's book, MicroC/OS-II, The Real-Time Kernel (ISBN: 0-87930-543-6). The data structures (e.g., Event Control Block) referenced in the Dynamic C µC/OS-II function descriptions are fully explained in Labrosse's book. It can be purchased at the Rabbit store, www.rabbit.com/store/, or at http://www.ucos-ii.com/.
The Dynamic C version of µC/OS-II has the new features and API changes available in version 2.51 of µC/OS-II. The documentation for these changes will be in the
/Samples/UCos-IIdirectory. The fileNewv251.pdfcontains all of the features added since version 2.00 andRelv251.pdfcontains release notes for version 2.51.The remainder of this section discusses the following:
Dynamic C enhancements to µC/OS-II
Tasking aware ISRs
Dynamic C library reentrancy
How to get a µC/OS-II application running
TCP/IP compatibility
API function descriptions
Debugging tips
5.10.1 Changes to µC/OS-II
Minor changes have been made to µC/OS-II to take full advantage of services provided by Dynamic C.
5.10.1.1 Ticks per Second
In most implementations of µC/OS-II,
OS_TICKS_PER_SECinforms the operating system of the rate at whichOSTimeTickis called; this macro is used as a constant to match the rate of the periodic interrupt. In µC/OS-II for the Rabbit, however, changing this macro will change the tick rate of the operating system set up duringOSInit. Usually, a real-time operating system has a tick rate of 10 Hz to 100 Hz, or 10-100 ticks per second. Since the periodic interrupt on the Rabbit occurs at a rate of 2 kHz, it is recommended that the tick rate be a power of 2 (e.g., 16, 32, or 64). Keep in mind that the higher the tick rate, the more overhead the system will incur.In the Rabbit version of µC/OS-II, the number of ticks per second defaults to 64. The actual number of ticks per second may be slightly different than the desired ticks per second if
TicksPerSecdoes not evenly divide 2048.Changing the default tick rate is done by simply defining
OS_TICKS_PER_SECto the desired tick rate before callingOSInit(). For example, to change the tick rate to 32 ticks per second:#define OS_TICKS_PER_SEC 32
...
OSInit();
...
OSStart();5.10.1.2 Task Creation
In a µC/OS-II application, stacks are declared as static arrays, and the address of either the top or bottom (depending on the CPU) of the stack is passed to
OSTaskCreate. In a Rabbit-based system, the Dynamic C development environment provides a superior stack allocation mechanism that µC/OS-II incorporates. Rather than declaring stacks as static arrays, the number of stacks of particular sizes are declared, and when a task is created using eitherOSTaskCreateorOSTaskCreateExt,only the size of the stack is passed, not the memory address. This mechanism allows a large number of stacks to be defined without using up root RAM.There are five macros located in
ucos2.libthat define the number of stacks needed of five different sizes. To have three 256-byte stacks, one 512-byte stack, two 1024-byte stacks, one 2048-byte stack, and no 4096-byte stacks, the following macro definitions would be used:#define STACK_CNT_256 3 // number of 256 byte stacks
#define STACK_CNT_512 1 // number of 512 byte stacks
#define STACK_CNT_1K 2 // number of 1K stacks
#define STACK_CNT_2K 1 // number of 2K stacks
#define STACK_CNT_4K 0 // number of 4K stacksThese macros can be placed into each µC/OS-II application so that the number of each size stack can be customized based on the needs of the application. Suppose that an application needs 5 tasks, and each task has a consecutively larger stack. The macros and calls to
OSTaskCreatewould look as follows#define STACK_CNT_256 2 // number of 256 byte stacks
#define STACK_CNT_512 2 // number of 512 byte stacks
#define STACK_CNT_1K 1 // number of 1K stacks
#define STACK_CNT_2K 1 // number of 2K stacks
#define STACK_CNT_4K 1 // number of 4K stacksOSTaskCreate(task1, NULL, 256, 0);
OSTaskCreate(task2, NULL, 512, 1);
OSTaskCreate(task3, NULL, 1024, 2);
OSTaskCreate(task4, NULL, 2048, 3);
OSTaskCreate(task5, NULL, 4096, 4);Note that
STACK_CNT_256is set to 2 instead of 1. µC/OS-II always creates an idle task which runs when no other tasks are in the ready state. Note also that there are two 512 byte stacks instead of one. This is because the program is given a 512 byte stack. If the application utilizes the µC/OS-II statistics task, then the number of 512 byte stacks would have to be set to 3. (Statistic task creation can be enabled and disabled via the macroOS_TASK_STAT_ENwhich is located inucos2.lib). If only 6 stacks were declared, one of the calls toOSTaskCreatewould fail.If an application uses
OSTaskCreateExt, which enables stack checking and allows an extension of the Task Control Block, fewer parameters are needed in the Rabbit version of µC/OS-II. Using the macros in the example above, the tasks would be created as follows:OSTaskCreateExt(task1, NULL, 0, 0, 256, NULL, OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR);OSTaskCreateExt(task2, NULL, 1, 1, 512, NULL, OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR);OSTaskCreateExt(task3, NULL, 2, 2, 1024, NULL, OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR);OSTaskCreateExt(task4, NULL, 3, 3, 2048, NULL, OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR);OSTaskCreateExt(task5, NULL, 4, 4, 4096, NULL, OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR);5.10.1.3 Restrictions
At the time of this writing, µC/OS-II for Dynamic C is not compatible with the use of slice statements. Also, see the function description for
OSTimeTickHook()for important information about preserving registers if that stub function is replaced by a user-defined function.Due to Dynamic C's stack allocation scheme, special care should be used when posting messages to either a mailbox or a queue. A message is simply a void pointer, allowing the application to determine its meaning. Since tasks can have their stacks in different segments, auto pointers declared on the stack of the task posting the message should not be used since the pointer may be invalid in another task with a different stack segment.
5.10.2 Tasking Aware Interrupt Service Routines (TA-ISR)
Special care must be taken when writing an interrupt service routine (ISR) that will be used in conjunction with µC/OS-II so that µC/OS-II scheduling will be performed at the proper time.
5.10.2.1 Interrupt Priority Levels
µC/OS-II for the Rabbit reserves interrupt priority levels 2 and 3 for interrupts outside of the kernel. Since the kernel is unaware of interrupts above priority level 1, interrupt service routines for interrupts that occur at interrupt priority levels 2 and 3 should not be written to be tasking aware. Also, a µC/OS-II application should only disable interrupts by setting the interrupt priority level to 1, and should never raise the interrupt priority level above 1.
5.10.2.2 Possible ISR Scenarios
There are several different scenarios that must be considered when writing an ISR for use with µC/OS-II. Depending on the use of the ISR, it may or may not have to be written so that it is tasking aware. Consider the scenario in Figure 5-7. In this situation, the ISR for Interrupt X does not have to be tasking aware since it does not re-enable interrupts before completion and it does not post to a semaphore, mailbox, or queue.
If, however, an ISR needs to signal a task to the ready state, then the ISR must be tasking aware. In the example in Figure 5-8, the TA-ISR increments the interrupt nesting counter, does the work necessary for the ISR, readies a higher priority task, decrements the nesting count, and returns to the higher priority task.
It may seem as though the ISR in Figure 5-8 does not have to increment and decrement the nesting count. However, this is very important. If the ISR for Interrupt X is called during an ISR that re-enables interrupts before completion, scheduling should not be performed when Interrupt X completes; scheduling should instead be deferred until the least nested ISR completes. Figure 5-9 shows an example of this situation.
As can be seen here, although the ISR for interrupt Z does not signal any tasks by posting to a semaphore, mailbox, or queue, it must increment and decrement the interrupt nesting count since it re-enables interrupts (
ipres) prior to finishing all of its work.5.10.2.3 General Layout of a TA-ISR
A TA-ISR is just like a standard ISR except that it does some extra checking and house-keeping. The following table summarizes when to use a TA-ISR.
Table 5-2. Use of TA-ISR TA-ISR Required?
1 Type 1--Leaves interrupts disabled and does not signal task to ready state
2 Type 2--Leaves interrupts disabled and signals task to ready state
3 Type 3--Reenables interrupts before completion
Figure 5-10 shows the logical flow of a TA-ISR.
5.10.2.3.1 Sample Code for a TA-ISR
Fortunately, the Rabbit BIOS and libraries provide all of the necessary flags to make TA-ISRs work. With the code found in Listing 1, minimal work is needed to make a TA-ISR function correctly with µC/OS-II. TA-ISRs allow µC/OS-II the ability to have ISRs that communicate with tasks as well as the ability to let ISRs nest, thereby reducing interrupt latency.
Just like a standard ISR, the first thing a TA-ISR does is to save the registers that it is going to use (1). Once the registers are saved, the interrupt source is cleared (2) and the nesting counter is incremented (3). Note that
bios_intnestingis a global interrupt nesting counter provided in the Dynamic C libraries specifically for tracking the interrupt nesting level. If anipresinstruction is executed (4) other interrupts can occur before this ISR is completed, making it necessary for this ISR to be a TA-ISR.If it is possible for the ISR to execute before µC/OS-II has been fully initialized and started multi-tasking, a check should be made (5) to insure that µC/OS-II is in a known state, especially if the TA-ISR signals a task to the ready state (6).
After the TA-ISR has done its necessary work (which may include making a higher priority task than is currently running ready to run),
OSIntExitmust be called (7). This µC/OS-II function determines the highest priority task ready to run, sets it as the currently running task, and sets the global flagbios_swpendif a context switch needs to take place. Interrupts are disabled since a context switch is treated as a critical section (8).If the TA-ISR decrements the nesting counter and the count does not go to zero, then the nesting level is saved in
bios_intnesting(9), the registers used by the TA-ISR are restored, interrupts are re-enabled (if not already done in (4)), and the TA-ISR returns (12). However, if decrementing the nesting counter in (9) causes the counter to become zero, thenbios_swpendmust be checked to see if a context switch needs to occur (10).If a context switch is not pending, then the nesting level is set (9) and the TA-ISR exits (12). If a context switch is pending, then the remaining context of the previous task is saved and a long call, which insures that the
xpcis saved and restored properly, is made tobios_intexit(11).bios_intexitis responsible for switching to the stack of the task that is now ready to run and executing a long call to switch to the new task. The remainder of (11) is executed when a previously preempted task is allowed to run again.Listing 1
#asm
taskaware_isr::
push af ;push regs needed by isr (1)
push hl ;clear interrupt source (2)
ld hl,bios_intnesting ;increase the nesting count (3)
inc (hl)
; ipres (optional) (4)
; do processing necessary for interrupt
ld a,(OSRunning) ;MCOS multitasking yet? (5)
or a
jr z,taisr_decnesting; possibly signal task to become ready (6)
call OSIntExit ;sets bios_swpend if higher
; prio ready (7)
taisr_decnesting:
push ip (8)
ipset 1ld hl,bios_intnesting ; nesting counter == 1?
dec (hl) (9)
jr nz,taisr_noswitchld a,(bios_swpend) ; switch pending? (10)
or a
jr z,taisr_noswitchpush de (11)
push bc
ex af,af'
push af
exx
push hl
push de
push bc
push iylcall bios_intexitpop iy
pop bc
pop de
pop hl
exx
pop af
ex af,af'
pop bc
pop detaisr_noswitch:
pop iptaisr_done:
pop hl (12)
pop af
ipres
ret
#endasm5.10.3 Library Reentrancy
When writing a µC/OS-II application, it is important to know which Dynamic C library functions are non-reentrant. If a function is non-reentrant, then only one task may access the function at a time, and access to the function should be controlled with a µC/OS-II semaphore. The following is a list of Dynamic C functions that are non-reentrant.
Table 5-3. Dynamic C Non-Reentrant Functions MATH.LIB
randg, randb, rand
RS232.LIB
All RTCLOCK.LIB
write_rtc, tm_wr
STDIO.LIB
kbhit, getchar, gets, getswf, selectkey
STRING.LIB
atof1, atoi1, strtok
SYS.LIB
clockDoublerOn, clockDoublerOff, useMainOsc, useClockDivider, use32kHzOsc
VDRIVER.LIB
VdGetFreeWd, VdReleaseWd
XMEM.LIB
WriteFlash
JRIO.LIB
digOut, digOn, digOff, jrioInit, anaIn, anaOut, cof_anaIn
JR485.LIB
All
1 reentrant but sets the global _xtoxErrflag
The Dynamic C serial port functions (
RS232.LIBfunctions) should be used in a restricted manner with µC/OS-II. Two tasks can use the same port as long as both are not reading, or both are not writing; i.e., one task can read from serial port X and another task can write to serial port X at the same time without conflict.5.10.4 How to Get a µC/OS-II Application Running
µC/OS-II is a highly configureable, real-time operating system. It can be customized using as many or as few of the operating system's features as needed. This section outlines:
The configuration constants used in µC/OS-II
How to override the default configuration supplied in
UCOS2.LIBThe necessary steps to get an application running
It is assumed that the reader has a familiarity with µC/OS-II or has a µC/OS-II reference (MicroC/OS-II, The Real-Time Kernel by Jean J. Labrosse is highly recommended).
5.10.4.1 Default Configuration
µC/OS-II usually relies on the include file
os_cfg.hto get values for the configuration constants. In the Dynamic C implementation of µC/OS-II, these constants, along with their default values, are inos_cfg.lib. A default stack configuration is also supplied inos_cfg.lib. µC/OS-II for the Rabbit uses a more intelligent stack allocation scheme than other µC/OS-II implementations to take better advantage of unused memory.The default configuration allows up to 10 normally created application tasks running at 64 ticks per second. Each task has a 512-byte stack. There are 2 queues specified, and 10 events. An event is a queue, mailbox or semaphore. You can define any combination of these three for a total of 10. If you want more than 2 queues, however, you must change the default value of
OS_MAX_QS.Some of the default configuration constants are:
OS_MAX_EVENTS
- Max number of events (semaphores, queues, mailboxes)
Default is 10OS_MAX_TASKS
- Maximum number of tasks (less stat and idle tasks)
Default is 10OS_MAX_QS
- Max number of queues in system
Default is 2OS_MAX_MEM_PART
- Max number of memory partitions
Default is 1OS_TASK_CREATE_EN
- Enable normal task creation
Default is 1OS_TASK_CREATE_EXT_EN
- Disable extended task creation
Default is 0OS_TASK_DEL_EN
- Disable task deletion
Default is 0OS_TASK_STAT_EN
- Disable statistics task creation
Default is 0OS_Q_EN
- Enable queue usage
Default is 1OS_MEM_EN
- Disable memory manager
Default is 0OS_MBOX_EN
- Enable mailboxes
Default is 1OS_SEM_EN
- Enable semaphores
Default is 1OS_TICKS_PER_SEC
- Number of ticks in one second
Default is 64STACK_CNT_256
- Number of 256 byte stacks (idle task stack)
Default is 1STACK_CNT_512
- Number of 512-byte stacks
(task stacks + initial program stack)
Default isOS_MAX_TASKS+1 (11)If a particular portion of µC/OS-II is disabled, the code for that portion will not be compiled, making the overall size of the operating system smaller. Take advantage of this feature by customizing µC/OS-II based on the needs of each application.
5.10.4.2 Custom Configuration
In order to customize µC/OS-II by enabling and disabling components of the operating system, simply redefine the configuration constants as necessary for the application.
#define OS_MAX_EVENTS 2
#define OS_MAX_TASKS 20
#define OS_MAX_QS 1
#define OS_MAX_MEM_PART 15
#define OS_TASK_STAT_EN 1
#define OS_Q_EN 0
#define OS_MEM_EN 1
#define OS_MBOX_EN 0
#define OS_TICKS_PER_SEC 64If a custom stack configuration is needed also, define the necessary macros for the counts of the different stack sizes needed by the application.
#define STACK_CNT_256 1 // idle task stack
#define STACK_CNT_512 2 // initial program + stat task stack
#define STACK_CNT_1K 10 // task stacks
#define STACK_CNT_2K 10 // number of 2K stacksIn the application code, follow the µC/OS-II and stack configuration constants with a
#use "ucos2.lib"statement. This ensures that the definitions supplied outside of the library are used, rather than the defaults in the library.This configuration uses 20 tasks, two semaphores, up to 15 memory partitions that the memory manager will control, and makes use of the statistics task. Note that the configuration constants for task creation, task deletion, and semaphores are not defined, as the library defaults will suffice. Also note that ten of the application tasks will each have a 1024 byte stack, ten will each have a 2048 byte stack, and an extra stack is declared for the statistics task.
5.10.4.3 Examples
The following sample programs demonstrate the use of the default configuration supplied in
UCOS2.LIBand a custom configuration which overrides the defaults.Example 1
In this application, ten tasks are created and one semaphore is created. Each task pends on the semaphore, gets a random number, posts to the semaphore, displays its random number, and finally delays itself for three seconds.
Looking at the code for this short application, there are several things to note. First, since µC/OS-II and slice statements are mutually exclusive (both rely on the periodic interrupt for a "heartbeat"),
#use "ucos2.lib"must be included in every µC/OS-II application (1). In order for each of the tasks to have access to the random number generator semaphore, it is declared as a global variable (2). In most cases, all mailboxes, queues, and semaphores will be declared with global scope. Next,OSInit()must be called before any other µC/OS-II function to ensure that the operating system is properly initialized (3). Before µC/OS-II can begin running, at least one application task must be created. In this application, all tasks are created before the operating system begins running (4). It is perfectly acceptable for tasks to create other tasks. Next, the semaphore each task uses is created (5). Once all of the initialization is done,OSStart()is called to start µC/OS-II running (6). In the code that each of the tasks run, it is important to note the variable declarations. Each task runs as an infinite loop and once this application is started, µC/OS-II will run indefinitely.// 1. Explicitly use µC/OS-II library
#use "ucos2.lib"void RandomNumberTask(void *pdata);// 2. Declare semaphore global so all tasks have access
OS_EVENT* RandomSem;void main(){
int i;// 3. Initialize OS internals
OSInit();for(i = 0; i < OS_MAX_TASKS; i++)// 4. Create each of the system tasks
OSTaskCreate(RandomNumberTask, NULL, 512, i);// 5. semaphore to control access to random number generator
RandomSem = OSSemCreate(1);// 6. Begin multitasking
OSStart();
}void RandomNumberTask(void *pdata)
{
OS_TCB data;
INT8U err;
INT16U RNum;OSTaskQuery(OS_PRIO_SELF, &data);
while(1)
{
// Rand is not reentrant, so access must be controlled via a semaphore.
OSSemPend(RandomSem, 0, &err);
RNum = (int)(rand() * 100);
OSSemPost(RandomSem);
printf("Task%d's random #: %d\n",data.OSTCBPrio,RNum);// Wait 3 seconds in order to view output from each task.
OSTimeDlySec(3);
}
}Example 2
This application runs exactly the same code as Example 1, except that each of the tasks are created with 1024-byte stacks. The main difference between the two is the configuration of µC/OS-II.
First, each configuration constant that differs from the library default is defined. The configuration in this example differs from the default in that it allows only two events (the minimum needed when using only one semaphore), 20 tasks, no queues, no mailboxes, and the system tick rate is set to 32 ticks per second (1). Next, since this application uses tasks with 1024 byte stacks, it is necessary to define the configuration constants differently than the library default (2). Notice that one 512 byte stack is declared. Every Dynamic C program starts with an initial stack, and defining
STACK_CNT_512is crucial to ensure that the application has a stack to use during initialization and before multi-tasking begins. Finallyucos2.libis explicitly used (3). This ensures that the definitions in (1 and 2) are used rather than the library defaults. The last step in initialization is to set the number of ticks per second viaOSSetTicksPerSec(4). The rest is identical to example 1 and is explained in the previous section.// 1. Define necessary configuration constants for uC/OS-II
#define OS_MAX_EVENTS 2
#define OS_MAX_TASKS 20
#define OS_MAX_QS 0
#define OS_Q_EN 0
#define OS_MBOX_EN 0
#define OS_TICKS_PER_SEC 32// 2. Define necessary stack configuration constants
#define STACK_CNT_512 1 // initial program stack
#define STACK_CNT_1K OS_MAX_TASKS // task stacks// 3. This ensures that the above definitions are used
#use "ucos2.lib"void RandomNumberTask(void *pdata);// Declare semaphore global so all tasks have access
OS_EVENT* RandomSem;void main(){
int i;// Initialize OS internals
OSInit();for(i = 0; i < OS_MAX_TASKS; i++){// Create each of the system tasks
OSTaskCreate(RandomNumberTask, NULL, 1024, i);}
// semaphore to control access to random number generator
RandomSem = OSSemCreate(1);// 4. Set number of system ticks per second
OSSetTicksPerSec(OS_TICKS_PER_SEC);// Begin multi-tasking
OSStart();
}void RandomNumberTask(void *pdata)
{
// Declare as auto to ensure reentrancy.
auto OS_TCB data;
auto INT8U err;
auto INT16U RNum;
OSTaskQuery(OS_PRIO_SELF, &data);
while(1)
{