FreeRTOS在ARM Cortex-M3架构的32位单片机中的内存管理详解
以下探讨源自我在学习FreeRTOS在ARM Cortex-M3下部署时遇到的内存管理问题。在我查阅许多资料后总结出了一些观点,希望这些观点可以帮助刚接触嵌入式操作系统的朋友,让大家对内存管理方面有一个比较直观的认识。
嵌入式系统开发中,内存管理是一个至关重要的环节。对于使用ARM Cortex-M3架构的32位单片机(如GD32F103RCT6)来说,理解FreeRTOS的内存管理机制不仅能提升系统的稳定性,还能优化资源利用率。本文将深入解析FreeRTOS在Cortex-M3单片机中的内存管理,帮助新人快速、直观地掌握整体概况。
目录
1.引言
2.嵌入式系统的内存架构
(1)内存布局示意图
3.系统启动文件中的内存分配
(1)堆栈和堆的定义
(2)堆栈和堆的初始化
(3)中断向量表
4.FreeRTOS的内存管理机制
(1)配置参数
5.FreeRTOS堆与系统堆的关系
(1)修改堆大小的影响
6.代码实例分析
(1)全局变量和静态数据
(2)函数声明
(3)main函数
(4)SwitchToSystem1函数
(5)CloseSystem1函数
(6)key_task函数
(7)全局变量与静态变量
(8)任务栈的分配与管理
7.调试与优化建议
8.总结
引言
在ARM Cortex-M3架构的32位单片机中,内存资源有限,如何高效管理这有限的资源是开发者需要解决的关键问题。FreeRTOS作为广泛使用的实时操作系统,提供了多种内存管理策略,适用于不同的应用场景。本文将结合实际代码,详细讲解FreeRTOS在Cortex-M3单片机中的内存管理方式,帮助开发者建立清晰的内存管理概念。
嵌入式系统的内存架构
在典型的嵌入式系统中,内存主要分为两大类:
- Flash(程序存储器):
- 存储程序代码、常量数据和中断向量表。
- 非易失性存储,电源断电后数据依然保留。
- SRAM(数据存储器):
- 存储动态分配的数据、堆、栈和静态变量。
- 易失性存储,电源断电后数据丢失。
内存布局示意图
| High Address |
+-------------------------+
| Flash |
| 程序代码 (.text) |
+-------------------------+
| SRAM |
| 静态数据 |
| (全局变量、静态变量) |
+-------------------------+
| SRAM |
| FreeRTOS 堆 |
| (任务栈、TCB、动态分配) |
+-------------------------+
| SRAM |
| 系统堆 |
| (Heap_Mem, 512字节) |
+-------------------------+
| SRAM |
| 主栈 (MSP) |
| (启动代码、中断) |
+-------------------------+
| Low Address |
系统启动文件中的内存分配
系统启动文件(如startup_gd32f10x_hd.s
)负责初始化单片机的堆栈和堆区,并设置中断向量表。以下是关键部分的解析:
堆栈和堆的定义
Stack_Size EQU 0x00000800 ; 2KB 主栈大小
AREA STACK, NOINIT, READWRITE, ALIGN = 3
Stack_Mem SPACE Stack_Size
__initial_sp
Heap_Size EQU 0x00000200 ; 512 字节 堆大小
AREA HEAP, NOINIT, READWRITE, ALIGN = 3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
主栈 (MSP):
0x800
字节)。Stack_Mem
。系统堆 (Heap_Mem
):
0x200
字节)。malloc
、free
。Heap_Mem
。堆栈和堆的初始化
__user_initial_stackheap PROC
LDR R0, = Heap_Mem
LDR R1, =(Stack_Mem + Stack_Size)
LDR R2, = (Heap_Mem + Heap_Size)
LDR R3, = Stack_Mem
BX LR
ENDP
R0
:指向系统堆的起始地址(Heap_Mem
)。R1
:指向主栈的结束地址(Stack_Mem + Stack_Size
)。R2
:指向系统堆的结束地址(Heap_Mem + Heap_Size
)。R3
:指向主栈的起始地址(Stack_Mem
)。中断向量表
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
; ... 其他中断处理函数
__initial_sp
:主栈的初始指针,指向主栈顶部。FreeRTOS的内存管理机制
FreeRTOS提供了多种内存分配方案(heap_1.c到heap_5.c),以适应不同的需求。核心概念如下:
-
总堆 (
ucHeap
): - 定义在FreeRTOS配置文件中,通常通过以下方式声明:
extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
或者在某些配置下:
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
- 用途:用于FreeRTOS的动态内存分配,包括任务栈、任务控制块(TCB)以及其他需要动态分配的内存。
-
任务栈:
- 每个任务在创建时(通过
xTaskCreate
)从总堆中分配一块独立的内存区域作为其栈空间。 - 独立性:每个任务有自己的栈,互不干扰。
-
内存分配方案:
- heap_1.c:简单的无释放机制,适用于不需要动态释放内存的应用。
- heap_2.c:支持释放内存,但容易产生碎片。
- heap_3.c:直接使用C库的
malloc
和free
,灵活但不推荐。 - heap_4.c:支持内存块的合并和释放,适合多任务环境。
- heap_5.c:支持多个堆区域,适用于复杂的内存分配需求。
配置参数
在FreeRTOSConfig.h
中,关键的内存管理配置包括:
#define configTOTAL_HEAP_SIZE ( ( size_t ) 2048 )
#define configAPPLICATION_ALLOCATED_HEAP 1
configTOTAL_HEAP_SIZE
:定义FreeRTOS总堆的大小。configAPPLICATION_ALLOCATED_HEAP
:如果设置为1,表示堆数组由应用程序定义,而不是由FreeRTOS自行定义。FreeRTOS堆与系统堆的关系
在嵌入式系统中,内存资源有限,合理划分和管理堆区至关重要。具体来说:
-
独立的堆区域:
- 系统堆 (
Heap_Mem
):由启动文件定义,512字节,用于系统级别的动态内存分配(如标准库函数)。 - FreeRTOS堆 (
ucHeap
):由FreeRTOS管理,用于任务栈、TCB和其他动态分配。 -
内存分配的独立性:
- 互不影响:系统堆和FreeRTOS堆是独立的内存区域,分别由系统和FreeRTOS管理。
- 配置一致性:确保
configTOTAL_HEAP_SIZE
不超过FreeRTOS堆所在的内存区域大小,避免内存分配错误。
修改堆大小的影响
修改系统堆 (Heap_Size
):
修改FreeRTOS堆 (configTOTAL_HEAP_SIZE
):
代码实例分析
通过具体代码实例,进一步理解内存区域的划分与使用。
全局变量和静态数据
TaskHandle_t system1_tasks[7]; // 保存系统1的任务句柄
float Xmin = 0, Xmax = 0, Ymin = 0, Ymax = 0;
解释:
system1_tasks
:全局数组,用于存储FreeRTOS任务的句柄。Xmin, Xmax, Ymin, Ymax
:全局浮点变量,可能用于坐标或数据范围。内存区域:
.bss
或.data
段)。函数声明
void SwitchToSystem1(void);
void SwitchToSystem2(void);
void key_task(void *pvParameters);
解释:
内存区域:
main
函数
int main(void)
{
nvic_priority_group_set(NVIC_PRIGROUP_PRE4_SUB0); // 设置中断优先级分组
OLED_Init();
main_logic(); // OLED开场动画
xTaskCreate(key_task, "KEY_TASK", ( ( unsigned short ) 1024 ), NULL, tskIDLE_PRIORITY+6, NULL);
// 启动调度器
vTaskStartScheduler();
return 0;
}
-
nvic_priority_group_set(NVIC_PRIGROUP_PRE4_SUB0);
- 解释:设置中断优先级分组。
- 内存区域:
- Flash:函数代码存储在Flash中。
- SRAM:执行过程中使用的寄存器和临时变量存储在**主栈(MSP)**上。
-
OLED_Init();
- 解释:初始化OLED显示。
- 内存区域:
- Flash:函数代码存储在Flash中。
- SRAM:执行过程中使用的临时数据存储在**主栈(MSP)**上。
-
main_logic();
- 解释:执行OLED开场动画的主逻辑。
- 内存区域:
- Flash和SRAM:函数代码存储在Flash中,临时数据在**主栈(MSP)**上。
-
xTaskCreate(key_task, "KEY_TASK", ( ( unsigned short ) 1024 ), NULL, tskIDLE_PRIORITY+6, NULL);
- 解释:创建一个名为"KEY_TASK"的FreeRTOS任务,栈大小为1024字节,优先级为
tskIDLE_PRIORITY + 6
。 - 内存区域:
- FreeRTOS堆:分配1024字节用于任务栈,分配任务控制块(TCB)。
- Flash:
xTaskCreate
函数和key_task
的代码存储在Flash中。 -
vTaskStartScheduler();
- 解释:启动FreeRTOS调度器,开始任务调度。
- 内存区域:
- Flash:函数代码存储在Flash中。
- FreeRTOS堆:调度器启动后,任务栈和其他动态分配的内存使用FreeRTOS堆。
-
return 0;
- 解释:返回主函数。
- 内存区域:
- 主栈(MSP):存储返回地址和可能的临时数据。
SwitchToSystem1
函数
void SwitchToSystem1(void) {
// 创建系统1的任务
// 主控程序:循迹、避障等功能实现
xTaskCreate(A_star_task, "A_STAR_TASK", ( ( unsigned short ) 654 ), NULL, tskIDLE_PRIORITY+3, &system1_tasks[5]);
// 串口打印fps、滤波后的angle_compass、以及红外扫描距离
xTaskCreate(usart0_task, "USART0_TASK", ( ( unsigned short ) 128 ), NULL, tskIDLE_PRIORITY+5, &system1_tasks[0]);
// 红外发射接收
xTaskCreate(echo_task, "ECHO_TASK", ( ( unsigned short ) 128 ), NULL, tskIDLE_PRIORITY, &system1_tasks[1]);
// 红外舵机云台运动控制、oled清屏刷新、获取避障标志位
xTaskCreate(pwm_task, "PWM_TASK", ( ( unsigned short ) 256 ), NULL, tskIDLE_PRIORITY+1, &system1_tasks[2]);
// 罗盘航向角实时检测
xTaskCreate(QMC_task, "QMC_TASK", ( ( unsigned short ) 256 ), NULL, tskIDLE_PRIORITY+2, &system1_tasks[4]);
// oled显示红外雷达界面
xTaskCreate(oled_task, "OLED_TASK", ( ( unsigned short ) 256 ), NULL, tskIDLE_PRIORITY+4, &system1_tasks[3]);
}
-
xTaskCreate(A_star_task, "A_STAR_TASK", ( ( unsigned short ) 654 ), NULL, tskIDLE_PRIORITY+3, &system1_tasks[5]);
- 解释:创建
A_star_task
任务,栈大小654字节,优先级tskIDLE_PRIORITY + 3
,句柄存储在system1_tasks[5]
。 - 内存区域:
- FreeRTOS堆:分配654字节用于任务栈,分配TCB。
- Flash:任务函数
A_star_task
的代码存储在Flash中。 - SRAM静态数据区域:
system1_tasks[5]
存储在全局数组中。 -
xTaskCreate(usart0_task, "USART0_TASK", ( ( unsigned short ) 128 ), NULL, tskIDLE_PRIORITY+5, &system1_tasks[0]);
- 解释:创建
USART0_TASK
任务,栈大小128字节,优先级tskIDLE_PRIORITY + 5
,句柄存储在system1_tasks[0]
。 - 内存区域:
- FreeRTOS堆:分配128字节用于任务栈,分配TCB。
- Flash:任务函数
usart0_task
的代码存储在Flash中。 - SRAM静态数据区域:
system1_tasks[0]
存储在全局数组中。 -
xTaskCreate(echo_task, "ECHO_TASK", ( ( unsigned short ) 128 ), NULL, tskIDLE_PRIORITY, &system1_tasks[1]);
- 解释:创建
ECHO_TASK
任务,栈大小128字节,优先级tskIDLE_PRIORITY
,句柄存储在system1_tasks[1]
。 - 内存区域:
- FreeRTOS堆:分配128字节用于任务栈,分配TCB。
- Flash:任务函数
echo_task
的代码存储在Flash中。 - SRAM静态数据区域:
system1_tasks[1]
存储在全局数组中。 -
xTaskCreate(pwm_task, "PWM_TASK", ( ( unsigned short ) 256 ), NULL, tskIDLE_PRIORITY+1, &system1_tasks[2]);
- 解释:创建
PWM_TASK
任务,栈大小256字节,优先级tskIDLE_PRIORITY + 1
,句柄存储在system1_tasks[2]
。 - 内存区域:
- FreeRTOS堆:分配256字节用于任务栈,分配TCB。
- Flash:任务函数
pwm_task
的代码存储在Flash中。 - SRAM静态数据区域:
system1_tasks[2]
存储在全局数组中。 -
xTaskCreate(QMC_task, "QMC_TASK", ( ( unsigned short ) 256 ), NULL, tskIDLE_PRIORITY+2, &system1_tasks[4]);
- 解释:创建
QMC_TASK
任务,栈大小256字节,优先级tskIDLE_PRIORITY + 2
,句柄存储在system1_tasks[4]
。 - 内存区域:
- FreeRTOS堆:分配256字节用于任务栈,分配TCB。
- Flash:任务函数
QMC_task
的代码存储在Flash中。 - SRAM静态数据区域:
system1_tasks[4]
存储在全局数组中。 -
xTaskCreate(oled_task, "OLED_TASK", ( ( unsigned short ) 256 ), NULL, tskIDLE_PRIORITY+4, &system1_tasks[3]);
- 解释:创建
OLED_TASK
任务,栈大小256字节,优先级tskIDLE_PRIORITY + 4
,句柄存储在system1_tasks[3]
。 - 内存区域:
- FreeRTOS堆:分配256字节用于任务栈,分配TCB。
- Flash:任务函数
oled_task
的代码存储在Flash中。 - SRAM静态数据区域:
system1_tasks[3]
存储在全局数组中。
CloseSystem1
函数
void CloseSystem1(void) {
// 删除系统1任务
for (int i = 0; i < 6; i++) {
if (system1_tasks[i] != NULL) {
vTaskDelete(system1_tasks[i]);
system1_tasks[i] = NULL;
}
}
line_set_flag = MOTOR_MOVE_OFF;
Motor_OUT_PWM(0,0,0,0);
}
-
任务删除循环
for (int i = 0; i < 6; i++) { if (system1_tasks[i] != NULL) { vTaskDelete(system1_tasks[i]); system1_tasks[i] = NULL; } }
- 解释:遍历
system1_tasks
数组,删除非空任务。 - 内存区域:
- SRAM静态数据区域:
system1_tasks
数组位于静态数据区域。 - FreeRTOS堆:
vTaskDelete
释放任务栈和TCB所占用的堆空间。 -
设置标志位
line_set_flag = MOTOR_MOVE_OFF;
- 解释:设置一个全局或静态变量
line_set_flag
,用于控制电机状态。 - 内存区域:
- SRAM静态数据区域。
-
停止电机
Motor_OUT_PWM(0,0,0,0);
- 解释:调用
Motor_OUT_PWM
函数,设置电机PWM输出为零,停止电机。 - 内存区域:
- Flash:函数代码存储在Flash中。
- SRAM:执行过程中使用的临时数据存储在**主栈(MSP)**上。
key_task
函数
void key_task(void *pvParameters){
usart0_init(115200); // 初始化串口0,USART1_TX to PC
usart1_init(115200); // 初始化串口1,USART1_RX to esp32_BT
pwm_timer_config();
timer2_pwm_init(10800-1,1-1);
// 初始化超声波模块和中断
hcsr04_init();
exti_config();
timer_config(); // 初始化定时器
OLED_Init();
main_logic();
u8 i=0;
gpio_init_pc0_pc1();
u8 aaa=1;
u8 p=0;
// [注释掉的代码段省略]
GUODUsn = 1;
OLED_transition(0);
while(1){
vTaskDelay(40);
p = menu_Enter_event();
if(p==2)
{
CloseSystem1();
GUODUsn=1;
OLED_transition(0);
GUODUsn=1;
main_menu1(); //进入菜单1
aaa=0;
GUODUsn=1;
}else if(aaa==0){
aaa=1;
GUODUsn=1;
SwitchToSystem1();
}
}
}
-
初始化外设
usart0_init(115200); usart1_init(115200); pwm_timer_config(); timer2_pwm_init(10800-1,1-1); hcsr04_init(); exti_config(); timer_config(); OLED_Init(); main_logic(); gpio_init_pc0_pc1();
- 解释:初始化串口、PWM、定时器、超声波模块、中断、OLED和GPIO。
- 内存区域:
- Flash:所有这些函数的代码存储在Flash中。
- FreeRTOS堆:执行过程中使用的临时数据存储在任务栈上。
-
定义局部变量
u8 i=0; u8 aaa=1; u8 p=0;
- 解释:定义并初始化三个局部变量
i
、aaa
、p
。 - 内存区域:
- FreeRTOS任务栈:这些局部变量存储在
key_task
的任务栈上。 -
设置全局变量和OLED过渡
GUODUsn = 1; OLED_transition(0);
- 解释:设置全局或静态变量
GUODUsn
,并执行OLED过渡动画。 - 内存区域:
- SRAM静态数据区域:
GUODUsn
存储在静态数据区域。 - Flash:
OLED_transition
函数代码存储在Flash中。 - FreeRTOS任务栈:执行过程中使用的临时数据存储在任务栈上。
-
无限循环
while(1){ vTaskDelay(40); p = menu_Enter_event(); if(p==2) { CloseSystem1(); GUODUsn=1; OLED_transition(0); GUODUsn=1; main_menu1(); //进入菜单1 aaa=0; GUODUsn=1; }else if(aaa==0){ aaa=1; GUODUsn=1; SwitchToSystem1(); } }
-
解释:进入无限循环,处理任务逻辑,包括延迟、事件处理和任务切换。
-
内存区域:
- FreeRTOS任务栈:循环中的局部变量和函数调用使用任务栈。
- FreeRTOS堆:
vTaskDelay
依赖于FreeRTOS的时间管理机制,使用堆中的数据结构。 -
循环内部:
vTaskDelay(40); p = menu_Enter_event(); if(p==2) { CloseSystem1(); GUODUsn=1; OLED_transition(0); GUODUsn=1; main_menu1(); //进入菜单1 aaa=0; GUODUsn=1; } else if(aaa==0) { aaa=1; GUODUsn=1; SwitchToSystem1(); }
-
vTaskDelay(40);
- 解释:任务延迟40个时钟周期。
- 内存区域:
- FreeRTOS堆:延迟机制依赖于FreeRTOS的时间管理,使用堆中的定时器数据结构。
-
p = menu_Enter_event();
- 解释:处理菜单事件,返回值存储在
p
。 - 内存区域:
- Flash:函数代码存储在Flash中。
- FreeRTOS任务栈:返回值存储在任务栈上。
-
条件判断和任务切换:
-
if(p == 2)
: - 解释:如果菜单事件返回2,关闭系统1,进入菜单1。
- 内存区域:
CloseSystem1();
:- Flash:函数代码存储在Flash中。
- FreeRTOS堆:释放任务栈和TCB。
- SRAM静态数据区域:操作全局变量。
main_menu1();
:- Flash:函数代码存储在Flash中。
-
else if(aaa == 0)
: - 解释:如果
aaa
为0,重新切换到系统1,创建相关任务。 - 内存区域:
SwitchToSystem1();
:- Flash:函数代码存储在Flash中。
- FreeRTOS堆:分配任务栈和TCB。
- SRAM静态数据区域:操作全局变量。
全局变量与静态变量
TaskHandle_t system1_tasks[7];
float Xmin = 0, Xmax = 0, Ymin = 0, Ymax = 0;
.bss
或.data
段)。任务栈的分配与管理
每个通过xTaskCreate
创建的任务都会从FreeRTOS的堆(ucHeap
)中分配一块独立的栈空间。任务栈用于存储该任务的函数调用、局部变量和临时数据。
FreeRTOS堆 (ucHeap
):
Heap_Mem
)。系统堆 (Heap_Mem
):
调试与优化建议
在开发过程中,合理配置和监控内存使用情况至关重要。以下是一些调试与优化建议:
-
合理配置FreeRTOS堆大小:
- 根据任务数量和每个任务的栈大小,合理设置
configTOTAL_HEAP_SIZE
。 - 例如,如果有多个任务,每个任务需要256字节栈,FreeRTOS堆至少需要任务数量乘以栈大小,再加上TCB的大小。
-
监控堆和栈的使用情况:
- 使用FreeRTOS提供的函数如
uxTaskGetStackHighWaterMark()
监控任务栈的使用情况,避免栈溢出。 - 定期检查FreeRTOS堆的剩余内存,确保系统稳定运行。
-
优化内存使用:
- 减少全局变量和静态变量的使用,尽量将数据存储在任务的本地存储或动态分配的内存中。
- 使用适当的内存分配策略(如heap_4.c)以减少内存碎片。
-
调整系统堆大小:
- 根据系统级别的动态内存需求,适当调整启动文件中定义的
Heap_Size
,确保不与FreeRTOS堆冲突。 - 确保系统堆和FreeRTOS堆的总和不超过SRAM的可用大小。
-
保持启动文件与FreeRTOS配置一致:
- 确保启动文件中定义的
Heap_Size
与FreeRTOSConfig.h
中的configTOTAL_HEAP_SIZE
协调一致,避免内存分配错误。 -
使用调试工具:
- 使用调试器监控寄存器(如
R0
到R3
)的值,确保堆和栈的边界正确初始化。 - 检查FreeRTOS堆和系统堆的内存分配,确保没有内存溢出或冲突。
总结
FreeRTOS在ARM Cortex-M3架构的32位单片机中提供了高效的内存管理机制,但理解其与系统堆的独立性至关重要。通过合理配置FreeRTOS堆大小、监控堆和栈的使用情况,以及优化内存分配策略,可以有效提升系统的稳定性和资源利用率。结合实际代码和启动文件的分析,希望本文能帮助开发者快速、直观地掌握FreeRTOS在Cortex-M3单片机中的内存管理概况,为嵌入式系统开发打下坚实的基础。
参考资料:
作者:tsistbasit