嵌入式系统中的代码执行

时间:2009-09-02 10:46:12

标签: c embedded microcontroller

我在嵌入式系统域工作。我想知道从C文件开始,如何从微控制器执行代码(uC通常不需要是主观的)。此外,我想知道像启动代码,目标文件等的东西。我找不到有关上述内容的任何在线文档。如果可能,请提供从头开始解释这些事情的链接。在此先感谢您的帮助

7 个答案:

答案 0 :(得分:39)

作为一名微处理器架构师,我有机会以非常低的水平为软件工作。基本上,低级嵌入式只与硬件特定级别的普通PC编程有很大不同。

低级嵌入式软件可分为以下几种:

  1. Reset vector - 这通常是在汇编中编写的。这是在启动时运行的第一件事,可以被视为特定于硬件的代码。它通常会执行简单的功能,例如通过配置寄存器等将处理器设置为预定义的稳定状态。然后它将跳转到启动代码。最基本的复位向量只是直接跳转到启动代码。
  2. 启动代码 - 这是第一个运行的特定于软件的代码。它的工作基本上是设置软件环境,以便C代码可以在顶部运行。例如,C代码假定存在一个定义为堆栈和堆的内存区域。这些通常是软件构造而不是硬件。因此,这段启动代码将定义堆栈指针和堆指针等。这通常归入“c-runtime”。对于C ++代码,也会调用构造函数。在例程结束时,它将执行main()编辑:需要初始化的变量以及需要清算的某些内存部分在此处完成。基本上,将事物转变为“已知状态”所需的一切。
  3. 应用程序代码 - 这是您从main()函数开始的实际C应用程序。正如您所看到的,即使在调用第一个主函数之前,很多事情实际上都在发生。如果有一个好的hardware abstraction layer,这段代码通常可以写成与硬件无关的代码。应用程序代码肯定会使用很多库函数。这些库通常在嵌入式系统中静态链接。
  4. - 这些是提供原始C函数的标准C库。还有一些特定于处理器的库可以实现软件浮点支持等功能。还可以使用特定于硬件的库来访问I / O设备,例如stdin / stdout。一些常见的C库是NewlibuClibc
  5. Interrupt / Exception处理程序 - 这些是在正常代码执行期间由于硬件或处理器状态的变化而在随机时间运行的例程。这些例程通常也是用汇编语言编写的,因为它们应该以最小的软件开销运行,以便为所调用的实际硬件提供服务。
  6. 希望这将提供一个良好的开端。如果您有其他疑问,请随时发表评论。

答案 1 :(得分:5)

通常,您的工作水平远低于通用计算机。

每个CPU在上电时都会有某些行为,例如清除所有寄存器并将程序计数器设置为0xf000(这里的所有内容都是非特定的,就像你的问题一样)。

诀窍是确保您的代码在正确的位置。

编译过程通常类似于通用计算机,因为您将C转换为机器代码(目标文件)。从那里,您需要将该代码链接到:

  • 您的系统启动代码,通常是汇编程序。
  • 任何运行时库(包括C RTL的必需位)。

系统启动代码通常只是初始化硬件并设置环境,以便C代码可以工作。嵌入式系统中的运行时库通常会使大块的东西(如浮点支持或printf)可选,以防止代码膨胀。

嵌入式系统中的链接器通常也更简单,输出固定位置代码而不是可重定位的二进制文件。您可以使用它来确保启动代码位于(例如)0xf000。

在嵌入式系统中,您通常希望可执行代码从一开始就存在,因此您可以将其刻录到EPROM(或EEPROM或Flash或其他在断电时维护内容的设备)。

当然,请记住,我的最后一次尝试是使用8051和68302处理器。现在可能是“嵌入式”系统是带有各种奇妙硬件的完整Linux系统盒,在这种情况下,通用和嵌入式系统之间没有真正的区别。

但我对此表示怀疑。仍然需要严格的低规格硬件,需要自定义操作系统和/或应用程序代码。

SPJ Embedded Technologies的8051开发环境downloadable evaluation看起来就像你想要的那样。您可以创建最大2K的程序,但它似乎经历了整个过程(编译链接,生成HEX或BIN文件以转储到目标硬件,甚至是模拟器,可以访问片上内容和外部设备)。

非评估产品的成本为200欧元,但是,如果您想要的只是一个游戏,我只需下载评估 - 除了2K限制外,它就是完整的产品。

答案 2 :(得分:3)

我得到的印象是你对sybreon所谓的“第2步”最感兴趣。很多都可以在那里发生,并且它因平台而异。通常,这些东西是由引导加载程序,板支持包,C运行时(CRT)的一些组合处理的,如果你有一个,那就是操作系统。

通常,在复位向量之后,某些引导加载程序将从闪存执行。这个引导加载程序可能只是设置硬件并跳转到你的应用程序的CRT,也在闪存中。在这种情况下,CRT可能会清除.bss,将.data复制到RAM等。在其他系统中,引导加载程序可以从编码文件(如ELF)中分散加载应用程序,而CRT只是设置其他运行时的东西(堆等)。所有这些都发生在CRT调用应用程序的main()之前。

如果您的应用程序是静态链接的,则链接器指令将指定初始化.data / .bss和堆栈的地址。这些值要么链接到CRT,要么编码到ELF中。在动态链接环境中,应用程序加载通常由操作系统处理,该操作系统重新定位ELF以在操作系统指定的任何内存中运行。

此外,一些目标从闪存运行应用程序,但其他目标将可执行文件.text从闪存复制到RAM。 (这通常是速度/足迹权衡,因为RAM在大多数目标上比闪存更快/更宽。)

答案 3 :(得分:2)

好的,我会试一试......

首先是架构。冯诺依曼与哈佛大学。哈佛架构为代码和数据提供了独立的内存。冯诺依曼没有。哈佛用于许多微控制器,这是我所熟悉的。

从基本的哈佛架构开始,您就拥有了程序存储器。当微控制器首次启动时,它会在存储器位置零处执行指令。通常这是一个JUMP来解决主代码启动的命令。

现在,当我说指令时我的意思是操作码。操作码是编码为二进制数据的指令 - 通常为8位或16位。在一些体系结构中,每个操作码被硬编码以表示特定的事物,在其他体系结构中,每个比特可以是重要的(即,比特1表示检查进位,比特2表示检查零标志等)。因此,操作码有操作码和参数。 JUMP指令是操作码和代码“跳转”到的8或16或32位存储器地址。即,控制权转移到该地址的指令。它通过操作包含要执行的下一条指令的地址的特殊寄存器来实现此目的。因此,对于JUMP到内存位置0x0050,它将用0x0050替换该寄存器的内容。在下一个时钟周期,处理器将读取寄存器并找到存储器地址并在那里执行指令。

执行指令会导致机器状态发生变化。有一个通用状态寄存器记录有关最后一个命令的信息(即,如果它是一个加法,那么是否需要执行,有一点,等等)。有一个'累加器'寄存器,其中放置了指令的结果。指令的参数可以是几个通用寄存器之一,也可以是累加器,也可以是存储器地址(数据OR程序)。不同的操作码只能在某些地方的数据上执行。例如,您可以从两个通用寄存器中添加数据并将结果显示在累加器中,但不能从两个数据存储器位置获取数据并将结果显示在另一个数据存储器位置。您必须将所需数据移动到通用寄存器,执行添加操作,然后将结果移动到所需的内存位置。这就是为什么装配被认为是困难的原因。该架构设计的状态寄存器数量与之相同。更复杂的架构可能有更多允许更复杂的命令。更简单的可能不会。

还有一个称为堆栈的内存区域。对于某些微控制器(如8051)而言,它只是内存中的一个区域。在其他人,它可以有特殊的保护。有一个称为堆栈指针的寄存器,用于记录堆栈“顶部”所在的内存位置。当你从累加器“推送”某个堆栈时,“顶部”存储器地址递增,并且累加器的数据被放入前一个地址。从堆栈中检索或弹出数据时,反向完成,堆栈指针递减,堆栈中的数据被放入累加器。

现在我还有一些关于如何“执行”指令的问题。好吧,这是你开始使用数字逻辑 - VHDL类型的东西。多路复用器和解码器以及真值表等。这是设计的真正细节 - 有点像。因此,如果你想将存储器位置的内容“移动”到累加器中,你必须弄清楚寻址逻辑,清除累加器寄存器,并将它与存储器位置的数据等等。当放在一起时,这是令人生畏的但是如果你已经用VHDL或任何数字逻辑方式完成了单独的部分(如寻址,半加法器等),你可能知道需要什么。

这与C有什么关系?好吧,编译器将采用C指令并将它们转换为一系列执行所请求操作的操作码。所有这些基本上都是十六进制数据 - 在程序存储器中的某个点放置一个和零。这是通过编译器/链接器指令完成的,这些指令告诉哪些内存位置用于什么代码。它被写入芯片上的闪存,然后当芯片重新启动时,它转到代码存储单元0x0000和JUMPs到程序存储器中代码的起始地址,然后开始插入操作码。

答案 4 :(得分:1)

我有使用AVR微控制器的经验,但我认为这对所有人来说几乎都是一样的:

编译与普通C代码的行相同。它被编译成目标文件,它们被链接在一起,但是输出只是放在uC内存中的某个固定地址上,而不是输出一些复杂的格式,如ELF或PE。

启动代码(如果编译器生成任何代码)的添加方式与“普通”计算机的启动代码相同 - 在main()代码之前添加了一些代码(也可能在代码之后)。

另一个区别是链接 - everythig必须静态链接,因为微控制器没有操作系统来处理动态链接。

答案 5 :(得分:1)

你可以看看Jim Lynch非常详细的GNU ARM Tutorial

答案 6 :(得分:1)

您可以参考链接https://automotivetechis.wordpress.com/

以下序列概述了控制器指令执行的顺序:

1)为程序的执行分配主存储器。

2)将地址空间从辅助存储器复制到主存储器。

3)将.text和.data部分从可执行文件复制到主存储器中。

4)将程序参数(例如,命令行参数)复制到堆栈上。

5)初始化寄存器:将esp(堆栈指针)设置为指向堆栈顶部,清除其余部分。

6)跳转到启动例程,其中:将main()的参数从堆栈中复制出来,然后跳转到main()。