第三章 硬件初始化要点:时钟、DDR、串口与GPIO
硬件初始化,说白了就是给芯片“通电醒脑”的过程。我做了这么多年Bootloader,见过太多系统死在启动早期,十有八九都是这四块没伺候好——时钟、DDR、串口、GPIO。今天咱们就挨个捋一遍。
核心观点:硬件初始化的顺序不能乱。时钟是心脏,DDR是大脑内存,串口是嘴巴(输出调试信息),GPIO是手脚。心脏不跳,后面全白搭。
3.1 时钟系统配置——芯片的心脏起搏器
时钟配置是Bootloader里最先要干的事。芯片上电后默认跑的是内部低速RC振荡器,频率低得可怜,连DDR都初始化不了。你得先把它切换到外部晶振,再倍频到目标频率。
我个人习惯把时钟配置分成三步走:
- 选源——确定用外部晶振还是内部振荡器
- 锁相环(PLL)配置——把低频倍到高频
- 分频与使能——给各个外设分配时钟
举个例子,某款ARM Cortex-A7芯片的时钟树大概长这样:
/* 伪代码:时钟初始化流程 */
void clock_init(void) {
// 1. 等待外部晶振稳定
while(!(OSC_STATUS & OSC_READY));
// 2. 配置PLL:24MHz * 25 = 600MHz
PLL_CFG = (25 << 16) | (1 << 0); // N=25, 使能PLL
while(!(PLL_STATUS & PLL_LOCKED));
// 3. 切换系统时钟到PLL输出
CLK_SEL = CLK_SRC_PLL;
// 4. 配置外设时钟分频
PERIPH_CLK_DIV = 4; // 600MHz / 4 = 150MHz
DDR_CLK_DIV = 3; // 600MHz / 3 = 200MHz
}
我曾经踩过的坑:有一次芯片死活锁不住PLL,查了两天才发现是外部晶振的负载电容焊错了。晶振的匹配电容不是随便选的,一定要看数据手册。还有,PLL锁定需要时间,别一使能就去读状态,加个延时更稳妥。
3.2 DDR初始化——内存的“唤醒仪式”
DDR初始化是Bootloader里最复杂、最容易出问题的环节。你想想看,DDR控制器和DDR颗粒之间有一套严格的时序协议,一个参数不对,系统就死给你看。
DDR初始化的核心步骤:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 配置DDR控制器 | 设置地址映射、突发长度、CAS延迟等 |
| 2 | 上电与复位 | DDR颗粒需要特定的上电序列 |
| 3 | 初始化序列 | 发送NOP、预充电、刷新等命令 |
| 4 | ZQ校准 | DDR3/DDR4必须做阻抗校准 |
| 5 | 写训练 | 调整DQS与数据的相位关系 |
| 6 | 读训练 | 调整读数据的采样窗口 |
这里我重点说一下训练(Training)。DDR颗粒和控制器之间的信号延迟会因为PCB走线长度、温度、电压而变化。训练的目的就是动态调整这些参数,找到最佳采样点。
/* 伪代码:DDR写训练简化流程 */
void ddr_write_training(void) {
uint32_t delay;
for(delay = 0; delay < MAX_DELAY; delay++) {
// 设置DQS延迟
DDR_DQS_DELAY = delay;
// 写入测试数据
*DDR_TEST_ADDR = 0xA5A5A5A5;
// 读回验证
if(*DDR_TEST_ADDR == 0xA5A5A5A5) {
// 记录通过点
save_pass_point(delay);
}
}
// 取通过点的中间值作为最终配置
DDR_DQS_DELAY = get_median_pass_point();
}
我的经验:DDR训练失败时,别急着改代码。先检查硬件——用示波器看时钟、DQS、数据线的信号质量。很多时候是PCB走线阻抗不匹配导致的。另外,DDR初始化代码建议放在IRAM里跑,别放在DDR里——你还没初始化完呢,代码就没了,这不搞笑吗?
3.3 串口初始化——Bootloader的“嘴巴”
串口是调试Bootloader最重要的工具。没有串口输出,你就像在黑盒子里修电路,全靠猜。我习惯在时钟配置完后立刻初始化串口,这样后面每一步都能打印信息。
串口初始化的要点:
- 波特率计算——根据系统时钟和分频系数算,别算错了
- 数据格式——8N1是标配,但也有用7E1的
- FIFO配置——建议使能FIFO,减少CPU干预
- 流控——调试阶段一般不使能,省事
/* 伪代码:串口初始化 */
void uart_init(uint32_t baudrate) {
uint32_t divisor;
// 1. 使能UART时钟
CLK_ENABLE(UART_CLK);
// 2. 计算波特率分频系数
divisor = UART_CLK_FREQ / (16 * baudrate);
// 3. 配置数据格式:8位数据,1位停止,无校验
UART_LCR = 0x03; // 8N1
// 4. 设置波特率
UART_DLL = divisor & 0xFF;
UART_DLM = (divisor >> 8) & 0xFF;
// 5. 使能FIFO
UART_FCR = 0x01;
// 6. 使能发送和接收
UART_IER = 0x03;
}
注意:串口输出乱码,90%是波特率算错了。我遇到过一种情况——系统时钟在PLL锁定前后频率变了,但串口分频系数没重新算,结果波特率对不上。所以,如果你在时钟配置前后都用了串口,记得重新初始化一次。
3.4 GPIO与引脚复用——别让引脚“打架”
现在的芯片引脚功能多,一个引脚能当GPIO、UART、I2C、SPI用。你得告诉芯片,这个引脚现在要干什么。这就是引脚复用(Pin Mux)的活。
GPIO配置的常见步骤:
- 设置复用功能——从寄存器里选功能编号
- 配置电气特性——上下拉、驱动能力、施密特触发
- 设置方向——输入还是输出
- 写数据——输出高还是低
/* 伪代码:GPIO配置为UART功能 */
void gpio_uart_config(void) {
// 1. 引脚复用:GPIO1_IO3 复用为 UART1_TXD
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO3 = 0x02; // ALT2 = UART1_TXD
// 2. 电气配置:上拉,驱动能力中等
IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO3 = (0x1 << 3) | // 100k上拉
(0x2 << 0); // 驱动能力R0/2
// 3. 同样的方式配置RXD引脚
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO2 = 0x02; // ALT2 = UART1_RXD
IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO2 = (0x1 << 3) | (0x2 << 0);
}
我常用的调试技巧:如果某个外设不工作,先检查引脚复用配置对不对。用万用表量引脚电平,看是不是被其他功能占用了。我曾经遇到过UART和I2C共用一个引脚,结果两个都跑不起来——引脚在“打架”。
3.5 知识结构总览
下面这张图把四个初始化模块的关系画清楚了。记住,时钟是基础,DDR是核心,串口是调试手段,GPIO是外设接口。顺序不能乱。
3.6 初始化顺序的“潜规则”
你可能觉得,按1234的顺序走就行了。但实际项目中,有些细节要注意:
- 时钟配置后要等稳定——PLL锁定、晶振起振都需要时间,别急着往下走
- DDR初始化前别碰DDR空间——代码、堆栈、全局变量都不能放在DDR里
- 串口初始化后立刻打印——哪怕只打印一个字符,也能确认串口通了
- GPIO配置要避免瞬态——先配复用功能,再配电气特性,最后设方向和数据
总结一下:硬件初始化是Bootloader的“第一公里”。这四步走稳了,后面的代码搬运、系统跳转才有基础。我见过太多人一上来就调应用层代码,结果发现是DDR没初始化好——白忙活。记住,从时钟开始,一步一个脚印。