前言

本文面向想要手动在基于 HAL 库的 STM32 开发板上移植 FreeRTOS 的学习者,通过阅读本文,你能够清晰掌握 FreeRTOS 源码整体架构,厘清各文件对应的功能作用,方便反复实操练习手动移植流程。

本文内容整理自正点原子 FreeRTOS 配套教学视频,文中会附上完整移植代码与原视频链接,配套资料可辅助大家对照实操。视频实操硬件为 STM32F429 阿波罗 V2 开发板,适配 STM32F4 系列芯片;若使用 STM32F103 等其他型号开发板,直接照搬视频步骤会出现编译或运行报错。建议初次移植时跟随视频配套硬件操作,先吃透通用移植思路,熟练后再自行适配 STM32F103 等其他芯片。

文中提供的全部代码均与教学视频完全对应,实操时可快速对照排查问题、降低调试难度;完整实操 1~2 遍熟悉流程后,便可自主更换其他型号芯片完成移植练习。另外,部分开发者仍在使用标准库开发,但推荐大家优先掌握 HAL 库开发。ST 官方主推 STM32CubeMX 工具,可一键自动生成 HAL 库初始化代码,大幅缩短开发周期,在多芯片适配、项目快速迭代的量产场景中优势突出。

视频链接: 【【正点原子】手把手教你学FreeRTOS实时系统【真人出镜】FreeRTOS实时系统视频教程 单片机 嵌入式】

程序代码及其资料: 程序代码及其资料

1、获取Free RTOS源码(熟悉)

Free RTOS官网: https://www.freertos.org/

正点原子资料: 正点原子资料下载中心 -> 开发板A盘资料 -> 路径:(A盘) -> 6,软件资料 -> 13/14,FreeRTOS学习资料

简介Free RTOS的源码内容:

名称 描述
FreeRTOS FreeRTOS 内核
FreeRTOS-Plus FreeRTOS 组件
tools 工具
GitHub-FreeRTOS-Home FreeRTOS 的 GitHub 仓库链接
Quick_Start_Guide 快速入门指南官方文档链接
Upgrading-to-FreeRTOS-xxx 升级到指定 FreeRTOS 版本官方文档链接
History.txt FreeRTOS 历史更新记录
其他 其他

这里面最重要的是Free RTOS文件夹,这是移植的重点,而FreeRTOS-Plus提供的组件我们一般都不用它提供的而是取用第三方的。下面是Free RTOS文件夹里的内容,其中的Demo(即演示示例)比较有参考意义,以及Source(源码)最重要。

名称 描述
Demo FreeRTOS 演示例程
License FreeRTOS 相关许可
Source FreeRTOS 源码
Test 公用以及移植层测试代码

Dome文件夹是官方提供的Free RTOS演示例程,从中可以发现,支持多种芯片架构并且支持多种不同型号芯片,对学习Free RTOS非常有帮助。如下所示:

下面是Source文件夹,里面是Free RTOS源码本尊,其中最重要的是include和portable文件夹,还有list.c,queue.c以及tasks.c;其他的都不是很重要可以根据情况添加。

名称 描述
include 内包含了 FreeRTOS 的头文件
portable 内包含了 FreeRTOS 的移植文件
croutine.c 协程相关文件
event_groups.c 事件相关文件
list.c 列表相关文件
queue.c 队列相关文件
stream_buffer.c 流式缓冲区相关文件
tasks.c 任务相关文件
timers.c 软件定时器相关文件

这里面着重介绍一下portable文件夹,FreeRTOS操作系统归根到底是一个软件层面的东西,那FreeRTOS是如何跟硬件联系在一起的呢?(portable文件夹里面的东西就是连接桥梁)

可以看到里面的东西是很多的,但实际上我们是用不到这么多的,主要是根据我们的硬件和编译器来选择的。由于使用MDK开发,因此这里只重点介绍其中的部分移植文件,下面是经过选择后需要的三个重要文件夹:

名称 描述
Keil 指向 RVDS 文件夹
RVDS 不同内核芯片的移植文件
MemMang 内存管理文件

以前 Keil 文件夹里有 ARM_CM3、ARM_CM4F 这些按芯片架构分的子文件夹,里面是 port.c 和 portmacro.h 这些移植核心文件。现在这些内容被移到了 portable/RVDS 文件夹下,结构和原来的 Keil 文件夹几乎一样,比如 ARM_CM3 文件夹还是在 RVDS 下面,文件也没变。官方这么调整主要是为了统一编译器相关的移植代码存放路径。

2、Free RTOS手把手移植(掌握)

移植准备
  1. Free RTOS源码
  1. 基础工程:由于后续实验需使用LED、LCD、定时器、内存管理等等,建议使用HAL库版本的内存管理的实验工程为基础工程进行FreeRTOS的移植
移植步骤
  1. 添加FreeRTOS源码:将FreeRTOS源码添加至基础工程、头文件路径等
  2. FreeRTOSConfig.h:添加FreeRTOSConfig.h配置文件(用来裁剪FreeRTOS的一些功能,以及一些API的使能)
  3. 修改SYSTEM文件:修改SYSTEM文件中的sys.c、delay.c、usart.c(因为这些文件是基于裸机以及UCOS实现的,所以使用Free RTOS是不能直接使用的)
  4. 修改中断相关的文件:修改Systick中断、SVC中断、PendsV中断(这些和任务切换是息息相关的)
  5. 添加应用程序:验证一下移植是否成功
实操
  1. 将基础工程(即实验38)解压,然后更换名字为“FreeRTOS移植”
  1. 进入“FreeRTOS移植”文件内的Middlewares下,新建一个“FreeRTOS”文件夹,并将FreeRTOS源码放入其中

补:其中不是源码的“.gitmodules”可以删掉

  1. 进入其中的“portable”文件夹,根据硬件和编译器把不重要的文件删掉,我用的STM32和Keil,所以保留下面三个。(其实打开 Keil 文件夹后可以看到里面都没有东西了,Keil 文件夹中之后一个文件,文件名为:“See-also-the-RVDS-directory.txt”,看文件名就知道要转到 RVDS 文件夹了)
  1. 打开基础工程,新建两个文件分组,分别为 Middlewares/FreeRTOS_CORE 和Middlewares/FreeRTOS_PORT,如下图所示:
  1. 将刚刚新建的FreeRTOS 目录下所有的 FreeRTOS 的内核 C 源文件(包含"croutine.c" “event_groups.c” “list.c” “queue.c” “stream_buffer.c” “tasks.c” “timers.c” )添加到Middlewares/FreeRTOS_CORE 分组中。Middlewares/FreeRTOS_PORT 分组用于存放 FreeRTOS 内核的移植文件,需要添加两类文件到这个分组,分别为 heap_x.c 和 port 文件。
    heap_x.c,在路径 FreeRTOS/portable/MemMang 下有五个 C 源文件,这五个 C 源文件对应了五种 FreeRTOS 提供的内存管理算法,读者在进行 FreeRTOS 移植的时候可以根据需求选择合适的方法,具体这五种内存管理的算法,在后续 FreeRTOS 内存管理部分会具体分析,这里就先使用 heap_4.c,将 heap_4.c 添加到 Middlewares/FreeRTOS_PORT 分组中。
    port 文件,port 文件是 FreeRTOS 这个软件与 MCU 这个硬件连接的桥梁,因此对于STM32 系列不同的开发板,所使用的 port 文件是不同的。port 文件的路径在FreeRTOS/portable/RVDS 或 FreeRTOS/portable/GCC (这个我们刚才删了)下。进入到 FreeRTOS/portable/RVDS 或FreeRTOS/portable/GCC,可以看到 FreeRTOS 针对不同的 MCU 提供了不同的 port 文件,具体的 STM32 系列开发板与不同 port 文件的对应关系如下表所示(我的是STM32F103所以选ARM_CM3):
  1. 添加一下头文件,添加一下“FreeRTOS/include”和“FreeRTOS/portable/RVDS”的port 文件的路径
  1. 添加 FreeRTOSConfig.h 文件到项目的“User”文件夹下,FreeRTOSConfig.h 是 FreeRTOS 操作系统的配置文件,FreeRTOS 操作系统是可裁剪的,用户可以根据需求对 FreeRTOS 进行裁剪,裁剪掉不需要用到的 FreeRTOS 功能,以此来节约 MCU
    中寸土寸金的内存资源。
    添加途径有三种:
  • 根据需求,自行编写,可以参考FreeRTOS 官网的在线文档中就详细地对 FreeRTOSConfig.h 中各个配置项进行了描述,网页链接: https://www.freertos.org/a00110.html 。(对新手非常不友好)
  • FreeRTOS 内核的演示工程,即Demo 文件夹内的示例(但有些演示工程使用的是老版本的 FreeRTOS,部分演示工程的 FreeRTOSConfig.h 文件并不能够很好的适用于新版本的 FreeRTOS)
  • 使用正点原子的 STM32 系列开发板编写的FreeRTOSConfig.h文件(因为教程用的这个,这个FreeRTOSConfig.h文件为了适用于学习FreeRTOS,并没有对 FreeRTOS 的功能作过多的裁剪,在后续的部分实验中还需要对 FreeRTOSConfig.h 文件作相应的修改,以满足实验的需求。)
8. 修改 SYSTEM 文件,SYSTEM 文件夹中的文件一开始是针对 µC/OS 编写的,因此使用 FreeRTOS 的话,就需要作相应的修改。SYSTEM 文件夹中一共需要修改三个文件,分别是 sys.h、usart.c、delay.c。
8.1. sys.h 文件;将其中的宏 SYS_SUPPORT_OS 定义为 1 注意:是sys.h文件,不要搞错了,可以从sys.c文件跳转过去。
/**
* SYS_SUPPORT_OS 用于定义系统文件夹是否支持 OS
* 0,不支持 OS
* 1,支持 OS
*/
#define SYS_SUPPORT_OS 1
8.2. usart.c 文件;一共有两个地方需要修改,首先就是串口的中断服务函数,原本在使用 µC/OS 的时候,进入和退出中断需要添加 OSIntEnter()和 OSIntExit()两个函数,这是 µC/OS 对于中断的相关处理机制,而 FreeRTOS 中并没有这种机制,因此将这两行代码删除。
STM32F1 系列该部分修改后:
void USART_UX_IRQHandler(void)
{
 HAL_UART_IRQHandler(&g_uart1_handle); /* 调用 HAL 库中断处理公用函数 */
 
 while (HAL_UART_Receive_IT( &g_uart1_handle,
 (uint8_t *)g_rx_buffer,
 RXBUFFERSIZE) != HAL_OK)/* 重新开启中断并接收数据 */
 {
 /* 如果出错会卡死在这里 */
 }
}
STM32F4/G4/F7/H7/H5 系列该部分修改后:
void USART_UX_IRQHandler(void)
{
 uint32_t timeout = 0;
 uint32_t maxDelay = 0x1FFFF;
 
 HAL_UART_IRQHandler(&g_uart1_handle); /* 调用 HAL 库中断处理公用函数 */
 
 timeout = 0;
 while (HAL_UART_GetState(&g_uart1_handle)
 != HAL_UART_STATE_READY) /* 等待就绪 */
 {
 timeout++; /* 超时处理 */
 if(timeout > maxDelay)
 {
 break;
 }
 }
         
 timeout=0;
 
 /* 一次处理完成之后,重新开启中断并设置 RxXferCount 为 1 */
 while (HAL_UART_Receive_IT( &g_uart1_handle,
 (uint8_t *)g_rx_buffer,
 RXBUFFERSIZE) != HAL_OK)
 {
 timeout++;/* 超时处理 */
 if (timeout > maxDelay)
 {
 break;
 }
 }
}
接下来 usart.c 要修改的第二个地方就是导入的头文件,因为在串口的中断服务函数当中已经删除了 µC/OS 的相关代码,并且也没有使用到 FreeRTOS 的相关代码,因此将 usart.c 中包含的关于 OS 的头文件删除,要删除的代码如下所示:
/* 如果使用 os,则包括下面的头文件即可. */
#if SYS_SUPPORT_OS
#include "includes.h" /* os 使用 */
#endif
8.3. delay.c 文件;接下来修改 SYSTEM 文件夹中的最后一个文件——delay.c,delay.c 文件需要改动的地方比较多,大致可分为三个步骤:删除适用于 µC/OS 但不适用于 FreeRTOS 的相关代码、添加FreeRTOS 的相关代码、修改部分内容。
8.3.1. 删除适用于 µC/OS 但不适用于 FreeRTOS 的相关代码;一共需要删除 1 个全局变量、6 个宏定义、3 个函数,这些要删除的代码在使用 µC/OS 的时候会使用到,但是在使用 FreeRTOS 的时候无需使用,需要删除的代码内容如下所示:
/* 定义 g_fac_ms 变量, 表示 ms 延时的倍乘数,
* 代表每个节拍的 ms 数, (仅在使能 os 的时候,需要用到)
*/
static uint16_t g_fac_ms = 0;
/*
* 当 delay_us/delay_ms 需要支持 OS 的时候需要三个与 OS 相关的宏定义和函数来支持
* 首先是 3 个宏定义:
* delay_osrunning :用于表示 OS 当前是否正在运行,以决定是否可以使用相关函数
* delay_ostickspersec :用于表示 OS 设定的时钟节拍,
* delay_init 将根据这个参数来初始化 systick
* delay_osintnesting :用于表示 OS 中断嵌套级别,因为中断里面不可以调度,
* delay_ms 使用该参数来决定如何运行
* 然后是 3 个函数:
* delay_osschedlock :用于锁定 OS 任务调度,禁止调度
* delay_osschedunlock :用于解锁 OS 任务调度,重新开启调度
* delay_ostimedly :用于 OS 延时,可以引起任务调度.q
* 
* 本例程仅作 UCOSII 和 UCOSIII 的支持,其他 OS,请自行参考着移植
*/
/* 支持 UCOSII */
#ifdef OS_CRITICAL_METHOD /* OS_CRITICAL_METHOD 定义了
 * 说明要支持 UCOSII
 */
#define delay_osrunning OSRunning /* OS 是否运行标记,0,不运行;1,在运行 */
#define delay_ostickspersec OS_TICKS_PER_SEC /* OS 时钟节拍,即每秒调度次数 */
#define delay_osintnesting OSIntNesting /* 中断嵌套级别,即中断嵌套次数 */
#endif
/* 支持 UCOSIII */
#ifdef CPU_CFG_CRITICAL_METHOD /* CPU_CFG_CRITICAL_METHOD 定义了
 * 说明要支持 UCOSIII
 */
#define delay_osrunning OSRunning /* OS 是否运行标记,0,不运行;1,在运行 */
#define delay_ostickspersec OSCfg_TickRate_Hz /* OS 时钟节拍,即每秒调度次数 */
#define delay_osintnesting OSIntNestingCtr /* 中断嵌套级别,即中断嵌套次数 */
#endif
/**
* @brief us 级延时时,关闭任务调度(防止打断 us 级延迟)
* @param 无
* @retval 无
*/
static void delay_osschedlock(void)
{
#ifdef CPU_CFG_CRITICAL_METHOD /* 使用 UCOSIII */
 OS_ERR err;
 OSSchedLock(&err); /* UCOSIII 的方式,禁止调度,防止打断 us 延时 */
#else /* 否则 UCOSII */
 OSSchedLock(); /* UCOSII 的方式,禁止调度,防止打断 us 延时 */
#endif
}

/**
* @brief us 级延时时,恢复任务调度
* @param 无
* @retval 无
*/
static void delay_osschedunlock(void)
{
#ifdef CPU_CFG_CRITICAL_METHOD /* 使用 UCOSIII */
 OS_ERR err;
 OSSchedUnlock(&err); /* UCOSIII 的方式,恢复调度 */
#else /* 否则 UCOSII */
 OSSchedUnlock(); /* UCOSII 的方式,恢复调度 */
#endif
}
/**
* @brief us 级延时时,恢复任务调度
* @param ticks: 延时的节拍数
* @retval 无
*/
static void delay_ostimedly(uint32_t ticks)
{
#ifdef CPU_CFG_CRITICAL_METHOD
 OS_ERR err;
 OSTimeDly(ticks, OS_OPT_TIME_PERIODIC, &err); /* UCOSIII 延时采用周期模式 */
#else
 OSTimeDly(ticks); /* UCOSII 延时 */
#endif
}
8.3.2. 添加 FreeRTOS 的相关代码;在使用 ARMClang(AC6)的情况下可跳过这个步骤,而在使用 ARMCC(AC5)的情况下只需要在 delay.c 文件中使用 extern 关键字导入一个 FreeRTOS 函数——xPortSysTickHandler()即可,这个函数是用于处理 FreeRTOS 系统时钟节拍的,本教程是使用 SysTick 作为 FreeRTOS操作系统的心跳,因此需要在 SysTick 的中断服务函数中调用这个函数,因此将代码添加到SysTick 中断服务函数之前,代码修改如下:
补充:注意我们使用的是AC5;AC5(ARM Compiler 5,别名 armcc)和AC6(ARMClang / ARM Compiler 6)都是给单片机(STM32、Cortex-M 内核芯片)写代码用的编译器,就是 MDK-Keil 里把你写的 C 语言翻译成单片机能识别机器码的工具
/*下面这一行代码为要添加的内容*/
extern void xPortSysTickHandler(void);
/**
* @brief systick 中断服务函数,使用 OS 时用到
* @param ticks: 延时的节拍数
* @retval 无
*/

void SysTick_Handler(void)
{
 /* 代码省略 */
}
8.3.3. 修改部分内容;最后要修改的内容包括两个,分别是包含头文件和 4 个函数。首先来看需要修改的 4 个函数,分别是 SysTick_Handler()、delay_init()、delay_us()和delay_ms()
(a) SysTick_Handler()
这个函数是 SysTick 的中断服务函数,由于 FreeRTOS 在针对 ARMClang(AC6)编译器的port 文件中实现了该函数,而在针对 ARMCC(AC5)编译器的 port 文件中没有实现改函数,因此该函数的修改针对实际使用的编译器类型(ARMClang 或 ARMCC)分为两种情况。对于使用 ARMCC(AC5)的情况,只需在这个函数中不断调用上个步骤中导入的函数xPortSysTickHandler(),代码修改后如下所示:
/**
* @brief systick 中断服务函数,使用 OS 时用到
* @param ticks: 延时的节拍数
* @retval 无
*/
void SysTick_Handler(void)
{
 HAL_IncTick();
 /* OS 开始跑了,才执行正常的调度处理 */
 if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
 {
 xPortSysTickHandler();
 }
}
补充: 对于使用 ARMClang(AC6)的情况,由于 port 文件中定义并实现了 SysTick_Handler函数,因此首先需要屏蔽FreeRTOS的portable中GCC目录下对应的port文件中定义和实现的SysTick_Handler函数,该函数在port 文件中的定义和实现如下所示:
/**
* @brief SysTick handler.
*/
void SysTick_Handler( void ) PRIVILEGED_FUNCTION;
void SysTick_Handler( void ) /* PRIVILEGED_FUNCTION */
{
 uint32_t ulPreviousMask;
 
 ulPreviousMask = portSET_INTERRUPT_MASK_FROM_ISR();
 {
 /* Increment the RTOS tick. */

 if( xTaskIncrementTick() != pdFALSE )
 {
 /* Pend a context switch. */
 portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
 }
 }
 portCLEAR_INTERRUPT_MASK_FROM_ISR( ulPreviousMask );
}
对于使用 ARMClang(AC6)的情况,接着参考该 SysTick_Handler 函数的实现,实现 delay.c 文件中的 SysTick_Handler 函数,如下所示:
/**
* @brief systick 中断服务函数,使用 OS 时用到
* @param ticks: 延时的节拍数
* @retval 无
*/
void SysTick_Handler(void)
{
 uint32_t ulPreviousMask;
 
 HAL_IncTick();
 /* OS 开始跑了,才执行正常的调度处理 */
 if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
 {
 ulPreviousMask = portSET_INTERRUPT_MASK_FROM_ISR();
 {
 /* Increment the RTOS tick. */
 if( xTaskIncrementTick() != pdFALSE )
 {
 /* Pend a context switch. */
 portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
 }
 }
 portCLEAR_INTERRUPT_MASK_FROM_ISR( ulPreviousMask );
 }
}
(b) delay_init();函 数 delay_init() 主要用于初始化 SysTick 。 这 里 要 说 明 的 是 , 在 后 续 调 用 函 数vTaskStartScheduler()(这个函数在后面中讲解到 FreeRTOS 任务调度器的时候会具体分析)的时候,FreeRTOS 会按照 FreeRTOSConfig.h 文件的配置对 SysTick 进行初始化,因此 delay_init()函数初始化的 SysTick 主要使用在 FreeRTOS 开始任务调度之前。函数 delay_init()要修改的部分主要为 SysTick 的重装载值以及删除不用的代码,代码修改如下:
STM32F1 系列:
void delay_init(uint16_t sysclk)
{
#if SYS_SUPPORT_OS
 uint32_t reload;
#endif
 SysTick->CTRL = 0;
 HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK_DIV8);
 
 g_fac_us = sysclk / 8;
#if SYS_SUPPORT_OS
 reload = sysclk / 8;
 /* 使用 configTICK_RATE_HZ 计算重装载值
 * configTICK_RATE_HZ 在 FreeRTOSConfig.h 中定义
 */
 reload *= 1000000 / configTICK_RATE_HZ;
 /* 删除不用的 g_fac_ms 相关代码 */
 SysTick->CTRL |= 1 << 1;
 SysTick->LOAD = reload;
 SysTick->CTRL |= 1 << 0;
#endif
}
STM32F4/G4/F7/H7/H5 系列:
void delay_init(uint16_t sysclk)
{
#if SYS_SUPPORT_OS
 uint32_t reload;
#endif
 HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
 g_fac_us = sysclk;
 
#if SYS_SUPPORT_OS
 reload = sysclk;
 /* 使用 configTICK_RATE_HZ 计算重装载值
 * configTICK_RATE_HZ 在 FreeRTOSConfig.h 中定义
 */
 reload *= 1000000 / configTICK_RATE_HZ;
 /* 删除不用的 g_fac_ms 相关代码 */
 SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk;
 SysTick->LOAD = reload;
 SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
#endif 
}
可以看到STM32F1 系列的函数delay_init()将 SysTick 的时钟频率设置为 CPU 时钟频率的 1/8,而 STM32F4/G4/F7/H7/H5 系列的函数 delay_init()则将 SysTick 的时钟频率设置为与 CPU 相同的时钟频率,由于 FreeRTOS 在配置 SysTick 时,并不会配置 SysTick 的时钟源,因此这将导致STM32F1 系列与 STM32F4/G4/F7/H7/H5 系列的 FreeRTOSConfig.h 文件有所差异,这是读者在使用正点原子提供的 FreeRTOSConfig.h 文件时需要注意的地方。
(c)delay_us()
函数 delay_us()用于微秒级的 CPU 忙延时,原本的函数 delay_us()延时的前后加入了自定义函数 delay_osschedlock()和 delay_osschedunlock()用于锁定和解锁 µC/OS 的任务调度器,以此来让延时更加准确。在 FreeRTOS 中可以不用加入这两个函数,但是要注意的是,这会让函数delay_us()的微秒级延时的精度有所下降,函数 delay_us()修改后的代码如下所示:
void delay_us(uint32_t nus)
{
 uint32_t ticks;
 uint32_t told, tnow, tcnt = 0;
 uint32_t reload = SysTick->LOAD;
 ticks = nus * g_fac_us;
 /* 删除适用于 µC/OS 用于锁定任务调度器的自定义函数 */
 told = SysTick->VAL;
 while (1)
 {
 tnow = SysTick->VAL;
 if (tnow != told)
 {
 if (tnow < told)
 {
 tcnt += told - tnow;
 }
 else
 {
 tcnt += reload - tnow + told;
 }
 told = tnow;
 if (tcnt >= ticks)
 {
 break;
 }
 }
 }
 /* 删除适用于 µC/OS 用于解锁任务调度器的自定义函数 */
}
(d) delay_ms()
函数 delay_ms()用于毫秒级的 CPU 忙延时,原本的函数 delay_ms()会判断 µC/OS 是否运行,如果 µC/OS 正在运行的话,就使用 µC/OS 的 OS 延时进行毫秒级的延时,否则就调用函数delay_us()进行毫秒级的 CPU 忙延时。在 FreeRTOS 中,可以将函数 delay_ms()定义为只进行CPU 忙延时,当需要 OS 延时的时候,调用 FreeRTOS 提供的 OS 延时函数 vTaskDelay()(在下文讲解 FreeRTOS 时间管理的时候会对此函数进行分析)进行系统节拍级延时,函数 delay_ms()修改后的代码如下所示:
void delay_ms(uint16_t nms)
{
     uint32_t i;
     
     for (i=0; i<nms; i++)
     {
     delay_us(1000);
     }
}
(e) 包含头文件
根据上述步骤的修改,delay.c 文件中使用到了 FreeRTOS 的相关函数,因此就需要在 delay.c文件中包含 FreeRTOS 的相关头文件,并且移除掉原本存在的 µC/OS 相关头文件。先看一下修改前 delay.c 文件中包含的 µC/OS 相关的头文件:
/* 添加公共头文件 ( ucos 需要用到) */
#include "includes.h
修改成如下内容:
/* 添加公共头文件 (FreeRTOS 需要用到) */
#include "FreeRTOS.h"
#include "task.h"
9. 修改中断相关文件;在 FreeRTOS 的移植过程中会这几到三个重要的中断,分别是 FreeRTOS 系统时基定时器的中断(SysTick 中断)、SVC 中断、PendSV 中断(SVC 中断和 PendSV 中断在下文讲解 FreeRTOS中断和 FreeRTOS 任务切换的时候会具体分析),这三个中断的中断服务函数在 HAL 库提供的文件中都有定义,对于不同的 STM32 开发板,对应了不同的文件,具体对应关系如下表所示:
其中,SysTick 的中断服务函数在 delay.c 文件中已经定义了,并且 FreeRTOS 也提供了 SVC和 PendSV 的中断服务函数,因此需要将 HAL 库提供的这三个中断服务函数注释掉,这里采用宏开关的方式让 HAL 库中的这三个中断服务函数不加入编译,使用的宏在 sys.h 中定义,因此还需要导入 sys.h 头文件,根据上图找到对应的文件进行修改,修改后的代码如下所示:
/* 仅展示修改部分,其余代码未修改、不展示 */
/* Includes --------------------------------------*/
/* 导入 sys.h 头文件 */
#include "./SYSTEM/SYS/sys.h"
/**
 * @brief This function handles SVCall exception.
 * @param None
 * @retval None
 */
/* 加入宏开关 */
#if (!SYS_SUPPORT_OS)
void SVC_Handler(void)
{
}
#endif
/**
 * @brief This function handles PendSVC exception.
 * @param None
 * @retval None
 */
/* 加入宏开关 */
#if (!SYS_SUPPORT_OS)
void PendSV_Handler(void)
{
}
#endif
/**
 * @brief This function handles SysTick Handler.
 * @param None
 * @retval None
 */
/* 加入宏开关 */
#if (!SYS_SUPPORT_OS)
void SysTick_Handler(void)
{
 HAL_IncTick();
}
#endif
最后,也是移植 FreeRTOS 要修改的最后一个地方,FreeRTOSConfig.h 文件中有如下定义:
#define configPRIO_BITS __NVIC_PRIO_BITS
补充:这个FreeRTOSConfig.h需要编译一下才会出现,编译后在Middlewares/FreeRTOS_CORE的croutine.c文件下面
对于这个宏定义,在下文讲解到 ARM Corten-M 和 FreeRTOS 中断的时候会具体分析。可以看到,这个宏定义将 configPRIO_BITS 定义成__NVIC_PRIO_BITS,而__NVIC_PRIO_BITS在 HAL 库中有相关定义,对于不同的 STM32 开发板,__NVIC_PRIO_BITS 定义在不同的文件中,具体的对应关系如下表所示:
找到那个宏后跳转到对应的文件进行修改。虽然不同类型的开发板对应的文件不同,但是__NVIC_PRIO_BITS 都被定义成了相同的值,如下所示: 补充:右键宏“__NVIC_PRIO_BITS”进行跳转时,无法跳转,显示如下信息:
此时请点击魔术棒,选择output选项卡,并将Browse Infommation勾选上,然后将项目全部编译。
之后右键“__NVIC_PRIO_BITS”跳转就能直接找到
#define __NVIC_PRIO_BITS 4U
这个值是正确的,但是如果将__NVIC_PRIO_BITS 定义成 4U 的话,在编译 FreeRTOS 工程的时候,Keil 会报错,具体的解决方法就是将 4U 改成 4,代码修改后如下所示:
#define __NVIC_PRIO_BITS 4
到此为止,FreeRTOS 就移植完毕了,可能刚开始感觉会很繁琐,但后续多次练习就好了。
10. (可选步骤,建议完成)这个步骤是可选的,但是笔者强烈建议读者完成,因为在后续实验中会使用到,并且规范工程。本小节可分为 3 个小部分,分别为修改工程目标名、移除 USMART 调试组件、添加定时器驱动。
10.1. 修改工程目标名称
本教程是以标准例程-HAL 库版本的内存管理实验工程为基础工程,内存管理实验工程的工程目标名为“MALLOC”,为了规范工程,建议将工程目标名修改为“FreeRTOS”或根据读者的实际场景进行修改,修改如下图所示:
10.2. 移除 USMART 调试组件
由于本教程并未使用到 USMART 调试组件,因此建议将 USMART 调试组件从工程中移除,如果读者需要使用 USMART 调试组件的话,也可选择保留,移除 USAMRT 调试组建后工程文件分组如下图所示:
补充:注意要去User/main.c文件里把“#include "./USMART/usmart.h"”头文件以及“usmart_dev.init(90);/*初始化*/”删了
10.3. 添加定时器驱动
由于在后续的实验中需要使用到 STM32 的基本定时器外设,因此需要向工程中添加定时器的相关驱动文件,读者也可在后续实验需要用到定时器的时候再进行添加。定时器的相关驱动文件也就是在“实验7 基本定时器中断实验”里面找来将文件添加到工程的 Drivers/BSP 文件分组中,如下图所示:
11. (可选步骤,可不完成)添加应用程序,这个是可以完全不做的,因为那个视频做了,我就跟着记录了一下。
去“D:\【正点原子】MiniSTM32F103开发板V4资料-(A盘)\4,程序源码\3,扩展例程\2,FreeRTOS例程\FreeRTOS实验例程2 FreeRTOS移植实验\User”找的“freertos_demo.c”和“freertos_demo.h”文件复制到我们修改的项目“FreeRTOS例程”的User文件夹内,然后在Keil中添加“freertos_demo.c”。
然后将“freertos_demo.c”的头文件“#include "freertos_demo.h"”添加到main.c文件中
main.c里的主循环1和定义的一些变量直接全删了,反正也没用到,删了后在后面再添加一个“freertos_demo();”,具体main.c代码如下:
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/SDRAM/sdram.h"
#include "./MALLOC/malloc.h"
#include "freertos_demo.h"

const char *SRAM_NAME_BUF[SRAMBANK] = {" SRAMIN  ", " SRAMCCM ", " SRAMEX  "};

int main(void)
{

    HAL_Init();                                 /* 初始化HAL库 */
    sys_stm32_clock_init(360, 25, 2, 8);        /* 设置时钟,180Mhz */
    delay_init(180);                            /* 延时初始化 */
    usart_init(115200);                         /* 串口初始化为115200 */
    led_init();                                 /* 初始化LED */
    key_init();                                 /* 初始化按键 */
    sdram_init();                               /* SRAM初始化 */
    lcd_init();                                 /* 初始化LCD */

    my_mem_init(SRAMIN);                        /* 初始化内部内存池 */
    my_mem_init(SRAMEX);                        /* 初始化外部内存池 */
    my_mem_init(SRAMCCM);                       /* 初始化CCM内存池 */
    
    lcd_show_string(30,  50, 200, 16, 16, "STM32", RED);
    lcd_show_string(30,  70, 200, 16, 16, "MALLOC TEST", RED);
    lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "KEY0:Malloc & WR & Show", RED);
    lcd_show_string(30, 130, 200, 16, 16, "KEY_UP:SRAMx KEY1:Free", RED);
    lcd_show_string(60, 160, 200, 16, 16, " SRAMIN ", BLUE);
    lcd_show_string(30, 176, 200, 16, 16, "SRAMIN   USED:", BLUE);
    lcd_show_string(30, 192, 200, 16, 16, "SRAMCCM  USED:", BLUE);
    lcd_show_string(30, 208, 200, 16, 16, "SRAMEX   USED:", BLUE);
    
    freertos_demo();
}
简单修改一下“freertos_demo.c”文件,将其中没有的屏幕显示信息删了,然后效果换成两个灯闪烁,修改了一下两个灯的延时用于区别效果,完整如下:
#include "freertos_demo.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
/*FreeRTOS*********************************************************************************************/
#include "FreeRTOS.h"
#include "task.h"

/******************************************************************************************************/
/*FreeRTOS配置*/

/* START_TASK 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define START_TASK_PRIO 1                   /* 任务优先级 */
#define START_STK_SIZE  128                 /* 任务堆栈大小 */
TaskHandle_t            StartTask_Handler;  /* 任务句柄 */
void start_task(void *pvParameters);        /* 任务函数 */

/* TASK1 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define TASK1_PRIO      2                   /* 任务优先级 */
#define TASK1_STK_SIZE  128                 /* 任务堆栈大小 */
TaskHandle_t            Task1Task_Handler;  /* 任务句柄 */
void task1(void *pvParameters);             /* 任务函数 */

/* TASK2 任务 配置
 * 包括: 任务句柄 任务优先级 堆栈大小 创建任务
 */
#define TASK2_PRIO      3                   /* 任务优先级 */
#define TASK2_STK_SIZE  128                 /* 任务堆栈大小 */
TaskHandle_t            Task2Task_Handler;  /* 任务句柄 */
void task2(void *pvParameters);             /* 任务函数 */

/******************************************************************************************************/

/* LCD刷屏时使用的颜色 */
uint16_t lcd_discolor[11] = {WHITE, BLACK, BLUE, RED,
                             MAGENTA, GREEN, CYAN, YELLOW,
                             BROWN, BRRED, GRAY};

/**
 * @brief       FreeRTOS例程入口函数
 * @param       无
 * @retval      无
 */
void freertos_demo(void)
{
    
    xTaskCreate((TaskFunction_t )start_task,            /* 任务函数 */
                (const char*    )"start_task",          /* 任务名称 */
                (uint16_t       )START_STK_SIZE,        /* 任务堆栈大小 */
                (void*          )NULL,                  /* 传入给任务函数的参数 */
                (UBaseType_t    )START_TASK_PRIO,       /* 任务优先级 */
                (TaskHandle_t*  )&StartTask_Handler);   /* 任务句柄 */
    vTaskStartScheduler();
}

/**
 * @brief       start_task
 * @param       pvParameters : 传入参数(未用到)
 * @retval      无
 */
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();           /* 进入临界区 */
    /* 创建任务1 */
    xTaskCreate((TaskFunction_t )task1,
                (const char*    )"task1",
                (uint16_t       )TASK1_STK_SIZE,
                (void*          )NULL,
                (UBaseType_t    )TASK1_PRIO,
                (TaskHandle_t*  )&Task1Task_Handler);
    /* 创建任务2 */
    xTaskCreate((TaskFunction_t )task2,
                (const char*    )"task2",
                (uint16_t       )TASK2_STK_SIZE,
                (void*          )NULL,
                (UBaseType_t    )TASK2_PRIO,
                (TaskHandle_t*  )&Task2Task_Handler);
    vTaskDelete(StartTask_Handler); /* 删除开始任务 */
    taskEXIT_CRITICAL();            /* 退出临界区 */
}

/**
 * @brief       task1
 * @param       pvParameters : 传入参数(未用到)
 * @retval      无
 */
void task1(void *pvParameters)
{
    uint32_t task1_num = 0;
    
    while(1)
    {

        LED0_TOGGLE();                                                  /* LED0闪烁 */
        vTaskDelay(1000);                                               /* 延时1000ticks */
    }
}

/**
 * @brief       task2
 * @param       pvParameters : 传入参数(未用到)
 * @retval      无
 */
void task2(void *pvParameters)
{
    float float_num = 0.0;
    
    while(1)
    {
        LED1_TOGGLE();                                                  /* LED1闪烁 */
        vTaskDelay(500);                           										/* 延时1000ticks */
    }
}
补充:
想到了一个一篇文章里面简单介绍了一些裸机多任务处理的方案,像对于这种简单的多任务可以使用前后台 if else 标志位轮询的方法,如需要实现两个不同频率的 led 闪烁任务,此时可以考虑使用前后台。例如下面这段代码,前台主循环不断轮询标志位,后台中断触发标志位,从而实现两个互不干扰的不同周期 led 闪烁任务。
// 任务标志位
uint8_t flag_led1 = 0;
uint8_t flag_led2 = 0;
uint8_t flag_button = 0;

int main(void){
    while(1){
        // led1 闪烁任务,100ms 翻转一次
        if(flag_led1){
            // 翻转引脚电平
            gpio_toggle(led1_pin);
            // 清除标志位
            flag_led1 = 0;
        }

        // led2 闪烁任务,20ms 翻转一次
        if(flag_led2){
            // 翻转引脚电平
            gpio_toggle(led2_pin);
            // 清除标志位
            flag_led2 = 0;
        }

        // 按键检测任务
        if(flag_button){
            // 打印按键状态变化
            printf("Button state change\n");
            // 其他操作
            // ...
            
            // 清除标志位
            flag_button = 0;
        }

        // 其他任务
        // ...
    }
}

uint64_t systick_counter = 0;
// 系统嘀嗒中断,1ms 中断一次
void systick_handler(void){

    systick_counter ++;

    if(systick_counter % 100 == 0){ // led1 100ms 翻转一次
        flag_led1 = 1;
    }

    if(systick_counter % 20 == 0){ // led2 20ms 翻转一次
        flag_led2 = 1;
    }

    // 其他任务
    // ...
}

// 按键边沿中断
void exti_0_handler(void){
    flag_button = 1;
}

3、Free RTOS系统配置文件简介

FreeRTOSConfig.h配置文件作用:对FreeRTOS进行功能配置和裁剪以及API函数的使能
学习途径:

官方的在线文档中有详细的说明: https://www.freertos.org/a00110.html

正点原子《FreeRTOS开发指南》第三章的内容一-FreeRTOS系统配置

这个文件里面主要是一些宏定义,相关的宏大致可以分为三类:
“INCLUDE":配置FreeRTOS中可选的API函数。
"config”:完成FreeRTOS的功能配置和裁剪。
其他配置项:PendSV宏定义、SVC宏定义。
Logo

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

更多推荐