内存管理
一、事情的开始
为了解决在单片机GUI设计中动态创建和删除控件的需求,开始对内存管理进行了大量的研究,对单片机内存的运行原理可以简略的参考:《单片机内存及运行原理》。
在对内存管理策略上主要参考《一文搞懂栈(stack)、堆(heap)、单片机裸机内存管理malloc》。
二、内存的实质
在单片机的数据存储中都是以字节为单位存储的(根据单片机内核的情况,各种数据类型都有一定的地址对其方式)。
1个字节(Byte)也就是8位(bit),在C程序中 char 数据类型就是8位,这必然是组成一个可以管理的内存最优数据类型。
需要特别理解的是各种类型不过就是需要多少字节的空间来存储这种数据,从物理角度来看他只是向单片机的存储空间中单个或者多个以字节为单位的空间里填充数据,这个数据是什么类型并不关心,如果需要某个数据程序中会直接体现出这个数据应该从哪个物理地址中取,连续取多少个以字节为单位的数据。
具体的数据类型和内存的关系可以参考图灵社区的JAVA教程数据类型部分。
在C语言中数据类型的转换如图所示:
在上图的数据类型转换中,char可以转换为任何一种数据类型,这再次说明了char类型作为可管理的内存的基本类型是可行的。
三、定义一个可管理的内存
使用 char 类型开辟一个可管理的动态内存空间,实质上就是用char定义一个数组:
#define mem_size 100 //字节大小
char Array[mem_size];
利用宏定义开辟一个指定大小的内存空间(数组)。这个空间将作为动态管理的内存。
四、内存管理算法说明
1.内存信息块
内存管理主要依据链表,链表中的内存应该是动态申请的内存信息,包括所申请的内存的物理起始地址、内存空间大小,外加一个可以再编程的内存内存(1个字节,图中的type,这里主要是为了数据对齐)。
链表的表头内存的是总内存信息,包括内存地址、可分配的内存字节大小、已使用的内存大小、最后申请的内存块管理指针。
需要注意的是结构体都会按一定数据类型对齐,对齐的方式请自行查找资料学习
2.动态内存的初始化
动态内存的初始化就是将定义的数组链接到内存控制链表的头节点,并且使数组中的数据全部清除为0。初始化完成后的内存如下:
3.动态申请内存
申请内存意味着将创建一个内存控制块加入到内存控制链表中,并且将链表头节点的数据更新,最后应该将所申请的存储用户数据内存的基地址由函数返回出去。
可能看到这里你会奇怪,为什么内存的控制块和用户内存是分离的而不是连续的地址上,这里主要是为了避免内存溢出导致内存块数据变化,最终导致其中某一块或多块用户内存地址无法释放。更加详细的解释可以看《一文搞懂栈(stack)、堆(heap)、单片机裸机内存管理malloc》。
申请多块内存后,数组中的数据如下图所示
4.释放内存
内存的释放是最重要的环节。
释放内存就是去查询链表中有没有这个要删除的数据基地址,如果有就将内存管理块从链表中删除,并且将链表控制块中的用户数据内存地址指向NULL,最后进行用户数据的移动。
释放内存可以从两个角度去考虑:释放的是最后一个申请的地址、释放的是其他地址。
为了避免内存碎片的产生这里将利用时间换取空间,将内存进行移动,将已释放的空间从物理链接上去除。
五、重要的源码
1.初始化内存
void mem_info(char* array,uint16_t size)
{
int a, b,d;
print(UART_Send,"Array addr:%#x\r\n", array);
for (a = 0; a < size; a++)
{
if ((a + 1) % 8)
{
d = (((char)(&array[a])) & 0xff);
print(UART_Send, "%#x\t", d);
}
else
{
d = (((char)(&array[a])) & 0xff);
print(UART_Send,"%#x\t", d);
print(UART_Send,"\r\n");
for (b = a - 7; b < a+1; b++)
print(UART_Send,"%x\t", array[b] & 0xff);
print(UART_Send,"\r\n----------------------------------------------------------------\r\n");
}
}
if (size % 8 && (a == size))
{
print(UART_Send,"\r\n");
for (a = a - size % 8; a < size; a++)
print(UART_Send,"%x\t", array[a] & 0xff);
print(UART_Send,"\r\n----------------------------------------------------------------\r\n");
}
}
1.申请内存
/// 申请内存块
void* malloc(uint16_t size_t)
{
mem_t* mem_t_;
__disable_irq();
//初始化状态检测
if (!_mem_t.head)
init_mem();
//内存空间检测
if (_mem_t.size_16 < size_t + sizeof(mem_t))
return NULL;
//申请第一个内存块
if (_mem_t.tail == NULL)
_mem_t.tail = mem_t_ = (mem_t*)&user_Memory_Array[0];
else
mem_t_ = (mem_t*)++_mem_t.tail;
mem_t_->pointer = &user_Memory_Array[mem_size - _mem_t.size_end - size_t];
mem_t_->size_16 = size_t;
_mem_t.size_16 -= size_t + sizeof(mem_t);
_mem_t.size_end += size_t;
__enable_irq();
return mem_t_->pointer;
}
2.释放内存
/// 释放内存块
void free(void** pointer)
{
//过程块;下一个内存块;上一个内存块
mem_t* mem_t_, *mem_t_next,*mem_t_prev;
uint16_t i;
uint8_t* data;
__disable_irq();
if (!_mem_t.head)
{
init_mem();
return;
}
else if((mem_t*)*pointer < _mem_t.head || (char*)*pointer > &user_Memory_Array[mem_size-1])
return;
for (mem_t_ = _mem_t.head; mem_t_ <= _mem_t.tail; mem_t_++)
{
//查找删除内存
if (mem_t_->pointer == *pointer)
break;
}
if (mem_t_ > _mem_t.tail)
return;
//调整总可用内存大小
_mem_t.size_16 += mem_t_->size_16 + sizeof(mem_t);
_mem_t.size_end -= mem_t_->size_16;
//尾部删除修改(空间换时间)
if (mem_t_ == _mem_t.tail)
{
if(_mem_t.tail == _mem_t.head)
_mem_t.tail = NULL;
else
_mem_t.tail = --_mem_t.tail;
*pointer = NULL;
return;
}
else //其他位置删除(时间换空间)
{
//将下一个节点的内存管理指针指向要删除的节点
mem_t_next = mem_t_;
for (; mem_t_next < _mem_t.tail; )
{
//指向下一个节点
mem_t_next++;
data = (uint8_t*)mem_t_next->pointer;
data += mem_t_next->size_16-1;
for (i = 0; i < mem_t_next->size_16; i++)
{
//数据迁移
*(data + mem_t_->size_16) = *data;
data--;
}
//修改控制块指向
mem_t_next->pointer += mem_t_->size_16;
}
for (mem_t_next = mem_t_prev = mem_t_; mem_t_next < _mem_t.tail; ++mem_t_prev)
{
//控制块迁移
mem_t_next++;
mem_t_prev->pointer = mem_t_next->pointer;
mem_t_prev->size_16 = mem_t_next->size_16;
mem_t_prev->type = mem_t_next->type;
}
//清除最后数据增加内容(这一段代码用于调试)
mem_t_next->pointer = NULL;
mem_t_next->size_16 = NULL;
mem_t_next->type = NULL;
_mem_t.tail = --mem_t_next;
*pointer = NULL;
}
__enable_irq();
}
3.内存数据打印
为了方便查看数组中的数据存储情况,编写了一个数组打印函数。
void mem_info(char* array,uint16_t size)
{
int a, b,d;
print(UART_Send,"Array addr:%#x\r\n", array);
for (a = 0; a < size; a++)
{
if ((a + 1) % 8)
{
d = (((char)(&array[a])) & 0xff);
print(UART_Send, "%#x\t", d);
}
else
{
d = (((char)(&array[a])) & 0xff);
print(UART_Send,"%#x\t", d);
print(UART_Send,"\r\n");
for (b = a - 7; b < a+1; b++)
print(UART_Send,"%x\t", array[b] & 0xff);
print(UART_Send,"\r\n----------------------------------------------------------------\r\n");
}
}
if (size % 8 && (a == size))
{
print(UART_Send,"\r\n");
for (a = a - size % 8; a < size; a++)
print(UART_Send,"%x\t", array[a] & 0xff);
print(UART_Send,"\r\n----------------------------------------------------------------\r\n");
}
}
在上面的打印函数中,peint 函数是我重新编写的一个串口打印函数,在这个函数中,第一个参数传入的应该是串口输出一个字符的接口函数。
void print(void (*UART_Send)(char data), unsigned char* Data, ...)
{
const char* s;
char buf[16];
int d;
unsigned char x=0;
va_list ap;
//开始取值
va_start(ap, Data);
while (*Data != 0) { //判断是否到达字符串结束符
if (*Data == 0x5c) { //'\'
switch (*++Data) {
case 'r': //回车符
UART_Send(0x0d);
Data++;
break;
case 'n': //换行符
UART_Send(0x0a);
Data++;
break;
default:
Data++;
break;
}
}
else if (*Data == '%' || *Data == '#') {
switch (*++Data) {
case 's': //字符串
s = va_arg(ap, const char*);
for (; *s; s++) {
UART_Send(*s);
}
Data++;
break;
case 'd': //十进制
d = va_arg(ap, int);
itoa(d, buf, 10);
for (s = buf; *s; s++) {
UART_Send(*s);
}
Data++;
break;
case '#': //十进制
x = 1;
break;
case 'x': //十六进制
d = va_arg(ap, unsigned int);
if (x)
{
x = 0;
UART_Send('0');
UART_Send('x');
}
itoa(d, buf, 16);
for (s = buf; *s; s++) {
UART_Send(*s);
}
Data++;
break;
default:
Data++;
break;
}
}
else UART_Send(*Data++);
}
va_end(ap);
}
itoa 函数是实现对数据转换的一个函数
char* itoa(int value, char* string, int radix)
{
int i, d;
int flag = 0;
char* ptr = string;
char ptr_t[8];
if (radix != 10 && radix != 16)
{
*ptr = 0;
return string;
}
if (!value)
{
*ptr++ = 0x30;
*ptr = 0;
return string;
}
if (radix == 10)
{
if (value < 0)
{
*ptr++ = '-';
value *= -1;
}
for (i = 10000; i > 0; i /= 10)
{
d = value / i;
if (d || flag)
{
*ptr++ = (char)(d + 0x30);
value -= (d * i);
flag = 1;
}
}
}
else if (radix == 16)
{
i = 0;
for (; value > 0;)
{
d = value & 0xf;
value = value >> 4;
if (d < 10)
{
ptr_t[i] = d + '0';
}
else if(d >= 10)
{
d -= 10;
ptr_t[i] = d + 'a';
}
i++;
}
for (; i > 0;)
{
*ptr++ = ptr_t[--i];
}
}
*ptr = 0;
return string;
}
打印效果如图所示:
六、特别注意
在ARM处理器中浮点数是需要偶地址对齐,就目前而言还没有更加高效的解决方案。