发帖数

53

原创数

53

关注者

11

阅读数

9260

点赞数

1

黄忠

  • 什么是程序映像

    大家好,我是张飞实战电子黄忠老师。

    我们通常说的单片机的程序映像一般包含以下几个部分:

    向量表;C启动例程;程序代码(应用程序代码和数据);C库代码(C库函数的程序代码,链接时插入)

    分别来看下组成部分都是什么,代表什么……

    向量表

    向量表可以用C语言或汇编语言实现。由于向量表的入口需要编译器和链接器生成的内容,所以向量表代码的实现细节是同开发工具链接相关的。例如,栈指针的初始值被链接到链接器生成的栈空间地址,而复位向量则指向了C启动代码的地址,这些都是同编译器相关的。有些开发工具,包括Keil MDK,则将向量表作为汇编启动代码的一部分,并且使用定义常量数(DCD)指令创建。

    汇编实现的向量表的例子:

    image.png

    这个例子中,向量表被赋予了一个段名(RESET),为了将向量表置于系统存储器映射的开头(地址:0x00000000),链接文件或命令行选项需要知道段的名字,以便链接器能够正确识别向量并将其进行地址映射。复位向量一般指向C启动代码的开头,不过,也可以自己定义复位处理,在跳转到C启动代码前执行附加的初始化操作。

    C启动代码

    C启动代码用于设置像全局变量之类的数据,也会清零加载时未被初始化的内存区域。对于使用malloc()C函数的应用程序,C启动代码还需要初始化堆空间的控制变量。初始化完成后,启动代码跳转到main()程序执行。

    C启动代码由编译器/链接器自动嵌入到程序中,并且是和开发工具链相关的,而只使用汇编代码编程则可能不存在C启动代码。对于ARM编译器,C启动代码被标识为“_main”,而使用GNU C编译器生成的代码则通常被标记为“_start”。

    程序代码

    用户指定的任务是由应用程序生成的指令完成的,除了指令以外,还有以下各类数据:

    ①变量的初始值,函数或子程序中的局部变量需要初始化,这些初始值会在程序执行期间被赋给相应的变量。

    ②程序代码中的常量。

    ③有些应用程序可能也会包括其他的常量,比如查找表和图像数据,他们也被合并在程序映像中。

    C库代码

    当使用特定的C/C++库函数时,它们的库代码就会由链接器嵌入到程序映像中,另外,由于有些数据处理任务需要浮点数或除法运算,在进行这些运算时,C库代码也会被包含进来。具体应用场合不同,内核不同,对C库代码多少以及使用情况也不同。

    RAM中的数据

    像程序ROM一样,微控制器的RAM也有很多种用法。典型地,RAM的使用一般可以分为数据、栈和堆区域。

    对于嵌入式操作系统(如uClinux)或RTOS(如Keil RTX)的微控制器系统,每个任务的栈空间都是独立的。有些操作系统允许用户自定义任务的栈,这样也就需要更大的栈空间。有些操作系统则将内存分为若干个段,每个任务分配一个段,用于各自的数据、栈和堆区域。

    那么,这些数据、栈和堆区域都存储了什么内容?

    数据,数据存储在内存的底部,包含全局变量和静态变量。

    栈,栈空间用于临时数据存储、局部变量的存储空间、函数调用参数传递和异常处理的寄存器备份等。

    堆,堆存储用于C函数自动分配存储器区域,例如alloc()malloc(),以及其他使用这些函数的函数调用,为了确保这些函数能够正确地分配存储器空间,C启动代码需要初始化堆存储及其控制变量。

    image.png

    一般说来,栈位于存储器空间的顶部,而堆区域则位于底部,这样做使得内存使用具有最大的灵活性。在操作系统环境中,可能会有多个内存区域用作数据、栈和堆。


    收藏 0 回复 0 浏览 112
  • 你知道“链接”吗

    在最开始人们编写程序时,都将所有的代码都写在同一个源文件中,经过长期的积累,程序可能包含了N多行的代码,程序员维护起来非常困难。迫切地希望将程序源代码分散到多个文件中,一个文件一个模块,能够更好地阅读和维护程序,这个时候,链接器就闪亮登场了。

    我们知道,数据是保存在存储器中的,对于单片机来说,必须知道这些数据的地址才能使用。变量名、函数名等仅仅是地址的一种代名词儿,旨在编程时更加方便地使用数据,当源文件被编译成可执行文件后,这些标识符都不存在了,它们都被替换成了数据的地址。

    任何程序的执行,最终都要依靠计算机硬件来完成,单片机是大规模集成电路,它只认识高低两个电平(电压),假设高电平为 3.3V,用1表示,低电平为 0V,用0表示。也就是说,在单片机底层,只有 0 和 1 两个二进制数字,这就是机器语言。

    使用机器语言编程,十分繁琐又耗时,并且很容易出错。如果程序包含了多个源文件,就很可能会有跨文件的跳转、在程序拥有多个模块时会导致更加严重的问题。于是大神们发明了汇编语言,这相比机器语言来说是个很大的进步。汇编语言使用接近人类的各种标号来帮助记忆,比如用jmp表示跳转指令,用func表示一个子程序(C语言中的函数就是一个子程序)的起始地址,标号的方法使得人们从具体的机器指令和二进制地址中解放出来。标号这个概念随着汇编语言的普及被广泛接受,它用来表示一个地址,这个地址可能是一段子程序的起始地址,也可以是一个变量的地址。

    随着软件规模的日渐庞大,代码量开始疯长,汇编语言的缺点逐渐暴露出来。汇编虽然提供了多种标号,但它依然非常接近计算机硬件,程序员要考虑很多细节问题和边界问题,而且不利于模块化开发,所以后来人们发明了C语言。C语言是比汇编更加高级的编程语言,极大地提高了开发效率,以加法为例,C语言只需要一条语句,汇编却需要四五条。

    单片机编程中,程序员通过会把很多功能分散到成许多个模块中。这些模块之间相互依赖又相互独立,原则上每个模块都可以单独开发、编译、测试,改变一个模块中的代码不需要编译整个程序。在程序被分隔成多个模块后,需要解决的一个重要问题是如何将这些模块组合成一个单一的可执行程序。在C语言中,模块之间的依赖关系主要有两种:一种是模块间的函数调用,另外一种是模块间的变量访问。函数调用需要知道函数的首地址,变量访问需要知道变量的地址,所以这两种方式可以归结为一种,那就是模块间的符号引用。这种通过符号将多个模块拼接为一个独立的可执行程序的过程就叫做链接(Linking)。
        在一个STM32项目中,代码被分为多个文件时,链接器可以链接ARM代码、Thumb代码、Thumb-2 代码,并自动生成交互操作中间代码,以便在需要时切换处理器状态。链接器还可以在需要时自动生成内联中间代码或长跳转中间代码,以扩展跳转指令的范围。

    链接器还可以生成关于链接文件的调试和引用信息、生成静态调用图并列出堆栈的使用情况、控制输出映像中符号表的内容、显示输出中代码和数据的大小。链接器针对下一次文件编译提供反馈信息,提示编译器有关未使用函数的情况。 可以根据提示在后续编译中将未使用的函数放置在各自的节中,以便链接器将来删除这些函数。

    图片38.png 

    使用链接器构建可执行映像时,链接器将解析输入对象文件之间的符号引用,从库中提取对象模块来满足还未满足的符号引用的需要,根据属性和名称排序输入节,并将属性和名称相似的节合并为相邻块,删除未使用节,删除重复的公共组和公共代码、数据及调试节,根据提供的分组和布局信息将对象片段组织为内存区,给可重定位值分配地址,最终生成可执行映像。


    收藏 0 回复 0 浏览 95
  • 程序的优化技巧

    大家好,我是张飞实战电子黄忠老师,今天我们来讨论下程序的优化技巧!

    在嵌入式系统中由于资源比较有限,特别是内存资源,因此对程序运行的性能要求比较高。对执行效率高的程序段所占用的空间和运行效率进行全方位的优化,可以对程序运行的整体效率将产生可观的提升。

    1、循环缓冲区

    在一些嵌入式的系统中,常常需要开辟一块缓冲区保存数据。例如:对于数据采集系统,需要将一定时间段内的数据放入一个内存区域中。这个内存区域的放置方法是从低地址开始放置,如果放满了(到达了最高的地址),则需要从头部的低地址开始重新放置。这样的内存结构就组成了一个循环缓冲区。

    在一般的嵌入式处理器中没有硬件自动完成循环放置的功能,通常的做法是在程序的每次循环中都判断缓冲区是否放满了,显然这样的开销很大。

    如果要在程序中执行缓冲区类型的操作,这些操作一般需要占用一块连续的内存。在栈上分配的内存,一般只能在函数内部使用,函数退出的时候就会被释放,因此不适合作为缓冲区使用。而在堆上的内存和静态内存都可以作为缓冲区内存使用。

    我们举例来看下:

    #define  BUFFERSIZE 256

    int x[BUFFERSIZE];

    unsigned int k;

    unsigned int i;

    while(1)

    {

    k = i & (BUFFERSIZE-1);

    x[k] = ImputData();

    /*……*/

    i++;

    }

    从程序中可见,数组x[]是作为程序的缓冲区使用的,而由于开始并没有进行数组的初始化,x[]是一个建立在BSS段上的数组,其大小由BUFFERSIZE确定。

    我们看循环内的操作,可以完成自动循环的过程,这个例程中,当i增加到256的时候,k作为数组下标,又会返回为0i本身增加到最大值的时候也会变为0

    那么大家很容易看出来,由于不需要使用if做判断,可以节省几条程序指令的时间。对于这几条指令看似节省的时间不多,但是由于上述语句执行的频率非常高,所以这些时间的节省占程序总运行时间的权重还是比较大的。尤其对于实时采样处理问题,程序必须在指定时间内完成一系列的操作。所以对于执行效率比较高的指令,哪怕只节省一条指令,对运行效率的提高都是很有意义的。

    从以上的例子中可以看出,当进行程序优化的时候,不仅需要考虑程序段运行的绝对时间,还应该考虑程序段运行的频率。对于运行频率非常高的程序,对其进行优化会在很大的程度上提高系统的性能。

    2、查表法

    由于资源有限,程序的运行效率在嵌入式系统上比在PC上的程序开发更为重要。程序的运行速度和所占用的存储器空间这两个效率问题都是必须考虑的。嵌入式系统程序的运行速度与处理器频率有关系;而程序所能占用的存储器空间与ROMRAM的大小有关系。

    在当前的嵌入式系统中,程序的运行速度比程序所占用的存储器空间显得更重要,一是存储器方便扩展,二是存储器的容量是比较容易控制,程序运行占用的处理器时间比较难控制。

    在设计过程中,程序的容量和速度在很多时候是有些矛盾的,在程序中牺牲一定的存储容量换取程序的运行速度,这对于嵌入式系统来说是有一定好处的。典型的例子就是查表法。

    例如:在一个4位的二进制数中,确定有几位为1,也就是要统计0x0~0xf中的任何一个数,中间有几个1

    典型思路:         查表法:

    int getnumber(unsigned int a)     const int table[16] = {0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4};

    {           int getnumber(unsigned int a)

    unsigned int i=0;       {

    int num = 0;         return table[a&0x0f];

    unsigned int temp = a & (0xf);    }

    for(i =0;i < 4;i++)

    {

    if ((temp>>i)&0x01)

    num++;

    }

    return num;

    }

    典型的思路就是使用循环的方法让程序在这个4位的数中依次查找各个位是否为1,最后累加得出1的数目。那么在实现这个简单的功能的过程中,需要进行4次循环、4次判断,这是有一定开销的,占用了不少处理器的时间。从程序需要实现的功能考虑,输入是一个4位的数,范围是0x0~0xf,输出数的范围是0~4,这实际上是完成了一种映射功能,可以换成第二种查表法的思路,就是构造一个16个元素的数组,可以通过数组得到结果。实际上数组的下标就是输入的数值,而数组的元素就是输出的数值。

    那么很容易看出来,这种做法的优点是每个数值的获取非常快,代价则是增加了一个有16个元素的数组。数组是预先固化好的常量,而不是程序动态生成的,这种利用静态空间换取程序执行时间的方式转换后的程序执行效率非常高。如果把它应用在使用频率很高的程序中,就可以节省很多的系统开销。

    同样,大家可以考虑一下如果是查找8位数中的1的个数怎么做?16位呢?如果变通。

    3、针对循环执行效率的优化

    循环是C语言程序中的常用语法功能,由于循环执行的次数较多,占程序执行时间的权重大,所以对循环的优化是提高程序效率的关键点。

    例如,

    void change_list_value()

    {

    int i,count;

    POSITION pos;

    CPtrList* plist;

    plist = get_start(pos);

    for(i = 0; i < get_count(); i++)     count = get_count();      

    {          for(i = 0; i < count ; i++)

    plist = get_next(pos);     {}

    set_val (plist);

    }

    return 0;

    }

    上面这个循环代码左边是原始写法,右边是改进的。可以发现循环中执行的函数减少了,原来的get_count()函数从原来的内部转移到了循环外部,也就是说这个循环函数改进后只执行一次,如果这个链表中的元素有几千个至几万个,那么第一段代码比第二段代码多执行了几千条几万条的语句,这样会导致时间上巨大的开销。

    总结:在循环系统中,针对于循环条件,应该尽可能地使用临时变量来替代函数调用,这样可以在循环次数较多的情况下,减少大量不必要的函数调用。

    你有没有更好的优化技巧也分享出来啊~


    收藏 0 回复 0 浏览 99
  • 在嵌入式系统中大小端和对齐问题

    大家好,我是张飞实战电子的黄忠老师,今天我们来讲解在嵌入式系统中大小端和对齐端的问题

    C语言是一种高级语言,在大多数情况下C语言的代码是和具体的处理器体系结构无关的。然而,在嵌入式系统的编程中,有可能涉及对内存的具体操作。在大小端和内存对齐问题上,C语言就不能屏蔽不同体系结构处理器的差别,也就是说同样的C语言代码在不同的体系结构的处理器上,有可能产生不同的结果。

    大小端问题又叫字节序的问题。在各种体系结构的处理器中,对多字节数据的内存操作有着不同的定义。处理器对内存数据的操作有读写两种,这就涉及处理器在读写一个多字节的内存的时候,高字节是在内存的高地址还是低地址。一般在32位或者16位的处理器中,都具有将32位数据和16位数据读写到内存中的指令,这时不同的大小端模式将有不同的结果。

    如果读写指令针对的数据长度和类型是一致的,无论数据在内存中存放的形式如何,处理器整体读写都没有问题。这种整内存协调的读写操作问题,一般不会涉及处理器的大小端。

    当处理器读写指令针对的数据长度不一致的时候就会涉及大小端的问题,例如:

    0x76543210整体放入内存,然后在内存的首地址用单字节读取的命令读出。

    如果不知道大小端模式的情况下,读取的值是多少你能确定吗?

    这时就涉及处理器是大端还是小端的问题。

    对于小端处理器,写内存的时候会将内存低地址处放入源数据的低字节,在内存的高地址处放入源数据的高字节;读内存的时候,将内存中低地址的数据就视为目标数据的低字节,对应的高地址数据是目标数据的高字节。

    对于大端处理器,跟小端就相反的。内存低地址存放数据的高字节,高地址存放数据的低字节。

    例如:数据0x76543210在内存中的大端或小端的存放形式如下:

    图片36.png

    上面的示例只是处理器自身读取和写入内存的情况,在更多的情况下,内存中的数据可能来自外界的输入,例如:来自网络的数据包;处理器在写内存的时候,这块内存也可能是给系统中别的设备使用的,例如:处理器写显示内存的情况。这时,就更需要注意处理器的大小端问题,只有大小端处理协调匹配,才能获得正确的结果。

    C语言中,使用指针就可以操作内存,指针的基本类型longshort分别代表了32位和16位的数据。使用16位或32位指针操作内存的时候,同样涉及内存的大小端问题。

    上面我们说了一下内存读写的模式不同,一个地址存的数据不同。

    接下来我们说一下内存对齐的问题,有人会说了内存对齐不对齐还需要你来管吗?这个在写程序的时候也是有讲究的,那么到底什么是内存对齐?为什么要有这个概念呢,我们来一起学习一下吧。

    内存对齐操作的含义是:对于一个4字节的数据,要求其内存是4字节对齐的(地址为4字节的整数倍)。32位对齐的含义是其内存的地址的最低位是:0x0,0x4,0x8,0xC

    16位对齐的含义是其内存的地址的最低位是:0x0,0x2,0x4,0x6,0x8,0xA,0xC,0xE

    显然,对于单字节的内存读写操作,没有内存对齐的问题。从处理器硬件的角度,处理器更适合处理对齐的内存操作。对于非对齐的内存操作,不同的处理器则有不同的结果。

    局部变量建立在栈空间上的,由编译器分配,一般保证它们都是对齐的。但是在程序中可能出现不对齐的内存操作。对于嵌入式系统中常用的ARM体系结构,并不支持不对齐的地址操作,当进行不对齐的地址访问的时候,处理器将引发异常。

    在嵌入式程序的编写过程中,更需要注意内存对齐的问题。对于内存操作,使用字节操作(8bit)不会有内存对齐的问题,但是效率比较低。在32位系统中,应该尽量使用32位的数据操作,但这将带来内存对齐的问题,因此需要根据系统的具体情况选择合适的内存操作。

    我们再来说说常纠结或者容易迷惑的结构体成员的对齐问题

    结构体是一个基本的语法单元。在32位系统中,编译器一般会对结构体的成员变量作一定的对齐处理。例如,在程序中定义如下结构体:

    typedef struct _S1

    {

    char m1;

    int m2;

    char m3;

    short m4;

    }S1;

    在结构体的定义上,结构体的大小应该是各个结构体成员的大小之和。但是,对于上面这个结构体S1,它的大小并不等于4个成员变量之和。在这种定义中,三个成员变量之和是1+4+2+2=8,但是结构体的大小并不是8字节。

    图片37.png

    编译器在处理结构体的时候,默认将结构体内部各个变量的内存都是对齐的,由此在结构体的内部可能出现一些空的字节。

    一般情况下,在结构体含有4字节长整型成员的时候,结构体的大小将是4字节的倍数。为了对齐可能需要在结构体的最后补充1~3个字节。

    如果结构体中含有2字节短整型成员的时候,结构体的大小将是2字节的倍数。为了对齐可能需要在结构体的最后补充一个字节。

    这个算字节数的一般出现在找工作中的笔试题的概率还是很高的,其实就是考察的对这个内存对齐的掌握。


    收藏 0 回复 0 浏览 157
  • 一文带你读懂RFID

    大家好,我是张飞实战电子的黄忠老师,今天我们来了解下什么是RFID。

    RFID又称无线射频识别,通过无线电信号识别并读写特定目标数据,很容易就可完成识别与读写数据。RFID技术已经存在于我们生活中的方方面面,比如门禁卡、鸿蒙一碰传等等。       
           
    电子标签,比如说我们拿的卡门禁卡,进入天线磁场后,若接收到读写器发出的特殊射频信号,就能凭借感应电流所获得的能量发送出存储在芯片中的产品信息(无源标签,即不带电源),或者主动发送某一频率的信号(有源标签,即带电源),读写器读取信息并解码后,送至MCU系统进行有关数据处理。
                                   
    图片35.png
    关于RFID有几大产品:
     
    1. 无源RFID产品:
       
    无源RFID标签,被称为被动RFID标签这类产品需要近距离接触式读卡器来进行识别,比如房卡、银行卡等,当无源RFID标签靠近RFID读卡器时,无源RFID标签的天线将接收到的电磁波能量转化成电能,激活RFID标签中的芯片,并将RFID芯片中的数据发送出来。具有抗干扰能力,用户可自定义读写标准数据,在专门的应用系统效率更加快捷。无源RFID标签的性能受标签大小(影响到天线的参数),调制形式,电路Q值、器件功耗以及调制深度等影响。这类产品也是我们生活中比较常见,也是发展比较早的产品。价格相对有源RFID便宜,且使用寿命相对较长。其识别距离比有源系统要小,一般为几米到十几米,且需要较大的读卡器来发射功率。
    2. 有源RFID产品:
           
    有源电子标签是指标签工作的能量由电池提供,电池、内存与天线一起构成有源电子标签,不同于被动射频的激活方式,一直通过设定频段主动外发信息。这类型的产品则具有远距离自动识别的特性,所以相应地应用到一些大型环境下,比如智能停车场、智慧城市、智慧交通及物联网等领域。识别距离较长,通常可达几十米甚至上百米,缺点是成本高寿命有限,不易做成薄卡。       
    3. 半有源RFID产品:
         
     半有源电子标签顾名思义就是有源RFID产品和无源RFID产品的结合,内有电池,但电池只对标签内部电路供电,并不主动发射信号,其能量传递方式与无源系统类似,因此其工作寿命比一般有源系统标签要长许多。它结合二者的优点,只有在其进入低频读卡器的激活信号范围时,标签被激活后,才开始工作。解决了有源RFID产品和无源RFID产品不能解决的问题,比如门禁出入管理、区域定位管理及安防报警等方面的应用,近距离激活定位、远距离传输数据。
           
    RFID运用领域举例:
    1.仓库/运输/物资管理:给货品嵌入RFID芯片,管理人员就可以在系统迅速查询货品信息,防丢弃,提高货品交接速度,提高准确率,防止窜货和防伪。
    2.门禁/考勤
    3.固定资产管理:像图书馆、艺术馆及博物馆等资产庞大或者物品贵重的一些场所,当书籍或者贵重物品的存放信息有异常变动,就会第一时间在系统里提醒管理员,从而处理相关情况。


    收藏 0 回复 0 浏览 38
×
黄忠