系统移植

移植 FreeRTOS 的主要原因是将其核心功能与目标硬件平台结合,从而为嵌入式开发提供实时操作系统支持,解决裸机开发中的不足。

准备

  1. 开发板。案例是以立创天空星为开发板。
  2. Windows 系统的电脑。当前是以 Win11 的电脑来实现案例的。
  3. Keil 开发工具。并且已经安装好 GD32 依赖环境。
  4. FreeRTOS 源码包。下载地址为: https://github.com/FreeRTOS/FreeRTOS/releases

我们下载好 FreeRTOS 源码包之后可以参照官方示例代码,也就是 Demo 中的例程进行参考。

移植

FreeRTOS 移植指南

熟悉目录结构

从顶部开始,下载被分割成两个子目录:FreeRTOS 和 FreeRTOS-Plus。如下所示:

1
2
3
+-FreeRTOS-Plus // 包含[FreeRTOS Plus](/文档/03库/02 FreeRTOS Plus/01简介)组件和演示项目。
|
+-FreeRTOS // 包含FreeRTOS实时内核源文件和演示项目

FreeRTOS:包含 FreeRTOS 内核(Kernel)的源代码(例如 task.c, queue.c, timers.c 等核心文件)。

FreeRTOS-Plus:包含 FreeRTOS Plus 系列库和组件,这些是对 FreeRTOS 内核功能的扩展。

FreeRTOS 内核目录结构

1
2
3
4
5
FreeRTOS
|
+-Demo // 包含演示应用程序项目。
|
+-Source // 包含 RTOS 内核源代码。

FreeRTOS/Source 目录的结构如下所示

1
2
3
4
5
6
7
8
9
10
FreeRTOS
|
+-Source // FreeRTOS内核文件
|
+-include // FreeRTOS内核头文件
|
+-Portable // 处理器相关的代码
|
+-MemMang // 内存管理实现
+-.... // 与处理器相关的代码

FreeRTOS/Demo 目录的结构如下所示

1
2
3
4
5
6
7
FreeRTOS
|
+-Demo
|
+-Common The demo application files that are used by all the demos.
+-Dir x The demo application build files for port x
+-Dir y The demo application build files for port y

拷贝

FreeRTOS 官方提供了丰富且详细的示例代码,我们只需根据需求拷贝并适配即可。

在 FreeRTOS/Demo 目录下,有大量针对不同架构平台的示例项目。我们需要找到与当前使用的架构和开发环境匹配的示例代码进行移植。

当前我们的开发环境为 Keil,编译器是 GCC,目标处理器的内核架构为 Cortex-M4F。按照这些条件选择合适的示例代码即可。

CORTEX_M4F_STM32F407ZG-SK 这里先不要参考这个, SK 指代的是 STM32 官方自带的 IDE,它可以在创建项目的时候自动添加 FreeRTOS 相关的库文件,所有我们打开会发现除了一个 .h 外它不包含任何与 FreeRTOS 相关的库文件;

在示例代码中我们可以找 CORTEX_M4F_CEC1302_Keil_GCC 这个示例代码进行参考,通过 Keil 将其打开:

会发现它包含的文件如下:

  • FreeRTOS/Source/include 目录下所有的头文件
  • FreeRTOS/Source/Portable 目录下处理器相关的代码
  • FreeRTOS/Source 目录下 FreeRTOS 处理器相关代码
  • FreeRTOSConfig.h 头文件;

我们参照例程将这些文件拷贝到我们自己对应的 Middleware/FreeRTOS 目录下;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/FreeRTOS

├── /portable
│ ├── /GCC/ARM_CM4F # 针对 ARM Cortex-M4 架构和 GCC 编译器的移植代码
│ │ ├── port.c
│ │ └── portmacro.h
│ └── /MemMang # 内存管理相关代码
│ └── heap_4.c

├── /include # FreeRTOS 公共头文件
│ ├── FreeRTOS.h
│ └── …

├── croutine.c # 协程相关代码
├── event_groups.c # 事件组相关代码
├── heap_4.c # 内存分配代码
├── list.c # 链表操作代码
├── queue.c # 队列操作代码
├── stream_buffer.c # 流缓冲区相关代码
├── tasks.c # 任务管理相关代码
└── timers.c # 定时器管理相关代码

/User

└── FreeRTOSConfig.h # FreeRTOS 配置文件

简单点来说就是将 FreeRTOS/Source 下所有的文件都复制到我们 FreeRTOS 目录中;删除 Portable 中不需要的文件,再将 FreeRTOSConfig.h 放置在 main.c 同级目录下即可;

运行

将上述 FreeRTOS 文件添加到到 Keil 工程中,添加后的目录如下:

freertosmljg

添加完成后运行,解决对应错误即可;

配置时钟

在 FreeRTOS 成功移植并且不报错之后,系统就已经具备了基本的任务调度能力,但要让它完全正常运行,还需要完成一些硬件初始化和配置工作。这些配置工作通常与时钟、外设、系统节拍等相关,确保 FreeRTOS 的调度器能够正确地运行并与硬件配合。

系统时钟

配置当前 FreeRTOS 的 CPU 主频(即系统时钟频率)。

1
#define configCPU_CLOCK_HZ ( SystemCoreClock )

SystemCoreClock 通常是由硬件库定义的全局变量,它会根据系统时钟初始化过程自动更新。

SysTick

滴答时钟用于产生系统节拍中断,驱动 FreeRTOS 的任务调度器。

滴答时钟的频率由 configTICK_RATE_HZ 定义;

1
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )

这里的 1000 表示每秒产生 1000 次节拍中断,即 1 毫秒一次。

滴答频率的选择需要根据应用需求决定:

  • 高频(如 1000 Hz):调度更频繁,但 CPU 负载增加。
  • 低频(如 100 Hz):调度较少,适合实时性要求不高的场景。

时间片的长度由滴答时钟(tick rate)与调度方式决定。如果启用了时间片轮转,每个任务的时间片是基于系统的滴答周期来分配的,如果启用了抢占式调度(configUSE_PREEMPTION 1)系统会在每个滴答周期内检查是否有更高优先级的任务需要执行,确保高优先级任务及时获得 CPU 时间。

由于滴答时钟已经交予 FreeRTOS 进行是使用,我们就不能在 main 中初始化 SysTick 了(systick_config()),因为二者同时去操作 SysTick 寄存器就会产生一些错误问题;

所以这里我们还是和前面一样通过宏进行判断,如果移植了 FreeRTOS 就让 FreeRTOS 控制滴答时钟的配置,否则就是用我们芯片自己的;

自定义延时函数

在某些情况下我们需要使用我们自定义的延时函数,这里我们还是和之前一样过读取 SysTick 定时器的计数值,利用系统的时钟频率来精确计算经过的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void delay_1us(uint32_t count) {
uint32_t ticks;
uint32_t told, tnow, reload, tcnt = 0;
reload = SysTick -> LOAD;
ticks = count * (SystemCoreClock / 1000000);
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;
}
}
}

void delay_1ms(uint32_t count) {
uint32_t i;
for (i=0; i<count; i++)
{
delay_1us(1000);
}
}

问题

一、我没有调用 FreeRTOS 相关函数他为啥会自动运行并报错呢

FreeRTOS 的部分代码(例如任务调度器、内存管理等)可能依赖于特定的硬件初始化或全局配置。如果你的工程中没有正确配置它的基础环境,编译或链接时就可能报错。

FreeRTOS 的部分代码可能会在工程初始化或链接时被调用,即使未在 main 中使用。

二、error: use of undeclared identifier ‘SystemCoreClock’错误

1
2
3
4
#ifdef __ICCARM__
#include <stdint.h>
extern uint32_t SystemCoreClock;
#endif

在 port.c 文件中,这个变量只有在 ICCARM 这个宏存在,也就是说编译器是 ICC 的时候才执行其中的代码,而我们是 Keil 编译器,所以我们需要修改为:

1
2
3
4
#if defined(__ICCARM__)||defined(__CC_ARM)||defined(__GNUC__)
#include <stdint.h>
extern uint32_t SystemCoreClock;
#endif

三、重复定义错误

1
2
3
4
// 中断处理函数重复定义
Error: L6200E: Symbol SysTick_Handler multiply defined (by port.o and gd32f4xx_it.o).
Error: L6200E: Symbol SVC_Handler multiply defined (by port.o and gd32f4xx_it.o).
Error: L6200E: Symbol PendSV_Handler multiply defined (by port.o and gd32f4xx_it.o).

中断处理函数重复定义错误原因:

FreeRTOS 的移植代码(port.c 文件)会定义 Cortex-M 核心的中断处理函数,例如:

  • SysTick_Handler:用于系统节拍定时器(SysTick)的中断服务。
  • PendSV_Handler:用于上下文切换。
  • SVC_Handler:用于启动第一个任务。

同时,你的工程中可能有一个文件(例如 gd32f4xx_it.c),也定义了这些中断处理函数,导致冲突。

我们希望的是,如果移植了 FreeRTOS 就使用 FreeRTOS 中的函数,否则就使用我们自己函数;这就可以通过宏定义来实现:

当我们移植 FreeRTOS 时,我们可以手动在 Keil 中添加一个自定义宏( SYS_SUPPORT_OS 这个宏定义),将重复的函数包裹起来;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// gd32f407xx.it.h
#ifndef SYS_SUPPORT_OS
/* this function handles SVC exception */
void SVC_Handler(void);
/* this function handles PendSV exception */
void PendSV_Handler(void);
/* this function handles SysTick exception */
void SysTick_Handler(void);
#endif

// gd32f407xx.it.c
#ifndef SYS_SUPPORT_OS
void SVC_Handler(void) {
/* if SVC exception occurs, go to infinite loop */
while(1) {
}
}
void PendSV_Handler(void) {
/* if PendSV exception occurs, go to infinite loop */
while(1) {
}
}
void SysTick_Handler(void) {
// led_spark();
delay_decrement();
}
#endif

四、变量未定义错误

1
2
3
4
Error: L6218E: Undefined symbol vApplicationStackOverflowHook (referred from tasks.o).
Error: L6218E: Undefined symbol vApplicationIdleHook (referred from tasks.o).
Error: L6218E: Undefined symbol vApplicationTickHook (referred from tasks.o).
Error: L6218E: Undefined symbol vApplicationMallocFailedHook (referred from heap_4_1.o).

上述错误表明你的工程中缺少一些 FreeRTOS 要求的钩子函数的定义。这些函数用于处理特定的系统事件,如任务堆栈溢出、空闲任务的操作、系统时钟节拍钩子等。虽然它们在某些场景中是可选的,但如果在配置文件 FreeRTOSConfig.h 中启用了相关功能而未定义这些钩子函数,就会导致链接时的 undefined symbol 错误。

这里我们以 vApplicationStackOverflowHook 为例了解如何解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// task.h
#if ( configCHECK_FOR_STACK_OVERFLOW > 0 )
void vApplicationStackOverflowHook( TaskHandle_t xTask, char * pcTaskName );
#endif

#if ( ( configCHECK_FOR_STACK_OVERFLOW == 1 ) && ( portSTACK_GROWTH > 0 ) )
#define taskCHECK_FOR_STACK_OVERFLOW() {
if( pxCurrentTCB->pxTopOfStack >= pxCurrentTCB->pxEndOfStack - portSTACK_LIMIT_PADDING ) { vApplicationStackOverflowHook( ( TaskHandle_t ) pxCurrentTCB, pxCurrentTCB->pcTaskName );
}
}
#endif

// FreeRTOSConfig.h
#configCHECK_FOR_STACK_OVERFLOW 0
#define configUSE_IDLE_HOOK 0
#define configUSE_TICK_HOOK 0
#define configUSE_MALLOC_FAILED_HOOK 0

可以看到 vApplicationStackOverflowHook 的声明与使用是通过 configCHECK_FOR_STACK_OVERFLOW 这个宏是否 > 0 来控制的,所以我们这里有两个解决方案,1 是将 configCHECK_FOR_STACK_OVERFLOW 定义小于 0,而是手动实现这个函数;这里我们简单一点我们将这个宏的值改为 0;

其他三个也是同理;