海思3531 U-Boot移植:DDR3参数配置修改实战指南
简介:海思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和驱动强度 |
更多推荐

所有评论(0)