第一章 硬件初始化(汇编阶段)

各位同学,欢迎来到Bootloader底层开发的第一站。说实话,很多初学者一上来就盯着C代码看,觉得汇编又老又难。但我得说一句——硬件初始化这块,汇编是绕不过去的坎。你想想看,C语言跑起来之前,CPU连个栈都没有,拿什么调用函数?

这一章,我们就来搞定四件事:关看门狗、配时钟、初始化SDRAM、设置栈指针。这四步走完,你的CPU才算真正"活"过来。

1.1 为什么要先关看门狗?

看门狗(Watchdog)是什么?说白了就是一个定时器。它不停地在数数,数到溢出就复位CPU。正常工作时,你得定期"喂狗"——也就是清零计数器。但Bootloader刚启动时,谁去喂它?没人喂。

所以,第一步必须是关掉看门狗。不然你还在初始化时钟呢,啪,复位了。我在项目中遇到过这种情况,查了半天以为是代码问题,结果发现是看门狗没关。嗯,这种低级错误犯一次就够了。

注意:有些芯片的看门狗默认就是开启的,比如S3C2440。上电后如果不关,大约几秒钟就会触发复位。所以关狗动作要放在最前面。

关看门狗的代码很简单,往控制寄存器写个0就行。以ARM为例:

@ 关闭看门狗
ldr r0, =WTCON       @ 看门狗控制寄存器地址
mov r1, #0           @ 写入0,关闭所有功能
str r1, [r0]

这里有个小细节——不同芯片的寄存器地址不一样。WTCON这个宏定义,你得去芯片手册里查。我习惯把这类地址定义放在一个单独的汇编头文件里,方便维护。

1.2 初始化时钟系统

时钟系统,是CPU的心脏。你给它多快的频率,它就干多快的活。但刚上电时,芯片通常跑在一个很慢的"安全频率"上,比如12MHz的外部晶振。我们要做的,就是通过配置PLL(锁相环),把频率提上去。

为什么要提频?你想想看,如果一直跑12MHz,初始化SDRAM那得等到猴年马月去。我见过一个项目,忘了配时钟,结果Bootloader加载内核花了十几秒——用户还以为是死机了。

时钟初始化的步骤,大致是这样的:

  1. 设置PLL的倍频系数——决定最终频率
  2. 设置分频器——给不同外设分配时钟
  3. 等待PLL锁定——频率稳定需要时间
  4. 切换系统时钟源——从晶振切到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。我建议你把这个公式背下来,或者直接写在代码注释里。每次配新板子都要翻手册,太累了。

我的习惯:先把目标频率算好,再反推寄存器值。比如我要跑400MHz,Fin=12MHz,那就试几组m、p、s的组合,选一个最接近的。别纠结于精确值,差个几MHz不影响。

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
避坑指南:我曾经遇到过一个问题——SDRAM初始化后,读写前几个字节总是出错。后来发现是SDRAM的"预充电"没做。在配置完控制器后,先对SDRAM执行一次预充电命令,再正常读写。这个坑我踩过,你们别踩了。

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栈
小技巧:栈的大小怎么定?我一般给SVC模式分配8KB,IRQ模式分配4KB。Bootloader不像操作系统那样需要大栈,够用就行。但别太小,万一函数调用嵌套深了,栈溢出就麻烦了。

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语言阶段,开始做更复杂的事情——比如内存布局、设备驱动初始化。到时候你会发现,有了这一章的基础,一切都顺理成章。

嗯,今天就到这里。各位回去可以拿自己的开发板试试,把这几步走一遍。遇到问题别慌,先检查寄存器地址对不对,再检查时序参数。调试多了,你自然就熟了。