第四章 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的命令实现。比如bootm、go这些命令。我们要添加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.c的do_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引导要单独处理,环境变量要灵活配置,菜单实现要兼顾用户体验。下一章我们聊聊分区方案和文件系统布局,那是双系统共存的另一个关键点。