我傻逼了,这里的注释还是不要用;
全部换成
/* */
这里就分为两块,一部分是复位中断部分,第二部分就是IRQ部分(中断部分最重要)
我就围绕着两部分来展开我的认识
首先声明全局 .global_start
在 ARM 架构的程序中,_start
常常是 C 程序的 main
函数之前的汇编代码,它负责设置程序的初始状态,比如初始化堆栈指针、设置 BSS 段、调用 C/C++ 运行时初始化代码等,然后将控制权传递给 main
函数
不知道为什么视频里的清楚和设置bss段,后面的代码没有了
第一步:设置中断向量表
这里设置中断向量表的顺序是固定的,依照架构技术手册设置。
发生了哪个中断就把其地址加载给pc指针
;中断向量表
ldr pc,=Reset_Handler
ldr pc,=Undefined_Handler
ldr pc,=SVC_Handler
ldr pc,=PrefAbort_Handler
ldr pc,=DataAbort_Handler
ldr pc,=NotUsed_Handler
ldr pc,=IRQ_Handler
ldr pc,=FIQ_Handler
一共有8种中断类型,我们最关心的两种一个是系统复位中断,第二种是外设中断IRQ中断,其他的目前是不太关心。所以其实现也是一个死循环,如下
SVC_Handler:
ldr r0,=SVC_Handler
bx r0
第二步:实现复位中断函数和IRQ中断函数
关闭C1寄存器里的几个功能,主要是操作SCTLR寄存器
mrc p15,0,r0,c1,c0,0 /* 读取CP15的C1寄存器到R0中 */
bic r0,r0,#(1<<12)/*清除C1寄存器的bit12位(I位),关闭I Cache */
bic r0,r0,#(1<<11) /*清除C1寄存器的bit11(Z位),关闭分支预测 */
bic r0,r0,#(1<<2)/*清除C1寄存器的bit2(C位),关闭D Cache */
bic r0,r0,#(1<<1)/*清除C1寄存器的bit1(A位),关闭对齐 */
bic r0,r0,#(1<<0)/*清除C1寄存器的bit0(M位),关闭MMU */
mcr p15,0,r0,c1,c0,0 /* 将r0寄存器中的值写入到CP15的C1寄存器中*/
配置完该寄存器后就开始设置中断向量表基地址,这几个中断的偏移是在基地址为0x0的基础上的,但是我们存放其位置是在0x87800000
/* 中断向量表偏移*/
ldr r0,=0x87800000
dsb
isb
mcr p15,0,r0,c12,c0,0
dsb
isb
其中dsb和isb是为了保障操作完成。
mcr p15,0,r0,c12,c0,0
该段代码意思是从CP15协处理器c12存入这个偏移地址
而c12里的VBAR是一个用来存放中断向量偏移基地址的寄存器
初始化这八种中断情况下的pc堆栈指针了。
当然可以全部实现,也可以只实现我们需要的
/* 设置各个模式下的堆栈指针*/
/* IRQ模式*/
mrs r0,spsr;
bic r0,r0,#(0x1f)/*将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0,r0,#(0x12)/*r0或上0x12,表示使用IRQ模式 */
msr cpsr,r0 /*将r0 的数据写入到cpsr_c中 */
ldr sp,=0x80600000/* 设置IRQ模式下的栈首地址为0X80600000,大小为2MB*/
/* SYS模式*/
mrs r0,spsr;
bic r0,r0,#(0x1f) /*将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0,r0,#(0x1f)/* r0或上0x1f,表示使用SYS模式*/
msr cpsr,r0/*将r0 的数据写入到cpsr_c中 */
ldr sp,=0x80400000/* 设置IRQ模式下的栈首地址为0X80400000,大小为2MB*/
/* IRQ模式*/
mrs r0,spsr;
bic r0,r0,#(0x1f)/*将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0,r0,#(0x13)/* r0或上0x13,表示使用SVC模式*/
msr cpsr,r0/*将r0 的数据写入到cpsr_c中 */
ldr sp,=0x80200000/*设置SVC模式下的栈首地址为0X80200000,大小为2MB */
在 ARM 架构中,状态寄存器 `SPSR`(Saved Program Status Register)用于保存当前的程序状态信息,包括处理器模式。
以下是切换处理器模式的几种常见方法:
1. **使用 `CPS` 指令**:ARM 架构提供了 `CPS`(Change Processor State)指令,用于切换处理器模式。例如,`CPS #0x13` 将切换到 SVC(Supervisor)模式,其中 `0x13` 是模式值加上一个优先级级别。
2. **修改 `CPSR` 寄存器**:直接修改 `CPSR` 寄存器可以改变处理器模式,但这通常不是安全的编程实践,因为它可能影响当前的程序状态。
3. **使用 `MSR` 指令**:`MSR`(Move to Status Register)指令可以用来修改 `CPSR` 或 `SPSR` 的某些字段。例如,`MSR spsr_cxsf, r0` 可以将寄存器 `r0` 的值写入 `SPSR` 的控制和状态字段。
4. **异常和中断处理**:当异常或中断发生时,处理器会自动切换到相应的模式,并保存当前的 `CPSR` 到相应的 `SPSR`。处理完异常或中断后,处理器会从 `SPSR` 恢复 `CPSR` 的值,从而切换回原来的模式。
5. **使用 `BX` 指令**:在某些情况下,使用 `BX` 指令跳转到一个具有不同模式的代码位置也可以实现模式切换。被跳转的目标代码需要设置正确的模式。
`SPSR` 主要用于异常和中断处理中保存和恢复程序状态,而不是直接用来切换模式。在编写程序时,应该使用 `CPS` 指令或 `MSR` 指令来安全地切换处理器模式,并确保程序状态的正确性。直接修改 `SPSR` 可能会导致不可预测的行为,因为 `SPSR` 也包含了其他状态信息,如中断屏蔽位等。
SPSR寄存器用来改变状态的就是低5位,当然我们也可以直接用cps 加上那5位切换到对应模式
cps #0x13/*切换到SVC模式*/
ldr sp,=0x80600000/*设置堆栈指针*/
这样也许更简洁。
设置完堆栈指针就该跳转到 main函数了,但是前面的设置过程为保证安全会关闭全局中断,设置完后再打开,类似于freertos里的进入临界区
2.1 具体的复位函数实现
Reset_Handler:
cpsid i /* 关闭全局中断*/
mrc p15,0,r0,c1,c0,0 /* 读取CP15的C1寄存器到R0中 */
bic r0,r0,#(1<<12)/*清除C1寄存器的bit12位(I位),关闭I Cache */
bic r0,r0,#(1<<11) /*清除C1寄存器的bit11(Z位),关闭分支预测 */
bic r0,r0,#(1<<2)/*清除C1寄存器的bit2(C位),关闭D Cache */
bic r0,r0,#(1<<1)/*清除C1寄存器的bit1(A位),关闭对齐 */
bic r0,r0,#(1<<0)/*清除C1寄存器的bit0(M位),关闭MMU */
mcr p15,0,r0,c1,c0,0 /* 将r0寄存器中的值写入到CP15的C1寄存器中*/
/* 设置各个模式下的堆栈指针*/
/* IRQ模式*/
mrs r0,spsr;
bic r0,r0,#(0x1f)/*将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0,r0,#(0x12)/*r0或上0x12,表示使用IRQ模式 */
msr cpsr,r0 /*将r0 的数据写入到cpsr_c中 */
ldr sp,=0x80600000/* 设置IRQ模式下的栈首地址为0X80600000,大小为2MB*/
/* SYS模式*/
mrs r0,spsr;
bic r0,r0,#(0x1f) /*将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0,r0,#(0x1f)/* r0或上0x1f,表示使用SYS模式*/
msr cpsr,r0/*将r0 的数据写入到cpsr_c中 */
ldr sp,=0x80400000/* 设置IRQ模式下的栈首地址为0X80400000,大小为2MB*/
/* IRQ模式*/
mrs r0,spsr;
bic r0,r0,#(0x1f)/*将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0,r0,#(0x13)/* r0或上0x13,表示使用SVC模式*/
msr cpsr,r0/*将r0 的数据写入到cpsr_c中 */
ldr sp,=0x80200000/*设置SVC模式下的栈首地址为0X80200000,大小为2MB */
cpsie i /*打开全局中断 */
b main/*跳转到main函数 */
然后就是最重要的IRQ中断函数的编写
2.2 具体IRQ函数的实现
首先保存lr寄存器的值,也就是保存现场,也就是入栈操作,把寄存器中的值保存进内存中去。
入栈 r0-r3,r12,因为这几个寄存器不会自动保存,要手动保存
然后就使spsr状态寄存器,它也不会自动保存
好了这几个寄存器全部压入栈内了,等于中断前的现场已经保存完整
进入协处理器CP15的C15经过操作数4的选择后最终我们选择读取CBAR寄存器的值
这个寄存器全名叫做配置基地址寄存器
我们为什么要得到该寄存器的值呢,因为我们要进一步得到GIC控制器的基地址。GIC的基地址存在CBAR里,这点是最重要的。没有CBAR的读取就找不到GIC的基地址
通过找到GIC的基地址,再经过偏移0x2000我们就得到GIC-CPU接口的基地址再偏移0xc我们得到了GIC-IAR寄存器的地址。这个GIC-IAR寄存器就是我们干这麽久最关键的地方。如果是知道这个偏移,那我们这样就更便捷,但是可读性就差了一些。
add r1,r1,#(0x200C);
找到IAR寄存器后读取寄存器里的值我们就得到了CPUID和中断号,他们被存入r0寄存器
得到中断号后我们的目的就得到了,就能找到对应的中断函数地址了。入栈r0,r1,把中断号和GIC-CPU接口的地址保存下来
切换到SVC模式,保存当前模式下的lr寄存器
r0,r1都用了,根据system_irqhandler得到入口地址存到r2寄存器内
blx跳到对应函数。
出栈SVC模式下的lr寄存器,切换到IRQ模式。出栈r0,r1。在不同的处理模式下出入栈lr都是成对的,它们拥有不完全公用的堆栈空间。这里的出栈入栈都是针对内存而言的,出栈就是内存到寄存器,入栈就使寄存器到内存。此刻r0存的是中断号,r1存的是GIC-CPU的基地址。把中断号存入GIC-CPU的基地址偏移0x10处的寄存器内表示中断执行完成,写EOIR 出栈r0恢复状态寄存器也就是
当前堆栈里还存有r0,r0-r3,r12的值,其中第一个r0存的是spsr寄存器的值
pop {r0}
msr spsr, r0
这个与那个应该是等价的,都是接受栈内原本存入的spsr寄存器的值
然后恢复现场,还IRQ中断发生前的r0-r3,r12寄存器的内容
弹出lr-4的值给pc
这里我开始困惑的地方就是为什么没有-8,后来我才知道运行那段会被强制执行完,所以-4就可以了。
IRQ_Handler:
push {lr} /*保存当前运行地址 */
push {r0-r3, r12} /* 其他寄存器会自动保存,这几个要手动*/
mrs r0 ,spsr /*读取状态寄存器 */
push {r0} /*保存 */
mrc p15,4,r1,c15,c0,0 /*读取CP15的CBAR寄存器 */
add r1,r1,#(0x2000) /*基地址偏移0x2000得到GIC-CPU接口的基地址 */
ldr r0,[r1,#(0xc)] /* 将GIC-CPU基地址再偏移0xc得到GICC-IAR寄存器的基地址,取该地址的值*/
/*得到中断号及CPUID */
push {r0,r1}
cps #(0x13) /* 切换到SVC模式*/
push {lr} /*保存svc模式下的lr寄存器 */
ldr r2,=system_irqhandler /* 加载C语言的IRQ中断处理函数*/
blx r2 /*跳转到对应的IRQ中断函数 */
pop {lr}
cps #0x12 /* 进入IRQ模式*/
pop {r0,r1}
str r0,[r1,#(0x10)] /*将其中断ID号写入r0保存的地址中,也就是IAR基地址 */
pop {r0}
msr spsr_cxsf, r0 /*恢复状态寄存器 */
pop {r0-r3,r12}
pop {lr}
subs pc,lr,#4
经过我的测试,把中断向量基地址的偏移放在 _start:下也是没有问题的。