发帖数

50

原创数

50

关注者

12

阅读数

8966

点赞数

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 浏览 101
  • MCU低功耗设计注意要点

    随着便携式移动设备,各种穿戴设备的兴起,我们不得不关注设备的功耗问题,因为这些设备都具有一个特征:使用电池系统供电,一块线路板上跟功耗相关的单元电路可能有很多,今天我们来谈谈关于MCU的低功耗问题。


    谈到这个问题,首先得选用一款低功耗的MCU,一般MCU的功耗,在其对应参考手册的电器属性章节都会有说明,其次我们在使用MCU低功耗时经常会出现实际功耗理论功耗偏差较大,遇到这样的情况,需要仔细检查以下几点


     
      1、关闭外设时钟
      时钟就相当于是人的心脏一样,外设模块的正常工作不能脱离时钟。对于大多数的MCU,外设模块都一个时钟控制开关,只要打开外设时钟,就可以正常使用该外设了,当然,该外设也就会产生相应的功耗;如果用不到这个外设一定要记得把这个外设时钟关闭,降低功耗


      2、调整时钟频率
      一般我们使用单片机的时候,都喜欢上来就把时钟频率调到最高这样的优点是程序的执行速度快了,因为周期T = 1/F,一般我们进行频率调整都使用的是单片机内部的PLL倍频模块,把一个输入很低的频率倍频到很高的频率,一方面倍频的模块会增加功耗,另一方面时钟线上的对应的外设模块工作频率增加了,功耗也会相应的变大。你会看到,一般低功耗模式下,单片机的工作频率很低很低,所以考虑功耗一定不要忘记考虑时钟频率


      3、注意IO口的电平状态

    注意你没有使用的IO口部分的状态,以及IO口内部的上拉或者下拉的情况,这个也会积少成多,增加功耗的浪费。另外从本质上讲,我们不仅仅是只关注空闲的IO状态就罢了,对于使用的IO口,我们也需要考虑它们在正常工作时的一个状态,联合外围电路一起考虑,效果会更好一些。比如说我们需要点一个LED灯,如果单片机IO口一直输出一个高电平对应点亮LED,那我们是不是可以换一种思路,单片机输出低的时候对应LED亮呢?这样积少成多就能省下一部分功耗。


     4、断开仿真器等测试工具

    我们在测试的时候,通常会连接一些测试的工具,来辅助调试,有可能这些辅助的调试工具都是由线路板供电的,可能你会误认为把这部分功耗当成MCU的功耗,当然这里不仅仅是要关注测试的工具带来的功耗,而且你还需要关注你的测试方法,很有可能你的测试方法不对或者测试工具的本身也会有功耗。


      
        MCU的低功耗设计是一个细致活,要养成良好的习惯,每加一个外设功能模块,对应的外设带来的功耗我们可以测试,当前增加功能带来的功耗增加量,包括静态下的模块功耗,正常融入系统后工作时的功耗,随时掌握模块动态我们不仅要考虑内部的时钟状态、时钟频率,还需要考虑IO与外围电路的配合状况等等,通过调整电路的工作方式来减小一部分功耗。当然关于功耗可不止文中的几点,你还知道有其它什么原因影响MCU自身功耗吗?可以留言来跟大家一起分享哦!

    收藏 0 回复 0 浏览 89
  • 聊一聊内存指针操作

    在嵌入式系统中,对内存地址的操作是一个重要的方面,从广义上讲,嵌入式系统的地址空间可以分成以下三种类型:

    系统的内存

    处理器内部的寄存器映射

    处理器外部部件的内存映射


    无论哪种内存,一般都映射到处理器的内存空间中。在x86系统中,分为内存和I/O映射两种内存;在ARM体系中,全部的地址都在32位的内存空间中,所有的操作都是对32位地址空间内存的操作。


    从编程的角度看,嵌入式系统和PC系统的软件设计的一个重要的区别即在于嵌入式系统更重视对硬件的操作。而对硬件的操作需要通过操作内部寄存器和外围部件内存映射的地址实现,其实现方式都是通过对内存读、写两种操作。


    在汇编语言中,各种处理器都有对内存的不同的寻址方式读写内存。在高级语言中,C语言唯一可以进行内存操作的语言,C语言对内存的操作主要需要通过指针来完成。


    1、使用指针操作内存

    C语言中,指针是一种非常重要的数据类型。使用指针变量可以表示各种数据结构,能很方便地使用数组和字符串,并能像汇编一样处理内存地址。指针的本质就是一个地址,在32位的系统中,指针是一个32位的无符号整数。指针可以用一个变量来表示,变量的指针实际上就是变量的地址。存放变量地址的变量是指针变量。一个指针变量的值就是某个变量的地址或称为某变量的指针。

    一个简单的指针应用如下所示:

    int a;

    int *p = &a;

    这个例子表示,整型指针型变量p指向a的地址,此时对*p的操作等同于对a的操作。

    使用指针可以指向一个变量,也可以指向一个由malloc函数分配的内存,例如:

    void *p = malloc(1024);

    系统分配1024字节的内存,然后让变量p指向这块内存,即p的值是这1024字节的连续内存的地址。在程序中就可以通过p来操作这块内存区域。在内存使用完成后,需要使用free函数讲内存释放。

    free(p);

    在嵌入式系统中的程序开发中,指针的值除了以上的两种形式(从系统内存分配或者指向变量)以外,还可以使用绝对的数值。这是由于在嵌入式系统中,外设寄存器和外部部件的内存映射的地址空间可能都是固定的,因此可以使用指针来处理他们。

    例如,如果需要在地址0x0040处写入一个字节的数据0xf0,可以使用如下的程序:

    unsigned char *p = (unsigned char *)0x0040;

    *p = 0xf0;

    这个程序定义了一个指向0x0040地址的字节型的指针,然后向该地址写入数据0xf0

    上面的程序等同于:

    *(unsigned char *)0x0040 = 0xf0;

    所以说不使用指针变量也可以对实际的地址操作。读内存的程序与之类似,可以使用指针变量或者直接使用地址得到内存中的数据。


    2、指针的类型

    前面的程序在内存的一个指定的地址处写入一个字节(8位)的数据。如果需要写入两个字节(16位)的数据,需要改变指针类型。例如:同样向地址0x0040处写入两个字节的数据0x0f0f,需要使用如下的语句:

    *(unsigned short*)0x0040 = 0x0f0f;

    在这个语句中,使用unsigned short而不使用unsigned charshortC语言中代表16位的整数。

    如果写入4个字节(32位)的数据,则需要使用一下的程序:

    *(unsigned long*)0x0040 = 0xf0f0f0f0;

    在这个语句中,使用unsigned longlongC语言中代表32位的整数。

    32位的系统中,一般编译器认为int代表是32位的整数,等同于long,所以习惯使用int代替long作为内存操作的数据类型。

    注意:指针的类型决定了使用指针进行读写操作时每次读写字节的数目


    3、指针的增量

    在对指针变量的操作中,有时需要对指针变量进行加减运算。例如:

    unsigned char *p = (unsigned char *)0x0040;

    P++;

    *p = 0xf0;

    这段程序的含义是向地址0x0041的字节处写入数据0xf0

    指针加减运算的含义是:指针的单位增量(或减量)等于指针类型所占的内存量。

    对指针进行增量操作的使用,增加的单位是以指针类型的大小:char类型的增量表示增加1字节的内存,short表示增加2字节的内存,longint表示增加4字节的内存,这些工作是编译器根据指针的类型自动完成的。

    总结:对指针进行加减运算的时候,它的变化量与指针的类型有关


    4、指针的类型转换

    C语言中,指针的类型可以在使用的时候进行转换。指针的本质是一个地址,在32位系统中,指针就是一个32位无符号的整数。因此,各种指针都可以相互转化,而且指针在转换过程并没有任何实质性的变化,只是告诉编译器,目前的指针指向何种的内存区域。

    在嵌入式系统中,处理器的片内设备一般都会映射到处理器的地址空间中。这些寄存器有可能是32位的,有可能只有8位,这时就需要使用C语言中不同类型的指针。


    总结:指针的本质是一个无符号的整数,各个类型的指针都可以进行相互转换


    收藏 0 回复 0 浏览 58
  • 白话文讲解ModBus协议

    ModBus是什么?干什么用的?我们一起来了解下吧。


    作为一些大型工业商,其实需要不同厂商提供的控制设备来组成一个工业网路,有了网络是不是需要监控啊?就像你的汽车一样,其实有一个总的控制系统在监控着油箱,行驶安全等等很多信息,所以才呈现给我们一个安全的驾驶体验。那么开发总的控制系统如果那么多的设备都各自有一套通信方案,开发难度可想而知了吧?这个时候就需要有一个标准了,那么这个modbus就是自动控制业界的标准,其实可以理解为我们交互的一个标准协议。这样对开发进度提升了,对通信安全提升了,还方便多个链路之间的通信,集中监控也更加方便了。


    既然是通信的标准,也就是规范了通信的应用层。根据这个标准我们各自去进行工作就好了。这个标准的网络架构我们一起来看下:


    图片1.jpg 

    这个协议定义了一个控制器能认识的使用的消息结构,而不管是经过何种网络进行通信的。


    我们看到网络中每种设备(PLCHMI、控制面板、驱动程序、动作控制、输入/输出设备)都能使用modbus协议来启动远程操作。

    通常我们在串行通信中用到modbus都是主从结构,总线上有一个主节点,一个或多个从节点。从节点地址是唯一的,通信模式是主节点发起请求,子节点没有收到来自主节点的请求时,从不会发送数据,子节点之间从不会互相通信。主节点在同一时刻只会发起一个modbus事物处理。


    我们说到这种协议是主从协议模式,主节点可以广播给所有节点请求,这就是广播模式,这种模式就是从节点不需要应答,接收到处理就可以了,地址0是专门用于表示广播数据的。


    还有一种是单播模式,就是主节点以特定地址访问子节点,子节点接到并处理完请求后,子节点向主节点返回一个应答。

    那我们一起看下协议描述:

    图片2.jpg 

    首先我们看两个概念,ADU:应用数据单元;PDU:协议数据单元。可以理解为PDU就是我们的应用层协议解析需要的。


    其实简单来看就是这样的一个结构,地址域只针对于从节点而言,也有规定(0是广播地址,1~247是从节点地址,248-255预留)其实就是一个字节的分配了。


    那么功能码就是指明要执行的动作。

    功能码后面的数据域就很容易理解了,是表示含有请求和响应参数的数据域。数据域虽然给了长度范围,也可以是没有的,功能码就能代表操作了。


    后面还有一个校验,就是做过通信的都知道校验是必须要的。否则怎么确保数据的正确性。

    对于ADU的长度也是有限制的,最大ADU256个字节,具体在什么物理层上通信对于PDU就有区别了,比如RS232/RS485 ADU = 地址域(1字节)+ PDU253字节)+CRC2字节)=256字节;还支持以太网口,也就是在TCP/IP协议层上封装了一层modbus协议,这样应用范围更广了。那么TCP MODBUS ADU = 249字节 + MBAP7字节) =256字节。


    到这里我们就可以知道了协议结构、协议模型、协议规则。简单来说就是主机是老大,它说了算,它想设置或者读取哪个从机,从机才能做出响应,所以说从机是被动的。当广播发送的时候无需应答,这就是定义的规则,有了规则做事就方便多了啊。


    具体的还详细做了主机的处理模型,从机的处理模型,所有正常的和异常的处理都在规则里面了,是不是很贴心。基本上把逻辑关系都考虑到了,只要按照逻辑关系图去写程序就好了。那么对于解析而言,主要还是对于功能码和数据域的值了。也就是到了上层应用了。

    对于串行传输还有两种模式,RTU模式和ASCII模式。


    我们先来了解一下RTU模式:

    图片3.jpg 

    对于报文格式就是上面这样的,子节点收到信息先解析是否是自己节点,然后对校验做出比对处理。然后就是上层的功能码和数据的处理了。除了数据解析还有一个超时处理,总不能一直接收吧,有个超时的要求,两个字节之间间隔大于1.5个字符时间,报文帧就被认为不完整应该被接收节点丢弃。两帧数据之间也有时间间隔要求,最小间隔是3.5个字符时间。


    这个模式也是我们在工业控制中通用的模式,协议紧凑。

    还有一个ASCII模式:

    图片4.jpg 

    我们都知道ASCII码是一个字符一个字符发送的,也就是表示0-9A-F;那么就是说一个字符表示四位二进制,也就是我们前面说的一个字节需要两个ASCII字符表示,所以这个相对RTU模式时序要求不高,自然应用场合也是有区别的。


    RTU不同的是还增加了一个起始字符和结束字符,校验方式是LRC校验方式,校验不包含起始字符和结束字符的。对比而言,我们看到是一个字节由两个字符表示的。字符间隔最大可以达到1S,相对宽松。


    结构清晰了,规则有了就是可以去解析了,主要还是对功能码和数据域的规则解析了,不同的功能码要处理什么功能的数据区,这个都是要根据规则去解析处理。


    解析这种标准协议首先就是要分好层,逻辑关系要处理清楚,模块处理要结合实际应用映射关系,对于从节点有接收有应答,就是一个完整的闭环。你对modbus了解了吗?


    收藏 0 回复 0 浏览 146
  • 一文带你读懂ARM文字池

    说到文字池,首先第一个问题:什么是文字池?文字池又叫literal pool,它的本质就是ARM汇编语言代码节中的一块用来存放常量数据而非可执行代码的内存块。


    那为什么要使用文字池呢?当想要在一条指令中使用一个 4字节长度的常量数据(这个数据可能是内存地址,可能是数字常量)的时候,由于ARM指令集是定长的(ARM指令4字节或Thumb指令2字节),就无法把这个4字节的常量数据编码在一条编译后的指令中。此时,ARM编译器(编译C源程序)/汇编器(编译汇编程序) 就会在代码节中分配一块内存,并把这个4字节的数据常量保存起来,之后,再使用一条指令把这个4 字节的数字常量加载到寄存器中参与运算。 C程序中,文字池的分配是由编译器在编译时自行分配安排的,但是,汇编程序时,开发者可以自己进行文字池的分配,当然如果没有自己分配汇编器会代劳。不管何种情况,这不影响我们来了解学习一下文字池的知识。


    LDR Rd,=const 伪指令可在单个指令中构造任何 32 位数字常数。 使用此伪指令可生成超出MOVMVN指令范围的常数。LDR 伪指令可为特定的常数生成最高效的单个指令:如果可以用单个MOVMVN 指令构造该常数,则汇编器会生成适当的指令。如果不能用单个MOVMVN 指令构造该常数,则汇编器会执行下列操作:将该值放入文字池中生成一个使用程序相对地址的 LDR 指令,用于从文字池中读取该常数。说的通俗一点,如果LDR Rd, =const能够被转换成MOV 或者MVN指令,则汇编器将转换成它成为相应的指令,如果不能被转换,则汇编器会将value存放在文字池中,并且产生一个LDR指令操作。


    汇编器默认把文字池放在每一个代码节的末尾处。代码节的末尾的确定或者是由汇编源文件尾部的指示符END确定或者由相邻代码节的起始行AREA确定。在大的代码节中(通俗理解为这个节中的代码量比较大),默认文字池在最后,可能与代码节中一条或多条LDR伪指令的距离很远,可能超出LDR伪指令操作数的寻址范围。


    当伪指令是32位时,在ARMThumb代码中,必须小于4K字节,文字池常量数据的位置可以是在伪指令的前面,也可以是在伪指令的后面当伪指令是16Thumb指令时,必须小于1K字节,且文字池必须位于伪指令的后面。


    LDR  Rd, =const 伪指令需要一个文字池来存放立即数常量时,汇编器检查已经存在文字池中是否有相同的常量并且检查文字池是否在伪指令允许寻址范围内。如果条件满足,汇编器引用这个满足条件的常量,否则汇编器会尝试把该常量值放到文字池未用的空间中。如果空间地址超出伪指令的寻址范围,汇编器会产生一条错误信息。这种情况下,程序员必须得自己用指示符LTORG在代码中设置增加一个文字池。指示符LTORG放在导致错误的伪指令后面,并且位于伪指令LDR的有效寻址范围内(一般节的代码量不是特别大的情况下,可以放于中间位置)而且要保证设置的这个文字池,处理器执行代码的时候不会执行到这个地址。 它们应放在无条件跳转指令的后面,或者放在子例程末尾处的返回指令的后面。


    应用举例如下:

    Fun1
        LDR R0, =0X12345678
        ADD R1, R1, R0
     
       
    BX  LR    ;子程序返回
        LTORG    ;声明文字池
    ,存储0x12345678

    POOL  SPACE  20
     
    好了,关于文字池,本片文章就讲到这里了,大家有不明白的地方可以留言提问哦,谢谢大家。

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