第三章 硬件初始化基础:ARM架构基础(Cortex-A系列)、MTK芯片内部总线架构、时钟与复位系统
各位同学,咱们今天聊点硬核的。做MTK平台启动流程,绕不开硬件初始化。说白了,你得先搞清楚芯片是怎么“活过来”的。我个人习惯把这一章叫做“芯片的觉醒时刻”——从你按下电源键,到CPU开始跑第一条指令,中间发生了什么?嗯,这就是我们要啃的硬骨头。
3.1 ARM Cortex-A系列架构基础
MTK的芯片,尤其是手机SoC,清一色用ARM Cortex-A系列核心。从早期的Cortex-A7、A53,到现在的A78、X1,架构在变,但底子没变。
3.1.1 处理器模式与寄存器
Cortex-A系列有9种工作模式。你想想看,一个CPU要处理中断、处理异常、跑用户程序,总得有个“身份切换”吧?
- User模式:跑App用的,权限最低
- FIQ模式:快中断模式,优先级最高
- IRQ模式:普通中断模式
- Supervisor模式:Bootloader和OS内核跑的地方
- Abort模式:数据访问异常
- Undefined模式:遇到不认识指令时
- System模式:特权级,但用User的寄存器组
- Monitor模式:安全扩展用的
- Hyp模式:虚拟化用的
重点来了:Bootloader启动时,CPU默认在Supervisor模式。我建议你写启动代码时,第一件事就是确认当前模式。我在项目中遇到过,有人把中断向量表配错了,结果IRQ模式下的SP指针没设,一进中断就崩了。
寄存器方面,Cortex-A有16个通用寄存器(R0-R15),其中R15是PC指针,R14是LR链接寄存器。嗯,这里要注意:不同模式下,R13(SP)和R14(LR)是独立的。你切换模式时,硬件自动帮你保存返回地址,但SP你得自己设。
3.1.2 异常向量表
芯片上电后,CPU会从0x00000000或0xFFFF0000取第一条指令。这个地址就是异常向量表的基址。Bootloader要干的第一件事,就是把这张表配好。
@ 典型的异常向量表(ARM汇编)
_start:
b reset @ 0x00: Reset
b undef_handler @ 0x04: Undefined Instruction
b swi_handler @ 0x08: Software Interrupt
b prefetch_abort @ 0x0C: Prefetch Abort
b data_abort @ 0x10: Data Abort
nop @ 0x14: Reserved
b irq_handler @ 0x18: IRQ
b fiq_handler @ 0x1C: FIQ
我的经验:曾经有个项目,Bootloader在DDR初始化之前就开了中断。结果中断一来,CPU跳去查向量表,但DDR还没好,代码都取不到。死锁了。所以,中断一定要在DDR和MMU都配好之后再开。
3.2 MTK芯片内部总线架构
MTK的SoC内部,不是所有模块都直接挂在CPU上的。它有一套自己的总线拓扑。说白了,就是芯片内部的“高速公路网”。
3.2.1 总线层次结构
以MT6765(Helio P35)为例,内部总线大致分三层:
| 总线层级 | 名称 | 挂载模块 | 频率 |
|---|---|---|---|
| L3总线 | CCI-550 | CPU集群、GPU、DSP | ~800MHz |
| L2总线 | AXI互联 | 显示、ISP、视频编解码 | ~400MHz |
| L1总线 | APB/AHB | UART、I2C、GPIO、定时器 | ~100MHz |
你想想看,CPU要读一个GPIO的状态,如果走L3总线绕一圈,那延迟得多大?所以低速外设挂在APB上,CPU通过桥接器访问,效率高很多。
避坑指南:我曾经在调试一个I2C通信问题时,发现数据总是丢包。查了半天,原来是I2C控制器挂在APB总线上,而APB时钟没配好,导致总线频率低于I2C的SCL时钟。嗯,总线时钟必须大于等于外设时钟的2倍,这是铁律。
3.2.2 总线矩阵与DMA
MTK的总线矩阵支持多主多从。CPU、GPU、DSP都可以当master,内存、外设是slave。但有个关键点:Bootloader阶段,总线矩阵的默认配置是“安全模式”。什么意思?就是只有CPU能访问内存,其他master都被挡住了。
我记得在某个项目里,Bootloader要初始化显示,但显示控制器(DISPLAY)去读帧缓冲区时,总线矩阵没给它开权限。结果屏幕一直黑着。后来在启动代码里加了这么一段:
// 打开显示控制器的总线访问权限
#define EMI_CONN_BASE 0x10203000
#define EMI_CONN_DISPLAY_MASK (1 << 5)
void bus_matrix_init(void) {
uint32_t reg_val;
reg_val = readl(EMI_CONN_BASE + 0x10);
reg_val |= EMI_CONN_DISPLAY_MASK;
writel(reg_val, EMI_CONN_BASE + 0x10);
}
3.3 时钟与复位系统
时钟是芯片的“心跳”。没有时钟,CPU就是一块死硅。MTK的时钟系统,我把它分成三级:
- 源时钟:26MHz晶振(TCXO),这是所有时钟的根
- PLL锁相环:把26MHz倍频到GHz级别,比如ARMPLL、MEMPLL、SYSPLL
- 分频器:从PLL输出分频,给各个模块用
3.3.1 时钟树初始化顺序
Bootloader里配时钟,顺序不能乱。我总结了一个口诀:先开晶振,再锁PLL,最后分频。
- 等待26MHz晶振稳定(一般要等2ms)
- 配置PLL的倍频系数和分频系数
- 等待PLL Lock信号(硬件置位)
- 将系统主时钟切换到PLL输出
- 配置各模块的分频器
关键参数:MTK的ARMPLL,典型配置是26MHz * 52 = 1.352GHz。但不同芯片的倍频范围不同,你得看数据手册。我建议你写代码时,把PLL参数做成宏定义,方便调试时改。
3.3.2 复位系统
MTK的复位分三级:
| 复位级别 | 触发源 | 影响范围 |
|---|---|---|
| POR(上电复位) | 电源上电 | 整个芯片 |
| HR(硬件复位) | 复位按键、看门狗 | CPU+总线+外设 |
| SR(软件复位) | WDT超时、软件触发 | CPU核心 |
嗯,这里要注意:POR之后,BootROM会先跑一段固化代码,然后才跳到你写的Bootloader。这段固化代码会做最基本的时钟和复位初始化。所以你在Bootloader里看到的时钟配置,其实是“二次配置”。
我的习惯:调试Bootloader时,我会在复位向量处加一个GPIO翻转。这样用示波器一看,就知道芯片有没有正常复位、BootROM跑了多久。比看log快多了。
3.3.3 看门狗与复位源检测
Bootloader里一定要处理看门狗。MTK的WDT默认是开的,超时时间大约10秒。如果你Bootloader初始化时间太长,WDT就会复位芯片,然后陷入死循环。
// 关闭看门狗(在Bootloader最前面执行)
#define WDT_BASE 0x10007000
#define WDT_MODE (WDT_BASE + 0x00)
void disable_watchdog(void) {
writel(0x0000, WDT_MODE); // 写0关闭WDT
// 读回确认
if (readl(WDT_MODE) != 0) {
// 有些芯片需要写两次
writel(0x0000, WDT_MODE);
}
}
另外,复位后要读一下复位状态寄存器,看看这次复位是上电、看门狗还是软件触发的。这在调试时特别有用。我曾经遇到一个bug,芯片总是莫名其妙复位,后来发现是某个外设的电源没稳,导致PMIC反复触发POR。
小结
这一章的内容,说白了就是回答三个问题:
- CPU怎么知道第一条指令在哪?——异常向量表
- CPU怎么访问外设?——总线架构
- CPU怎么跑起来?——时钟和复位
搞懂了这些,你再看MTK的Bootloader源码,就不会觉得那是一堆天书了。下一章,咱们正式进入BootROM,看看芯片出厂时固化的那几KB代码到底干了什么。