复位后的 ARM PC 值

ARM PC value after Reset

我是 MCU 的新手,正在尝试弄清楚基于 arm (Cortex M3-M4) 的 MCU 是如何启动的。因为开机是特定于任何SOC的,所以我以STM的硬件板为例进行案例研究。

Board: STMicroelectronics – STM32L476 32-bit.

在这个板子里,当启动模式是(x0)"Boot from User Flash",板子将0x0000000地址映射到闪存地址。在闪存上,我粘贴了我的二进制文件,其中前 4 个字节指向向量 table 第一个条目,即 esp。现在,如果我按下重置按钮,ARM 文档说 PC 值将设置为 0x00000000

CPU一般执行基于PC -> PC + 1循环的指令流。在这种情况下,如果我看到 PC 值指向 esp,这不是指令。 Arm CPU 如何执行不使用此指令地址的逻辑,而是跳转到地址 0x00000004 处的值存储?

或者是这样的: 重置会产生特殊的硬件中断并导致 PC 值为 0x00000004,如果是这种情况,为什么 Arm 文档说它将 PC 值设置为 0x00000000?

参考:http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka3761.html

What values are in ARM registers after a power-on reset? Applies to: ARM1020/22E, ARM1026EJ-S, ARM1136, ARM720T, ARM7EJ-S, ARM7TDMI, ARM7TDMI-S, ARM920/922T, ARM926EJ-S, ARM940T, ARM946E-S, ARM966E-S, ARM9TDMI

Answer Registers R0 - R14 (including banked registers) and SPSR (in all modes) are undefined after reset.

The Program Counter (PC/R15) will be set to 0x000000, or 0xFFFF0000 if the core has a VINITHI or CFGHIVECS input which is set high as the core leaves reset. This input should be set to reflect where the base of the vector table in your system is located.

The Current Program Status Register (CPSR) will indicate that the ARM core has started in ARM state, Supervisor mode with both FIQ and IRQ mask bits set. The condition code flags will be undefined. Please see the ARM Architecture Manual for a detailed description of the CPSR.

cortex-m 的启动方式与传统和全尺寸内核的启动方式不同。那些至少用于重置,正如您指出的那样,从地址 0x00000000(或断言的备用地址)获取第一条指令,将其称为 PC 值并不公平,因为此时 PC 有点错误,有多个程序计数器在r15中产生了一个假的,一个在fetching前,一个在做prefetch,none是真正的程序计数器。反正没关系。

armv7-m 文档中记录的 cortex-m(对于 m3 和 m4,对于 m0 和 m0+,请参见 armv6-m,尽管它们到目前为止都以相同的方式启动)。这些使用矢量 TABLE 而不是指令。 CORE 读取地址 0x00000000(如果断言了 strap,则为备用地址),并将该 32 位值加载到堆栈指针寄存器中。它读取地址 0x00000004 它检查 lsbit(可能不是所有内核都这样做)如果设置那么这是一个有效的拇指地址,剥离 lsbit(使其为零)并开始为该地址的重置处理程序获取第一条指令所以如果你的 flash 以

开头
0x00000000 : 0x20001000
0x00000004 : 0x00000101

cortex-m 会将 0x20001000 放入堆栈指针并从地址 0x100 获取第一条指令。作为 thumb 指令是 16 位,thumb2 扩展是两个 16 位部分,它不是 x86,程序计数器针对全尺寸处理器对齐,它使用 32 位指令在对齐地址 0x0000、0x0004、0x0008 上获取它不会递增 pc <= pc + 1;对于 Thumb 模式或 Thumb 处理器,它是 pc = pc + 2。而且提取不一定是单指令事务,对于全尺寸它们每个事务可能提取 4 或 8 个字,如技术参考手册中所述的 cortex-ms有些可以一次编译或绑定到 16 位或一次 32 位。所以无需谈论或考虑获取 pc = pc + 1 的执行循环,即使在如今的 x86 中也没有意义。

公平地说,武器文档总体上是不错的,与许多其他文档相比更好,但不是最好的。与全尺寸 arm 异常 table 不同,cortex-m 文档中的向量 table 并没有像它本来应该做的那样好,have/should 是否可以像全尺寸那样做一些事情但表明它们是矢量而不是指令。它在 armv6-m 和 armv7-m 的体系结构参考手册中(我假设 armv8-m 也是如此,但没有看过,上周拿到了一些零件,但电路板还没有,很快就会知道).无法在该手册中查找诸如重置之类的词,必须查找中断或未定义或硬故障等。

编辑

了解处理器如何开始获取的概念,它可以是他们添加到设计中的任意地址,然后指令的执行决定下一个地址和下一个地址等。

也明白不像 x86 或 microchip pic 或 avrs 等,核心和芯片是两个不同的公司。即使在那些相同的公司设计中,但肯定在 IP 与已知总线之间存在明确划分的情况下,ARM CORE 将在 AMBA/AXI/AHB 总线上读取地址 0x00000004,芯片供应商可以将该地址映射到许多不同的地址他们想要的地方,在这种情况下,对于 stm32,0x00000000 实际上可能没有任何东西,正如他们的文档所暗示的那样,基于引导引脚,他们将其映射到内部引导加载程序,或者将其映射到 0x08000000(或大多数情况下)的用户应用程序stm32 如果有异常那很好,我还没有看到它)所以当以这种方式捆绑并且逻辑镜像了这些地址时,您将在 0x00000000 和 0x08000000、0x00000004 和 0x08000004 等处看到相同的 32 位值,依此类推,数量有限地址 space。这就是为什么即使 linking for 0x00000000 会在某种程度上起作用(直到你达到可能小于应用程序闪存大小的限制),你会看到大多数人 link for 0x08000000 和硬件处理剩下的事情,所以你的 table 真的很想看起来像

0x08000000 : 0x20001000
0x08000004 : 0x08000101

一个stm32,至少我目前看到的几十个。

处理器读取镜像到应用程序闪存中第一个项目的 0x00000000,找到 0x20001000,然后读取镜像到应用程序闪存中第二个字的 0x00000004 并获得 0x08000101,这导致从 0x08000100 获取数据,现在我们正在从正确的完全映射的应用程序闪存地址 space 执行。只要你不改变镜像,我不知道你是否可以在 stm32 上(nxp 芯片你可以,我不知道 ti 或其他品牌)。 VTOR 寄存器的一些 cortex-m 内核在那里并且可以更改(其他它固定在 0x00000000,你不能更改它),你不需要将它更改为 0x08000000 对于 stm32,至少我知道的所有那些。仅当您主动更改零地址 space 的镜像时,如果可能的话,或者如果您说有自己的引导加载程序,并且您的应用程序 space 可能是 0x08004000 并且该应用程序需要一个向量 table 它自己的。然后您要么使用 VTOR,要么构建引导加载程序向量 table,以便它 运行 的代码读取 0x08004000 处的向量并分支到这些向量。 NXP 和其他过去肯定使用 ARMV7TDMI 内核的公司,会让您更改地址零的镜像,因为那些较旧的内核没有可编程向量 table 偏移寄存器,可以帮助您解决他们芯片设计中的问题。带有 VTOR 的较新 ARM 内核消除了这种需求,并且随着时间的推移,芯片供应商可能不会再费心了......

编辑

我不知道你是否有发现板或核,我假设后者,因为前者不可用(希望我知道那个人想要一个。And/or我已经有一个它被埋在抽屉里,我从来没有拿到过它)。

所以这是一个你可以在你的 stm32 上试用的最小程序

.cpu cortex-m0
.thumb
.globl _start
_start:
.word 0x20000400
.word reset
.word loop
.word loop
.thumb_func
loop: b loop
.thumb_func
reset:
    ldr r0,=0x20000000
    mov r2,sp
    str r2,[r0]
    add r0,r0,#4
    mov r2,pc
    str r2,[r0]
    add r0,r0,#4
    mov r1,#0
top:
    str r1,[r0]
    add r1,r1,#1
    b top

建设

arm-none-eabi-as so.s -o so.o
arm-none-eabi-ld -Ttext=0x08000000 so.o -o so.elf
arm-none-eabi-objdump -D so.elf > so.list
arm-none-eabi-objcopy so.elf -O binary so.bin

这应该使用 arm-linux-whatever- 或过去 10 年的 binutils 中的其他 arm-whatever-whatever 工具构建。

在使用二进制文件之前检查反汇编很重要,不想把你的芯片弄成砖块(使用 stm32 有一种方法可以解除砖块)

08000000 <_start>:
 8000000:   20000400    andcs   r0, r0, r0, lsl #8
 8000004:   08000013    stmdaeq r0, {r0, r1, r4}
 8000008:   08000011    stmdaeq r0, {r0, r4}
 800000c:   08000011    stmdaeq r0, {r0, r4}

08000010 <loop>:
 8000010:   e7fe        b.n 8000010 <loop>

08000012 <reset>:
 8000012:   4805        ldr r0, [pc, #20]   ; (8000028 <top+0x6>)
 8000014:   466a        mov r2, sp
 8000016:   6002        str r2, [r0, #0]
 8000018:   3004        adds    r0, #4
 800001a:   467a        mov r2, pc
 800001c:   6002        str r2, [r0, #0]
 800001e:   3004        adds    r0, #4
 8000020:   2100        movs    r1, #0

08000022 <top>:
 8000022:   6001        str r1, [r0, #0]
 8000024:   3101        adds    r1, #1
 8000026:   e7fc        b.n 8000022 <top>
 8000028:   20000000    andcs   r0, r0, r0

反汇编程序不知道向量 table 不是指令,因此您可以忽略它们。

08000000 <_start>:
 8000000:   20000400
 8000004:   08000013
 8000008:   08000011
 800000c:   08000011

08000010 <loop>:
 8000010:   e7fe        b.n 8000010 <loop>

08000012 <reset>:

它是否在 0x08000000 开始向量 table,检查。我们的堆栈指针初始值是 0x00000000,是的,我们有工具为我们放置的重置向量。 thumb_func 告诉他们下面的标签是一些 code/function/procedure/whatever_not_data 的地址,所以他们或那里的那个给我们。我们的重置处理程序位于地址 0x08000012,因此我们希望在向量 table 中看到 0x08000013,请检查。为了演示目的,我又扔了几个,将它们发送到地址 0x08000010 处的无限循环,因此向量 table 应该有 0x08000011,检查。

因此,假设您有一个 nucleo 板而不是 discovery,那么您可以将 so.bin 文件复制到插入时显示的拇指驱动器。

如果你使用openocd通过stlink接口连接到开发板,你现在可以看到它是运行ning(细节留给reader弄清楚)

Open On-Chip Debugger
> halt
stm32f0x.cpu: target state: halted
target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08000022 msp: 0x20000400
> mdw 0x20000000 20
0x20000000: 20000400 0800001e 0048cd01 200002e7 200002e9 200002eb 200002ed 00000000 
0x20000020: 00000000 00000000 00000000 200002f1 200002ef 00000000 200002f3 200002f5 
0x20000040: 200002f7 200002f9 200002fb 200002fd 
> resume
> halt
stm32f0x.cpu: target state: halted
target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08000022 msp: 0x20000400
> mdw 0x20000000 20
0x20000000: 20000400 0800001e 005e168c 200002e7 200002e9 200002eb 200002ed 00000000 
0x20000020: 00000000 00000000 00000000 200002f1 200002ef 00000000 200002f3 200002f5 
0x20000040: 200002f7 200002f9 200002fb 200002fd 

因此我们可以看到堆栈指针如预期的那样具有 0x20000400

0x20000000: 20000400 0800001e 0048cd01

程序计数器不是什么神奇的东西,他们必须稍微伪造它才能使指令集工作。

 800001a:   467a        mov r2, pc

按照指令集中的定义,这条指令中使用的 pc 值比这条指令的地址提前两条指令,所以 0x0800001A + 4 = 0x0800001E 这就是我们在内存转储中看到的。

第三项是一个计数器,显示我们正在 运行ning,resume 和 halt 显示该计数一直在进行

0x20000000: 20000400 0800001e 005e168

因此,这演示了向量 table、初始化堆栈指针、重置向量、代码执行开始的位置、程序中某个时刻 pc 的值是什么,以及查看程序 运行.

.cpu cortex-m0 使它为 cortex-m 系列构建了最兼容的程序,而 mov r0,=0x20000000 是作弊,你在评论中发布了相同的功能,它说我想要将 blah 的地址加载到寄存器中,标签只是一个地址,他们让你只放一个地址 =_estack 是标签的地址 =0x20000000 只是一个被视为地址的数字(地址也只是数字,没什么他们很神奇)。我本可以通过轮班完成较小的立即数,或者明确地完成 pc 相对负载。在这种情况下是习惯的力量。

编辑2

为了让程序员理解芯片是逻辑的,只有一部分是 software/instruction 驱动的,即使在逻辑中它只是做比软件指令本身指示的更多的事情。你想从内存中读取你的指令要求处理器执行它但是在一个真正的芯片中有很多步骤来实际执行它,微编码与否(ARM 没有微编码)有状态机来完成各个步骤执行这些任务中的每一项。从寄存器中获取值,计算地址,执行内存事务(这是几个单独的步骤),获取 return 值并将其放入寄存器文件中。

.thumb
.globl _start
_start:
.word 0x20001000
.word reset
.word loop
.word loop
.thumb_func
loop: b loop
.thumb_func
reset:
    ldr r0,loop_counts
loop_top:
    sub r0,r0,#1
    bne loop_top
    b reset
.align
loop_counts: .word 0x1234


00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000013    andeq   r0, r0, r3, lsl r0
   8:   00000011    andeq   r0, r0, r1, lsl r0
   c:   00000011    andeq   r0, r0, r1, lsl r0

00000010 <loop>:
  10:   e7fe        b.n 10 <loop>

00000012 <reset>:
  12:   4802        ldr r0, [pc, #8]    ; (1c <loop_counts>)

00000014 <loop_top>:
  14:   3801        subs    r0, #1
  16:   d1fd        bne.n   14 <loop_top>
  18:   e7fb        b.n 12 <reset>
  1a:   46c0        nop         ; (mov r8, r8)

0000001c <loop_counts>:
  1c:   00001234    andeq   r1, r0, r4, lsr r2

指令集模拟器仅够 运行 该程序。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


#define ROMMASK 0xFFFF
#define RAMMASK 0xFFF

unsigned short rom[ROMMASK+1];
unsigned short ram[RAMMASK+1];

unsigned int reg[16];
unsigned int pc;
unsigned int cpsr;
unsigned int inst;

int main ( void )
{
    unsigned int ra;
    unsigned int rb;
    unsigned int rc;
    unsigned int rx;

    //just putting something there, a real chip might have an MBIST, might not.
    memset(reg,0xBA,sizeof(reg));
    memset(ram,0xCA,sizeof(ram));
    memset(rom,0xFF,sizeof(rom));

    //in a real chip the rom/flash would contain the program and not
    //need to do anything to it, this sim needs to have the program
    //various ways to have done this...

                            //00000000 <_start>:
    rom[0x00>>1]=0x1000;    //   0: 20001000    andcs   r1, r0, r0
    rom[0x02>>1]=0x2000;
    rom[0x04>>1]=0x0013;    //   4: 00000013    andeq   r0, r0, r3, lsl r0
    rom[0x06>>1]=0x0000;
    rom[0x08>>1]=0x0011;    //   8: 00000011    andeq   r0, r0, r1, lsl r0
    rom[0x0A>>1]=0x0000;
    rom[0x0C>>1]=0x0011;    //   c: 00000011    andeq   r0, r0, r1, lsl r0
    rom[0x0E>>1]=0x0000;
                            //
                            //00000010 <loop>:
    rom[0x10>>1]=0xe7fe;    //  10: e7fe        b.n 10 <loop>
                            //
                            //00000012 <reset>:
    rom[0x12>>1]=0x4802;    //  12: 4802        ldr r0, [pc, #8]    ; (1c <loop_counts>)
                            //
                            //00000014 <loop_top>:
    rom[0x14>>1]=0x3801;     //  14:    3801        subs    r0, #1
    rom[0x16>>1]=0xd1fd;     //  16:    d1fd        bne.n   14 <loop_top>
    rom[0x18>>1]=0xe7fb;     //  18:    e7fb        b.n 12 <reset>
    rom[0x1A>>1]=0x46c0;     //  1a:    46c0        nop         ; (mov r8, r8)
                            //
                            //0000001c <loop_counts>:
    rom[0x1C>>1]=0x0004;     //  1c:    00001234    andeq   r1, r0, r4, lsr r2
    rom[0x1E>>1]=0x0000;


    //reset
    //THIS IS NOT SOFTWARE DRIVEN LOGIC, IT IS JUST LOGIC
    ra=rom[0x00>>1];
    rb=rom[0x02>>1];
    reg[14]=(rb<<16)|ra;
    ra=rom[0x04>>1];
    rb=rom[0x06>>1];
    rc=(rb<<16)|ra;
    if((rc&1)==0) return(1); //normally run a fault handler here
    pc=rc&0xFFFFFFFE;
    reg[15]=pc+2;
    cpsr=0x000000E0;

    //run
    //THIS PART BELOW IS SOFTWARE DRIVEN LOGIC
    //still you can see that each instruction requires some amount of
    //non-software driven logic.
    //while(1)
    for(rx=0;rx<20;rx++)
    {
        inst=rom[(pc>>1)&ROMMASK];
printf("0x%08X : 0x%04X\n",pc,inst);        
        reg[15]=pc+4;
        pc+=2;
        if((inst&0xF800)==0x4800)
        {
            //LDR
printf("LDR r%02u,[PC+0x%08X]",(inst>>8)&0x7,(inst&0xFF)<<2);
            ra=(inst>>0)&0xFF;
            rb=reg[15]&0xFFFFFFFC;
            ra=rb+(ra<<2);
printf(" {0x%08X}",ra);            
            rb=rom[((ra>>1)+0)&ROMMASK];
            rc=rom[((ra>>1)+1)&ROMMASK];
            ra=(inst>>8)&0x07;
            reg[ra]=(rc<<16)|rb;
printf(" {0x%08X}\n",reg[ra]);            
            continue;
        }
        if((inst&0xF800)==0x3800)
        {
            //SUB
            ra=(inst>>8)&0x07;
            rb=(inst>>0)&0xFF;
printf("SUBS r%u,%u ",ra,rb);
            rc=reg[ra];
            rc-=rb;
            reg[ra]=rc;
printf("{0x%08X}\n",rc);            
            //do flags
            if(rc==0) cpsr|=0x80000000; else cpsr&=(~0x80000000); //N flag
            //dont need other flags for this example
            continue;
        }
        if((inst&0xF000)==0xD000) //B conditional
        {
            if(((inst>>8)&0xF)==0x1) //NE
            {
                ra=(inst>>0)&0xFF;
                if(ra&0x80) ra|=0xFFFFFF00;
                rb=reg[15]+(ra<<1);
printf("BNE 0x%08X\n",rb);
                if((cpsr&0x80000000)==0)
                {
                    pc=rb;
                }
                continue;
            }
        }
        if((inst&0xF000)==0xE000) //B 
        {
            ra=(inst>>0)&0x7FF;
            if(ra&0x400) ra|=0xFFFFF800;
            rb=reg[15]+(ra<<1);
printf("B 0x%08X\n",rb);
            pc=rb;
            continue;
        }

        printf("UNDEFINED INSTRUCTION 0x%08X: 0x%04X\n",pc-2,inst);
        break;
    }
    return(0);
}

欢迎你讨厌我的编码风格,这是为了这个问题而拼凑的蛮力。不,我不为 ARM 工作,这都可以从 public documents/information 中提取。我将循环缩短为 4 个计数以查看它是否命中外循环

0x00000012 : 0x4802
LDR r00,[PC+0x00000008] {0x0000001C} {0x00000004}
0x00000014 : 0x3801
SUBS r0,1 {0x00000003}
0x00000016 : 0xD1FD
BNE 0x00000014
0x00000014 : 0x3801
SUBS r0,1 {0x00000002}
0x00000016 : 0xD1FD
BNE 0x00000014
0x00000014 : 0x3801
SUBS r0,1 {0x00000001}
0x00000016 : 0xD1FD
BNE 0x00000014
0x00000014 : 0x3801
SUBS r0,1 {0x00000000}
0x00000016 : 0xD1FD
BNE 0x00000014
0x00000018 : 0xE7FB
B 0x00000012
0x00000012 : 0x4802
LDR r00,[PC+0x00000008] {0x0000001C} {0x00000004}
0x00000014 : 0x3801
SUBS r0,1 {0x00000003}
0x00000016 : 0xD1FD
BNE 0x00000014
0x00000014 : 0x3801
SUBS r0,1 {0x00000002}
0x00000016 : 0xD1FD
BNE 0x00000014
0x00000014 : 0x3801
SUBS r0,1 {0x00000001}
0x00000016 : 0xD1FD
BNE 0x00000014
0x00000014 : 0x3801
SUBS r0,1 {0x00000000}
0x00000016 : 0xD1FD
BNE 0x00000014
0x00000018 : 0xE7FB
B 0x00000012

也许这会有所帮助,也许这会使情况变得更糟。大多数逻辑不是由指令驱动的,每条指令都需要一定数量的逻辑,不包括指令获取等常见逻辑。

如果你添加更多的代码,这个模拟器会破坏它,它只支持这些指令和这个循环。

当您对 Arm 处理器的某些行为感到困惑时,最重要的检查可能是检查适用的体系结构版本。您会发现大量与 ARM7 和 ARM9 设计相关的非常古老的遗留文档。虽然今天并非所有这些都是错误的,但它可能会产生很大的误导。

  • ARM v4、ARM v5、ARM v6:这些是遗留设计,现在甚至很少用于衍生产品。
  • ARM v7A:这是 Cortex 系列的第一个。 Cortex-A5 是 2018 年 linux class 设备的入门级。
  • ARM v7M、ARM v6M:这些是像您的 STM32 一样常见的微控制器设备,并且已经有超过 10 年的历史
  • ARM v8A:这些介绍了 64 位指令集(T32/A32/A64 在一个设备中),例如 R-pi 3 中的入门级。
  • ARM v8M:具有更高级安全功能的微控制器架构的最新版本,2018 年第 2 季度刚刚开始可用

具体而言,ARMv6M/ARMv7M/ARMv8M 与所有其他 ARM 架构(在该系列中保持相似)相比,提供了一个非常不同的异常模型,而许多其他差异是渐进的或集中在特定领域。