Table of Contents |
---|
About Semaphores
As defined in Resource Management, a semaphore is a protocol mechanism offered by most multitasking kernels. Semaphores were originally used to control access to shared resources. However, better mechanisms exist to protect access to shared resources. Semaphores are best used to synchronize an ISR to a task, or synchronize a task with another task as shown in the figure below.
Note that the semaphore is drawn as a flag to indicate that it is used to signal the occurrence of an event. The initial value for the semaphore is typically zero (0), indicating the event has not yet occurred.
The value “N” next to the flag indicates that the semaphore can accumulate events or credits. An ISR (or a task) can post (or signal) multiple times to a semaphore and the semaphore will remember how many times it was posted. It is possible to initialize the semaphore with a value other than zero, indicating that the semaphore initially contains that number of events.
Also, the small hourglass close to the receiving task indicates that the task has an option to specify a timeout. This timeout indicates that the task is willing to wait for the semaphore to be signaled (or posted to) within a certain amount of time. If the semaphore is not signaled within that time, µC/OS-III resumes the task and returns an error code indicating that the task was made ready-to-run because of a timeout and not the semaphore was signaled.
Panel | ||
---|---|---|
| ||
There are a number of operations to perform on semaphores as summarized in the table below and the figure above. However, in this chapter, we will only discuss the three functions used most often: OSSemCreate()
, OSSemPend()
, and OSSemPost()
. The other functions are described in uC-OS-III API Reference. Also note that every semaphore function is callable from a task, but only OSSemPost()
can be called by an ISR.
Panel | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||||
|
When used for synchronization, a semaphore keeps track of how many times it was signaled using a counter. The counter can take values between 0 and 255, 65,535, or 4,294,967,295, depending on whether the semaphore mechanism is implemented using 8, 16, or 32 bits, respectively. For µC/OS-III, the maximum value of a semaphore is determined by the data type OS_SEM_CTR
(see os_type.h
), which is changeable, as needed (assuming access to µC/OS-III’s source code). Along with the semaphore’s value, µC/OS-III keeps track of tasks waiting for the semaphore to be signaled.
Unilateral Rendez-Vous
The figure below shows that a task can be synchronized with an ISR (or another task) by using a semaphore. In this case, no data is exchanged, however there is an indication that the ISR or the task (on the left) has occurred. Using a semaphore for this type of synchronization is called a unilateral rendez-vous.
Panel | ||
---|---|---|
| ||
A unilateral rendez-vous is used when a task initiates an I/O operation and waits (i.e., call OSSemPend()
) for the semaphore to be signaled (posted). When the I/O operation is complete, an ISR (or another task) signals the semaphore (i.e., calls OSSemPost()
), and the task is resumed. This process is also shown on the timeline of the figure below and described below. The code for the ISR and task is shown in the listing follows the figure below.
Panel | ||
---|---|---|
| ||
Panel | ||
---|---|---|
| ||
(1) A high priority task is executing. The task needs to synchronize with an ISR (i.e., wait for the ISR to occur) and call (2) (3) Since the ISR has not occurred, the task will be placed in the waiting list for the semaphore until the event occurs The scheduler in µC/OS-III will then select the next most important task and context switch to that task. (5) The low-priority task executes. (6) The event that the original task was waiting for occurs. The lower-priority task is immediately preempted (assuming interrupts are enabled), and the CPU vectors to the interrupt handler for the event. (7) (8) The ISR handles the interrupting device and then calls (9) (10) µC/OS-III notices that a higher-priority task is waiting for this event to occur and context switches back to the original task. (11) The original task resumes execution immediately after the call to |
Code Block | ||
---|---|---|
| ||
OS_SEM MySem; void MyISR (void) { OS_ERR err; /* Clear the interrupting device */ OSSemPost(&MySem, (7) OS_OPT_POST_1, &err); /* Check "err" */ } void MyTask (void *p_arg) { OS_ERR err; CPU_TS ts; : : while (DEF_ON) { OSSemPend(&MySem, (1) 10, OS_OPT_PEND_BLOCKING, &ts, &err); /* Check "err" */ (11) : : } } |
Credit Tracking
A few interesting things are worth noting about this process. First, the task does not need to know about the details of what happens behind the scenes. As far as the task is concerned, it called a function (OSSemPend()
) that will return when the event it is waiting for occurs. Second, µC/OS-III maximizes the use of the CPU by selecting the next most important task, which executes until the ISR occurs. In fact, the ISR may not occur for many milliseconds and, during that time, the CPU will work on other tasks. As far as the task that is waiting for the semaphore is concerned, it does not consume CPU time while it is waiting. Finally, the task waiting for the semaphore will execute immediately after the event occurs (assuming it is the most important task that needs to run).
As previously mentioned, a semaphore “remembers” how many times it was signaled (or posted to). In other words, if the ISR occurs multiple times before the task waiting for the event becomes the highest-priority task, the semaphore will keep count of the number of times it was signaled. When the task becomes the highest priority ready-to-run task, it will execute without blocking as many times as there were ISRs signaled. This is called Credit Tracking and is illustrated in the figure below and described below.
Panel | ||
---|---|---|
| ||
Panel | ||
---|---|---|
| ||
(1) A high-priority task is executing. (2) (3) An event meant for a lower-priority task occurs which preempts the task (assuming interrupts are enabled). The ISR executes and posts the semaphore. At this point the semaphore count is 1. (4) (6) µC/OS-III is called at the end of the ISR to see if the ISR caused a higher-priority task to be ready-to-run. Since the ISR was an event that a lower-priority task was waiting for, µC/OS-III will resume execution of the higher-priority task at the exact point where it was interrupted. (7) The high-priority task is resumed and continues execution. (8) (9) The interrupt occurs a second time. The ISR executes and posts the semaphore. At this point the semaphore count is 2. (10) (12) µC/OS-III is called at the end of the ISR to see if the ISR caused a higher-priority task to be ready-to-run. Since the ISR was an event that a lower-priority task was waiting for, µC/OS-III resumes execution of the higher-priority task at the exact point where it was interrupted. (13) (14) The high-priority task resumes execution and actually terminates the work it was doing. This task will then call one of the µC/OS-III services to wait for “its” event to occur. (15) (16) µC/OS-III will then select the next most important task, which happens to be the task waiting for the event and will context switch to that task. (17) The new task executes and will know that the ISR occurred twice since the semaphore count is two. The task will handle this accordingly. |
Multiple Tasks Waiting on a Semaphore
It is possible for more than one task to wait on the same semaphore, each with its own timeout as illustrated in the figure below.
Panel | ||
---|---|---|
| ||
When the semaphore is signaled (whether by an ISR or task), µC/OS-III makes the highest-priority task waiting on the semaphore ready-to-run. However, it is also possible to specify that all tasks waiting on the semaphore be made ready-to-run. This is called broadcasting and is accomplished by specifying OS_OPT_POST_ALL
as an option when calling OSSemPost()
. If any of the waiting tasks has a higher priority than the previously running task, µC/OS-III will execute the highest-priority task made ready by OSSemPost()
.
Broadcasting is a common technique used to synchronize multiple tasks and have them start executing at the same time. However, some of the tasks that we want to synchronize might not be waiting for the semaphore. It is fairly easy to resolve this problem by combining semaphores and event flags. This will be described after examining event flags.
Semaphore Internals (for synchronization)
Note that some of the material presented in this section is also contained in Resource Management, as semaphores were also discussed in that chapter. However, the material presented here will be applicable to semaphores used for synchronization and thus will differ somewhat.
A counting semaphore allows values between 0 and 255, 65,535, or 4,294,967,295, depending on whether the semaphore mechanism is implemented using 8, 16, or 32 bits, respectively. For µC/OS-III, the maximum value of a semaphore is determined by the data type OS_SEM_CTR
(see os_type.h
), which can be changed as needed. Along with the semaphore’s value, µC/OS-III keeps track of tasks waiting for the semaphore’s availability.
The application programmer can create an unlimited number of semaphores (limited only by available RAM). Semaphore services in µC/OS-III start with the OSSem???()
prefix, and services available to the application programmer are described in µC-OS-III API Reference. Semaphore services are enabled at compile time by setting the configuration constant OS_CFG_SEM_EN
to DEF_ENABLED
in os_cfg.h
.
Semaphores must be created before they can be used by the application. The second listing below shows how to create a semaphore.
As previously mentioned, a semaphore is a kernel object as defined by the OS_SEM
data type, which is derived from the structure os_sem
(see os.h
) as shown in the listing below. The services provided by µC/OS-III to manage semaphores are implemented in the file os_sem.c
.
Code Block | ||
---|---|---|
| ||
typedef struct os_sem OS_SEM; (1) struct os_sem { OS_OBJ_TYPE Type; (2) CPU_CHAR *NamePtr; (3) OS_PEND_LIST PendList; (4) OS_SEM_CTR Ctr; (5) CPU_TS TS; (6) }; |
Panel |
---|
(1) In µC/OS-III, all structures are given a data type. In fact, all data types start with “ (2) The structure starts with a “ (3) Each kernel object can be given a name to make them easier to be recognized by debuggers or µC/Probe. This member is simply a pointer to an ASCII string, which is assumed to be (4) Since it is possible for multiple tasks to be waiting (or pending) on a semaphore, the semaphore object contains a pend list as described in Pend Lists. (5) A semaphore contains a counter. As explained above, the counter can be implemented as either an 8-, 16- or 32-bit value, depending on how the data type (6) A semaphore contains a time stamp, which is used to indicate the last time the semaphore was signaled (or posted to). µC/OS-III assumes the presence of a free-running counter that allows the application to make time measurements. When the semaphore is signaled, the free-running counter is read and the value is placed in this field, which is returned when |
Even for users who understand the internals of the OS_SEM
data type, the application code should never access any of the fields in this data structure directly. Instead, you should always use the APIs provided with µC/OS-III.
Semaphores must be created before they can be used by an application. The listing below shows how to create a semaphore.
Code Block | ||
---|---|---|
| ||
OS_SEM MySem; (1) void MyCode (void) { OS_ERR err; : OSSemCreate(&MySem, (2) "My Semaphore", (3) (OS_SEM_CTR)0, (4) &err); (5) /* Check "err" */ : } |
Panel | ||
---|---|---|
| ||
(1) The application must declare a variable of type (2) You create a semaphore by calling (3) You can assign an ASCII name to the semaphore, which can be used by debuggers or µC/Probe to easily identify this semaphore. (4) You need to initialize the semaphore to zero (0) when using a semaphore as a signaling mechanism. (5) |
( OSSemCreate()
performs a check on the arguments passed to this function (assuming OS_CFG_ARG_CHK_EN
is set to DEF_ENABLED
in os_cfg.h
) and only initializes the contents of the variable of type OS_SEM
used for signaling.
A task waits for a signal from an ISR or another task by calling OSSemPend()
as shown in the listing below (see Appendix A, µC-OS-III API Reference for details regarding the arguments).
Code Block | ||
---|---|---|
| ||
void MyTask (void *p_arg) { OS_ERR err; CPU_TS ts; : while (DEF_ON) { OSSemPend(&MySem, (1) 0, OS_OPT_PEND_BLOCKING, &ts, &err); /* Check "err" */ (2) : } |
Panel | ||
---|---|---|
| ||
(1) When called, If the semaphore counter ( If the semaphore counter is zero, this indicates that the signal has not occurred and the calling task might need to wait for the semaphore to be released. If you specify If you specify If you further specify a non-zero timeout, the task will also be inserted in the tick list. A zero value for a timeout indicates that the calling task is willing to wait forever for the semaphore to be signaled. The scheduler is then called since the current task is not able to run (it is waiting for the semaphore to be signaled). The scheduler will then run the next highest-priority task that is ready-to-run. When the semaphore is signaled and the task that called 1) The semaphore was signaled which is the desired outcome 2) The pend was aborted by another task 3) The semaphore was not signaled within the specified timeout 4) The semaphore was deleted When (2) If |
To signal a task (either from an ISR or a task), simply call OSSemPost()
as shown in the listing below.
Code Block | ||
---|---|---|
| ||
OS_SEM MySem; void MyISR (void) { OS_ERR err; : OSSemPost(&MySem, (1) OS_OPT_POST_1, (2) &err); (3) /* Check "err" */ : : } |
Panel | |||||
---|---|---|---|---|---|
| |||||
(1) Your task signals (or posts to) the semaphore by calling (2) The next argument specifies how the task wants to post. There are a number of options to choose from. When you specify If specifying The calling task can “add” the option OS_OPT_POST_1 OS_OPT_POST_ALL OS_OPT_POST_1 + OS_OPT_POST_NO_SCHED OS_OPT_POST_ALL + OS_OPT_POST_NO_SCHED
(3) |