目录

FreeRTOS 实时操作系统应用笔记

前言

操作系统一般分为两种:实时操作系统 RTOS(Real Time Operating System)和 分时操作系统 TSOS(Time Sharing Operating System)。对于工控方面的编程,不是“裸跑”就是使用 RTOS 编程。本文是 FreeRTOS 操作系统 API 快速应用手册,也就是我们程序员Ctrl+CCtrl+V的编程过程。本文主要介绍动态 API 接口函数,静态 API 接口函数以后再补上。

FreeRTOS 简介

点击展开内容

FreeRTOS 是 RTOS 系统的一种,由 Richard Barry 于 2003 年推出,其十分的小巧,可以在资源有限的微控制器中运行(当然,FreeRTOS 不仅局限于在微控制器中使用)。从文件数量上来看 FreeRTOS 要比 uC/OSII 和 uC/OSIII 小的多。FreeRTOS 操作系统是完全免费(MIT 开源许可证)的操作系统,具有源码公开、可移植、可裁减、调度策略灵活的特点,可以方便地移植到各种单片机上运行。FreeRTOS 的作者 Richard 于 2017 年加入了亚马逊(Amazon),并将 FreeRTOS 从 V9 版本升级至 V10 版本。同时,也推出了Amazon FreeRTOS,它基于 FreeRTOS 内核,并且增加了重要的 AWS 支持和 IoT 使用案例支持。例如,可轻松安全地将设备连接到云以及本地网络的软件库,这些库已经开源。所以,现在 FreeRTOS 将由亚马逊管理。
FreeRTOS 特点:

  • FreeRTOS 的内核支持抢占式,合作式和时间片调度。
  • 提供了一个用于低功耗的 Tickless 模式。
  • 系统的组件在创建时可以选择动态或者静态的 RAM,比如任务、消息队列、信号量、软件定时器等等。
  • FreeRTOS-MPU 支持 Corex-M 系列中的 MPU 单元,如 STM32F429。
  • FreeRTOS 系统简单、小巧、易用,通常情况下内核占用 4k-9k 字节的空间。
  • 高可移植性,代码主要 C 语言编写。
  • 高效的软件定时器。
  • 强大的跟踪执行功能。
  • 堆栈溢出检测功能。
  • 任务数量不限。
  • 任务优先级不限。

官方 API 手册
FreeRTOS 官网
FreeRTOS 仓库
Amazon FreeRTOS 官网
Amazon FreeRTOS 仓库
FreeRTOS 基础篇
FreeRTOS 高级篇

基本功能配置

点击展开内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//主要功能头文件
#include "FreeRTOSConfig.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
#include "event_groups.h"

//基本功能配置
FreeRTOSConfig.h

网文:学 RTOS 从配置文件开始

临界中断开关

点击展开内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//配置 (FreeRTOSConfig.h)
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 191        //有一个临界区中断优先级(数值越小优先级越低)

//线程中调用
taskENTER_CRITICAL(); //>>>>>>>>>>>
taskEXIT_CRITICAL();  //<<<<<<<<<<<

//中断中调用
UBaseType_t uxSavedInterruptStatus;
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR(); //>>>>>>>>>>>
taskEXIT_CRITICAL_FROM_ISR(status_value);               //<<<<<<<<<<<

网文:FreeRTOS 系统内核控制函数与临界段保护

调度器上解锁

点击展开内容
1
2
3
//使用
vTaskSuspendAll(); //>>>>>>>>>>>
xTaskResumeAll();  //<<<<<<<<<<<

系统延时函数

点击展开内容
1
2
3
4
5
6
7
8
//配置 (FreeRTOSConfig.h)
#define configUSE_16_BIT_TICKS    0                  //32位类型节拍计数器
#define configTICK_RATE_HZ        ((TickType_t)100)  //系统节拍频率(10毫秒一个节拍)

//使用
vTaskDelay(50);                                      //相对节拍延时(调用那一刻开始计时。)
TickType_t tick = xTaskGetTickCount();               //获取节拍计数(获取那一刻开始计时,)
vTaskDelayUntil(&tick, 50);                          //绝对节拍延时(之后以tick增量计时。)

线程基本操作

点击展开内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//配置
#define NETWORK_THREAD_STK_SIZE   512                //线程堆栈大小
#define NETWORK_THREAD_PRIO       2                  //线程优先级(数值越小优先级越低)

//创建 (动态线程)
TaskHandle_t *const p_network;                       //动态线程句柄
xTaskCreate(network_thread_entry,                    //动态线程函数
            "network",                               //动态线程名称
            NETWORK_THREAD_STK_SIZE,                 //动态线程堆栈大小
            NULL,                                    //动态线程入口
            NETWORK_THREAD_PRIO,                     //动态线程优先级
            p_network);                              //动态线程句柄(当填 NULL 表示不需要获取)
//使用
UBaseType_t pri = uxPriorit = uxTaskPriorityGet();   //获取当前线程优先级
vTaskPrioritySet(NULL, 1);                           //修改当前线程优先级(如果指出线程句柄,则表示指定线程)
taskYIELD();                                         //主动让出cpu让同优先级的其他task获得cpu
vTaskDelete(p_network);                              //删除线程(自杀最后还要 return 退出)

//线程
void vAppTask(void *pvParameters)
{
	for(;;)
	{
		vTaskDelay(50);
	}
}

网文:FreeRTOS 修改任务优先级

任务通知操作

点击展开内容

相比于消息队列、信号量、事件组的应用,使用任务通知速度更快、内存更少、执行更高效。任务通知只能由发送方指定线程,与线程耦合在一起。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//eSetBits:事件标志, 
//eIncrement:计数信号量, 
//eSetValueWithOverwrite:直接写覆盖消息值, 
//eSetValueWithoutOverwrite:不写覆盖消息值(写失败返回pdFAIL), 
//eNoAction:通知线程但不更新通知值。
BaseType_t true /*= pdTRUE*/;
uint32_t   u32d;

//二值信号量形式通知
true = xTaskNotifyGive(p_network/*线程*/);                          //以【二值信号量形式】通知任务
true = xTaskNotify(p_network/*线程*/, 0/*无效*/, eIncrement);       //以【二值信号量形式】通知任务(等同 xTaskNotifyGive())
true = xTaskNotifyAndQuery(p_network, 0/*无效*/, eIncrement, &u32d);//以【二值信号量形式】通知任务(相比 xTaskNotify() 多了个回传更新前的通知值)
u32d = ulTaskNotifyTake(pdTRUE/*读后清零通知值*/, portMAX_DELAY);   //以【二值信号量形式】获取通知(最终的通知值:清零后的值)

//计数信号量形式通知
true = xTaskNotifyGive(p_network/*线程*/);                          //以【计数信号量形式】通知任务
true = xTaskNotify(p_network/*线程*/, 0/*无效*/, eIncrement);       //以【计数信号量形式】通知任务(等同 xTaskNotifyGive())
u32d = ulTaskNotifyTake(pdFALSE/*计数信号量减1*/, portMAX_DELAY);   //以【计数信号量形式】获取通知(最终的通知值:减一后的值)

//事件标志形式通知
true = xTaskNotify(p_network/*线程*/, 0x00000001, eSetBits);        //以【事件标志形式】通知任务
true = xTaskNotifyAndQuery(p_network, 0x00000001, eSetBits, &u32d); //以【事件标志形式】通知任务(相比 xTaskNotify() 多了个回传更新前的通知值)
true = xTaskNotifyWait(0xFFFFFFFE/*获取前清bit31~1标志*/, 0x00000001/*获取后清bit0标志*/, &u32d/*获取前后两个清除操作中间的值*/, portMAX_DELAY);
                                                                    //以【事件标志形式】获取通知
//在中断里通知任务
BaseType_t xHigherPriorityTaskWoken = pdFALSE;                      //在中断里以【计数信号量形式】通知任务(中断->线程)
vTaskNotifyGiveFromISR(p_network/*线程*/, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);                       //需要调用本函数执行一次上下文切换

BaseType_t xHigherPriorityTaskWoken = pdFALSE;                      //在中断里以【事件标志形式】通知任务(中断->线程)
true = xTaskNotifyFromISR(p_network/*线程*/, 0x00000001, eSetBits/*事件标志*/, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);                       //如果 true == pdTRUE, 则要调用本函数执行一次上下文切换

BaseType_t xHigherPriorityTaskWoken = pdFALSE;                      //在中断里以【事件标志形式】通知任务(中断->线程)
true = xTaskNotifyAndQueryFromISR(p_network/*线程*/, 0x00000001, eSetBits/*事件标志*/, &u32d, &xHigherPriorityTaskWoken); //(相比 xTaskNotifyFromISR() 多了个回传更新前的通知值)
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);                       //如果 true == pdTRUE, 则要调用本函数执行一次上下文切换

网文:FreeRTOS 任务通知

信号量操作

点击展开内容

关于二值信号量与计数信号量无本质区别,当计数信号量最大计数值为 1 及初始值为 0 时,即是二值信号量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//配置 (FreeRTOSConfig.h)
#define configSUPPORT_DYNAMIC_ALLOCATION  1          //开启信号量
#define INCLUDE_vTaskSuspend     1                   //如果需要永远等待->portMAX_DELAY

//建删
SemaphoreHandle_t Sem_Handle = NULL;                 //信号量句柄
Sem_Handle = xSemaphoreCreateBinary();               //动态【二值信号量】创建
Sem_Handle = xSemaphoreCreateCounting(0xFFFF, 0);    //动态【计数信号量】创建(入口:最大值,初始值)
vSemaphoreDelete(Sem_Handle);                        //信号量删除(如果有任务阻塞在这个信号量上,则这个信号量不要删除!)

//使用
BaseType_t true /*= pdTRUE*/;
true = xSemaphoreGive(Sem_Handle);                   //信号量释放    (线程与线程)
true = xSemaphoreTake(Sem_Handle, portMAX_DELAY);    //信号量获取    (线程与线程)(非死等必要判断返回值:pdTRUE)

//在中断里使用
BaseType_t xHigherPriorityTaskWoken = pdFALSE;       //在中断里信号量释放(中断->中断)或(中断->线程)
true = xSemaphoreGiveFromISR(Sem_Handle, &xHigherPriorityTaskWoken); 
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);        //如果 true == pdTRUE, 则要调用本函数执行一次上下文切换

BaseType_t true /*= pdTRUE*/;
BaseType_t xHigherPriorityTaskWoken;                 //在中断里信号量获取(中断->中断)
true = xSemaphoreTakeFromISR(Sem_Handle, &xHigherPriorityTaskWoken);

网文:FreeRTOS 信号量分析

互斥量操作

点击展开内容

关于可递归锁也可称为可重入锁,与非递归锁唯一的区别是:同一个线程可以多次获取同一个递归锁,不会产生死锁。而如果一个线程多次获取同一个非递归锁,则会产生死锁。还有需要注意:中断是不能使用互斥锁的!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//配置 (FreeRTOSConfig.h)
#define configUSE_MUTEXES           1                 //开启互斥量
#define configUSE_RECURSIVE_MUTEXES 1                 //开启递归互斥量
#define INCLUDE_vTaskSuspend        1                 //如果需要永远等待->portMAX_DELAY

//建删
SemaphoreHandle_t MuxSem_Handle = NULL;               //信号量句柄
MuxSem_Handle = xSemaphoreCreateMutex();              //动态【互斥量】创建
MuxSem_Handle = xSemaphoreCreateRecursiveMutex();     //动态【递归互斥量】创建
vSemaphoreDelete(MuxSem_Handle);                      //信号量删除(如果有任务阻塞在这个信号量上,则这个信号量不要删除!)

//使用
BaseType_t true /*= pdTRUE*/;
true = xSemaphoreGive(MuxSem_Handle);                        //【互斥量】释放
true = xSemaphoreTake(MuxSem_Handle, portMAX_DELAY);         //【互斥量】获取(portMAX_DELAY 表示永远等待)(非死等必要判断返回值:pdTRUE)

true = xSemaphoreGiveRecursive(MuxSem_Handle);               //【递归互斥量】释放
true = xSemaphoreTakeRecursive(MuxSem_Handle, portMAX_DELAY);//【递归互斥量】获取(portMAX_DELAY 表示永远等待)(非死等必要判断返回值:pdTRUE)

网文:可递归锁与非递归锁

事件标志操作

点击展开内容

获取事件标志一般有两个分类操作:读后是否清除指定要读取的标志、当指定的标志全部触发时才读取。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//建删
EventGroupHandle_t xCreatedEventGroup = NULL;                      //动态事件句柄
xCreatedEventGroup = xEventGroupCreate();                          //动态事件创建(返回 NULL 表示事件标志组创建失败)
vEventGroupDelete(xCreatedEventGroup);                             //动态事件删除

//使用
EventBits_t bits;
bits = xEventGroupSetBits(xCreatedEventGroup, 0x00000011);         //事件标志设置(事件位置位之后的事件组值)(事件标志释放)
bits = xEventGroupGetBits(xCreatedEventGroup);                     //事件标志获取(但不清除标志)(等同 bits = xEventGroupClearBits(xCreatedEventGroup, 0))
bits = xEventGroupClearBits(xCreatedEventGroup, 0x00000011);       //事件标志清除(事件位清零之前的事件组值)
bits = xEventGroupWaitBits(xCreatedEventGroup, 0x00000011, pdTRUE/*读后清除*/, pdTRUE /*当指定的条件全部成立时才触发*/, 10/*最长等待节拍数*/);     //事件标志获取(返回值可以判断是否得到想要的标志位)
bits = xEventGroupWaitBits(xCreatedEventGroup, 0x00000011, pdTRUE/*读后清除*/, pdFALSE/*当指定的条件中有成立都可触发*/, portMAX_DELAY/*永远等待*/);//事件标志获取(返回值可以判断是否得到想要的标志位)

//在中断里使用
BaseType_t true /*= pdTRUE*/;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;                     //在中断里事件标志释放(中断->线程)
true = xEventGroupSetBitsFromISR(xCreatedEventGroup, 0x00000011, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);                      //如果 true == pdTRUE, 则要调用本函数执行一次上下文切换

EventBits_t bits;
BaseType_t true /*= pdTRUE*/;
bits = xEventGroupGetBitsFromISR(xCreatedEventGroup);              //事件标志获取(但不清除标志)
true = xEventGroupClearBitsFromISR(xCreatedEventGroup, 0x00000011);//事件标志清除

网文:FreeRTOS 事件标志组

消息队列操作

点击展开内容

投递消息一般分为两种:向队列尾部投递、向队列头部投递。FreeRTOS 的消息每次获取都是固定长度的信息(其获取函数是没有返回信息长度参数),所以如果用于传递变长的数据帧,则由应用自行处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//建删
QueueHandle_t xQueueHandle = NULL;
xQueueHandle = xQueueCreate(20/*消息条数*/, 4/*单条大小*/); //创建消息   (单条大小:字节)
vQueueDelete(xQueueHandle);                                 //删除消息
vQueueAddToRegistry(xQueueHandle, "msg name");              //注册消息名称(仅仅用于调试)
vQueueUnregisterQueue(xQueueHandle);                        //解除注册消息(仅仅用于调试)

//使用
char msg[] = {1, 2, 3};
BaseType_t true /*= pdTRUE*/;
true = xQueueReset(xQueueHandle);
true = xQueueOverwrite(xQueueHandle, msg);                  //覆盖消息(仅仅适合只有一条消息的队列)
true = xQueueSend(xQueueHandle, msg, portMAX_DELAY);        //投递消息(等同 xQueueSendToBack())
true = xQueueSendToBack(xQueueHandle, msg, portMAX_DELAY);  //【向队列尾部】投递消息
true = xQueueSendToFront(xQueueHandle, msg, portMAX_DELAY); //【向队列头部】投递消息
true = xQueueReceive(xQueueHandle, msg, portMAX_DELAY);     //获取消息(返回 pdTRUE 表示获取到消息)
true = xQueuePeek(xQueueHandle, msg, portMAX_DELAY);        //查看消息(相比 xQueueReceive() 不会清除队列中读出的消息)
UBaseType_t n = uxQueueMessagesWaiting(xQueueHandle);       //查看入列信息数目(单位:条数)
UBaseType_t n = uxQueueSpacesAvailable(xQueueHandle);       //查看队列空闲数目(单位:条数)

//在中断里使用
char msg[] = {1, 2, 3};
BaseType_t xHigherPriorityTaskWoken = pdFALSE;              //在中断里覆盖消息(中断->中断)或(中断->线程)(仅仅适合只有一条消息的队列)
true = xQueueOverwriteFromISR(xQueueHandle, msg, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);               //如果 true == pdTRUE, 则要调用本函数执行一次上下文切换

char msg[] = {1, 2, 3};
BaseType_t xHigherPriorityTaskWoken = pdFALSE;              //在中断里投递消息(中断->中断)或(中断->线程)
true = xQueueSendFromISR(xQueueHandle, msg, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);               //如果 true == pdTRUE, 则要调用本函数执行一次上下文切换

char msg[] = {1, 2, 3};
BaseType_t xHigherPriorityTaskWoken = pdFALSE;              //在中断里【向队列尾部】投递消息(中断->中断)或(中断->线程)
true = xQueueSendToBackFromISR(xQueueHandle, msg, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);               //如果 true == pdTRUE, 则要调用本函数执行一次上下文切换

char msg[] = {1, 2, 3};
BaseType_t xHigherPriorityTaskWoken = pdFALSE;              //在中断里【向队列头部】投递消息(中断->中断)或(中断->线程)
true = xQueueSendToFrontFromISR(xQueueHandle, msg, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);               //如果 true == pdTRUE, 则要调用本函数执行一次上下文切换

char msg[4];
BaseType_t xHigherPriorityTaskWoken = pdFALSE;              //在中断里获取消息
true = xQueueReceiveFromISR(xQueueHandle, msg, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);               //如果 true == pdTRUE, 则要调用本函数执行一次上下文切换

char msg[4];
BaseType_t xHigherPriorityTaskWoken = pdFALSE;              //在中断里查看消息
true = xQueuePeekFromISR(xQueueHandle, msg, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);               //如果 true == pdTRUE, 则要调用本函数执行一次上下文切换

true = xQueueIsQueueEmptyFromISR(xQueueHandle);             //查看消息队列是否为空(返回 pdTRUE 表示为空)
true = xQueueIsQueueFullFromISR(xQueueHandle);              //查看消息队列是否为满(返回 pdTRUE 表示为满)
UBaseType_t n = uxQueueMessagesWaitingFromISR(xQueueHandle);//查看入列信息数目(单位:条数)

动态内存操作

点击展开内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//配置 (FreeRTOSConfig.h)
#define configTOTAL_HEAP_SIZE        ( ( size_t ) ( 17 * 1024 ) )  //动态内存大小定义(单位:字节)

//使用
size_t size = xPortGetFreeHeapSize();                //获得FreeRTOS动态内存的剩余
void *pd = pvPortMalloc(1024);                       //申请动态内存
vPortFree(pd);                                       //释放动态内存
// 五种动态内存管理方式简单总结如下,实际项目中,用户根据需要选择合适的文件:
//(1)heap_1.c:五种方式里面最简单的,但是申请的内存不允许释放。
//(2)heap_2.c:支持动态内存的申请和释放,但是不支持内存碎片的处理,并将其合并成一个大的内存块。
//(3)heap_3.c:将编译器自带的malloc和free函数进行简单的封装,以支持线程安全,即支持多任务调用。
//(4)heap_4.c:支持动态内存的申请和释放,支持内存碎片处理,支持将动态内存设置在个固定的地址。
//(5)heap_5.c:在heap_4的基础上支持将动态内存设置在不连续的区域上。

网文:FreeRTOS 动态内存管理
网文:FreeRTOS 动态内存管理《heap_1.c》详解