Semaphore Management

Relationships Between Tasks, ISRs, and a Semaphore

µC/OS-II semaphores consist of two elements: a 16-bit unsigned integer used to hold the semaphore count (0 to 65535) and a list of tasks waiting for the semaphore count to be greater than 0. µC/OS-II provides eight services to access semaphores: OSSemAccept(), OSSemCreate(), OSSemDel(), OSSemPend(), OSSemPendAbort()OSSemPost() and OSSemQuery().

To enable µC/OS-II semaphore services, you must set the configuration constants in OS_CFG.H. Specifically, table 7.1 shows which services are compiled based on the value of configuration constants found in OS_CFG.H. You should note that NONE of the semaphore services are enabled when OS_SEM_EN is set to 0. To enable the feature (i.e. service), simply set the configuration constant to 1. You will notice that OSSemCreate(), OSSemPend() and OSSemPost() cannot be individually disabled like the other services. That’s because they are always needed when you enable µC/OS-II semaphore management.

Table - Table 7.1 Semaphore configuration constants in OS_CFG.H
µC/OS-II Semaphore Service>Enabled when set to 1 in OS_CFG.H
OSSemAccept()OS_SEM_ACCEPT_EN
OSSemCreate()
OSSemDel()OS_SEM_DEL_EN
OSSemPend()
OSSemPendAbort()OS_SEM_PEND_ABORT_EN
OSSemPost()
OSSemQuery()OS_SEM_QUERY_EN
OSSemSet()OS_SEM_SET_EN


Figure 7.1 shows a flow diagram to illustrate the relationship between tasks, ISRs, and a semaphore. Note that the symbology used to represent a semaphore is either a key or a flag. You would us a key symbol in such flow diagrams if the semaphore is used to access shared resources. The N next to the key represents how many resources are available. N is 1 for a binary semaphore. Use a flag symbol when a semaphore is used to signal the occurrence of an event. N in this case represents the number of times the event can be signaled. The hourglass represents a timeout that can be specified with the OSSemPend() call.

As you can see from Figure 7.1, a task or an ISR can call OSSemAccept(), OSSemPost() or OSSemQuery(). However, only tasks are allowed to call OSSemDel() or OSSemPend().

Figure - Figure 7.1 Relationships between tasks, ISRs, and a semaphore


Creating a Semaphore, OSSemCreate()

A semaphore needs to be created before it can be used. You create a semaphore by calling OSSemCreate() (see next section) and specifying the initial count of the semaphore. The initial value of a semaphore can be between 0 and 65535. If you use the semaphore to signal the occurrence of one or more events, you would typically initialize the semaphore to 0. If you use the semaphore to access a single shared resource, you need to initialize the semaphore to 1 (i.e., use it as a binary semaphore). Finally, if the semaphore allows your application to obtain any one of n identical resources, initialize the semaphore to n and use it as a counting semaphore.

The code to create a semaphore is shown in Listing 7.1.

Figure 7.2 shows the content of the ECB just before OSSemCreate() returns.

Listing - Listing 7.1 Creating a semaphore
OS_EVENT  *OSSemCreate (INT16U cnt)
{
#if OS_CRITICAL_METHOD == 3 
    OS_CPU_SR  cpu_sr;                                             (1)
#endif    
    OS_EVENT  *pevent;
 
 
    if (OSIntNesting > 0) {                                        (2)
        return ((OS_EVENT *)0);   
    }
    OS_ENTER_CRITICAL();
    pevent = OSEventFreeList;                                      (3)
    if (OSEventFreeList != (OS_EVENT *)0) {                        (4)
        OSEventFreeList = (OS_EVENT *)OSEventFreeList->OSEventPtr; (5)
    }
    OS_EXIT_CRITICAL();
    if (pevent != (OS_EVENT *)0) {                                 (6)
        pevent->OSEventType = OS_EVENT_TYPE_SEM;                   (7)
        pevent->OSEventCnt  = cnt;                                 (8)
        pevent->OSEventPtr  = (void *)0;                           (9)
#if OS_EVENT_NAME_EN > 0u
        pevent->OSEventName = (INT8U *)(void *)"?";
#endif
        OS_EventWaitListInit(pevent);                             (10)
    }
    return (pevent);                                              (11)
}

(1) A local variable called cpu_sr to support OS_CRITICAL_METHOD #3 is allocated.

(2) OSSemCreate() starts by making sure you are not calling this function from an ISR because this is not allowed. All kernel objects need to be created from task level code or before multitasking starts.

(3) OSSemCreate() then attempts to obtain an ECB (Event Control Block) from the free list of ECBs (see Figure 6.??).

(4) & (5) The linked list of free ECBs is adjusted to point to the next free ECB.

(6) & (7) If there is an ECB available, the ECB type is set to OS_EVENT_TYPE_SEM . Other OSSem???() function calls will check this structure member to make sure that the ECB is of the proper type (i.e. a semaphore). This prevents you from calling OSSemPost() on an ECB that was created for use as a message mailbox (see 10.??, Message Mailboxes).

(8) Next, the desired initial count for the semaphore is stored in the ECB.

(9) The .OSEventPtr field is then initialized to point to NULL because it doesn’t belong to the free ECB linked list anymore.

(10) The wait list is then initialized by calling OS_EventWaitListInit() [see 6.??, Initializing an ECB, OS_EventWaitListInit()]. Because the semaphore is being initialized, there are no tasks waiting for it and thus, OS_EventWaitListInit() clears .OSEventGrp and .OSEventTbl[].

(11) Finally, OSSemCreate() returns a pointer to the ECB. This pointer must be used in subsequent calls to manipulate semaphores [OSSemAccept(), OSSemDel(), OSSemPend(), OSSemPost() and OSSemQuery()]. The pointer is basically used as the semaphore’s handle. If there are no more ECBs, OSSemCreate() returns a NULL pointer. You should make it a habbit to check the return value of µC/OS-II return values to ensure that you are getting the desired results. Passing NULL pointers to µC/OS-II will not make it fail because µC/OS-II validates arguments (only if OS_ARG_CHK_EN is set to 1, though).


Figure - Figure 7.2 ECB just before OSSemCreate() returns

Deleting a Semaphore, OSSemDel()

The code to delete a semaphore is shown in listing 7.2 and code will only be generated by the compiler if OS_SEM_DEL_EN is set to 1 in OS_CFG.H. This is a function you must use with caution because multiple tasks could attempt to access a deleted semaphore. You should always use this function with great care. Generally speaking, before you would delete a semaphore, you would first delete all the tasks that access the semaphore.

Listing - Listing 7.2 Deleting a Semaphore
OS_EVENT  *OSSemDel (OS_EVENT *pevent, INT8U opt, INT8U *err)
{
#if OS_CRITICAL_METHOD == 3  
    OS_CPU_SR  cpu_sr;
#endif    
    BOOLEAN    tasks_waiting;


    if (OSIntNesting > 0) {                                        (1)
        *err = OS_ERR_DEL_ISR;
        return (pevent);
    }
#if OS_ARG_CHK_EN > 0
    if (pevent == (OS_EVENT *)0) {                                 (2)
        *err = OS_ERR_PEVENT_NULL;
        return (pevent);
    }
    if (pevent->OSEventType != OS_EVENT_TYPE_SEM) {                (3)
        *err = OS_ERR_EVENT_TYPE;
        return (pevent);
    }
#endif
    OS_ENTER_CRITICAL();
    if (pevent->OSEventGrp != 0x00) {                              (4)
        tasks_waiting = OS_TRUE;        
    } else {
        tasks_waiting = OS_FALSE;
    }
    switch (opt) {
        case OS_DEL_NO_PEND:  
             if (tasks_waiting == OS_FALSE) {                      (5)
#if OS_EVENT_NAME_EN > 0u
                 pevent->OSEventName = (INT8U *)(void *)"?";
#endif
                 pevent->OSEventType = OS_EVENT_TYPE_UNUSED;       (6)
                 pevent->OSEventPtr  = OSEventFreeList;            (7)
                 pevent->OSEventCnt  = 0u;
                 OSEventFreeList     = pevent;         
                 OS_EXIT_CRITICAL();
                 *err = OS_ERR_NONE;
                 return ((OS_EVENT *)0);                           (8) 
             } else {
                 OS_EXIT_CRITICAL();
                 *err = OS_ERR_TASK_WAITING;
                 return (pevent);
             }

        case OS_DEL_ALWAYS:                                        (9) 
             while (pevent->OSEventGrp != 0x00) {                 (10)
                 OS_EventTaskRdy(pevent, (void *)0, OS_STAT_SEM);
             }
#if OS_EVENT_NAME_EN > 0u
             pevent->OSEventName = (INT8U *)(void *)"?";
#endif
             pevent->OSEventType = OS_EVENT_TYPE_UNUSED;          (11)
             pevent->OSEventPtr  = OSEventFreeList;               (12)
             pevent->OSEventCnt  = 0u;
             OSEventFreeList     = pevent;         
             OS_EXIT_CRITICAL();
             if (tasks_waiting == OS_TRUE) {          
                 OS_Sched();                                      (13)
             }
             *err = OS_ERR_NONE;
             return ((OS_EVENT *)0);                              (14)       

        default:
             OS_EXIT_CRITICAL();
             *err = OS_ERR_INVALID_OPT;
             return (pevent);
    }
}

(1) OSSemDel() starts by making sure that this function is not called from an ISR because that’s not allowed.

(2) & (3) OSSemDel() validates pevent to ensure that it’s not a NULL pointer and that it points to an ECB that was created as a semaphore.

(4) OSSemDel() then determines whether there are any tasks waiting on the semaphore. The flag tasks_waiting is set accordingly.

Based on the option (i.e. opt) specified in the call, OSSemDel() will either delete the semaphore only if no tasks are pending on the semaphore (opt == OS_DEL_NO_PEND) or, delete the semaphore even if tasks are waiting (opt == OS_DEL_ALWAYS).

(5), (6) & (7) When opt is set to OS_DEL_NO_PEND and there is no task waiting on the semaphore, OSSemDel() marks the ECB as unused and the ECB is returned to the free list of ECBs. This will allow another semaphore (or any other ECB based object) to be created.

(8) You will note that OSSemDel() returns a NULL pointer since, at this point, the semaphore should no longer be accessed through the original pointer. OSSemDel() returns an error code if there were task waiting on the semaphore (i.e., OS_ERR_TASK_WAITING) because by specifying OS_DEL_NO_PEND you indicated that you didn’t want to delete the semaphore if there are tasks waiting on the semaphore.

(9) & (10) When opt is set to OS_DEL_ALWAYS then all tasks waiting on the semaphore will be readied. Each task will think it has access to the semaphore. Of course, that’s a dangerous outcome since the whole point of having a semaphore is to protect against multiple access to a resource.

(11) & (12) Once all pending tasks are readied, OSSemDel() marks the ECB as unused and the ECB is returned to the free list of ECBs.

(13) The scheduler is called only if there were tasks waiting on the semaphore.

(14) Again, you will note that OSSemDel() returns a NULL pointer since, at this point, the semaphore should no longer be accessed through the original pointer.


Waiting on a Semaphore (blocking), OSSemPend()

The code to wait on a semaphore is shown in Listing 7.3.

Listing - Listing 7.3 Waiting for a semaphore
void  OSSemPend (OS_EVENT *pevent, INT16U timeout, INT8U *err)
{
#if OS_CRITICAL_METHOD == 3
    OS_CPU_SR  cpu_sr;
#endif    


#if OS_ARG_CHK_EN > 0
    if (pevent == (OS_EVENT *)0) {                   (1)
        *err = OS_ERR_PEVENT_NULL;
        return;
    }
#endif
    if (pevent->OSEventType != OS_EVENT_TYPE_SEM) {  
        *err = OS_ERR_EVENT_TYPE;
        return;
    }
    if (OSIntNesting > 0) {                          (2)
        *err = OS_ERR_PEND_ISR;                    
        return;
    }
    if (OSLockNesting > 0) {                         (3) 
        *err = OS_ERR_PEND_LOCKED;                    
        return;
    }
    OS_ENTER_CRITICAL();
    if (pevent->OSEventCnt > 0) {                    (4)
        pevent->OSEventCnt--;                        (5)
        OS_EXIT_CRITICAL();
        *err = OS_ERR_NONE;                          (6)
        return;
    }
    OSTCBCur->OSTCBStat |= OS_STAT_SEM;              (7)
    OSTCBCur->OSTCBDly   = timeout;                  (8)
    OS_EventTaskWait(pevent);                        (9)
    OS_EXIT_CRITICAL();
    OS_Sched();                                     (10)
    OS_ENTER_CRITICAL();
    switch (OSTCBCur->OSTCBStatPend) {              (11)           
        case OS_STAT_PEND_OK:
             *perr = OS_ERR_NONE;
             break;

        case OS_STAT_PEND_ABORT:
             *perr = OS_ERR_PEND_ABORT;               
             break;

        case OS_STAT_PEND_TO:
        default:
             OS_EventTaskRemove(OSTCBCur, pevent);  (12)
             *perr = OS_ERR_TIMEOUT;                  
             break;
    }
    OSTCBCur->OSTCBStat          =  OS_STAT_RDY;    (13)
    OSTCBCur->OSTCBStatPend      =  OS_STAT_PEND_OK;  
    OSTCBCur->OSTCBEventPtr      = (OS_EVENT  *)0;    
#if (OS_EVENT_MULTI_EN > 0u)
    OSTCBCur->OSTCBEventMultiPtr = (OS_EVENT **)0;
#endif
    OS_EXIT_CRITICAL();
}

(1) If OS_ARG_CHK_EN is set to 1, OSSemPend() checks that pevent is not a NULL pointer and the ECB being pointed to by pevent has been created by OSSemCreate() .

(2) OSSemPend() checks to see if the function was called by an ISR. It doesn’t make sense to call OSSemPend() from an ISR because an ISR cannot be made to wait. Instead, you should call OSSemAccept() (see section 7.05).

(3) You should not wait on a semaphore when the scheduler is locked.

(4) & (5) If the semaphore is available (its count is nonzero), the count is decremented and the function returns to its caller with an error code indicating success. If your code calls OSSemPend() , this is the outcome you are looking for because it indicates that your code can proceed and access the resource (if OSSemPend() is used to guard a shared resource). This also happens to be the fastest path through OSSemPend() .

(6) If the semaphore is not available (the count was zero), OSSemPend() checks to see if the function was called by an ISR. It doesn’t make sense to call OSSemPend() from an ISR because an ISR cannot be made to wait. Instead, you should call OSSemAccept() (see secrion 7.05). I decided to add this check just in case. However, if the semaphore is in fact available, the call to OSSemPend() would be successful even if called by an ISR!

If the semaphore count is zero, the calling task needs to be put to sleep until another task (or an ISR) signals the semaphore (see section 7.04). OSSemPend() allows you to specify a timeout value (in integral number of ticks) as one of its arguments (i.e., timeout). This feature is useful to avoid waiting indefinitely for the semaphore. If the value passed is nonzero, OSSemPend() suspends the task until the semaphore is signaled or the specified timeout period expires. Note that a timeout value of 0 indicates that the task is willing to wait forever for the semaphore to be signaled.

(7) To put the calling task to sleep, OSSemPend() sets the status flag in the task’s TCB (Task Control Block) to indicate that the task is suspended waiting for a semaphore.

(8) The timeout is also stored in the TCB so that it can be decremented by OSTimeTick(). You should recall (see section 3.??, Clock Tick) that OSTimeTick() decrements each of the created task’s .OSTCBDly field if it’s nonzero.

(9) The actual work of putting the task to sleep is done by OS_EventTaskWait() [see section 6.??, Making a Task Wait for an Event, OS_EventTaskWait()].

(10) Because the calling task is no longer ready to run, the scheduler is called to run the next highest priority task that is ready to run. As far as your task is concerned, it made a call to OSSemPend() and it doesn’t know that it will be suspended until the semaphore is signaled.

(11) When the semaphore is signaled (or the timeout period expires) OSSemPend() will resume execution immediately after the call to OS_Sched(). OSSemPend() then checks to see if the TCB status flag is still set to indicate that the task is waiting for the semaphore. If the task is still waiting for the semaphore, it must not have been signaled by an OSSemPost() call. Indeed, the task must have been readied by OSTimeTick(), indicating that the timeout period has expired.

(12) In this case, the task is removed from the wait list for the semaphore by calling OS_EventTaskRemove() , and an error code is returned to the task that called OSSemPend() to indicate that a timeout occurred. If the status flag in the task’s TCB doesn’t have the OS_STAT_SEM bit set, then the semaphore must have been signaled by OSSemPost() (see section 7.04) and the task that called OSSemPend() can now conclude that it has the semaphore.

(14) Finally, the link to the ECB is removed.


Signaling a Semaphore, OSSemPost()

The code to signal a semaphore is shown in Listing 7.4.

Listing - Listing 7.4 Signaling a semaphore
INT8U  OSSemPost (OS_EVENT *pevent)
{
#if OS_CRITICAL_METHOD == 3
    OS_CPU_SR  cpu_sr;                               
#endif    
 
 
#if OS_ARG_CHK_EN > 0
    if (pevent == (OS_EVENT *)0) {                                          (1)
        return (OS_ERR_PEVENT_NULL);
    }
#endif
    if (pevent->OSEventType != OS_EVENT_TYPE_SEM) {                         (2)
        return (OS_ERR_EVENT_TYPE);
    }
    OS_ENTER_CRITICAL();
    if (pevent->OSEventGrp != 0u) {                                         (3)
        OS_EventTaskRdy(pevent, (void *)0, OS_STAT_SEM, OS_STAT_PEND_OK);   (4)
        OS_EXIT_CRITICAL();
        OS_Sched();                                                         (5)
        return (OS_ERR_NONE);
    }
    if (pevent->OSEventCnt < 65535u) {                         
        pevent->OSEventCnt++;                                               (6)
        OS_EXIT_CRITICAL();
        return (OS_ERR_NONE);
    }
    OS_EXIT_CRITICAL();                        
    return (OS_ERR_SEM_OVF);                                                (7)
}

(1) & (2) If OS_ARG_CHK_EN is set to 1, OSSemPost() checks that pevent is not a NULL pointer and the ECB being pointed to by pevent has been created by OSSemCreate() .

(3) OSSemPost() then checks to see if any tasks are waiting on the semaphore. There are tasks waiting when the .OSEventGrp field in the ECB contains a nonzero value.

(4) & (5) The highest priority task waiting for the semaphore is removed from the wait list by OS_EventTaskRdy() [see section 6.??, Making a Task Ready, OS_EventTaskRdy()] and made ready-to-run. OS_Sched() is then called to see if the task made ready is now the highest priority task ready-to-run. If it is, a context switch results [only if OSSemPost() is called from a task] and the readied task is executed. In other words, the task that called OSSemPost() will NOT continue execution because OSSemPost() made a more important task ready-to-run and µC/OS-II will thus resume execution of that task. If the readied task is not the highest priority task, OS_Sched() returns and the task that called OSSemPost() continues execution.

(6) & (7) If there are no tasks waiting on the semaphore, the semaphore count simply gets incremented. Note that a counting semaphore is implemented in µC/OS-II using a 16-bit variable and OSSemPost() ensures that the semaphore is not overflowed otherwise, an error is returned to the task that called OSSemPost() .


It’s important to note that a context switch does NOT occur if OSSemPost() is called by an ISR because context switching from an ISR can only occur when OSIntExit() is called at the completion of the ISR from the last nested ISR (see section 3.??, Interrupts under µC/OS-II).

Getting a Semaphore without Waiting (non-blocking), OSSemAccept()

It is possible to obtain a semaphore without putting a task to sleep if the semaphore is not available. This is accomplished by calling OSSemAccept() as shown in Listing 7.5.

The code that called OSSemAccept() needs to examine the returned value. A returned value of zero indicates that the semaphore is not available; a nonzero value indicates that the semaphore is available. Furthermore, a nonzero value indicates to the caller the number of resources that are available. Keep in mind that, in this case, one of the resources has been allocated to the calling task because the count has been decremented.

An ISR could use OSSemAccept(). However, it’s not recommended to have a semaphore shared between a task and an ISR. Semaphores are supposed to be task level objects. If a semaphore is used as a signalling object between an ISR and a task then, the ISR should only POST to the semaphore.

Listing - Listing 7.5 Getting a semaphore without waiting
INT16U  OSSemAccept (OS_EVENT *pevent)
{
#if OS_CRITICAL_METHOD == 3
    OS_CPU_SR  cpu_sr;
#endif    
    INT16U     cnt;


#if OS_ARG_CHK_EN > 0
    if (pevent == (OS_EVENT *)0) {                        (1)
        return (0);
    }
#endif
    if (pevent->OSEventType != OS_EVENT_TYPE_SEM) {       (2)
        return (0);
    }
    OS_ENTER_CRITICAL();
    cnt = pevent->OSEventCnt;                             (3)
    if (cnt > 0u) {                                       (4)
        pevent->OSEventCnt--;                             (5)
    }
    OS_EXIT_CRITICAL();
    return (cnt);                                         (6)
}


(1) & (2) If OS_ARG_CHK_EN is set to 1 in OS_CFG.H, OSSemAccept() starts by checking that pevent is not a NULL pointer and that the ECB being pointed to by pevent has been created by OSSemCreate().

(3) & (4) OSSemAccept() then gets the current semaphore count to determine whether the semaphore is available (i.e., a nonzero value).

(5) The count is decremented only if the semaphore was available.

(6) Finally, the original count of the semaphore is returned to the caller.


Obtaining the Status of a Semaphore, OSSemQuery()

OSSemQuery() allows your application to take a “snapshot” of an ECB that is used as a semaphore (Listing 7.6). OSSemQuery() receives two arguments: pevent contains a pointer to the semaphore, which is returned by OSSemCreate() when the semaphore is created, and pdata is a pointer to a data structure (OS_SEM_DATA, see uCOS_II.H) that holds information about the semaphore. Your application will thus need to allocate a variable of type OS_SEM_DATA that will be used to receive the information about the desired semaphore. I decided to use a new data structure because the caller should only be concerned with semaphore-specific data as opposed to the more generic OS_EVENT data structure, which contain two additional fields (.OSEventType and .OSEventPtr). OS_SEM_DATA contains the current semaphore count (.OSCnt) and the list of tasks waiting on the semaphore (.OSEventTbl[] and .OSEventGrp).

Listing - Listing 7.6 Obtaining the status of a semaphore
INT8U  OSSemQuery (OS_EVENT *pevent, OS_SEM_DATA *p_sem_data)
{
#if OS_CRITICAL_METHOD == 3
    OS_CPU_SR  cpu_sr;
#endif    
    INT8U     *psrc;
    INT8U     *pdest;
 
 
#if OS_ARG_CHK_EN > 0
    if (pevent == (OS_EVENT *)0) {                          (1)
        return (OS_ERR_PEVENT_NULL);
    }
    if (p_sem_data == (OS_SEM_DATA *)0) {                  
        return (OS_ERR_PDATA_NULL);
    }
#endif
    if (pevent->OSEventType != OS_EVENT_TYPE_SEM) {         (2)
        return (OS_ERR_EVENT_TYPE);
    }
    OS_ENTER_CRITICAL();
    p_sem_data->OSEventGrp = pevent->OSEventGrp;            (3)
    psrc                   = &pevent->OSEventTbl[0]; 
    pdest                  = &p_sem_data->OSEventTbl[0];
    for (i = 0u; i < OS_EVENT_TBL_SIZE; i++) {              (4)
        *pdest++ = *psrc++;
    }
    p_sem_data->OSCnt = pevent->OSEventCnt;                
    OS_EXIT_CRITICAL();
    return (OS_ERR_NONE);
}

(1) & (2) As always, if OS_ARG_CHK_EN is set to 1, OSSemQuery() checks that pevent is not a NULL pointer and that it points to an ECB containing a semaphore.

(3) OSSemQuery() then copies the wait list from the OS_EVENT structure to the OS_SEM_DATA structure.

(4) Finally, OSSemQuery() copies the current semaphore count from the OS_EVENT structure to the OS_SEM_DATA structure.