You are on page 1of 6

<BACK

Embedded Systems Programming

BACK>

Implementing a Real-Time Kernel Deciding whether to buy or build a real-time operating system or kernel can be difficult. I recently chose to write my own kernel. My list of requirements was the same as it would be for many readers of this magazine. I wanted a portable, ROMable, preemptive, realtime multitasking kernel suitable for use on a variety of 8- and 16-bit systems. The resulting kernel, uCOS, was explained in an article in last month's issue.1 This month, we'll follow up on our discussion of the overall design with a look at the system services available to application programmers. The microprocessor used to demonstrate uCOS's capabilities is the Intel 80C188. All code and performance data assumes a small memory model. System services are written entirely in C and should be merged with the code in the UCOS.C and UCOS.H files. Those interested in using the code will find the source-code ties on the Embedded Systems Programming BBS at (415) 905-2689 and in library 12 of CLMFORUM on CompuServe. SEMAPHORES Let's start out with one of the most basic kernel services, the semaphore. A semaphore is used to provide mutual exclusion to a resource or to signal that an event has occurred. The semaphore used in uCOS is a 16-bit signed integer that must be initialized to a value between zero and 32,767 before its use. A semaphore is defined by the OS_SEM data structure (UCOS.H) and contains three fields: OSSemCnt, OSSemGrp, and OSSemTbl[]. Each semaphore requires 11 bytes of RAM. OSSemCnt contains the value of the semaphore and can be between -63 and 32,767. A positive value indicates how many tasks can access the resource at one time or how many times an event has occurred. When zero, the resource is either not available or the event didn't occur. Finally, when negative, the semaphore indicates how many tasks are waiting for the resource to become available or for the event to occur. OSSemGrp and OSSemTbl[8] are used to determine which tasks are waiting for the semaphore (when OSSemCnt is negative). I used the same scheme to determine which tasks are ready to run and which are waiting for the semaphore. (It might be helpful here to review the discussion of OSRdyGrp and OSRdyTbl [8] in last month's article.) When the resource is available, the highest priority task waiting for the semaphore is extracted from OSSemTbl[8]. This method yields a constant time to insert and remove a task that is pending on the semaphore. Before using a semaphore, you must allocate storage for it in your application and initialize it by calling OSSemInit(). A semaphore guarding a resource that may only be accessed by a single task at one time (a printer or display device, for example) is initialized to one. This semaphore is called a mutual-exclusion semaphore. You call OSSemPend() prior to using the resource. If the

semaphore count is one (meaning the resource is available), the semaphore is decremented, and the calling function is resumed. If the semaphore is negative or zero, it is decremented and the task is suspended until the resource is made available. From the point of view of the task, OSSemPend() just doesn't return for a while. You can also specify a nonzero timeout value instead of waiting indefinitely. When your code is done using the resource, it calls OSSemPost() to increment the semaphore and release the resource. You can also use a semaphore to signal that an event has occurred. In this case, you would initialize the semaphore to zero. The task that waits for the event to occur calls OSSemPend() and the task or interrupt-service routine (ISR) that detects the event calls OssemPost(). MAILBOXES UCOS allows a task or ISR to send a pointer-size variable (such as a message) to one or more tasks through a mailbox. Your application decides the significance of what the pointer points to. A mailbox is defined by the OS_MBOX data structure (in UCOS.H) and contains three fields: OSMboxMsg, OSMboxGrp, and OSMboxTbl[]. Like the semaphore, each mailbox requires 11 bytes of RAM. Before using a mailbox, you must allocate storage for it in your application and initialize the mailbox by calling OSMboxInit(). OSMboxMsg contains the message sent by a task or ISR when it contains a non-NULL pointer value. A NULL pointer indicates that the mailbox is empty. OSMboxGrp and OSMboxTbl[8] are used to determine which tasks are waiting for the mailbox, similar to their counterparts, OSSemGrp and OSSemTbl[8] in the semaphores. When a message is placed in the mailbox, the highest priority task waiting for the mailbox is extracted from OSMboxTbl[8]. A task calls OSMboxPend() to receive a message. If the mailbox contains a message, the mailbox is cleared and the message is returned to the task. If the mailbox is empty, the task is suspended until a message is sent to the mailbox. From the task's point of view, OSMboxPend() just doesn't return for a while. You can specify a nonzero timeout value instead of waiting indefinitely for a message. To send a message to a mailbox, a task or ISR calls OSMboxPost(). An error code is returned if the mailbox already contains a message. QUEUES A mailbox allows a task or ISR to send a single pointer-sized message to one or more tasks. Queues are used to send a user-definable number of messages to one or more tasks. As with the mailbox, the content of the message depends on the application. A queue is defined by the OS_Q data structure (found in the UCOS.H header file) and contains eight fields, which are described in Figure 1. Before using a queue, you must allocate storage

for it in your application and initialize it by calling OSQInit(). A queue's RAMrequirements are: 19 + 2 * OSQSize (bytes) OSQStart contains a pointer to the start of the message-storage area. OSQEnd contains a pointer to the end of the message-storage area. It is assumed that the message-storage area will be declared as an array of void pointers, such as void *Array[QSize]. OSQIn contains a pointer to the next location in the queue where a message will be inserted. The queue is a circular buffer that wraps around when the end of the queue is reached. This data structure is also called a "hopper." OSQOut is a pointer to the next location where a message will be extracted from the queue. As with OSQIn, OSQOut wraps around when the end of the queue is reached. OSQSize contains the size of the queue. This field is initialized by OSQInit(). Queues can be of any size up to 254 entries. (If you find this limitation to be a problem in your application, bear in mind that, by changing the data type of OSQSize and OSQEntries to UWORD, the size of a queue can be increased to support as many as 65,534 entries at the price of an extra two bytes of RAM for each queue.) OSQEntries keeps track of the number of entries in the queue. The queue is empty when OSQEntries is zero and full when equal to OSQSize. OSQGrp and OSQTbl[8] determine which tasks are waiting on the queue, similar to their semaphore counterparts, OSSemGrp and OSSemTbl[8]. When a message is placed in the queue and one or more tasks are waiting on the queue, the highest priority task is extracted from OSQTbl[8]. A task calls OSQPend() to receive a message. If the queue contains a message, it is removed from the queue and returned to the task. If the queue is empty, the task is suspended until a message is sent to the queue. From the task's point of view, OSQPend() just doesn't return for a while. You can specify a nonzero timeout value instead of waiting indefinitely for a message. To send a message to a queue, a task or an ISR calls OSQPost(). An error code is returned from OSQPost() if the queue is already full. The OSLock() function prevents task rescheduling until its counterpart, OSUnlock(), is called. The task that calls OS-Lock() keeps control of the CPU even though other higher priority tasks are ready to run. However, interrupts will still be recognized and serviced if they are enabled. OSLock() and OSUnlock() must be used in pairs. The variable OSLockNesting keeps track of the number of times OSLock() has been called to allow for nesting. Scheduling is allowed when an equal number of OSUnlock() functions have been issued. The nesting level in uCOS is 255. OSLock() and OSUnlock() must be used with caution because they affect normal multitasking management. MANAGING TASKS

A task may return to the dormant state by calling OSTaskDelete() to delete itself. You should note that a task cannot delete a task other than itself for reasons of control. That is, the task to be deleted may be pending on a semaphore, a mailbox or a queue. Since an OS_TCB has no way to know which semaphore, mailbox, orqueue the task is pending on, it would be difficult, at best, to adjust for this situation. However, it's possible to alter the kernel to provide this feature if you require it. (After all, that's why you have the source code.) When the task deletes itself, the related OS_TCB is freed and the task stack can be reused by another task. OSTaskDelete() always forces rescheduling. Another task-management problem that sometimes occurs in real-time systems is priority inversion. Priority inversion is any situation in which a low priority task holds a resource while a higher priority task is ready to use it. In this situation, the lower priority task prevents the higher priority task from executing until it releases the resource. This condition is aggravated when the lower priority task is preempted by other tasks or ISRs, which further delays access to the resource by the higher priority task. To correct this problem, the priority of the lower priority task can be raised while accessing the resource and restored to its initial value when done. uCOS allows you to change the priority of the running task through OSChangePrio(). The desired priority must not have already been assigned. If it has, an error code is returned. OSChangePrio() always forces rescheduling. PERFORMANCE ISSUES The execution time for each of the kernel services is shown in Table 1. The values were obtained by inspecting the code generated by the compiler and adding up the number of cycles for each instruction.3 The times were computed assuming an Intel 80188 processor with zero wait states. To obtain the execution time of each kernel service, divide the number of cycles provided in Table 1 by the processor-bus frequency. Nothing is free in software. As you add more services, the amount of ROM required is naturally going to increase. Table 2 shows the approximate cost (in bytes) of the different kernel services. You can remove the code for the uCOS services not used in your application to reduce the required amount of ROM. A complete uCOS kernel requires about three kbytes of ROM. AN EXAMPLE APPLICATION A flow diagram of the example code in TEST2.C is shown in Figure 2. In this example, I have created five tasks. KeyTask() monitors the keyboard every system tick. If the "1" key is pressed, a message is posted to the mailbox (represented by an I-beam in Figure 2). If

the "2" key is pressed, a message is posted to the queue (represented by a thicker I-beam). If the "x" key is pressed, the program terminates. Task1() waits until a message is sent to the mailbox. When a message is received, Ctr1 is incremented. However, if a message doesn't arrive within two seconds (36 ticks of the system clock), OSMboxPend() times out, and increments Ctr1 anyway. Task2() works similarly, except that it waits for a message posted to a queue. In this case, if no message is received within four seconds (72 ticks), OSQPend() times out, and the counter is incremented. Task3() executes every second to maintain a simple (and admittedly not very accurate) clock with minutes and seconds. Finally, every 300 milliseconds or so, DispTask() displays the value of Ctr1, Ctr2, and the clock on the screen. Since Ctr1, Ctr2, Min, and Sec are shared by more than one task, a semaphore (represented by a block with the letter "K" indicating a key) is used to gain exclusive access to these variables when any are to be updated or displayed. SUPPORT Real-time kernels offer features that make designing real-time software applications easier. This ease of development isn't free, however. The price you pay is in increased processing time and increased RAM and ROM requirements. This tradeoff applies to any commercially available kernel. Applications that cannot afford the interrupt latency, extra processing time, or the memory requirements imposed by uCOS should consider other alternatives. I advise you to be careful when evaluating the claims of a kernel vendor. My decision to design uCOS was prompted by the lack of support from a well-known kernel vendor. Although the manufacturer claimed that the kernel was bug free, experience proved it was not. It took over a year to get a fix for a potentially dangerous bug we identified. Besides paying a high one-time fee and royalties, we had to pay for the service contract to get the kernel fixed. I believe that if you find a critical bug in a product, the manufacturer should show some appreciation and at least fix the bug for free. Support is also an issue for software. Free software rarely includes a service contract but having the source code is an advantage. The kernel is reasonably mature, and I really don't anticipate readers finding major bugs. I won't claim it is 100% bug free, however. I am currently working on a book that explains the operation of an enhanced version of uCOS in greater detail and will also cover real-time software development issues. I would appreciate hearing from readers on their experiences with this version of uCOS. If your evaluation leads you to buy a kernel, you have a wide variety of the choices. A recent survey located over 40 vendors of real-time kernels and operating systems.4 Finding the one that will satisfy all your needs is not a simple task. It depends on your application's requirements and your experience in utilizing kernel facilities, among other factors. I wish you the best of luck.

BY JEAN J. LABROSSE Jean J. Labrosse has a Master's degree in Electrical Engineering from the University of Sherbrooke in Quebec, Canada, and has been developing real-time software for over 10 years. He is intimately familiar with the Z-80, 680x, 80x86, 680x0, 6805 and 68HC11 microprocessors. Labrosse is employed by Dynalco Controls in Fort Lauderdale, Fla., where he designs control software for industrial reciprocating engines. REFERENCES 1. Labrosse, Jean J. "A Real-Time Kernel in C," Embedded Systems Programming, May 1992, pp. 40-53. 2. Allworth, S.T. Introduction to Real-Time Software Design; New York, N.Y.: SpringerVerlag, 1981. 3. Intel Corp. "iAPX 186/188 User's Manual and Programmer's Reference." 4. Sperry, Tyler and Gretchen Bay. "Real-Time Operating System," Embedded Systems Programming, Jan. 1992, pp. 67-71.

You might also like