这里不再讲如何使用CubeMX结合VScode进行开发,只讲如何移植Free RTOS并在VScode中进行开发使用

首先要有一份Free RTOS官网下载下来的源码,这里使用的是FreeRTOSv202406.04-LTS

下图是Free RTOS的总图:

可见Free RTOS中重要的.c和.h文件都在FreeRTOS-Kernel这个文件中

接下来进行移植即可

首先先在自己原来的工程中创建一个文件夹,比如我这里新建了一个FreeRTOS文件夹来存放移植Free RTOS相关的文件

之后在创建的FreeRTOS文件中再新建两个文件分别叫做Inc和Src,用来存放源文件和头文件

之后选中FreeRTOS-Kernel中的所有7个的.c文件复制粘贴到自己工程文件中的FreeRTOS/Src文件中

这里的port.c和heap_4是之后粘贴的这里不用管

之后找到FreeRTOSv202406.04-LTS\FreeRTOS-LTS\FreeRTOS\FreeRTOS-Kernel\include文件夹,将其中的所有文件复制粘贴到自己工程文件中的FreeRTOS/Inc文件中

之后找到FreeRTOSv202406.04-LTS\FreeRTOS-LTS\FreeRTOS\FreeRTOS-Kernel\portable文件,这里使用的是GCC编译器所以此处选择GCC文件,进入之后由于本次移植使用的是STM32F103C8T6开发板使用的是Cotex-M3的芯片所以选择ARM-CM3,之后将文件夹中的port.c和portmacro.h文件分别复制粘贴到FreeRTOS的Inc和Src文件中。

一般情况下FreeRTOS里的内核对象都是动态创建的。既然是动态创建,那么就需要动态地分配内存来存储它们,而且一旦销毁内核对象,就要动态地释放掉之前分配的内存。

FreeRTOS为我们提供了5种可选的堆内存管理策略,分别是heap_1、heap_2、heap_3、heap_4和heap_5。

这里使用的是heap_4,将FreeRTOSv202406.04-LTS\FreeRTOS-LTS\FreeRTOS\FreeRTOS-Kernel\portable\MemMang\heap_4.c文件复制粘贴到FreeRTOS/Src

最后是Free RTOS的配置文件:FreeRTOSConfig.h

在FreeRTOSv202406.04-LTS\FreeRTOS-LTS\FreeRTOS\FreeRTOS-Kernel\examples\template_configuration\FreeRTOSConfig.h

因为FreeRTOSConfig.h文件是配置文件,所以将FreeRTOSConfig.h复制粘贴到工程文件中的Core\Inc

最后移植完成的工程文件是这样的

1.工程文件

2.Core/Inc文件

3.FreeRTOS文件

4.FreeRTOS/Inc文件

5.FreeRTOS/Src文件

至此文件移植工作已完成

配置文件(FreeRTOSConfig.h)修改

接下来是配置文件(FreeRTOSConfig.h)修改工作

配置项1:configCPU_CLOCK_HZ - 内核的运行频率

这个配置项用来告诉FreeRTOS你的单片机的内核频率是多少,你必须把实际情况向FreeRTOS如实上报。比如这里使用的STM32F103C8T6的最大频率是72MHz,这里选择72MHz,在FreeRTOSConfig.h文件中的第54行将20000000改为72000000

配置项2:configTICK_TYPE_WIDTH_IN_BITS - 设置TickType_t类型的位宽

configTICK_TYPE_WIDTH_IN_BITS用来设置TickType_t类型的位宽

这里的数据位宽其实只是针对“整数”而言的。在计算机里有很多种数据类型可以表示整数,比如uint8_t(无符号8位整型)、uint16_t(无符号16位整型)等,它们的区别在于所使用的比特位的数量不同。比如,一个uitn8_t占8个比特,这里的数字8指的就是这种数据类型的位宽。说白了,位宽就是用多少个比特位表示一个整数,当然位宽越大数字能表示的范围就越大。比如,uint8_t所能表示的数字范围是0~255,uint16_t所能表示的数字范围是0~65535等。

因此,通过configTICK_TYPE_WIDTH_IN_BITS来设置TickType_t类型的位宽,其实就是选择用多少位二进制数来表示TickType_t。

另外一个问题,什么是TickType_t呢?它其实是用来记录FreeRTOS的系统节拍的数据类型,说白了就是用来记录时间的。之前学习STM32的HAL库编程的时候,我们知道可以使用HAL_GetTick()获取单片机的当前时间,使用HAL_Delay()进行延迟。这些都是跟时间相关的函数。同样,在FreeRTOS内部也维护了一个时间值,也有一套函数用来获取当前时间或者延迟。

可以看到,在FreeRTOS里我们使用xTaskGetTickCount()来获取单片机的当前时间,使用vTaskDelay()进行延迟。FreeRTOS获取时间的方式与HAL库一模一样。为了记录当前时间FreeRTOS也声明了一个xTickCount变量。它以ms为单位,每次进入SYSTICK中断的时候xTickCount的值自动递增1,因此它的值就等于FreeRTOS运行的时间。所以,调用xTaskGetTickCount()获取系统时间其实就是直接读取xTickCount的值

这里需要特别注意,用来记录当前时间的xTickCount的类型是TickType_t,而我们正在学习的configTICK_TYPE_WIDTH_IN_BITS就是用来设置这个数据类型的位宽的。相信到了这里大家应该明白了,这个配置项其实就是在设置用多少位比特位来表示当前的时间

但是设置不同的位宽到底有什么区别呢?这就要讲到“时间戳回绕”的问题。

什么是“时间戳回绕”呢,其实就是用来记录时间的变量递增到最大后,从0开始重新计数。

比如uint16_t的计数范围是0~65535,因此每当xTickCount的值递增到65535就会从0开始重新计数,这个时候就发生了时间戳回绕。可以看出,此时两次回绕之间的时间间隔为65.535秒,约1分钟回绕一次。

在计算机系统里,时间戳回绕往往是一个很棘手的问题,如果处理不当它会让很多以时间为基础运行的代码失效。虽然FreeRTOS早就考虑到了这一点,对时间戳回绕进行了处理,但那会消耗一定的系统性能。因此我们应该尽量减少回绕的次数。怎么做到这一点呢?答案是增加TickType_t类型的位宽

举个例子,如果我们将TickType_t的数据类型设置为uint32_t,那么它的最大计数值将达到2^32-1 = 4,294,967,295,也就是大概每49天回绕一次;如果我们将TickType_t的数据类型设置为uint64_t的话,情况会更乐观,约5亿8千万年回绕一次,这种情况下可以认为永不回绕。

但也并不是位宽越宽越好,应该根据单片机的实际情况选择。对于我们使用的32位单片机来说,它处理32位整数的效率最高,因此从效率来考量应该将TickType_t设置为uint32_t。一般的规则如下:

当然,我们的开发板上使用的STM32F103属于32位单片机,因此选择32位位宽就可以了。

配置项3:configKERNEL_INTERRUPT_PRIORITY - PendSV和SYSTICK的中断优先级

configKERNEL_INTERRUPT_PRIORITY用来设置PendSV中断和SYSTICK中断的优先级。从字面意思看,这个配置项永安里设置FreeRTOS内核中断的优先级。FreeRTOS内核的运行依赖3种中断,它们分别是SVC中断、PendSV中断和SYSTICK中断。SVC中断默认设置最高优先级,剩下的两个中断的优先级由该配置项设置。

请注意,在FreeRTOS里PendSV和SYSTICK的中断优先级都应该被设置为最低。

如刚才所说,我们应该给configKERNEL_INTERRUP_PRIORITY设置最低的优先级。但最低的优先级是多少呢?有的同学可能会说15。确实如此,但又不完全正确,这里我们要填写优先级的原始值。

我们开发板上的单片机使用的是Cortex-M3内核,它使用8位二进制数来表示中断的优先级大小。但是具体到STM32F103RCT6这颗单片机,它用到了其中的高4位。如下图:

并且,数字越小、优先级越高。比如,对于我们的单片机来说,(15<<4)表示最低优先级,(0 << 4)表示最高优先级。因此,这个配置项应该填写 (15<<4)。

如下图所示将FreeRTOSConfig.h文件中的第311行修改为(15 << 4)

配置项4:configMAX_SYSCALL_INTERRUPT_PRIORITY - 能够调用FreeRTOS API的最高优先级

配置项configMAX_SYSCALL_INTERRUPT_PRIORITY用来设置能够调用FreeRTOS API的最高中断等级。这个概念可能比较抽象,接下来我们通过一个实际的例子来解释它的含义。

如下图,这是一个STM32F103RCT6单片机的例子。这种单片机的中断一共有15级优先级,数字越大优先级越低,数字越小优先级越高。因此我们把优先级最低的0画在了最上边。

根据之前学习的STM32的知识我们知道,中断一般用来处理比较紧急的事情,而且越紧急的事情分配的优先级就越高。

在编写基于FreeRTOS的单片机程序的时候,我们习惯上使用configMAX_SYSCALL_INTERRUPT_PRIORITY对所有16级优先级进行划分。现在我们把configMAX_SYSCALL_INTERRUPT_PRIORITY设置成5,这样就把16级优先级一分为二了。

黄色部分表示优先级比configMAX_SYSCALL_INTERRUPT_PRIORITY高的中断。我们用这些中断处理一些非常紧急的代码,比如对时间非常敏感的电机控制代码。因为这些中断所处理的代码对实时性的要求非常高,所以不希望被别的什么东西干扰,甚至包括FreeRTOS内核。因此,这些中断里边是不能调用FreeRTOS的任何API(实际原因是因为FreeRTOS的临界区,这里就不详细介绍了)。

绿色部分部分表示优先级小于或等于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断。这些中断优先级较低,因此被FreeRTOS干扰一下也没啥事(实际还是因为临界区),所以你可以在这些中断里边调用FreeRTOS的API。

当然,configMAX_SYSCALL_INTERRUPT_PRIORITY的值可以根据系统设计的要求来灵活调整,如果没有什么特别要求的话我们把它设置成(5<<4)。

如下图所示将FreeRTOSConfig.h文件中的第319行修改为(5 << 4)

配置项5:configUSE_TIME_SLICING - 是否使用时间片

configUSE_TIM_SLICING用来设置是否使用时间片,英文单词slicing是切片的意思。

FreeRTOS的调度器有3种可选的调度算法,分别是:不带时间片的抢占式调度、带时间片的抢占式调度和协作式调度。同样使用抢占式调度的前提下,如果configUSE_TIM_SLICING等于1,那就是带时间片的抢占式调度;如果configUSE_TIM_SLICING等于0,就是不带时间片的抢占式调度。在一般情况下,我们习惯带时间片,所以需要把configUSE_TIM_SLICING的值设置成1。

如下图所示将FreeRTOSConfig.h文件中的第90行修改为1

安装中断服务程序

FreeRTOS的运行离不开3种中断:PendSV中断、Systick中断和SVC中断。它们对于FreeRTOS内核的运行都有至关重要的作用,比如SVC中断负责启动第一个任务,PendSV中断负责任务的上下文切换,Systick中断负责产生时间片和维护系统节拍。如上图,FreeRTOS在port.c文件里为每个中断都准备了一个函数,它们分别是xPortPendSVHandler()、xPortSystickHandler()和vPortSVCHandler(),它们分别用来执行诸如任务上下文切换、产生时间片、产生系统节拍等功能。当我们移植FreeRTOS的时候,需要把这3个函数直接安装在单片机的中断向量表里,以确保每当中断发生时这些函数能够被调用。

如下图所示将startup_stm32f103rctx.s中进行下列修改

使用其它定时器作为HAL库的时间基准

才我们已经把中断向量表里原有的中断服务程序替换成了FreeRTOS提供的函数,其中就包括了Systick定时器的中断服务程序。之前我们介绍过,HAL库要使用Systick中断来递增当前时间,代码如下:

// @作用:这是SYSTICK定时器的中断服务程序

void SysTick_Handler(void)

{

HAL_IncTick(); // 每次进入中断递增uwTick的值

}

如果我们用FreeRTOS提供的xPortSystickHandler()替换掉了原有的Systick_Handler(),那么是不是就意味着HAL库的计时功能失效了呢?事实确实如此,一旦我们为FreeRTOS安装了中断服务程序,HAL库里和时间有关的函数就都不能用了。比如,HAL_GetTick()、 HAL_Delay(),甚至一切带超时时间的函数都将失去效果。

如果我既想使用FreeRTOS提供的函数,又想HAL库提供的函数,那该怎么办?当然,你可以让FreeRTOS和HAL库共享Systick,但一般基于效率考虑我们会让HAL库让出Systick定时器,因为共用Systick会略微降低FreeRTOS的运行效率。让出Systick之后,HAL库可以改用其它的定时器。

通过STM32CubeIDE的图形化配置让HAL库让出Systick定时器

STM32F103单片机除了Systick之外,还能使用别的定时器来自增时间值,比如众多通用定时器:TIM1、TIM2、TIM3等等。本次我们使用TIM4来替代Systick。

.c和.h编译环境配置

而使用CubeMX+VScode开发STM32还要为之前添加的头文件和源文件做好配置

在左侧Cmake文件夹中新建一个User文件夹并在User文件夹下新建CMakeLists.txt文件用来配置我们新添加的文件

如下图所示左边的是User文件夹中新建的CMakeLists.txt文件,右边的是创建工程时cmake文件夹下stm32cubemx文件中自动生成的CMakeLists.txt文件,将右边自动生成文件对于头文件和源文件的配置复制粘贴到左边User文件夹中新建的CMakeLists.txt文件中,注意要根据自己的文件路径进行修改,并且要将原来的MX改为User。头文件.h直接只需要引入FreeRTOS/Inc即可,而源文件.c需要将FreeRTOS/Src中的每一个.c文件都列出来。

还需要将头文件和源文件有下面这一步(注意要将MX改为User)

做完以上而任务就可以进行编译了,但是在烧录是还会遇到这样一个问题

此处需要将2改为0

原因:

  1. configCHECK_FOR_STACK_OVERFLOW = 2 启用了 FreeRTOS 的栈溢出检测功能(级别 2,使用模式填充检测)

  2. FreeRTOS 内核代码tasks.c)在检测到栈溢出时,会调用 vApplicationStackOverflowHook() 这个函数

  3. 这个函数是 FreeRTOS 要求用户自己实现的回调,它不会帮你自动生成

  4. 如果你没有实现这个函数 → 链接器(Linker)找不到这个符号 → 报错:

    undefined reference to `vApplicationStackOverflowHook'
    

    → 编译/链接失败 → 没有生成 .bin.hex 文件 → 无法烧录

  5. 加上这个函数之后 → 链接器找到了符号定义 → 链接成功 → 生成固件 → 可以烧录

之后就可以正常编译、烧录、调试了

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐