发帖数

53

原创数

53

关注者

11

阅读数

9261

点赞数

1

黄忠

  • 你知道“链接”吗

    在最开始人们编写程序时,都将所有的代码都写在同一个源文件中,经过长期的积累,程序可能包含了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
  • 什么是程序映像

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

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

    向量表;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
  • 变量的初始化技巧

    由于在嵌入式系统中必须考虑程序规模的问题,因此,对程序中的变量的初始化也需要进行慎重的考虑。在C语言中,基本数据结构(字符型、整型)的初始化相对简单;数组、结构体属于C语言中的构造类型,其变量在初始化的时候相对复杂,也有一些比较特殊的技巧和方法。

    数组的初始化

    以下的代码是一个关于数组的初始化的示例:

    image.png

    从程序上来看,方式1直接使用数组初始化的方式,方式2使用了函数完成数组的赋值。方式3是方式2的等价形式。

    从表面上来看方式1要简单很多,实际上,无论从代码的规模上,还是效率上,二者都没有太大区别。

    方式1看似直接使用初始化的过程完成赋值,实际上对于类似char a[10]=abcde形式的语句,编译器还是需要做很多事情才能完成。a是函数中使用局部数组变量,开辟在栈内存空间上。当程序运行至fun处,不会凭空得到一段字符串,也就是说abcde必须有地方存放,这就是只读区(RO Data)。因此,程序运行赋值语句时,要在栈上开辟10个字节的空间,然后将调用内存复制函数将只读区的abcde复制到这个栈空间上。

    由此可见,方式1和方式2的运行没有本质区别,只是方式1利用编译器完成的操作,方式2要在运行程序时完成,二者依赖的库不同,但是都是内存复制一类的功能,同时二者的abcde都需要占用只读数据区的空间。

    从占用空间和运行效率上,方式1,方式2,方式3基本都是等价的。无论程序中有没有声明,abcde所占用的只读数据区(RO Data)都是必需的,复制的过程也是必需的。

    方式4是直接把a定义为已初始化可读写的全局变量,在使用的时候直接操作。作为已初始化的全局变量(RW Data),将在程序总体初始化的阶段复制到内存中,而不是在函数调用的时候复制。其优点是不用在函数调用的时候完成内存复制操作,缺点是全局的数据会一直占用内存,而栈上数据将在函数退出的时候释放。

    实质上,在数组的定义中,变量可以是全局变量或者局部变量,如果是全局变量,将会增加10字节已初始化的数据区(RW Data),初始化的内容将被放入,这段数据区是可读写的,对全局变量的访问就是对这段已初始化的数据区的访问。如果是局部变量,内容被放入只读数据区,函数运行到的时候要在栈上分配相应的数据区,把只读区的内容复制到栈上,对数组的访问是访问这段在栈上的内存。

    结构体的初始化

    在数组初始化的时候可以使用直接赋值的方式,而在结构体初始化的时候可以使用参数列表。这两种形式比较类似,因此结构体在初始化阶段和数组的情况是相似的。

    例如:

    image.png

    结构体的两种初始化方式和上面数组的两种初始化方式有一定的对应关系。第一种方式使用成员列表的方式初始化,第二种使用对结构体成员变量赋值的方式。实质上,第1种方式编译器将自动生成一些指令完成变量a的初始化,而第2种方式编译器在处理Score a语句的时候只需要开辟栈空间,而在后面在对其每个成员进行赋值,开辟栈空间和赋值都是简单的处理语句,编译器没有做过多的工作。

    在嵌入式系统中,对程序性能是非常敏感的,有以下几个方面的开销:首先是程序各段执行的效率,这是程序开销的主要方面,其次是函数的参数和返回值传递中入栈和出栈的时间。由于各个处理器一般都具有直接栈操作的指令(入栈和出栈),因此函数中使用的局部变量的可以使用处理器的基本的入栈和出栈指令来完成,这种指令的执行性能是很高的。但如果是为变量赋初值,虽然是C语言中基本的语法,却并不能以简单的方式处理,编译器实际上需要做一些附加的工作,来完成对局部变量的初始化。也就是说在程序中没有写出的语句,编译器也需要处理。根据以上的程序和分析,可见如果栈上变量需要初始化,有可能也会带来一定的开销。


    收藏 0 回复 0 浏览 211
  • 单片机的异常处理

    大家好!我是张飞实战电子黄忠老师!今天给大家分享单片机的异常处理

    ARM处理器中,如果一个程序产生了错误并且被处理器检测到,这是就会产生错误异常。

    错误是怎么发生的呢?

    许多可能的原因都会引起错误发生,比如对于存储器相关错误,总线系统的异常响应可以有以下原因:

    访问的地址非法;

    由于传输的类型非法,总线的从设备不接受此次传输(从设备决定)

    由于传输未使能或初始化,总线的从设备无法进行此次传输(例如,如果外设的时钟被关闭,那么访问这个外设时,微控制器就可能会产生错误响应)。

    当确定了硬件错误异常的直接原因以后,我们可能还得花费一些时间来确定问题的根源。例如,总线错误可以由很多种情况引发,例如错误的指针操作、栈空间损坏、内存溢出、非法存储器映射以及其他原因。

    分析错误

    根据错误类型的不同,通常能够直接确定引起硬件错误异常的指令的位置。要实现这个目的,就需要知道进入硬件错误异常时的寄存器的内容,以及异常处理前压入栈中的寄存器的内容。这些值中包含了程序返回地址,通过它也能知道引起错误的指令地址。

    如果使用了调试器,那么可在工程中创建硬件错误异常处理,并且在其中添加一个用以暂停处理器的断点指令;或者也可以在硬件错误异常处理的开始部分设置一个断点,这样当硬件错误发生时,处理器就会自动暂停。在处理器由于硬件错误暂停后,我们就可以尝试着按照下面图的流程对错误进行定位。

    image.png

    为了给分析提供更多的信息,也可以生成程序映像的汇编代码,并且利用在栈帧中找到的PC值确定错误的位置。如果错误的地址为存储器访问指令,就应该检查寄存器的值确定存储器访问的地址是否合法。除了检查地址范围,也应该确认存储器的地址是否正确地对齐。


    除了压入栈中的PC值(返回地址),栈帧中也包含了其他有助于调试的寄存器值。例如,压入栈的IPSR能够反映处理器是否在进行异常处理,EPSR则代表了处理器状态(EPSR的T位为0,则表示错误由意外切换至ARM状态引起)。


    栈中的LR也可能会提供一些信息,例如发生错误的函数的返回地址,错误是否发生在异常处理中,以及EXC_RETURN的值是否被异常破坏等。


    另外,当前的寄存器值也可以提供有助于定位错误原因的各种信息,除了当前栈指针的值,当前的链接寄存器的值也可能有帮助。如果LR中为非法的EXC_RETURN的值,这就意味着它在前面异常处理中被错误地修改了。


    CONTROL寄存器也可以提供帮助。在没有OS的简单应用程序中,进程栈指针(PSP)不会被用到,并且CONTROL寄存器会一直保持为0。如果CONTROL寄存器被设置为0x2(PSP用于线程状态),这就意味着LR在之前的异常处理中被错误地修改了,或者栈内容被破坏导致了EXC_RETURN的值错误。


    收藏 0 回复 0 浏览 115
  • 烧写算法FLM文件如何实现呢?

    大家好!我是张飞实战电子黄忠老师!今天给大家分享烧写算法FLM文件如何实现的

    当我们在开发过程中用到MDK下载程序的时候可能都知道,在下载程序之前需要都在Debug设置的Flash Download子选项卡选择编程算法。大多数时候,我们只要安装了芯片包之后,就可以直接得到对应的编程算法,并不需要我们去修改它。但是,当我们是一个芯片包的开发者,或者我们有独特的下载需求(比如在程序里加入一些校验信息),这个时候我们就需要去了解它了!

    image.png

    编程算法其实就是一段程序,主要功能就是擦除相应的内存块,并将我们的程序写入到相应的内存区域上去。在点击下载按钮的时候,这段程序会被先下载到RAM上(RAM for Algorithm上的设置),然后才会通过它,将用户写的程序写入到指定的内存区域内。


    怎么去实现一个自己的编程算法?首先我们找到自己的MDK的安装路径,进入到ARMFlash文件夹下。这里有个编程算法的工程模板,复制这个工程到你的工程文件夹下,重命名你自己的想要的名字。

    image.png

    打开工程,里面主要有两个文件 FlashPrg.c 和 FlashDev.c:

    image.png

    FlashDev.c主要实现了一个设备相关的结构体(根据自己的Flash情况去实现)

    image.png

    比如STM32F103实现如下:

    image.png

    FlashPrg.c实现了几个Flash编程相关的函数:

    image.png

    根据自己的需要去实现,从上面我们就可以看出,下载程序的时候就是调用了上面的几个函数,跟我们自己写Flash没有太大的区别。那么程序都编程完成之后,怎么生成FLM文件呢?我们先编译工程,完成之后你去看你的工程输出目录,这个时候你就已经可以找到FLM后缀的文件了,这个就是我们自己的编程算法,把它复制到 ' MDK安装路径 'ARMFlash下面就可以了,在选项卡里选择我们自己的编程算法就可以使用了。但是为什么我们自己的工程就生成不了FLM文件呢?工程中的.axf文件跟.FLM文件是一样的,把.axf后缀改为.FLM即可。


    怎么样?心动嘛?赶快去写一个文件试试吧!



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