简介:海思Hi3531的U-Boot移植与DDR3参数配置详解,面向嵌入式底层开发者与BSP工程师,是一份实操型PDF。内容覆盖从SDK目录拷贝、解压编译、板级文件与配置头文件改造,到自定义编译脚本、IP/PHY/服务器地址修改的完整U-Boot移植流程;文中特别针对镁光MT41K256M16HA-125IT内存颗粒,结合原理图说明DDRC_RNKCFG(0x2C)与DDRC_TIMING1(0x54)寄存器的参数推导与修改依据,并给出FastBoot烧录、网络ping测试以及首次ping异常需按Ctrl+C重新执行的注意事项。压缩包仅含1个PDF,大小1.38MB,文字精炼、图文步骤清晰,已有2518人学习下载,适合正调试Hi3531平台或有uboot定制需求的工程师参考。

1. 海思3531的U-Boot移植,量产项目里最容易翻车的环节是DDR3参数配置修改

很多工程师以为改改配置头、换掉工具链、make一次就算完事,结果上电要么串口全黑,要么打印几行就重启。原因在于:U-Boot重定位之前就要完成DDRC和PHY初始化,代码还跑在片内SRAM里,一旦时序、ODT端接或驱动强度与颗粒不匹配,内存访问立即异常。下面按动手顺序,把要动的文件、时序如何从手册换算成寄存器值、起不来时怎么定位讲清楚,适合正在做3531核心板Bring-up或计划更换DDR3容量的工程师。

2. 移植前准备: 交叉工具链、启动介质与最小构建命令

2.1 工具链选型: 为什么优先用SDK自带的arm-hisiv300-linux-gcc

海思3531是Cortex-A9内核,SDK里配套的交叉编译器通常是arm-hisiv300-linux-gcc。这个前缀不是随便起的,海思在标准gcc之外补了自己的库和链接脚本,SDK的顶层Makefile和board目录里大量脚本默认按CROSS_COMPILE=arm-hisiv300-linux-拼命令。换用gcc-arm-none-eabi不是不行,但会多出一堆与U-Boot本身无关的坑:glibc版本不匹配、浮点ABI选错、链接脚本路径找不到。

export PATH=/opt/hisi-linux/x86-arm/arm-hisiv300-linux/bin:$PATH
export CROSS_COMPILE=arm-hisiv300-linux-
arm-hisiv300-linux-gcc -v

第一行把SDK工具链的bin目录放进PATH;第二行定义交叉编译前缀,U-Boot的Makefile用拼接$(CROSS_COMPILE)gcc的方式调用编译器;第三行打印版本信息,顺手验证前缀拼出来的命令真实存在。如果在64位主机上报"cannot execute binary file",通常是SDK里的工具链还是32位程序,装好lib32运行时库就能解决。环境变量只在当前终端生效,建议写进一个env.sh,避免每次开终端重新export。

2.2 启动介质与烧录地址: SPI NOR上U-Boot放在哪

3531支持SPI NOR、SPI NAND、eMMC、SD卡启动,开发期最常用SPI NOR。烧写前要做两个确认:板子的启动选择,以及U-Boot镜像在介质上的偏移。常见做法是SPI NOR偏移0x0放u-boot.bin,之后是环境变量区、内核区;海思的HiTool可以烧,命令行里用sf命令也很快。

启动介质 U-Boot常用偏移 开发期注意事项
SPI NOR 0x0 偏移直观,sf erase/write可整片操作
SPI NAND 0x0块起始 烧录依赖ECC配置,HiTool更稳
eMMC 1MB对齐偏移 需要先确认mmc设备号和分区
sf probe
sf erase 0x0 0x100000
sf write 0x40000800 0x0 0x100000

sf probe先探测SPI flash并打印jedec id,确认驱动识别的颗粒型号和板上一致;sf erase把起始1MB区域擦除,0x100000是长度,必须大于u-boot.bin实际大小;sf write把内存0x40000800处的镜像写入flash偏移0x0。这里的0x40000800是DDR临时加载地址,0x40000000是DDR起始地址,加8KB偏移是为了让出文件头预留区;具体加载地址取决于你下载镜像时用的tftp或mw命令,别和CONFIG_SYS_TEXT_BASE搞混。

2.3 最小构建: 从make hi3531_config到u-boot.bin

动手改DDR3参数之前,先让SDK自带配置完整编译一遍。这一步的意义是建立基线:之后任何改动导致编译失败或启动异常,至少知道错是从哪一次改动引入的。

make distclean
make hi3531_config
make -j8
ls -l u-boot.bin

make hi3531_config不是编译动作,它根据boards.cfg里登记的配置生成include/config.mk和include/config.h,并选定board目录下的源文件集合;make -j8并发编译,j数不要超过CPU物理线程,老版本U-Boot的Makefile并发能力有限,过高反而报错。产物里u-boot.bin是实际烧写文件,u-boot是无扩展名的elf,后面用gdb单步跟DDR初始化时加载它更方便。如果这一节通过,环境没有问题,问题空间就收窄到板级配置和DDR参数本身。

3. 改造board级文件: 配置头、时钟频率与串口早期打印

DDR3参数是整个移植的深水区,但它前面还排着配置头、时钟、串口三道闸。这三处不对,DDR参数写得再正确,你连一行输出都看不到。

3.1 先改hi3531.h: DDR容量、起始地址和bank数量

include/configs/hi3531.h是所有board级宏的入口,dram_init()、内存重定位、以及往内核传参的meminfo都从这里取值。移植时第一件事不是动DDRC寄存器,而是确认起始地址、容量、bank数量与实际贴的颗粒一致。

#define CONFIG_NR_DRAM_BANKS        1
#define CONFIG_SYS_SDRAM_BASE       0x40000000
#define PHYS_SDRAM_1_SIZE           0x20000000
#define CONFIG_SYS_SDRAM_SIZE       PHYS_SDRAM_1_SIZE

CONFIG_NR_DRAM_BANKS是DDR bank数,单通道多片并联也算一个bank,双通道双控制器才需要写成2;CONFIG_SYS_SDRAM_BASE是DDR在CPU地址空间的映射基址,不是SPI flash地址;PHYS_SDRAM_1_SIZE写容量,0x20000000对应512MB。换颗粒容量时,宏和硬件必须同时改,宏比实际大,U-Boot会把重定位和堆放到不存在的内存区域;宏比实际小,rank配置错才是隐患。

颗粒组合 容量 PHYS_SDRAM_x_SIZE
2片 128MB×16bit 256MB 0x10000000
2片 256MB×16bit 512MB 0x20000000
4片 256MB×16bit 1GB 0x40000000

3.2 board_early_init_f里解决时钟,再谈DDR频率

DDR3的800MHz不是晶振直接给的,而是由DDR PLL倍频再分频得到。海思平台的常见做法是把timer_init和各路PLL配置都收进board_early_init_f,因为start.S只完成了关中断和基本CPU设置,系统定时器、PLL这些都还没跑起来。

static int board_early_init_f(void)
{
    timer_init();                  /* 先让系统定时器工作 */
    pll_init(CONFIG_SYS_DDR_PLL);  /* 配置DDR PLL输出频率 */
    return 0;
}

timer_init先于其他外设初始化,原因很简单:后面串口波特率、延时函数都依赖一个能走的时基。pll_init按核心板晶振值设置PLL,CONFIG_SYS_DDR_PLL若定义为800,表示目标DDR时钟800MHz。这个宏是从demo板沿用过来的,换了核心板后最常出问题的就是它——晶振从24MHz换成27MHz,PLL分频系数没跟着改,实际DDR频率和预期差一大截,高频问题会伪装成DDR3时序错误,让你在错误方向上浪费半天。

3.3 串口初始化要够早: 盲调阶段唯一的眼睛

DDR参数写错的典型症状是没有任何输出,这时候串口越早可用,定位越快。常见做法是在init_sequence里把serial_init挪到board_early_init_f之后,让UART在任何DDR访问之前就能打印。

/* 临时调试代码,确认时钟和串口链路畅通 */
#ifdef EARLY_DEBUG
    serial_init();
    puts("[early] board_early_init_f done\n");
#endif

这段代码放在board_early_init_f末尾。能看到这行打印,说明CPU、PLL、UART、时钟源整条链路是通的,之后卡住的位置就锁定在DDR初始化和重定位环节。看不到这行,优先级依次是:串口管脚mux没配、波特率分频时钟源不对、board_early_init_f之前已经有未初始化内存访问。注意puts本身不能依赖DDR,它只写UART FIFO,所以插在这里是安全的。

4. DDR3时序与端接参数修改: 手册换算、寄存器位域与降频定位

这章是标题里"DDR3参数配置修改"的正主。3531的DDR3初始化代码通常分两段:CPU侧的DDRC控制器配置,和颗粒侧的PHY训练/物理层参数。移植中绝大多数返工,都出在第一段的时序参数和颗粒不匹配,或者第二段的ODT/驱动强度和PCB布局不匹配。

4.1 从DDR3数据手册拿到tRCD/tRP/tRAS/tRC

以常见的DDR3-1600颗粒为例,数据手册speed bin表格会直接给出一组CL-tRCD-tRP(如11-11-11),以及tRAS、tRC的ns数值。寄存器需要的是DDR时钟周期数,换算规则是向上取整:cycle = ceil(ns / tCK)。800MHz对应tCK=1.25ns,533MHz对应tCK=1.875ns。

参数 800MHz周期数 533MHz周期数 原始时间参考
CL 11 8 手册speed bin
tRCD 11 8 约13.125ns
tRP 11 8 约13.125ns
tRAS 30 20 约37.5ns
tRC 41 28 tRAS+tRP

换算上取整的意义在tRCD这类临界值:13.125ns除以1.25ns等于10.5,取整后是11,直接写10就是时序过紧。调试初期宁高勿低,每个参数加1周期性能损失可以忽略,稳定性提升却很明显。常见错误是把手册的ns数值原样填进寄存器——寄存器只认周期数,否则等于把tCK错当成1ns,时序全线偏紧。

4.2 把ns换成寄存器周期数: 位域写入与参数表

这些换算好的周期数最终落到DDRC时序寄存器。各版本SDK的寄存器布局不完全一样,但写套路一致,以常见的DDRC_TIMING0为例:

#define DDRC_TIMING0 (SYS_CTRL_BASE + 0x8100)
#define TRCD_SHIFT  0
#define TRP_SHIFT   8
#define TRAS_SHIFT  16
#define TRC_SHIFT   24

static void ddr_timing_write(u32 tRCD, u32 tRP, u32 tRAS, u32 tRC)
{
    u32 v = readl(DDRC_TIMING0);
    v &= ~(0x1f << TRCD_SHIFT);   /* 清tRCD位域 */
    v &= ~(0x1f << TRP_SHIFT);    /* 清tRP位域  */
    v &= ~(0x3f << TRAS_SHIFT);   /* 清tRAS位域 */
    v &= ~(0x3f << TRC_SHIFT);    /* 清tRC位域  */
    v |= (tRCD & 0x1f) << TRCD_SHIFT;
    v |= (tRP  & 0x1f) << TRP_SHIFT;
    v |= (tRAS & 0x3f) << TRAS_SHIFT;
    v |= (tRC  & 0x3f) << TRC_SHIFT;
    writel(v, DDRC_TIMING0);
}

readl先读回当前寄存器值,清位域再用或操作写入新值,最后writel一次写回。位域宽度不统一:tRCD/tRP一般5bit(够表示0~31周期),tRAS/tRC用6bit(0~63周期)。具体地址和偏移务必以手头SDK的reg_ddr.h头文件为准,不同SDK版本对调过字段位置。调用点通常长这样:

/* DDR3-1600 @800MHz */
ddr_timing_write(11, 11, 30, 41);

提示: 写寄存器前不清位域是DDR3参数配置修改里最常见的低级错误,残留的旧值会和新值按位或,产生一个既不是新配置也不是旧配置的随机结果。

到这里常规修改就结束了。真正让人头疼的是参数看着都对,内存测试还是挂,这类问题多半在端接和驱动强度,不在时序表。

4.3 ODT端接与驱动强度: 参考DDR3布线规则选初值

时序正确但高频随机报错,方向要转向信号完整性。颗粒侧的Rtt_Nom决定读操作端接电阻,常见RZQ/6约40Ω、RZQ/4约60Ω;Rtt_Wr用于写操作,常见RZQ/2约120Ω。控制器侧的驱动强度控制输出阻抗,34Ω/40Ω是常见档位。

这两组参数和PCB布局强相关,也就是DDR3布线规则里反复强调的: DQS与DQ等长、VREF独立走线、地址命令线与数据线分群走。PCB走线短、器件密度高的板子,驱动强度可以下降一档减少过冲;走线长的板子反而要强驱动,否则沿太缓。调试顺序建议固定下来:先确认源端匹配电阻和波形,再动ODT,最后调驱动强度,一次只改一个变量。

4.4 降频定位: 800MHz跑不起来,先落到533MHz对照

高频调不动时果断降频,这是把变量拆开的手段,不是放弃。降频后仍然死机,问题在PHY训练或端接基础配置;降频后稳定,问题锁定在时序紧张度或信号完整性。改降频需要同步改两处:DDR PLL分频,以及4.1的整张时序表。CL按speed bin降档,比如800MHz的CL=11,降到533MHz通常用7或8;tRCD/tRP/tRAS/tRC按新tCK重算。降频验证通过后逐档回升频率,每一档都完整跑内存测试,不要一次跳回标称值。

5. 用串口日志定位启动卡死,再用md/mw做DDR3回读验证

参数改完,下一步不是立刻烧内核,而是确认DDR这层是干净的。U-Boot命令行里最快的是内存回读,日志插桩则负责把"卡在哪"变成可观察的事实。

5.1 在init_sequence数组里插阶段打印点

把init_sequence数组每隔两三个函数插一行puts,标记按B0、B1、B2编号,重启后看停在哪一步:

static init_fnc_t init_sequence[] = {
    arch_cpu_init,       /* B0: CPU内部初始化完成 */
    board_early_init_f,  /* B1: 时钟与管脚完成 */
    serial_init,         /* B2: 串口可用 */
    dram_init,           /* B3: 读取容量宏,尚未触碰DDR */
    ...
};

停在B1之后没有B2,问题在串口链路;B2有了但B3没有,说明board_early_init_f里已经有人提前访问DDR;走到B3之后才死,反而是DDRC配置半套生效的典型表现——控制器部分配置成功,时序相关部分没生效。这种半初始化状态比全黑好排查:回到4.2检查位域宽度和清位操作。

5.2 mtest与pattern回读: 验证数据线和地址线

三个命令组合覆盖大部分DDR3验证场景:

mw.l 0x40000000 0xa5a55a5a 0x4000
md.l 0x40000000 0x10
mtest 0x40000000 0x42000000 0x1000

mw.l按32位字宽往0x40000000起连续写0x4000个字,0x40000000必须与CONFIG_SYS_SDRAM_BASE一致;md.l回读开头16个字,肉眼核对pattern是否原样返回,数据线错位会在这里立刻暴露;mtest在32MB范围跑地址线、数据线、反码综合测试,0x1000是迭代次数。固定bit翻转查焊接和数据线等长,随机错误查ODT和驱动强度。mtest通过后把迭代次数加大,配合多次上下电回归,再进入内核启动验证。

5.3 失败现象与调参方向对照表

现象 优先检查 调参方向
上电完全无输出 启动介质选择、串口mux 先过B0/B1阶段,别动DDR
打印几行后重启 电源纹波、时序过紧 放宽tRAS/tRC,其次CL
卡在dram_init之前 PLL分频、PHY配置 降频或核对时钟链
mtest固定bit翻转 焊接、数据线错位 查布线等长和虚焊
只在高频出现随机错 信号完整性 调ODT和驱动强度

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

海思 HiSpark 开发者社区是上海海思为开发者搭建的内容和交流平台,依托完备芯片解决方案、专业开发环境、开源/开放的软件资源与社区技术内容,构建完整服务生态开发者的闭环体系。

更多推荐