title: 【从零写OS内核】336 U-Boot 移植:从零让你的开发板跑起来
category: os
tag: [u-boot, embedded, arm,移植]
source: https://github.com/golang12306/os-kernel-from-scratch

【从零写OS内核】336 U-Boot 移植:从零让你的开发板跑起来

1. U-Boot 移植的本质

U-Boot 移植本质是让引导程序认识你的硬件。芯片原厂提供参考板代码,我们在此基础上修改 GPIO、时钟、DDR、存储驱动,使 U-Boot 能在新板上稳定运行。

类比 Windows:把 U-Boot 想象成 Windows PE 环境下的 BIOS 配置工具——你需要告诉系统怎么初始化内存、怎么访问磁盘、怎么加载内核。

Bash
┌─────────────────────────────────────────────────────────────┐
│                U-Boot 移植分层架构                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐          │
│  │  cmd_xxx    │  │  net_xxx    │  │  drivers/   │          │
│  │  (命令层)    │  │  (网络层)    │  │  (驱动层)   │          │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘          │
│         └────────────────┼────────────────┘                │
│                          │                                  │
│                   ┌──────┴──────┐                           │
│                   │   common/   │  (通用代码)               │
│                   └──────┬──────┘                           │
│                          │                                  │
│         ┌────────────────┼────────────────┐                │
│         │                │                │                │
│   ┌─────┴─────┐    ┌─────┴─────┐    ┌─────┴─────┐           │
│   │  board/  │    │   arch/   │    │ configs/  │           │
│   │  板级     │    │  架构层   │    │  defconfig │           │
│   │  配置     │    │  ARM/RISC │    │  编译配置  │           │
│   └───────────┘    └───────────┘    └───────────┘           │
└─────────────────────────────────────────────────────────────┘

2. 移植前的硬件资料准备

移植前必须拿到以下资料(通常向芯片厂申请):

资料 说明 获取途径
SoC datasheet 寄存器定义、时钟树 原厂
Hardware Manual 硬件设计参考 原厂
DDR timing spec 内存时序参数 DDR 芯片厂
Board schematic 原理图 板厂
Reference U-Boot 原厂提供的参考代码 原厂/社区
BSP code 板级支持包 原厂

2.1 原理图关键网络

Bash
必须确认的网络:
1. DDR 数据线 DQ[0:31] — 连到哪个 GPIO 组
2. DDR 地址线 A[0:15] — 连到哪个 GPIO 组
3. DDR CLK/QSPI — 时钟和片选
4. eMMC/SD 卡信号 — 哪个 GPIO 组
5. UART TX/RX — 调试串口
6. PMIC I2C — 电源管理芯片通信
7.reset 复位电路 — 上电时序要求

3. 移植第一步:创建板级目录

U-Boot 源码结构树(关键路径):

Bash
u-boot/
├── arch/
│   └── arm/
│       └── dts/
│           └── exynos4412-scp.dts    # 设备树
├── board/
│   └── samsung/
│       └── smdk4412/                 # 你的板子目录
│           ├── Kconfig
│           ├── MAINTAINERS
│           ├── Makefile
│           └── board.c               # 板级初始化
├── configs/
│   └── smdk4412_defconfig            # 编译配置文件
├── drivers/
│   ├── dwmci/                        # SD/MMC 驱动
│   ├── exynos/                       # 三星 SoC 驱动
│   └── ddr/                          # DDR 驱动
└── include/
    └── configs/
        └── smdk4412.h                # 板级头文件

3.1 新建 board/samsung/mybsp

Bash
# 1. 复制参考板目录作为模板
cp -r board/samsung/smdk4412 board/samsung/mybsp

# 2. 修改目录名和文件名
cd board/samsung/mybsp
mv board.c board_mybsp.c

# 3. 修改 Makefile
# 原来:obj-y += board.o
# 改为:obj-y += board_mybsp.o

3.2 configs/defconfig

Bash
# configs/mybsp_defconfig
CONFIG_ARM=y
CONFIG_ARCH_EXYNOS=y
CONFIG_CPU_EXYNOS4412=y
CONFIG_TARGET_MYBSP=y
CONFIG_SOC_EXYNOS4412=y

# 串口配置
CONFIG_DEBUG_UART=y
CONFIG_DEBUG_UART_S5PV210=y
CONFIG_DEBUG_UART_BASE=0x13800000
CONFIG_BAUDRATE=115200

# DDR 配置
CONFIG_DDR_16BIT=y
CONFIG_DDR_16BIT_BUS_WIDTH=32

# 启动命令
CONFIG_BOOTCOMMAND="load mmc 0:1 0x4000_7000 uImage; load mmc 0:1 0x4000_1000 mybsp.dtb; bootm 0x4000_7000"
CONFIG_BOOTARGS="console=ttySAC0,115200 root=/dev/mmcblk0p2"

4. 核心移植文件解析

4.1 board.c — 板级初始化

C
// board/samsung/mybsp/board_mybsp.c
#include <common.h>
#include <clk.h>
#include <asm/io.h>
#include <asm/arch/clock.h>
#include <asm/arch/gpio.h>
#include <asm/arch/pinmux.h>

DECLARE_GLOBAL_DATA_PTR;

int board_init(void)
{
    /* 1. GDTM 初始化 (Global Data Structure) */
    gd = (gd_t *)CONFIG_SYS_INIT_SP_ADDR;

    /* 2. 时钟初始化 */
    struct exynos_clock *clock = (struct exynos_clock *)EXYNOS4_CLOCK_BASE;

    /* 设置 ARMCLK = 1500MHz, HCLK = 150MHz, PCLK = 75MHz */
    clock->apll_con1 = 0x20203;      @ APLL M=250, P=6, S=11.5GHz
    clock->set_div = 0x141403;        @ DIV_CORE2=2, HCLK_G=2, PCLK_G=2

    /* 3. GPIO 引脚配置 */
    /* UART: GPA0_0=UART_TXD, GPA0_1=UART_RXD */
    pinmux_config(GPA0, 0, UART0);

    /* 4. PMIC (电源管理) 初始化 */
    /* 检查 POWEN_KEY 按下进入下载模式 */
    if (gpio_get_value(GPIO_PWREN_KEY) == 0) {
        /* 进入 U-Boot 下载模式 (fastboot) */
        board_fastboot_setup();
    }

    /* 5. 存储介质初始化 */
    #ifdef CONFIG_MMC
    exynos_mmc_init();
    #endif

    return 0;
}

4.2 DDR 移植 — 最关键的部分

DDR 是移植中最复杂的一步。DDR 初始化失败会导致 U-Boot 运行不稳定。

C
// drivers/ddr/mybsp/ddr_ctrl.c (DDR 驱动核心)

/*
 * DDR3 初始化序列(基于 JEDEC 标准)
 * 实际参数需参考 DDR 芯片手册和 PCB 走线
 */
int ddr_init(void)
{
    struct ddr_ctrl *ddr = (struct ddr_ctrl *)EXYNOS4_DMC_BASE;

    /* Step 1: PHY 初始化 — 配置 DQ 引脚 */
    ddr->phy_con0 = 0x11110010;
    ddr->phy_con1 = 0x10000010;
    ddr->phy_con2 = 0x00330000;

    /* Step 2: 时序参数 (Timing Inject) 
     * 这些参数由 DDR 厂商提供(tRCD, tRP, tRL, tWL等)
     */
    ddr->timing1 = 0x1845008A;    @ tRDC=18, tRP=18, tRC=52
    ddr->timing2 = 0x38387090;    @ tRAS=36, tWTR=8, tRRD=8
    ddr->timing3 = 0x2F186810;    @ tXP=3, tXPDLL=36, tRTP=8

    /* Step 3: DLL 使能(同步时钟输出) */
    ddr->mdma_con = 0x00102312;

    /* Step 4: DRAM Mode Register 配置 */
    /* MR0: 突发长度 8, 突发类型 顺序 */
    ddr->mode = 0x00000330;      @ BL=8, BT=0, CL=6, WR=5
    ddr->mode >> start_dll();     @ 启动 DLL

    /* Step 5: ODT 配置(片上终结电阻) */
    #ifdef DDR_ODT_ON
    ddr->phy_odt = 0x00000F03;
    #endif

    /* Step 6: ZQ 校准 */
    ddr->ZQ_con = 0xC000107A;    @ ZQ 校准使能

    /* Step 7: 刷新配置 */
    ddr->refresh_con = 0x000003C3; @ 7.8us 刷新一次

    /* Step 8: 验证 DRAM 工作 */
    mem_ctrl_write(0x00000000, 0x12345678);
    if (mem_ctrl_read(0x00000000) != 0x12345678) {
        debug("DDR read/write test failed!n");
        return -1;
    }

    printf("DDR3 %dMB initialized OKn", DDR_SIZE_MB);
    return 0;
}

4.3 DDR Training — 信号校准

DDR 训练是让控制器自动调整 DQ 采样窗口的过程:

C
/*
 * Read DQS Gate Training(读门限训练)
 * DQS 是 DDR 数据的同步时钟,门限不准会读错数据
 */
int ddr_read_gate_training(void)
{
    struct ddr_ctrl *ddr = (struct ddr_ctrl *)EXYNOS4_DMC_BASE;

    /* 写入已知模式 */
    mem_ctrl_write_pattern(DDR_TEST_ADDR, 0x55555555);
    mem_ctrl_write_pattern(DDR_TEST_ADDR+4, 0xAAAAAAAA);

    /* 手动扫描最佳 DQ 采样点 */
    for (int dqs_delay = 0; dqs_delay < 0x40; dqs_delay++) {
        ddr->phy_dqs_con = dqs_delay;

        u32 val1 = mem_ctrl_read(DDR_TEST_ADDR);
        u32 val2 = mem_ctrl_read(DDR_TEST_ADDR+4);

        if (val1 == 0x55555555 && val2 == 0xAAAAAAAA) {
            printf("Read gate training PASS: DQS delay=0x%02Xn", dqs_delay);
            return 0;
        }
    }

    printf("Read gate training FAILEDn");
    return -1;
}

5. 引脚复用 (Pinmux) 配置

引脚复用是让同一个 GPIO 引脚在不同功能间切换:

C
// arch/arm/include/asm/arch-exynos/pinmux.h

/* GPA0 组: UART0 */
static inline void pinmux_config(int group, int pin, int func)
{
    struct exynos_pinmux *pmu = (struct exynos_pinmux *)EXYNOS4_PINMUX_BASE;

    u32 reg = pmu->con[group][pin];
    reg &= ~(0xF << (pin * 4));           @ 清除 4 位
    reg |= (func & 0xF) << (pin * 4);     @ 写入功能值
    pmu->con[group][pin] = reg;

    /* 设置上下拉 */
    pmu->pull[group][pin] = EXYNOS_PULL_UP; @ 上拉
}

/* 典型引脚配置表 */
static const struct pin_config board_pinmux[] = {
    /* UART0: GPA0_0=TXD, GPA0_1=RXD */
    { GPA0, 0, PIN_UART0_TX },
    { GPA0, 1, PIN_UART0_RX },

    /* SD/MMC: GPK2 组 */
    { GPK2, 0, PIN_SD_CMD },
    { GPK2, 1, PIN_SD_CLK },
    { GPK2, 2, PIN_SD_D0 },
    { GPK2, 3, PIN_SD_D1 },
    { GPK2, 4, PIN_SD_D2 },
    { GPK2, 5, PIN_SD_D3 },

    /* I2C0: GPD0 组 */
    { GPD0, 0, PIN_I2C0_SCL },
    { GPD0, 1, PIN_I2C0_SDA },
};

6. 编译与烧写

Bash
# 1. 交叉编译工具链(ARM Cortex-A9)
export CROSS_COMPILE=arm-none-linux-gnueabi-
export ARCH=arm

# 2. 清理后编译
make distclean
make mybsp_defconfig
make -j$(nproc)

# 3. 生成的镜像
# u-boot.bin — 完整 U-Boot (烧到 SD/eMMC)
# u-boot-spl.bin — SPL (烧到 SRAM 起始位置)
# spl/u-boot-spl.bin — SPL 二进制文件

# 4. 烧写到 SD 卡
dd if=u-boot-spl.bin of=/dev/sdb bs=512 seek=8
dd if=u-boot.bin of=/dev/sdb bs=512 seek=16896

# 5. SD 卡分区布局
# ┌────────────────┬──────────────────────────────┐
# │  0-7 sectors   │  8-16895 sectors            │  ← 16896 = 8 + 16*1024
# │   SPL (4KB)    │   U-Boot (8MB)               │
# ├────────────────┼──────────────────────────────┤
# │ 16896-...      │  ...                        │
# │  Kernel        │  Device Tree                │
# └────────────────┴──────────────────────────────┘

6.1 编译错误排查

Bash
# 错误1: "libgcc is different for guest vs target"
export CROSS_COMPILE=arm-none-linux-gnueabi-

# 错误2: "compiler version mismatch"
# 检查 GCC 版本和 U-Boot 支持的版本
arm-none-linux-gnueabi-gcc --version

# 错误3: DDR 初始化失败 — 串口输出卡住
# 检查:
# 1. 串口波特率是否正确 (115200 8N1)
# 2. DDR timing 参数是否匹配 DDR 芯片手册
# 3. GPIO 引脚配置是否正确

# 调试技巧:U-Boot 启动时加 delay 留出串口接收时间
# 在 board.c 的 board_init() 开头加:
printf("board_init startn");
udelay(1000000);  @ 1秒延迟,给串口时间

7. Windows WDM vs U-Boot 分层对比

层次 U-Boot Windows WDM Driver
初始化 board_init() DriverEntry()
硬件抽象 arch层面 drivers HAL.DLL
配置存储 env vars (环境变量) Registry
驱动模型 driver model KMDF/WDM
启动参数 bootargs BCD store
升级方式 mmc update / tftp Windows Update

8. 调试实践

Bash
# 串口调试:连接 TTL 串口 (FTDI 3.3V)
# TX ──── RX
# RX ──── TX
# GND ─── GND
screen /dev/ttyUSB1 115200

# U-Boot 内置调试命令
printenv           # 查看所有环境变量
setenv baudrate 115200
saveenv            # 保存环境变量到存储介质
md 0x40000000 16   # 内存显示 (dump)
mw 0x40000000 0x1234ABCD 1  # 内存写入
mmc info           # MMC 信息
mmc read 0x4000_7000 0x100 0x200  # 读取 MMC

# 网络烧写 (在 U-Boot 中)
setenv ipaddr 192.168.1.100
setenv serverip 192.168.1.1
tftp 0x4000_7000 uImage.bin  # 从 TFTP 服务器下载

9. 小结

Bash
U-Boot 移植步骤:

1. 硬件资料准备
   └── SoC datasheet + DDR timing + schematic

2. 创建板级目录
   └── board/samsung/mybsp/

3. 配置 Pinmux
   └── 引脚复用:UART/I2C/MMC

4. DDR 初始化
   └── timing 参数 + Training

5. 存储驱动
   └── eMMC/SD/MMC

6. 环境变量
   └── bootcmd / bootargs

7. 编译烧写
   └── make + dd

每块板子都不一样,移植的核心是:
把"初始化代码"适配到"你的硬件设计"。
像 Windows 装机一样,先 BIOS 设置好,才能跑系统。

微信公众号
· 深入浅出技术解读 ☕️
· 欢迎搜索关注「码农的螺丝刀」

最后修改: 2024年1月13日

作者

评论

发表评论

您的邮箱地址不会被公开。