FreeRTOS内存管理 基于STM32

目录

一、内存管理的基本概念

二、内存管理的应用场景

三、heap_4.c

1.内存申请函数 pvPortMalloc()

2.内存释放函数 vPortFree()

 四、内存管理的实验

五、内存管理的实验现象


一、内存管理的基本概念

      在计算系统中,变量、中间数据一般存放在系统存储空间中,只有在实际使用时才将 它们从存储空间调入到中央处理器内部进行运算。通常存储空间可以分为两种:内部存储 空间和外部存储空间。内部存储空间访问速度比较快,能够按照变量地址随机地访问,也 就是我们通常所说的 RAM(随机存储器),或电脑的内存;而外部存储空间内所保存的内 容相对来说比较固定,即使掉电后数据也不会丢失,可以把它理解为电脑的硬盘。在这一 章中我们主要讨论内部存储空间(RAM)的管理——内存管理。

      FreeRTOS 操作系统将内核与内存管理分开实现,操作系统内核仅规定了必要的内存管 理函数原型,而不关心这些内存管理函数是如何实现的,所以在 FreeRTOS 中提供了多种 内存分配算法(分配策略),但是上层接口(API)却是统一的。这样做可以增加系统的 灵活性:用户可以选择对自己更有利的内存管理策略,在不同的应用场合使用不同的内存 分配策略。

      在嵌入式程序设计中内存分配应该是根据所设计系统的特点来决定选择使用动态内存 分配还是静态内存分配算法,一些可靠性要求非常高的系统应选择使用静态的,而普通的 业务系统可以使用动态来提高内存使用效率。静态可以保证设备的可靠性但是需要考虑内 存上限,内存使用效率低,而动态则是相反。

      FreeRTOS 内存管理模块管理用于系统中内存资源,它是操作系统的核心模块之一。主 要包括内存的初始化、分配以及释放。

      很多人会有疑问,什么不直接使用 C 标准库中的内存管理函数呢?在电脑中我们可以 用 malloc()和 free()这两个函数动态的分配内存和释放内存。但是,在嵌入式实时操作系统 中,调用 malloc()和 free()却是危险的,原因有以下几点:

 1.这些函数在小型嵌入式系统中并不总是可用的,小型嵌入式设备中的 RAM 不足。

 2.它们的实现可能非常的大,占据了相当大的一块代码空间。

 3.他们几乎都不是安全的。

 4.它们并不是确定的,每次调用这些函数执行的时间可能都不一样。

 5. 它们有可能产生碎片。

 6. 这两个函数会使得链接器配置得复杂。

 7.如果允许堆空间的生长方向覆盖其他变量占据的内存,它们会成为 debug 的灾难。

      在一般的实时嵌入式系统中,由于实时性的要求,很少使用虚拟内存机制。所有的内 存都需要用户参与分配,直接操作物理内存,所分配的内存不能超过系统的物理内存,所 有的系统堆栈的管理,都由用户自己管理。

      同时,在嵌入式实时操作系统中,对内存的分配时间要求更为苛刻,分配内存的时间 必须是确定的。一般内存管理算法是根据需要存储的数据的长度在内存中去寻找一个与这段数据相适应的空闲内存块,然后将数据存储在里面。而寻找这样一个空闲内存块所耗费 的时间是不确定的,因此对于实时系统来说,这就是不可接受的,实时系统必须要保证内 存块的分配过程在可预测的确定时间内完成,否则实时任务对外部事件的响应也将变得不可确定。

      而在嵌入式系统中,内存是十分有限而且是十分珍贵的,用一块内存就少了一块内存, 而在分配中随着内存不断被分配和释放,整个系统内存区域会产生越来越多的碎片,因为 在使用过程中,申请了一些内存,其中一些释放了,导致内存空间中存在一些小的内存块, 它们地址不连续,不能够作为一整块的大内存分配出去,所以一定会在某个时间,系统已 经无法分配到合适的内存了,导致系统瘫痪。其实系统中实际是还有内存的,但是因为小 块的内存的地址不连续,导致无法分配成功,所以我们需要一个优良的内存分配算法来避 免这种情况的出现。

      不同的嵌入式系统具有不同的内存配置和时间要求。所以单一的内存分配算法只可能 适合部分应用程序。因此,FreeRTOS 将内存分配作为可移植层面(相对于基本的内核代码 部分而言),FreeRTOS 有针对性的提供了不同的内存分配管理算法,这使得应用于不同场 景的设备可以选择适合自身内存算法。

      FreeRTOS 对内存管理做了很多事情,FreeRTOS 的 V9.0.0 版本为我们提供了 5 种内存 管理算法,分别是 heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c,源文件存放于 FreeRTOS\Source\portable\MemMang 路径下,在使用的时候选择其中一个添加到我们的工 程中去即可。

      FreeRTOS 的内存管理模块通过对内存的申请、释放操作,来管理用户和系统对内存的 使用,使内存的利用率和使用效率达到最优,同时最大限度地解决系统可能产生的内存碎片问题。

二、内存管理的应用场景

      首先,在使用内存分配前,必须明白自己在做什么,这样做与其他的方法有什么不同, 特别是会产生哪些负面影响,在自己的产品面前,应当选择哪种分配策略。

      内存管理的主要工作是动态划分并管理用户分配好的内存区间,主要是在用户需要使 用大小不等的内存块的场景中使用,当用户需要分配内存时,可以通过操作系统的内存申 请函数索取指定大小内存块,一旦使用完毕,通过动态内存释放函数归还所占用内存,使 之可以重复使用(heap_1.c 的内存管理除外)。

      例如我们需要定义一个 float 型数组:floatArr[];

      但是,在使用数组的时候,总有一个问题困扰着我们:数组应该有多大?在很多的情 况下,你并不能确定要使用多大的数组,可能为了避免发生错误你就需要把数组定义得足 够大。即使你知道想利用的空间大小,但是如果因为某种特殊原因空间利用的大小有增加 或者减少,你又必须重新去修改程序,扩大数组的存储范围。这种分配固定大小的内存分 配方法称之为静态内存分配。这种内存分配的方法存在比较严重的缺陷,在大多数情况下会浪费大量的内存空间,在少数情况下,当你定义的数组不够大时,可能引起下标越界错 误,甚至导致严重后果。

      我们用动态内存分配就可以解决上面的问题。所谓动态内存分配就是指在程序执行的 过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不象数组等静态内 存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的 大小就是程序要求的大小。

三、heap_4.c

     我们这里只讲heap_4.c 因为这个最常用 。 内 存 分 配 时 需 要 的 总 的 堆 空 间 由 文 件 FreeRTOSConfig.h 中 的 宏 configTOTAL_HEAP_SIZE 配置,单位为字。通过调用函数 xPortGetFreeHeapSize() 我们可 以知道还剩下多少内存没有使用,但是并不包括内存碎片。这样一来我们可以实时的调整 和优化 configTOTAL_HEAP_SIZE 的大小。

      heap_4.c 方案的空闲内存块也是以单链表的形式连接起来的,BlockLink_t 类型的局部 静态变量 xStart 表示链表头,但 heap_4.c 内存管理方案的链表尾部则保存在内存堆空间最 后位置,并使用 BlockLink_t 指针类型局部静态变量 pxEnd 指向这个区域(而 heap_2.c 内 存管理方案则使用 BlockLink_t 类型的静态变量 xEnd 表示链表尾)

      heap_4.c 内存管理方案的空闲块链表不是以内存块大小进行排序的,而是以内存块起 始地址大小排序,内存地址小的在前,地址大的在后,因为 heap_4.c 方案还有一个内存合 并算法,在释放内存的时候,假如相邻的两个空闲内存块在地址上是连续的,那么就可以 合并为一个内存块,这也是为了适应合并算法而作的改变。

      heap_4.c 方案具有以下特点:

     1、可用于重复删除任务、队列、信号量、互斥量等的应用程序

     2、可用于分配和释放随机字节内存的应用程序,但并不像 heap2.c 那样产生严重的内 存碎片。

     3、具有不确定性,但是效率比标准 C 库中的 malloc 函数高得多。

1.内存申请函数 pvPortMalloc()

      heap_4.c 方案的内存申请函数与 heap_2.c 方案的内存申请函数大同小异,同样是从链 表头 xStart 开始遍历查找合适的内存块,如果某个空闲内存块的大小能容得下用户要申请 的内存,则将这块内存取出用户需要内存空间大小的部分返回给用户,剩下的内存块组成 一个新的空闲块,按照空闲内存块起始地址大小顺序插入到空闲块链表中,内存地址小的 在前,内存地址大的在后。在插入到空闲内存块链表的过程中,系统还会执行合并算法将 地址相邻的内存块进行合并:判断这个空闲内存块是相邻的空闲内存块合并成一个大内存 块,如果可以则合并,合并算法是 heap_4.c 内存管理方案和 heap_2.c 内存管理方案最大的 不同之处,这样一来,会导致的内存碎片就会大大减少,内存管理方案适用性就很强,能 一样随机申请和释放内存的应用中,灵活性得到大大的提高,heap_4.c 内存初始化完成示意图具体见图1。

heap_4.c 的内存 申请源码

void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;

	vTaskSuspendAll();
	{
		/* If this is the first call to malloc then the heap will require
		initialisation to setup the list of free blocks. */
		if( pxEnd == NULL )
		{
			prvHeapInit();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

		/* Check the requested block size is not so large that the top bit is
		set.  The top bit of the block size member of the BlockLink_t structure
		is used to determine who owns the block - the application or the
		kernel, so it must be free. */
		if( ( xWantedSize & xBlockAllocatedBit ) == 0 )
		{
			/* The wanted size is increased so it can contain a BlockLink_t
			structure in addition to the requested amount of bytes. */
			if( xWantedSize > 0 )
			{
				xWantedSize += xHeapStructSize;

				/* Ensure that blocks are always aligned to the required number
				of bytes. */
				if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
				{
					/* Byte alignment required. */
					xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
					configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}

			if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
			{
				/* Traverse the list from the start	(lowest address) block until
				one	of adequate size is found. */
				pxPreviousBlock = &xStart;
				pxBlock = xStart.pxNextFreeBlock;
				while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
				{
					pxPreviousBlock = pxBlock;
					pxBlock = pxBlock->pxNextFreeBlock;
				}

				/* If the end marker was reached then a block of adequate size
				was	not found. */
				if( pxBlock != pxEnd )
				{
					/* Return the memory space pointed to - jumping over the
					BlockLink_t structure at its start. */
					pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );

					/* This block is being returned for use so must be taken out
					of the list of free blocks. */
					pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

					/* If the block is larger than required it can be split into
					two. */
					if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
					{
						/* This block is to be split into two.  Create a new
						block following the number of bytes requested. The void
						cast is used to prevent byte alignment warnings from the
						compiler. */
						pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
						configASSERT( ( ( ( size_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 );

						/* Calculate the sizes of two blocks split from the
						single block. */
						pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
						pxBlock->xBlockSize = xWantedSize;

						/* Insert the new block into the list of free blocks. */
						prvInsertBlockIntoFreeList( pxNewBlockLink );
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}

					xFreeBytesRemaining -= pxBlock->xBlockSize;

					if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
					{
						xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}

					/* The block is being returned - it is allocated and owned
					by the application and has no "next" block. */
					pxBlock->xBlockSize |= xBlockAllocatedBit;
					pxBlock->pxNextFreeBlock = NULL;
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

		traceMALLOC( pvReturn, xWantedSize );
	}
	( void ) xTaskResumeAll();

	#if( configUSE_MALLOC_FAILED_HOOK == 1 )
	{
		if( pvReturn == NULL )
		{
			extern void vApplicationMallocFailedHook( void );
			vApplicationMallocFailedHook();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
	#endif

	configASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );
	return pvReturn;
}

图1 内存初始化完成示意图

2.内存释放函数 vPortFree()

    heap_4.c 内存管理方案的内存释放函数 vPortFree()也比较简单,根据传入要释放的内 存块地址,偏移之后找到链表节点,然后将这个内存块插入到空闲内存块链表中,在内存 块插入过程中会执行合并算法,这个我们已经在内存申请中讲过了(而且合并算法多用于 释放内存中)。最后是将这个内存块标志为“空闲”(内存块节点的 xBlockSize 成员变量 最高位清 0)、再更新未分配的内存堆大小即可,下面来看看 vPortFree()的源码实现过程。

void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;

	if( pv != NULL )
	{
		/* The memory being freed will have an BlockLink_t structure immediately
		before it. */
		puc -= xHeapStructSize;

		/* This casting is to keep the compiler from issuing warnings. */
		pxLink = ( void * ) puc;

		/* Check the block is actually allocated. */
		configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );
		configASSERT( pxLink->pxNextFreeBlock == NULL );

		if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 )
		{
			if( pxLink->pxNextFreeBlock == NULL )
			{
				/* The block is being returned to the heap - it is no longer
				allocated. */
				pxLink->xBlockSize &= ~xBlockAllocatedBit;

				vTaskSuspendAll();
				{
					/* Add this block to the list of free blocks. */
					xFreeBytesRemaining += pxLink->xBlockSize;
					traceFREE( pv, pxLink->xBlockSize );
					prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
				}
				( void ) xTaskResumeAll();
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
}

      调用 prvInsertBlockIntoFreeList()函数将释放的内存块添加到空闲 内存块链表中,在这过程中,如果内存块可以合并就会进行内存块合并,否则就单纯插入 空闲内存块链表(按内存地址排序)。 按照内存释放的过程,当我们释放一个内存时,如果与它相邻的内存块都不是空闲的, 那么该内存块并不会合并,只会被添加到空闲内存块链表中,其过程示意图具体见图 2。而如果某个时间段释放了另一个内存块,发现该内存块前面有一个空闲内存块与它 在地址上是连续的,那么这两个内存块会合并成一个大的内存块,并插入空闲内存块链表 中,其过程示意图具体见图 3,

图二释放一个内存块(无法合并)

图3 释放一个内存块(可以合并)

 四、内存管理的实验

      内存管理实验使用 heap_4.c 方案进行内存管理测试,创建了两个任务,分别是 LED 任 务与内存管理测试任务,内存管理测试任务通过检测按键是否按下来申请内存或释放内存, 当申请内存成功就像该内存写入一些数据,如当前系统的时间等信息,并且通过串口输出 相关信息;LED 任务是将 LED 翻转,表示系统处于运行状态。在不需要再使用内存时,注 意要及时释放该段内存,避免内存泄露。

/* FreeRTOS头文件 */
#include "FreeRTOS.h"
#include "task.h"
/* 开发板硬件bsp头文件 */
#include "bsp_led.h"
#include "bsp_usart.h"
#include "bsp_key.h"
/**************************** 任务句柄 ********************************/
/* 
 * 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
 * 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
 * 这个句柄可以为NULL。
 */
static TaskHandle_t AppTaskCreate_Handle = NULL;/* 创建任务句柄 */
static TaskHandle_t LED_Task_Handle = NULL;/* LED_Task任务句柄 */
static TaskHandle_t Test_Task_Handle = NULL;/* Test_Task任务句柄 */



/******************************* 全局变量声明 ************************************/
/*
 * 当我们在写应用程序的时候,可能需要用到一些全局变量。
 */
uint8_t *Test_Ptr = NULL;


/*
*************************************************************************
*                             函数声明
*************************************************************************
*/
static void AppTaskCreate(void);/* 用于创建任务 */

static void LED_Task(void* pvParameters);/* LED_Task任务实现 */
static void Test_Task(void* pvParameters);/* Test_Task任务实现 */

static void BSP_Init(void);/* 用于初始化板载相关资源 */

/*****************************************************************
  * @brief  主函数
  * @param  无
  * @retval 无
  * @note   第一步:开发板硬件初始化 
            第二步:创建APP应用任务
            第三步:启动FreeRTOS,开始多任务调度
  ****************************************************************/
int main(void)
{	
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  
  /* 开发板硬件初始化 */
  BSP_Init();
	printf("这是一个FreeRTOS内存管理实验\n");
  printf("按下KEY1申请内存,按下KEY2释放内存\n");
   /* 创建AppTaskCreate任务 */
  xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                        (const char*    )"AppTaskCreate",/* 任务名字 */
                        (uint16_t       )512,  /* 任务栈大小 */
                        (void*          )NULL,/* 任务入口函数参数 */
                        (UBaseType_t    )1, /* 任务的优先级 */
                        (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
  /* 启动任务调度 */           
  if(pdPASS == xReturn)
    vTaskStartScheduler();   /* 启动任务,开启调度 */
  else
    return -1;  
  
  while(1);   /* 正常不会执行到这里 */    
}


/***********************************************************************
  * @ 函数名  : AppTaskCreate
  * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
  * @ 参数    : 无  
  * @ 返回值  : 无
  **********************************************************************/
static void AppTaskCreate(void)
{
  BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
  
  taskENTER_CRITICAL();           //进入临界区

  /* 创建LED_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )LED_Task, /* 任务入口函数 */
                        (const char*    )"LED_Task",/* 任务名字 */
                        (uint16_t       )512,   /* 任务栈大小 */
                        (void*          )NULL,	/* 任务入口函数参数 */
                        (UBaseType_t    )2,	    /* 任务的优先级 */
                        (TaskHandle_t*  )&LED_Task_Handle);/* 任务控制块指针 */
  if(pdPASS == xReturn)
    printf("创建LED_Task任务成功\n");
  
  /* 创建Test_Task任务 */
  xReturn = xTaskCreate((TaskFunction_t )Test_Task,  /* 任务入口函数 */
                        (const char*    )"Test_Task",/* 任务名字 */
                        (uint16_t       )512,  /* 任务栈大小 */
                        (void*          )NULL,/* 任务入口函数参数 */
                        (UBaseType_t    )3, /* 任务的优先级 */
                        (TaskHandle_t*  )&Test_Task_Handle);/* 任务控制块指针 */ 
  if(pdPASS == xReturn)
    printf("创建Test_Task任务成功\n\n");
  
  vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
  
  taskEXIT_CRITICAL();            //退出临界区
}



/**********************************************************************
  * @ 函数名  : LED_Task
  * @ 功能说明: LED_Task任务主体
  * @ 参数    :   
  * @ 返回值  : 无
  ********************************************************************/
static void LED_Task(void* parameter)
{	
  while (1)
  {
    LED1_TOGGLE;
    vTaskDelay(1000);/* 延时1000个tick */
  }
}

/**********************************************************************
  * @ 函数名  : Test_Task
  * @ 功能说明: Test_Task任务主体
  * @ 参数    :   
  * @ 返回值  : 无
  ********************************************************************/
static void Test_Task(void* parameter)
{	 
  uint32_t g_memsize;
  while (1)
  {
    if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
    {
      /* KEY1 被按下 */
      if(NULL == Test_Ptr)
      {
                  
        /* 获取当前内存大小 */
        g_memsize = xPortGetFreeHeapSize();
        printf("系统当前内存大小为 %d 字节,开始申请内存\n",g_memsize);
        Test_Ptr = pvPortMalloc(1024);
        if(NULL != Test_Ptr)
        {
          printf("内存申请成功\n");
          printf("申请到的内存地址为%#x\n",(int)Test_Ptr);

          /* 获取当前内剩余存大小 */
          g_memsize = xPortGetFreeHeapSize();
          printf("系统当前内存剩余存大小为 %d 字节\n",g_memsize);
                  
          //向Test_Ptr中写入当数据:当前系统时间
          sprintf((char*)Test_Ptr,"当前系统TickCount = %d \n",xTaskGetTickCount());
          printf("写入的数据是 %s \n",(char*)Test_Ptr);
        }
      }
      else
      {
        printf("请先按下KEY2释放内存再申请\n");
      }
    } 
    if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
    {
      /* KEY2 被按下 */
      if(NULL != Test_Ptr)
      {
        printf("释放内存\n");
        vPortFree(Test_Ptr);	//释放内存
        Test_Ptr=NULL;
        /* 获取当前内剩余存大小 */
        g_memsize = xPortGetFreeHeapSize();
        printf("系统当前内存大小为 %d 字节,内存释放完成\n",g_memsize);
      }
      else
      {
        printf("请先按下KEY1申请内存再释放\n");
      }
    }
    vTaskDelay(20);/* 延时20个tick */
  }
}

/***********************************************************************
  * @ 函数名  : BSP_Init
  * @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
  * @ 参数    :   
  * @ 返回值  : 无
  *********************************************************************/
static void BSP_Init(void)
{
	/*
	 * STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
	 * 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
	 * 都统一用这个优先级分组,千万不要再分组,切忌。
	 */
	NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
	
	/* LED 初始化 */
	LED_GPIO_Config();

	/* 串口初始化	*/
	USART_Config();
  
  /* 按键初始化	*/
  Key_GPIO_Config();

}

/********************************END OF FILE****************************/

五、内存管理的实验现象

程序编译好,用 USB 线连接电脑和开发板的 USB 接口(对应丝印为 USB 转串口), 用 DAP 仿真器把配套程序下载到 STM32 开发板,在电脑上打开串口调试助手,然后复位开发板,我 们按下 KEY1 申请内存,然后按下 KEY2 释放内存,可以在调试助手中看到串口打印信息 与运行结果,开发板的 LED 也在闪烁。

猜你喜欢

转载自blog.csdn.net/qq_61672347/article/details/125670837
今日推荐