发帖数

50

原创数

50

关注者

12

阅读数

11349

点赞数

4

蔡琰

  • 堆内存的那些事

    上一篇我们分享了栈内存的概念,现在我们分享下堆内存的概念。


    在一般的编译系统中,堆内存的分配方向和栈内存是相反的。当栈内存从高地址向低地址增长的时候,堆内存从低地址向高地址分配。

    C语言中,堆内存在分配和释放的时候,是程序通过调用C语言的库函数完成的。这和栈内存的分配有区别,栈内存利用的是处理器的硬件机制,而堆内存的处理使用的是库函数。


    我们来看下堆内存的分配情况:

    1.jpg

    在堆内存的分配过程中,每次分配将返回一个当前分配地址的指针。在程序中如果多次分配内存,可以得到多个内存指针,每个内存指针都是本次分配内存的地址。在释放内存的时候,只需要对每个指针进行操作,那个指针所指向的内存就会被释放,而对其他的内存区域没有影响。


    从内存的分配和使用上,可以看出栈内存和堆内存的区别:栈内存只有一个入口点,就是栈指针,栈内存压入和弹出的时候栈指针将发生变化,栈指针标识当前栈区域中已使用和未使用的界限,程序在访问栈内存的时候都只能通过栈指针及其偏移量;而堆内存有多个入口点,每次分配得到的指针是访问内存的入口,每个分配内存区域都可以被单独释放,程序对堆内存可以通过每次分配得到的指针访问。


    堆内存有一个整体分配的过程,按照向上的堆内存分配方向。随着堆内存使用量的增加,堆内存将逐渐向高地址分配。这只是一个大体的增长的方面,在堆内存中,已使用的区域和未使用的区域是交错的,而不是像栈区域那样有明显的分界线。

    堆内存的释放看下面这个图:


    2.jpg

    看到这样频繁的使用区域和释放,那么很容易看出堆内存是不连续的,跟堆内存的使用方式有关系,这个分配就相对自由灵活了,但是也是会在低地址向高地址发展的方向分配的。


    比如上面释放后再分配就可以是下面两种情况:

    3.jpg

    先看再次分配1的情况:当新分配的需求比中间(刚刚释放)区域小,那么就会在紧接着的区域给分配。


    再看再次分配2的情况:当新分配的需求比中间(释放的)区域大,那么只能往后寻求能给的区域。

    当频繁的分配和释放内存的过程中,会很容易出现在两块已经分配的内存之间较小的未分配内存区域,这些其实可以用,但是由于他们的空间比较小,不够连续内存的分配,所以分配的时候就很难再次使用,这些较小的内存就是我们常说的内存碎片。


    我们再来聊一下在C程序中堆空间的使用。

    C语言中,堆内存区域的分配和释放是通过调用库函数来完成的,实现的函数主要有四个:

    void *malloc(size_t size);    //分配内存空间

    void free(void *ptr);     //释放内存空间

    void *calloc(size_t nmemb,size_t size);  //分配内存空间

    void *realloc(void * ptr,size_t size);  //重新分配内存空间

    注意:使用上面这几个函数需要包含标准库文件 <stdlib.h>

    那么库函数怎么使用呢,内存分配了就要有释放,那么常用的就是malloc()free()两个函数。malloc()函数的输入是需要分配内存的大小,输出是分配内存的指针。如果分配不成功,则返回NULL


    free()函数的输入是需要释放的指针,可以接受任何形式的指针。这个指针必须是由分配函数分配出来的。

    例如:

    int *pa;

    pa = (int *)malloc(sizeof(int));//分配一个int大小的指针

    if(NULL != pa)

    {

    free(pa);

    }

    内存使用完成需要释放,以便分配给其他程序使用。


    calloc()也是内存分配的,只是可以把分配好的内存区域的初始值全部设置为0。还有这个分配内存有两个参数,第一个是分配单元的大小,第二个是要分配的数目。

    malloc(sizeof(unsigned int)*10);   ==     calloc(sizeof(unsigned int),10)


    realloc()有两个参数,一个是指向内存的地址指针,一个是要重分配内存的大小,返回值是指向所分配内存的指针。

    1、当参数指针为NULL的时候,作为malloc使用,分配内存

    2、当重分配内存大小为0的时候,作为free使用,释放内存

    3、当指针和重分配内存大小均不为0的时候,根据指针指向的堆内存区域的情况和指针大小重新分配内存。


    对于realloc()作为重新分配内存的时候,有三种可能出现:

    1、缩小内存

    2、扩大内存,不需要移动指针

    3、扩大内存,需要移动指针(指定内存区域大小不够)


    在堆内存的管理上,主要容易出现以下几个问题:

    1、开辟的内存没有释放,造成内存泄漏(系统不会释放任何用户分配的内存)

    2、野指针被使用或释放(内存释放后,需要将内存指针置为NULL

    3、非法释放指针(分配了有效内存才存在释放,否则是非法的)


    C语言语法的方面对栈内存和堆内存如何使用没有限制。然后从使用的角度,栈内存更适用于容量较小的单个变量(例如:C语言的基本变量类型、较小的结构体和数组),堆内存则适用于开辟较大块的内存。栈内存由编译器分配和释放,堆内存由程序员分配和释放。

    收藏 0 回复 0 浏览 133
  • 【重磅】在这 一芯难求 各种涨价的时代,STM32G0闪亮登场了........

    由于芯片制造工艺的区别,STM32G0被委以重任,撑起一片天,据我从官方了解的情况可以得出一个结论,不管从价格上还是从性能上STM32G0都可以用来代替STM32F0,我们话不多说直接来看看它的区别:

    一:我们从外部封装引脚上来看

    1.jpg 

    G0没有F0的100PIN的封装,但是它增加了SO8封装的引脚,这样的话性能很强大,价格很有诱惑力,极具性价比。


    二:我们从外设配置资源来看:

     2.jpg

     

    1. 内核更高级:F0:Cortex-M0内核,主频高达48MHZ;G0:Cortex-M0+内核,主频高达64MH,主频高,程序运行更快!

    2. G0的FLASH存储器方面有缩小,SRAM方面有扩展。

    4.  ADC速率更高,F0:ADC时钟频率提高到14Mhz,G0:ADC时钟频率提高到16Mhz,G0的AD模拟采样转换速率更快。举例:(1.5为采样周期,12.5为转换周期)

    G0:With ADC_CLK = 16 MHz and a sampling time of 1.5 ADC clock cycles:

    Tconv = 1.5 + 12.5 = 14 ADC clock cycles = 0.875 µs

    F0:With ADC_CLK = 14 MHz and a sampling time of 1.5 ADC clock cycles:

    Tconv = 1.5 + 12.5 = 14 ADC clock cycles = 1 µs

    5.  外设资源更丰富,增加了AES加密单元、普通定时器单元、硬件随机数RNG单元、DMA多路复用请求仲裁单元,可编程映射DMA请求,好处是使DMA通道对应的外设更加灵活,不再受限、低功耗串口等,更加安全高效。当然相比之下也有牺牲,比如说全速USB2.0,串口数量、CAN等。


    三:我们从系统架构上来瞧瞧看:

    3.jpg

    4.jpg

    1. F0的AHB2总线消失了,引入了新的IOPORT总线,STM32F0的GPIO Ports由总线矩阵通过AHB2总线访问,STM32G0的GPIO Ports直接挂在IOPORT总线上CPU可直接访问,速度更快!


    5.jpg 

    2. STM32F0的外部中断EXIT模块由总线矩阵通过AHB1总线->(AHB->APB桥)来访问,STM32G0的外部中断EXIT模块由总线矩阵通过AHB1总线直接访问,速度更快!


    四:从时钟来看:

    6.jpg 

    由于内核使用区别,两款芯片的RCC时钟也略有区别,如上图所示,主要体现在内部的时钟资源及频率差异,最大主频差异以及复位后的系统时钟频率差异。


    五:从电源供电来看

     

    7.jpg 

    供电电压范围更宽,G0:1.6V-3.6V    F0:1.8V-3.6V,相比之下G0的低功耗睡眠模式更加出色。


    六:最后跟大家看一下M0和M0+内核的区别

    8.jpg 

     

    除了上面展示的内容之外,还有部分区别未展示,比如说中断向量表的内容有改变,G0支持向量表位置重定义、低功耗改善等等、其中不得不提到一点相比F0,G0增加了内部外设的互联功能,通过配置外设中间可直接互联,可以减小CPU额外开销!


    收藏 0 回复 0 浏览 130
  • STM32的几种开发方式,你都知道吗?

    经常有人会问,你们STM32编程是用库函数 还是用寄存器的...会说库函数方便,容易,都用库函数...等等这样的问题,今天我们就来dis一下这几种编程方式,STM32编程目前常见的几种形式如下:


    1. 使用标准外设库开发  

    2.使用寄存器开  

    3.使用CubeMax生成代码工具开发

    下面我们先来说一下目前这几种形式的区别:

    1.使用标准外设库开发

    Stm32标准外设库是stm32官方提供给用户的全系列芯片的外设驱动,官方把单片机外设的功能进行包装,提供给用户一个现成的接口函数,用户不用去管寄存器到底是如何操作的,直接调用接口函数,即可使用这些外设。在这个基础上你直接开发你的应用层程序即可。

    这个驱动包名字一般是STM32Fxxx_StdPeriph_Lib_Vx.x.x有了它可以大大加速我们开发stm32我们以STM32F10x_StdPeriph_Lib_V3.5.0驱动包为例,解压该zip文件,得到如下文件夹和文件
       _htmresc
       Libraries
       Project
       Utilities
       Release_Notes.html
       stm32f10x_stdperiph_lib_um.chm

    其中Libraries包含库的源代码,Project包含stm32各个外设的使用范例和一个工程模板,Utilities是使用st公司评估板的例子,stm32f10x_stdperiph_lib_um.chm教我们怎么用标准外设库。


    2. 使用寄存器开发

    使用寄存器开发,用户需要自己去操作底层外设寄存器,实现想要的外设功能,这个过程是需要自己对照手册,一行一行把代码敲出来的。


    3. 使用CubeMax开发

    这个是官方提供的图形化的一键生产代码的工具,在这个IDE中我们只要根据你想要的功能,点点鼠标就可以生成你想要的功能,但是注意这个功能只是你想要的单片机外设配置功能,并不是你的应用程序功能,比如你想要以一个什么样的方式通讯,还是需要你自己去完善的。如下图是图形化开发的界面:

    图片1.jpg 

    上面我们以简洁明了的描述了三种编程方式的使用方式和编程过程,接下来我们来分析一下这三种方式的优缺点:

    第一种方法,官方提供了现成的驱动库,用户可以直接使用,使用起来方便,快捷,开发速度快,相对起来也容易上手一点,但是对底层的寄存器操作原理了解不深,只知其一,不知其二,出了问题,解决起来比较麻烦。且官方的驱动库为了容错性高一些等原因,会引入一些判断机制,相对复杂一些,但是实际上有的东西是我们用不到的,这就会造成代码执行效率会相对低一些。


    第二种方法,虽然开发起来相对来说比较慢一些,比较繁琐一些,但是接触的都是真正的底层内容,出了问题,我们也能从源头来快速分析解决问题,而且写的代码中省去了一些不必要的判断过程,执行效率会相对高一些,代码看起来也会清爽一些。


    第三种方法,使用IDE开发,这个相比较来讲入门是最快的,不用接触那些库函数接口,也不用去理会那些寄存器操作,只要在图形化的界面上勾选一些选项就可以了,这简直是懒人必备神器啊,但是话说回来,这种开发方式,基本上接触单片机底层内容为0%,只要写应用程序就可以了,出了问题更难解决。如果换了个平台,没有这种方便的IDE,怎么办呢?


    上面我们全面分析了STM32的几种编程方式,我认为大家在学习STM32单片机时,应该先从寄存器入手,知其所以然,理解了原理,底层知识之后,可以再拐回头使用标准库,或者IDE,这样效果会更好,你认为呢?


    收藏 0 回复 0 浏览 130
  • STM32学习回忆录---------第3次写流水灯程序

    这是我第3次用STM32写流水灯程序了,感触颇深,想和大家分享我学习STM32的故事。


                     初次相识STM32

    2008年春天的第一场雪,来的比往年更大一些,这个春节,我留在深圳,无法回到河南。我在街上乱逛,无意间看到公交站台上的广告,深圳会在春季举办IIC (国际集成电路)大会,那是我刚刚大学毕业,学的是芯片制造专业,所以我决定参加。

     

    到那一天,在会展上,我看到了国外IC的先进制程,心想中国再过几年也会达到这个水平吧。

    后来,我看到了一个蝴蝶展厅,那是STM32展馆,我感觉这个芯片不错。这是我和STM32的初次相识。

     

    几年以后,STM32在国内迅速火了起来,因为我的专业在国内很难找到工作,我决定学习STM32


       1次学习STM32

     

    培训地点: 河南郑州,时间: 2013年,学习方式:线下。

    这是我第1次用STM32写流水灯,也是第一次用库函数,使用的芯片是STM32F103,用库函数的方式写流水灯,终于告别了寄存器,当时认为这是一种先进的方法,因为使用起来相当方便,想着以后单片机就不用查寄存器了,这是单片机历史上的伟大跨越。这个老师的教学方式是直接使用库函数,没有讲过库函数。在学习中,查错是一件让我头痛的事情,没有用过单步仿真,出现问题不知道,错在哪里,库函数固然好用,但库函数无法找出错误。使用printf(),有效果,但效果有限。我的这个老师非常诚实,我有不会的问题,他能给我解决就解决了,解决不了,老师就会说:“这个我也不会” !


     

    2次学习STM32

     

    培训地点: 广东深圳,时间: 2016年,学习方式:线下。

    这是我第2次用STM32写流水灯,也是用库函数的方式,使用的芯片是STM32F103。这个老师的水平要高一些,他把用到的库函数都讲了一遍,但是很少讲解寄存器。当我写流水灯程序的时候,我不仅会用库函数,而且还能够看懂库函数。当时觉得官方的库函数写的太好了,我不禁为官方点赞!。我问老师,这个启动代码能不能讲一下,老师说:你会用就行了,启动代码官方已经为你写好了,你不用知道为什么,你需要知道哪个是F1 的启动代码就可以了。出现错误的时候,依然没有查看寄存器和变量,用的是printf(),来进行调试,排查错误还是让人头疼。

    4月份的时候,我参加了STM32深圳峰会,官方介绍了CubeMx ,并说这是一种比库函数还方便的图形化编程方法,我认为汇编语言过时了,用C语言查寄存器的方法也过时了,现在STM32用的是库函数,但这种方法必将被CubeMx 所代替,因为随着编程技术的不断发展,新的技术必然会取代旧的技术。

    随着STM32 学习的深入,我开始学习一些高大上的东西,例如 触摸屏CAN SDIO 文件系统 USB等,难度太大了,我陷入其中,无法自拔。


    3次学习STM32

    培训地点: 河南老家,时间2021年,学习方式:线上。

     

    学了单片机两个月了,以流水灯为例,学到了前两次所没有学到的东西,因此感悟大不相同。

     

    1. 对寄存器的操作,就是往正确的地址写或读正确的数据。有点像送快递的,找到地址送或收快递。

     

    2. 能够被编译器发现的错误,解决比较容易,而能通过编译,却不能实现期望的功能,这种错误往往要费点时间,这个需要单步仿真,查看寄存器或变量值,此便对错误进行定位,因此必须缩小代码的范围,从工程到文件,到函数,到语句,这个有点像修电路板,从电路板到单元电路,最终找到坏的那个元器件。有些情况下,即便通过单步调试,但当整个程序运行时,还是无法实现功能。这时,还要对程序进行优化,直到实现期望的功能。自己的程序出现错误,自己检查几遍都检查不出来,但是交给别人,别人很快就能找到问题,可是问题来了,自已必须具备查错的能力,因为别人不可能总为你查错。

     

    3.即然编译器可以把C语言翻译为汇编语言,那么写程序的人也能够这样,汇编语言有一定的难度,不是面向人的,那么就可以用面向人的C语言,去翻译成汇编语言,用C语言作为桥梁,就可以大大降低难度。不得不承认,汇编语言在一些场合,还是要用到的,无法替代,例如有的8位单片机,或者FOC中。以前,我认为学习单片机,跳过汇编语言,跳过寄存器,是一种好的方法,但事实并非如此!而操作寄存器是一种通用的方法,不受库函数的依赖,换了单片机,依旧可以很快上手!

     

    我感觉这两个月确实收获很大,报这个单片机线上班我觉得很值,比前两个靠谱多了,因为我发现:我再也不用走弯路了!

     

     

     

                                                                       河南学员

       2021/3/26


    收藏 0 回复 0 浏览 125
  • 常见的内存错误及对策

    对于用CC++除了考虑上层应用,还需要考虑底层的内存管理,或者说内存泄漏的问题。


    1、指针没有指向一块合法的内存

    定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内存。

    ①结构体成员指针未初始化

    定义一个结构体变量,但是结构体内部定义了指针成员,往往应用结构体变量的时候如果不给这 个成员指向一个合法的地址只是给成员分配了字节数,应用的时候对应内存的区域指针成员是无权 访问的,所以需要对结构体分配内存,结构体成员中指针变量也要分配内存,否则访问不到有效地 址。

    ②没有为结构体指针分配足够的内存

    分配内存的时候,分配的内存大小不合适,比如开辟内存空间时sizeof(struct stu)误写为sizeof(struct stu *),书写错误会导致开辟空间不正确。

    ③函数的入口校验

    不管什么时候,我们使用指针之前一定要确保指针是有效的。一般在函数入口处使用         assert NULL !=p)对参数进行校验。在非参数的地方使用if(NULL != p)来校验。但这都有一个要求,也就 p在定义的同时被初始化为NULL,如果没有被初始化为NULL,那么校验也起不了作用,没有被 初始化的指针变量,内部是一个非NULL的乱码

    assert是一个宏,而不是函数,包含在assert.h头文件中。如果其后面括号里的值为假,则程序终 止运行,并提示出错;如果后面括号里的值为真,则继续运行后面的代码。


    2、为指针分配的内存太小或内存访问越界

    为指针分配了内存,但是内存大小不够,导致出现越界错误。

    通常这种问题都会出现在我们容易忽略的字符串常量中,往往会忘记结束标志”,在开辟内存的时候sizeof计算中需要把结束标志加上,还有计算分配空间大小的时候最好用sizeof来操作,移植性也好。

    再有内存分配成功,且已经初始化,但是操作越过了内存的边界。

    这种错误经常是由于操作数组或者指针时出现“多1”或“少1”而出现的。比如:

    forint i = 0; i < = 10;i++

    一般for循环的循环变量一定要使用半开半闭的区间,而且如果不是特殊情况,循环变量尽量从0开始。


    3、内存泄漏

    内存泄漏几乎是很难避免的,不管是老手还是新手,都存在这个问题。甚至包括WindowsLinux这类软件,都或多或少有内存泄漏。也许对于一般的应用软件来说,这个问题似乎不是那么突出,重启一下也不会造成太大损失。但是如果你开发的是嵌入式系统软件,比如汽车制动系统、心跳起搏器等对安全要求非常高的系统,你总不能让心脏起搏器重启吧。

    会产生内存泄漏的内存就是堆上的内存,也就是由malloc系列函数或new操作符分配的内存。如果用完没有及时freedelete,这块内存就无法释放,直到整个程序终止。

    malloc是一个函数,专门用来从堆上分配内存,malloc函数的返回值是一个void类型的指针,参数是申请分配的内存大小,内存分配成功后,malloc函数返回这块内存的首地址,需要一个指针来接收这个地址,那么申请了就能成功吗?不一定的,如果所申请的内存块大于目前堆上剩余内存块,则内存分配失败,函数返回NULL。我们需要知道内存申请分配是连续的一块内存,如果剩余内存不够返回就是空,那么我们可以用校验的方式(if(NULL != p))来验证是否分配成功。当然也可以申请0字节内存,返回不是NULL,是一个正常的内存地址,但是你却无法使用这块大小为0的内存,这就好比尺子上的某个刻度,刻度本身并没有长度,只有某两个刻度一起才能量出长度。

    再来说下内存释放,既然有分配,就有释放,不然的话,有限的内存总会用光,而没有释放的内存却在空闲。与malloc对应的就是free函数了。free函数只有一个参数,就是所要释放的内存块的首地址,比如:free(p);这个函数主要是让指针变量和这块内存脱离关系,从此这个指针变量和分配过的这快内存没有关系了,那么指针变量p本身保存的地址并没有改变,但是它对这个地址处的那块内存却已经没有所有权了。释放后,那块内存里面保存的值并没有改变,只是再也没有办法使用了。

    需要记住一点mallocfree是匹配使用的,如果多写两次malloc或者free都会出错。

    再有内存释放之后需要给指针变量赋值为NULL,如果没有把指针置NULL,这个指针就成为了野指针,这是很危险的,也是经常出错的地方。


    4、内存已经被释放了,但是继续通过指针来使用

    一般会有以下三种情况:

    ①就是上面所说的,free(p)之后,继续通过p指针来访问内存,解决的办法就是给PNULL

    ②函数返回栈内存,这是初学者最容易犯的错误。比如在函数内部定义了一个数组,却用return 语句返回指向该数组的指针。解决的办法就是弄明白栈上变量的声明周期。

    ③内存使用太复杂,弄不清到底哪块内存被释放,哪块没有释放。解决的办法是重新设计程序,改善对象之间的调用关系。

    写程序就是要多练习,多调试代码,同时多总结经验,少走弯路。共勉。


    收藏 0 回复 0 浏览 123
×
蔡琰