第一章 硬件初始化(汇编阶段)
各位同学,欢迎来到Bootloader底层开发的第一站。说实话,很多初学者一上来就盯着C代码看,觉得汇编又老又难。但我得说一句——硬件初始化这块,汇编是绕不过去的坎。你想想看,C语言跑起来之前,CPU连个栈都没有,拿什么调用函数?
这一章,我们就来搞定四件事:关看门狗、配时钟、初始化SDRAM、设置栈指针。这四步走完,你的CPU才算真正"活"过来。
1.1 为什么要先关看门狗?
看门狗(Watchdog)是什么?说白了就是一个定时器。它不停地在数数,数到溢出就复位CPU。正常工作时,你得定期"喂狗"——也就是清零计数器。但Bootloader刚启动时,谁去喂它?没人喂。
所以,第一步必须是关掉看门狗。不然你还在初始化时钟呢,啪,复位了。我在项目中遇到过这种情况,查了半天以为是代码问题,结果发现是看门狗没关。嗯,这种低级错误犯一次就够了。
关看门狗的代码很简单,往控制寄存器写个0就行。以ARM为例:
@ 关闭看门狗
ldr r0, =WTCON @ 看门狗控制寄存器地址
mov r1, #0 @ 写入0,关闭所有功能
str r1, [r0]
这里有个小细节——不同芯片的寄存器地址不一样。WTCON这个宏定义,你得去芯片手册里查。我习惯把这类地址定义放在一个单独的汇编头文件里,方便维护。
1.2 初始化时钟系统
时钟系统,是CPU的心脏。你给它多快的频率,它就干多快的活。但刚上电时,芯片通常跑在一个很慢的"安全频率"上,比如12MHz的外部晶振。我们要做的,就是通过配置PLL(锁相环),把频率提上去。
为什么要提频?你想想看,如果一直跑12MHz,初始化SDRAM那得等到猴年马月去。我见过一个项目,忘了配时钟,结果Bootloader加载内核花了十几秒——用户还以为是死机了。
时钟初始化的步骤,大致是这样的:
- 设置PLL的倍频系数——决定最终频率
- 设置分频器——给不同外设分配时钟
- 等待PLL锁定——频率稳定需要时间
- 切换系统时钟源——从晶振切到PLL输出
以S3C2440为例,它的MPLL(主PLL)配置代码:
@ 设置MPLL
ldr r0, =MPLLCON
ldr r1, =((MDIV << 12) | (PDIV << 4) | SDIV)
str r1, [r0]
@ 等待PLL锁定
ldr r0, =LOCKTIME
ldr r1, =0xFFFFFF
str r1, [r0]
这里MDIV、PDIV、SDIV的值怎么算?芯片手册里有个公式:
MPLL(FCLK) = (2 × m × Fin) / (p × 2^s)
其中 m = MDIV + 8, p = PDIV + 2, s = SDIV。我建议你把这个公式背下来,或者直接写在代码注释里。每次配新板子都要翻手册,太累了。
1.3 初始化SDRAM控制器
SDRAM,就是你的内存。Bootloader要加载内核、运行程序,都得靠它。但SDRAM不是插上就能用的——你得告诉控制器:你的行地址多少位?列地址多少位?刷新周期多长?
这部分配置,最容易出问题。我曾经在一个项目里,SDRAM初始化代码写了三遍才跑通。原因很简单——时序参数配错了。SDRAM对时序要求很严格,差一个时钟周期都可能读写出错。
配置SDRAM控制器,主要设置以下几项:
| 参数 | 说明 | 常见值 |
|---|---|---|
| 行地址数 | 行地址的位数 | 12或13 |
| 列地址数 | 列地址的位数 | 8或9 |
| 突发长度 | 一次连续读写的数据量 | 4或8 |
| CAS延迟 | 从发出读命令到数据有效的时钟数 | 2或3 |
| 刷新周期 | 多久刷新一次,防止数据丢失 | 7.8μs |
以S3C2440为例,它的SDRAM控制器配置代码:
@ 设置SDRAM控制器
ldr r0, =BWSCON @ 总线宽度和等待控制寄存器
ldr r1, =0x22000000 @ 配置bank6为32位数据宽度
str r1, [r0]
ldr r0, =BANKCON6 @ bank6控制寄存器
ldr r1, =0x00018005 @ 设置时序参数
str r1, [r0]
ldr r0, =REFRESH @ 刷新控制寄存器
ldr r1, =0x008C0000 @ 设置刷新周期
str r1, [r0]
这里有个关键点——配置完SDRAM控制器后,要等一段时间,让SDRAM完成初始化。通常需要等待200μs左右。我习惯在配置完后加一个延时循环:
@ 等待SDRAM稳定
mov r2, #0x1000
delay_loop:
subs r2, r2, #1
bne delay_loop
1.4 设置栈指针
栈指针(SP),是C语言运行的基础。没有栈,你连函数都调用不了。为什么?因为函数调用时,返回地址、局部变量都要压栈。SP指向哪里,栈就在哪里。
设置栈指针很简单,就是把SP指向一块内存的末尾。注意,栈是向下生长的,所以SP要指向内存的高地址。
@ 设置栈指针
ldr sp, =0x34000000 @ SDRAM的末尾地址
这里0x34000000怎么来的?假设你的SDRAM起始地址是0x30000000,大小是64MB(0x04000000),那末尾就是0x34000000。我习惯留一点余量,不要把栈顶刚好放在内存边界上。
另外,ARM处理器有多个模式,每个模式都有自己的SP。比如:
- SVC模式(管理模式)——Bootloader主要跑在这个模式
- IRQ模式(中断模式)——中断服务程序用的
- FIQ模式(快速中断模式)——快速中断用的
- ABT模式(中止模式)——处理异常
- UND模式(未定义模式)——处理未定义指令
我建议至少把SVC模式和IRQ模式的SP都设好。因为Bootloader里可能会开中断,如果IRQ的SP没设,一进中断就崩了。
@ 设置各模式栈指针
mrs r0, cpsr
bic r0, r0, #0x1F @ 清除模式位
orr r1, r0, #0x12 @ IRQ模式
msr cpsr, r1
ldr sp, =0x33F00000 @ IRQ栈
orr r1, r0, #0x13 @ SVC模式
msr cpsr, r1
ldr sp, =0x34000000 @ SVC栈
1.5 完整初始化流程
好了,四步走完,我们把它们串起来。一个完整的汇编初始化代码,大概长这样:
.section .text.startup
.global _start
_start:
@ 1. 关闭看门狗
ldr r0, =WTCON
mov r1, #0
str r1, [r0]
@ 2. 初始化时钟
ldr r0, =MPLLCON
ldr r1, =((MDIV << 12) | (PDIV << 4) | SDIV)
str r1, [r0]
@ 3. 初始化SDRAM
ldr r0, =BWSCON
ldr r1, =0x22000000
str r1, [r0]
@ ... 其他SDRAM配置
@ 4. 设置栈指针
ldr sp, =0x34000000
@ 5. 跳转到C语言入口
bl main
这个顺序不能乱。你想想看,如果先设栈再配时钟——栈在SDRAM里,SDRAM还没初始化,你往哪压栈?所以,硬件依赖关系决定了初始化顺序。
最后说一句,汇编阶段的硬件初始化,是Bootloader里最"硬核"的部分。搞懂了它,后面的C代码就是水到渠成的事。下一章,我们会进入C语言阶段,开始做更复杂的事情——比如内存布局、设备驱动初始化。到时候你会发现,有了这一章的基础,一切都顺理成章。
嗯,今天就到这里。各位回去可以拿自己的开发板试试,把这几步走一遍。遇到问题别慌,先检查寄存器地址对不对,再检查时序参数。调试多了,你自然就熟了。