第三章 Uboot移植与定制:从源码到产品落地的关键一步

说实话,Uboot移植这块,我当年刚入行时也觉得挺神秘的。总觉得把引导代码跑起来,那得是多高深的技术。后来做多了才发现,其实没那么玄乎。说白了,Uboot就是个硬件初始化加引导加载的小系统。今天我就把海思平台上的Uboot移植经验,掰开了揉碎了讲给你听。

3.1 Uboot源码结构:别被目录吓到

我第一次打开Uboot源码时,说实话有点懵。目录太多了。但你仔细看,其实就几块核心内容。

Uboot源码主要分这么几层:

  • arch/:架构相关代码。ARM、x86、RISC-V都在这里。海思芯片走ARM架构,所以重点关注arch/arm/。
  • board/:板级支持代码。每个开发板都有自己的文件夹。海思的板级代码一般在board/hisilicon/下。
  • drivers/:外设驱动。串口、网口、I2C、SPI都在这里。
  • include/:头文件。特别是include/configs/下的配置文件,那是移植的关键。
  • common/:通用功能。比如命令行解析、环境变量管理。

我个人习惯,拿到一个新芯片的Uboot,先看board/目录。因为板级代码是最贴近硬件的,从这里入手能最快了解硬件配置。

小技巧: 我建议你先用tree命令看看目录结构,心里有个谱。别一上来就扎进代码细节里。

3.2 海思平台Uboot配置:从零开始

海思芯片的Uboot配置,其实有一套固定的套路。你照着做就行。

配置流程大致是这样:

  1. 选择默认配置:在Uboot根目录执行 make hi3516dv300_defconfig(具体芯片型号替换一下)。
  2. 菜单配置make menuconfig,这里可以开关各种功能。比如你要不要支持NFS启动,要不要开启USB烧录。
  3. 编译make -j4,生成u-boot.bin。

嗯,这里要注意。海思的配置文件和标准Uboot有点不一样。它多了很多海思特有的配置项。比如:

// include/configs/hi3516dv300.h
#define CONFIG_HI3516DV300
#define CONFIG_SYS_TEXT_BASE 0x40000000
#define CONFIG_BAUDRATE 115200
#define CONFIG_BOOTARGS "mem=128M console=ttyAMA0,115200 root=/dev/mtdblock2 rw"

我曾经遇到过一个坑:CONFIG_SYS_TEXT_BASE这个地址设错了。结果Uboot加载到内存后,直接覆盖了内核的加载地址。那叫一个惨,折腾了两天才发现是地址冲突。

避坑指南: 配置内存地址时,一定要确认DDR的映射范围。我建议你先读芯片手册的Memory Map章节,把地址空间理清楚再动手。

3.3 DDR初始化:让芯片有“记忆”

DDR初始化,说白了就是让CPU能访问内存。这一步要是错了,后面全白搭。

海思平台的DDR初始化,一般放在SPL(Secondary Program Loader)里。SPL是Uboot的一级引导,它负责初始化DDR,然后把完整的Uboot加载到内存里。

DDR初始化的关键参数:

参数 说明 常见值
DDR类型 DDR3还是DDR4 DDR3
频率 DDR工作频率 1066MHz
时序参数 CL、tRCD、tRP等 根据颗粒手册
容量 单颗容量和通道数 512MB x 2

我记得有一次,客户反馈说板子偶尔启动不起来。查了半天,发现是DDR时序参数设得太紧。温度一高,时序就hold不住了。后来我把时序放宽了一档,问题就解决了。

DDR初始化的代码一般在board/hisilicon/xxx/ddr/下。你打开看看,会发现一堆寄存器配置。别怕,其实就是按照芯片手册的时序要求,往寄存器里写值。

// DDR初始化示例(伪代码)
void ddr_init(void)
{
    // 1. 配置DDR控制器
    writel(0x12345678, DDR_CTRL_BASE + 0x00);
    // 2. 设置时序参数
    writel(0x000A0008, DDR_TIMING_BASE + 0x10);
    // 3. 启动DDR校准
    writel(0x01, DDR_CALIB_BASE + 0x00);
    // 4. 等待校准完成
    while(!(readl(DDR_CALIB_BASE + 0x04) & 0x01));
}

核心要点: DDR初始化失败,最常见的现象就是串口没输出。因为Uboot的串口驱动依赖DDR来存放数据。所以,如果你发现串口完全没反应,先查DDR。

3.4 时钟配置:心脏跳动的节奏

时钟配置,就是给芯片各个模块提供工作频率。海思芯片的时钟树比较复杂,但核心就几个:

  • CPU时钟:决定处理器主频。一般1GHz左右。
  • DDR时钟:决定内存访问速度。
  • 总线时钟:决定外设通信速度。
  • 外设时钟:串口、网口、USB各自需要独立的时钟。

时钟配置的代码一般在arch/arm/mach-hi35xx/clock.c里。你想想看,如果时钟配错了,CPU跑得太慢,系统响应就会很迟钝。我见过有人把CPU时钟设成100MHz,结果系统启动花了半分钟。

时钟配置的步骤:

  1. 配置PLL(锁相环)产生基准频率。
  2. 设置分频器,得到各模块需要的频率。
  3. 使能时钟输出。
// 时钟配置示例
void clock_init(void)
{
    // 配置CPU PLL:24MHz输入,倍频到1GHz
    writel(0x1F, PLL_CTRL_BASE + 0x00); // 设置倍频系数
    udelay(100); // 等待PLL锁定
    
    // 配置DDR PLL:1066MHz
    writel(0x2C, PLL_CTRL_BASE + 0x10);
    udelay(100);
    
    // 使能串口时钟
    writel(0x01, CLOCK_GATE_BASE + 0x20);
}
经验之谈: 时钟配置时,我建议你先把PLL锁定时间设长一点。有些芯片的PLL锁定需要几百微秒,设短了会导致时钟不稳定。

3.5 网口驱动移植:让板子能联网

网口驱动移植,是Uboot定制里比较麻烦的一块。因为网口涉及到PHY芯片、MAC控制器、DMA传输等多个环节。

海思平台常用的网口方案:

  • 内部MAC + 外部PHY:比如海思内部集成了GMAC,外接RTL8211 PHY。
  • 内部MAC + 内部PHY:有些芯片把PHY也集成进去了。

移植网口驱动,主要做这几件事:

  1. 配置MAC控制器:设置MAC地址、工作模式(MII/RGMII)。
  2. 初始化PHY:通过MDIO总线读写PHY寄存器,配置速率和双工模式。
  3. 设置DMA描述符:让数据能在内存和网口之间高效传输。

我遇到过最头疼的问题,是PHY的复位时序。有些PHY芯片需要先拉低复位引脚,等一段时间再拉高。时间太短,PHY没准备好;时间太长,又影响启动速度。后来我查了PHY手册,发现复位时间至少要10ms。改了之后,网口就稳定了。

// 网口初始化示例
int eth_init(void)
{
    // 1. 复位PHY
    gpio_set_value(PHY_RST_PIN, 0);
    mdelay(20);
    gpio_set_value(PHY_RST_PIN, 1);
    mdelay(50);
    
    // 2. 配置MAC
    writel(0x01, GMAC_BASE + GMAC_CTRL); // 使能MAC
    writel(0x02, GMAC_BASE + GMAC_MODE); // 设置RGMII模式
    
    // 3. 读取PHY ID,确认通信正常
    u16 phy_id = phy_read(PHY_ADDR, 0x02);
    if(phy_id != 0x001C) {
        printf("PHY not found!\n");
        return -1;
    }
    
    return 0;
}
注意: 网口调试时,我建议你先用示波器看PHY的时钟输出。如果时钟波形不对,那肯定是时钟配置有问题,别在驱动代码上浪费时间。

3.6 串口调试:开发者的“眼睛”

串口调试,是嵌入式开发里最基础也最重要的调试手段。没有串口输出,你就像瞎子摸象。

海思平台的串口驱动移植,相对简单。因为串口协议很成熟,无非就是配置波特率、数据位、停止位。

串口初始化的关键点:

  • 引脚复用:确认串口引脚被配置成UART功能,而不是GPIO。
  • 时钟使能:给UART模块提供时钟。
  • 波特率设置:根据时钟频率计算分频系数。
// 串口初始化示例
void uart_init(void)
{
    // 1. 使能UART时钟
    writel(0x01, UART_CLOCK_GATE);
    
    // 2. 配置波特率:115200 @ 24MHz
    // 分频系数 = 24000000 / (16 * 115200) = 13
    writel(0x0D, UART_BASE + UART_DLL);
    writel(0x00, UART_BASE + UART_DLM);
    
    // 3. 配置数据格式:8N1
    writel(0x03, UART_BASE + UART_LCR);
    
    // 4. 使能FIFO
    writel(0x01, UART_BASE + UART_FCR);
}

嗯,这里有个小细节。串口波特率设错了,输出就是乱码。我刚开始做时,经常看到一堆乱码,还以为是硬件坏了。后来才发现是分频系数算错了。

调试技巧: 如果你发现串口没输出,先检查这几个地方:

  • 串口线有没有接反?TX接RX,RX接TX。
  • 波特率对不对?用示波器量一下TX引脚的波形,看脉冲宽度。
  • 引脚复用有没有配?有些芯片默认引脚是GPIO,需要手动切换。

好了,Uboot移植这块,核心内容就这些。从源码结构到DDR、时钟、网口、串口,每一步都有坑,但每一步也都有规律可循。你多做几个项目,自然就熟练了。下次我们聊聊内核移植,那又是另一番天地了。