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 Settings | CubeMX 生成代码预配置项 |
User Constants | 用户常量定义 |
以上各个功能分的很清晰,我们需要配置什么功能就去对应的选项下进行配置,下面根据各个配置项进行详细配置介绍
三、系统设置
首先我们先了解一下 Config Parameters,他的配置参数如下
参数功能表:
参数 | 功能 |
---|---|
API | 显示 FreeRTOS API 接口版本 |
Version | 显示 FreeRTOS 内核版本 显示 CMSIS 版本 |
Kernel Setting | FreeRTOS 调度内核设置 |
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 时使用协程,如果使用抢占式调度器的话内核会在每个时钟节拍中断中进行任务切换,当使用协程的话会在如下地方进行任务切换
- 一个任务调用了函数 taskYIELD()。
- 一个任务调用了可以使任务进入阻塞态的 API 函数。
- 应用程序明确定义了在中断中执行上下文切换。
- 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)
- 1
每当空闲任务执行一次,钩子函数都会被执行一次
- USE_TICK_HOOK
使能后,系统生成一个空回调函数,由用户编写函数主体
void vApplicationTickHook(void)
- 1
每个TICK周期,钩子函数都会执行一次
- USE_MALLOC_FAILED_HOOK
使能后,系统生成一个空回调函数,由用户编写函数主体
void vApplicationMallocFailedHook(void)
- 1
当申请动态内存失败时,钩子函数会执行一次
- USE_DAEMON_TASK_STARTUP_HOOK
使能后,系统生成一个空回调函数,由用户编写函数主体
void vApplicationDaemonTaskStartupHook(void).
- 1
任务刚启动时,钩子函数会执行一次
- CHECK_FOR_STACK_OVERFLOW
使能后,系统生成一个空回调函数,由用户编写函数主体
void vApplicationStackOverflowHook( xTaskHandle xTask, signed char *pcTaskName );
- 1
任务栈溢出时,钩子函数会执行一次,传入任务 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是不会再更新和维护协程了,因此大家解一下就行
协程特点:
- 堆栈使用
所有的协程使用同一个堆栈(如果是任务的话每个任务都有自己的堆栈),这样就比使用任务消耗更少的 RAM - 调度器和优先级
协程使用合作式的调度器,但是可以在使用抢占式的调度器中使用协程 - 宏实现
协程是通过宏定义来实现的 - 使用限制
为了降低对 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"
- 1
在队列头文件内我们可以在 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);
- 1
参数的功能
参数 | 功能 |
---|---|
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 */
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
发送消息的优先级暂时无用,CubeMX 对 FreeRTOS 的支持还不完善,发送消息里面的优先级未使用到,并且入队方式使用的是发送到队列尾部,没有从头部插入的方式,有需求可以 通过包含 queue.h 文件,调用 FreeRTOS 的官方代码,或者自己修改 生成代码的 API 接口结合优先级使用队列的向前插入和向后插入,丰富系统功能!
除了发送消息到队列,接受队列的消息 API 接口也经常用到,函数原型如下
osStatus_t osMessageQueueGet (osMessageQueueId_t mq_id, void *msg_ptr, uint8_t *msg_prio, uint32_t timeout);
- 1
参数的功能
参数 | 功能 |
---|---|
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 */
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
注意: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);
- 1
参数介绍:
参数 | 功能 |
---|---|
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 */
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
按照例程启动定时器,定时器会以 10个tick 的周期,调用回调函数
回调函数不要放阻塞函数,程序尽可能短
定时器启动: osTimerStop
,函数原型
osStatus_t osTimerStop (osTimerId_t timer_id);
- 1
参数只有一个,就是定时器的控制句柄,传入即可停止定时器,例程如下
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 */
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
软件定时器是由软件定时器维护任务进行维护,检测各个定时器的状态,进行处理,回调回调函数,软件定时器维护任务的参数配置在前面的 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);
- 1
参数介绍
参数 | 功能 |
---|---|
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 */
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
释放信号量 osSemaphoreRelease
函数原型
osStatus_t osSemaphoreRelease (osSemaphoreId_t semaphore_id);
- 1
参数 | 功能 |
---|---|
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 */
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
二值信号量和计数信号量的操作基本一致,没用区别,只是用有的信号队列最大数目不同而已
同时注意信号量在使用过程中会出现优先级反转的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);
- 1
参数介绍:
参数 | 功能 |
---|---|
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 */
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
释放互斥量 osMutexRelease
函数原型
osStatus_t osMutexRelease (osMutexId_t mutex_id);
- 1
参数介绍:
参数 | 功能 |
---|---|
mutex_id | 互斥量控制句柄 |
使用方式
void StartTask02(void *argument)
{
/* USER CODE BEGIN StartTask02 */
osStatus_t result;
result= osMutexRelease(myMutex01Handle);
if(result == osOK)
{
//释放成功
}else
{
//释放失败
}
/* Infinite loop */
for(;;)
{
osDelay(10);
}
/* USER CODE END StartTask02 */
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
使用方式和信号量基本相同,因为互斥量本质上就是信号量的一种
七、创建事件标志组
7.1 CubeMX下事件的创建和配置
任务间的同步除了信号量还有时间标志组,信号的同步通常是一对一的同步,有的时候系统需要多对一的同步,比如同时满足5个按键按下时,任务启动,如果使用信号会很占据资源,所以 RTOS 引入了事件标志组来满足这一需求,下面我们看一下 CubeMX 内事件标志组的配置方法:
点击 Add 创建事件标志组
配置介绍
参数 | 功能 |
---|---|
Event flags Name | 事件标志组名称 |
Allocation | 内存分配方式,一般使用动态 Static: 静态方式是直接在RAM占据一个静态空间 Dynamic:动态方则是在初始配置的内存池大小数组中动态申请、释放空间 |
配置完成后,生成代码,在系统初始化内,看有没有生成事件标志组控制句柄,可以看到句柄创建完成
CubeMX 提供的配置事件标志组的接口 API 如下:
函数 | 功能 |
---|---|
osEventFlagsNew | 创建事件标志组 |
*osEventFlagsGetName | 获取事件标志组名称 |
osEventFlagsSet | 设置事件标志组 |
osEventFlagsClear | 清除事件标志组 |
osEventFlagsGet | 获取当前事件组标志信息 |
osEventFlagsWait | 等待事件标志组触发 |
osEventFlagsDelete | 删除事件标志组 |
常用的 API 接口是设置事件标志组以及等待事件标志组的触发,下面我们分析一下这两个 API
在了解 API 前我们需要简单了解一下事件的触发原理:首先事件标志组的数据类型为 EventGroupHandle_t,事件标志组中的所有事件位都存储在一个无符号的 EventBits_t 类型的变量中,当 configUSE_16_BIT_TICKS 为 1 的时候事件标志组可以存储 8 个事件位,当 configUSE_16_BIT_TICKS 为 0 的时候事件标志组存储 24个事件位,每个事件位其实就是一个0或者1数字,就像下面的24位组成一个事件标志组
我们在使用事件API接口函数前需要先定义我们需要的触发事件位,比如添加如下的代码
#define event1 1<<1 //事件1
#define event2 1<<2 //事件2
- 1
- 2
编写好触发事件后,我们在看如何使用 API 接口
设置事件标志 osEventFlagsSet
函数原型
uint32_t osEventFlagsSet (osEventFlagsId_t ef_id, uint32_t flags);
- 1
参数介绍:
参数 | 功能 |
---|---|
ef_id | 事件标志组控制句柄 |
flags | 事件位 |
使用方式:设置事件1和事件2
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
osStatus_t result;
/* Infinite loop */
for(;;)
{
result = osEventFlagsSet(myEvent01Handle,event1);
if(result == osOK)
{
//事件1设置成功
}else
{
//事件1设置失败
}
result = osEventFlagsSet(myEvent01Handle,event2);
if(result == osOK)
{
//事件2设置成功
}else
{
//事件2设置失败
}
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
- 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
等待事件标志 osEventFlagsWait
函数原型
uint32_t osEventFlagsWait (osEventFlagsId_t ef_id, uint32_t flags, uint32_t options, uint32_t timeout);
- 1
参数介绍:
参数 | 功能 |
---|---|
ef_id | 事件标志组控制句柄 |
flags | 等待的事件位 |
options | 等待事件位的操作 osFlagsWaitAny :等待的事件位有任意一个等到就恢复任务 osFlagsWaitAll:等待的事件位全部等到才恢复任务 osFlagsNoClear:等待成功后不清楚所等待的标志位(默认清除) |
timeout | 等待事件组的等待时间(等待期间任务挂起在内核对象的挂起队列) |
使用例子:同时等待事件1和事件2,且等待到不清除
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
osStatus_t result;
/* Infinite loop */
for(;;)
{
result = osEventFlagsWait(myEvent01Handle,event1|event2,osFlagsWaitAll|osFlagsNoClear,10);
if(result == osOK)
{
//等待成功
}else
{
//等待失败
}
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
八、用户常量
User Constants 用于添加用户常量,将不变的量转化为常量保存,可以节省 RAM 资源空间,因为常量和变量的保存位置不同,详细了解可以参考这篇文章:C语言:内存四区
九、任务通知
FreeRTOS 的每个任务都有一个 32 位的通知值,任务控制块中的成员变量 ulNotifiedValue 就是这个通知值。任务通知是一个事件,假如某个任务通知的接收任务因为等待任务通知而阻塞的话,向这个接收任务发送任务通知以后就会解除这个任务的阻塞状态,CubeMX内没有提供相关的配置项,但在其生成的 FreeRTOS 接口里面有相关函数进行配置,函数位置如下:
接口函数功能:
函数 | 功能 |
---|---|
osThreadFlagsSet | 设置任务的通知标志 |
osThreadFlagsClear | 清除任务通知 |
osThreadFlagsGet | 获取任务标志 |
osThreadFlagsWait | 等待特定的任务标志 |
常用的两个 API 就是设置任务通知和等待任务通知函数
设置通知 osThreadFlagsSet
函数原型
uint32_t osThreadFlagsSet (osThreadId_t thread_id, uint32_t flags);
- 1
参数介绍:
参数 | 功能 |
---|---|
thread_id | 任务控制块 |
flags | 设置的标志 |
使用方式
先定义一个事件标志
#define event1 1<<1 //事件1
- 1
然后调用 API 通知对应任务事件发生
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
osStatus_t result;
/* Infinite loop */
for(;;)
{
result = osThreadFlagsSet(myTask02Handle,event1);
if(result == osOK)
{
//设置成功
}else
{
//设置失败
}
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
等待通知 osThreadFlagsWait
函数原型
uint32_t osThreadFlagsWait (uint32_t flags, uint32_t options, uint32_t timeout);
- 1
参数介绍:
参数 | 功能 |
---|---|
flags | 设置的标志 |
options | 设置功能 |
timeout | 超时时间 |
options参数
参数 | 功能 |
---|---|
osFlagsWaitAny | 等待32位通知值任意一位触发后恢复任务(默认) |
osFlagsWaitAll | 等待指定的任务通知值全部触发后再恢复任务 |
osFlagsNoClear | 恢复任务后不清除任务标志(默认清除) |
使用方式
调用 API 等待对应的任务通知就绪,当其他任务设置到对应的通知后,任务恢复运行
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
osStatus_t result;
/* Infinite loop */
for(;;)
{
result = osThreadFlagsWait(myTask02Handle,osFlagsWaitAll,event1);
if(result == osOK)
{
//等待成功
}else
{
//等待失败
}
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
任务通知其实个任务事件标志组使用上没有多大的区别,但他们两个的实现原理不同,同时任务通知对资源的占用更少
根据 FreeRTOS 官方的统计,使用任务通知替代二值信号量的时候任务解除阻 塞的时间要快 45%,并且需要的 RAM 也更少
十、系统内核配置
CubeMX 生成的代码中封装了一系列内核配置函数,有些函数也经常使用到,比如获取时间戳和调度器管理的函数,这里不做过多解释,简单的介绍一下函数的功能
函数 | 功能 |
---|---|
osKernelInitialize | 初始化RTOS的内核 |
osKernelGetInfo | 获取RTOS的信息 |
osKernelGetState | 获取当前内核的运行状态 |
osKernelStart | 启动内核调度 |
osKernelLock | 锁内核调度器 |
osKernelUnlock | 解锁内核调度器 |
osKernelRestoreLock | 恢复RTOS内核调度器锁状态 |
osKernelSuspend | 挂起任务 |
osKernelResume | 恢复任务 |
osKernelGetTickCount | 用于获取系统当前运行的时钟节拍数 |
osKernelGetTickFreq | 用于获取系统当前运行的时钟节拍的分频频率 |
osKernelGetSysTimerCount | 获取系统时钟(SysTick)的计数值 |
osKernelGetSysTimerFreq | 获取系统时钟(SysTick)的频率 |
文章来源: blog.csdn.net,作者:JeckXu666,版权归原作者所有,如需转载,请联系作者。
原文链接:blog.csdn.net/qq_45396672/article/details/120877303
- 点赞
- 收藏
- 关注作者
评论(0)