第2章 启动日志基础:串口输出原理、波特率设置、日志级别与打印函数

做 U-Boot 调试,第一件事就是看串口输出。没有串口日志,你就像蒙着眼睛修车——全靠摸。这一章我们就把串口输出这件事彻底讲透。

2.1 串口输出原理:数据是怎么从芯片跑到你屏幕上的?

说白了,U-Boot 的打印信息就是通过 UART(通用异步收发器)逐字节发出来的。CPU 把要打印的字符写入 UART 的发送寄存器,UART 硬件按约定的速率把数据一位一位地推出去,经过电平转换芯片(比如 MAX3232)变成 RS-232 电平,最后被你的串口工具接收。

整个过程分三步:

  1. U-Boot 调用 printf() → 把格式化字符串转成字节流
  2. 字节流写入 UART 发送 FIFO → 硬件自动加起始位、校验位、停止位
  3. 串口工具接收并显示 → 按相同波特率解析成字符

关键点:发送端和接收端必须使用完全相同的波特率、数据位、停止位、校验位配置。任何一个参数不匹配,你看到的就会是乱码或者干脆没输出。

我在项目中遇到过好几次这种情况:板子明明在跑,串口却一片空白。查了半天,发现是串口工具的波特率设错了。嗯,这种低级错误,犯过一次就再也不会忘了。

2.2 波特率设置:为什么是 115200 而不是 114514?

波特率就是每秒传输的符号数。U-Boot 里最常见的波特率是 115200,也有用 9600、38400、57600 的。为什么是这些数字?因为它们能从常见晶振频率(比如 11.0592MHz、16MHz、25MHz)通过整数分频精确得到。

U-Boot 的波特率配置路径:

  • 源码级: include/configs/你的板卡.h 中的 CONFIG_BAUDRATE 宏
  • 环境变量: baudrate 变量,可在 U-Boot 命令行用 setenv 修改
  • 设备树: 部分新平台通过 chosen 节点或 clock-frequency 属性配置

来看一个典型的配置示例:

/* include/configs/myboard.h */
#define CONFIG_BAUDRATE         115200
#define CONFIG_SYS_BAUDRATE_TABLE \
    { 9600, 19200, 38400, 57600, 115200 }

这个 CONFIG_SYS_BAUDRATE_TABLE 定义了 U-Boot 命令行下 baudrate 命令可切换的速率列表。你想想看,如果板子连上串口工具后输出乱码,第一件事就是检查两边波特率是否一致。

波特率 典型应用场景 优缺点
9600 老旧设备、低速调试 稳定但慢,刷大量日志时明显卡顿
38400 部分工业控制板 折中选择,兼容性较好
57600 中速调试场景 速度与稳定性平衡
115200 绝大多数开发板默认值 速度快,兼容性好,推荐使用
230400 / 460800 高速调试、大量日志输出 容易丢数据,线缆质量要求高

注意:不要盲目追求高波特率。我曾经在一块走线较长的板子上用 460800,结果每三行日志就丢一行。降到 115200 后一切正常。串口线长度超过 1.5 米时,建议不要超过 115200。

2.3 日志级别:U-Boot 的打印也有等级之分

很多人以为 U-Boot 的打印就是简单的 printf(),其实它有一套日志级别系统。U-Boot 定义了从 LOGL_EMERG 到 LOGL_DEBUG 共 7 个级别:

级别宏 数值 含义 典型输出内容
LOGL_EMERG 0 紧急(系统不可用) 致命硬件错误
LOGL_ALERT 1 警报(必须立即处理) 内存校验失败
LOGL_CRIT 2 严重(关键条件不满足) 找不到启动设备
LOGL_ERR 3 错误(常规错误) 文件系统挂载失败
LOGL_WARNING 4 警告(可能有问题) 环境变量校验和错误
LOGL_NOTICE 5 注意(正常但重要) 版本信息、启动进度
LOGL_DEBUG 6 调试(开发阶段使用) 寄存器值、函数调用跟踪

U-Boot 通过 CONFIG_LOGLEVEL 宏控制哪些级别的日志会被输出。比如设置为 4,则只输出 LOGL_WARNING 及以上级别的信息,LOGL_NOTICE 和 LOGL_DEBUG 会被静默丢弃。

/* 在板卡配置头文件中设置 */
#define CONFIG_LOGLEVEL  6   /* 输出所有级别,包括调试信息 */

我个人习惯在开发阶段把日志级别开到最大(6),发布前再降到 4 或 5。这样既能保留调试信息,又不会让最终用户看到一堆看不懂的调试输出。

2.4 打印函数家族:不只是 printf()

U-Boot 里常用的打印函数有这几个:

  • printf() — 标准格式化输出,最常用
  • puts() — 输出字符串,不带换行
  • putc() — 输出单个字符
  • debug() — 条件编译的调试打印,只在 DEBUG 宏定义时生效
  • log() — 带日志级别的打印,配合 CONFIG_LOGLEVEL 使用
  • serial_puts() / serial_putc() — 直接操作串口硬件的底层函数

来看一个实际使用场景:

/* 传统方式:直接用 printf */
printf("Board: %s, Rev: %d\n", board_name, board_rev);

/* 带日志级别的方式 */
log(LOGL_NOTICE, "Board: %s, Rev: %d\n", board_name, board_rev);

/* 调试打印,只在 DEBUG 模式下生效 */
debug("DRAM init: base=0x%08x, size=%dMB\n", dram_base, dram_size);

你可能会问:为什么不用 printf() 一把梭?原因很简单——控制输出量。在启动早期,串口速度有限,如果所有模块都疯狂打印,反而会拖慢启动速度,甚至导致 FIFO 溢出丢数据。

我的经验:在调试 DDR 初始化或时钟配置这类时序敏感代码时,尽量少用 printf(),改用 putc() 输出一个字符表示进度。等基本功能调通后再打开详细日志。我曾经因为 printf() 调用太多,导致 DDR 训练时序被破坏,折腾了两天才发现是打印函数惹的祸。

2.5 串口输出流程全景图

下面这张图展示了从 U-Boot 调用打印函数到串口工具显示字符的完整链路:

U-Boot 串口输出流程 U-Boot 串口输出完整流程 printf() / log() / debug() console 框架 (stdio) serial_putc() / 驱动回调 UART 发送 FIFO / 移位寄存器 RS-232 电平转换 (MAX3232) 串口工具 (PuTTY / minicom / screen) 关键配置一致性 波特率:必须相同 数据位:8(默认) 停止位:1(默认) 校验位:无(默认) 常见问题 乱码 → 波特率不匹配 无输出 → 电平/接线问题 丢数据 → 波特率过高/线缆过长

2.6 实战排查思路

当你面对一个串口无输出的 U-Boot 板子时,按这个顺序排查:

  1. 硬件层面:用示波器或逻辑分析仪测量 UART TX 引脚是否有波形。没有波形 → 问题在板子;有波形但乱码 → 问题在配置。
  2. 波特率匹配:确认串口工具和 U-Boot 配置的波特率一致。不确定时,从 115200 开始试,不行再换 9600、57600。
  3. 日志级别:检查 CONFIG_LOGLEVEL 是否设得太低,把关键启动信息过滤掉了。
  4. 打印函数:确认代码中用的是 printf() 还是 debug()。如果是 debug(),检查 DEBUG 宏是否定义。
  5. 串口初始化:确认 serial_init() 在 board_init_f() 或 board_init_r() 中被正确调用。

避坑提醒:我曾经遇到一个情况——U-Boot 在 SPL 阶段有输出,但跳到 U-Boot proper 后就没了。查了两天发现是 proper 阶段的设备树里串口时钟频率配错了,导致波特率实际值和配置值差了 3 倍。所以,如果你遇到「SPL 有输出,主 U-Boot 没输出」,优先检查设备树中串口节点的 clock-frequency 属性。

串口输出是 U-Boot 调试的「眼睛」。把这章的内容吃透,后面分析启动日志时你就能一眼看出问题出在哪个环节。记住:先看有没有输出,再看输出对不对,最后看输出全不全。这三步走完,大部分串口问题都能解决。