第四章 U-Boot改造与多系统引导

好,咱们进入第四章。这一章我打算聊聊U-Boot的改造,说白了就是怎么让U-Boot既能引导Linux,也能引导QNX。我在做第一个双系统项目时,光U-Boot这块就折腾了两周。嗯,今天我把这些经验拆开揉碎了讲给你听。

4.1 U-Boot源码结构

先说说U-Boot的源码结构。你下载下来会发现,它大概长这样:

u-boot/
├── arch/          # 架构相关代码
├── board/         # 板级支持包
├── common/        # 通用功能
├── cmd/           # 命令行命令
├── drivers/       # 驱动
├── include/       # 头文件
├── lib/           # 库函数
├── net/           # 网络协议
└── tools/         # 工具链

我个人习惯重点关注三个目录:arch/board/cmd/。为什么?因为引导QNX需要改动的代码,基本都在这三个地方。

arch/arm/下面存放的是ARM架构的启动代码,包括异常向量表、MMU初始化这些底层东西。QNX对MMU的要求和Linux不太一样,我遇到过因为MMU配置不对,QNX内核一启动就挂掉的情况。

board/目录里是板级初始化代码。每个板子都有自己的DDR配置、时钟设置、GPIO复用。这里最容易出问题,因为QNX的硬件抽象层和Linux的设备树,对同一块硬件的理解可能不同。

cmd/目录存放的是U-Boot的命令实现。比如bootmgo这些命令。我们要添加QNX引导支持,就得在这里动手脚。

4.2 添加QNX引导支持

QNX的引导方式和Linux不太一样。Linux通常用bootm命令,加载内核和设备树。QNX呢?它用的是bootelf或者go命令。

具体怎么做?我分三步讲:

第一步:添加QNX镜像类型识别

include/image.h中,需要添加QNX镜像的魔术字定义:

#define IH_TYPE_QNX6      0x10  /* QNX 6.x 内核镜像 */
#define IH_OS_QNX         0x12  /* QNX 操作系统类型 */

第二步:实现QNX引导函数

cmd/bootm.c中,添加一个专门处理QNX镜像的函数:

static int do_bootm_qnx(int flag, int argc, char *const argv[])
{
    void (*entry)(void) = (void (*)(void))images.ep;
    
    /* 关闭MMU和缓存,QNX需要自己管理 */
    dcache_disable();
    icache_disable();
    
    /* 跳转到QNX入口 */
    printf("Starting QNX...\n");
    entry();
    
    /* 正常情况下不会返回 */
    return 0;
}

这里有个坑:QNX启动时要求MMU和缓存都是关闭状态。我刚开始没注意这个,结果QNX内核一跑就报异常。后来查了QNX的启动文档才发现,它要自己建立页表。

第三步:注册引导命令

cmd/bootm.cdo_bootm_states函数中,添加对QNX镜像的处理分支:

case IH_TYPE_QNX6:
    ret = do_bootm_qnx(flag, argc, argv);
    break;

注意:QNX镜像的加载地址必须和QNX内核链接地址一致。我曾经因为地址没对齐,导致QNX启动后串口输出全是乱码。建议用readelf -l查看QNX镜像的加载地址。

4.3 U-Boot环境变量配置

环境变量是U-Boot的灵魂。我习惯把引导参数都放在环境变量里,这样不用每次重新编译U-Boot。

典型的双系统环境变量配置如下:

# 公共参数
baudrate=115200
bootdelay=3
ethaddr=00:11:22:33:44:55

# Linux引导参数
loadaddr_linux=0x82000000
fdtaddr=0x83000000
bootargs_linux=console=ttyS0,115200 root=/dev/mmcblk0p2 rw

# QNX引导参数
loadaddr_qnx=0x88000000
bootargs_qnx=console=ttyS0,115200 root=/dev/sd0

# 默认引导系统
boot_system=linux

这里我建议把boot_system变量做成可配置的。用户可以通过串口修改它,选择默认启动哪个系统。

实际项目中,我还会加一个boot_retry变量:

boot_retry=3  # 引导失败后重试次数

为什么要加这个?有一次现场演示时,SD卡接触不良导致引导失败,场面一度很尴尬。有了重试机制,至少能多试几次。

4.4 多系统菜单实现

多系统菜单,说白了就是让用户能在启动时选择进Linux还是QNX。我见过两种实现方式:

方式一:基于U-Boot脚本的菜单

include/configs/目录下的板级配置文件中,添加菜单脚本:

#define CONFIG_BOOTCOMMAND \
    "if test \"${boot_system}\" = \"qnx\"; then " \
        "run boot_qnx; " \
    "else " \
        "run boot_linux; " \
    "fi"

#define CONFIG_EXTRA_ENV_SETTINGS \
    "boot_linux=echo 'Booting Linux...'; " \
        "ext4load mmc 0:1 ${loadaddr_linux} /boot/zImage; " \
        "ext4load mmc 0:1 ${fdtaddr} /boot/board.dtb; " \
        "bootz ${loadaddr_linux} - ${fdtaddr}\0" \
    "boot_qnx=echo 'Booting QNX...'; " \
        "ext4load mmc 0:1 ${loadaddr_qnx} /boot/ifs-qnx; " \
        "go ${loadaddr_qnx}\0"

方式二:交互式菜单

我更喜欢这种方式,用户体验更好。在cmd/目录下新建一个menu.c文件:

#include <common.h>
#include <command.h>

static int do_boot_menu(cmd_tbl_t *cmdtp, int flag, 
                        int argc, char *const argv[])
{
    char c;
    
    printf("\n==========================\n");
    printf("  双系统启动菜单\n");
    printf("==========================\n");
    printf("  1. 启动 Linux\n");
    printf("  2. 启动 QNX\n");
    printf("  3. 进入 U-Boot 命令行\n");
    printf("==========================\n");
    printf("请选择 (1-3): ");
    
    c = getc();
    printf("\n");
    
    switch(c) {
    case '1':
        run_command("run boot_linux", 0);
        break;
    case '2':
        run_command("run boot_qnx", 0);
        break;
    case '3':
        /* 直接进入命令行 */
        break;
    default:
        printf("无效选择,默认启动 Linux\n");
        run_command("run boot_linux", 0);
    }
    
    return 0;
}

U_BOOT_CMD(
    bootmenu, 1, 0, do_boot_menu,
    "显示双系统启动菜单",
    " - 显示启动菜单供用户选择系统"
);

小技巧:我习惯在菜单里加一个倒计时。如果用户3秒内没选择,就按默认系统启动。这样既给了用户选择权,又不会耽误自动化启动流程。

实现倒计时其实很简单,用U-Boot的tstc()函数配合udelay()就行:

for (i = 3; i > 0; i--) {
    printf("\r倒计时 %d 秒...", i);
    if (tstc()) {
        c = getc();
        break;
    }
    udelay(1000000);  // 等待1秒
}

嗯,到这里U-Boot改造的核心内容就讲完了。总结一下:源码结构要熟悉,QNX引导要单独处理,环境变量要灵活配置,菜单实现要兼顾用户体验。下一章我们聊聊分区方案和文件系统布局,那是双系统共存的另一个关键点。