02_Free RTOS源码获取+移植
前言
本文面向想要手动在基于 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手把手移植(掌握)
移植准备
- Free RTOS源码
- 基础工程:由于后续实验需使用LED、LCD、定时器、内存管理等等,建议使用HAL库版本的内存管理的实验工程为基础工程进行FreeRTOS的移植
移植步骤
- 添加FreeRTOS源码:将FreeRTOS源码添加至基础工程、头文件路径等
- FreeRTOSConfig.h:添加FreeRTOSConfig.h配置文件(用来裁剪FreeRTOS的一些功能,以及一些API的使能)
- 修改SYSTEM文件:修改SYSTEM文件中的sys.c、delay.c、usart.c(因为这些文件是基于裸机以及UCOS实现的,所以使用Free RTOS是不能直接使用的)
- 修改中断相关的文件:修改Systick中断、SVC中断、PendsV中断(这些和任务切换是息息相关的)
- 添加应用程序:验证一下移植是否成功
实操
- 将基础工程(即实验38)解压,然后更换名字为“FreeRTOS移植”
- 进入“FreeRTOS移植”文件内的Middlewares下,新建一个“FreeRTOS”文件夹,并将FreeRTOS源码放入其中
补:其中不是源码的“.gitmodules”可以删掉
- 进入其中的“portable”文件夹,根据硬件和编译器把不重要的文件删掉,我用的STM32和Keil,所以保留下面三个。(其实打开 Keil 文件夹后可以看到里面都没有东西了,Keil 文件夹中之后一个文件,文件名为:“See-also-the-RVDS-directory.txt”,看文件名就知道要转到 RVDS 文件夹了)
- 打开基础工程,新建两个文件分组,分别为 Middlewares/FreeRTOS_CORE 和Middlewares/FreeRTOS_PORT,如下图所示:
- 将刚刚新建的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):
- 添加一下头文件,添加一下“FreeRTOS/include”和“FreeRTOS/portable/RVDS”的port 文件的路径
- 添加 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宏定义。
更多推荐







所有评论(0)