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 之间的”握手协商”,只不过协商的是时序参数。

Bash
┌─────────────────────────────────────────────────────────────┐
│                   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 的基础:

Bash
┌─────────────────────────────────────────────────────────────┐
│               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 策略:

Bash
┌──────────────────────┐  ┌──────────────────────┐
│   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 对齐。

C
/*
 * 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;
}

Bash
┌─────────────────────────────────────────────────────────────┐
│                   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 数据读出时的同步信号,门限开早了或晚了都会采到垃圾数据。

C
/*
 * 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,让眼图最大化。

C
/*
 * 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 边沿。

C
/*
 * 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 标准)

C
/*
 * 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。

Bash
┌─────────────────────────────────────────────────────────────┐
│              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 常见失败现象

Bash
# 现象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 调试命令

Bash
# 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/count

6.3 眼图扫描脚本

Python
#!/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. 小结

Bash
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 就是找到这个窗口的边界并把采样点设在正中间。


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

最后修改: 2024年9月9日

作者

评论

发表评论

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