ARM Cortex-M嵌入式C基础编程(上)

ARM Cortex-M嵌入式C基础编程(上)

ARM Cortex-M Embedded C Fundamentals/Tutorial
-Aviral Mittal

此技术是关于从编写简单的嵌入式C代码到执行的过程。             

这项技术试图不使用行话,并针对任何人谁有兴趣知道如何开始编写一个嵌入式C程序或ARM Cortex-M系列处理器的汇编语言程序。             

世界上充斥着信息,然而这些信息的存在方式使得所有的信息对于一个来自其他背景的人来说都是垃圾。例如,如果您来自硬件背景,则软件信息看起来很神秘。例如,在软件世界中,他们使用“图像”一词来表示从相应的C程序获得的二进制文件。说真的,如果你不知道“图像文件”是什么,那么jpeg或gif就会出现在你的想象中。我不知道为什么它叫“形象”。这项技术是一种试图保持它非常简单,而不使用'什么软件假设每个人都知道出生'之类的行话。             

这都是关于ARM架构的,所以它关注的是嵌入式c,也就是为基于ARM的微控制器编写的c程序。             

C代码是人类可读的,处理器无法执行它。它必须转换成0和1,因为这是处理器可以执行的。             

因此,在执行C代码之前,必须将此C代码转换为二进制格式的处理器指令,然后将此二进制格式的指令放入内存中,然后处理器将开始从该内存中获取并执行指令(请参阅软件术语“图像文件”的使用)             

但这听起来很简单。在幕后发生了很多事情,很多事情都要考虑。

The very basic Embedded C program:

本节描述了一个非常简单的嵌入式C程序,并给出了一些解释。             

让我们考虑一个非常简单的嵌入式C程序:

typedef unsigned long uint32_t;
int main ()
{
    int ii;
    for(ii=0;ii<305419896;ii++) {
    *((uint32_t *)0x40E00018) = 0x87654321;
      asm("NOP");
    }
    while(1){}
}

如果您之前不知道嵌入式C,那么上面的代码已经很神秘了,但是让我逐行解释一下:             

第一行是数据类型的定义,程序将使用。定义了一种新的数据类型,称为“uint32”。这被定义为“长”long'是C语言中预定义的数据类型,表示32位宽的二进制数。             

第二行是“main”函数调用。对每一个C程序都是必不可少的。这是用户写他们想做的事情的地方。             

int ii”是一个不言而喻的整数声明,它将在“for循环”中使用。在C语言中,需要显式声明所有变量,然后才能使用它们。             

for(ii……)同样是不言而喻的,一个for循环被启动,它将执行305419896次。             

然后你就有了这行命令:

*((uint32_t *)0x40E00018) = 0x87654321;

看起来像是第二次世界大战的加密密码,用来指示某人发射鱼雷!。             

让我解释一下:             

C语言使用所谓的“指针”。指针是指向内存位置的地址。“星号”或“星号”用于定义/声明指针。              

0x”:这意味着“0x”后面的值是十六进制格式。             

现在,上面的代码行简单地表示,用户希望将0x8765_4321的十六进制值发送到内存位置0x40E0_0018。很简单。             

上面用大括号写的'(unit32_t*)表示用户打算将“0x40E0_0018”的常量值转换为另一种类型的数据,即指针,以便它可以用作内存位置的地址。指针指向简单的内存位置。             

这里我们刚刚解释了如何将常量数据(0x40E0_0018)转换为另一种类型的数据,称为指针数据类型。软件人员称之为“类型转换”。也就是,把一种数据转换成另一种数据。或者将一种类型的数据更改为另一种类型的数据。所以这里是“类型选择”行话。

这里我们刚刚解释了如何将常量数据(0x40E0_0018)转换为另一种类型的数据,称为指针数据类型。软件人员称之为“类型转换”。也就是,把一种数据转换成另一种数据。或者将一种类型的数据更改为另一种类型的数据。所以这里是“类型选择”行话。             

那么“*((uint32_t*)0x40E00018)=0x87654321”总体上意味着用户现在希望将0x8765_4321的值写入内存位置0x40E0_0018。指针的“星”表示指针指向的位置处的值。因此,在上面的行中,用户将0x8765_4321分配给(uint32_t*)0x40E00018的“star”。记住,'(uint32_t*)0x40E00018'是指向内存位置0x4E0_0018的指针。             

asm(“NOP”)是汇编语言中的“no operation”指令,在这里使用,因为我不知道C语言中有什么替代方法。要使用C语言中的汇编指令,请使用asm(“汇编指令”)。             

现在是while循环:             

while (1) {}。             

这段代码将出现在大多数嵌入式C程序中。在嵌入式世界,只要处理器有电,它就可以运行。它就像一个瓶子里的金妮,它必须一直做些什么。如果处理器的电源没有关闭,或者处理器没有进入睡眠状态,它将继续执行“某些操作”。所以上面的while(1)do nothing循环的作用完全相同。

为某目标微控制器编写了嵌入式C程序。典型的微控制器至少有微处理器、存储器、外围设备和时钟源。上述C程序写入一个内存位置该内存位置可能属于外围设备中的“寄存器”。

The Compile Flow:

技术的这一部分解释编译器/链接器生成的机器代码(汇编代码)的位。             

程序集代码的部分已显示/描述。             

本文还描述了编译过程中的幕后操作。             

好吧,但什么是“编译”?             

处理器只能执行二进制指令。编译器将用高级语言(如C)编写的可读程序转换成二进制指令。然后将这些二进制指令放入内存。处理器启动时,从内存中获取这些二进制指令并执行它们。将人类可读代码转换为二进制代码的过程称为“编译”             

下面是前面介绍的简单C程序:

typedef unsigned long uint32_t;
int main ()
{
    int ii;
    for(ii=0;ii<305419896;ii++) {
    *((uint32_t *)0x40E00018) = 0x87654321;
      asm("NOP");
    }
    while(1){}

}

Compile it using KEIL uVision: Click Here to go to a Very simple Quick Tutorial

编译上述C代码和'statup.s'文件会生成一个名为'axf'的可执行文件。axf是一个人类无法读取的二进制文件,但是可以生成这个“axf”文件的人类可读取版本,它将以“汇编语言”显示指令:这是由Keil提供的名为“fromelf”实用程序完成的。Keil教程演示了如何使用这个实用程序和精确的命令语法来完成这个转换             

下面是“axf”文件的可读版本的一节。可以看到C语言中的“main()”是如何转换为汇编指令的。             

下面截取的代码还显示了每个指令的地址,即在内存中存储该段代码的位置。例如,“main()”的第一条指令存储在位置0x0000_0134,这是指令MOVS r0,#0。这意味着在寄存器r0中移动值“0”。             

还可以注意到,上述“C”程序中的所有“常量”值都存储在从0x0000_014C开始的完全独立的内存位置。有3个这样的常量,如下所示。

注意,上面的main()中的指令不是从地址0x0000_0000开始的,而是从0x0000_0134开始的。             

然后,如果对“axf”文件的全文版本进行分析,它将显示出许多情况。上面的“main”代码只有几行。             

axf”文件中的额外内容是什么。             

axf”文件包含许多调试信息。当代码下载到目标设备上,并且设备仍连接到主机PC时,此调试信息有助于调试代码。当目标代码加载到目标本身时,代码和调试信息都加载到开发主机PC的内存中。             

当使用某些编译时选项删除调试信息时,axf文件将如下所示:             

这又是很多代码,这是多余的'主要'代码。             

将二进制axf文件转换成ARM体系结构能够执行的格式需要多余的信息。             

在执行用户“main()”之前,将调用以下函数。

__main -> this is not the user main(), but a function called at the start of the binary executable, which calls other functions.
    __scatterload
    __rt_entry
    __rt_entry in turn will call
        __rt_lib_init
        User Code (your code inside main)
        exit()

主程序是用户程序的入口点。此主函数是预定义的(尽管用户可以编写自己的主函数)。请注意,这个main与用户的C程序中的main()不同。如果用户打算编写自己的'uuu main',他们可以使用自己的代码和名称。但是,用户必须更新链接器的默认“--startup”选项,例如“--startup myu main”,因为默认链接器选项是以下“--startup=u main”。如果用户愿意,也可以使用“--no_startup”。但是,这样做的后果超出了本教程的范围。             

__ __main then calls __scatterload。             

对于understand __scatterload,重要的是要进一步了解代码如何存储在内存中以及如何执行。             

典型的微控制器系统通常有几种类型的存储器。例如,闪存、ROM、RAM等。             

这意味着,同一代码可能在不执行时驻留在一个内存中,然后在执行时移动到另一个内存中。例如,代码及其数据在不执行时可以驻留在ROM中,然后将其移动到RAM中执行。              

在另一个例子中,代码可以直接从ROM执行,但是它的变量必须复制到RAM,因为这些变量可能需要由运行的代码更新。Keil教程2展示了如何将变量的初始值存储在只读存储器中,而变量本身存储在读写存储器中,然后在程序执行之前将这些变量的初始值复制到读写存储器中。在Keil教程2中,可以看到C程序有两个整数数组变量,即avar[10]和bvar[10],它们有一些初始值。avar[10]的初始值存储在地址0x0000_015c到0x0000_0180处。这可能是ROM地址。              

然而,当程序执行时,变量avar[10]和bvar[10]存储在堆栈存储器的某处堆栈存储器是从0x2000_0000开始的区域中的读/写存储器。在执行用户的main()之前,这些变量的初始值已经在堆栈中可用。这意味着在执行用户的main()之前,这些初始值是如何从区域0x0000_0xxx(只读区域)复制到0x2000_0yyy(读写区域)的。

原文地址:https://www.cnblogs.com/wujianming-110117/p/13187686.html