4. 引导加载程序中的串口配置:U-Boot串口初始化流程、设备树中串口节点设置、早期控制台传递参数
在Linux内核启动的早期阶段,串口输出的可用性完全依赖于引导加载程序(Bootloader)的初始化。U-Boot作为最广泛使用的嵌入式引导加载程序,负责在跳转到内核之前完成硬件串口的初始化,并通过设备树(Device Tree)和内核命令行参数将串口配置信息传递给内核。本章将深入剖析U-Boot的串口初始化流程、设备树中串口节点的标准设置方法,以及早期控制台参数的传递机制。
4.1 U-Boot串口初始化流程
U-Boot的串口初始化是一个分阶段的过程,从汇编阶段的早期调试输出到C语言阶段的完整驱动初始化。理解这一流程对于调试启动早期的问题至关重要。
4.1.1 初始化阶段划分
| 阶段 | 代码位置 | 串口状态 | 主要任务 |
|---|---|---|---|
| 汇编早期 | start.S / lowlevel_init.S |
轮询模式,无中断 | 配置引脚复用(Pin Mux)、时钟门控、波特率发生器 |
| 板级初始化 | board_init_f() |
轮询模式,可输出 | 调用串口驱动预初始化,设置控制台设备指针 |
| 重定位后 | board_init_r() |
完整驱动模式 | 初始化完整串口驱动框架,支持环境变量配置 |
| 最终阶段 | serial_init() |
全功能就绪 | 设置波特率、数据位、停止位、流控 |
4.1.2 关键初始化代码路径
U-Boot的串口驱动遵循标准模型。以下是一个典型的NS16550兼容UART初始化序列:
/* drivers/serial/ns16550.c - 简化初始化流程 */
static int ns16550_serial_init(struct uart_port *port)
{
/* 1. 使能UART时钟 */
clk_enable(port->clk);
/* 2. 设置波特率 (例如 115200) */
unsigned int divisor = UART_CLK / (16 * baudrate);
serial_out(port, UART_LCR, UART_LCR_DLAB); /* 使能除数锁存 */
serial_out(port, UART_DLL, divisor & 0xff);
serial_out(port, UART_DLM, (divisor >> 8) & 0xff);
serial_out(port, UART_LCR, UART_LCR_WLEN8); /* 8位数据,无校验 */
/* 3. 配置FIFO */
serial_out(port, UART_FCR, UART_FCR_ENABLE_FIFO |
UART_FCR_RX_TRIG_1 | UART_FCR_TX_TRIG_1);
/* 4. 使能DTR/RTS (如果需要流控) */
serial_out(port, UART_MCR, UART_MCR_DTR | UART_MCR_RTS);
}
lowlevel_init.S中的GPIO复用配置。许多SoC在上电复位后,UART引脚默认处于GPIO模式,必须显式切换为UART功能。
4.1.3 U-Boot串口初始化流程图
下图展示了从U-Boot入口到串口完全就绪的完整初始化流程:
4.2 设备树中串口节点设置
设备树(Device Tree)是描述硬件配置的标准方式。U-Boot和内核都通过解析设备树来获取串口参数。一个正确的串口节点设置是早期控制台正常工作的前提。
4.2.1 标准串口节点结构
以下是一个典型的ARM平台串口设备树节点:
/* arch/arm/boot/dts/xxx.dtsi */
soc {
uart0: serial@ff010000 {
compatible = "arm,pl011", "arm,primecell";
reg = <0x0 0xff010000 0x0 0x1000>;
interrupts = <0 12 4>;
clocks = <&uart_clk>;
clock-names = "uartclk";
pinctrl-0 = <&uart0_pins>;
pinctrl-names = "default";
current-speed = <115200>;
status = "okay";
};
};
4.2.2 关键属性说明
| 属性 | 含义 | 典型值 | 调试要点 |
|---|---|---|---|
compatible |
驱动兼容性字符串 | "arm,pl011" |
必须与内核驱动匹配,否则无法绑定 |
reg |
寄存器基地址和范围 | <0x0 0xff010000 0x0 0x1000> |
地址必须与SoC手册一致,错误会导致挂死 |
interrupts |
中断号、类型、触发方式 | <0 12 4> |
早期轮询模式不需要,但DMA模式必须正确 |
clocks |
时钟句柄 | <&uart_clk> |
时钟频率错误会导致波特率偏差 |
pinctrl-0 |
引脚复用配置 | <&uart0_pins> |
必须与硬件连线一致,否则无信号 |
current-speed |
默认波特率 | <115200> |
U-Boot和内核应保持一致 |
status |
设备启用状态 | "okay" |
必须为"okay","disabled"会被忽略 |
4.2.3 引脚复用配置示例
引脚复用(pinctrl)是串口初始化中最容易出错的部分。以下是一个完整的pinctrl节点示例:
/* 引脚复用配置 */
&pinctrl {
uart0_pins: uart0-pins {
function = "uart0";
groups = "uart0_tx", "uart0_rx";
bias-pull-up;
drive-strength = <4>;
};
};
4.3 早期控制台传递参数
U-Boot通过两种方式将串口配置传递给内核:内核命令行参数(bootargs)和设备树中的chosen节点。理解这些参数的传递机制对于调试早期内核启动至关重要。
4.3.1 内核命令行参数
U-Boot通过bootargs环境变量设置内核命令行参数。早期控制台相关的关键参数包括:
| 参数 | 格式 | 说明 | 示例 |
|---|---|---|---|
console |
console=ttyXXX,baudrate |
指定控制台设备和波特率 | console=ttyAMA0,115200 |
earlycon |
earlycon=uart,mmio32,addr |
启用早期控制台,指定寄存器地址 | earlycon=uart,mmio32,0xff010000 |
keep_bootcon |
无参数 | 保留引导控制台,防止输出丢失 | keep_bootcon |
ignore_loglevel |
无参数 | 忽略日志级别,输出所有内核消息 | ignore_loglevel |
4.3.2 U-Boot环境变量设置
在U-Boot命令行中设置早期控制台参数的典型方法:
# 设置bootargs环境变量
setenv bootargs 'console=ttyAMA0,115200 earlycon=uart,mmio32,0xff010000 ignore_loglevel'
# 保存到非易失存储
saveenv
# 查看当前设置
printenv bootargs
4.3.3 设备树chosen节点
除了命令行参数,U-Boot还会在设备树的chosen节点中设置stdout-path属性,指示内核使用哪个串口作为控制台:
/* U-Boot自动生成的chosen节点 */
chosen {
stdout-path = &uart0;
bootargs = "console=ttyAMA0,115200 earlycon";
};
4.3.4 参数传递流程图
下图展示了从U-Boot环境变量到内核控制台初始化的完整参数传递路径:
4.4 常见问题与调试方法
案例1:U-Boot有输出,内核无输出
- 原因: 内核命令行参数
console指定的设备名与驱动不匹配。 - 解决方案: 检查内核串口驱动名称(如
ttyAMA0、ttyS0、ttyMT0),确保与设备树compatible属性对应。 - 验证命令:
cat /proc/consoles查看内核注册的控制台列表。
案例2:波特率不匹配导致乱码
- 原因: U-Boot设置的波特率与内核命令行参数不一致。
- 解决方案: 在U-Boot中执行
baudrate=115200,并确保bootargs中的波特率相同。 - 验证方法: 使用逻辑分析仪捕获TX引脚信号,测量实际波特率。
bootargs中同时添加earlycon和keep_bootcon参数。earlycon确保内核在初始化完整控制台驱动之前就能输出日志,而keep_bootcon防止在切换控制台时出现输出空白期。这两个参数配合使用,可以捕获从U-Boot跳转到内核后最早的启动信息。
4.5 本章小结
引导加载程序中的串口配置是Linux内核早期调试的基础。U-Boot通过分阶段的初始化流程确保串口在启动早期即可使用,设备树提供了标准化的硬件描述方式,而内核命令行参数则实现了从U-Boot到内核的配置传递。掌握这三者的协同工作机制,能够帮助开发者快速定位启动早期的串口输出问题,为后续的内核调试奠定基础。