【问题标题】:Code execution in embedded systems嵌入式系统中的代码执行
【发布时间】:2009-09-02 10:46:12
【问题描述】:

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

【问题讨论】:

  • 这将有助于指出什么类型的微控制器。
  • 正在使用 8051 控制器。我对如何用汇编语言获取和执行操作码有所了解)。但想知道具有多个 C 文件的项目如何在 uC 上执行。
  • C 文件不执行! :-) 它们被编译为目标文件,并链接到最终的可执行映像中,该映像可以加载到闪存或 RAM 并从那里运行。

标签: c embedded microcontroller


【解决方案1】:

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

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

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

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

【讨论】:

  • 击中目标!!谢谢赛布伦。现在内存分配呢?假设我的 uC 有一个闪存,程序使用的静态和全局变量将由系统启动代码存储在 RAM(.bss 和数据部分)中,堆栈中的局部变量(同样是 RAM)和代码保留在闪存中(ROM )。实际执行是通过执行闪存中的每条指令来实现的。我说的对吗?
  • @Guru_newbie:“实际执行是通过执行闪存中的每条指令来实现的。”有些处理器直接从闪存运行代码,有些则不运行。我相信 8051 会从闪存运行代码。高端(32 位)嵌入式处理器,如 PC 会将应用程序代码复制到 RAM 并从 RAM 执行。
  • @sybreon:步骤 2 还将在 RAM 中设置静态变量并复制初始化数据。
  • @simon - 是的,通常是这样。有时,编译器标志可以控制诸如将 .bss 归零之类的东西。
  • 我喜欢你回答 sybreon,但是(在第 2 项中)我不认为 C 假设会有堆。许多嵌入式 C 编译器将为静态变量预分配内存,并为堆栈中的局部变量分配空间。在这种情况下,只有在使用带有 malloc() 的库时,堆才会发挥作用。不确定 C++ 构造函数。
【解决方案2】:

通常,您的工作水平比通用计算机低很多。

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

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

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

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

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

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

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

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

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

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

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

【讨论】:

  • 感谢您的快速回复,pax。如果可能的话,你可以尝试提供任何好的链接来解释上述过程(不管实际的 uC 是什么)
【解决方案3】:

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

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

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

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

【讨论】:

    【解决方案4】:

    好的,我会试一试...

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

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

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

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

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

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

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

    【讨论】:

    • 复位时,处理器在重新启动向量处开始执行,该向量可能位于也可能不在位置 0x0000。您必须查看特定处理器数据表以了解重启向量的位置。
    【解决方案5】:

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

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

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

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

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

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

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

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

    【讨论】:

      【解决方案6】:

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

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

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

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

      【讨论】:

      • 感谢多维数据集。我现在知道将在主机 PC 中创建一个可执行文件,并将其放入 uC 的非易失性存储器中。我想知道从那以后实际执行如何在实际目标中开始。任何在线文档或案例研究都是可取的。
      • 关于 ELF/PE 不太准确。许多嵌入式系统的链接器输出ELF,只是其中的二进制代码是固定地址的,而不是与位置无关的。因此,可以生成一个 hex 文件(Motorola S-record 或 Intel Hex)或直接二进制转储(假设您知道起始地址)以加载到 Flash 中。
      【解决方案7】:

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

      【讨论】:

      • 请注意:ARM 是更复杂的嵌入式系统之一。与较小的 uC 相比,启动代码特别复杂,例如AVR。
      猜你喜欢
      • 2014-01-28
      • 1970-01-01
      • 1970-01-01
      • 2010-10-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多