10

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

4

7 回答 7

45

作为一名微处理器架构师,我有机会在非常低的软件方面工作。基本上,低级嵌入式仅在硬件特定级别上与一般 PC 编程有很大不同。

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

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

希望这将提供一个良好的开端。如果您有其他疑问,请随时发表评论。

于 2009-09-02T11:24:19.313 回答
5

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

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

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

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

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

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

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

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

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

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

SPJ Embedded Technologies对其 8051 开发环境进行了可下载的评估,看起来正是您想要的。您可以创建最大 2K 大小的程序,但它似乎要经历整个过程(编译链接、生成 HEX 或 BIN 文件以转储到目标硬件,甚至是一个可以访问片上内容和外部设备的模拟器)。

非评估产品的价格为 200 欧元,但如果您只想玩一点,我会下载评估 - 除了 2K 限制之外,它是完整的产品。

于 2009-09-02T10:52:26.567 回答
3

我觉得你最感兴趣的是 sybreon 所说的“第 2 步”。那里可能发生很多事情,并且因平台而异。通常,这些东西是由引导加载程序、板级支持包、C 运行时 (CRT) 以及操作系统(如果有的话)的某种组合来处理的。

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

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

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

于 2009-09-02T16:56:12.273 回答
2

好的,我会试一试...

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

因此,从您的基本哈佛架构开始,您就有了程序存储器。当微控制器第一次启动时,它会在内存位置 0 处执行指令。通常这是一个跳转到主要代码开始的地址命令。

现在,当我说指令时,我指的是操作码。操作码是编码成二进制数据的指令——通常是 8 位或 16 位。在某些架构中,每个操作码都被硬编码以表示特定的事物,而在其他架构中,每个位都可能是重要的(即,位 1 表示检查进位,位 2 表示检查零标志等)。所以有操作码,然后是操作码的参数。JUMP 指令是一个操作码和一个代码“跳转”到的 8 位或 16 位或 32 位内存地址。即,控制权被转移到那个地址的指令。它通过操作一个包含下一条要执行的指令地址的特殊寄存器来实现这一点。因此,要跳转到内存位置 0x0050,它将将该寄存器的内容替换为 0x0050。

执行指令会导致机器状态发生变化。有一个通用状态寄存器记录关于最后一个命令做了什么的信息(即,如果它是一个加法,那么如果需要执行,则有一点,等等)。有一个“累加器”寄存器存放指令的结果。指令的参数可以进入几个通用寄存器之一,或者累加器,或者存储器地址(数据或程序)。不同的操作码只能对特定位置的数据执行。例如,您可以将两个通用寄存器中的数据相加并将结果显示在累加器中,但您不能从两个数据存储器位置获取数据并将结果显示在另一个数据存储器位置。你' d 必须将您想要的数据移动到通用寄存器,进行加法,然后将结果移动到您想要的内存位置。这就是为什么组装被认为是困难的。有与架构设计的一样多的状态寄存器。更复杂的架构可能有更多以允许更复杂的命令。更简单的可能不会。

还有一个称为堆栈的内存区域。它只是一些微控制器(如 8051)的内存区域。在其他情况下,它可以具有特殊保护。有一个称为堆栈指针的寄存器,它记录堆栈的“顶部”所在的内存位置。当您从累加器将某些内容“推入”堆栈时,“顶部”内存地址会递增,并且来自累加器的数据将放入前一个地址。当从堆栈中检索或弹出数据时,会执行相反的操作,堆栈指针会递减,堆栈中的数据会被放入累加器。

现在,我也对指令是如何“执行”的感到有些困惑。嗯,这是你开始研究数字逻辑的时候——VHDL类型的东西。多路复用器和解码器以及真值表等。这就是设计的真正本质——有点。因此,如果您想将内存位置的内容“移动”到累加器中,您必须弄清楚寻址逻辑,清除累加器寄存器,并将其与内存位置的数据进行运算,等等。将所有内容放在一起时会令人生畏,但如果您已经用 VHDL 或任何数字逻辑方式完成了单独的部分(如寻址、半加器等),您可能知道需要什么。

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

于 2009-09-03T01:54:12.813 回答
2

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

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

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

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

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

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

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

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

于 2016-05-24T18:43:41.280 回答
1

我有使用 AVR 微控制器的经验,但我认为这对所有人来说都差不多:

编译过程与普通 C 代码相同。它被编译成目标文件,这些文件链接在一起,但不是像 ELF 或 PE 那样输出一些复杂的格式,而是简单地将输出放在 uC 内存中的某个固定地址上,没有任何标题。

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

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

于 2009-09-02T11:00:07.787 回答
1

您可以查看 Jim Lynch 的非常详细的GNU ARM 教程

于 2009-09-02T19:49:26.527 回答