• STM32CubeMX实战教程(九)——外部SRAM+内存管理


    前言

    内存管理作为STM32及其他单片机非常重要的知识,可以说是单片机学习中必须要学到的,它不是像其他知识一样基于外设展开,而是基于自身内部的内存或是外部内存出发的;是学习较高级复杂的外设或功能如:USB,emWin,以及操作系统的基础,因为这是当单片机功能越来越复杂时绕不开的问题
    简单举个栗子:有时候需要申请一个很大的内存时,虽然现在STM32具备的内存已经非常大了,但是还是会出现不够用的情况,但是通过内存管理,可以根据所需内存是否长期需要,以及其他条件去判断是请外部内存,还是释放部分当前不用的内存来进行分配。

    材料

    • STM32F4正点原子探索者
    • 开发板原理图
    • HAL库函数手册

    运行内存RAM

    STM32F4的RAM主要由内部SRAM,内部CCRAM构成,如果有增加外部SRAM,那就由这三部分构成,下面简单说明一下三者区别

    SRAM

    静态随机存取存储器(Static Random-Access Memory,SRAM)是随机存取存储器的一种。所谓的“静态”,是指这种存储器只要保持通电,里面储存的数据就可以恒常保持。相对之下,动态随机存取存储器(DRAM)里面所储存的数据就需要周期性地更新。然而,当电力供应停止时,SRAM储存的数据还是会消失(被称为volatile memory),这与在断电后还能储存资料的ROM或闪存是不同的。
    内部SRAM则是单片机本身自带的SRAM,其大小都在STM32CubeMX选型中就可以看到,STM32F407ZG是自带192K的RAM,其中包括了SRAM和CCRAM。
    在这里插入图片描述
    外部SRAM则是有外部RAM芯片提供,内部空间不够时使用。

    内部CCRAM

    相比内部SRAM,内部CCRAM的读写速度会更加快,但它只受CPU直接控制,而不受外设读写控制。其他与内部SRAM无异,地址也在RAM中。

    IS62WV51216

    外部SRAM芯片,512K,顾名思义作为内部SRAM的补充和扩展,我们可以直接把变量定义在外部SRAM中,具体性能这里不详细介绍,感兴趣的话百度都能查,这里主要详细看看它的时序及工作方式。数据手册里面也是写得非常清楚了。
    在这里插入图片描述

    • 图中A0~18为地址线,总共19根地址线(即2^19=512K,1K=1024);
    • I/O0~15为数据线,总共16根数据线
    • CS2和CS1都是片选信号,不过CS2是高电平有效CS1是低电平有效;OE是输出使能信号(读信号);WE为写使能信号;UB和LB分别是高字节控制和低字节控制信号,即允许单独进行高8位或低8位的数据写入操作;

    在这里插入图片描述

    然后再根据各个时序图和时序表,一顿操作后得到几组读写时序数据,懒得计算的小伙伴也可以直接上正点原子的例程抄结果,结果和抄的姿势后面将列出。

    内存管理

    在面对有限的内存时,合理运用好内存管理可以使得MCU的资源分配更高效且快速,正如C语言中,当我们面对长度未知的变量时,往往采用动态内存分配的方法,利用malloc函数和指针,进行内存的动态申请和释放,STM32也可以用同样的原理进行内存的分配和调用,具体原理本文暂不深究,重在用法的掌握。

    工程配置

    基础配置

    基础配置与前面其他文章完全一致,时钟主频为168M,使能一个LED灯,以及三个按键,便于现象观察
    晶振

    时钟树
    LED
    在这里插入图片描述

    另外把LCD液晶使能一下,方便观察现象,具体配置如下,不懂的参考《STM32CubeMX实战教程(七)——TFT_LCD液晶显示》,当然也可以用串口输出。

    在这里插入图片描述
    在这里插入图片描述

    FSMC

    对于FSMC外设的作用和原理,已经在《STM32CubeMX实战教程(七)——TFT_LCD液晶显示》介绍过了,这里也不再重复,这里直接开始配置。
    在这里插入图片描述

    由于NE4已经使能给LCD显示了,这里就使用NE3作为SRAM,其对应的地址范围为0x68000000~6BFFFFFF当然也可以用NE1或者NE2,但是在代码中的基地址则需要根据下表相对应修改,具体在生成代码后将进行解释。
    在这里插入图片描述

    根据IS62WV51216芯片介绍,需要19位地址信号及6位数据信号,于是对应了这里的Address和Data,同时由于该芯片支持字节控制,我们需要另外提供高低字节信号,即使能Byte enable。不使能等待信号。
    以下是参数配置,
    在这里插入图片描述
    Wirte Operation

    • Enable 允许向存储器写数据
    • Disabled 只允许从存储器中读数据

    Extended

    • Disable 读写时序相同
    • Enable 读写时序不同(分别配置)

    然后是信号时序,直接对着抄就完事。

    进入代码

    生成代码后,先在Options for target中添加外部SRAM的地址和大小,此时编译器在变量的地址自动分配时也会把外部SRAM考虑在内了,如果想修改内存自动分配的优先级的话可以编写sct文件的方法实现,这不是本文重点,暂不深究。
    在这里插入图片描述
    default上的选项框不建议勾上,因为勾上即代表所选内存由编译器管理分配,即这时候不允许用户在此内存定义变量,所以也就不能用__attribute__关键字把变量定义在这个地方,否则出现类似以下的报错
    在这里插入图片描述

    attribute((at()))

    uint32_t test[250000] __attribute__((at(0X68000000)));
    
    • 1

    在MDK中__attribute__是一个可以指定所定义变量地址的关键字,例如上述代码,可以利用这个关键字将数组test直接定义到指定地址0X68000000,非常灵活。但是需要注意的是,用这种方法定义变量时必须将变量定义成全局变量,即函数外。

    动态内存分配

    要实现内存的动态分配,就要有动态分配函数,对应C语言中的malloc函数,但是STM32本身并不自带这个功能,需要用户自己编写对应接口函数,这里正点原子也已经有成熟的malloc文件了,我对其稍作修改以适配HAL库版本开发,大家可以自行下载,下载链接,提取码为jmnj,下面我将为大家讲解基本函数及其用法。

    malloc.h

    
    #define MEM1_BLOCK_SIZE			32  	  						
    #define MEM1_MAX_SIZE			110*1024  						
    #define MEM1_ALLOC_TABLE_SIZE	MEM1_MAX_SIZE/MEM1_BLOCK_SIZE 	
    
    #define MEM2_BLOCK_SIZE			32  	  						
    #define MEM2_MAX_SIZE			800 *1024  						
    #define MEM2_ALLOC_TABLE_SIZE	MEM2_MAX_SIZE/MEM2_BLOCK_SIZE 	
    		 
    
    #define MEM3_BLOCK_SIZE			32  	  						
    #define MEM3_MAX_SIZE			60 *1024  						
    #define MEM3_ALLOC_TABLE_SIZE	MEM3_MAX_SIZE/MEM3_BLOCK_SIZE 	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    首先进入malloc.h代码,在第21行到第34行定义了各部分内存的内存大小,如上的参数MEM~_MAX_SIZE是可以根据实际灵活调整的,这里是指需要管理的内存大小,并不一定要等于实际,可以偏小,但是不能偏大,例如根据实际STM32型号不同或是外部SRAM芯片不同可以将如上的110,800,60几个参数对应修改。

    void my_mem_init(uint8_t memx);		
    uint8_t my_mem_perused(uint8_t memx);		
    
    void myfree(uint8_t memx,void *ptr);  		
    void *mymalloc(uint8_t memx,uint32_t size);			
    void *myrealloc(uint8_t memx,void *ptr,uint32_t size);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    往下可以看到有几个函数,这里只需要关心以上几个函数就可以了,其他都是供内部调用使用的

    • my_mem_init,这个函数是负责初始化内存块的,只有一个参数,填SRAMIN表示内部内存池;SRAMEX表示外部内存池;SRAMCCM表示CCM内存池
    • my_mem_perused,这个函数可以用来获得内存的使用率,同样只有一个参数,填入数据同上,返回参数为内存使用率,即0~100
    • myfree,内存释放函数,同C语言的free函数功能,输入参数为内存种类和首地址,那么所指定的内存块中指定地址之后的内存都会被释放
    • mymalloc,内存申请函数,参数为内存块种类即需要申请的大小,单位为字节,返回申请到的内存首地址
    • myrealloc,重新分配内存函数,输入参数为内存块种类,旧首地址和需要分配的内存大小,作用是将指定内存里的内容拷贝至新内存中,并返回新内存的首地址

    malloc.c

    在这个文件中我们需要关注的只有一处,也就是SRAM内存地址,在这个文件中是0X68000000,也就是对应FSMC配置时所选的NE3,如果选的是NE2,那么这里则改成0x64000000,按照FSMC一节表中列出的参数,以此类推,当然CCM内存的地址也可以对应实际情况进行修改。
    这里再次强调,由于这里用到了关键字__attribute__,在Options for target中的target栏,如果填了对应的内存地址,那么在default选项框内千万不能打勾,否则编译时报错!!!

    实验代码

    由于内存管理和外部SRAM的实验本身是没有实验现象的,为了能够看到初始化成功的现象,这里参考正点原子的代码另外加入了实验现象的代码以便观察。具体原理不多解释了,直接抄代码就行。如果是用串口输出现象的话就把LCD相关输出函数改成串口打印。

    main

    首先,在main.c文件中引用

    #include "ILI93xx.h"
    #include "malloc.h" 
    
    • 1
    • 2

    不用液晶的话只包含malloc.h就行
    其次,在main函数中定义新变量

    	uint8_t key;
    	uint8_t sramx=0;	
    	uint8_t i=0;	    
    	uint8_t *p=0;
    	uint8_t *tp=0;
    	uint8_t paddr[18];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在while循环前添加

    	TFTLCD_Init();
    	LCD_Clear(WHITE);
    	my_mem_init(SRAMIN);			//³õʼ»¯ÄÚ²¿ÄÚ´æ³Ø
    	my_mem_init(SRAMEX);			//³õʼ»¯ÍⲿÄÚ´æ³Ø
    	my_mem_init(SRAMCCM);			//³õʼ»¯CCMÄÚ´æ³Ø
    		POINT_COLOR=RED;//ÉèÖÃ×ÖÌåΪºìÉ« 
    	LCD_ShowString(30,50,200,16,16,"Explorer STM32F4");	
    	LCD_ShowString(30,70,200,16,16,"MALLOC TEST");	
    	LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
    	LCD_ShowString(30,110,200,16,16,"2014/5/15");   
    	LCD_ShowString(30,130,200,16,16,"KEY0:Malloc  KEY2:Free");
    	LCD_ShowString(30,150,200,16,16,"KEY_UP:SRAMx KEY1:Read"); 
     	POINT_COLOR=BLUE;//ÉèÖÃ×ÖÌåΪÀ¶É« 
    	LCD_ShowString(30,170,200,16,16,"SRAMIN");
    	LCD_ShowString(30,190,200,16,16,"SRAMIN  USED:   %");
    	LCD_ShowString(30,210,200,16,16,"SRAMEX  USED:   %");
    	LCD_ShowString(30,230,200,16,16,"SRAMCCM USED:   %");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在while循环中添加

    				key=KEY_Scan(0);
    				switch(key)
    		{
    			case 0:	
    				break;
    			case KEY0_PRES:
    				p=mymalloc(sramx,2048);
    				if(p!=NULL)sprintf((char*)p,"Memory Malloc Test%03d",i);
    				break;
    			case KEY1_PRES:	   
    				if(p!=NULL)
    				{
    					sprintf((char*)p,"Memory Malloc Test%03d",i);
    					LCD_ShowString(30,270,200,16,16,p);			
    				}
    				break;
    			case KEY2_PRES:	  
    				myfree(sramx,p);
    				p=0;			
    				break;
    			case WKUP_PRES:
    				sramx++; 
    				if(sramx>2)sramx=0;
    				if(sramx==0)LCD_ShowString(30,170,200,16,16,"SRAMIN ");
    				else if(sramx==1)LCD_ShowString(30,170,200,16,16,"SRAMEX ");
    				else LCD_ShowString(30,170,200,16,16,"SRAMCCM");
    				break;
    		}
    				if(tp!=p)
    		{
    			tp=p;
    			sprintf((char*)paddr,"P Addr:0X%08X",(uint32_t)tp);
    			LCD_ShowString(30,250,200,16,16,paddr);	
    			if(p)LCD_ShowString(30,270,200,16,16,p);
    		    else LCD_Fill(30,270,239,266,WHITE);	
    		}
    		HAL_Delay(10);   
    		i++;
    		if((i%20)==0)//DS0ÉÁ˸.
    		{
    			LCD_ShowNum(30+104,190,my_mem_perused(SRAMIN),3,16);
    			LCD_ShowNum(30+104,210,my_mem_perused(SRAMEX),3,16);
    			LCD_ShowNum(30+104,230,my_mem_perused(SRAMCCM),3,16);
    			HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
    		}
    
    • 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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    在main.h添加下面代码用于位带操作

    #define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2)) 
    #define MEM_ADDR(addr)  *((volatile unsigned long  *)(addr)) 
    #define BIT_ADDR(addr, bitnum)   MEM_ADDR(BITBAND(addr, bitnum)) 
    
    #define GPIOA_ODR_Addr    (GPIOA_BASE+20) 
    #define GPIOB_ODR_Addr    (GPIOB_BASE+20)
    #define GPIOC_ODR_Addr    (GPIOC_BASE+20) 
    #define GPIOD_ODR_Addr    (GPIOD_BASE+20) 
    #define GPIOE_ODR_Addr    (GPIOE_BASE+20)
    #define GPIOF_ODR_Addr    (GPIOF_BASE+20)
    #define GPIOG_ODR_Addr    (GPIOG_BASE+20) 
    #define GPIOH_ODR_Addr    (GPIOH_BASE+20) 
    #define GPIOI_ODR_Addr    (GPIOI_BASE+20)
    #define GPIOJ_ODR_ADDr    (GPIOJ_BASE+20)
    #define GPIOK_ODR_ADDr    (GPIOK_BASE+20) 
    
    #define GPIOA_IDR_Addr    (GPIOA_BASE+16)
    #define GPIOB_IDR_Addr    (GPIOB_BASE+16)
    #define GPIOC_IDR_Addr    (GPIOC_BASE+16) 
    #define GPIOD_IDR_Addr    (GPIOD_BASE+16) 
    #define GPIOE_IDR_Addr    (GPIOE_BASE+16) 
    #define GPIOF_IDR_Addr    (GPIOF_BASE+16) 
    #define GPIOG_IDR_Addr    (GPIOG_BASE+16) 
    #define GPIOH_IDR_Addr    (GPIOH_BASE+16) 
    #define GPIOI_IDR_Addr    (GPIOI_BASE+16) 
    #define GPIOJ_IDR_Addr    (GPIOJ_BASE+16) 
    #define GPIOK_IDR_Addr    (GPIOK_BASE+16) 
    
    #define PAout(n)   BIT_ADDR(GPIOA_ODR_Addr,n) 
    #define PAin(n)    BIT_ADDR(GPIOA_IDR_Addr,n)  
    
    #define PBout(n)   BIT_ADDR(GPIOB_ODR_Addr,n) 
    #define PBin(n)    BIT_ADDR(GPIOB_IDR_Addr,n) 
    
    #define PCout(n)   BIT_ADDR(GPIOC_ODR_Addr,n) 
    #define PCin(n)    BIT_ADDR(GPIOC_IDR_Addr,n)
    
    #define PDout(n)   BIT_ADDR(GPIOD_ODR_Addr,n) 
    #define PDin(n)    BIT_ADDR(GPIOD_IDR_Addr,n) 
    
    #define PEout(n)   BIT_ADDR(GPIOE_ODR_Addr,n)
    #define PEin(n)    BIT_ADDR(GPIOE_IDR_Addr,n)  
    
    #define PFout(n)   BIT_ADDR(GPIOF_ODR_Addr,n)  
    #define PFin(n)    BIT_ADDR(GPIOF_IDR_Addr,n)  
    
    #define PGout(n)   BIT_ADDR(GPIOG_ODR_Addr,n) 
    #define PGin(n)    BIT_ADDR(GPIOG_IDR_Addr,n)  
    
    #define PHout(n)   BIT_ADDR(GPIOH_ODR_Addr,n)
    #define PHin(n)    BIT_ADDR(GPIOH_IDR_Addr,n)
    
    #define PIout(n)   BIT_ADDR(GPIOI_ODR_Addr,n)
    #define PIin(n)    BIT_ADDR(GPIOI_IDR_Addr,n) 
    
    #define PJout(n)   BIT_ADDR(GPIOJ_ODR_Addr,n)  
    #define PJin(n)    BIT_ADDR(GPIOJ_IDR_Addr,n)  
    
    #define PKout(n)   BIT_ADDR(GPIOK_ODR_Addr,n)  
    #define PKin(n)    BIT_ADDR(GPIOK_IDR_Addr,n)  
    
    • 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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    gpio

    gpio.c定义函数

    uint8_t KEY_Scan(uint8_t mode)
    {
        static uint8_t key_up=1;     //按键松开标志
        if(mode==1)key_up=1;  //支持连按
        if(key_up&&(KEY0==0||KEY1==0||KEY2==0||WK_UP==1))
        {
            HAL_Delay(10);
            key_up=0;
            if(KEY0==0)       return KEY0_PRES;
            else if(KEY1==0)  return KEY1_PRES;
            else if(KEY2==0)  return KEY2_PRES;
            else if(WK_UP==1) return WKUP_PRES;          
        }else if(KEY0==1&&KEY1==1&&KEY2==1&&WK_UP==0)key_up=1;
        return 0; //无按键按下
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这是正点原子的按键算法,比普通延时消抖可靠性更高,且支持连按,很好看懂,这里不多解释。在gpio.h中添加

    #define KEY0        HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_4)  //KEY0按键PE4
    #define KEY1        HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_3)  //KEY1按键PE3
    #define KEY2        HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_2) 	//KEY2按键PE2
    #define WK_UP       HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)  //WKUP按键PA0
    
    #define KEY0_PRES 	1
    #define KEY1_PRES	2
    #define KEY2_PRES	3
    #define WKUP_PRES   4
    uint8_t KEY_Scan(uint8_t mode);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    以上就是所有代码,接下来就可以下载至板子中看现象了。

    下载验证

    下载进开发板后可以根据屏幕上的提示进行操作了,KEY0为申请内存,KEY2为释放内存,KEY1为读内存,KEY_UP为切换内存块
    在这里插入图片描述
    具体下载方法这里不再重复,可查看《STM32CubeMX实战教程(一)——软件入门》,工程源文件我已经上传,

    结语

    本章就是STM32CubeMX的最后一篇实战教程了,考虑到其他外设的配置其实也可以依葫芦画瓢,以及更高阶的外设和STM32功能其实并不常用,而且也更加复杂,没办法一篇文章就能够讲清楚,所以教程数量并不多,但是我会把我做的例程都放在资源处,有需要的自行下载,没积分的私我邮箱,感谢大家支持,以后如果有其他教程或者学习笔记可能也会另外记录讲解~
    祝大家事业蒸蒸日上!
    奥里给~

  • 相关阅读:
    “干翻”GPT-3,Meta 用开源发起攻势
    Spring Boot:Dao层-实例介绍
    使用stream流来对结果进行计算
    【毕业设计】 基于STM32的人体红外测温枪温度采集系统
    一键自动化博客发布工具,用过的人都说好(segmentfault篇)
    java基于移动端的产后康复机构管理系统uniapp
    【KingFusion】如何在3D场景实现流水效果
    Java 中如何将一个类中方法的局部变量在另一个方法中调用?
    【LeetCode刷题】-- 50.Pow(x,n)
    pyqt---子线程进行gui操作导致界面崩溃
  • 原文地址:https://blog.csdn.net/weixin_43892323/article/details/122989100