好的,先看题目3的内容。 ---

3. UFS 设备初始化:上电复位、设备枚举、UFS 初始化序列

设备初始化,说白了就是让 UFS 芯片从死寂状态变得活蹦乱跳。整个过程分为三步:上电复位 → 设备枚举 → UFS 初始化序列。每一步出问题,后面的链路都起不来。我早年调第一个 UFS 项目时,总是在第二步卡住——设备枚举超时,后来发现是上电时序里 REF_CLK 启动太慢。嗯,咱们一步一步拆开看。

3.1 上电复位

这一步的目标:给 UFS 设备提供干净、稳定的初始状态。具体要做三件事:

  1. 上电:VCC(主电源)、VCCQ(I/O 电源)、VCCQ2(可选辅助电源)按照 UFS Spec 规定的时序依次上升。注意,两个电源之间通常有延迟要求(tVCC_ENA 等)。
  2. 提供 REF_CLK:参考时钟,频率通常为 19.2 MHz、26 MHz 或 38.4 MHz。时钟必须在复位释放之前稳定。
  3. 释放复位信号(RST_n):复位引脚从低拉高,设备开始内部初始化。

我习惯用示波器同时抓电源和复位波形。有一次我发现 VCC 刚落到 2.95V 以下就释放了 RST_n,结果设备根本无法完成内部校准。后来查 Spec,发现 VCC 必须保持在 3.0V 以上持续至少 1ms。

关键参数速查表
参数典型值说明
tVCC_ENA≥ 0.5 msVCC 有效到 VCCQ 使能之间的延时
tRST_HOLD≥ 100 μsRST_n 低电平保持时间,确保内部复位完成
tREF_CLK_STABLE≥ 1 ms时钟稳定后到复位释放之间的等待时间

3.2 设备枚举:链路启动

复位释放后,UFS 设备会通过低速模式(LS-Mode)在数据线上发送一个特殊的训练模式(TRAINING PATTERN),直接告诉你“我在这儿”。主机(Host)侧会监听并回应,完成链路初始化。具体流程如下:

  • Local Reset:主机驱动 M-PHY 进入复位状态,然后启动参考时钟。
  • Link Startup:主机向设备方向发送连续的低速码流,等待设备回应。
  • 设备回复 TRAINING PATTERN:设备收到主机码流后,开始发送自己的训练序列。
  • 速率协商:双方通过 DME(Device Management Entity)命令(例如 DME_GET、DME_SET)交换各自支持的速率和 Gear。
  • 进入 Active 状态:协商成功后,链路进入 Active 状态,数据可以开始传输。
避坑小经验
我曾经在 Link Startup 阶段踩过坑——主机侧发送的训练模式持续了 200μs 还没收到设备回包,导致超时。最后发现是设备的上电时序里 REF_CLK 不稳定。所以建议代码里加上重试机制,至少重试 3 次。

链路启动成功后,设备会进入DME 模式。这时候主机可以通过 UIC_CMD(UniPro Interface Command)配置 UniPro 链路属性,比如把数据线从 1-lane 扩展到 2-lane 或 4-lane。

3.3 UFS 初始化序列

链路起来了,但设备还没准备好。接下来主机需要通过Query Request(查询请求)向设备索取基本信息,并按照需求配置设备。我习惯把这一步叫做“设备信息拉取与配置”。

主要步骤:

  1. 发送 UPIU 查询请求:主机发送 QUERY REQUEST(Opcode: 0x01),请求读取 Device Descriptor(描述符),获取设备类型、版本号、支持的命令集等。
  2. 读取 Geometry Descriptor:获取物理存储参数,比如总容量、Erase Block Size、Max Number of Blocks 等。
  3. 读取 Unit Descriptor:获取两个默认逻辑单元(LU 0 和 LU 1)的参数,比如可写状态、逻辑块大小。
  4. 配置任务优先级:可选步骤,通过 SET_FLAG 或 CLEAR_FLAG 修改内部标志。
  5. 发送 NOP OUT UPIU:用来检测设备是否应答正常,确认链路侧已就绪。
  6. 进入 Ready 状态:一切就绪后,主机发送 START_STOP_UNIT 命令(或直接切换设备状态),设备进入 Ready 状态,可以接收 IO 请求了。
注意:
有些设备在 UFS 3.1 规范中,需要先通过查询请求确认它是否支持 Command Duration Limits(CDL)。如果不支持却下发 CDL 命令,设备会返回 CHECK CONDITION,导致初始化失败。我就在量产测试里遇到过这种问题——还好 log 里有恶心的 ILLEGAL REQUEST,让我快速定位到了。

为了让整个过程更直观,我画了一张流程图:

UFS 设备初始化流程图 UFS 设备初始化流程 上电复位 (VCC → REF_CLK → RST_n) 链路启动 (LS-Mode → Training) 设备枚举 (DME Get/Set → 速率协商) 发送 Query Request 读取描述符 配置设备 (Set Flag, NOP OUT) 设备 Ready,可接收 IO 说明:每个步骤都可能失败,建议在代码中加入重试和超时处理。 尤其是链路启动阶段,不同厂商的设备对时序要求有微小差异。 我曾遇到设备在 DME 协商时返回错误的 Gear,导致链路降速——所以别忘了做兼容性测试。

3.4 初始化序列的代码示例

下面是一个简化版的初始化序列伪代码,我用它来解构整个流程:

// 伪代码:UFS 设备初始化
int ufs_init(void) {
    // 1. 上电时序
    set_power(VCC, ENABLE);
    delay_ms(1);
    set_ref_clk(START);
    delay_ms(2);
    set_reset(RST_n, HIGH);
    delay_ms(100);  // 等待设备内部初始化

    // 2. 链路启动
    while (link_startup() != SUCCESS) {
        if (retry_count++ > 3) return ERROR_TIMEOUT;
        delay_ms(10);
    }

    // 3. 设备枚举:读取设备描述符
    uic_cmd(UIC_DME_GET, ATTRIBUTE_PA_ACTIVETXDATALANES);
    uic_cmd(UIC_DME_SET, ATTRIBUTE_PA_ACTIVETXDATALANES, 2);  // 设置 2-lane

    // 4. 发送 Query Request
    upiu_send(UPIU_QUERY_REQ, QUERY_OPCODE_READ_DESC, DESC_TYPE_DEVICE, 0);
    wait_response();  // 解析描述符

    // 5. 配置设备
    upiu_send(UPIU_QUERY_REQ, QUERY_OPCODE_SET_FLAG, FLAG_POWER_ON_WP_EN, 0);
    wait_response();

    // 6. 发送 NOP OUT 以验证链路
    upiu_send(UPIU_NOP_OUT, 0, 0, 0);
    wait_response();

    // 7. 准备就绪
    scsi_send(START_STOP_UNIT, 0x02);  // 启动设备
    return SUCCESS;
}
面试官常问
“为什么上电后要等 100 毫秒才发 Query Request?”
因为设备内部还有一个内部初始化完成标志(通过设备描述符的第 0 字节可以读),但我更建议读取设备描述符后再继续,不要硬等。如果设备支持 ASYNC EVENT,也可以用中断来通知。

好了,至此你已经了解了从上电到 Ready 的完整链路。实际项目中可能还会涉及端到端 CRC 校验、Boot LUN 切换等特殊场景,但基础流程就是上面这些。我把它们做成了一张知识结构表,方便你复习:

阶段核心动作常见失败原因
上电复位VCC/VCCQ 上电,REF_CLK 稳定,释放 RST_n电源上升斜率过慢,时钟不稳
链路启动LS-Mode 训练,DME 速率协商主机/设备速率不匹配,信号完整性差
设备枚举读取 Device/Geometry/Unit 描述符查询超时,描述符内容非法
配置阶段设置标志,发送 NOP OUT设备不支持某些 Flag,NOP OUT 无应答
进入 Ready下发 START STOP UNIT设备处于故障状态,电源不稳定

我在调试过程中曾遇到过最奇葩的一个问题:设备明明处于 Ready 状态,但一发送 READ 命令就返回 BUSY。后来发现是因为几何描述符里的 Max Number of Blocks 被我读错了——一个 32 位值的高 16 位和低 16 位我解析反了。嗯,这些小坑,你们自己绕一下就好。