微控制器如何逐步启动和启动?


17

编写,编译并上载C代码到微控制器后,微控制器开始运行。但是,如果我们以慢动作一步步地执行此上载和启动过程,则我会对MCU内部实际发生的事情(内存,CPU,引导程序)感到困惑。如果有人问我,这是(很可能是错误的)我会回答的问题:

  1. 已编译的二进制代码通过USB写入闪存ROM(或EEPROM)
  2. 引导加载程序将此代码的一部分复制到RAM。如果为true,则引导加载程序如何知道要复制的内容(将ROM的哪一部分复制到RAM)?
  3. CPU开始从ROM和RAM获取指令和代码数据

错了吗

是否可以用有关此阶段内存,引导加载程序和CPU如何交互的一些信息来总结此引导和启动过程?

我发现了许多有关PC如何通过BIOS引导的基本解释。但是,我仍然坚持微控制器的启动过程。

Answers:


31

1)将编译后的二进制文件写入prom / flash yes。USB,串行,i2c,jtag等取决于该设备所支持的设备,这与了解启动过程无关。

2)对于微控制器通常情况并非如此,主要用例是将指令存储在rom / flash中,将数据存储在ram中。无论采用哪种架构。对于非微控制器,您的PC,笔记本电脑,服务器,程序将从非易失性(磁盘)复制到ram,然后从那里运行。有些微控制器也允许您使用ram,即使那些声称违反哈佛定义的人也可以使用ram。哈佛没有什么可以阻止您将ram映射到指令端的,您只需要有一种机制,在加电后就可以在那里获取指令(这违反了定义,但是哈佛系统必须这样做才能使其他有用)比作为微控制器)。

3)。

每个cpu按照设计的确定性方式“启动”。最常见的方式是向量表,其中上电后要执行的第一条指令的地址位于复位向量中,硬件会读取该地址,然后使用该地址开始运行。另一种通用方法是让处理器开始执行,而无需在某个众所周知的地址使用向量表。有时,芯片会带有“绑带”,您可以在释放复位之前将某些引脚连接到高电平或低电平,该逻辑用于引导不同的方式。您必须将cpu本身(处理器核心)与系统的其余部分分开。了解cpu的工作方式,然后了解芯片/系统设计人员在cpu的外部具有设置地址解码器,以便cpus地址空间的某些部分与闪存进行通信,有些带有ram,有些带有外围设备(uart,i2c,spi,gpio等)。如果愿意,可以采用相同的cpu核心,并以不同的方式包装它。这是您购买手臂或arm嘴产品时得到的。arm和mips制作了cpu内核,人们购买和包装自己的东西时会用到这些芯片,由于各种原因,他们无法使这些东西在品牌之间相互兼容。这就是为什么当涉及核心以外的问题时,很少有人会问一个通用的问题。

微控制器试图成为一个芯片上的系统,因此其非易失性存储器(闪存/ ROM),易失性(SRAM)和CPU都与同一个外围设备混合在同一芯片上。但是芯片是内部设计的,因此闪存被映射到与该CPU的启动特性匹配的CPU的地址空间中。例如,如果cpu在地址0xFFFC处有一个复位向量,则需要有flash / rom来响应我们可以通过1)编程的地址,并且在地址空间中要有足够的flash / rom来进行有用的编程。芯片设计人员可以选择从0xF000开始具有0x1000字节的闪存,以满足这些要求。也许他们在较低地址或0x0000处放置了一些ram,外围设备则在中间。

cpu的另一种体系结构可能从地址零开始执行,因此他们需要做相反的事情,放置闪存,以使其响应零附近的地址范围。例如说0x0000到0x0FFF。然后在其他地方放一些公羊。

芯片设计师知道CPU的启动方式,并在其中放置了非易失性存储(闪存/ ROM)。然后由软件人员编写引导代码以匹配该CPU的众所周知的行为。您必须将重置向量地址放置在重置向量中,并将引导代码放置在重置向量中定义的地址上。工具链可以在这里极大地帮助您。有时,尤其是指向和点击ide或其他沙盒,它们可能为您完成大部分工作,而您要做的就是用高级语言(C)调用api。

但是,无论如何,加载到flash / rom中的程序必须匹配cpu的硬接线启动行为。在程序main()的C部分之前,以及如果使用main作为入口点,则必须完成一些操作。AC程序员假定,当声明一个具有初始值的变量时,他们希望该变量实际起作用。好吧,除了const变量外,其他变量都在ram中,但是如果您有一个带有初始值的变量,则初始值必须在非易失性ram中。因此,这是.data段,C引导程序需要将.data内容从闪存复制到ram(通常由工具链为您确定位置)。您声明的没有初始值的全局变量在程序启动之前被假定为零,尽管您实际上不应假定这样做,而且值得庆幸的是,一些编译器开始警告未初始化的变量。这是.bss段,C引导程序将ram中的零置零,内容零不必存储在非易失性存储器中,而是起始地址和多少。同样,工具链在这里可以为您提供极大的帮助。最后,最基本的要求是您需要设置堆栈指针,因为C程序希望能够具有局部变量并调用其他函数。然后也许完成其他一些特定于芯片的工作,或者让其余的特定于芯片的工作发生在C中。不一定要存储在非易失性存储器中,而是起始地址及其大小。同样,工具链在这里可以为您提供极大的帮助。最后,最基本的要求是您需要设置堆栈指针,因为C程序希望能够具有局部变量并调用其他函数。然后也许完成其他一些特定于芯片的工作,或者让其余的特定于芯片的工作发生在C中。不一定要存储在非易失性存储器中,而是起始地址及其大小。同样,工具链在这里可以为您提供极大的帮助。最后,最基本的要求是您需要设置堆栈指针,因为C程序希望能够具有局部变量并调用其他函数。然后也许完成其他一些特定于芯片的工作,或者让其余的特定于芯片的工作发生在C中。

来自arm的cortex-m系列内核将为您完成某些工作,堆栈指针位于向量表中,有一个重置向量指向重置后要运行的代码,因此您无需执行其他任何操作要生成向量表(通常无论如何通常使用asm),都可以使用纯C语言而不使用asm。现在,您无需复制.data或将.bss清零,因此,如果您想尝试在基于cortex-m的东西上不使用asm,则必须自己做。更大的功能不是复位向量,而是中断向量,其中硬件遵循推荐的C调用约定并为您保留寄存器,并为该向量使用正确的返回值,因此您不必在每个处理程序周围包装正确的asm(或针对目标使用特定于工具链的指令,以使工具链为您包装它)。

例如,特定于芯片的东西可能是微控制器,通常在基于电池的系统中使用,因此功耗低,因此有些器件会在大多数外设关闭的情况下退出复位状态,因此您必须打开每个子系统才能使用它们。Uart,gpios等。通常使用低时钟频率,直接来自晶体或内部振荡器。而且您的系统设计可能表明您需要更快的时钟,因此您将其初始化。您的时钟对于Flash或ram来说可能太快了,因此您可能需要在更改时钟之前更改等待状态。可能需要设置uart,usb或其他接口。那么您的应用程序就可以完成它的工作。

计算机台式机,笔记本电脑,服务器和微控制器的启动/工作方式没有什么不同。除了它们不是大多数都在一个芯片上。BIOS程序通常位于与CPU分离的单独Flash / ROM芯片上。尽管最近x86 cpus将越来越多的过去支持芯片的东西(同一个pcie控制器等)拉到同一个封装中,但是您仍然拥有大部分的ram和rom芯片,但是它仍然是一个系统,并且仍然可以正常工作在高水平上相同。cpu引导过程是众所周知的,电路板设计人员将flash / rom放置在cpu引导的地址空间中。该程序(x86电脑上BIOS的一部分)完成了上述所有操作,启动了各种外围设备,初始化了dram,枚举了pcie总线,依此类推。用户通常可以根据bios设置或我们以前称为cmos设置的方式进行配置,因为当时使用的是技术。没关系,您可以更改用户设置,以告诉BIOS引导代码如何更改其功能。

不同的人会使用不同的术语。芯片启动,这是第一个运行的代码。有时称为引导程序。一个带有“装载程序”一词的引导程序通常意味着,如果您不做任何干扰的引导程序,它将引导您从常规引导进入更大的应用程序或操作系统。但是加载器部分意味着您可以中断引导过程,然后加载其他测试程序。例如,如果您曾经在嵌入式linux系统上使用过uboot,则可以按一个键并停止正常启动,然后可以将测试内核下载到ram中并启动它,而不是从闪存中启动,或者可以下载自己的程序,或者您可以下载新内核,然后让引导程序将其写入闪存,以便下次引导时运行新程序。

至于CPU本身,就是核心处理器,它不知道外设的闪存中的RAM。没有引导程序,操作系统,应用程序的概念。只是将指令序列馈送到要执行的cpu中。这些是软件术语,用于区分不同的编程任务。彼此之间的软件概念。

某些微控制器在芯片的单独闪存或单独的闪存区域中具有芯片供应商提供的单独的引导加载程序,您可能无法对其进行修改。在这种情况下,通常会有一个针脚或一组针脚(我称它们为皮带),如果您在释放复位之前将它们拉高或拉低,则会告诉逻辑和/或引导加载程序该做什么,例如,一个皮带组合可能告诉芯片运行该引导程序,然后在uart上等待将数据编程到闪存中。以其他方式设置束带,则程序不会启动芯片供应商的引导加载程序,从而允许对芯片进行现场编程或从程序崩溃中恢复。有时,纯逻辑允许您对闪存进行编程。如今这很普遍

大多数微控制器具有比ram更多的flash的原因是,主要用例是直接从flash运行程序,并且仅具有足够的ram来覆盖堆栈和变量。尽管在某些情况下,您可以从ram运行程序,但必须对其进行编译,然后将其存储在flash中,然后在调用之前进行复制。

编辑

闪存

.cpu cortex-m0
.thumb

.thumb_func
.global _start
_start:
stacktop: .word 0x20001000
.word reset
.word hang
.word hang
.word hang

.thumb_func
reset:
    bl notmain
    b hang

.thumb_func
hang:   b .

notmain.c

int notmain ( void )
{
    unsigned int x=1;
    unsigned int y;
    y = x + 1;

    return(0);
}

flash.ld

MEMORY
{
    bob : ORIGIN = 0x00000000, LENGTH = 0x1000
    ted : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
    .text : { *(.text*) } > bob
    .rodata : { *(.rodata*) } > bob
    .bss : { *(.bss*) } > ted
    .data : { *(.bss*) } > ted AT > bob
}

因此,这是一个cortex-m0的示例,在这个示例中,cortex-ms的工作原理都相同。对于此示例,特定的芯片在臂地址空间中的地址为0x00000000的应用程序闪存,在0x20000000的ram的应用程序。

cortex-m引导方式是地址0x0000处的32位字,是初始化堆栈指针的地址。对于这个示例,我不需要太多堆栈,因此0x20001000就足够了,显然在该地址下方必须有ram(机械臂压入的方式,是先减去然后压入,所以如果您设置0x20001000,则堆栈中的第一项位于地址0x2000FFFC您不必使用0x2000FFFC)。地址0x0004的32位字是复位处理程序的地址,基本上是复位后运行的第一个代码。然后,还有更多特定于该皮质内核和芯片的中断和事件处理程序,可能多达128或256,如果您不使用它们,则无需为它们设置表,我专门介绍了一些目的。

在此示例中,我不需要处理.data或.bss,因为通过查看代码,我已经知道这些段中没有任何内容。如果有的话,我会处理,一会儿。

这样就完成了堆栈的设置,检查,.data处理,检查,.bss,检查,因此C引导程序工作已经完成,可以跳转到C的入口函数。因为某些编译器在看到该函数时会添加额外的垃圾main(),在通往main的路上,我没有使用确切的名称,在这里我使用notmain()作为我的C入口点。因此,重置处理程序将调用notmain(),然后如果/当notmain()返回时,它将挂起,这只是一个无限循环,可能命名不正确。

我坚信精通这些工具,很多人没有,但是您会发现,每个裸机开发人员都会做自己的事情,这是因为它们几乎完全自由,而不像您制作应用程序或网页那样受限制。他们再次做自己的事。我更喜欢拥有自己的引导程序代码和链接脚本。其他人则依靠工具链,或者在供应商沙盒中发挥作用,其中大部分工作是由其他人完成的(如果某件事中断了,您将处于一个充满伤害的世界中,而裸机则经常且以戏剧性的方式破坏)。

因此,我使用gnu工具进行组装,编译和链接:

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   2000        movs    r0, #0
  1e:   4770        bx  lr

那么,引导程序如何知道东西在哪里。因为编译器完成了工作。在第一种情况下,汇编器为flash.s生成了代码,这样做就知道标签在哪里(标签只是地址,就像函数名或变量名一样,等等),因此我不必计数字节并填写向量手动使用表格,我使用了标签名称,而汇编程序为我完成了该操作。现在,您问,如果复位地址为0x14,汇编器为何将0x15放在向量表中。好吧,这是一个cortex-m,它可以引导并且仅在拇指模式下运行。如果使用ARM模式,则当分支到Thumb模式时,如果分支到一个地址,则需要设置lsbit;如果ARM模式然后复位,则需要设置lsbit。因此,您始终需要设置该位。我知道这些工具,可以通过在标签前放置.thumb_func来实现,如果该标签在矢量表中被使用,或者用于分支到其他对象。工具链知道设置lsbit。因此此处为0x14 | 1 = 0x15。对于挂起也是如此。现在,反汇编程序不显示对notmain()的调用的0x1D,但不用担心工具正确地构建了指令。

现在,在notmain中的代码中,不再使用那些局部变量,它们是无效代码。编译器甚至通过说y已设置但未使用来评论该事实。

注意地址空间,这些东西都从地址0x0000开始,然后从那里开始,所以向量表被正确放置了,.text或程序空间也被正确放置了,我如何在notmain.c的代码前面得到flash.s。知道这些工具后,一个常见的错误就是无法正确使用它,然后崩溃并重燃。IMO,您必须拆卸一下以确保在第一次启动之前就将物品放置在正确的位置,一旦将物品放在正确的位置,则不必每次都检查一下。仅用于新项目或是否挂起。

现在让某些人感到惊讶的是,没有任何理由期望任何两个编译器从相同的输入产生相同的输出。甚至是具有不同设置的同一编译器。使用clang,通过llvm编译器,我可以得到有优化和无优化这两个输出

llvm / clang优化

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   2000        movs    r0, #0
  1e:   4770        bx  lr

没有优化

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   b082        sub sp, #8
  1e:   2001        movs    r0, #1
  20:   9001        str r0, [sp, #4]
  22:   2002        movs    r0, #2
  24:   9000        str r0, [sp, #0]
  26:   2000        movs    r0, #0
  28:   b002        add sp, #8
  2a:   4770        bx  lr

所以这是一个谎言,编译器确实优化了加法运算,但是它确实在堆栈上为变量分配了两个项目,因为这些是局部变量,它们位于ram中,但是在堆栈上的地址不是固定的,因此可以看到全局变量变化。但是编译器意识到它可以在编译时计算y,并且没有理由在运行时计算它,因此它只是在为x分配的堆栈空间中放置了1,为y分配的堆栈空间中放置了2。编译器使用内部表“分配”此空间,我声明堆栈加0表示变量y,堆栈声明加4表示变量x。只要编译器实现的代码符合C标准或C程序员的要求,编译器就可以执行所需的任何操作。没有任何理由使编译器在函数持续时间内必须将x保留在堆栈+ 4处,

如果我在汇编器中添加函数虚拟

.thumb_func
.globl dummy
dummy:
    bx lr

然后叫它

void dummy ( unsigned int );
int notmain ( void )
{
    unsigned int x=1;
    unsigned int y;
    y = x + 1;
    dummy(y);
    return(0);
}

输出变化

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f804   bl  20 <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <dummy>:
  1c:   4770        bx  lr
    ...

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   2002        movs    r0, #2
  24:   f7ff fffa   bl  1c <dummy>
  28:   2000        movs    r0, #0
  2a:   bc10        pop {r4}
  2c:   bc02        pop {r1}
  2e:   4708        bx  r1

现在我们有了嵌套函数,notmain函数需要保留其返回地址,以便它可以掩盖嵌套调用的返回地址。这是因为手臂使用寄存器来返回,如果它像x86或其他一些东西一样使用了堆栈……它仍然会使用堆栈,但是方式有所不同。现在您问为什么它推动了r4?好吧,不久前的调用约定发生了变化,以使堆栈在64位(两个字)的边界上对齐,而不是在32位(一个字)的边界上对齐。因此,他们需要推入一些东西来保持堆栈对齐,因此编译器出于某种原因任意选择了r4,无论原因如何。弹出到r4将是一个错误,尽管按照此目标的调用约定,我们不会在函数调用上破坏r4,而是可以通过r3破坏r0。r0是返回值。看起来它正在进行尾部优化,

但是我们看到x和y数学被优化为传递给虚拟函数的硬编码值2(虚拟对象专门编码在一个单独的文件中,在本例中为asm,因此编译器不会完全优化该函数的调用,如果我有一个虚函数仅在notmain.c中以C返回,则优化器将删除x,y和虚函数调用,因为它们都是无效的/无用的代码)。

还要注意,因为flash.s代码变得更大了,所以不是,并且工具链已经为我们修补了所有地址,因此我们不必手动进行。

未优化的clang供参考

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   b082        sub sp, #8
  26:   2001        movs    r0, #1
  28:   9001        str r0, [sp, #4]
  2a:   2002        movs    r0, #2
  2c:   9000        str r0, [sp, #0]
  2e:   f7ff fff5   bl  1c <dummy>
  32:   2000        movs    r0, #0
  34:   b002        add sp, #8
  36:   bd80        pop {r7, pc}

优化的lang

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   2002        movs    r0, #2
  26:   f7ff fff9   bl  1c <dummy>
  2a:   2000        movs    r0, #0
  2c:   bd80        pop {r7, pc}

该编译器作者选择使用r7作为虚拟变量来对齐堆栈,即使它在堆栈框架中没有任何内容,它也正在使用r7创建框架指针。基本上本来可以优化指令。但是它使用了pop命令,没有返回三条指令,我敢打赌我可以让gcc使用正确的命令行选项(指定处理器)来执行此操作。

这应该大部分可以回答您的其余问题

void dummy ( unsigned int );
unsigned int x=1;
unsigned int y;
int notmain ( void )
{
    y = x + 1;
    dummy(y);
    return(0);
}

我现在有全局变量。因此,如果未进行优化,它们就会进入.data或.bss。

在我们看最终输出之前,先看一下itermediate对象

00000000 <notmain>:
   0:   b510        push    {r4, lr}
   2:   4b05        ldr r3, [pc, #20]   ; (18 <notmain+0x18>)
   4:   6818        ldr r0, [r3, #0]
   6:   4b05        ldr r3, [pc, #20]   ; (1c <notmain+0x1c>)
   8:   3001        adds    r0, #1
   a:   6018        str r0, [r3, #0]
   c:   f7ff fffe   bl  0 <dummy>
  10:   2000        movs    r0, #0
  12:   bc10        pop {r4}
  14:   bc02        pop {r1}
  16:   4708        bx  r1
    ...

Disassembly of section .data:
00000000 <x>:
   0:   00000001    andeq   r0, r0, r1

现在,这里缺少信息,但是它给出了一个正在发生的情况的链接器,链接器是一个接收对象并将它们与提供的信息(在本例中为flash.ld)链接在一起的信息,该信息告诉它.text和的位置。数据等等。编译器不知道这样的事情,它只能专注于所提供的代码,它的任何外部都必须留有空隙供链接器填充连接。它必须留下任何将这些东西链接在一起的数据,因此这里的所有内容的地址都是零,仅仅是因为编译器和该反汇编程序不知道。链接器用来放置事物的其他信息未在此处显示。此处的代码足够独立于位置,因此链接程序可以完成其工作。

然后,我们至少看到链接输出的反汇编

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   4b05        ldr r3, [pc, #20]   ; (38 <notmain+0x18>)
  24:   6818        ldr r0, [r3, #0]
  26:   4b05        ldr r3, [pc, #20]   ; (3c <notmain+0x1c>)
  28:   3001        adds    r0, #1
  2a:   6018        str r0, [r3, #0]
  2c:   f7ff fff6   bl  1c <dummy>
  30:   2000        movs    r0, #0
  32:   bc10        pop {r4}
  34:   bc02        pop {r1}
  36:   4708        bx  r1
  38:   20000004    andcs   r0, r0, r4
  3c:   20000000    andcs   r0, r0, r0

Disassembly of section .bss:

20000000 <y>:
20000000:   00000000    andeq   r0, r0, r0

Disassembly of section .data:

20000004 <x>:
20000004:   00000001    andeq   r0, r0, r1

编译器基本上要求在ram中提供两个32位变量。.bss中有一个,因为我没有初始化它,所以假定它被初始化为零。另一个是.data,因为我确实在声明时对其进行了初始化。

现在,由于这些是全局变量,因此假定其他函数可以修改它们。编译器不对何时可以调用notmain做出任何假设,因此它无法根据其所看到的y = x + 1数学进行优化,因此它必须执行该运行时。它必须从ram读取两个变量,然后将它们相加并保存。

现在,显然此代码将无法正常工作。为什么?因为这里所示的引导程序在调用notmain之前没有准备ram,所以在芯片唤醒时0x20000000和0x20000004中的任何垃圾将用于y和x。

这里不会显示。您可以阅读我对.data和.bss甚至更漫长的漫谈,以及为什么我在裸机代码中不再需要它们,但是如果您觉得自己必须并且想要掌握这些工具,而不是希望别人做对了。 。

https://github.com/dwelch67/raspberrypi/tree/master/bssdata

链接描述文件和引导程序在某种程度上是特定于编译器的,因此您所了解的有关一个编译器的一个版本的所有信息都可能被下一版本或其他某个编译器所困扰,这又是我不花大量精力进行.data和.bss准备的另一个原因只是为了这个懒惰:

unsigned int x=1;

我宁愿这样做

unsigned int x;
...
x = 1;

然后让编译器为我保存在.text中。有时它会以这种方式保存闪光灯,有时会燃烧更多。从工具链版本或一个编译器进行编程和移植无疑是最容易的。更可靠,更不易出错。是的,不符合C标准。

现在,如果我们使这些静态全局变量呢?

void dummy ( unsigned int );
static unsigned int x=1;
static unsigned int y;
int notmain ( void )
{
    y = x + 1;
    dummy(y);
    return(0);
}

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   2002        movs    r0, #2
  24:   f7ff fffa   bl  1c <dummy>
  28:   2000        movs    r0, #0
  2a:   bc10        pop {r4}
  2c:   bc02        pop {r1}
  2e:   4708        bx  r1

显然,这些变量不能被其他代码修改,因此编译器现在可以像以前一样在编译时优化死代码。

未优化

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   4804        ldr r0, [pc, #16]   ; (38 <notmain+0x18>)
  26:   6800        ldr r0, [r0, #0]
  28:   1c40        adds    r0, r0, #1
  2a:   4904        ldr r1, [pc, #16]   ; (3c <notmain+0x1c>)
  2c:   6008        str r0, [r1, #0]
  2e:   f7ff fff5   bl  1c <dummy>
  32:   2000        movs    r0, #0
  34:   bd80        pop {r7, pc}
  36:   46c0        nop         ; (mov r8, r8)
  38:   20000004    andcs   r0, r0, r4
  3c:   20000000    andcs   r0, r0, r0

这个使用本地堆栈的编译器,现在将ram用于全局变量,由于我没有正确处理.data或.bss,编写的代码已损坏。

还有我们在反汇编中看不到的最后一件事。

:1000000000100020150000001B0000001B00000075
:100010001B00000000F004F8FFE7FEE77047000057
:1000200080B500AF04480068401C04490860FFF731
:10003000F5FF002080BDC046040000200000002025
:08004000E0FFFF7F010000005A
:0400480078563412A0
:00000001FF

我将x更改为使用0x12345678进行预初始化。我的链接程序脚本(这是针对gnu ld的)在bob上有这个问题。告诉链接器我希望最终位置在ted地址空间中,但将其存储在ted地址空间中的二进制文件中,然后有人会为您移动它。我们可以看到发生了这种情况。这是英特尔十六进制格式。我们可以看到0x12345678

:0400480078563412A0

在二进制文件的闪存地址空间中。

readelf也显示了这一点

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  EXIDX          0x010040 0x00000040 0x00000040 0x00008 0x00008 R   0x4
  LOAD           0x010000 0x00000000 0x00000000 0x00048 0x00048 R E 0x10000
  LOAD           0x020004 0x20000004 0x00000048 0x00004 0x00004 RW  0x10000
  LOAD           0x030000 0x20000000 0x20000000 0x00000 0x00004 RW  0x10000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10

LOAD行,其中虚拟地址为0x20000004,物理地址为0x48


在开始的时候,我对事情有两个模糊的印象:
user16307 '16

1.)“主要用例是在rom / flash中有指令,在ram中有数据。” 当您说“这里的数据在RAM中”时,是指程序过程中生成的数据。或者您是否还包含初始化的数据。我的意思是当我们将代码上传到ROM时,我们的代码中已经有初始化数据。例如,在我们的oode中,如果有:int x = 1; 整数y = x +1; 上面的代码有指令,并且初始数据为1。(x = 1)。此数据是否也复制到RAM或仅保留在ROM中。
user16307

13
哈哈,我现在知道堆栈交换答案的字符数限制!
old_timer '16

2
你应该写一本书给新手解释这些概念。“我在github上有无数的示例”-是否可以分享一些示例
AkshayImmanuelD

1
我已经做了。没有人会做任何有用的事情,但它仍然是微控制器代码的示例。我确实放置了一个github链接,从中可以找到我共享的其他所有内容,无论好坏,还是其他。
old_timer

8

这个答案将更多地集中在引导过程上。首先,进行校正-在MCU(或至少其中的一部分)启动后,写入闪存。在某些MCU(通常是更高级的MCU)上,CPU本身可能会操作串行端口并写入闪存寄存器。因此,编写和执行程序是不同的过程。我将假定该程序已被写入闪存。

这是基本的启动过程。我会列举一些常见的变体,但主要是我保持简单。

  1. 重置:有两种基本类型。第一个是上电复位,它是在电源电压上升时在内部产生的。第二个是外部引脚切换。无论如何,复位都会强制MCU中的所有触发器进入预定状态。

  2. 额外的硬件初始化:在CPU开始运行之前,可能需要额外的时间和/或时钟周期。例如,在我研究的TI MCU中,有一个内部配置扫描链被加载。

  3. CPU引导: CPU从称为复位向量的特殊地址中获取其第一条指令该地址是在设计CPU时确定的。从那里开始,这只是正常的程序执行。

    CPU一遍又一遍地重复三个基本步骤:

    • 提取:从存储在程序计数器(PC)寄存器中的地址中读取一条指令(8位,16位或32位值),然后递增PC。
    • 解码:将二进制指令转换为一组用于CPU内部控制信号的值。
    • 执行:执行指令-添加两个寄存器,从存储器中读取或写入存储器,转移(更改PC),或其他任何操作。

    (实际上这要复杂得多。CPU通常是流水线的,这意味着它们可以同时在不同的指令上执行上述每个步骤。以上每个步骤可能有多个流水线阶段。然后是并行的流水线,分支预测,以及所有使这些Intel CPU需要十亿个晶体管进行设计的花哨的计算机体系结构。)

    您可能想知道提取的工作方式。CPU具有由地址(输出)和数据(输入/输出)信号组成的总线。为了进行获取,CPU将其地址线设置为程序计数器中的值,然后通过总线发送时钟。地址被解码以启用存储器。存储器接收时钟和地址,并将该值放在数据线上的该地址处。CPU接收该值。数据读取和写入相似,除了地址来自指令或通用寄存器(而非PC)中的值。

    具有von Neumann架构的 CPU具有一条用于指令和数据的总线。具有哈佛架构的 CPU具有一条用于指令的总线和一条用于数据的总线。在实际的MCU中,这两种总线都可能连接到相同的存储器,因此通常(但并非总是)您不必担心。

    回到启动过程。复位后,PC加载一个称为复位向量的起始值它可以内置在硬件中,或者(在ARM Cortex-M CPU中)可以自动从内存中读出。CPU从复位向量中提取指令,并开始循环执行上述步骤。此时,CPU正常执行。

  4. 引导加载程序:通常需要进行一些底层设置,以使其余的MCU正常运行。这可能包括清除RAM以及加载模拟组件的制造调整设置之类的事情。也可以选择从外部源(如串行端口或外部存储器)加载代码。MCU可能包含一个引导ROM,其中包含一个执行这些操作的小程序。在这种情况下,CPU复位向量指向引导ROM的地址空间。这基本上是正常的代码,它是由制造商提供的,因此您不必自己编写。:-)在PC中,BIOS相当于启动ROM。

  5. C环境设置: C希望具有一个堆栈(用于在函数调用期间存储状态的RAM区域)和用于全局变量的初始化内存位置。这些是Dwelch讨论的.stack,.data和.bss部分。在此步骤中,已初始化的全局变量将其初始化值从闪存复制到RAM。未初始化的全局变量的RAM地址彼此靠近,因此可以很容易地将整个内存块初始化为零。堆栈不需要初始化(尽管可以初始化)-您真正需要做的就是设置CPU的堆栈指针寄存器,使其指向RAM中的指定区域。

  6. Main函数:设置C环境后,C加载程序将调用main()函数。那就是您的应用程序代码通常开始的地方。如果需要,可以省略标准库,跳过C环境设置,并编写自己的代码以调用main()。某些MCU可能会让您编写自己的引导加载程序,然后您可以自己进行所有低级设置。

杂项:许多MCU可以让您从RAM中执行代码以提高性能。这通常是在链接器配置中设置的。链接器为每个功能分配两个地址- 加载地址(首先存储代码(通常是闪存))和运行地址运行地址),加载到PC中以执行功能的地址(闪存或RAM)。要从RAM中执行代码,您需要编写代码以使CPU将功能代码从闪存中的加载地址复制到RAM中的运行地址,然后在运行地址处调用函数。链接器可以定义全局变量来帮助实现这一点。但是在MCU中,从RAM中执行代码是可选的。通常,只有在确实需要高性能或要重写闪存时,才需要这样做。


1

对于Von Neumann体系结构,您的摘要大致正确。初始代码通常是通过引导加载程序加载到RAM的,而不是(通常)通过该术语通常指的软件引导加载程序加载的。这通常是“烘焙到硅片中”的行为。这种体系结构中的代码执行通常涉及对ROM的预测性缓存,其方式是使处理器最大化其执行代码的时间,而不必等待代码加载到RAM。我在某处读到了MSP430是这种架构的示例。

哈佛架构设备中,指令是从ROM直接执行的,而数据存储器(RAM)是通过单独的总线访问的。在这种架构中,代码只是从复位向量开始执行。PIC24和dsPIC33是该架构的示例。

至于启动这些进程的实际位翻转,可能因设备而异,并且可能涉及调试器,JTAG,专有方法等。


但是您正在快速跳过一些要点。让我们慢动作吧。可以说二进制代码“ first”被写入ROM。OK ..然后写“访问数据存储器”。...但是,“到RAM”中的数据最初是从哪里来的呢?它又从ROM来吗?如果是这样,引导加载程序如何知道一开始将ROM的哪一部分写入RAM?
user16307 '16

你是对的,我确实跳过了很多。其他人有更好的答案。很高兴您能找到想要的东西。
slightlynybbled
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.