title: 【从零写OS内核】337 DRAM Training:从原理到调试
category: os
tag: [dram, ddr, embedded, signal-integrity]
source: https://github.com/golang12306/os-kernel-from-scratch
【从零写OS内核】337 DRAM Training:从原理到调试
1. 什么是 DRAM Training?
DRAM Training(内存训练)是 DDR 控制器在初始化时自动调整信号参数的过程,目的是让控制器准确地在数据眼图中心采样,避免读写出错。
类比 USB Type-C 线插入笔记本后,笔记本会先检测 CC 线的阻抗并协商功率档位——DDR Training 就是 CPU/SoC 和 DDR 之间的”握手协商”,只不过协商的是时序参数。
┌─────────────────────────────────────────────────────────────┐
│ DRAM Training 解决的核心问题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ DDR 信号传输示意: │
│ │
│ DQ[0] ──────────────────────► ───────────────── DQ[0] │
│ ──►Data Valid Window◄── │
│ DQS ──► ──────────────────────► ─────────────── DQS │
│ ▲ ▲ │
│ │ │ │
│ 上升沿 上升沿 │
│ │
│ 问题: │
│ 1. 数据到达时间不一致(fly-by vs direct topology) │
│ 2. DQS 和 DQ 之间的 skew 随温度漂移 │
│ 3. 信号反射造成眼图闭合 │
│ 4. DDR 芯片之间特性不一致(同一批次也可能不同) │
│ │
│ 解决:控制器动态调整每个 DQ 的 delay 找到最佳采样点 │
│ │
└─────────────────────────────────────────────────────────────┘2. DDR 信号完整性基础
2.1 关键时序参数
DDR 信号涉及多个时序参数,理解它们是理解 Training 的基础:
┌─────────────────────────────────────────────────────────────┐
│ DDR3 Read Timing (简化) │
├─────────────────────────────────────────────────────────────┤
│ │
│ Read burst 时序: │
│ │
│ DQS ─────┐ ┌───────────── ┌──── │
│ │ │ │ │
│ └──► │ │ │
│ ↑ ↑ │
│ 上升沿 上升沿 │
│ │ │ │
│ DQ ──► X0 ───►X1◄────────►X2◄──────►X3 │
│ │ │ │ │ │ │ │
│ ↑ ↑ ↑ ↑ ↑ ↑ │
│ tDQSQ tQH tDSC tDVC tDVQ │
│ │
│ 关键参数: │
│ tDQSQ — DQS 上升沿到 DQ 有效窗口结束的时间 │
│ tQHS — DQ hold skew between bits │
│ tDQSCK — DQS output clock skew │
│ │
│ 如果 DQS 和 DQ 的关系不对,数据窗口采样就会错位 │
│ Training 要找的,就是 DQS 上升沿在 DQ 眼图中的精确位置 │
└─────────────────────────────────────────────────────────────┘2.2 Fly-by vs Direct Topology
DDR3 布局拓扑影响 Training 策略:
┌──────────────────────┐ ┌──────────────────────┐
│ Direct Topology │ │ Fly-by Topology │
│ (DDR2 常用) │ │ (DDR3 推荐) │
├──────────────────────┤ ├──────────────────────┤
│ │ │ │
│ CLK ────┬──► DQ0 │ │ CLK ────► ───► DQ0 │
│ ├──► DQ1 │ │ │ │
│ ├──► DQ2 │ │ ▼ │
│ └──► DQ3 │ │ DQ1 │
│ │ │ │ │
│ 同一时钟到所有 DQ │ │ DQ2 │
│ 线长必须严格等长 │ │ │ │
│ │ │ DQ3 │
│ │ │ │
│ 优点: 低延迟 │ │ 优点: 信号干净 │
│ 缺点: 信号反射多 │ │ 缺点: 需 Training │
│ │ │ 补偿时序差异 │
└──────────────────────┘ └──────────────────────┘Fly-by 拓扑中,CLK 先到 DQ0 再到 DQ1……时序差异需要 Training 补偿。
3. Training 类型详解
3.1 Write Leveling(写均衡)
写均衡解决 DQS 和 CLK 之间的 skew。当 CPU 写数据到 DDR 时,DQS 边沿必须和 CLK 对齐。
/*
* Write Leveling — DDR3 标准初始化序列
* JEDEC JESD79-3C 定义的 MR3[7] = 1 触发写均衡模式
*/
int write_leveling(void)
{
struct ddr_ctrl *ddr = (struct ddr_ctrl *)DDR_CTRL_BASE;
/* Step 1: 设置 MR3[7]=1 进入写均衡模式 */
ddr->mr3 = 0x00000080; // WL Bit = 1
/* Step 2: 控制器发送 DQS,DDR 芯片返回 CLK 信息 */
/* DDR 芯片会比较 DQS 和 CLK,返回 "1" 或 "0" */
/* Step 3: 逐步调整 DQSm delay */
for (int dqs_delay = 0; dqs_delay < 0xFF; dqs_delay++) {
ddr->phy_dqs_con[0] = dqs_delay; // DQS0 delay
/* 读取 DDR 返回值(通过 MEMIF 的特殊寄存器) */
int dqs_feedback = ddr_read_wleveling_feedback();
if (dqs_feedback == 1) {
/* DQS 上升沿已对齐到 CLK 上升沿 */
printf("Write leveling PASS: DQS delay=0x%02Xn", dqs_delay);
return 0;
}
}
printf("Write leveling FAILEDn");
return -1;
}┌─────────────────────────────────────────────────────────────┐
│ Write Leveling 时序 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 控制器发出: │
│ CLK ──► ─────────► ─────────► │
│ DQS ───────► ───────────► ─ │
│ ↑ ↑ │
│ 初始 调整后 │
│ │
│ DDR 返回给控制器的 DQS feedback: │
│ "0" ← DQS 和 CLK 未对齐 │
│ "1" ← DQS 和 CLK 对齐(目标状态) │
│ │
│ 控制器逐步增大 DQS delay,直到收到 "1" │
└─────────────────────────────────────────────────────────────┘3.2 Read Gate Training(读门限训练)
读门限训练让控制器找到正确的 DQS gate 开启时机。DQS 是 DDR 数据读出时的同步信号,门限开早了或晚了都会采到垃圾数据。
/*
* Read Gate Training — 找 DQS gate 最佳开启位置
* 检测 Read Preamble 和数据窗口
*/
int read_gate_training(void)
{
struct ddr_ctrl *ddr = (struct ddr_ctrl *)DDR_CTRL_BASE;
/* 写入测试模式 */
write_data_pattern(DDR_TEST_ADDR, 0x55AA55AA);
/* 扫描 DQS gate 位置 */
for (int gate_delay = 0; gate_delay < 0x80; gate_delay++) {
ddr->phy_gate_con = gate_delay;
/* 读回验证 */
u32 val = read_data(DDR_TEST_ADDR);
if (val == 0x55AA55AA) {
/* 记录这个 gate 位置为候选 */
save_candidate_gate_delay(gate_delay);
}
}
/* 找所有候选位置的中间值作为最终 gate 位置 */
int final_gate = find_middle_point();
ddr->phy_gate_con = final_gate;
printf("Read gate training PASS: gate=0x%02Xn", final_gate);
return 0;
}3.3 Read DQS DQ Training(读数据眼图训练)
这是最精细的训练——逐 bit 调整每个 DQ 的 delay,让眼图最大化。
/*
* Read DQS Training — 每个 DQ 独立调整
* DDR3 有 16/32 位数据总线,每个 bit 都要单独调
*/
int read_dqs_training(void)
{
struct ddr_ctrl *ddr = (struct ddr_ctrl *)DDR_CTRL_BASE;
for (int dq_bit = 0; dq_bit < 16; dq_bit++) { // 假设 x16 颗粒
/* 写入互补测试数据 */
write_dq_bit_pattern(dq_bit, 0x55555555); // 交替位模式
int best_delay = 0;
int best_score = 0;
/* 扫描该 DQ bit 的 delay 范围 */
for (int dq_delay = 0; dq_delay < 0x80; dq_delay++) {
ddr->phy_dq_con[dq_bit] = dq_delay;
/* 读回并验证 */
int errors = verify_dq_bit(dq_bit, 0x55555555);
if (errors < best_score) {
best_score = errors;
best_delay = dq_delay;
}
}
/* 保存最佳 delay */
ddr->phy_dq_best_delay[dq_bit] = best_delay;
printf("DQ[%d] best delay: 0x%02X (errors: %d)n",
dq_bit, best_delay, best_score);
}
return 0;
}3.4 Write DQ Centering(写数据居中训练)
写均衡后,写方向也需要调整,让 DQ 中心对准 DQS 边沿。
/*
* Write DQ Centering — 调整写 DQ delay
* 与读方向类似,但目标是让写数据在 DQS 窗口中心
*/
int write_dq_centering(void)
{
struct ddr_ctrl *ddr = (struct ddr_ctrl *)DDR_CTRL_BASE;
/* 先做 Write Leveling 拿到 DQS delay */
int dqs_delay = get_write_leveling_result();
/* 对每个 DQ bit 找最佳 delay */
for (int dq_bit = 0; dq_bit < 16; dq_bit++) {
write_dq_bit_pattern(dq_bit, 0xAA55AA55);
int best_delay = 0;
/* 双向扫描找中心 */
int left_edge = scan_left_edge(dq_bit);
int right_edge = scan_right_edge(dq_bit);
best_delay = (left_edge + right_edge) / 2;
ddr->phy_wr_dq_delay[dq_bit] = best_delay;
}
return 0;
}4. Training 完整流程(JEDEC 标准)
/*
* DDR3 完整初始化 + Training 流程
* 基于 JEDEC JESD79-3C 标准
*/
int ddr_init_sequence(void)
{
printf("=== DDR3 Init Start ===n");
/* Step 1: CKE low, nRST low — 准备初始化 */
set_cke_low();
set_reset_low();
udelay(500);
/* Step 2: nRST high — 释放复位 */
set_reset_high();
udelay(100); // tDLLK 之前不能访问 DRAM
/* Step 3: MRS (Mode Register Set) 序列 */
/* MR2: 写均衡使能 */
ddr_mrs(2, 0x00000018); // CWL=5, RTT_WR=ODT120 (但我们让控制器做 WL)
udelay(1);
/* MR3: 写均衡/读 DQS Training 模式 */
ddr_mrs(3, 0x00000000);
udelay(1);
/* MR1: DLL 使能, ODT 配置 */
ddr_mrs(1, 0x00000342);
udelay(1);
/* MR0: DLL 使能, CL=7, CWL=5, WR=12 clocks */
ddr_mrs(0, 0x000018A32); // 0xA32 = 1010 0011 0010
udelay(1);
/* Step 4: ZQ Calibration */
ddr->ZQ_con = 0xC000107A;
udelay(1); // tZQinit=1us
/* Step 5: DLL Reset */
ddr_mrs(0, 0x000018B32); // DLL Reset bit set
udelay(1000); // tDLLK=1024 clocks
/* ========== Training 开始 ========== */
/* 5a. Write Leveling */
printf("Running Write Leveling...n");
int wl = write_leveling();
assert(wl == 0);
/* 5b. Read Gate Training */
printf("Running Read Gate Training...n");
int rg = read_gate_training();
assert(rg == 0);
/* 5c. Read DQS Training */
printf("Running Read DQS Training...n");
int rd = read_dqs_training();
assert(rd == 0);
/* 5d. Write DQ Centering */
printf("Running Write DQ Centering...n");
int wd = write_dq_centering();
assert(wd == 0);
/* Step 6: 刷新验证 */
printf("Running DRAM verify test...n");
for (int i = 0; i < 32; i++) {
u32 pat = 0x12345678 ^ (i << 24);
ddr_write(DDR_TEST_ADDR + i*4, pat);
}
for (int i = 0; i < 32; i++) {
u32 val = ddr_read(DDR_TEST_ADDR + i*4);
assert(val == (0x12345678 ^ (i << 24)));
}
printf("=== DDR3 Init Complete ===n");
return 0;
}5. 温度对 Training 的影响
DDR 有一个独特的特性:温度依赖的 ODT 行为。高温下 DDR 芯片内部阻抗下降,需要重新 Training。
┌─────────────────────────────────────────────────────────────┐
│ DDR3 温度特性与 Training │
├─────────────────────────────────────────────────────────────┤
│ │
│ DDR3 的 RTT (Termination Resistance) 随温度变化: │
│ │
│ 温度 ─────────────────────► │
│ │ │
│ │ ▲ │
│ │ │ │
│ 125°C ──────── RTT_NOM │
│ │ │ │
│ 85°C ────────────── RTT_WR (Write ODT) │
│ │ │
│ 25°C ───────────────────────────────────────── │
│ │ │
│ │
│ 解决方案: │
│ 1. 热敏电阻检测 DRAM温度 │
│ 2. 温度阈值触发重新 Training │
│ 3. Linux: /sys/class/thermal/thermal_zone*/temp │
│ │
│ 典型触发阈值: │
│ - 回到低温: 重新 Training │
│ - 进入高温(>85°C): 重新 Training │
│ - 差值超过 20°C: 重新 Training │
└─────────────────────────────────────────────────────────────┘6. 调试实践
6.1 常见失败现象
# 现象1: DDR 初始化通过,但随机内存错误
# 原因: Training 参数刚好在临界点
# 解决: 增加 DQ delay 裕量(margin)
# 现象2: 只在特定温度下出错
# 原因: 温度漂移导致 Training 参数偏移
# 解决: 实现温度补偿机制
# 现象3: 长时间运行后死机
# 原因: Memory refresh 间隔在高配下出问题
# 解决: 调整 refresh rate 或开启 self-refresh
# 现象4: DDR 读数据全为 0xFFFFFFFF
# 原因: DQS gate 未正确开启
# 解决: 检查 Read Gate Training 结果6.2 调试命令
# Linux 下查看 DDR 信息(若内核支持)
cat /sys/firmware/devicetree/base/ddr-controller@memory_map
# 使用 JTAG + Lauterbach 跟踪 Training 过程
regress.do ddr_training.cmm
# 手动触发 DRAM Training(部分 U-Boot 支持)
echo "ddr training" > /proc/driver/ddr/training
# 检查 ECC 错误(如果有 ECC)
cat /sys/class/misc/ecc_err/count6.3 眼图扫描脚本
#!/usr/bin/env python3
"""DDR Read Eye Scanner — 扫描 DRAM DQ 眼图"""
import struct
DDR_BASE = 0x40000000
TEST_SIZE = 0x1000
def scan_read_eye(dq_bit, delay_range=128):
"""扫描单个 DQ bit 的读数据眼图"""
results = []
for delay in range(delay_range):
# 设置 PHY DQ delay
set_dq_delay(dq_bit, delay)
# 多次读取引脚状态
eye_data = []
for _ in range(1000):
eye_data.append(read_dq_bit(dq_bit))
# 计算 "1" 的比例(眼图高度)
ones_ratio = sum(eye_data) / len(eye_data)
results.append((delay, ones_ratio))
return results
def plot_eye_diagram(results, dq_bit):
"""ASCII 打印眼图"""
print(f"n=== DQ[{dq_bit}] Read Eye Diagram ===")
print("Delay ->")
for y in [1.0, 0.75, 0.5, 0.25, 0.0]:
row = ""
for x, ratio in results:
if ratio >= y:
row += "█"
elif ratio >= y - 0.25:
row += "▓"
elif ratio >= y - 0.5:
row += "▒"
elif ratio >= y - 0.75:
row += "░"
else:
row += " "
print(f"{y:.2f} │{row}")
print(" +" + "-" * len(results) + "+")7. Linux vs Windows DRAM Training
| 维度 | Linux (ARM SoC) | Windows (x86) |
|---|---|---|
| Training 触发 | Bootloader (U-Boot/SPL) | BIOS/UEFI |
| 温度补偿 | 内核 thermal 驱动 | SMBIOS 温度报告 |
| 运行时调整 | 不支持(重启重新 Training) | 不支持 |
| 错误检测 | ECC + BadRAM | ECC + MCE |
| 调试接口 | /proc, /sys, debugfs | WMI, Event Viewer |
x86 UEFI BIOS 也做 Memory Training(Intel MRC 或 AMDAGESA),原理与 ARM DDR Training 完全相同,只是集成在 UEFI 固件中。
8. 小结
DRAM Training 完整流程:
DDR Reset ──► MRS 配置 ──► ZQ Cal ──► Write Leveling
│
Read Gate Training
│
Read DQS Training
│
Write DQ Centering
│
验证测试
核心思想:
每一条 DQ 信号都有自己的"到达时间"
Training 就是让控制器逐 bit 学习这个时间
然后在正确的时间点采样
温度会影响结果:
重新 Training ← 温度变化超过阈值
Training 做不好:
内存读错 ──► 系统随机崩溃 ──► 难以复现的 bug理解 DRAM Training,关键是理解数据眼图概念——DQ 数据必须在 DQS 的采样窗口内有效,而 Training 就是找到这个窗口的边界并把采样点设在正中间。
微信公众号
· 深入浅出技术解读 ☕️
· 欢迎搜索关注「码农的螺丝刀」
评论