CubeMX使用FreeRTOS编程指南

Posted JeckXu666

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CubeMX使用FreeRTOS编程指南相关的知识,希望对你有一定的参考价值。

CubeMX使用FreeRTOS编程指南

一、开发前言

1.1 软件准备

  • STM32CubeMX 代码生成软件

  • MDK 集成代码开发环境

1.2 开启FreeRTOS

新建一个 CubeMX 工程,在配置好时钟后,点击 Middleware -> 选择 FreeRTOS -> 下拉框选择 V2 版本 CMSIS

到此在 CubeMX 中就已经开启 FreeRTOS 系统了,下面分享 FreeRTOS 的配置:

二、配置界面

开启 FreeRTOS 之后,可以看到配置项主要分为以下几个部分

这几个部分的主要功能如下表:

配置项功能
Tasks and Queues任务与队列,用于配置任务体以及消息队列;
Timers and Semaphores软件定时器与信号量,用于配置内核对象 (软件定时器和信号量);
Mutexes互斥量,用于配置内核对象(互斥量)
Events事件,配置内核对象(事件)
FreeRTOS Heap Usage查看用户任务和系统任务的堆占用
Config Parameters系统的参数配置
Include Parameters系统的功能裁剪
Advanced SettingsCubeMX 生成代码预配置项
User Constants用户常量定义

以上各个功能分的很清晰,我们需要配置什么功能就去对应的选项下进行配置,下面根据各个配置项进行详细配置介绍

三、系统设置

首先我们先了解一下 Config Parameters,他的配置参数如下

参数功能表:

参数功能
API显示 FreeRTOS API 接口版本
Version显示 FreeRTOS 内核版本
显示 CMSIS 版本
Kernel SettingFreeRTOS 调度内核设置
Memory management setting内存管理设置
Hook function related definitions钩子函数有关定义
Run time and task stats gathering related definitions系统运行时的参数收集配置
Co-routine related definitions协程配置
Software timer definitons软件定时器任务配置
Interrupt nesting behaviour configuration中断优先级配置

API 和 Version 不过多解释,显示版本信息

2.1 调度内核设置

Kernel Setting 是 FreeRTOS 的调度内核配置,展开后有下面的配置项,使用时一般保持默认,也可以根据需要修改

  • USE_PREEMPTION

USE_PREEMPTION 是 RTOS 的调度方式选择,为 1 时使用抢占式调度器,为 0 时使用协程,如果使用抢占式调度器的话内核会在每个时钟节拍中断中进行任务切换,当使用协程的话会在如下地方进行任务切换

  1. 一个任务调用了函数 taskYIELD()。
  2. 一个任务调用了可以使任务进入阻塞态的 API 函数。
  3. 应用程序明确定义了在中断中执行上下文切换。
  • CPU_CLOCK_HZ

CPU_CLOCK_HZ 是 CPU 系统时钟频率,默认使用的是晶振通过时钟树后获得的时钟频率

  • TICK_RATE_HZ

TICK_RATE_HZ 是 RTOS 的心跳时钟频率,默认为最大值 1000 ,即心跳时钟 1ms 跳动一次

  • MAX_PRIORITIES

MAX_PRIORITIES 是 RTOS 任务的最高优先级设置,默认56级,一般来说一个优先级表是32位,这里用了两个,对应64位,其中8位用于系统任务的优先级处理

  • MINIMAL_STACK_SIZE

MINIMAL_STACK_SIZE 设置分配给空闲任务的堆栈大小,该值是用字(32位)指定的,而不是字节,默认为128个字,如果修改过空闲任务,则根据实际情况修改

  • MAX_TASK_NAME_LEN

MAX_TASK_NAME_LEN 设置任务名称的最大字符数,默认16位足够

  • USE_16_BIT_TICKS

USE_16_BIT_TICKS 存放 Tick 周期的计数器的数字位宽,默认为 Disable 即 16 位

  • IDLE_SHOULD_YIELD

如果IDLE_SHOULD_YIELD 设置为0,则空闲任务永远不会让位于另一个任务,只在被抢占时才会离开运行状态。如果 IDLE_SHOULD_YIELD 设置为1,那么当有另一个空闲优先级任务处于Ready状态时,空闲任务将不会执行它定义的功能的不止一次迭代,而不会让位于另一个任务,这确保当应用程序任务处于空闲状态时,在空闲任务中花费的时间最少,即同在空闲优先级下,空闲任务优先级更高,不会被抢占,不会以时间片运行

  • USE_MUTEXES、USE_RECURSIVE_MUTEXES、USE_COUNTING_SEMAPHORES

为 1 则开启系统构建过程中的互斥量、递归互斥量和信号量,该值强制为1(ENABLE)

  • QUEUE_REGISTRY_SIZE

队列注册表的大小,可以用于管理队列名称和队列实体,方便运行中进行查看与管理,默认为8

  • USE_APPLICATION_TASK_TAG

使能时会给任务一个 TAG 标签,便于用户进行使用

  • ENABLE_BACKWARD_COMPATIBILITY

一个兼容性使能,使能后, FreeRTOS 8.0.0 之后的版本可以通过宏定义使用 8.0.0 版本之前的函数接口,默认使能

  • USE_PORT_OPTIMISED_TASK_SELECTION

查找下一个任务方式的选择,查找下一个就绪任务就是查找优先级表,对优先级表进行导0算法分为通用切换或者针对性切换,一般默认不使能,使用通用切换,通用切换使用C编写,执行效率低,兼容性高;针对性切换使用处理器自带的导0指令,使用汇编编写,切换效率高,但兼容性差

  • USE_TICKLESS_IDLE

使能后会生成的两个空函数PreSleepProcessing和PostSleepProcessing,用户可以编写代码进入低功耗模式,生成函数如下图

  • USE_TASK_NOTIFICATIONS

任务通知使能,每个RTOS任务都有一个32位的通知值,RTOS任务通知是一个直接发送给任务的事件,它可以解除接收任务的阻塞,并可选地更新接收任务的通知值,为1开启,为0关闭,关闭可以为每个任务节省8个字节的内存空间

  • RECORD_STACK_HIGH_ADDRESS

记录任务的堆栈入口地址到TCB,为1使能,为0关闭

2.2 内存管理设置

内存管理可以看到3个配置参数

  • Memory Allocation

内存分配方式,此处默认动态和静态都可以

  • TOTAL_HEAP_SIZE

内存堆的分配大小,堆本质上就是一个数组,此处是设置堆数组的大小,设置时要考虑最小要满足所有任务的使用要求,最大不要超过系统的分配上限

  • Memory Management scheme

内存分配方式,有heap_1.c, heap_2.c, heap_3.c, heap_4.c and heap5.c 5种,其中1、2、4、5都是先建立一个堆数组,从数组中申请,用完再释放,与C语言中molloc和free使用链表的方式不同,该方式在 MCU 中更安全稳定,此处默认使用的方式4,具体申请释放方式可以在heap4.c中阅读到

关于堆和栈的区别,可以阅读我的另外一篇文章进行了解:C语言:内存四区

2.3 钩子函数配置

钩子函数是一种回调函数,用于在任务执行一次之后或者某些事件发生后执行的函数,该配置项里面有五个选项,控制5种不同功能的钩子函数开启,当然用户也可以在代码中自己定义

  • USE_IDLE_HOOK

使能后,系统生成一个空回调函数,由用户编写函数主体

void vApplicationIdleHook(void)

每当空闲任务执行一次,钩子函数都会被执行一次

  • USE_TICK_HOOK

使能后,系统生成一个空回调函数,由用户编写函数主体

void vApplicationTickHook(void)

每个TICK周期,钩子函数都会执行一次

  • USE_MALLOC_FAILED_HOOK

使能后,系统生成一个空回调函数,由用户编写函数主体

void vApplicationMallocFailedHook(void)

当申请动态内存失败时,钩子函数会执行一次

  • USE_DAEMON_TASK_STARTUP_HOOK

使能后,系统生成一个空回调函数,由用户编写函数主体

void vApplicationDaemonTaskStartupHook(void).

任务刚启动时,钩子函数会执行一次

  • CHECK_FOR_STACK_OVERFLOW

使能后,系统生成一个空回调函数,由用户编写函数主体

void vApplicationStackOverflowHook( xTaskHandle xTask, signed char *pcTaskName );

任务栈溢出时,钩子函数会执行一次,传入任务 TCB 和任务名称

当我们在 CubeMX 里面开启对应钩子函数,生成代码之后,在FreeRTOS就可以看到自动生成的钩子函数,我们在里面编写相应的功能就行

2.5 任务运行追踪配置

功能配置项如下:

  • GENERATE_RUN_TIME_STATS

开启时间统计功能,在调用 vTaskGetRunTimeStats() 函数时,将任务运行时间信息保存到可读列表中

  • USE_TRACE_FACILITY

使能后会包含额外的结构成员和函数以帮助执行可视化和跟踪,默认开启,方便 MDK 软件工具调试使用

  • USE_STATS_FORMATTING_FUNCTIONS

使能后会生成 vTaskList() 和 vTaskGetRunTimeStats() 函数用于获取任务运行状态

2.6 协程配置

Co-routine related definitions 是协程的配置项,两个选项用来配置协程是否开启,以及协程的优先级,开启后,需要用户手动创建协程,在协程几乎很少用到了,是 FreeRTOS目前还没有把协程移除的计划,但 FreeRTOS是不会再更新和维护协程了,因此大家解一下就行

协程特点:

  1. 堆栈使用
    所有的协程使用同一个堆栈(如果是任务的话每个任务都有自己的堆栈),这样就比使用任务消耗更少的 RAM
  2. 调度器和优先级
    协程使用合作式的调度器,但是可以在使用抢占式的调度器中使用协程
  3. 宏实现
    协程是通过宏定义来实现的
  4. 使用限制
    为了降低对 RAM 的消耗做了很多的限制

具体 API 接口和调度原理可以参考这篇文章 : FreeRTOS协程

2.7 软件定时器配置

软件定时器配置的一些相关项如下:

这四个配置项主要与软件定时器处理任务有关,软件定时器任务属于系统任务(守护线程),开启软件定时器后用于维护软件定时器

  • USE_TIMERS

默认开启软件定时器任务

  • TIMER_TASK_PRIORITY

软件定时器任务优先级

  • TIMER_QUEUE_LENGTH

定时器任务队列长度FreeRTOS 是通过队列来发送控制命令给定时器任务,叫做定时器命令队列,此处设置队列长度

  • TIMER_TASK_STACK_DEPTH

软件定时器任务堆栈大小

2.8 中断优先级配置

  • LIBRARY_LOWEST_INTERRUPT_PRIORITY

此宏是用来设置最低优先级,FreeRTOS 使用的4位优先级,对应16位优先级,对应的最低优先级为15

  • LIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY

设置FreeRTOS 系统可管理的最大优先级,也就是设置阈值优先级,这个大家可以自由设置,这里设置为5,也就是高于5 的优先级(优先级数小于5)不归 FreeRTOS 管理

三、内核裁剪

Include Parameters 下的选项应用于内核裁剪,裁剪不必要的功能,精简系统功能,减少资源占用,主要有以下几个选项:

配置项可裁剪的函数功能如下:

选项功能
vTaskPrioritySet改变某个任务的任务优先级。
uxTaskPriorityGet查询某个任务的优先级。
vTaskDelete删除任务
vTaskCleanUpResources回收任务删除后的资源如RAM等等
vTaskSuspend挂起任务
vTaskDelayUntil阻塞延时一段绝对时间(绝对延时去去除程序执行时间,执行更精准)
vTaskDelay阻塞延时一段相对时间
xTaskGetSchedulerState获取任务调度器的状态,开启或未开启
xTaskResumeFromISR在中断服务函数中恢复一个任务的运行
xQueueGetMutexHolder获取信号量的队列拥有者,返回拥有此信号量的队列
xSemaphoreGetMutexHolder查询拥有互斥锁的任务,返回任务控制块
pcTaskGetTaskName获取任务名称
uxTaskGetStackHighWaterMark获取任务的堆栈的历史剩余最小值,FreeRTOS 中叫做“高水位线”
xTaskGetCurrentTaskHandle此函数用于获取当前任务的任务句柄,就是获取当前任务控制块
eTaskGetState此函数用于查询某个任务的运行壮态,比如:运行态、阻塞态、挂起态、就绪态等
xEventGroupSetBitFromISR在中断服务函数中将指定的事件位清零
xTimerPendFunctionCall定时器守护任务的回调函数(定时器守护任务使用到一个命令队列,只要向队列发送信号就可以执行相应代码,可以实现“中断推迟处理”功能)
xTaskAbortDelay中止延时函数,该函数能立即解除任务的阻塞状态,将任务插入就绪列表中
xTaskGetHandle此函数根据任务名字获取的任务句柄(控制块)

四、创建任务与队列

4.1 CubeMX 下任务创建与配置

任务(线程)是操作系统运行的基本单元,也是资源分配的基本单元, CubeMX 任务的创建基本以图形化进行,配置方式如下

进入Tashs and Queues 配置,点击 Add 添加新任务

任务配置参数介绍

参数功能
Task Name任务名称,保存在 TCB 结构体中,设置时自己起名字
Priority任务优先级,任务的调度等级,根据自己创建任务的紧急程度设定
比如通信任务不能被打断,可以设计较高优先级
Stack Size(Words)设定给任务分配的内存大小,单位是字,对于32位单片机来说占4个字节
Entry Function任务实体,即任务的运行函数名
Code Generation代码生成模式
As weak: 产生一个用 __weak 修饰的弱定义任务函数,用户可自己在进行定义;
As external: 产生一个外部引用的任务函数,用户需要自己定义该函数;
Default: 产生一个默认格式的任务函数,用户需要在该函数内实现自己的功能
Parameter:传入的参数,保持默认就行
Allocation:内存分配方式
Static: 静态方式是直接在RAM占据一个静态空间
Dynamic:动态方则是在初始配置的内存池大小数组中动态申请、释放空间

设置完成后点击OK,配置就完成了,之后生成代码,使用 MDK 进一步配置任务的具体信息

在生成的代码中,我们打开 freertos.c 文件可以在代码中看到任务的配置信息

在 freertos.c 文件的末尾部分,我们可以看到生成的任务实体

任务实体本身就是一个死循环函数,循环执行程序代码,但循环体代码里面必须要有延时函数,释放当前任务对 MCU 的控制权,使其他低优先级可以执行,此外,关于任务,CubeMX 提供了一系列的用户调用接口函数,具体如下

函数功能
osThreadNew创建新任务
*osThreadGetName获取任务名称
osThreadGetId获取当前任务的控制块(TCB)
osThreadGetState获取当前任务的运行状态
osThreadGetStackSize获取任务的堆栈大小
osThreadGetStackSpace获取任务剩余的堆栈大小
osThreadSetPriority设定任务优先级
osThreadGetPriority获取任务优先级
osThreadYield切换控制权给下一个任务
osThreadSuspend挂起任务
osThreadResume恢复任务(挂起多少次恢复多少次)
osThreadDetach分离任务,方便任务结束进行回收
osThreadJoin等待指定的任务停止
osThreadExit停止当前任务
osThreadTerminate停止指定任务
osThreadGetCount获取激活的任务数量
osThreadEnumerate列举激活的任务

4.2 CubeMX 下队列的创建与配置

队列,又称为消息队列,用于任务间的数据通信,传输数据,在操作系统里面,直接使用全局变量传输数据十分危险,看似正常运行,但不知道啥时候就会因为寄存器或者内存等等原因引起崩溃,所以引入消息,队列的概念,任务发送数据到队列,需要接受消息的任务挂起在队列的挂起列表,等待消息的到来,CubeMX 创建队列的步骤如下:

先点击 Add 添加队列

队列配置参数介绍

参数功能
Queue Name队列名称(自己设定)
Queue Size消息队列大小
Item Size队列传输类型,保持默认16 位就行
Allocation队列内存的分配方式
Static: 静态方式是直接在RAM占据一个静态空间
Dynamic:动态方则是在初始配置的内存池大小数组中动态申请、释放空间

配置需要的参数后,点击OK,然后生成代码

生成代码后,我们可以在 freertos.c 中系统初始话函数中看到队列的初始化

初始化函数会在一开始被调用,对 FreeRTOS 系统和内核对象进行初始化,初始化后系统就可以进行调度和使用内核对象,CubeMX 生成的代码自动将创建的内核对象放到初始化函数内,所以我们在任务和中断中直接使用就可以,队列的 FreeRTOS API 接口在CubeMX 内再次进行了封装,使用更加简单,使用方式如下:

我们使用的 CMSIS 2.0 版本,所以在任务文件中包含调用声明头文件

#include "cmsis_os2.h"

在队列头文件内我们可以在 600 多行的位置找到有关队列的 API 函数声明:

下面介绍一下队列有关接口的函数接口:

函数功能
osMessageQueueNew创建并初始化一个新的队列
osMessageQueueGetName获取队列的名字
osMessageQueuePut发送一条消息到队列
osMessageQueueGet从队列等待一条消息
osMessageQueueGetCapacity获取队列传输消息的峰值
osMessageQueueGetMsgSize获取队列使用内存池的最大峰值
osMessageQueueGetCount获取队列的消息数量
osMessageQueueGetSpace获取队列剩余的可用空槽
osMessageQueueReset清空队列
osMessageQueueDelete删除队列

以上的API接口有其对应的传入参数,具体使用方式需要在翻源码的注释,这里我选常用的来介绍一下:

消息队列常用的是插入与获取消息,初始化系统已经帮助我们完成,在初始化的时候会获取一个队列的句柄,之后对队列的操作都是围绕这个句柄展开,比如上面的代码中,句柄就是 myQueue01Handle ,我们发送一个消息到这个队列,就是调用发送函数,对句柄进行操作,先看一下发送消息的函数原型

osStatus_t osMessageQueuePut (osMessageQueueId_t mq_id, const void *msg_ptr, uint8_t msg_prio, uint32_t timeout);

参数的功能

参数功能
mq_id传入队列的句柄
*msg_ptr指向需要发送的消息内容的指针
msg_prio本次发送消息的优先级(目前API未加入功能)
timeout发送消息的超时时间(设置为0代表一直等待发送成功)
osStatus_t(返回值)返回执行结果

返回值的可能

错误含义
osOK执行正常
osError系统错误
osErrorTimeout执行超时
osErrorResource资源不可用
osErrorParameter参数无效
osErrorNoMemory内存不足
osErrorISR不允许在中断调用
osStatusReserved防止编译器优化项,不需要管他

所以我们发送一个消息到队列,函数用法如下:

void StartTask02(void *argument)
{
  /* USER CODE BEGIN StartTask02 */
	osStatus_t result;
	uint8_t dat[]="666\\r\\n";
  /* Infinite loop */
  for(;;)
  {
		result= osMessageQueuePut(&myQueue01Handle,dat,1,0);
		if(result == osOK)
		{
			//发送成功
		}else
		{
			//发送失败
		}
    osDelay(1);
  }
  /* USER CODE END StartTask02 */
}

发送消息的优先级暂时无用,CubeMX 对 FreeRTOS 的支持还不完善,发送消息里面的优先级未使用到,并且入队方式使用的是发送到队列尾部,没有从头部插入的方式,有需求可以 通过包含 queue.h 文件,调用 FreeRTOS 的官方代码,或者自己修改 生成代码的 API 接口结合优先级使用队列的向前插入和向后插入,丰富系统功能!

除了发送消息到队列,接受队列的消息 API 接口也经常用到,函数原型如下

osStatus_t osMessageQueueGet (osMessageQueueId_t mq_id, void *msg_ptr, uint8_t *msg_prio, uint32_t timeout);

参数的功能

参数功能
mq_id接受队列的句柄
*msg_ptr用于接受消息内容的指针
msg_prio存放接受消息的优先级(目前API未加入功能)
timeout接受消息的超时时间(设置为10代表,当前任务挂起在挂起列表,直到接收成功时恢复,或者10个TICK等待周期到达然后任务强行恢复,不再等待,为0则是不等待,等待期间任务挂起在内核对象的挂起队列)
osStatus_t(返回值)返回执行结果

函数用法

void StartTask02(void *argument)
{
  /* USER CODE BEGIN StartTask02 */
	osStatus_t result;
	uint8_t dat[10]={};
	uint8_t *pro;
  /* Infinite loop */
  for(;;)
  {
		result= osMessageQueueGet(&myQueue01Handle,dat,pro,10);
		if(result == osOK)
		{
			//接受成功
		}else
		{
			//接受失败
		}
    osDelay(1);
  }
  /* USER CODE END StartTask02 */
}

注意:FreeRTOS 中获取和发送消息的 API 接口函数分为任务中调用和中断中调用,CubeMX 代码接口将两者整合了,调用时自动判断调用环境是在 ISR 还是正常运行环境中

五、创建定时器和信号量

5.1 CubeMX下定时器的创建和配置

软件定时器本质上就是设置一段时间,当设置的时间到达之后就执行指定的功能函数,调用的这个函数叫做回调函数。回调函数的两次执行间隔叫做定时器的定时周期,简而言之,当定时器的定时周期到了以后就会执行回调函数,下面介绍一下 CubeMX 中开启定时器的方法:

在 CubeMX 里面按下面步骤添加定时器

然后配置具体参数,参数的功能如下:

参数功能
Timer Name设置定时器的名称
Callback设定定时器的回调函数体
Type设定定时器的执行类型
osTimerPeriodic 定时器周期执行回调函数
osTimerOnce 定时器只执行一次回调函数
Code Generation Option代码生成模式
As weak: 产生一个用 __weak 修饰的弱定义任务函数,用户可自己在进行定义;
As external: 产生一个外部引用的任务函数,用户需要自己定义该函数;
Default: 产生一个默认格式的任务函数,用户需要在该函数内实现自己的功能
Parameter传入参数,保持默认NULL就行
Allocation软件定时器内存的分配方式,一般使用动态
Static: 静态方式是直接在RAM占据一个静态空间
Dynamic:动态方则是在初始配置的内存池大小数组中动态申请、释放空间

参数配置完成后,生成代码,我们可以在 freertos.c 文件里面看到定时器创建后获得的句柄,以及生成的回调函数:

有了句柄,我们就可以调用 cmsis_os2.c 里面的定时器接口函数对定时器进行操作,先看一下 CubeMX 提供的定时器接口函数及其功能

函数功能
osTimerNew新建定时器,返回定时器控制句柄
osTimerGetName获取定时器名称
osTimerStart设置定时器周期,启动定时器
osTimerStop停止定时器
osTimerIsRunning检测定时器是否在运行
osTimerDelete删除定时器

其中常用的接口是定时器的启动和停止

定时器启动: osTimerStart,函数原型

osStatus_t osTimerStart (osTimerId_t timer_id, uint32_t ticks);

参数介绍:

参数功能
timer_id需要启动的定时器句柄
ticks设置定时器的运行周期

此处的 ticks 设定的数字是定时器两次调用回调函数的周期数目,每个 tick 是一个心跳时钟的长度

使用例程:

void StartTask02(void *argument)
{
  /* USER CODE BEGIN StartTask02 */
    osStatus_t result;
    uint8_t dat[10]={0};
    uint8_t *pro;
    result= osTimerStart(&myTimer01Handle,10);
    if(result == osOK)
    {
        //启动成功
    }else
    {
        //启动失败
    }
    
  /* Infinite loop */
  for(;;)
  {
		

    osDelay(10);
  }
  /* USER CODE END StartTask02 */
}

按照例程启动定时器,定时器会以 10个tick 的周期,调用回调函数

回调函数不要放阻塞函数,程序尽可能短

定时器启动: osTimerStop,函数原型

osStatus_t osTimerStop (osTimerId_t timer_id);

参数只有一个,就是定时器的控制句柄,传入即可停止定时器,例程如下

void StartTask02(void *argument)
{
  /* USER CODE BEGIN StartTask02 */
	osStatus_t result;
	uint8_t dat[10]={0};
	uint8_t *pro;
    result= osTimerStop(&myTimer01Handle);
    if(result == osOK)
    {
        //停止成功
    }else
    {
        //停止失败
    }
  /* Infinite loop */
  for(;;)
  {
    osDelay(10);
  }
  /* USER CODE END StartTask02 */
}

软件定时器是由软件定时器维护任务进行维护,检测各个定时器的状态,进行处理,回调回调函数,软件定时器维护任务的参数配置在前面的 Config 就已经提到过

5.2 CubeMX下信号量的创建和配置

信号量是 RTOS 的一个内核对象,该对象有一个队列表示该信号量拥有的信号数目,任何任务都可以对这个信号数目进行获取和释放,获取时信号-1,释放时信号+1,为0时不能继续获取,此时有任务想要继续获取信号量的话,任务会挂起在该内核对象的挂起列表,等到信号可以获取时进行恢复,根据这个特性,信号量常用于控制对共享资源的访问和任务同步,下面介绍一下 CubeMX 下信号量的配置:

点开配置页面,可以看到有两个信号量添加页面,其中 Binary Semaphores 是二值信号量,Counting Semaphores 是计数信号量,二进制信号量,仅有一个队列或者说 token,用于同步一个操作;计数信号量则拥有多个 tokens,可用于同步多个操作,或者管理有限资源

二值信号量创建:

点击 Add,配置参数

参数介绍

参数功能
Semaphore Name信号量名称
Allocation内存分配方式,一般使用动态
Static: 静态方式是直接在RAM占据一个静态空间
Dynamic:动态方则是在初始配置的内存池大小数组中动态申请、释放空间

计数信号量:

点击 Add,配置参数

参数介绍

参数功能
Semaphore Name信号量名称
Count计数信号量的最大数目
Allocation内存分配方式,一般使用动态
Static: 静态方式是直接在RAM占据一个静态空间
Dynamic:动态方则是在初始配置的内存池大小数组中动态申请、释放空间

配置完成后我们生成代码,在 freertos.c 的初始化代码中可以看到信号量被创建,并且返回了信号量的控制句柄

下面介绍一下 CubeMX 提供的信号量操作函数接口:

函数功能
osSemaphoreNew创建新的信号量
*osSemaphoreGetName获取信号量的名称
osSemaphoreAcquire获取信号量
osSemaphoreRelease释放信号量
osSemaphoreGetCount获取当前可用信号量的数目
osSemaphoreDelete删除信号量

其中常用的函数有获取和释放信号量,下面介绍一下这两个函数的参数和使用方式

获取信号量 osSemaphoreAcquire

函数原型

osStatus_t osSemaphoreAcquire (osSemaphoreId_t semaphore_id, uint32_t timeout);

参数介绍

参数功能
semaphore_id传入要获取信号量的控制句柄
timeout获取等待时间(等待期间任务挂起在内核对象的挂起队列)

使用例程

void StartDefaultTask(void *argument)
{
  /* USER CODE BEGIN StartDefaultTask */
	osStatus_t result;
	
	
  /* Infinite loop */
  for(;;)
  {
		result = osSemaphoreAcquire(&myBinarySem01Handle,10);
		if(result == osOK)
		{
			//获取成功
		}else
		{
			//获取失败
		}
    osDelay(1);
  }
  /* USER CODE END StartDefaultTask */
}

释放信号量 osSemaphoreRelease

函数原型

osStatus_t osSemaphoreRelease (osSemaphoreId_t semaphore_id);
参数功能
semaphore_id传入要释放的信号量控制句柄

使用例程

void StartDefaultTask(void *argument)
{
  /* USER CODE BEGIN StartDefaultTask */
	osStatus_t result;
	
	
  /* Infinite loop */
  for(;;)
  {
		result = osSemaphoreRelease(&myBinarySem01Handle);
		if(result == osOK)
		{
			//释放成功
		}else
		{
			//释放失败
		}
    osDelay(1);
  }
  /* USER CODE END StartDefaultTask */
}

二值信号量和计数信号量的操作基本一致,没用区别,只是用有的信号队列最大数目不同而已

同时注意信号量在使用过程中会出现优先级反转的Bug,使用时需要注意

六、创建互斥量

6.1 CubeMX下互斥量的创建和配置

互斥量其实就是一个拥有优先级继承的二值信号量,互斥信号量适合用于那些需要互斥访问的应用中,在互斥访问中互斥信号量相当于一个钥匙,当任务想要使用资源的时候就必须先获得这个钥匙,当使用完资源以后就必须归还这个钥匙,这样其他的任务就可以拿着这个钥匙去使用资源,与信号量不同的是,互斥量的释放必须由获取他的任务进行释放,如果不释放,可能会造成死锁

死锁就是两个任务获取对方拥有的锁,各自进入挂起列表,无法释放互斥锁

下面介绍一下 CubeMX 下互斥量的配置,在配置界面我们可用看到两个互斥量配置界面,上面的是普通互斥量,其获取只能获取一次,重复获取是无效的,而第二个则是递归互斥量,递归互斥信号量可以获取多次,但对应的也要释放多次才能让出使用权,比如我获取3次,任务要释放3次才能释放该互斥量的使用权

使用互斥量,需要点击 Add 然后配置参数

参数介绍:

参数功能
Mutex Name互斥量名称
Allocation内存分配方式,一般使用动态
Static: 静态方式是直接在RAM占据一个静态空间
Dynamic:动态方则是在初始配置的内存池大小数组中动态申请、释放空间

递归互斥信号量的配置方式与其相同,包括配置参数也相同,两者只是在用法上有些许区别,添加方式如下:

添加配置完成后,点击生成代码,在 freertos.c 文件中我们可以看到互斥量初始化完成,并且生成了对应的控制句柄

CubeMX 提供的 API 接口函数如下

函数功能
osMutexNew创建互斥量
*osMutexGetName获取互斥量名称
osMutexAcquire任务获取互斥量
osMutexRelease任务释放互斥量
osMutexGetOwner获取互斥量的拥有任务的任务 TCB
osMutexDelete删除互斥量

主要使用到的还是互斥量的获取与释放,下面分析一下这两个函数:

获取互斥量 osMutexAcquire

函数原型

osStatus_t osMutexAcquire (osMutexId_t mutex_id, uint32_t timeout);

参数介绍:

参数功能
mutex_id互斥量控制句柄
timeout获取互斥量时的等待时间(等待期间任务挂起在内核对象的挂起队列)

使用方式

void StartTask02(void *argument)
{
  /* USER CODE BEGIN StartTask02 */
	osStatus_t result;
    result= osMutexAcquire(&myMutex01Handle,10);
    if(result == osOK)
    {
        //获取成功
    }else
    {
        //获取失败
    }
  /* Infinite loop */
  for(;;)
  {
    osDelay(10);
  }
  /* USER CODE END StartTask02 */
}

释放互斥量 osMutexRelease

函数原型

osStatus_t osMutexRelease (osMutexId_t mutex_id以上是关于CubeMX使用FreeRTOS编程指南的主要内容,如果未能解决你的问题,请参考以下文章

使用STM32CubeMX创建FreeRTOS项目

使用STM32CubeMX创建FreeRTOS项目

使用STM32CubeMX创建FreeRTOS项目

用STM32CubeMx创建的FreeRTOS可以运行多少个实例

STM32CubeMX学习笔记(32)——FreeRTOS实时操作系统使用(事件)

Stm32CubeMx lwip+freeRTOS TCP 服务