[FreeRTOS] Semaphore - Introduction, common API functions, precautions, project implementation

In FreeRTOS, semaphore is a very important synchronization mechanism, used to achieve mutually exclusive access and synchronization operations between tasks. Through semaphores, different tasks can safely share resources and avoid competition and conflicts, thereby ensuring the stability and reliability of the system. This blog will introduce the basic concepts, usage and practical applications of semaphores in FreeRTOS, helping readers deeply understand the principles of semaphores and their application scenarios in actual projects. We will explain in detail the creation, acquisition and release of semaphores, combined with actual sample code, to help readers better master the skills and precautions for using semaphores in FreeRTOS. Through the study of this blog, readers will be able to use semaphores more flexibly to achieve synchronization and resource sharing between tasks, and improve the reliability and efficiency of the system.



1. Introduction to semaphores

Semaphore (Semaphore) is a mechanism for realizing communication between tasks. Synchronized or mutually exclusive access to critical resources, often used to assist a group of competing tasks to access critical resources. In a multi-tasking system, tasks need to be synchronized or mutually exclusive to protect critical resources. The semaphore function can provide users with support in this regard.

1.1 Binary semaphore

Binary semaphores can be used both for critical resource access and for synchronization functions.

Insert image description here

can only take on two values: 0 and 1 . This semaphore is often used to implement mutually exclusive access, that is, only one process or thread can access a shared resource. When a process or thread occupies a resource, the value of the binary semaphore is 1, and other processes or threads need to wait until the value of the semaphore becomes 0 before they can access the resource.

Binary semaphores are often used to solve critical section problems, which may lead to data inconsistencies or race conditions when multiple processes or threads need to access shared resources. By using binary semaphores, you can effectively control access to shared resources and avoid these problems.

In practical applications, binary semaphores are usually used in combination with mutex locks (mutex) , to achieve mutually exclusive access to shared resources. When a process or thread needs to access a shared resource, it first attempts to add a binary semaphore. If successful, the resource can be accessed, otherwise it needs to wait. After the access is completed, the binary semaphore is decremented to release the resources.


1.2 Counting semaphore

A binary semaphore can be thought of as a queue with a length of 1, while a counting semaphore can be thought of as a queue with a length greater than 1. Semaphore users still do not need to care about the messages stored in the queue, they only need to care about whether there are messages in the queue. .

Insert image description here

A semaphore that can take on multiple values ​​and is used to control the amount of access to a set of resources. The value of the counting semaphore can be greater than or equal to 0, indicating the number of available resources. When a process or thread needs to access a resource, it will try to decrement the counting semaphore. If the value of the counting semaphore is greater than 0, the resource can be accessed and the value of the counting semaphore will be decremented; if the value of the counting semaphore is If it is 0, you need to wait until other processes or threads release resources so that the value of the counting semaphore is greater than 0.

Counting semaphores are often used to implement concurrent access control to a set of resources, such as limiting the number of processes or threads that access a resource at the same time. This mechanism can effectively control concurrent access to resources, prevent resources from being over-occupied, and improve system performance and stability.

In practical applications, counting semaphores can be used to implement concurrency control scenarios such as thread pools and connection pools, as well as to limit concurrent access to other limited resources. By properly managing the value of the counting semaphore, you can effectively control concurrent access to resources and avoid problems with race conditions and data inconsistencies.


1.3 Mutually exclusive semaphore

Mutually exclusive semaphores are actually special binary semaphores. Due to their unique priority inheritance mechanism, they are more suitable for simple interlocks, that is, to protect critical resources.

Insert image description here

It can only take two values: 0 and 1. It is used to achieve mutually exclusive access to shared resources, that is, only one process or thread can access shared resources. When a process or thread occupies a resource, the value of the mutex semaphore is 1, and other processes or threads need to wait until the value of the semaphore becomes 0 before they can access the resource.

Mutex semaphores are often used in conjunction with synchronization mechanisms such as mutexes to achieve mutually exclusive access to shared resources. Before accessing the shared resource, the process or thread will try to decrement the mutex semaphore. If successful, the resource can be accessed, otherwise it needs to wait. After the access is completed, add the mutex semaphore to release the resources.

Mutex semaphore is an important tool to achieve process synchronization and mutual exclusion. It can effectively avoid race conditions and data inconsistency problems. In concurrent programming, mutex semaphores are usually used to protect critical sections, that is, a piece of code can only be executed by one thread at the same time to ensure data consistency and correctness.


1.4 Recursive semaphore

Allows the same thread to add to the semaphore multiple times without causing deadlock. Normally, adding an ordinary semaphore to the semaphore multiple times in the same thread may cause deadlock, because the value of the semaphore will be reduced multiple times, but there is only one addition operation to release the resource.

Recursive semaphores allow the same thread to add operations to the semaphore multiple times. Each addition operation will increase the value of the semaphore, thus avoiding deadlock. This mechanism is usually used when shared resources need to be locked in recursive functions, and other situations where resources need to be locked multiple times within the same thread.


1.5 Semaphore control block

Insert image description here
Insert image description here


2. Commonly used semaphore API functions

2.1 Create semaphore function

2.1.1 Create a binary semaphore xSemaphoreCreateBinary()
  • prototype:SemaphoreHandle_t xSemaphoreCreateBinary( void );
  • Function: Used to create a binary semaphore to implement mutually exclusive access to shared resources.
  • Parameters: No parameters.
  • Return value: Returns a handle of type SemaphoreHandle_t, representing the created binary semaphore.

xSemaphoreCreateBinary()The function of is to create a binary semaphore. The initial value of the binary semaphore is 0, which is used to implement mutually exclusive access to shared resources. It returns a handle of type SemaphoreHandle_t, which can be used for subsequent operations on the binary semaphore.


2.1.2 Create counting semaphore xSemaphoreCreateCounting()
  • prototype:SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount, UBaseType_t uxInitialCount );
  • Function: Used to create a counting semaphore to control the number of concurrent accesses to a set of resources.
  • parameter:
    • uxMaxCount: The maximum count value of the counting semaphore.
    • uxInitialCount: The initial count value of the counting semaphore.
  • Return value: Returns a handle of type SemaphoreHandle_t, representing the created counting semaphore.

xSemaphoreCreateCounting()The function is used to create a counting semaphore. The initial value of the counting semaphore is uxInitialCount and the maximum count value is uxMaxCount. Counting semaphores are usually used to limit the number of concurrent accesses to a group of resources. By reasonably managing the value of the counting semaphore, you can effectively control concurrent access to resources and avoid problems with race conditions and data inconsistencies.


2.2 Semaphore delete function vSemaphoreDelete()

  • prototype:void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
  • Function: Used to delete an already created semaphore.
  • parameter:
    • xSemaphore: The handle of the semaphore to be deleted.
  • Return value: None.

vSemaphoreDelete()The function is used to delete an already created semaphore by passing in the handle of the semaphore to be deleted xSemaphore. Once a semaphore is deleted, its handle is no longer valid and the semaphore can no longer be operated on.


2.3 Semaphore release function

2.3.1 xSemaphoreGive()
  • prototype:BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
  • Function: Used to release a binary semaphore or increase the count value of a counting semaphore.
  • parameter:
    • xSemaphore: The handle of the semaphore to be released.
  • Return value: pdTRUE if the release is successful, otherwise pdFALSE.

xSemaphoreGive()The function is used to release a binary semaphore or increase the count value of a counting semaphore. For binary semaphores, calling this function will increase the semaphore's count value from 0 to 1; for counting semaphores, calling this function will increase the semaphore's count value. If a task is awakened waiting for the semaphore, calling xSemaphoreGive() will cause one of the waiting tasks to continue execution.


2.3.2 xSemaphoreGiveFromISR()
  • prototype:BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken );
  • Function: Used to release a binary semaphore from the interrupt service routine or increase the count value of a counting semaphore.
  • parameter:
    • xSemaphore: The handle of the semaphore to be released.
    • pxHigherPriorityTaskWoken: A pointer to a BaseType_t type used to indicate whether a high-priority task is awakened.
  • Return value: pdTRUE if the release is successful, otherwise pdFALSE.

xSemaphoreGiveFromISR()The function is similar to the xSemaphoreGive() function and is used to release the semaphore from the interrupt service routine. It can be used to release a semaphore in an interrupt service routine to wake up tasks waiting for the semaphore. Different from xSemaphoreGive(), xSemaphoreGiveFromISR() can wake up a high-priority task waiting for the semaphore and indicate whether through the pxHigherPriorityTaskWoken parameter A high-priority task is awakened.


2.4 Semaphore acquisition function

2.4.1 xSemaphoreTake()
  • prototype:BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );
  • Function: Used to obtain a binary semaphore or reduce the count value of a counting semaphore.
  • parameter:
    • xSemaphore: The handle of the semaphore to be obtained.
    • xTicksToWait: The maximum time to wait to obtain the semaphore, expressed in clock ticks.
  • Return value: pdTRUE if the semaphore is successfully obtained, otherwise pdFALSE.

xSemaphoreTake()The function is used to obtain a binary semaphore or decrement the count value of a counting semaphore. If the count value of the semaphore is greater than 0, the acquisition is successful and the count value is reduced by one; if the count value of the semaphore is 0, the task will enter the blocking state until the semaphore is available or times out.

xTicksToWaitThe parameter is used to specify the maximum waiting time for the task to obtain the semaphore. If set to portMAX_DELAY, the task will wait until the semaphore is available; if set to 0, the task will return to obtain the result immediately without waiting; if set to other values, the task will wait for the specified number of clock ticks before returning to obtain the result. .


2.4.2 xSemaphoreTakeFromISR()
  • prototype:BaseType_t xSemaphoreTakeFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken );
  • Function: Used to obtain a binary semaphore from the interrupt service program or reduce the count value of a counting semaphore.
  • parameter:
    • xSemaphore: The handle of the semaphore to be obtained.
    • pxHigherPriorityTaskWoken: A pointer to a BaseType_t type used to indicate whether a high-priority task is awakened.
  • Return value: pdTRUE if the semaphore is successfully obtained, otherwise pdFALSE.

xSemaphoreTakeFromISR()The function is similar to the xSemaphoreTake() function and is used to obtain the semaphore from the interrupt service routine. It can be used to acquire a semaphore in an interrupt service routine to wait for the semaphore to become available or to decrement the semaphore's count. Different from xSemaphoreTake(), xSemaphoreTakeFromISR() can wake up a high-priority task waiting for the semaphore and indicate whether through the pxHigherPriorityTaskWoken parameter A high-priority task is awakened.


3.Examples

When using FreeRTOS on STM32, semaphores can be used to achieve synchronization and mutually exclusive access between tasks. Below is a simple example that demonstrates how to use semaphores in FreeRTOS on STM32.

Suppose we have two tasks: Task1 and Task2. They need to share a resource. We can use a semaphore to synchronize. Task1 will acquire the resource, perform some operations, and then release the resource; Task2 will wait for the resource to be available, then acquire the resource and perform the operation.

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

// 定义一个全局的信号量句柄
SemaphoreHandle_t xSemaphore;

void Task1(void *pvParameters) {
    
    
  while (1) {
    
    
    // 尝试获取信号量,等待最长100个时钟节拍
    if (xSemaphoreTake(xSemaphore, 100) == pdTRUE) {
    
    
      // 信号量获取成功,执行任务1的操作
      // ...

      // 释放信号量
      xSemaphoreGive(xSemaphore);
    } else {
    
    
      // 等待超时或者获取失败的处理
      // ...
    }
  }
}

void Task2(void *pvParameters) {
    
    
  while (1) {
    
    
    // 尝试获取信号量,等待最长100个时钟节拍
    if (xSemaphoreTake(xSemaphore, 100) == pdTRUE) {
    
    
      // 信号量获取成功,执行任务2的操作
      // ...

      // 释放信号量
      xSemaphoreGive(xSemaphore);
    } else {
    
    
      // 等待超时或者获取失败的处理
      // ...
    }
  }
}

int main(void) {
    
    
  // 创建一个二值信号量
  xSemaphore = xSemaphoreCreateBinary();

  // 创建任务 Task1
  xTaskCreate(Task1, "Task1", configMINIMAL_STACK_SIZE, NULL, 1, NULL);

  // 创建任务 Task2
  xTaskCreate(Task2, "Task2", configMINIMAL_STACK_SIZE, NULL, 2, NULL);

  // 启动调度器
  vTaskStartScheduler();

  while (1) {
    
    
    // 如果调度器启动失败,进行错误处理
    // ...
  }
}

In the above example, we first create a binary semaphore xSemaphore, and then Task1 and Task2 Use xSemaphoreTake() and xSemaphoreGive() to acquire and release semaphores.

In each task, we use the xSemaphoreTake() function to try to get the semaphore. If the acquisition is successful, the task can perform the corresponding operation; if the acquisition fails, the task will wait for a period of time (here is 100 clock ticks) and then try to acquire again. After the acquisition is successful, use xSemaphoreGive() to release the semaphore after the task completes the operation.

In this way, through the acquisition and release of the semaphore, we can realize resource sharing and synchronization between Task1 and Task2. Such a design can ensure that the operations between the two tasks will not interfere with each other, thereby achieving synchronization and mutually exclusive access between tasks.

Guess you like

Origin blog.csdn.net/Goforyouqp/article/details/134882909