FreeRTOS 中断处理和中断安全API
2024-12-18 20:03:48+08:00

FreeRTOS 中断处理和中断安全API

FreeRTOS的任务优先级是数值越大,优先级越高,0是最低优先级;而Cortex-M的中断优先级是数值越大,优先级越低,0是最高优先级。

FreeRTOSConfig.h中的宏configLIBRARY_LOWEST_INTERRUPT_PRIORITY表示最低中断优先级,从FreeRTOS的demo中复制出来的这个头文件中这个宏的值是15,即从0到15一共16个优先级。Cortex-M的中断优先级有抢占优先级和子优先级两个,但FreeRTOS中没有提供处理子优先级的功能,只使用抢占优先级。因此需要将STM32的中断优先级组设置为16个抢占优先级、1个子优先级,即第四组中断优先级组。

//优先级组是STM32中优先级分配方式的选择,"组"这个翻译可能不太合适
//使用第四组中断优先级组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
//STM32有0到4共五组优先级组
//0组:1个抢占优先级,16个子优先级
//1组:2个抢占优先级,8个子优先级
//2组:4个抢占优先级,4个子优先级
//3组:8个抢占优先级,2个子优先级
//4组:16个抢占优先级,1个子优先级

portmacro.h中的宏portDISABLE_INTERRUPT()portENABLE_INTERRUPT()分别关闭和打开优先级低于FreeRTOSConfig.h中的宏configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY的中断。

在优先级高于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY的中断服务程序中,不能使用FreeRTOS的任何API。

FreeRTOS中的任务API可能会让任务进入阻塞,但中断服务程序不是FreeRTOS的任务,因此可能导致错误。因此在优先级低于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY的中断服务程序中,不能使用FreeRTOS的普通API,而必须使用名称中含有FromISR中断安全API

例如在中断服务程序中:保护临界区,不能使用taskENTER_CRITICAL()taskEXIT_CRITICAL(),而应当使用taskENTER_CRITICAL_FROM_ISR()taskEXIT_CRITICAL_FROM_ISR();向队列发送数据不能使用xQueueSendToBack()而应当使用xQueueSendToBackFromISR()

FreeRTOS 临界区
2024-12-18 20:03:48+08:00

FreeRTOS 临界区

在并行编程中,并行存取共享的资源时,常常会导致意外和错误的结果。例如下面代码task1和task2都通过串口打印消息,但由于任务调度,消息被截断了。

#include <stm32f4xx.h>
#include <FreeRTOS.h>
#include <task.h>
#include <uart.h>
 
void task1(void* args);
void task2(void* args);
 
int main()
{
    //配置USART1
    USART1_Config();
    //创建任务
    TaskHandle_t h1,h2;
    xTaskCreate(task1,"task1",configMINIMAL_STACK_SIZE,NULL,1,&h1);
    xTaskCreate(task2,"task2",configMINIMAL_STACK_SIZE,NULL,1,&h2);
    //开启任务调度
    vTaskStartScheduler();
    while(1);
}
 
 
void task1(void* args)
{
    USART_printf(USART1,"task1 is running.\n");
    vTaskDelete(NULL);
}
 
void task2(void* args)
{
    USART_printf(USART1,"task2 is running.\n");
    vTaskDelete(NULL);
}

Image

类似的,当修改公共变量时任务调度可能导致变量的值错误、操作时序严格的设备时任务调度可能导致设备不能正常工作。

因此,访问共享资源的部分代码会被保护起来,在执行这段代码时不进行任务调度,这样的代码段称为临界区(Critical Section)

FreeRTOS通过两个包含在task.h里的宏taskENTER_CRITICAL()taskEXIT_CRITICAL()来进入和离开临界区在。taskENTER_CRITICAL()之后、taskEXIT_CRITICAL()之前不会切换到其他任务。

修改USART_printf函数,在打印消息前后加入taskENTER_CRITICAL()taskEXIT_CRITICAL(),这样在USART_printf运行期间就不会切换任务,打印的消息也就不会被截断。

#define USART_PRINTF_BUFFER_SIZE 256
 
int USART_printf(USART_TypeDef* port,const char* fmt,...)
{
    char str[USART_PRINTF_BUFFER_SIZE];
    int length,index;
    va_list argList;
    va_start(argList,fmt);
    length = vsnprintf(str,USART_PRINTF_BUFFER_SIZE-1,fmt,argList);
    va_end(argList);
    taskENTER_CRITICAL();//进入临界区
    for(index = 0 ; index < length ; index++)
    {
        USART_WriteByte(port,str[index]);
    }
    taskEXIT_CRITICAL();//离开临界区
    return length;
}

Image

临界区嵌套是安全的,因为内核有维护一个嵌套深度计数。临界区只会在嵌套深度为0时才会真正退出——即在为每个之前调用的taskENTER_CRITICAL()都配套调用了taskEXIT_CRITICAL()之后。

临界区是互斥功能的一种非常原始的实现方式,通常只是关闭所有中断从而使任务调度暂停。这样可能会导致任务不能正常执行,中断不能及时响应等问题。所以临界区应当只具有很短的时间。上例中打印串口消息是一个相当耗费时间的过程,并不适合使用临界区。

另外也可以将调度器挂起来保护临界区,vTaskSuspendAll挂起调度器、xTaskResumeAll恢复调度器,在vTaskSuspendAllxTaskResumeAll之间不会进行任务调度,但可以响应中断。

#include <task.h>
void vTaskSuspendAll(void);
BaseType_t xTaskResumeAll(void);
FreeRTOS 事件标志组
2024-12-18 20:03:48+08:00

FreeRTOS 事件标志组

FreeRTOS可以使用事件标志组进行任务同步,一个事件标准组包含多个事件标志位,每一位标志一个事件。当configUSE_16BIT_TICKS为1时,一个事件标志组有8位;当这个宏为0时,一个事件标志组有24位。

创建事件标志组使用xEventGroupCreate

#include <event_groups.h>
EventGroupHandle_t xEventGroupCreate(void);
//返回事件标志组句柄,失败返回NULL

置位和清除事件标志位使用xEventGroupSetBitsxEventGroupClearBits。参数是事件标志组句柄和要操作的位。

#include <event_groups.h>
 
EventBits_t xEventGroupSetBits(EventGroupHandle_t xEventGroup,const EventBits_t uxBitsToSet);
EventBits_t xEventGroupClearBits(EventGroupHandle_t xEventGroup,const EventBits_t uxBitsToClear);

xEventGroupGetBits获取事件标志组的值。

#include <event_groups.h>
EventBits_t xEventGroupGetBits(EventGroupHandle_t xEventGroup);

xEventGroupWaitBits阻塞等待事件位。

#include <event_groups.h>
EventBits_t xEventGroupWaitBits(EventGroupHandle_t xEventGroup, 
                                const EventBits_t uxBitsToWaitFor, 
                                const BaseType_t xClearOnExit, 
                                const BaseType_t xWaitForAllBits, 
                                TickType_t xTicksToWait );
  • 第一个参数是事件标志组句柄
  • 第二个参数是要等待的事件标志位
  • 第三个参数是是否清除位,pdTRUE表示清除,pdFASLE表示不清除
  • 第四个参数是是否等待所有位,pdTRUE表示是,pdFASLE表示否
  • 第五个参数是阻塞超时时间
//等待bit0 bit1 bit2 bit3,其中任意一位置位就解除阻塞,之后清除bit0 bit1 bit2 bit3
xEventGroupWaitBits(handle,0x0f,pdTRUE,pdFALSE,100/portTICK_RATE_MS);
 
//等待bit0 bit1 bit2 bit3,四个位全部置位才解除阻塞,之后清除bit0 bit1 bit2 bit3
xEventGroupWaitBits(handle,0x0f,pdTRUE,pdTRUE,100/portTICK_RATE_MS);
FreeRTOS 任务的创建与删除
2024-12-18 20:03:48+08:00

FreeRTOS 任务的创建与删除

FreeRTOS的任务函数原型为:

void task(void* args);

使用FreeRTOS的xTaskCreate函数来创建任务:

#include <task.h>
BaseType_t xTaskCreate(TaskFunction_t pxTaskCode,
             const char * const pcName,
            const uint16_t usStackDepth,
            void * const pvParameters,
            UBaseType_t uxPriority,
            TaskHandle_t * const pxCreatedTask ) PRIVILEGED_FUNCTION;
//返回值:成功返回pdPASS;失败返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY
  • pxTaskCode是任务函数,没有返回值(void),参数为void*
  • pcName是任务的名字,用于调试,其长度不能超过configMAX_TASK_NAME_LEN
  • usStackDepth是任务的堆栈深度,其四倍即是堆栈的大小(字节数)
  • pvParameters是传给任务函数的参数
  • uxPriority是任务优先级,数值越大,优先级越高,0为最低优先级
  • pxCreatedTask是返回的任务句柄

使用vTaskStartScheduler函数来开启任务调度:

#include <task.h>
void vTaskStartScheduler(void);

FreeRTOS的任务函数不能返回,对于不再需要的任务,应当使用vTaskDelete函数来进行删除,其参数是xTaskCreate最后一个参数返回的任务句柄,将参数设为NULL表示删除当前任务:

#include <task.h>
void vTaskDelete(TaskHandle_t xTaskToDelete);

下面是一个例子,创建两个任务,通过串口打印消息:

#include <stm32f4xx.h>
#include <FreeRTOS.h>
#include <task.h>
#include <uart.h>
 
void task1(void* args);
void task2(void* args);
 
int main()
{
    //配置USART1
    USART1_Config();
    //创建任务
    TaskHandle_t h1,h2;
    xTaskCreate(task1,"task1",configMINIMAL_STACK_SIZE,"hello\n",1,&h1);
    xTaskCreate(task2,"task2",configMINIMAL_STACK_SIZE,"world\n",1,&h2);
    //开启任务调度
    vTaskStartScheduler();
    while(1);
}
 
 
void task1(void* args)
{
    int i = 0;
    while(1)
    {
        //循环5次后删除自己
        if(i >= 5)
        {
            vTaskDelete(NULL);
        }
        //打印参数
        USART_printf(USART1,args);
        //延时1000ms
        vTaskDelay(1000/portTICK_RATE_MS);
        i++;
    }
}
 
void task2(void* args)
{
    int i = 0;
    while(1)
    {
        //循环10次后删除自己
        if(i >= 10)
        {
            vTaskDelete(NULL);
        }
        //打印参数
        USART_printf(USART1,args);
        //延时1000ms
        vTaskDelay(1000/portTICK_RATE_MS);
        i++;
    }
}

Image

附.USART代码代码 :

/**
  * @file       uart.c
  * @author     PlanC
  * @blog       www.kurukurumi.com
  * @brief      configure USART1 to print message
  */
 
#include <stdarg.h>
#include <stdio.h>
#include <stm32f4xx.h>
#include <uart.h>
 
/**
  * @brief  Initialize the USART1
  * @note   set USART1_RX GPIOA10 as push-pull output
  *           set USART1_TX GPIOA9 as pull-up input
  *           set USART1 as 115200-8-N-1
  */
void USART1_Config(void)
{
    GPIO_InitTypeDef gpio;
    USART_InitTypeDef usart;
     
    // enable the perpheral clock
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
     
    gpio.GPIO_Mode = GPIO_Mode_AF;
    gpio.GPIO_PuPd = GPIO_PuPd_UP;
    gpio.GPIO_OType = GPIO_OType_PP;
    gpio.GPIO_Speed = GPIO_High_Speed;
     
    //set UART1_RX GPIOA10 as push-pull output
    gpio.GPIO_Pin = GPIO_Pin_10;
    GPIO_Init(GPIOA,&gpio);
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1);
     
    //set UART1_TX GPIOA9 as pull-up input
    gpio.GPIO_Pin = GPIO_Pin_9;
    GPIO_Init(GPIOA,&gpio);
    GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1);
     
    //set USART1 as 115200-8-N-1
    usart.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    usart.USART_BaudRate = 115200;
    usart.USART_WordLength = USART_WordLength_8b;
    usart.USART_Parity = USART_Parity_No;
    usart.USART_StopBits = USART_StopBits_1;
    USART_Init(USART1,&usart);
    USART_Cmd(USART1,ENABLE);
}
 
 
/**
  * @brief  Send a byte data from USART
  * @param  port: the USART which will be used
  * @param  data: the data which will be sent
  */
 
void USART_WriteByte(USART_TypeDef* port,uint8_t data)
{
    USART_SendData(port,data);
    while(USART_GetFlagStatus(port,USART_FLAG_TXE) == RESET);
}
 
 
/**
  * @brief  print a format string from USART
  * @param  port: the USART which will be used
  * @param  fmt: the format string
  *
  */
 
#define USART_PRINTF_BUFFER_SIZE 256
 
int USART_printf(USART_TypeDef* port,const char* fmt,...)
{
    char str[USART_PRINTF_BUFFER_SIZE];
    int length,index;
    va_list argList;
    va_start(argList,fmt);
    length = vsnprintf(str,USART_PRINTF_BUFFER_SIZE-1,fmt,argList);
    va_end(argList);
    for(index = 0 ; index < length ; index++)
    {
        USART_WriteByte(port,str[index]);
    }
    return length;
}
FreeRTOS 任务的状态
2024-12-18 20:03:48+08:00

FreeRTOS 任务的状态

xTaskCreate函数创建任务时,即为任务赋予了优先级。可以使用vTaskPrioritySet函数来修改任务的优先级,它的第一个参数是任务句柄、第二个参数是优先级,这个函数必须在调度器启动之后才能调用。相应的,uxTaskPriority函数返回一个任务的优先级。

#include <task.h>
void vTaskPrioritySet(TaskHandle_t xTask,UBaseType_t uxNewPriority);
 
UBaseType_t uxTaskPriorityGet(TaskHandle_t xTask);
//返回任务的优先级

对于优先级相同的任务,FreeRTOS采用时间片轮询调度,每个任务轮流执行一段微小的时间。下面这段代码创建两个优先级相同的任务打印串口消息:

#include <stm32f4xx.h>
#include <FreeRTOS.h>
#include <task.h>
#include <uart.h>
 
void task1(void* args);
void task2(void* args);
 
int main()
{
    //配置USART1
    USART1_Config();
    //创建任务
    TaskHandle_t h1,h2;
    xTaskCreate(task1,"task1",configMINIMAL_STACK_SIZE,"hello\n",1,&h1);
    xTaskCreate(task2,"task2",configMINIMAL_STACK_SIZE,"world\n",1,&h2);
     
    //开启任务调度
    vTaskStartScheduler();
    while(1);
}
 
 
void task1(void* args)
{
    int i = 0;
    while(1)
    {
        //循环5次后删除自己
        if(i >= 5)
        {
            vTaskDelete(NULL);
        }
        //打印参数
        USART_printf(USART1,args);
        i++;
    }
}
 
void task2(void* args)
{
    int i = 0;
    while(1)
    {
        //循环10次后删除自己
        if(i >= 10)
        {
            vTaskDelete(NULL);
        }
        //打印参数
        USART_printf(USART1,args);
        i++;
    }
}

Image

对于优先级不同的任务,FreeRTOS采用抢占式调度,高优先级的任务优先执行。将上面代码中的task1优先级设为2,task1会优先执行:

#include <stm32f4xx.h>
#include <FreeRTOS.h>
#include <task.h>
#include <uart.h>
 
void task1(void* args);
void task2(void* args);
 
int main()
{
    //配置USART1
    USART1_Config();
    //创建任务
    TaskHandle_t h1,h2;
    xTaskCreate(task1,"task1",configMINIMAL_STACK_SIZE,"hello\n",2,&h1);
    xTaskCreate(task2,"task2",configMINIMAL_STACK_SIZE,"world\n",1,&h2);
    //开启任务调度
    vTaskStartScheduler();
    while(1);
}
 
 
void task1(void* args)
{
    int i = 0;
    while(1)
    {
        //循环5次后删除自己
        if(i >= 5)
        {
            vTaskDelete(NULL);
        }
        //打印参数
        USART_printf(USART1,args);
        i++;
    }
}
 
void task2(void* args)
{
    int i = 0;
    while(1)
    {
        //循环10次后删除自己
        if(i >= 10)
        {
            vTaskDelete(NULL);
        }
        //打印参数
        USART_printf(USART1,args);
        i++;
    }
}

Image

FreeRTOS中任务存在4种状态:运行就绪阻塞挂起。调度器总是让运行和就绪状态的任务中,优先级最高的任务进入运行状态。

状态 说明
运行状态(Running) 正在运行的任务处于运行状态
就绪状态(Ready) 可以运行但没有运行的任务处于就绪状态
阻塞状态(Blocked) 等待某一事件而不能运行的任务处于阻塞状态
挂起状态(Suspended) 调用vTaskSuspend挂起的任务处于挂起状态

Image

运行状态的任务可以通过调用阻塞函数进入阻塞状态,阻塞解除的事件可以让阻塞状态的任务进入就绪状态,vTaskSuspend函数让任务进入挂起状态,vTaskResume函数让挂起的任务进入就绪状态。

vTaskDelay是一个阻塞函数,它让任务进入阻塞状态等待一个定时事件,它的参数是定时事件的延时时间(周期数)。当定时时间到达,就会产生定时事件,让任务进入就绪状态。

#include <task.h>
void vTaskDelay(const TickType_t xTicksToDelay);

portTICK_RATE_MS表示每毫秒的周期数,用期望延时的毫秒数除以她可以得到相应的周期数,例如vTaskDelay(1000/portTICK_RATE_MS)延时1000毫秒。

仍然让task1的优先级高于task2,在task1中调用vTaskDelay阻塞,在task1阻塞期间,task2是优先级最高的任务,因此运行task2:

#include <stm32f4xx.h>
#include <FreeRTOS.h>
#include <task.h>
#include <uart.h>
 
void task1(void* args);
void task2(void* args);
 
int main()
{
    //配置USART1
    USART1_Config();
    //创建任务
    TaskHandle_t h1,h2;
    xTaskCreate(task1,"task1",configMINIMAL_STACK_SIZE,"hello\n",2,&h1);
    xTaskCreate(task2,"task2",configMINIMAL_STACK_SIZE,"world\n",1,&h2);
    //开启任务调度
    vTaskStartScheduler();
    while(1);
}
 
 
void task1(void* args)
{
    int i = 0;
    //阻塞1000ms
    vTaskDelay(1000/portTICK_RATE_MS);
    while(1)
    {
        //循环5次后删除自己
        if(i >= 5)
        {
            vTaskDelete(NULL);
        }
        //打印参数
        USART_printf(USART1,args);
        i++;
    }
}
 
void task2(void* args)
{
    int i = 0;
    while(1)
    {
        //循环10次后删除自己
        if(i >= 10)
        {
            vTaskDelete(NULL);
        }
        //打印参数
        USART_printf(USART1,args);
        i++;
    }
}

Image

挂起状态的任务对于调度器而言是不可见的。vTaskSuspend让任务进入挂起状态,vTaskResume让挂起的任务进入就绪状态,它们的参数都是要操作任务的句柄。

#include <task.h>
void vTaskSuspend(TaskHandle_t xTaskToSuspend);
void vTaskResume(TaskHandle_t xTaskToResume);
FreeRTOS 信号量和互斥量
2024-12-18 20:03:48+08:00

FreeRTOS 信号量和互斥量

信号量(Semaphore)也是一种任务间通信的常用方式,通常用于任务的同步。

#include <semphr.h>
 
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
//产生一个信号,成功返回pdPASS
//信号量已满则失败,返回errQUEUE_FULL

BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,TickType_t xBlockTime);
//获取一个信号,信号量为空则阻塞
//第二个参数是阻塞超时时间,成功返回pdTRUE,超时返回pdFALSE

值信号量是只有两个值(例如0或1、满或空)的信号量,FreeRTOS的二值信号量相当于只有一个元素的队列,用满和空表示信号量的值。

#include <semphr.h>
 
SemaphoreHandle_t xSemaphoreCreateBinary(void);
//创建一个二值信号量,成功返回信号量句柄,失败返回NULL
  • 二值信号量为空时调用xSemaphoreGive,信号量变满;为满时失败。
  • 二值信号量为满时调用xSemaphoreTake,信号量变空;为空时阻塞。

计数信号量顾名思义是计数用的信号量。

#include <semphr.h>
 
SemaphoreHandle_t xSemaphoreCreateCounting(void);
//创建一个计数信号量,成功返回信号量句柄,失败返回NULL
 
UBaseType_t uxSemaphoreGetCount(const QueueHandle_t xSemaphore);
//返回计数信号量的计数值
  • xSemaphoreGive使计数信号量加一。
  • xSemaphoreTake使计数信号量减一。

信号量通常用于任务同步,例如多个任务共享资源时,使用二值信号量进行访问控制,二值信号量为满则表示资源可用,二值信号量为空则表示资源正在被使用。也可以使用计数信号量计数有多少个任务在使用共享资源。

下面这个例子使用二值信号量标志串口是否被使用:

#include <stm32f4xx.h>
#include <FreeRTOS.h>
#include <task.h>
#include <queue.h>
#include <semphr.h>
#include <uart.h>
 
void task1(void* args);
void task2(void* args);
 
SemaphoreHandle_t binary;
 
int main()
{
    //配置USART1
    USART1_Config();
    //创建任务
    TaskHandle_t h1,h2;
    //创建二值信号量,用这个信号量标识串口是否可用
    binary = xSemaphoreCreateBinary();
    //使二值信号量变为满,表示资源可用
    xSemaphoreGive(binary);
    xTaskCreate(task1,"task1",configMINIMAL_STACK_SIZE,NULL,1,&h1);
    xTaskCreate(task2,"task2",configMINIMAL_STACK_SIZE,NULL,1,&h2);
    //开启任务调度
    vTaskStartScheduler();
    while(1);
}
 
 
void task1(void* args)
{
    int i = 0;
    while(1)
    {
        //循环五次后删除自己
        if(i >= 5)
        {
            vTaskDelete(NULL);
        }
        //使二值信号量变为空,表示资源被占用
        xSemaphoreTake(binary,100/portTICK_RATE_MS);
        USART_printf(USART1,"The task1 is running now.\n");
        //使二值信号量变为满,表示资源可用
        xSemaphoreGive(binary);
        i++;
    }
}
 
void task2(void* args)
{
    int i = 0;
    while(1)
    {
        //循环十次后删除自己
        if(i >= 10)
        {
            vTaskDelete(NULL);
        }
        //使二值信号量变为空,表示资源被占用
        xSemaphoreTake(binary,100/portTICK_RATE_MS);
        USART_printf(USART1,"The task2 is running now.\n");
        //使二值信号量变为满,表示资源可用
        xSemaphoreGive(binary);
        i++;
    }
}

Image

不过这种方式可能导致优先级反转:

(1)假设优先级task1>task2>task3  
(2)task1和task2阻塞或挂起  
(3)task3运行并通过二值信号量占用了某个公共资源  
(4)task1就绪并试图通过二值信号量使用被task3占用的资源,但由于资源被占用,task1阻塞  
(5)task2就绪,由于它优先级高于task3,因此task2优先执行  
(6)task2执行完之后task3继续执行  
(7)task3释放资源,task1解除阻塞并执行  

如此以来优先级相当于变成了task2>task1,原本优先级最高的task1不能及时运行,这在实时性要求高的场合是不被允许的。

互斥量(Mutex)是处理共享资源同步的常用方式。在FreeRTOS的实现中,互斥量是一种特殊的二值信号量,其操作方式和二值信号量一样,但互斥量具有优先级继承的特性。

(1)假设优先级task1>task2>task3  
(2)task1和task2阻塞或挂起  
(3)task3运行并通过互斥量占用了某个公共资源  
(4)task1就绪并试图通过互斥量使用被task3占用的资源,但由于资源被占用,task1阻塞  
(5)task3的优先级被提升到和task1一样  

如此一来就不会发生task2在task1之前运行的情况,提高了实时性。

xSemaphoreCreateMutex用于创建互斥量。

#include <semphr.h>
 
SemaphoreHandle_t xSemaphoreCreateMutex(void);
//创建一个互斥量,成功返回信号量句柄,失败返回NULL

假如任务中调用一个函数来使用公共资源,在调用这个函数前使用xSemaphoreTake占用该资源的互斥量,在这个函数返回后使用xSemaphoreGive来释放该资源的互斥量。但这个函数中可能也使用了xSemaphoreTakexSemaphoreGive来占用和释放资源。这种情况下就会导致错误。

递归互斥量可以让同一个任务多次占用资源,在释放同等次数互斥量后释放资源。

#include <semphr.h>
 
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(void);
//创建一个递归互斥量,成功返回信号量句柄,失败返回NULL
FreeRTOS 内存管理
2024-12-18 20:03:48+08:00

FreeRTOS 内存管理

FreeRTOS的内存管理API定义在heep_1.cheep_2.cheep_3.cheep_4.cheep_5.c中,这五个文件中的内存管理API有所不同。

  • heep_1.c只能分配内存而不能释放内存。
  • heep_2.c可以分配和释放内存但不能合并空闲内存块。
  • heep_3.c简单的封装了线程安全版的标准C语言malloc和free函数。
  • heep_4.c可以合并相邻的空闲内存块。
  • heep_5.c可以合并相邻的空闲内存块,且可以管理地址不连续的物理内存。

分配内存的API是pvPortMalloc,释放内存的API是vPortFree

#include <portable.h>
 
void* pvPortMalloc(size_t xWantedSize);
//分配指定大小的内存
 
void vPortFree(void *pv);
//释放内存,使用heep_1.c的话,这个函数不工作

如果使用heep_5.c则需要使用vPortDefineHeapRegions进行初始化。

#include <portable.h>
void vPortDefineHeapRegions(const HeapRegion_t * const pxHeapRegions);
FreeRTOS 协同程序
2024-12-18 20:03:48+08:00

FreeRTOS 协同程序

协同程序是一种特殊的多任务编程方式,多个协同程序之间共用调用栈,且正在运行的协同程序不会被其他协程抢占(可以被任务和中断抢占),正在运行的协同程序只能自己主动让出CPU的使用权。要使用协同程序,需要将FreeRTOSConfig.h中的configUSE_CO_ROUTINES设为1

FreeRTOS的协同程序采用switch-case实现,函数定义方式如下:

void CoRoutineTask(CoRoutineHandle_t handle,UBaseType_t uxIndex)
{
    //协同程序中变量如果要保证值在下一次运行时仍有效,则必须为static
    static const TickType_t delay = 1000 / portTICK_PERIOD_MS;
 
    //协同程序必须以crSTART(handle)开始
    crSTART(handle);
     
    //协同程序主体
    while(1)
    {
        /* 协同程序工作内容 */
         
        //主动阻塞1000ms,让出CPU使用权
        crDELAY(handle,delay);
    }
     
    //协同程序必须以crEND()结束
    crEND()    
}

使用vCoRoutineSchedule来调度协同程序,调用这个函数时,他会运行可运行的优先级最高的协同程序,当这个协同程序让出CPU时,vCoRoutineSchedule返回。需要不断的调用这个函数来进行协程的调度。

#include <croutine.h>
void vCoRoutineSchedule(void);

使用xCoRoutineCreate来创建一个协程,它的第一个参数是协程函数,第二个参数是协程优先级,可以用同一个协程函数创建多个协程,第三个参数用来区分同一个函数创建的协程。

#include <croutine.h>
BaseType_t xCoRoutineCreate(crCOROUTINE_CODE pxCoRoutineCode, 
                            UBaseType_t uxPriority, 
                            UBaseType_t uxIndex );

下面这个示例使用同一个函数创建了4个协程,协程函数打印uxIndex并阻塞1000ms:

#include <stm32f4xx.h>
#include <FreeRTOS.h>
#include <task.h>
#include <croutine.h>
#include <uart.h>
 
void coTask(CoRoutineHandle_t h,UBaseType_t uxIndex);
void task(void* args);
 
int main()
{
    //配置USART1
    USART1_Config();
    //创建协程
    xCoRoutineCreate(coTask,0,0);
    xCoRoutineCreate(coTask,0,1);
    xCoRoutineCreate(coTask,0,2);
    xCoRoutineCreate(coTask,0,3);
    //创建任务,用于调度协程
    xTaskCreate(task,"task1",configMINIMAL_STACK_SIZE,NULL,1,NULL);
    //启动任务调度器
    vTaskStartScheduler();
}
 
void task(void* args)
{
    while(1)
    {
        //运行优先级最高的协程,该协程让出时,函数返回
        vCoRoutineSchedule();
    }
}
 
 
void coTask(CoRoutineHandle_t handle,UBaseType_t uxIndex)
{
    //如果协程函数希望在阻塞后仍能保存变量的值,那么变量必须是static的
    static const TickType_t delay = 1000 / portTICK_PERIOD_MS;
     
    //协程函数必须以crSTART开始
    crSTART(handle);
     
    while(1)
    {
        //通过串口打印uxIndex
        USART_printf(USART1,"coTask1 : %d\n",uxIndex);
        //在协程中延时1000ms
        crDELAY(handle,delay);
    }
     
    //协程函数必须以crEND结束
    crEND();
     
}

Image

FreeRTOS 搭建开发环境
2024-12-18 20:03:48+08:00

FreeRTOS 搭建开发环境

本文使用Keil MDK和STM32为例

首先需要下载FreeRTOS的源码:http://www.freertos.org/。当前最新版本为9.0.0,其源码目录如下图 :

Image

FreeRTOS/Demo目录下已经有许多工程可以直接使用,例如FreeRTOS\Demo\CORTEX_STM32F103_Keil就是STM32F103在Keil下的工程。不过这个工程是针对小容量STM32F103的,对于其他型号的STM32芯片,需要自己组织工程。

首先创建一个文件夹用于存放工程文件,命名为“Project”。
FreeRTOS/Source目录下是FreeRTOS的源码,将其中除了portable文件夹以外所有的文件复制到Project文件夹中,这些是平台无关的代码。

Image

FreeRTOS/Source/portable目录下是平台有关的可移植代码。

Image

其中FreeRTOS/Source/protable/MemMang目录下是内存管理的相关代码。

  • heep_1.c只能分配内存而不能释放内存。
  • heep_2.c可以分配和释放内存但不能合并空闲内存块。
  • heep_3.c简单的封装了线程安全版的标准C语言malloc和free函数。
  • heep_4.c可以合并相邻的空闲内存块。
  • heep_5.c可以合并相邻的空闲内存块,且可以管理地址不连续的物理内存。

这里选用heep_2.c,将它复制到Project文件夹中。

Image

接下来,由于我们的目标平台是Keil,因此代开FreeRTOS/Source/protable/Keil文件夹看到如下文件。

Image

因此我们打开FreeRTOS/Source/protable/RVDS文件夹,根据芯片型号选择。我的目标芯片STM32F429IGT6Cortex-M4F内核,因此将ARM_CM4F中的port.c复制到Project中,portmacro.h复制到Project/include中。

Image

然后,我们还需要一个FreeRTOSConfig.h,在FreeRTOS/Demo/CORTEX_M4F_STM32F407ZG-SK中复制它到Project/include中。

最后得到Project以及include下的文件:
Image
Image

接下来将这些文件复制到一个Keil的STM32标准外设库工程中,将.c文件加入工程,将include文件夹加入头文件搜索路径。

接下来需要修改部分代码:

FreeRTOS使用了一些中断功能,因此port.c中定义了部分中断函数,STM32标准外设库中的stm32f4xx_it.c中也定义了这些中断函数,因此需要删除stm32f4xx_it.c中的这些函数。它们是SVC_HandlerPendSV_HandlerSysTick_Handler

port.c中使用了变量SystemCoreClock,这个变量定义在标准外设库的system_stm32f4xx.c中,需要在port.c中声明。

FreeRTOS中使用了一些hook函数,这些函数需要我们自己编写(可以写成空的函数),也可以将FreeRTOSConfig.h中的configUSE_IDLE_HOOKconfigUSE_TICK_HOOKconfigUSE_MALLOC_FAILED_HOOKconfigCHECK_FOR_STACK_OVERFLOW设为0从而不使用这些hook函数。

如此一来工程就配置完成了。

FreeRTOS 队列
2024-12-18 20:03:48+08:00

FreeRTOS 队列

队列(Queue)是一种数据结构,它是一种特殊的线性表,只允许在队列的首端进行删除,尾端进行插入,具有先入先出的特性。是任务间通信的一种常用方式。

xQueueCreate函数创建一个队列,它的第一个参数是队列的长度(元素的个数),第二个参数是每个元素的大小(byte)。

#include <queue.h>
QueueHandle_t xQueueCreate(const UBaseType_t uxQueueLength,const UBaseType_t uxItemSize);
//成功返回队列句柄,失败返回NULL

FreeRTOS中既可在队尾插入,也能在队首插入,但是只能从队首取出数据。

#include <queue.h>
 
BaseType_t xQueueSendToBack( QueueHandle_t xQueue, 
                            const void * const pvItemToQueue, 
                            TickType_t xTicksToWait);
//向队尾插入一个元素
 
BaseType_t xQueueSendToFront( QueueHandle_t xQueue, 
                            const void * const pvItemToQueue, 
                           TickType_t xTicksToWait);
//向队首插入一个元素

它们的第一个参数是队列句柄,第二个参数是指向要插入的数据的指针(该数据的大小必须和创建队列时设置的元素大小一样),第三个参数是阻塞超时时间。

如果队列未满。则插入数据并返回pdPASS;如果队列已满则阻塞,若在阻塞超时时间之前队列变成未满则插入数据并返回pdPASS,若到达阻塞超时时间则返回errQUEUE_FULL

xQueueReceive读取队首元素并将其从队列中删除,xQueuePeek读取队首元素但不删除。

#include <queue.h>
 
BaseType_t xQueueReceive( QueueHandle_t xQueue, 
                            void * const pvBuffer, 
                            TickType_t xTicksToWait);
//成功返回pdTRUE,失败返回pdFALSE
 
BaseType_t xQueuePeek( QueueHandle_t xQueue, 
                            void * const pvBuffer, 
                            TickType_t xTicksToWait);
//成功返回pdTRUE,失败返回pdFALSE

他们的第一个参数是队列句柄,第二个参数返回读取到的元素,第三个参数是阻塞超时时间。队列为空时阻塞等待数据,超时则返回pdFASLE

uxQueueMessagesWaiting可以查询队列中当前有多少个元素。

#include<queue.h>
UBaseType_t uxQueueMessagesWaiting(const QueueHandle_t xQueue);
//返回队列中元素的个数

下面是一个使用队列进行通信的例子:

#include <stm32f4xx.h>
#include <FreeRTOS.h>
#include <task.h>
#include <queue.h>
#include <uart.h>
 
void task1(void* args);
void task2(void* args);
 
QueueHandle_t queue;
 
int main()
{
    //配置USART1
    USART1_Config();
    //创建任务
    TaskHandle_t h1,h2;
    //创建队列
    queue = xQueueCreate(10,128);
    xTaskCreate(task1,"task1",configMINIMAL_STACK_SIZE,NULL,1,&h1);
    xTaskCreate(task2,"task2",configMINIMAL_STACK_SIZE,NULL,2,&h2);
    //开启任务调度
    vTaskStartScheduler();
    while(1);
}
 
 
void task1(void* args)
{
    char str[128] = "FreeRTOS queue demo.\n";
    //向队列发送数据
    xQueueSendToBack(queue,str,100/portTICK_RATE_MS);
    vTaskDelete(NULL);
}
 
void task2(void* args)
{
    char str[128];
    //从队列中读取数据
    xQueueReceive(queue,str,100/portTICK_RATE_MS);
    USART_printf(USART1,str);
    vTaskDelete(NULL);
}

Image