经过前面的学习,现在我已经对FreeRTOS有了一个初步的认识,而且也可以使用FreeRTOS来写代码了,为了让我们的开发更加方便迅速,本喵来介绍下如何使用CubeMX来配置带有FreeRTOS的工程。
Middleware and Software Packs
一栏中选择FREERTOS
。Interface
接口处选择CMSIS_V2
版本。Config parameters
中看到FreeRTOS系统的各种属性。
CMSIS
封装的RTOS接口有两个版本RTOS V1
和RTOS V2
,V2兼容V1,支持更多的cortex内核。
改变HAL库使用的时基源:
在默认情况下,ST的HAL库使用的时钟基准输入源也就是时基源,利用的是内核的滴答定时器(Tick),如果没有使用RTOS的话没有什么问题。
但是如果要使用一个RTOS比如FreeRTOS或者RT-Thread,这些RTOS的内核时钟通常也是利用的内核的滴答定时器(Tick),为了不影响内核的运行,HAL库的时基源就最好换一个。
如上图,本喵这里就选择STM32F103的定时器TIM8作为HAL库的时基源。
在实际设计中,选择哪一个定时器作为HAL库的时基源应该需要谨慎考虑,为了不要互相影响,最好不要选择与控制其它外设的定时器相同的定时器。
比如本喵要使用TIM3来输出一个PWM波,那么这里就最好不要再选择TIM3作为HAL库的时基源。
确认IDE的版本:
生成工程:
MDK工程勾选MicroLIB:
如上图,在MDK工程中,如果使用printf
等标准库函数的话,需要将Target
中的Use MicroLIB
勾选上。
为了方便调试程序,需要在该工程中添加UART1来打印调试信息,需要自己实现fputc
函数:
简单修改freertos.c:
如上图,在STM32CubeMX中选择好RTOS的接口版本后,默认参数中是有一个默认的任务的,双击红色框就会出现默认任务的配置:
如上图是该默认任务的属性,任务名defaultTask
,优先级,任务入口函数,等等属性。
如上图,该函数位于freertos.c
文件中,为了验证我们配置的RTOS是否能够正常运行,我们可以在freertos.c中默认任务执行的函数中添加测试打印。
如上图,使用软件模拟器中的串口1,可以看到能够正常打印,说明RTOS内核已经运行起来了,这样就得到了我们的FreeRTOS的工程模板。
CMSIS的英文全称是:Common Microcontroller Software Interface Standard,直译过来就是通用微控制器软件接口标准。
它是一个用来让微控制器开发者减少学习时间、简化软件移植、加速工程创建仿真和加速应用产品上架的工具集合。
如上图,CMSIS包含了这12个软件包,支持Cortex-M内核和Cortex-A内核处理器的多种外设,我们本次最关心的RTOS也在其中,并且有两个本。
从他们的描述可以看出,V2是在V1的基础上扩展的,既支持M内核也支持A内核。所以在兼容性上,V2版本的RTOS更强。
- CMSIS其实就是一个工具,本喵这里只用到这个工具中包含的RTOS。
CMSIS接口和应用层以及RTOS层之间的关系:
如上图,它将RTOS层的一些API又做了一层封装,目的是为了兼容多种RTOS,最下层的第三方的Real Time Kernel可以是FreeRTOS、RT-Thread,也可以是ucos等其它实时操作系统,我们这里主要应用的是FreeRTOS。
如上图,选择Tasks and Queue
添加任务和队列,在任务的位置选择Add
添加任务,然后回弹出New Task
属性选择窗口。
可以配置的任务属性包括:
如上图所示,将默认任务进行属性修改,再创建三个任务,一共四个不同属性的任务。
- 默认任务无法删除,这个任务必然会有,而且只能是动态的。
- 默认任务入口函数的属性只能选择default或者weak,不能选择extern。
- 能修改的只有名字优先级,栈大小等属性。
动态任务:
如上图,在freertos.c
文件中,会创建一个任务句柄GenericTaskHandle
,然后调用CMSIS的接口osThreadNew
创建新任务。
在调用该接口时传入任务的入口函数GenericTaskFun
,任务参数NULL
,以及GenericTask_attributes
任务属性。
GenericTask_attributes
任务属性是一个全局性的结构体变量,里面包括了该任务的名字name
,任务的栈大小128*4
字节,任务的优先级这几个属性。
如上图所示是任务属性结构体osThreadAttr_t
类型的定义,它包含任务名称,TCB内存和大小,独立栈内存和大小,等等诸多属性。
- 创建的动态任务时,任务属性结构体变量
GenericTask_attributes
只是填充了该结构体中的一部分成员。- 没有填充的成员初始值都是0。
如上图所示,CMSIS创建新任务的函数osThreadNew
内部实现中,会对传入的任务属性attr
进行判断。
如果该属性中的成员变量TCB内存地址attr->cb_mem
不是空,并且成员变量attr->cb_size
有值,并且栈内存地址attr->stack_mem
不是空,并且栈大小attr->stack_size
大于0,此时就会将标志位mem
置为1。
反之,如果attr->cb_mem == NULL
,且attr->cb_size == 0U
且attr->stack_mem == NULL
,就会将标志位mem
置为0。
xTaskCreateStatic
创建静态任务。xTaskCreate
创建动态任务。前面本喵展示任务属性结构体GenericTask_attributes
时说过,它只赋值了name
,任务的栈大小128*4
字节,任务的优先级这几个属性,其他没有赋值。
所以其他成员的初始值都是0,所以在创建新任务函数osThreadNew
内部会将标志位mem
置为0,最终调用的是xTaskCreate
创建动态任务。
静态任务:
如上图,在freertos.c
中同样调用CMSIS的接口osThreadNew
来创建静态任务,此时传入的任务属性结构体StaticTask_attributes
中,填充了cb_mem
,cb_size
,stack_mem
,stack_size
等成员。
在函数osThreadNew
内部将标志位mem
置一,从而调用xTaskCreateStatic
创建静态任务。
入口函数属性是default:
如上图,此时生成的工程文件中,入口函数是被定义在freertos.c
源文件中的,我们直接在入口函数内部的死循环中写自己的代码即可。
freertos.c
文件中有很多代码,包括函数属性,函数句柄,函数定义,以及一些FreeRTOS的源码,非常杂乱。
入口函数属性是external:
如上图,此时在freertos.c
中并没有入口函数的定义,而是只有一个extern
修饰的入口函数声明。
- extern:表明该函数的定义是在其他源文件中。
此时直接编译的话会报错入口函数ExternalTaskFun
不存在。
如上图,在另一个源文件external_func.c
中定义该外部函数ExternalTaskFun
,并将该文件添加到工程中,此时就可以编译通过且正常使用该函数了。
如上图,将用户定义的入口函数单独放在一个App
文件中管理起来,可以让整个工程更加容易维护。
- 一般在创建任务或者回调函数时,都会将入口函数设置为
external
属性。
入口函数属性是weak:
如上图,此时会在freertos.c
中生成一个__weak
修饰的弱函数,该入口函数和使用CubeMX生成的中断函数一样,如果用户没有定义,则使用这个弱定义的函数,如果用户定义了则使用用户定义的入口函数。
虽然也可以在其他源文件中定义入口函数来重写弱定义的入口函数,但是freertos.c
中始终存在这样一个入口函数而且毫无用处,所以还不如定义external
类型的入口函数。
接口对比:
如上图,CMSIS创建任务的函数osThreadNew
底层就是在调用FreeRTOS的xTaskCreate
和xTaskCreateStatic
这两个函数,具体调用哪个是根据任务属性osThreadAttr_t
来决定的。
如上图,选择Timers and Semaphore
选项,弹出的框中有计数型信号量和二进制信号量,分别点击Add
就可以增加相应的信号量。
如上图,可以创建静态和动态的信号量。其中,二进制型信号量在创建时不用指定计数值,因为它的计数值就是1,相当于互斥量。
计数型信号量在创建的时候可以指定计数值上限,如上图中的Count
那一栏,默认情况下的值是2,可以根据实际需求去修改。
如上图,生成工程以后,同样会在freertos.c
中创建信号量,CMSIS创建信号量的接口osSemaphoreNew
创建信号量。
如上图,同样会根据信号量属性osSemaphoreAttr_t
去设置标志位mem
来决定使用FreeRTOS中的哪个函数来创建信号量。
接口对比:
如上图便是CMSIS接口和FreeRTOS接口操作函数对比,本喵不再详细讲解。
如上图,点击Mutexex
选项,可以添加普通信号量Mutexes
和递归信号量Recursive Muxtexes
。
如上图,不同的互斥量会定义不同的属性结构体,在调用osMutexNew
的时候传入,在函数内部根据属性调用不同的FreeRTOS接口。
接口对比:
如上图,不同类型的事件组对应不同的事件组属性osThreadAttr_t
,在调用osEventFlagsNew
时传入,在函数内部根据属性调用不同的FreeRTOS接口函数。
接口对比:
如上图,点击Tasks and Queues
,然后点击Queues
那一栏中的Add
增加新队列。
如上图,支持队列的动态创建和静态创建,而且还支持在队列中存放自定义数据类型MyFrameDtat
。
如上图,不同类型的队列有不同的队列属性结构体,在调用osMessageQueueNew
时传入,内部再根据属性调用不同的FreeRTOS函数。
队列中存放的是自定义数据类型时,需要我们自己定义MyFrameData
,使用typedef
将结构名为为MyFrameData
即可,这部分CubeMX没有帮我们生成,需要我们自己定义。
接口对比:
由于任务通知并不需要具体的数据结构,而是直接操作目标任务的TCB,所以CubeMX也没有对应的创建选项,但是CMSIS封装了一些任务通知的函数。
函数名 | 参数说明 | 返回值 | 功能描述 |
---|---|---|---|
uint32_t osThreadFlagsSet (osThreadId_t thread_id, uint32_t flags) | thread_id:某个线程的ID flags:线程的标志值 |
uint32_t :设置好之后的标志值或者错误码 | 设置指定线程的标志值 |
uint32_t osThreadFlagsClear (uint32_t flags) | flags:线程的标志值 | uint32_t :清除前的标志值或错误码 | 清除指定线程的某个标志 |
uint32_t osThreadFlagsGet (void) | \ | uint32_t:当前线程的标志值 | 获取当前线程的标志值 |
uint32_t osThreadFlagsWait (uint32_t flags, uint32_t options, uint32_t timeout) | flags:线程的标志值 options:指定的操作 timeout:等待的延时时间 |
uint32_t:当前线程的被清除前的标志值 | 等待某个标志值 |
线程IDthread_id
是一个void *
类型的指针,在osThreadFlagsSet
函数内部,将其强转为TaskHandle_t
,成为一个任务句柄。
- 线程ID本质上就是一个任务的句柄,也就是一个TCB结构指针。
uint32_t osThreadFlagsWait (uint32_t flags, uint32_t options, uint32_t timeout):
如上图,等待函数osThreadFlagsWait
的options
选项可以传入这三个参数。
如上图,默认情况下是osFlagsWaitAny
,此时只要TCB中的通知值和传入的flags
中有一个比特位吻合就等待成功。osFlagsWaitAll
必须是通知值和传入的flags
完全吻合才会等待成功。
默认情况下等待成功后会将通知值清除,如果设置了osFlagsNoClear
则不会清除。
接口对比:
如上图,可以看到,使用CMSIS是的任务通知接口是比FreeRTOS的接口简洁了的,但是功能没有那么强大了,CMSIS只封装了最常用的功能。
如上图,选择Timers and Semaphores
,在Timers
中点击Add
添加软件定时器。
如上图,软件定时器可以在Type
中选择osTimerPeriodic
周期性定时器,也可以选择osTimerOnce
一次性定时器,回调函数的类型也有Defult
,external
,weak
三种类型。
- 和任务入口函数一样,回调函数一般选择
external
类型,在另外一个源文件中定义,方便维护。
如上图,软件定时器也有自己的属性结构体,在调用osTimerNew
创建定时器的时候传入属性osTimerAttr_t
。第二个参数决定该定时器是一次性还是周期性:
接口对比:
如上图,点击Config parameters
就可以查看版本属性以及配置FreeRTOS的各种属性。也就是前面FreeRTOS接口时配置的各种宏。
CMSIS-RTOS V2 | 作用 |
---|---|
FreeRTOS API | CMSIS V2 |
FreeRTOS version | 10.0.1 |
CMSIS-RTOS version | 2.00 |
USE_PREEMPTION | 可配置,可以选择有优先级的任务调度或者无优先级的任务调度 |
CPU_CLOCK_HZ | SystemCoreClock |
TICK_RATE_HZ | 滴答时钟频率,默认1k,可设置 |
MAX_PRIORITIES | 最大优先等级数,默认是56,不可配置 |
MINIMAL_STACK_SIZE | 最小堆空间,取值64~768,默认128words,可配置; |
MAX_TASK_NAME_LEN | 任务名称字符串的最大长度,范围12~255,默认16,可配置 |
USE_16_BIT_TICK | 16位滴答定时器的计数值,使能的话是16位无符号类型,不使能则是32位无符号类型,默认不使能,不可配置 |
IDLE_SHOULD_YIELD | 空闲任务让步给其它任务,使能则让步,否则不让步 |
USE_MUTEXES | 使能则在编译的时候包括互斥量功能,否则不包括,默认使能 |
USE_RECURSIVE_MUTEXES | 使能则包含递归互斥功能,否则不包含,默认使能 |
USE_COUNTING_SEMAPHORES | 使能则包括计数信号量,否则不包含,默认使能 |
QUEUE_REGISTRY_SIZE | 注册的队列个数,范围0~255,默认是8 |
USE_APPLICATION_TASK_TAG | 任务标签,默认不使能;这个功能是仅为高级用户设计的 |
ENABLE_BACKWARD_COMPATIBILITY | 兼容历史版本的宏定义名称,默认使能 |
USE_PORT_OPTIMISED_TASK_SELECTION | 最优的任务执行分配,默认不使能 |
USE_TICKLESS_IDLE | 空闲任务锁住tick,默认不使能 |
USE_TASK_NOTIFICATIONS | 任务通知值,默认使能 |
RECORD_STACK_HIGH_ADDRESS | 任务堆地址保存,默认不保存到TCB中 |
Memory Allocation | 内存分配,可选动态分配、静态分配或者两者皆可 |
TOTAL_HEAP_SIZE | 栈空间大小,范围512bytes~64kbytes,默认3072bytes |
Memory Management scheme | 内存管理,有5种可选,默认使用heap_4 |
USE_IDLE_HOOK | 空闲任务钩子函数使能,默认不使能 |
USE_TICK_HOOK | 滴答钩子函数使能,默认不使能 |
USE_MALLOC_FAILED_HOOK | 内存分配失败的钩子函数使能,默认不使能 |
USE_DAEMON_TASK_STARTUP_HOOK | 守护进程的启动的钩子函数,默认不使能 |
CHECK_FOR_STACK_OVERFLOW | 检查堆溢出的钩子函数,默认不使能 |
GENERATE_RUN_TIME_STATS | 使能获取任务的运行时间,默认不使能 |
USE_TRACE_FACILITY | 使能以可视化的执行和追踪其他结构体成员和函数,默认使能 |
USE_STATS_FORMATTING_FUNCTIONS | 搭配USE_TRACE_FACILITY一起使用,默认不使能 |
USE_CO_ROUTINES | 使能协同功能,默认不使能 |
MAX_CO_ROUTINE_PRIORITIES | 协同任务的最大优先等级,默认是2,范围1~255 |
USE_TIMERS | 软件定时器功能,默认使能 |
TIMER_TASK_PRIORITY | 软件定时器任务的优先等级,范围0~55,默认2 |
TIMER_QUEUE_LENGTH | 软件定时器队列长度,范围1~255,默认10 |
TIMER_TASK_STACK_DEPTH | 定时器任务堆的深度,范围128words~16384words,默认256words |
LIBRARY_LOWEST_INTERRUPT_PRIORITY | 最低等级的中断优先等级,范围是1~15,默认15 |
LIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY | 最高的中断优先等级,范围是1~15,默认是5 |
以上就是可以配置的宏,直接在CubeMX中点击即可,不用手动在FreeRTOSConfig.h
中配置了。
传送门。