一、引言
在嵌入式系统开发中,STM32F407 微控制器凭借其强大的性能和丰富的外设资源,成为众多应用场景的首选。而 FreeRTOS 作为一款开源的实时操作系统,为开发者提供了高效的任务管理、同步机制等功能,极大地提升了嵌入式系统开发的效率和可靠性。在多任务环境下,临界区保护是确保数据一致性和系统稳定性的关键技术。本文将深入探讨基于 STM32F407 HAL 库和 FreeRTOS 的临界区保护的作用以及使用方法。
二、STM32F407 与 FreeRTOS 简介
2.1 STM32F407 微控制器
STM32F407 是意法半导体(ST)推出的一款基于 ARM Cortex - M4 内核的 32 位微控制器。它具有高达 168MHz 的主频,拥有丰富的外设资源,如 GPIO、SPI、I2C、USART 等,适用于工业控制、消费电子、通信等多个领域。STM32F407 还具备浮点运算单元(FPU),能够高效地处理浮点运算,为一些对计算能力要求较高的应用提供了支持。
2.2 FreeRTOS 实时操作系统
FreeRTOS 是一个开源的、轻量级的实时操作系统内核,具有可裁剪性强、占用资源少、实时性高、易于移植等特点。它提供了任务管理、队列、信号量、互斥量等内核服务,使得开发者可以方便地实现多任务并发执行。在 FreeRTOS 中,任务是系统的基本执行单元,每个任务都有自己的优先级和堆栈空间。
三、临界区的概念
3.1 什么是临界区
临界区是指程序中访问共享资源的代码段。共享资源可以是全局变量、硬件寄存器、文件等。在多任务环境下,如果多个任务同时访问和修改共享资源,可能会导致数据不一致、程序崩溃等问题。例如,一个任务正在读取一个全局变量的值,而另一个任务同时在修改这个全局变量的值,那么读取到的值可能是错误的。
3.2 临界区问题的产生
临界区问题的产生主要是由于多任务并发执行和共享资源的存在。当多个任务同时访问和修改共享资源时,可能会出现以下几种情况:
- 数据竞争:多个任务同时对共享资源进行读写操作,导致数据的不一致性。
- 死锁:多个任务相互等待对方释放资源,从而导致程序陷入无限等待的状态。
- 优先级反转:低优先级任务持有高优先级任务所需的资源,导致高优先级任务无法及时执行。
四、临界区保护的作用
4.1 保证数据一致性
临界区保护的主要作用之一是保证数据的一致性。通过对临界区进行保护,确保在同一时间只有一个任务可以访问和修改共享资源,从而避免数据竞争问题。例如,在一个多任务系统中,多个任务需要对一个全局计数器进行操作,通过临界区保护,可以确保计数器的计数结果是正确的。
4.2 避免死锁和优先级反转
临界区保护还可以避免死锁和优先级反转问题。合理地使用同步机制(如互斥量、信号量等)可以确保任务在获取和释放资源时遵循一定的规则,从而避免死锁的发生。同时,通过优先级继承等机制,可以解决优先级反转问题,保证高优先级任务能够及时执行。
4.3 提高系统的稳定性和可靠性
通过对临界区进行保护,可以减少因数据不一致、死锁等问题导致的程序崩溃和错误,从而提高系统的稳定性和可靠性。在一些对可靠性要求较高的应用场景中,如航空航天、医疗设备等,临界区保护是必不可少的。
五、FreeRTOS 中临界区保护的方法
5.1 关中断
5.1.1 原理
关中断是一种简单而有效的临界区保护方法。在进入临界区之前,关闭所有中断,这样可以确保在临界区内不会有中断服务程序打断当前任务的执行,从而避免中断服务程序和当前任务同时访问共享资源。在离开临界区之后,再打开中断。
5.1.2 代码示例
#include "FreeRTOS.h"
#include "task.h"
// 共享资源
int shared_variable = 0;
// 任务函数
void task_function(void *pvParameters) {
portBASE_TYPE xStatus;
portBASE_TYPE uxSavedInterruptStatus;
while (1) {
// 进入临界区,关中断
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
// 访问共享资源
shared_variable++;
// 离开临界区,开中断
portCLEAR_INTERRUPT_MASK_FROM_ISR(uxSavedInterruptStatus);
// 任务延时
vTaskDelay(pdMS_TO_TICKS(100));
}
}
5.1.3 优缺点
- 优点:实现简单,能够有效避免中断服务程序和任务之间的竞争问题。
- 缺点:关中断会影响系统的实时性,因为在关中断期间,所有中断都被屏蔽,可能会导致一些紧急的中断事件无法及时处理。同时,关中断的时间不宜过长,否则会影响系统的稳定性。
5.2 互斥量
5.2.1 原理
互斥量是一种用于保护临界区的同步机制。它是一个二值信号量,只有两种状态:可用和已占用。当一个任务需要访问临界区时,它首先尝试获取互斥量。如果互斥量可用,任务获取互斥量并进入临界区;如果互斥量已被其他任务占用,任务将被阻塞,直到互斥量被释放。在离开临界区时,任务释放互斥量,以便其他任务可以获取。
5.2.2 代码示例
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
// 共享资源
int shared_variable = 0;
// 互斥量句柄
SemaphoreHandle_t xMutex;
// 任务函数
void task_function(void *pvParameters) {
portBASE_TYPE xStatus;
while (1) {
// 获取互斥量
xStatus = xSemaphoreTake(xMutex, portMAX_DELAY);
if (xStatus == pdPASS) {
// 进入临界区
shared_variable++;
// 离开临界区,释放互斥量
xSemaphoreGive(xMutex);
}
// 任务延时
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 创建互斥量
void create_mutex(void) {
xMutex = xSemaphoreCreateMutex();
if (xMutex == NULL) {
// 互斥量创建失败
}
}
5.2.3 优缺点
- 优点:不会影响系统的实时性,因为任务在获取不到互斥量时会被阻塞,不会占用 CPU 资源。同时,互斥量可以解决优先级反转问题,通过优先级继承机制,当高优先级任务等待低优先级任务持有的互斥量时,低优先级任务的优先级会临时提升到高优先级任务的优先级,从而避免高优先级任务长时间等待。
- 缺点:实现相对复杂,需要创建和管理互斥量对象。
5.3 信号量
5.3.1 原理
信号量是一种更通用的同步机制,它可以用于保护临界区,也可以用于任务之间的同步和通信。信号量有一个计数器,用于记录可用资源的数量。当一个任务需要访问临界区时,它首先尝试获取信号量。如果信号量的计数器大于 0,任务获取信号量并将计数器减 1,然后进入临界区;如果信号量的计数器等于 0,任务将被阻塞,直到有其他任务释放信号量。在离开临界区时,任务释放信号量,将计数器加 1。
5.3.2 代码示例
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
// 共享资源
int shared_variable = 0;
// 信号量句柄
SemaphoreHandle_t xSemaphore;
// 任务函数
void task_function(void *pvParameters) {
portBASE_TYPE xStatus;
while (1) {
// 获取信号量
xStatus = xSemaphoreTake(xSemaphore, portMAX_DELAY);
if (xStatus == pdPASS) {
// 进入临界区
shared_variable++;
// 离开临界区,释放信号量
xSemaphoreGive(xSemaphore);
}
// 任务延时
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 创建信号量
void create_semaphore(void) {
xSemaphore = xSemaphoreCreateCounting(1, 1);
if (xSemaphore == NULL) {
// 信号量创建失败
}
}
5.3.3 优缺点
- 优点:信号量可以用于多种场景,如资源管理、任务同步等。它的灵活性较高,可以根据实际需求设置信号量的初始值和计数器的上限。
- 缺点:与互斥量类似,实现相对复杂,需要创建和管理信号量对象。同时,信号量的使用需要谨慎,否则可能会导致死锁等问题。
六、基于 STM32F407 HAL 库和 FreeRTOS 的临界区保护示例
6.1 示例功能概述
本示例将创建两个任务,这两个任务都需要访问一个共享的全局变量。为了保证数据的一致性,使用互斥量对临界区进行保护。
6.2 代码实现
#include "stm32f4xx_hal.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
// 共享资源
int shared_variable = 0;
// 互斥量句柄
SemaphoreHandle_t xMutex;
// 任务 1 函数
void task1_function(void *pvParameters) {
portBASE_TYPE xStatus;
while (1) {
// 获取互斥量
xStatus = xSemaphoreTake(xMutex, portMAX_DELAY);
if (xStatus == pdPASS) {
// 进入临界区
shared_variable++;
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
// 离开临界区,释放互斥量
xSemaphoreGive(xMutex);
}
// 任务延时
vTaskDelay(pdMS_TO_TICKS(500));
}
}
// 任务 2 函数
void task2_function(void *pvParameters) {
portBASE_TYPE xStatus;
while (1) {
// 获取互斥量
xStatus = xSemaphoreTake(xMutex, portMAX_DELAY);
if (xStatus == pdPASS) {
// 进入临界区
shared_variable--;
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
// 离开临界区,释放互斥量
xSemaphoreGive(xMutex);
}
// 任务延时
vTaskDelay(pdMS_TO_TICKS(300));
}
}
// 系统时钟配置函数
void SystemClock_Config(void);
// GPIO 初始化函数
static void MX_GPIO_Init(void);
// FreeRTOS 初始化函数
static void MX_FREERTOS_Init(void);
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_FREERTOS_Init();
// 启动调度器
vTaskStartScheduler();
// 如果调度器启动失败,程序将执行到这里
while (1) {
}
}
void SystemClock_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** 初始化 CPU, AHB 和 APB 总线时钟
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
Error_Handler();
}
/** 初始化 CPU, AHB 和 APB 总线时钟
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK) {
Error_Handler();
}
}
static void MX_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_RESET);
/*Configure GPIO pin : PA5 */
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/*Configure GPIO pin : PD12 */
GPIO_InitStruct.Pin = GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
}
static void MX_FREERTOS_Init(void) {
// 创建互斥量
xMutex = xSemaphoreCreateMutex();
if (xMutex == NULL) {
// 互斥量创建失败
}
// 创建任务 1
xTaskCreate(task1_function, "Task1", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
// 创建任务 2
xTaskCreate(task2_function, "Task2", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
}
void Error_Handler(void) {
// 错误处理函数
while(1) {
}
}
6.3 代码解释
- 互斥量的创建 :在
MX_FREERTOS_Init
函数中,使用xSemaphoreCreateMutex
函数创建一个互斥量。 - 任务的创建 :创建了两个任务
task1_function
和task2_function
,这两个任务都需要访问共享变量shared_variable
。 - 临界区的保护 :在每个任务中,使用
xSemaphoreTake
函数获取互斥量,进入临界区;使用xSemaphoreGive
函数释放互斥量,离开临界区。这样可以确保在同一时间只有一个任务可以访问和修改共享变量。
七、临界区保护的注意事项
7.1 临界区的范围
临界区的范围应尽量小,只包含访问共享资源的必要代码。如果临界区范围过大,会导致任务在临界区内停留的时间过长,从而影响系统的并发性能。
7.2 避免死锁
在使用同步机制(如互斥量、信号量等)时,要避免死锁的发生。死锁通常是由于任务之间相互等待对方释放资源而导致的。为了避免死锁,可以采用以下方法:
- 按顺序获取资源:确保所有任务按照相同的顺序获取资源。
- 设置超时时间:在获取资源时设置超时时间,如果在规定时间内无法获取资源,任务可以放弃获取,避免无限等待。
7.3 中断服务程序中的临界区保护
在中断服务程序中访问共享资源时,也需要进行临界区保护。由于中断服务程序的执行不受任务调度的控制,因此不能使用互斥量等同步机制。可以使用关中断的方法来保护临界区,但要注意关中断的时间不宜过长。
八、总结
本文深入探讨了基于 STM32F407 HAL 库和 FreeRTOS 的临界区保护的作用以及使用方法。临界区保护是多任务环境下确保数据一致性和系统稳定性的关键技术。通过关中断、互斥量、信号量等方法,可以有效地保护临界区,避免数据竞争、死锁和优先级反转等问题。在实际开发中,开发者应根据具体需求选择合适的临界区保护方法,并注意临界区的范围、避免死锁和中断服务程序中的临界区保护等问题。