Cortex-A53 裸机开发笔记
硬件环境:创龙 imx8mm 开发板
软件开发环境:DS5
1. uboot 的 bootelf 命令无法正常运行 elf 文件
解决:在 DS-5 中,使用 fromelf.exe 应用程序将 axf 文件转换为 bin 文件,命令如下:
fromelf --bin -o os.bin ARMv8a_startup0.axf
然后使用 go 命令运行 bin 文件
程序下载与运行命令如下:
tftpboot 41000000 os.bin; go 0x41000000
2. os.bin 启动运行时触发异常
经过分析,有两种情况:
- 存在多个函数调用的话会触发异常,esr = 0x96000021,分析为对齐失败
- 陷入死循环的话会触发异常,esr = 0xbf000002,分析为同步异常
解决:
串口寄存器只能以字节的形式访问,以字的形式访问会卡死,出现上述两种异常。
这不对啊,沃日,控制寄存器这些不都是 32 位的,而且我在 uboot 里面以字的形式访问就没有出现问题!!!
再解决,读写寄存器宏是 unsigned long 指针类型的,如下:
#define readl( addr ) (*((volatile unsigned long*)(addr)))
#define writel( v, addr ) (*((volatile unsigned long*)(addr)) = (unsigned long)(v))
这在 Cortex-A53 也许是 8 字节长度的,所以并不是 word 类型,将读写寄存器的宏更改为 unsigned int 类型,如下:
#define readw( addr ) (*((volatile unsigned int*)(addr)))
#define writew( v, addr ) (*((volatile unsigned int*)(addr)) = (unsigned int)(v))
这样不会访问 0x3089048,异常触发是因为访问到了这个地址。上述问题解决。
待解决问题:unsigned long 数据长度?
经过测试:
unsigned long 是 8 字节长度,long 也是 8 字节长度;
unsigned int 是 4 字节长度,int 也是 4 字节长度;
指针类型都是 8 字节长度。
所以,cortex-a53 是 LP64 数据类型模型(long 与指针类型都是 64 位)
3.串口 FIFO 满后无法继续打印
测试发现,串口 fifo 为 32 个实体,当 fifo 填充满后,无法继续打印。
解决:
之前的逻辑为,只要判断 TXFIFO 不满就直接往里写;
现在更改为在写 TXFIFO 之前,判断 TXFIFO 是否空,只有空的情况下才往里写,写完了之后也需要判断 TXFIFO 是否为空,然后开启下一次写 TXFIFO,或者直接退出。
4.判断当前异常等级
经过读 CurrentEL 当前异常等级为 EL2
5.IRQ 路由设置
- 当 SCR_EL3 的 IRQ 位域置位时,低异常等级的 IRQ 全部路由到 EL3。
- 当 SCR_EL3 的 IRQ 位域没有置位,但 HCR_EL2 的 IMO 位域置位时,低异常等级的 IRQ 全部路由到 EL2。
- 当 SCR_EL3 的 IRQ 位域没有置位,且HCR_EL2 的 IMO 位域也没有置位时,低异常等级的 IRQ 全部路由到 EL1.
一般我们使用第 3 种配置,即 SCR_EL3 .IRQ = 0, HCR_EL2 .IMO = 0
6.无法使用 eret 指令从 EL2 返回指定的 EL1 地址运行
现象:指定的打印没有出现,代码如下。
el1_entry_aarch64:
/* 设置 EL1 堆栈指针 */
ldr x0, =Image$$ARM_LIB_STACK$$ZI$$Limit
mov sp, x0
bl bspDbgStartupMsg
b el1_entry_aarch64
按照代码逻辑,在跳转到 EL1 后,设置堆栈指针,应该执行bspDbgStartupMsg() 打印。但却并没有打印。
测试:将返回 el1 的指定移动到开头:
mov x1, #(AARCH64_SPSR_EL1h | \
AARCH64_SPSR_F | \
AARCH64_SPSR_I | \
AARCH64_SPSR_A | \
AARCH64_SPSR_D)
msr spsr_el2, x1
adr x1, el1_entry_aarch64
msr elr_el2, x1
bl bspDbgStartupMsg
eret
则出现如下异常:
[INFO]:CurrentEL =0x00000008 [INFO]:HCR_EL2 =0x00000022 [INFO]:SCTLR_EL2 =0x30c51835 [INFO]:cnthctl_el2 =0x00000003 [INFO]:id_aa64pfr0_el1 =0x01002222 [INFO]:ICC_SRE_EL2 =0x0000000f [INFO]:mpidr_el1 =0x80000000 [INFO]:sctlr_el1 =0x30d00800 [INFO]:VBAR_EL2 =0x7ecee000 [INFO]:elr_el2 =0x410000d0 “Synchronous Abort” handler, esr 0x3a000000 elr: 00000000025130d0 lr : 000000000251301c (reloc) elr: 00000000410000d0 lr : 000000004100001c x0 : 0000000000000000 x1 : 0000000000000001 x2 : 0000000000000000 x3 : 0000000000000000 x4 : 00000000ffffffff x5 : 000000007cce1de8 x6 : 0000000000000030 x7 : 000000000000000f x8 : 0000000000000000 x9 : 0000000000000000 x10: 0000000000000000 x11: 0000000000000000 x12: 0000000000000004 x13: 0000000000000200 x14: 000000007cce2ac8 x15: 0000000000000001 x16: 0000000041000000 x17: 00000000000041a0 x18: 000000007ccecdc8 x19: 000000007cd15478 x20: 0000000041000000 x21: 0000000000000002 x22: 000000007cd15470 x23: 0000000000000002 x24: 000000007eddbcc0 x25: 0000000000000000 x26: 0000000000000000 x27: 0000000000000000 x28: 000000007ccfb220 x29: 000000007cce2810
esr = 0x3a000000, 分析为非法的执行状态???
也就是说不认识下面要执行的指令,指令与执行状态不匹配,
所以:上述异常的出现是因为没有设置 hcr_el2 寄存器的 RW 位,导致他不认识这个指令。这个异常与不打印没有任何关系
测试发现:清除 SCTLR_EL2 的 bit2 DCache 使能位后,原本能打印的函数逻辑,此时也不能打印
猜测:
- 1.也许此时不打印是正常的,因为没有设置 MMU 以及使能 cache.
- 2.也许死在了 EL1 异常中.
继续测试:使用自定义的异常向量处理函数,查看是否可以正常打印异常。
EL2 异常向量表可以正常打印,但 EL1 无法测试,而且在 EL1 使用可以触发异常的函数发现,EL1 异常确实没有打印。
那么无法判断是否死在了异常中。
将 uboot 的 EL2 异常表基地址,设置为 VBAR_EL1 依旧没有打印。
添加硬件复位函数,在 EL2 阶段可以执行成功,但在 EL1 阶段无法执行成功。
经过测试,cpu 表现得好像不认识地址了一样。
继续测试发现,只要屏蔽的 SCTLR_EL2 寄存器的 bit2, 那么 EL2 也会出现这个现象, 串口打印没有用,复位函数也没有用。
所以,感觉像是 DCache 的问题。
继续测试:添加 MMU 页表配置再测试
添加 MMU 页表也不行。
对比 linux 启动代码进行分析:
下图为当前 cpu 寄存器的一些状态
[INFO]: CPU Startup Debug Test!!! [INFO]: sizeof(short) = 0x00000002 [INFO]:CurrentEL =0x00000008 [INFO]:HCR_EL2 =0x80000002 [INFO]:SCTLR_EL2 =0x30c51835 [INFO]:cnthctl_el2 =0x00000003 [INFO]:ICC_SRE_EL2 =0x0000000f [INFO]:VBAR_EL2 =0x41002000 [INFO]:VBAR_EL1 =0x41001800 [INFO]:elr_el2 =0x2131639a [INFO]:ACTLR_EL2 =0x00000073 [INFO]:ACTLR_EL1 =0x00000000 [INFO]:S3_1_c15_c2_1 =0x00000040 [INFO]:TTBR0_EL2 =0x7fff0000 0x7fff1003 0x00000000 0x7fffc003 0x00000000 0x80000611 [INFO]:MAIR_EL2 =0x440c0400 [INFO]:TCR_EL2 =0x80803520 [INFO]:SPsel =0x00000001 [INFO]:id_aa64pfr0_el1 =0x01002222 [INFO]:mpidr_el1 =0x80000000 [INFO]:sctlr_el1 =0x30d00800 [INFO]:SP_EL1 =0x00000000 [INFO]:DAIF =0x000002c0 [INFO]:id_aa64mmfr1_el1 =0x00000000 [INFO]:id_aa64dfr0_el1 =0x10305106
握草,必须在启动代码 EL2 入口处使无效所有的 DCache,否则会报莫名错误,添加如下两条汇编语句:
bl InvalidateUDCaches //使无效 DCache
tlbi ALLE2
可以进入 EL1,并正常打印。
7.在 EL1 中访问 SP_EL1 会触发同步异常
经过测试,在 EL1 中不能访问 SP_EL1寄存器,否则会触发异常,只能通过 sp 访问。
触发异常的代码如下:
/* 设置 EL1 堆栈指针, 下面两条语句会触发错误中断 */
ldr x0, =Image$$ARM_LIB_STACK$$ZI$$Limit
msr SP_EL1, x0
更改为:
ldr x0, =Image$$ARM_LIB_STACK$$ZI$$Limit
mov sp, x0
8. 在 EL1 的启动代码中, MMU 使能后无法正常打印
经过测试,MMU 不使能的情况下,可以正常打印,开关 cache 都没有任何影响;
但只要 MMU 使能,无法打印,猜测导致此问题的原因如下:
1.MMU 配置有问题,导致地址访问映射失败
2.一些初始化操作没有完成,导致 MMU 存在问题。
关闭 MMU 使能,CPU 打印如下:
[INFO]: CPU Startup Debug Test!!! [INFO]: sizeof(short) = 0x0000000000000002 [INFO]:CurrentEL =0x0000000000000004 [INFO]:SPsel =0x0000000000000001 [INFO]:id_aa64pfr0_el1 =0x0000000001002222 [INFO]:mpidr_el1 =0x0000000080000000 [INFO]:sctlr_el1 =0x0000000034d4d91c [INFO]:DAIF =0x00000000000003c0 [INFO]:id_aa64mmfr1_el1 =0x0000000000000000 [INFO]:id_aa64mmfr0_el1 =0x0000000000001122 [INFO]:id_aa64dfr0_el1 =0x0000000010305106 [INFO]:tcr_el1 =0x0000000000802520 [INFO]:mair_el1 =0x000000000004ff44 [INFO]:ttbr0_el1 =0x0000000041300000 [INFO]:ttbr1_el1 =0x0000000000000000
查看 linux 代码,进行猜测 2 的分析
经过细致的对比 linux 汇编启动代码,发现就算在汇编启动文件中,更改为与 linux 一致的系统寄存器配置,依旧存在这个问题。那么此时排除猜测 2(即,存在一些初始化操作没有完成,导致该问题)。
进行猜测 1 (即,MMU页表配置有问题)的分析
经过打印分析自己编写的页表设置函数发现,2级页表没有真正的设置为我们想要的值,代码有问题,草!!!!
如下为错误代码:
void mmuSetCacheMemory(unsigned long *pulTableAddr, unsigned long ulMemAddr)
{
unsigned int uiIndex;
uiIndex = ulMemAddr >> 21;
pulTableAddr[uiIndex] = (ulMemAddr) + MMU_DDR_CACHEABLE;
}
因为我们使用的 2 级映射,4KB页表粒度,所以每一个2级页表的页表项可以控制2MB的区域范围。而每一个 2 级页表可以控制 1GB 的区域范围,此时 index 的最大值 512。
上述代码如果传入的 ulMemAddr 大于 1GB, 就会导致 index 的值大于 512 ,导致页表设置错误。
更改代码如下:
/********************************************************************************************************
* 函数介绍:设置某个地址为 cacheable memory
* 输入参数:pulTableAddr 二级页表基地址
* ulMemAddr 起始地址
* 输出参数: NONE
* 返回值:
*/
void mmuSetCacheMemory(unsigned long *pulTableAddr, unsigned long ulMemAddr)
{
unsigned int uiIndex = ulMemAddr % 0x40000000;
uiIndex = uiIndex >> 21;
pulTableAddr[uiIndex] = (ulMemAddr) + MMU_DDR_CACHEABLE;
}
上述代码可以正确保证,页表项设置无误。
**解决:**经过修改mmu 属性设置代码,MMU 正常开启,代码正常运行。
9.运用算术右移将低31位全部设置为想要的位
int iCongfig = 0x1
iTmp0 = (unsigned int)(((signed int)iCongfig << (31 - 1)) >> 31);
iTmp0 = 0x00000000;
iTmp1 = (unsigned int)(((signed int)iCongfig << (31)) >> 31);
iTmp0 = 0xffffffff
iTmp0 原理解析如下:
- 首先,
(signed int)0x1 ,强制转换为 signed int 是为了让最后的右移变成算术右移 - 接着,
((signed int)0x1 << (31 - 1)) , 是为了让 bit1 转换为符号位,左移只存在逻辑左移,此时依旧是有符号数。 - 最后,
(((signed int)0x1 << (31 - 1)) >> 31) ,算术右移,让低31位全部变为符号位。如果只想设置bit[30:16], 右移15位即可
10.经过测试,枚举类型以及枚举成员都是整型,4字节
11.学习GICv3
在阅读《ARM? Generic Interrupt Controller Architecture Specification GIC architecture version 3.0 and version 4.0》后,我们需要知道:
- GICv3 不同于GICv2,GICv2 被称为传统 GIC 操作,而 GICv3 通常会使能亲密度路由功能,此时整个 GIC 的架构与 GICv2 存在一些区别,但 GICv2 仍具有很重要的参考性。
- GICv3 Distributor 只负责 SPI 外设中断的使能与失能,中断分组,优先级设置以及路由设置
- GICv3 Interface 提供对中断的应答与解激活,设置中断抢占点,设置最低屏蔽优先级等功能(与 GICv2 一致),但在 GICv3 中,interface 还负责 SGI 软中断的触发与绑核。
- GICv3 新增 Redistributor, 这个结构负责 SGI 与 PPI 中断的使能与失能,中断分组,优先级设置。
关于 GICv3 的其余新增功能,比如 ITS, 我们可以暂不学习.
通过阅读规范,有了以上知识之后,我们可以编写 GICv3 的驱动。
12.只要访问 GIC interface 系统寄存器中组 0 相关的寄存器就会卡死
比如访问ICC_BPR0_EL1 寄存器时,访问ICC_IGRPEN0_EL1 也会卡死。
调试了一下,不清楚,没有解决。
13.gic驱动与中断驱动写好之后,无法触发软中断
- 软中断触发寄存器
ICC_SGI1R_EL1 的设置问题,该寄存器也是 TargetList[15:0], 一个位对应一个核,还以为 TargetList 只需要设置 CPU 编号即可 - DAIF 寄存器设置问题,对应 I 位域置位的话,中断也不会触发。
解决以上问题,软中断可以正常触发。gic 驱动正常。
14.写访问generic timer 的 CNTFRQ_EL0 导致系统重启
查看 ARMv8 A-Profile 的 D7.1.2 章节,只有在已实现的最高等级异常中才能对 CNTFRQ_EL0 寄存器写访问。所以导致重启
所以这个系统计数器的频率应该在 bootloader 中设置?
15. ARM generic Timer 设置
我们使用 EL1 Physical Timer 作为之后的系统心跳定时器。那么我们需要掌握如下知识点(阅读 A-Profile D7 章节):
- physical timer 的计数值来自于 system counter. syatem counter 的值来自于 CNTFRQ_EL0 寄存器设置的频率。
- Physical Timer 是 64 位的向上计数的计数器。
- CNTPCT_EL0 系统寄存器保存着当前的 physical timer 计数值
- CNTP_CTL_EL0 系统寄存器用于设置 physical timer 的使能与中断屏蔽
- CNTP_CVAL_EL0 系统寄存器用于设置中断产生的计数器值,当 CNTPCT_EL0 中的计数器到达该值时,生成中断信号。
- CNTP_TVAL_EL0 系统寄存器保存着比较值与当前计数器值之间的差值。
当我们知道了上述6点,那么可以编写 physical timer 的驱动。
16.generic timer 中 physical timer 的中断向量号
通过阅读 GICv3 规范 2.2 章节,我们可以知道 AArch64 状态下 physical timer 的中断向量号为 30.
而在 AArch32 状态下,PPI 向量号 30 为安全状态的 physical timer, 而我们一般使用 29 向量号的非安全状态下的 physical timer.
综上,AArch64 状态下,我们 physical timer 的中断向量号为30, AArch32 状态下我们使用的 physical timer 向量号为 29.
|