Semaphores
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.
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.
Function Name | Operation |
---|---|
OSSemCreate() | Create a semaphore. |
OSSemDel() | Delete a semaphore. |
OSSemPend() | Wait on a semaphore. |
OSSemPendAbort() | Abort the wait on a semaphore. |
OSSemPost() | Signal a semaphore. |
OSSemSet() | Force the semaphore count to a desired value. |
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.
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.
(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 OSSemPend()
.
(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 OSSemPost()
to signal the semaphore. When the ISR completes, µC/OS-III is called (i.e. OSIntExit()
).
(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 OSSemPend()
.
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.
(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.
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
.
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) };
(1) In µC/OS-III, all structures are given a data type. In fact, all data types start with “OS_
” and are all uppercase. When a semaphore is declared, simply use OS_SEM
as the data type of the variable used to declare the semaphore.
(2) The structure starts with a “Type
” field, which allows it to be recognized by µC/OS-III as a semaphore. In other words, other kernel objects will also have a “Type
” as the first member of the structure. If a function is passed a kernel object, µC/OS-III will confirm that it is being passed the proper data type (assuming OS_CFG_OBJ_TYPE_CHK_EN
is set to DEF_ENABLED
in os_cfg.h
). For example, if passing a message queue (OS_Q
) to a semaphore service (for example OSSemPend()
), µC/OS-III will recognize that an invalid object was passed, and return an error code accordingly.
(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 NUL
terminated.
(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 OS_SEM_CTR
is declared in os_type.h
. µC/OS-III keeps track of how many times the semaphore is signaled with this counter and this field is typically initialized to zero by OSSemCreate()
.
(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 OSSemPend()
is called. This value allows the application to determine either when the signal was performed, or how long it took for the task to get control of the CPU from the signal. In the latter case, you should call OS_TS_GET()
to determine the current timestamp and compute the difference.
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.
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" */ : }
(1) The application must declare a variable of type OS_SEM
. This variable will be referenced by other semaphore services.
(2) You create a semaphore by calling OSSemCreate()
and pass the address to the semaphore allocated in (1).
(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()
returns an error code based on the outcome of the call. If all arguments are valid, err
will contain OS_ERR_NONE
.
( 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).
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) : }
(1) When called, OSSemPend()
starts by checking the arguments passed to this function to make sure they have valid values (assuming OS_CFG_OBJ_TYPE_CHK_EN
is set to DEF_ENABLED
in os_cfg.h
).
If the semaphore counter (.Ctr
of OS_SEM
) is greater than zero, the counter is decremented and OSSemPend()
returns, which indicates that the signal occurred. This is the outcome that the caller expects.
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 OS_OPT_PEND_NON_BLOCKING
as the option (the task is not to block), OSSemPend()
returns immediately to the caller and the returned error code will indicate that the signal did not occur.
If you specify OS_OPT_PEND_BLOCKING
as the option, the calling task will be inserted in the list of tasks waiting for the semaphore to be signaled. The task is inserted in the list by priority order with the highest priority task waiting on the semaphore at the beginning of the list as shown in the figure at the end of this page.
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 OSSemPend()
is again the highest-priority task, a task status is examined to determine the reason why OSSemPend()
is returning to its caller. The possibilities are:
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 OSSemPend()
returns, the caller is notified of the above outcome through an appropriate error code.
(2) If OSSemPend()
returns with err
set to OS_ERR_NONE
, you can assume that the semaphore was signaled and the task can proceed with servicing the ISR or task that caused the signal. If err
contains anything else, OSSemPend()
either timed out (if the timeout argument was non-zero), the pend was aborted by another task, or the semaphore was deleted by another task. It is always important to examine returned error code and not assume everything went as expected.
To signal a task (either from an ISR or a task), simply call OSSemPost()
as shown in the listing below.
OS_SEM MySem; void MyISR (void) { OS_ERR err; : OSSemPost(&MySem, (1) OS_OPT_POST_1, (2) &err); (3) /* Check "err" */ : : }
(1) Your task signals (or posts to) the semaphore by calling OSSemPost()
. You specify the semaphore to post by passing its address. The semaphore must have been previously created.
(2) The next argument specifies how the task wants to post. There are a number of options to choose from.
When you specify OS_OPT_POST_1
, you are indicating that you want to post to only one task (in case there are multiple tasks waiting on the semaphore). The task that will be made ready-to-run will be the highest-priority task waiting on the semaphore. If there are multiple tasks at the same priority, only one of them will be made ready-to-run. As shown in the figure below, tasks waiting are in priority order (HPT means High Priority Task and LPT means Low Priority Task). So, it is a fast operation to extract the HPT from the list.
If specifying OS_OPT_POST_ALL
, all tasks waiting on the semaphore will be posted and made ready-to-run.
The calling task can “add” the option OS_OPT_POST_NO_SCHED
to either of the two previous options to indicate that the scheduler is not to be called at the end of OSSemPost()
, possibly because additional postings will be performed, and rescheduling should only take place when finished. This means that the signal is performed, but the scheduler is not called even if a higher-priority task was waiting for the semaphore to be signaled. This allows the calling task to perform other post functions (if needed) and make all the posts take effect simultaneously. Note that OS_OPT_POST_NO_SCHED
is “additive,” meaning that it can be used with either of the previous options. You can thus specify:
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)OSSemPost()
returns an error code based on the outcome of the call. If the call was successful,err
will containOS_ERR_NONE
. If not, the error code will indicate the reason for the error (see Appendix A, µC-OS-III API Reference for a list of possible error codes forOSSemPost())
.