【基于FreeRTOS的STM32F103系统】简介及官方文件移植
【基于FreeRTOS的STM32F103系统】编写FreeRTOS程序
【基于FreeRTOS的STM32F103系统】内存管理及任务调度
【基于FreeRTOS的STM32F103系统】Heap_4内存管理机制程序详解
【基于FreeRTOS的STM32F103系统】移动底盘程序优化
前面我们铺垫了很多关于FreeRTOS的基础知识,包括任务调度机制、内存管理等,我们的最终目的都是为了优化我们移动机器人的代码,使得代码更加简洁易懂、效率更高,并便于用户模块化操作。
分为两部分介绍,先介绍如何将FreeRTOS简洁的移植到我们的程序中,只保留最核心的文件,并使用一个配置头文件对整个FreeRTOS进行管理;然后再介绍优化后的程序的执行逻辑。
在程序中,移植了正点原子的基于STM32的FreeRTOS程序,编写了自己的内存管理程序malloc.c,程序主要结构如下:
FreeRTOS_CORE中是FreeRTOS的核心文件,包括与协程有关的croutine.c,与事件组有关的event_groups.c,与列表有关的list.c,与队列有关的queue.c,与任务有关的tasks.c,与定时器时钟有关的timers.c
FreeRTOS_PORTABLE中是与FreeRTOS内存管理有关的文件,包括port.c和heap_4.c,port.c中主要包含一些与中断有关的函数,heap_4.c上一篇文章有详细介绍,与内存分配释放有关。
在FreeRTOSConfig.h文件中,对FreeRTOS的很多参数进行了配置,用户可以修改其中的文件对程序参量进行配置并对程序功能进行选择使用。
先声明了一个断言,方便提示用户,当程序出错时在FreeRTOS的那个程序哪一行出错
- //断言
- #define vAssertCalled(char,int) printf("Error:%s,%d\r\n",char,int)
- #define configASSERT(x) if((x)==0) vAssertCalled(__FILE__,__LINE__)
然后是与任务调度算法相关的配置选项,主要的是configUSE_PREEMPTION和configUSE_TIME_SLICING,不过一般都开启,置1就行。
- /***************************************************************************************************************/
- /* 调度算法配置选项 */
- /***************************************************************************************************************/
- #define configUSE_PREEMPTION 1 //1使用抢占式内核,0使用协程
- #define configUSE_TIME_SLICING 1 //1使能时间片调度(默认是使能的),同等优先级是否交替执行
- #define configUSE_PORT_OPTIMISED_TASK_SELECTION 1 //1启用特殊方法(使用硬件方法)来选择下一个要运行的任务
- //一般是硬件计算前导零指令,如果所使用的
- //MCU没有这些硬件指令的话此宏应该设置为0!
- #define configUSE_TICKLESS_IDLE 0 //1启用低功耗tickless模式
然后是基础配置选项,重要的几个是configUSE_QUEUE_SETS、configCPU_CLOCK_HZ、configTICK_RATE_HZ、configMAX_TASK_NAME_LEN、configUSE_16_BIT_TICKS,其他保持默认,configCPU_CLOCK_HZ要改成我们单片机的时钟频率,这里使用了代码定义的量SystemCoreClock,configTICK_RATE_HZ时钟节拍频率设置为1000,也就是周期为1ms,任务名字字符串长度configMAX_TASK_NAME_LEN如果你创建任务时的命名很长,需要改一下,configUSE_16_BIT_TICKS使用的时钟位数,需要和单片机一致,STM32为32位,这里需要将这个宏定义为0。
- /***************************************************************************************************************/
- /* FreeRTOS基础配置配置选项 */
- /***************************************************************************************************************/
- #define configUSE_QUEUE_SETS 1 //为1时启用队列
- #define configCPU_CLOCK_HZ (SystemCoreClock) //CPU频率
- #define configTICK_RATE_HZ (1000) //时钟节拍频率,这里设置为1000,周期就是1ms
- #define configMAX_PRIORITIES (32) //可使用的最大优先级
- #define configMINIMAL_STACK_SIZE ((unsigned short)130) //空闲任务使用的堆栈大小
- #define configMAX_TASK_NAME_LEN (20) //任务名字字符串长度
-
- #define configUSE_16_BIT_TICKS 0 //系统节拍计数器变量数据类型,
- //1表示为16位无符号整形,0表示为32位无符号整形
- #define configIDLE_SHOULD_YIELD 1 //为1时空闲任务放弃CPU使用权给其他同优先级的用户任务
- #define configUSE_TASK_NOTIFICATIONS 1 //为1时开启任务通知功能,默认开启
- #define configUSE_MUTEXES 1 //为1时使用互斥信号量
- #define configQUEUE_REGISTRY_SIZE 8 //不为0时表示启用队列记录,具体的值是可以
- //记录的队列和信号量最大数目。
- #define configCHECK_FOR_STACK_OVERFLOW 0 //大于0时启用堆栈溢出检测功能,如果使用此功能
- //用户必须提供一个栈溢出钩子函数,如果使用的话
- //此值可以为1或者2,因为有两种栈溢出检测方法。
- #define configUSE_RECURSIVE_MUTEXES 1 //为1时使用递归互斥信号量
- #define configUSE_MALLOC_FAILED_HOOK 0 //1使用内存申请失败钩子函数
- #define configUSE_APPLICATION_TASK_TAG 0 //1为每个任务分配一个“标签”值,标签钩子函数
- #define configUSE_COUNTING_SEMAPHORES 1 //为1时使用计数信号量
然后是与内存申请有关配置选项,一般保持默认就行,系统总堆大小可能需要微调
- /***************************************************************************************************************/
- #define configSUPPORT_DYNAMIC_ALLOCATION 1 //支持动态内存申请
- #define configTOTAL_HEAP_SIZE ((size_t)(20*1024)) //系统所有总的堆大小
-
然后是与钩子函数有关的配置选项,我们不需要使用钩子函数,也不建议初学者用,因为空闲任务的钩子函数需要写的很高效率尽量不阻塞
- #define configUSE_IDLE_HOOK 0 //1,使用空闲钩子;0,不使用
- #define configUSE_TICK_HOOK 0 //1,使用时间片钩子;0,不使用
-
与运行时间和任务状态收集有关的配置选项 ,这部分主要与调试代码有关,测一下代码运行时间,观察效率等。
- /***************************************************************************************************************/
- /* FreeRTOS与运行时间和任务状态收集有关的配置选项 */
- /***************************************************************************************************************/
- #define configGENERATE_RUN_TIME_STATS 0 //为1时启用运行时间统计功能
- #define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() ConfigureTimeForRunTimeStats() //定时器3提供时间统计的时基,频率为10K,即周期为100us
- #define portGET_RUN_TIME_COUNTER_VALUE() FreeRTOSRunTimeTicks //获取时间统计时间值
-
- #define configUSE_TRACE_FACILITY 1 //为1启用可视化跟踪调试
- #define configUSE_STATS_FORMATTING_FUNCTIONS 1 //与宏configUSE_TRACE_FACILITY同时为1时会编译下面3个函数
与协程有关的配置选项,保持默认,我们用不到协程。
- /***************************************************************************************************************/
- /* FreeRTOS与协程有关的配置选项 */
- /***************************************************************************************************************/
- #define configUSE_CO_ROUTINES 0 //为1时启用协程,启用协程以后必须添加文件croutine.c
- #define configMAX_CO_ROUTINE_PRIORITIES ( 2 ) //协程的有效优先级数目
与软件定时器有关的配置选项,FreeRTOS自带软件定时器,但是我们一般也用不到,STM32中有硬件定时器-滴答定时器,用这个就行
- /***************************************************************************************************************/
- /* FreeRTOS与软件定时器有关的配置选项 */
- /***************************************************************************************************************/
- #define configUSE_TIMERS 1 //为1时启用软件定时器
- #define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES-1) //软件定时器优先级
- #define configTIMER_QUEUE_LENGTH 5 //软件定时器队列长度
- #define configTIMER_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE*2) //软件定时器任务堆栈大小
FreeRTOS可选函数配置选项,这些以INCLUDE_开头的宏定义,主要是一些函数的开关,比如第一个INCLUDE_xTaskGetSchedulerState,设置为1意味着,使用TaskGetSchedulerState这个函数
- /***************************************************************************************************************/
- /* FreeRTOS可选函数配置选项 */
- /***************************************************************************************************************/
- #define INCLUDE_xTaskGetSchedulerState 1 //获取任务调度状态
- #define INCLUDE_vTaskPrioritySet 1 //任务优先级设置
- #define INCLUDE_uxTaskPriorityGet 1 //获取任务优先级
- #define INCLUDE_vTaskDelete 1 //删除任务
- #define INCLUDE_vTaskCleanUpResources 1 //清理任务占用资源
- #define INCLUDE_vTaskSuspend 1 //暂停任务
- #define INCLUDE_vTaskDelayUntil 1 //挂起任务,固定时间周期
- #define INCLUDE_vTaskDelay 1 //挂起任务,延时一段时间
- #define INCLUDE_eTaskGetState 1 //获取任务状态
- #define INCLUDE_xTimerPendFunctionCall 1 //暂停状态唤起
FreeRTOS与中断有关的配置选项,用不着,也不用深究
- /***************************************************************************************************************/
- /* FreeRTOS与中断有关的配置选项 */
- /***************************************************************************************************************/
- #ifdef __NVIC_PRIO_BITS
- #define configPRIO_BITS __NVIC_PRIO_BITS
- #else
- #define configPRIO_BITS 4
- #endif
-
- #define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 //中断最低优先级
- #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 //系统可管理的最高中断优先级
- #define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
- #define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
最后是与中断服务函数有关的配置选项,只不过做了两个重命名
- /***************************************************************************************************************/
- /* FreeRTOS与中断服务函数有关的配置选项 */
- /***************************************************************************************************************/
- #define xPortPendSVHandler PendSV_Handler
- #define vPortSVCHandler SVC_Handler
原来的程序是STM32裸机开发,程序是顺序执行,可能使用中断,对于电池电量收集、IMU数据采集、超声波检测等功能都是顺序执行,采用频率控制各个“进程”,即在while循环中使用函数获取程序当前时间,减去上一时刻时间,当差值大于某一值时,运行相应函数,如下图所示。
而在改进后的程序中,采用实时系统,使用多线程的方式管理这些功能,为每个功能创建一个任务(线程),根据任务调度机制运行,如下图所示。
并为各个任务设置优先级,move_base是最基本的移动任务优先级最高。
各个任务之间通过队列进行沟通,不然move_base任务的优先级最高,则会一直执行,其他任务得不到执行时间。
在move_base任务中以电池电量采集任务的执行为例:
代码逻辑:当执行到这部分代码时,判断电池队列的句柄是否为NULL(之前创建了,所以不为空),然后使用xQueueReceive函数进行队列数据采集,从Battery_Queue队列中将数据采集到Receive_vattery_volt变量中,刚开始采集时,队列中肯定是没数据的,这是就会进入队列阻塞,阻塞时间为portMAX_DELAY直到队列中有数据,这时move_base任务则会挂起,低优先级的任务会执行,当执行到电池电量采集任务时,向队列中放入了数据,move_base任务则会抢占,继续执行,然后将采集到的数据发给上位机。
其他任务都是这个逻辑,使用这种方法的好处就是提高了我们程序的效率,使得STM32程序多线程执行,并且提供了很多接口,方便用户修改和阅读。
FreeRTOS在STM32F103上的移植完结,整理来说FreeRTOS还是挺简单的,不过很多知识没用到我也就没仔细学,任务之间的同步互斥与沟通有很多方式,除了队列还有信号量、互斥量等等,我只是选择了最常用的一种。
多看看大佬的程序还是有好处的,深受启发。