0 引言
在嵌入式系统设计中,需要根据系统的功能需求选择相应的单片机。笔者参与开发的一款中央空调主控制板选用了意法半导体公司的 STM32F407 单片机,这一系列的单片机具有高集成度、高性能、嵌入式存储器和外设,适合作为主控制板的核心单片机使用。STM32F407 主要特征如下:提供了工作频率为 168 MHz 的 Cortex-M4 内核(具有浮点单元)的性能,在168MHz 频率下,从 Flash 存储器执行时,STM32F407 能够提供 210 DMIPS/566 CoreMark 性能,并且利用意法半导体的 ART 加速器实现了 Flash 零等待状态。DSP 指令和浮点单元扩大了产品的应用范围。
STM32F407产品系列具有 512 KB~1MB Flash 和 192 KBSRAM,采用尺寸小至 10 mm×10mm 的 100~ 176 引脚封装。在开发该控制板的软件时发现,由于 STM32F407 的 SRAM 地址不连续,造成动态内存分配存在问题,不能利用全部的片内 192 KB SRAM 资源,有必要深入分析并解决。
1 EWARM 7.40C/C++编译器的数据存储
笔者在开发该控制板的软件时,使用IAR公司的EWARM7.40作为 C/C++ 编译器,该编译器提供了静态内存分配与动态内存分配2种不同的内存分配机制。
1.1 简介
ARM内核可处理4GB 的连续内存,范围从0x00000000到0xFFFF FFFF。不同类型的物理内存可以放置在上述内存范围中。典型的应用程序同时具有只读存储器(ROM)和随机存取内存(RAM)。此外,内存范围的某些部分包含处理器控制寄存器和外围单元。
在典型的应用程序中,数据可以通过以下3种不同的方式存储在内存中:
(1)自动变量
除已声明为静态变量者外,所有函数的局部变量都存储在寄存器或堆栈上。这些变量在函数执行时可以使用。当函数返回到其调用者时,内存空间不再有效。
(2)全局变量、模块静态变量和声明为 static的局部变量
在这种情况下,内存的分配是一劳永逸的。在此语境中,“静态”一词表示应用程序运行时分配给此类变量的内存数量不会改变。ARM 内核有一个单一的地址空间且编译器支持完整的内存寻址。
(3)动态分配的数据
应用程序可以在堆上分配数据,此数据一直有效,直到它被应用程序显式地释放回系统。对于在应用程序执行之前不知道对象数量的场合,这种类型的内存是有用的。注意:就内存量有限的系统或预期运行很久的系统而言,存在着与使用动态分配的数据相关的潜在风险。
1.2 自动变量和参数的存储
按照 C 语言标准,函数内定义且未声明为静态的变 量被命名为自动变量。其中一些变量被放置在处理器寄存器中,其余的放在堆栈上。从语义的角度来看,这是等价的。与放在堆栈上的变量相比,主要区别在于访问寄存器更快,并且需要的内存更少。自动变量只能在函数执行时存活;当函数返回时,分配在堆栈上的内存被释放。
(1)堆栈
堆栈可以包含:未存储在寄存器中的局部变量和参数;表达式的临时结果;函数的返回值(在寄存器中传递的除外);中断期间的处理器状态;应在函数返回前恢复的处理器寄存器(被调用者保存的寄存器)。
堆栈是一个固定的内存块,分为两部分:第一部分包含为调用当前函数的函数,以及调用该函数的函数所使用而分配的内存;第二部分包含可分配的自由内存。这两个区域之间的边界线称为栈顶,由一个专门的处理器寄存器——堆栈指针来表示。通过移动堆栈指针,内存被分配到堆栈上。
函数绝不应指向包含自由内存的堆栈区域。原因是,如果发生中断,被调用的中断函数会分配、修改,当然还有在堆栈上去分配内存。
(2)优点
堆栈的主要优点是:程序不同部分的函数可以使用相同的内存空间存储其数据。不同于堆,堆栈永远不会变成碎片或出现内存泄漏。
函数可以直接或间接地调用自己(递归函数),每个调用可以在堆栈上存储自己的数据。
(3)潜在的问题
堆栈的工作方式使得臆想在函数返回后存储数据变得不可能。以下函数演示了常见的编程错误。它返回指向变量 x 的指针,该变量在函数返回后已不存在。
int * MyFunction(){
int x;
/*在此处理一些事情*/
return &.x; /* 不正确*/
另一个问题是堆栈耗尽的风险。这个问题在一个函数调用另一个、被调函数继续调用第3个函数等,每个函数使用堆栈的总和大于堆栈的大小时会发生。当大型数据对象存储在堆栈上或使用递归函数时,风险更高。
1.3 堆上的动态内存
分配在堆上的对象的内存将一直存在,直到对象被显式释放。这种类型的内存存储对于直到运行时才知道数据量的应用程序是非常有用的。
在 C 语言中,使用标准库函数 malloc或相关函数 calloc 和 realloc 中的一个来分配内存,使用free 来释放内存。
在 C++ 语言中,一个特殊的关键字new 分配内存和运行构造器。通过 new 分配的内存必须使用关键字 delete 来释放。
设计使用堆分配对象的应用程序时必须非常仔细,因为很容易出现无法在堆上分配对象的情形。如果你的应用程序使用过多的内存,堆可能会耗尽。如果不再使用的内存未被释放,则堆也会用完。
对于每个分配的内存块,若干字节用于管理目的的数据是必需的。对于分配大量小块的应用程序,此管理开销可能很大。
还存在碎片化的问题。这意味着在堆中,一小部分自由内存被分配对象使用的内存所分隔。如果没有一块足够大的自由内存给对象,即使自由内存大小的总和超过对象的大小,还是无法分配一个新的对象。不幸的是,随着内存的分配和释放,碎片化往往会增加。基于此,设计长时间运行的应用程序时应尽量避免使用分配在堆上的内存。
2 问题分析与解决方案
以下介绍的程序实例在编译器EWARM7.40、Fre-eRTOS 10.0.0 下验证通过。
2.1 STM32F407 的片内 SRAM
STM32F407 具有192 KB 的片内 SRAM 。片内 SRAM 可以字节、半字(16位)或全字(32位)的形式访问。读取和写人操作以 CPU 速度执行,等待状态为0。片内 SRAM 最多分为两个模块:SRAM1 和 SRAM2 映射地址0x20000000,可以被所有 AHB 主设备存取;CCM(核心耦合存储器)映射到地址0x10000000, 只能由 CPU 通过 D 总线存取。
STM32F407 的内存映射如图1所示,应用程序可以使用的系统SRAM 地址为0x20000000~0x2001 FFFF(128KB),CCM地址为0x10000000~0x1000 FFFF(64 KB)。
由于两块内存的地址不连续,使用编译器 EWARM 7.40时,new 运算符只能够在一片连续的空间分配内存,无法同时使用另外一片连续的空间。换言之,堆的实现只能在128 KB 的 SRAM 内存空间内,另外1/3的片内 SRAM 被白白浪费了,这在应用程序较为复杂时是个严重的缺陷,必须设法解决。
图1 STM32F407 的内存映射
2.2 解决方案
由于编译器 EWARM7.40 的局限性,堆无法在两片不连续的内存空间实现。为了解决此问题,笔者联想到了免费的实时操作系统 FreeRTOS, 该操作系统在2014年8月发布的 V8.1.0中提供了新的内存管理文件 heap_5.c, 允许堆跨越多个不连续的内存区域。后续版本对此功能做了优化与改进。笔者尝试用此方案解决堆的实现问题,取得了成功。具体方法如下:
①把FreeRTOS源程序包中的heap_5.c添加到软件工程中。
②在头文件中定义堆的大小。
#define configTOTAL_HEAP¹_SIZE((size_t)(64*1024))
//HEAP164KB
#define configTOTAL_HEAP2_SIZE((size_t)(100*1024))
//HEAP2100KB
说明:HEAP1 使用CCM 的全部 64KB;HEAP2 使用 SRAM 的100 KB, 剩余部分留给操作系统使用。此数值可根据应用程序的需求灵活调整。
③在main.c中定义两片内存区域:
#pragmalocation=”.ccmram”
uint8_tucHeapl[configTOTAL_HEAP1_SIZE];
uint8_tucHeap2[configTOTAL_HEAP2_SIZE];
constHeapRegion_txHeapRegions[]={
/*Startaddress with dummy offsets Size */
{ucHeapl,configTOTAL_HEAP¹_SIZE},
{ucHeap2,configTOTAL_HEAP2_SIZE},
{NULL,0}
};
④ 在 main.c 中重载new 和delete:
void *operator new(size_t size){
void *p=0;
p=pvPortMalloc(size); //调用FreeRTOS 的内存分配函数
return p;
void operator delete(void *p){
vPortFree(p); // 调用 FreeRTOS 的内存释放函数
经过上述改进后,应用程序可以使用的堆的大小为 164KB, 彻底摆脱了编译器 EWARM7.40 的局限性,满足了复杂应用程序的需求。
3 结语
本文详细介绍了使用多块不连续内存空间实现堆的软件方法,以及在 STM32F407 单片机上的软件实现方法。使用该技术可以在多块不连续内存空间实现堆,更好 地实现了内存的动态分配。使用该软件的控制器产品至今已在现场稳定运行18个月。