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 临界区
在并行编程中,并行存取共享的资源时,常常会导致意外和错误的结果。例如下面代码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);
}
类似的,当修改公共变量时任务调度可能导致变量的值错误、操作时序严格的设备时任务调度可能导致设备不能正常工作。
因此,访问共享资源的部分代码会被保护起来,在执行这段代码时不进行任务调度,这样的代码段称为临界区(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;
}
临界区嵌套是安全的,因为内核有维护一个嵌套深度计数。临界区只会在嵌套深度为0
时才会真正退出——即在为每个之前调用的taskENTER_CRITICAL()
都配套调用了taskEXIT_CRITICAL()
之后。
临界区是互斥功能的一种非常原始的实现方式,通常只是关闭所有中断从而使任务调度暂停。这样可能会导致任务不能正常执行,中断不能及时响应等问题。所以临界区应当只具有很短的时间。上例中打印串口消息是一个相当耗费时间的过程,并不适合使用临界区。
另外也可以将调度器挂起来保护临界区,vTaskSuspendAll
挂起调度器、xTaskResumeAll
恢复调度器,在vTaskSuspendAll
和xTaskResumeAll
之间不会进行任务调度,但可以响应中断。
#include <task.h>
void vTaskSuspendAll(void);
BaseType_t xTaskResumeAll(void);
FreeRTOS 事件标志组
FreeRTOS可以使用事件标志组进行任务同步,一个事件标准组包含多个事件标志位,每一位标志一个事件。当configUSE_16BIT_TICKS
为1时,一个事件标志组有8位;当这个宏为0时,一个事件标志组有24位。
创建事件标志组使用xEventGroupCreate
。
#include <event_groups.h>
EventGroupHandle_t xEventGroupCreate(void);
//返回事件标志组句柄,失败返回NULL
置位和清除事件标志位使用xEventGroupSetBits
和xEventGroupClearBits
。参数是事件标志组句柄和要操作的位。
#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 任务的创建与删除
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_LENusStackDepth
是任务的堆栈深度,其四倍即是堆栈的大小(字节数)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++;
}
}
附.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 任务的状态
在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++;
}
}
对于优先级不同的任务,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++;
}
}
FreeRTOS中任务存在4种状态:运行
、就绪
、阻塞
和挂起
。调度器总是让运行和就绪状态的任务中,优先级最高的任务进入运行状态。
状态 | 说明 |
---|---|
运行状态(Running) | 正在运行的任务处于运行状态 |
就绪状态(Ready) | 可以运行但没有运行的任务处于就绪状态 |
阻塞状态(Blocked) | 等待某一事件而不能运行的任务处于阻塞状态 |
挂起状态(Suspended) | 调用vTaskSuspend挂起的任务处于挂起状态 |
运行状态的任务可以通过调用阻塞函数进入阻塞状态,阻塞解除的事件可以让阻塞状态的任务进入就绪状态,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++;
}
}
挂起状态的任务对于调度器而言是不可见的。vTaskSuspend
让任务进入挂起状态,vTaskResume
让挂起的任务进入就绪状态,它们的参数都是要操作任务的句柄。
#include <task.h>
void vTaskSuspend(TaskHandle_t xTaskToSuspend);
void vTaskResume(TaskHandle_t xTaskToResume);
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++;
}
}
不过这种方式可能导致优先级反转:
(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
来释放该资源的互斥量。但这个函数中可能也使用了xSemaphoreTake
和xSemaphoreGive
来占用和释放资源。这种情况下就会导致错误。
递归互斥量可以让同一个任务多次占用资源,在释放同等次数互斥量后释放资源。
#include <semphr.h>
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(void);
//创建一个递归互斥量,成功返回信号量句柄,失败返回NULL
FreeRTOS 内存管理
FreeRTOS的内存管理API定义在heep_1.c
、heep_2.c
、heep_3.c
、heep_4.c
和heep_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 协同程序
协同程序是一种特殊的多任务编程方式,多个协同程序之间共用调用栈,且正在运行的协同程序不会被其他协程抢占(可以被任务和中断抢占),正在运行的协同程序只能自己主动让出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();
}
FreeRTOS 搭建开发环境
本文使用Keil MDK和STM32为例
首先需要下载FreeRTOS的源码:http://www.freertos.org/。当前最新版本为9.0.0,其源码目录如下图 :
在FreeRTOS/Demo
目录下已经有许多工程可以直接使用,例如FreeRTOS\Demo\CORTEX_STM32F103_Keil
就是STM32F103在Keil下的工程。不过这个工程是针对小容量STM32F103的,对于其他型号的STM32芯片,需要自己组织工程。
首先创建一个文件夹用于存放工程文件,命名为“Project
”。
FreeRTOS/Source
目录下是FreeRTOS的源码,将其中除了portable
文件夹以外所有的文件复制到Project文件夹中,这些是平台无关的代码。
FreeRTOS/Source/portable
目录下是平台有关的可移植代码。
其中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文件夹中。
接下来,由于我们的目标平台是Keil
,因此代开FreeRTOS/Source/protable/Keil
文件夹看到如下文件。
因此我们打开FreeRTOS/Source/protable/RVDS
文件夹,根据芯片型号选择。我的目标芯片STM32F429IGT6
是Cortex-M4F
内核,因此将ARM_CM4F
中的port.c
复制到Project
中,portmacro.h
复制到Project/include
中。
然后,我们还需要一个FreeRTOSConfig.h
,在FreeRTOS/Demo/CORTEX_M4F_STM32F407ZG-SK
中复制它到Project/include
中。
最后得到Project以及include下的文件:
接下来将这些文件复制到一个Keil的STM32标准外设库工程中,将.c文件加入工程,将include文件夹加入头文件搜索路径。
接下来需要修改部分代码:
FreeRTOS
使用了一些中断功能,因此port.c
中定义了部分中断函数,STM32标准外设库中的stm32f4xx_it.c
中也定义了这些中断函数,因此需要删除stm32f4xx_it.c
中的这些函数。它们是SVC_Handler
、PendSV_Handler
和SysTick_Handler
。
port.c
中使用了变量SystemCoreClock
,这个变量定义在标准外设库的system_stm32f4xx.c
中,需要在port.c
中声明。
FreeRTOS
中使用了一些hook
函数,这些函数需要我们自己编写(可以写成空的函数),也可以将FreeRTOSConfig.h
中的configUSE_IDLE_HOOK
、configUSE_TICK_HOOK
、configUSE_MALLOC_FAILED_HOOK
和configCHECK_FOR_STACK_OVERFLOW
设为0
从而不使用这些hook函数。
如此一来工程就配置完成了。
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);
}