4、核心参数详解(上):启动源选择、时钟频率配置、调试串口配置
各位好,欢迎来到第四章。这一章我们开始啃硬骨头——核心参数配置。
说实话,BootROM 的参数配置,是整个系统启动的基石。你想想看,芯片上电后第一件事就是读这些参数。要是配错了,板子可能连灯都不闪一下。我这些年调试过的板子,少说也有几十款,至少有一半的“点不亮”问题,最后都追到了这几个参数头上。
今天我们先聊三个最关键的:启动源选择、时钟频率配置、调试串口配置。它们决定了你的芯片从哪里启动、跑多快、以及怎么跟你“说话”。
4.1 启动源选择(Boot Source)
启动源,说白了就是告诉 BootROM:“嘿,你去哪儿把程序给我搬进来?”
常见的启动源有这么几种:
- NOR Flash —— 支持 XIP(片上执行),启动最快,但容量小、贵
- NAND Flash —— 容量大、便宜,但需要搬移到 RAM 里跑
- eMMC / SD 卡 —— 消费电子标配,协议复杂一点
- SPI NOR / SPI NAND —— 引脚少,适合小封装芯片
- USB / UART —— 工厂烧录或调试用,平时不选这个
我个人习惯,在设计阶段就把启动源的优先级想清楚。比如产品量产用 eMMC,但开发阶段可能先用 SPI NOR 调试。这时候 BootROM 的启动源选择逻辑就很重要了。
核心要点:启动源选择通常由硬件 Pin 脚或 OTP(一次性可编程)区域决定。BootROM 会按照预设的优先级依次尝试,直到某个启动源返回有效的启动代码。
举个例子,某款 SoC 的启动源配置寄存器长这样:
// 伪代码示例:启动源选择寄存器
#define BOOT_SOURCE_REG 0x1E000004
#define BOOT_SOURCE_NOR 0x00
#define BOOT_SOURCE_NAND 0x01
#define BOOT_SOURCE_EMMC 0x02
#define BOOT_SOURCE_SPI 0x03
#define BOOT_SOURCE_UART 0x04
// 读取当前启动源
uint32_t boot_src = readl(BOOT_SOURCE_REG) & 0x07;
switch (boot_src) {
case BOOT_SOURCE_NOR:
// 直接从 NOR Flash 执行
break;
case BOOT_SOURCE_NAND:
// 加载 NAND 启动块到 SRAM
break;
// ... 其他情况
}
避坑指南:我曾经遇到过一块板子,明明焊了 eMMC,但死活起不来。查了两天才发现,是启动源选择 Pin 脚的上拉电阻虚焊了,导致 BootROM 一直去读 SPI Flash。嗯,硬件上的小问题,软件排查起来真要命。
4.2 时钟频率配置(Clock Divider)
时钟频率配置,说白了就是告诉芯片:“你跑多快?”
BootROM 阶段的时钟配置,通常只涉及几个关键时钟:
- CPU 时钟 —— 核心频率,BootROM 阶段一般跑低频,省电且稳定
- 总线时钟(AHB / APB) —— 外设通信的基础
- 存储接口时钟 —— 比如 SPI 时钟、NAND 时钟、DDR 时钟
- 调试串口时钟 —— 决定了串口波特率是否准确
你想想看,如果时钟配错了,串口打印出来的全是乱码,你怎么调试?我刚开始做 UEFI 时就被这个问题坑过——串口能收到数据,但全是“ÿÿÿÿ”这种乱码,折腾了半天才发现是时钟分频算错了。
我的经验:BootROM 阶段的时钟,建议先跑一个保守的低频值。等主程序加载起来后,再通过 PLL 动态调频。这样既保证了启动可靠性,又不影响后续性能。
来看一个实际的时钟分频配置代码:
// 时钟分频配置示例
#define CLK_DIV_CTRL 0x1E001000
#define CLK_DIV_CPU 0x00
#define CLK_DIV_AHB 0x04
#define CLK_DIV_APB 0x08
void clock_init(void) {
uint32_t reg_val;
// 设置 CPU 时钟分频:输入 24MHz,分频后 12MHz
reg_val = readl(CLK_DIV_CTRL + CLK_DIV_CPU);
reg_val &= ~0xFF;
reg_val |= 0x01; // 2 分频
writel(reg_val, CLK_DIV_CTRL + CLK_DIV_CPU);
// 设置 AHB 时钟分频:12MHz / 2 = 6MHz
reg_val = readl(CLK_DIV_CTRL + CLK_DIV_AHB);
reg_val &= ~0xFF;
reg_val |= 0x01; // 2 分频
writel(reg_val, CLK_DIV_CTRL + CLK_DIV_AHB);
// 设置 APB 时钟分频:6MHz / 2 = 3MHz
reg_val = readl(CLK_DIV_CTRL + CLK_DIV_APB);
reg_val &= ~0xFF;
reg_val |= 0x01; // 2 分频
writel(reg_val, CLK_DIV_CTRL + CLK_DIV_APB);
}
警告:时钟配置时,一定要确认各模块的时钟依赖关系。比如 DDR 控制器初始化前,必须先配好 DDR 的参考时钟。我曾经见过有人先初始化 DDR 再配时钟,结果 DDR 训练直接失败——顺序搞反了。
4.3 调试串口配置(Debug UART)
调试串口,是嵌入式工程师的“眼睛”。没有它,你就像在黑屋子里修手表——全靠摸。
BootROM 阶段的串口配置,通常包括:
- 波特率设置 —— 常见 115200 或 921600
- 数据位 / 停止位 / 校验位 —— 8N1 是标配
- 引脚复用(Pin Mux) —— 确认 UART 引脚没有被其他外设占用
- 时钟源选择 —— 串口时钟来自哪个 PLL 或晶振
我个人习惯,在 BootROM 里把串口初始化做成一个独立函数。这样不管后续加载哪个阶段的代码,都能直接调用,保证调试信息不断。
来看一个典型的串口初始化代码:
// 调试串口初始化
#define UART_BASE 0x1C000000
#define UART_BAUD_REG (UART_BASE + 0x00)
#define UART_CTRL_REG (UART_BASE + 0x04)
#define UART_DATA_REG (UART_BASE + 0x08)
void debug_uart_init(uint32_t baudrate) {
uint32_t div;
// 假设串口时钟为 24MHz
div = 24000000 / (16 * baudrate);
// 设置波特率
writel(div, UART_BAUD_REG);
// 配置 8N1:8 数据位,无校验,1 停止位
writel(0x03, UART_CTRL_REG); // 使能发送和接收
// 输出一条测试信息
debug_uart_putc('U');
debug_uart_putc('A');
debug_uart_putc('R');
debug_uart_putc('T');
debug_uart_putc(' ');
debug_uart_putc('O');
debug_uart_putc('K');
debug_uart_putc('\n');
}
void debug_uart_putc(char c) {
// 等待发送缓冲区空
while (!(readl(UART_CTRL_REG) & 0x20));
// 发送字符
writel(c, UART_DATA_REG);
}
关键点:串口初始化成功后,一定要打印一条可见的标识信息。我习惯打印“UART OK”或者“Debug UART Ready”。这样一眼就能确认串口是否工作正常,不用去猜。
避坑指南:我曾经遇到过一个问题——串口能打印,但打印几行后就卡死了。查了半天,发现是串口中断没关,BootROM 阶段中断向量表还没初始化,一有中断进来就飞了。所以,BootROM 阶段的串口,建议用轮询模式,别开中断。
知识体系总览
这三个参数之间的关系,我用一张图来总结:
这张图展示了 BootROM 启动时的核心流程。先选启动源,再配时钟,最后开串口。顺序不能乱,依赖关系要理清。
好了,这一章就到这里。这三个参数是 BootROM 配置的基石,理解了它们,后续的 DDR 初始化、安全启动等内容就好办了。记住我上面说的那些坑,能帮你省下不少调试时间。